Fix `insertBefore` and `replace` relying on object key order

This commit is contained in:
Jos de Jong 2020-07-08 14:41:38 +02:00
parent 16d3092670
commit 0e5dabed89
4 changed files with 95 additions and 89 deletions

View File

@ -33,7 +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' import { getNextKeys, patchProps } from './utils/updateProps.js'
let divContents let divContents
@ -89,16 +89,14 @@
export function patch(operations) { export function patch(operations) {
const prevState = state const prevState = state
console.log('operations', operations)
const documentPatchResult = immutableJSONPatch(doc, operations) const documentPatchResult = immutableJSONPatch(doc, operations)
const statePatchResult = immutableJSONPatch(state, operations) const statePatchResult = immutableJSONPatch(state, operations)
// TODO: only apply operations to state for relevant operations: move, copy, delete? Figure out // TODO: only apply operations to state for relevant operations: move, copy, delete? Figure out
doc = documentPatchResult.json doc = documentPatchResult.json
state = statePatchResult.json state = patchProps(statePatchResult.json, operations)
// if a property is renamed (move operation), rename it in the object's props
// so it maintains its identity and hence its index
state = patchProps(state, operations)
history.add({ history.add({
undo: documentPatchResult.revert, undo: documentPatchResult.revert,
@ -151,7 +149,11 @@
console.log('paste', { clipboard, selection }) console.log('paste', { clipboard, selection })
if (selection.beforePath) { if (selection.beforePath) {
const operations = insertBefore(doc, selection.beforePath, clipboard) const parentPath = initial(selection.beforePath)
const beforeKey = last(selection.beforePath)
const props = getIn(state, parentPath.concat(STATE_PROPS))
const nextKeys = getNextKeys(props, parentPath, beforeKey, true)
const operations = insertBefore(doc, selection.beforePath, clipboard, nextKeys)
console.log('patch', operations) console.log('patch', operations)
handlePatch(operations) handlePatch(operations)
@ -163,7 +165,12 @@
// FIXME: must adjust STATE_PROPS of the object where we inserted the clipboard // FIXME: must adjust STATE_PROPS of the object where we inserted the clipboard
} else if (selection.paths) { } else if (selection.paths) {
const operations = replace(doc, selection.paths, clipboard) const lastPath = last(selection.paths) // FIXME: here we assume selection.paths is sorted correctly
const parentPath = initial(lastPath)
const beforeKey = last(lastPath)
const props = getIn(state, parentPath.concat(STATE_PROPS))
const nextKeys = getNextKeys(props, parentPath, beforeKey, true)
const operations = replace(doc, selection.paths, clipboard, nextKeys)
console.log('patch', operations) console.log('patch', operations)
handlePatch(operations) handlePatch(operations)

View File

@ -19,6 +19,7 @@
import { findUniqueName } from './utils/stringUtils.js' import { findUniqueName } from './utils/stringUtils.js'
import { isUrl, stringConvert, valueType } from './utils/typeUtils' import { isUrl, stringConvert, valueType } from './utils/typeUtils'
import { compileJSONPointer } from './utils/jsonPointer' import { compileJSONPointer } from './utils/jsonPointer'
import { getNextKeys } from './utils/updateProps.js'
export let key = undefined // only applicable for object properties export let key = undefined // only applicable for object properties
export let value export let value
@ -109,11 +110,7 @@
function handleUpdateKey (oldKey, newKey) { function handleUpdateKey (oldKey, newKey) {
const newKeyUnique = findUniqueName(newKey, value) const newKeyUnique = findUniqueName(newKey, value)
const nextKeys = getNextKeys(props, path, key, false)
const index = props.findIndex(prop => prop.key === oldKey)
const nextKeys = (index !== -1)
? props.slice(index + 1).map(prop => prop.key)
: []
onPatch(rename(path, oldKey, newKeyUnique, nextKeys)) onPatch(rename(path, oldKey, newKeyUnique, nextKeys))
} }

View File

@ -15,9 +15,12 @@ import { findUniqueName } from './utils/stringUtils'
* @param {JSON} json * @param {JSON} json
* @param {Path} path * @param {Path} path
* @param {Array.<{key?: string, value: JSON}>} values * @param {Array.<{key?: string, value: JSON}>} values
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
* these keys will be moved down, so the renamed
* key will maintain it's position above these keys
* @return {Array} * @return {Array}
*/ */
export function insertBefore (json, path, values) { // TODO: find a better name and define datastructure for values export function insertBefore (json, path, values, nextKeys) { // TODO: find a better name and define datastructure for values
const parentPath = initial(path) const parentPath = initial(path)
const parent = getIn(json, parentPath) const parent = getIn(json, parentPath)
@ -30,34 +33,20 @@ export function insertBefore (json, path, values) { // TODO: find a better name
})) }))
} }
else { // 'object' else { // 'object'
const addActions = values.map(entry => { return [
const newProp = findUniqueName(entry.key, parent) // insert new values
return { ...values.map(entry => {
op: 'add', const newProp = findUniqueName(entry.key, parent)
path: compileJSONPointer(parentPath.concat(newProp)), return {
value: entry.value op: 'add',
} path: compileJSONPointer(parentPath.concat(newProp)),
}) value: entry.value
}
}),
// we move all lower down properties to the same parent again, // move all lower down keys so the inserted key will maintain it's position
// to force them to move under the inserted properties instead of the ...nextKeys.map(key => moveDown(parentPath, key))
// new properties appearing at the bottom of the object ]
const keys = Object.keys(parent)
const beforeKey = last(path)
const beforeIndex = keys.indexOf(beforeKey)
const keysToMove = (beforeIndex !== -1)
? keys.slice(beforeIndex)
: []
const moveActions = keysToMove.map(key => {
const movePath = compileJSONPointer(parentPath.concat(key))
return {
op: 'move',
from: movePath,
path: movePath
}
})
return addActions.concat(moveActions)
} }
} }
@ -105,9 +94,8 @@ export function append (json, path, values) { // TODO: find a better name and d
* @param {string} oldKey * @param {string} oldKey
* @param {string} newKey * @param {string} newKey
* @param {string[]} nextKeys A list with all keys *after* the renamed key, * @param {string[]} nextKeys A list with all keys *after* the renamed key,
* used to maintain the position of the renamed key. * these keys will be moved down, so the renamed
* If not provided, the renamed key will be moved * key will maintain it's position above these keys
* to the bottom of the list with keys.
* @returns {Array} * @returns {Array}
*/ */
export function rename(parentPath, oldKey, newKey, nextKeys) { export function rename(parentPath, oldKey, newKey, nextKeys) {
@ -120,13 +108,7 @@ export function rename(parentPath, oldKey, newKey, nextKeys) {
}, },
// move all lower down keys so the renamed key will maintain it's position // move all lower down keys so the renamed key will maintain it's position
...nextKeys.map(key => { ...nextKeys.map(key => moveDown(parentPath, key))
return {
op: 'move',
from: compileJSONPointer(parentPath.concat(key)),
path: compileJSONPointer(parentPath.concat(key))
}
})
] ]
} }
@ -140,9 +122,12 @@ export function rename(parentPath, oldKey, newKey, nextKeys) {
* @param {JSON} json * @param {JSON} json
* @param {Path[]} paths * @param {Path[]} paths
* @param {Array.<{key?: string, value: JSON}>} values * @param {Array.<{key?: string, value: JSON}>} values
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
* these keys will be moved down, so the renamed
* key will maintain it's position above these keys
* @return {Array} * @return {Array}
*/ */
export function replace (json, paths, values) { // TODO: find a better name and define datastructure for values export function replace (json, paths, values, nextKeys) { // TODO: find a better name and define datastructure for values
const firstPath = first(paths) const firstPath = first(paths)
const parentPath = initial(firstPath) const parentPath = initial(firstPath)
const parent = getIn(json, parentPath) const parent = getIn(json, parentPath)
@ -161,35 +146,24 @@ export function replace (json, paths, values) { // TODO: find a better name and
return removeActions.concat(insertActions) return removeActions.concat(insertActions)
} }
else { // parent is Object else { // parent is Object
const removeActions = removeAll(paths) return [
const insertActions = values.map(entry => { // remove operations
const newProp = findUniqueName(entry.key, parent) ...removeAll(paths),
return {
op: 'add',
path: compileJSONPointer(parentPath.concat(newProp)),
value: entry.value
}
})
// we move all lower down properties to the same parent again, // insert operations
// to force them to move under the inserted properties instead of the ...values.map(entry => {
// new properties appearing at the bottom of the object const newProp = findUniqueName(entry.key, parent)
const keys = Object.keys(parent) return {
const beforeKey = last(firstPath) op: 'add',
const beforeIndex = keys.indexOf(beforeKey) path: compileJSONPointer(parentPath.concat(newProp)),
const keysToMove = (beforeIndex !== -1) value: entry.value
? keys.slice(beforeIndex) }
: [] }),
const moveActions = keysToMove.map(key => {
const movePath = compileJSONPointer(parentPath.concat(key))
return {
op: 'move',
from: movePath,
path: movePath
}
})
return removeActions.concat(insertActions, moveActions) // move down operations
// move all lower down keys so the renamed key will maintain it's position
...nextKeys.map(key => moveDown(parentPath, key))
]
} }
} }
@ -218,3 +192,13 @@ export function removeAll (paths) {
})) }))
.reverse() // reverse is needed for arrays: delete the last index first .reverse() // reverse is needed for arrays: delete the last index first
} }
// helper function to move a key down in an object,
// so another key can get positioned before the moved down keys
function moveDown(parentPath, key) {
return {
op: 'move',
from: compileJSONPointer(parentPath.concat(key)),
path: compileJSONPointer(parentPath.concat(key))
}
}

