Fix pasting properties inline in an object instead of at the bottom

This commit is contained in:
Jos de Jong 2020-07-08 12:49:12 +02:00
parent e44284df90
commit 2f393e5948
5 changed files with 140 additions and 48 deletions

View File

@ -19,9 +19,7 @@
import { createHistory } from './history.js' import { createHistory } from './history.js'
import Node from './JSONNode.svelte' import Node from './JSONNode.svelte'
import { expandSelection } from './selection.js' import { expandSelection } from './selection.js'
import { singleton } from './singleton.js'
import { import {
deleteIn,
existsIn, existsIn,
getIn, getIn,
setIn, setIn,
@ -35,6 +33,7 @@
import jump from './assets/jump.js/src/jump.js' import jump from './assets/jump.js/src/jump.js'
import { syncState } from './utils/syncState.js' import { syncState } from './utils/syncState.js'
import { isObject } from './utils/typeUtils.js' import { isObject } from './utils/typeUtils.js'
import { patchProps } from './utils/updateProps.js'
let divContents let divContents
@ -99,25 +98,7 @@
// if a property is renamed (move operation), rename it in the object's props // if a property is renamed (move operation), rename it in the object's props
// so it maintains its identity and hence its index // so it maintains its identity and hence its index
operations state = patchProps(state, operations)
.filter(operation => {
return operation.op === 'move' && isEqual(
initial(parseJSONPointer(operation.from)),
initial(parseJSONPointer(operation.path))
)
})
.forEach(operation => {
const pathFrom = parseJSONPointer(operation.from)
const to = parseJSONPointer(operation.path)
const parentPath = initial(pathFrom)
const oldKey = last(pathFrom)
const newKey = last(to)
const props = getIn(state, parentPath.concat(STATE_PROPS))
const index = props.findIndex(item => item.key === oldKey)
if (index !== -1) {
state = setIn(state, parentPath.concat([STATE_PROPS, index, 'key']), newKey)
}
})
history.add({ history.add({
undo: documentPatchResult.revert, undo: documentPatchResult.revert,
@ -303,6 +284,10 @@
emitOnChange() emitOnChange()
} }
function handleUpdateKey (oldKey, newKey) {
// should never be called on the root
}
function handleToggleSearch() { function handleToggleSearch() {
showSearch = !showSearch showSearch = !showSearch
} }
@ -526,6 +511,7 @@
state={state} state={state}
searchResult={searchResultWithActive} searchResult={searchResultWithActive}
onPatch={handlePatch} onPatch={handlePatch}
onUpdateKey={handleUpdateKey}
onExpand={handleExpand} onExpand={handleExpand}
onLimit={handleLimit} onLimit={handleLimit}
onSelect={handleSelect} onSelect={handleSelect}

View File

@ -1,5 +1,6 @@
<script> <script>
import { debounce, initial, isEqual, last } from 'lodash-es' import { debounce, isEqual } from 'lodash-es'
import { rename } from './actions.js'
import { import {
DEBOUNCE_DELAY, DEBOUNCE_DELAY,
DEFAULT_LIMIT, DEFAULT_LIMIT,
@ -24,6 +25,7 @@
export let state export let state
export let searchResult export let searchResult
export let onPatch export let onPatch
export let onUpdateKey
export let onExpand export let onExpand
export let onLimit export let onLimit
export let onSelect export let onSelect
@ -98,16 +100,21 @@
function updateKey () { function updateKey () {
const newKey = getPlainText(domKey) const newKey = getPlainText(domKey)
const parentPath = initial(path)
onPatch([{ // must be handled by the parent which has knowledge about the other keys
op: 'move', onUpdateKey(key, newKey)
from: compileJSONPointer(parentPath.concat(key)),
path: compileJSONPointer(parentPath.concat(newKey))
}])
} }
const updateKeyDebounced = debounce(updateKey, DEBOUNCE_DELAY) const updateKeyDebounced = debounce(updateKey, DEBOUNCE_DELAY)
function handleUpdateKey (oldKey, newKey) {
const index = props.findIndex(prop => prop.key === oldKey)
const nextKeys = (index !== -1)
? props.slice(index + 1).map(prop => prop.key)
: []
onPatch(rename(path, oldKey, newKey, nextKeys))
}
function handleKeyInput (event) { function handleKeyInput (event) {
const newKey = getPlainText(event.target) const newKey = getPlainText(event.target)
keyClass = getKeyClass(newKey, searchResult) keyClass = getKeyClass(newKey, searchResult)
@ -342,6 +349,7 @@
state={state && state[index]} state={state && state[index]}
searchResult={searchResult ? searchResult[index] : undefined} searchResult={searchResult ? searchResult[index] : undefined}
onPatch={onPatch} onPatch={onPatch}
onUpdateKey={handleUpdateKey}
onExpand={onExpand} onExpand={onExpand}
onLimit={onLimit} onLimit={onLimit}
onSelect={onSelect} onSelect={onSelect}
@ -409,6 +417,7 @@
state={state && state[prop.key]} state={state && state[prop.key]}
searchResult={searchResult ? searchResult[prop.key] : undefined} searchResult={searchResult ? searchResult[prop.key] : undefined}
onPatch={onPatch} onPatch={onPatch}
onUpdateKey={handleUpdateKey}
onExpand={onExpand} onExpand={onExpand}
onLimit={onLimit} onLimit={onLimit}
onSelect={onSelect} onSelect={onSelect}

View File

@ -97,6 +97,38 @@ export function append (json, path, values) { // TODO: find a better name and d
} }
} }
/**
* Rename an object key
*
* @param {Path} parentPath
* @param {string} oldKey
* @param {string} newKey
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
* used to maintain the position of the renamed key.
* If not provided, the renamed key will be moved
* to the bottom of the list with keys.
* @returns {Array}
*/
export function rename(parentPath, oldKey, newKey, nextKeys) {
return [
// rename a key
{
op: 'move',
from: compileJSONPointer(parentPath.concat(oldKey)),
path: compileJSONPointer(parentPath.concat(newKey))
},
// move all lower down keys so the renamed key will maintain it's position
...nextKeys.map(key => {
return {
op: 'move',
from: compileJSONPointer(parentPath.concat(key)),
path: compileJSONPointer(parentPath.concat(key))
}
})
]
}
/** /**
* Create a JSONPatch for an insert action. * Create a JSONPatch for an insert action.
* *

View File

@ -1,26 +1,90 @@
import initial from 'lodash-es/initial.js'
import { STATE_PROPS } from '../constants.js'
import { deleteIn, getIn, insertAt, setIn } from './immutabilityHelpers.js'
import { parseJSONPointer } from './jsonPointer.js'
import { isObject } from './typeUtils.js' import { isObject } from './typeUtils.js'
import { uniqueId } from 'lodash-es' import { isEqual, last, uniqueId } from 'lodash-es'
export function updateProps (value, prevProps) { export function updateProps (value, prevProps) {
if (isObject(value)) { if (!isObject(value)) {
// copy the props that still exist
const props = prevProps
? prevProps.filter(item => value[item.key] !== undefined)
: []
// add new props
const prevKeys = new Set(props.map(item => item.key))
Object.keys(value).forEach(key => {
if (!prevKeys.has(key)) {
props.push({
id: uniqueId(),
key
})
}
})
return props
} else {
return undefined return undefined
} }
// copy the props that still exist
const props = prevProps
? prevProps.filter(item => value[item.key] !== undefined)
: []
// add new props
const prevKeys = new Set(props.map(item => item.key))
Object.keys(value).forEach(key => {
if (!prevKeys.has(key)) {
props.push({
id: uniqueId(),
key
})
}
})
return props
}
// TODO: write unit tests
// TODO: split this function in smaller functions
export function patchProps (state, operations) {
let updatedState = state
operations.map(operation => {
if (operation.op === 'move') {
if (isEqual(
initial(parseJSONPointer(operation.from)),
initial(parseJSONPointer(operation.path))
)) {
// move inside the same object
const pathFrom = parseJSONPointer(operation.from)
const pathTo = parseJSONPointer(operation.path)
const parentPath = initial(pathFrom)
const props = getIn(updatedState, parentPath.concat(STATE_PROPS))
if (props) {
const oldKey = last(pathFrom)
const newKey = last(pathTo)
const index = props.findIndex(item => item.key === oldKey)
if (index !== -1) {
if (oldKey !== newKey) {
// A property is renamed. Rename it in the object's props
// so it maintains its identity and hence its index
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS, index, 'key']), newKey)
} else {
// operation.from and operation.path are the same:
// property is moved but stays the same -> move it to the end of the props
const oldProp = props[index]
const updatedProps = insertAt(deleteIn(props, [index]), [props.length - 1], oldProp)
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps)
}
}
}
}
}
if (operation.op === 'add') {
const path = parseJSONPointer(operation.path)
const parentPath = initial(path)
const props = getIn(updatedState, parentPath.concat(STATE_PROPS))
if (props) {
const key = last(path)
const newProp = {
key,
id: uniqueId()
}
const updatedProps = insertAt(props, [props.length], newProp)
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps)
}
}
})
return updatedState
} }

View File

@ -9,6 +9,7 @@ describe('updateProps', () => {
const props2 = updateProps({a: 1, b: 2}, props1) const props2 = updateProps({a: 1, b: 2}, props1)
assert.deepStrictEqual(props2.map(item => item.key), ['b', 'a']) assert.deepStrictEqual(props2.map(item => item.key), ['b', 'a'])
assert.deepStrictEqual(props2[0], props1[0]) // b must still have the same id
}) })
it('updateProps (2)', () => { it('updateProps (2)', () => {