View File

@ -49,18 +49,26 @@ export function patchProps (state, operations) {
if (props) { if (props) {
const oldKey = last(pathFrom) const oldKey = last(pathFrom)
const newKey = last(pathTo) const newKey = last(pathTo)
const index = props.findIndex(item => item.key === oldKey) const oldIndex = props.findIndex(item => item.key === oldKey)
if (index !== -1) { if (oldIndex !== -1) {
if (oldKey !== newKey) { if (oldKey !== newKey) {
// A property is renamed. Rename it in the object's props // A property is renamed.
// so it maintains its identity and hence its index
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS, index, 'key']), newKey) // in case the new key shadows an existing key, remove the existing key
const newIndex = props.findIndex(item => item.key === newKey)
if (newIndex !== -1) {
const updatedProps = deleteIn(props, [newIndex])
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps)
}
// Rename the key in the object's props so it maintains its identity and hence its index
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS, oldIndex, 'key']), newKey)
} else { } else {
// operation.from and operation.path are the same: // operation.from and operation.path are the same:
// property is moved but stays the same -> move it to the end of the props // property is moved but stays the same -> move it to the end of the props
const oldProp = props[index] const oldProp = props[oldIndex]
const updatedProps = insertAt(deleteIn(props, [index]), [props.length - 1], oldProp) const updatedProps = insertAt(deleteIn(props, [oldIndex]), [props.length - 1], oldProp)
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps) updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps)
} }
@ -74,10 +82,9 @@ export function patchProps (state, operations) {
const parentPath = initial(path) const parentPath = initial(path)
const props = getIn(updatedState, parentPath.concat(STATE_PROPS)) const props = getIn(updatedState, parentPath.concat(STATE_PROPS))
if (props) { if (props) {
const key = last(path)
const newProp = { const newProp = {
key, id: uniqueId(),
id: uniqueId() key: last(path)
} }
const updatedProps = insertAt(props, [props.length], newProp) const updatedProps = insertAt(props, [props.length], newProp)
@ -88,3 +95,14 @@ export function patchProps (state, operations) {
return updatedState return updatedState
} }
export function getNextKeys(props, parentPath, key, includeKey = false) {
if (props) {
const index = props.findIndex(prop => prop.key === key)
if (index !== -1) {
return props.slice(index + (includeKey ? 0 : 1)).map(prop => prop.key)
}
}
return []
}