diff --git a/src/components/JSONNode.js b/src/components/JSONNode.js index 00f9177..8dc2cd1 100644 --- a/src/components/JSONNode.js +++ b/src/components/JSONNode.js @@ -53,10 +53,10 @@ export default class JSONNode extends Component { if (data.expanded) { if (data.props.length > 0) { const props = data.props.map(prop => { - return h('li', {key : prop.name}, + return h('li', {key: prop.id}, h(this.constructor, { parent: this, - prop: prop, + prop, data: prop.value, options, events @@ -78,6 +78,7 @@ export default class JSONNode extends Component { return h('div', {}, [node, childs]) } + // TODO: extract a function renderChilds shared by both renderJSONObject and renderJSONArray (rename .props and .items to .childs?) renderJSONArray ({prop, index, data, options, events}) { const childCount = data.items.length const node = h('div', {name: compileJSONPointer(this.getPath()), key: 'node', className: 'jsoneditor-node jsoneditor-array'}, [ @@ -91,12 +92,12 @@ export default class JSONNode extends Component { let childs if (data.expanded) { if (data.items.length > 0) { - const items = data.items.map((child, index) => { - return h('li', {key : index}, + const items = data.items.map((item, index) => { + return h('li', {key : item.id}, h(this.constructor, { parent: this, index, - data: child, + data: item.value, options, events }) @@ -287,8 +288,6 @@ export default class JSONNode extends Component { target = target.parentNode } - console.log('value', this.props) - target.className = JSONNode.getValueClass(type, itsAnUrl, isEmpty) + JSONNode.getSearchResultClass(this.props.data.searchResult) target.title = itsAnUrl ? JSONNode.URL_TITLE : '' diff --git a/src/components/TreeMode.js b/src/components/TreeMode.js index 58f5425..435513a 100644 --- a/src/components/TreeMode.js +++ b/src/components/TreeMode.js @@ -118,16 +118,11 @@ export default class TreeMode extends Component { } // enrich the data with search results + // TODO: performance improvements in search would be nice though it's acceptable right now const searchResults = this.state.search.text ? search(data, this.state.search.text) : null if (searchResults) { data = addSearchResults(data, searchResults, this.state.search.active) } - // TODO: moveTo active search result (not focus!) - // if (this.state.search.active) { - // data = addFocus(data, this.state.search.active) - // } - - // console.log('data', data) return h('div', { className: `jsoneditor jsoneditor-mode-${props.mode}`, diff --git a/src/jsonData.js b/src/jsonData.js index a300cd6..9331748 100644 --- a/src/jsonData.js +++ b/src/jsonData.js @@ -9,7 +9,10 @@ import { setIn, updateIn, getIn, deleteIn, insertAt } from './utils/immutability import { isObject } from './utils/typeUtils' import isEqual from 'lodash/isEqual' -import type {JSONData, DataPointer, Path} from './types' +import type { + JSONData, ObjectData, ItemData, DataPointer, Path, + JSONPatch, JSONPatchAction, PatchOptions, JSONPatchResult +} from './types' /** * Expand function which will expand all nodes @@ -33,15 +36,21 @@ export function jsonToData (json, expand = expandAll, path = [], type = 'value') return { type: 'Array', expanded: expand(path), - items: json.map((child, index) => jsonToData(child, expand, path.concat(index))) + items: json.map((child, index) => { + return { + id: getId(), // TODO: use id based on index (only has to be unique within this array) + value: jsonToData(child, expand, path.concat(index)) + } + }) } } else if (isObject(json)) { return { type: 'Object', expanded: expand(path), - props: Object.keys(json).map(name => { + props: Object.keys(json).map((name, index) => { return { + id: getId(), // TODO: use id based on index (only has to be unique within this array) name, value: jsonToData(json[name], expand, path.concat(name)) } @@ -61,10 +70,10 @@ export function jsonToData (json, expand = expandAll, path = [], type = 'value') * @param {JSONData} data * @return {Object | Array | string | number | boolean | null} json */ -export function dataToJson (data) { +export function dataToJson (data: JSONData) { switch (data.type) { case 'Array': - return data.items.map(dataToJson) + return data.items.map(item => dataToJson(item.value)) case 'Object': const object = {} @@ -87,7 +96,7 @@ export function dataToJson (data) { * @return {Path} dataPath * @private */ -export function toDataPath (data, path) { +export function toDataPath (data: JSONData, path: Path) : Path { if (path.length === 0) { return [] } @@ -95,14 +104,14 @@ export function toDataPath (data, path) { if (data.type === 'Array') { // index of an array const index = path[0] - const item = data.items[index] + const item = data.items[parseInt(index)] if (!item) { throw new Error('Array item "' + index + '" not found') } - return ['items', index].concat(toDataPath(item, path.slice(1))) + return ['items', index, 'value'].concat(toDataPath(item.value, path.slice(1))) } - else { + else if (data.type === 'Object') { // object property. find the index of this property const index = findPropertyIndex(data, path[0]) const prop = data.props[index] @@ -111,19 +120,11 @@ export function toDataPath (data, path) { } return ['props', index, 'value'] - .concat(toDataPath(prop && prop.value, path.slice(1))) + .concat(toDataPath(prop.value, path.slice(1))) + } + else { + return [] } -} - -/** - * Convert a path of a JSON object into a path in the corresponding data model - * @param {JSONData} data - * @param {Path} path - * @return {Path} dataPath - * @private - */ -export function toPath (data, dataPath) { - } /** @@ -134,7 +135,7 @@ export function toPath (data, dataPath) { * what nodes must be expanded * @return {{data: JSONData, revert: Object[], error: Error | null}} */ -export function patchData (data, patch, expand = expandAll) { +export function patchData (data: JSONData, patch: JSONPatchAction[], expand = expandAll) { let updatedData = data let revert = [] @@ -142,6 +143,8 @@ export function patchData (data, patch, expand = expandAll) { const action = patch[i] const options = action.jsoneditor + // TODO: check whether action.op and action.path exist + switch (action.op) { case 'add': { const path = parseJSONPointer(action.path) @@ -172,6 +175,14 @@ export function patchData (data, patch, expand = expandAll) { } case 'copy': { + if (!action.from) { + return { + data, + revert: [], + error: new Error('Property "from" expected in copy action ' + JSON.stringify(action)) + } + } + const result = copy(updatedData, action.path, action.from, options) updatedData = result.data revert = result.revert.concat(revert) @@ -180,6 +191,14 @@ export function patchData (data, patch, expand = expandAll) { } case 'move': { + if (!action.from) { + return { + data, + revert: [], + error: new Error('Property "from" expected in move action ' + JSON.stringify(action)) + } + } + const result = move(updatedData, action.path, action.from, options) updatedData = result.data revert = result.revert.concat(revert) @@ -224,7 +243,7 @@ export function patchData (data, patch, expand = expandAll) { * @param {JSONData} value * @return {{data: JSONData, revert: JSONPatch}} */ -export function replace (data, path, value) { +export function replace (data: JSONData, path: Path, value: JSONData) { const dataPath = toDataPath(data, path) const oldValue = getIn(data, dataPath) @@ -247,7 +266,7 @@ export function replace (data, path, value) { * @param {string} path * @return {{data: JSONData, revert: JSONPatch}} */ -export function remove (data, path) { +export function remove (data: JSONData, path: string) { // console.log('remove', path) const pathArray = parseJSONPointer(path) @@ -259,6 +278,9 @@ export function remove (data, path) { if (parent.type === 'Array') { const dataPath = toDataPath(data, pathArray) + // remove the 'value' property, we want to remove the whole item from the items array + dataPath.pop() + return { data: deleteIn(data, dataPath), revert: [{ @@ -275,7 +297,9 @@ export function remove (data, path) { const dataPath = toDataPath(data, pathArray) const prop = pathArray[pathArray.length - 1] - dataPath.pop() // remove the 'value' property, we want to remove the whole object property + // remove the 'value' property, we want to remove the whole object property from props + dataPath.pop() + return { data: deleteIn(data, dataPath), revert: [{ @@ -298,12 +322,12 @@ export function remove (data, path) { * @param {JSONPatch} patch * @return {Array} */ -export function simplifyPatch(patch) { +export function simplifyPatch(patch: JSONPatch) { const simplifiedPatch = [] const paths = {} // loop over the patch from last to first - for (var i = patch.length - 1; i >= 0; i--) { + for (let i = patch.length - 1; i >= 0; i--) { const action = patch[i] if (action.op === 'test') { // ignore test actions @@ -331,7 +355,7 @@ export function simplifyPatch(patch) { * @return {{data: JSONData, revert: JSONPatch}} * @private */ -export function add (data, path, value, options) { +export function add (data: JSONData, path: string, value: JSONData, options) { const pathArray = parseJSONPointer(path) const parentPath = pathArray.slice(0, pathArray.length - 1) const dataPath = toDataPath(data, parentPath) @@ -341,16 +365,29 @@ export function add (data, path, value, options) { let updatedData if (parent.type === 'Array') { - updatedData = insertAt(data, dataPath.concat('items', prop), value) + const newItem = { + id: getId(), // TODO: create a unique id within current id's instead of using a global, ever incrementing id + value + } + updatedData = insertAt(data, dataPath.concat('items', prop), newItem) } else { // parent.type === 'Object' updatedData = updateIn(data, dataPath, (object) => { - const newProp = { name: prop, value } - const index = (options && typeof options.before === 'string') - ? findPropertyIndex(object, options.before) // insert before - : object.props.length // append + const existingIndex = findPropertyIndex(object, prop) + if (existingIndex !== -1) { + // replace existing item + return setIn(object, ['props', existingIndex, 'value'], value) + } + else { + // insert new item + const newId = getId() + const newProp = { id: newId, name: prop, value } + const index = (options && typeof options.before === 'string') + ? findPropertyIndex(object, options.before) // insert before + : object.props.length // append - return insertAt(object, ['props', index], newProp) + return insertAt(object, ['props', index], newProp) + } }) } @@ -387,7 +424,7 @@ export function add (data, path, value, options) { * @return {{data: JSONData, revert: JSONPatch}} * @private */ -export function copy (data, path, from, options) { +export function copy (data: JSONData, path: string, from: string, options) { const value = getIn(data, toDataPath(data, parseJSONPointer(from))) return add(data, path, value, options) @@ -402,7 +439,7 @@ export function copy (data, path, from, options) { * @return {{data: JSONData, revert: JSONPatch}} * @private */ -export function move (data, path, from, options) { +export function move (data: JSONData, path: string, from: string, options) { const fromArray = parseJSONPointer(from) const dataValue = getIn(data, toDataPath(data, fromArray)) @@ -446,7 +483,7 @@ export function move (data, path, from, options) { * @param {*} value * @return {null | Error} Returns an error when the tests, returns null otherwise */ -export function test (data, path, value) { +export function test (data: JSONData, path: string, value: any) { if (value === undefined) { return new Error('Test failed, no value provided') } @@ -462,6 +499,8 @@ export function test (data, path, value) { } } +type ExpandCallback = (Path) => boolean + /** * Expand or collapse one or multiple items or properties * @param {JSONData} data @@ -472,7 +511,7 @@ export function test (data, path, value) { * @param {boolean} [expanded=true] New expanded state: true to expand, false to collapse * @return {JSONData} */ -export function expand (data, callback, expanded: boolean = true) { +export function expand (data: JSONData, callback: Path | (Path) => boolean, expanded: boolean = true) { // console.log('expand', callback, expand) if (typeof callback === 'function') { @@ -499,7 +538,7 @@ export function expand (data, callback, expanded: boolean = true) { /** * Expand all Objects and Arrays on a path */ -export function expandPath (data: JSONData, path: Path) { +export function expandPath (data: JSONData, path: Path) : JSONData { let updatedData = data if (path) { @@ -521,7 +560,7 @@ export function expandPath (data: JSONData, path: Path) { * @param {JSONData} data * @param {JSONSchemaError[]} errors */ -export function addErrors (data, errors) { +export function addErrors (data: JSONData, errors) { let updatedData = data if (errors) { @@ -641,24 +680,6 @@ export function addSearchResults (data: JSONData, searchResults: DataPointer[], return updatedData } -/** - * Merge a object describing where the focus is to the data - */ -export function addFocus (data: JSONData, focusOn: DataPointer) { - if (focusOn.type == 'value') { - const dataPath = toDataPath(data, focusOn.path).concat('focus') - return setIn(data, dataPath, true) - } - - if (focusOn.type === 'property') { - const valueDataPath = toDataPath(data, focusOn.path) - const propertyDataPath = allButLast(valueDataPath).concat('focus') - return setIn(data, propertyDataPath, true) - } - - return data -} - /** * Do a case insensitive search for a search text in a text * @param {String} text @@ -669,13 +690,15 @@ export function containsCaseInsensitive (text: string, search: string): boolean return String(text).toLowerCase().indexOf(search.toLowerCase()) !== -1 } +type RecurseCallback = (JSONData, Path, JSONData) => JSONData + /** * Recursively transform JSONData: a recursive "map" function * @param {JSONData} data * @param {function(value: JSONData, path: Path, root: JSONData)} callback * @return {JSONData} Returns the transformed data */ -export function transform (data, callback) { +export function transform (data: JSONData, callback: RecurseCallback) { return recurseTransform (data, [], data, callback) } @@ -687,40 +710,33 @@ export function transform (data, callback) { * @param {function(value: JSONData, path: Path, root: JSONData)} callback * @return {JSONData} Returns the transformed data */ -function recurseTransform (value, path, root, callback) { +function recurseTransform (value: JSONData, path: Path, root?: JSONData, callback: RecurseCallback) : JSONData{ let updatedValue = callback(value, path, root) - switch (value.type) { - case 'Array': { - let updatedItems = updatedValue.items + if (value.type === 'Array') { + let updatedItems = updatedValue.items - updatedValue.items.forEach((item, index) => { - const updatedItem = recurseTransform(item, path.concat(String(index)), root, callback) - updatedItems = setIn(updatedItems, [index], updatedItem) - }) + updatedValue.items.forEach((item, index) => { + const updatedItem = recurseTransform(item.value, path.concat(String(index)), root, callback) + updatedItems = setIn(updatedItems, [index, 'value'], updatedItem) + }) - updatedValue = setIn(updatedValue, ['items'], updatedItems) - - break - } - - case 'Object': { - let updatedProps = updatedValue.props - - updatedValue.props.forEach((prop, index) => { - const updatedItem = recurseTransform(prop.value, path.concat(prop.name), root, callback) - updatedProps = setIn(updatedProps, [index, 'value'], updatedItem) - }) - - updatedValue = setIn(updatedValue, ['props'], updatedProps) - - break - } - - default: // type 'string' or 'value' - // no childs to traverse + updatedValue = setIn(updatedValue, ['items'], updatedItems) } + if (value.type === 'Object') { + let updatedProps = updatedValue.props + + updatedValue.props.forEach((prop, index) => { + const updatedItem = recurseTransform(prop.value, path.concat(prop.name), root, callback) + updatedProps = setIn(updatedProps, [index, 'value'], updatedItem) + }) + + updatedValue = setIn(updatedValue, ['props'], updatedProps) + } + + // (for type 'string' or 'value' there are no childs to traverse) + return updatedValue } @@ -729,7 +745,7 @@ function recurseTransform (value, path, root, callback) { * @param {JSONData} data * @param {function(value: JSONData, path: Path, root: JSONData)} callback */ -export function traverse (data, callback) { +export function traverse (data: JSONData, callback: RecurseCallback) { return recurseTraverse (data, [], data, callback) } @@ -740,13 +756,13 @@ export function traverse (data, callback) { * @param {JSONData | null} root The root object, object at path=[] * @param {function(value: JSONData, path: Path, root: JSONData)} callback */ -function recurseTraverse (value, path, root, callback) { +function recurseTraverse (value: JSONData, path: Path, root: JSONData, callback: RecurseCallback) { callback(value, path, root) switch (value.type) { case 'Array': { - value.items.forEach((item, index) => { - recurseTraverse(item, path.concat(String(index)), root, callback) + value.items.forEach((item: ItemData, index) => { + recurseTraverse(item.value, path.concat(String(index)), root, callback) }) break } @@ -783,7 +799,9 @@ export function pathExists (data, path) { if (data.type === 'Array') { // index of an array index = path[0] - return pathExists(data.items[index], path.slice(1)) + const item = data.items[index] + + return pathExists(item && item.value, path.slice(1)) } else { // object property. find the index of this property @@ -825,7 +843,7 @@ export function resolvePathIndex (data, path) { * @return {string | null} Returns the name of the next property, * or null if there is none */ -export function findNextProp (parent, prop) { +export function findNextProp (parent: ObjectData, prop: string) : string | null { const index = findPropertyIndex(parent, prop) if (index === -1) { return null @@ -841,7 +859,7 @@ export function findNextProp (parent, prop) { * @param {string} prop * @return {number} Returns the index when found, -1 when not found */ -export function findPropertyIndex (object, prop) { +export function findPropertyIndex (object: ObjectData, prop: string) { return object.props.findIndex(p => p.name === prop) } @@ -851,7 +869,7 @@ export function findPropertyIndex (object, prop) { * @param {string} pointer * @return {Array} */ -export function parseJSONPointer (pointer) { +export function parseJSONPointer (pointer: string) { const path = pointer.split('/') path.shift() // remove the first empty entry @@ -864,12 +882,33 @@ export function parseJSONPointer (pointer) { * @param {Path} path * @return {string} */ -export function compileJSONPointer (path) { +export function compileJSONPointer (path: Path) { return path .map(p => '/' + String(p).replace(/~/g, '~0').replace(/\//g, '~1')) .join('') } +/** + * Get a new "unique" id. Id's are created from an incremental counter. + * @return {number} + */ +// TODO: use createUniqueId instead of getId() +function getId () : number { + _id++ + return _id +} +let _id = 0 + +/** + * Find a unique id from an array with properties each having an id field. + * The + * @param {{id: string}} array + */ +// TODO: use createUniqueId instead of getId() +function createUniqueId (array) { + return Math.max(...array.map(item => item.id)) + 1 +} + /** * Returns the last item of an array */ diff --git a/src/types.js b/src/types.js index b093353..47f504a 100644 --- a/src/types.js +++ b/src/types.js @@ -1,19 +1,6 @@ // @flow /** - * - * @typedef {'Object' | 'Array' | 'value' | 'string'} JSONDataType - * - * @typedef {{ - * patch: JSONPatch, - * revert: JSONPatch, - * error: null | Error - * }} JSONPatchResult - * - * @typedef {{ - * dataPath: string, - * message: string - * }} JSONSchemaError * * @typedef {{ * name: string?, @@ -33,10 +20,6 @@ * ace: Object? * }} Options * - * @typedef {{ - * expand: function (path: Path)? - * }} PatchOptions - * */ @@ -53,11 +36,17 @@ export type SearchResultStatus = 'normal' | 'active' export type DataPointerType = 'value' | 'property' export type PropertyData = { + id: number, name: string, value: JSONData, searchResult: ?SearchResultStatus } +export type ItemData = { + id: number, + value: JSONData +} + export type ObjectData = { type: 'Object', expanded: ?boolean, @@ -67,7 +56,7 @@ export type ObjectData = { export type ArrayData = { type: 'Array', expanded: ?boolean, - items: ?JSONData[] + items: ItemData[] } export type ValueData = { @@ -78,6 +67,8 @@ export type ValueData = { export type JSONData = ObjectData | ArrayData | ValueData +export type JSONDataType = 'Object' | 'Array' | 'value' | 'string' + export type Path = string[] @@ -96,8 +87,25 @@ export type SetOptions = { export type JSONPatchAction = { op: string, // TODO: define allowed ops - path?: string, + path: string, from?: string, - value?: any + value?: any, + jsoneditor?: PatchOptions } export type JSONPatch = JSONPatchAction[] + +export type PatchOptions = { + type: JSONDataType, + expand: (Path) => boolean +} + +export type JSONPatchResult = { + patch: JSONPatch, + revert: JSONPatch, + error: null | Error +} + +export type JSONSchemaError = { + dataPath: string, + message: string +} diff --git a/test/jsonData.test.js b/test/jsonData.test.js index 7b2c20c..ec73803 100644 --- a/test/jsonData.test.js +++ b/test/jsonData.test.js @@ -20,45 +20,58 @@ const JSON_DATA_EXAMPLE = { expanded: true, props: [ { + id: '[ID]', name: 'obj', value: { type: 'Object', expanded: true, props: [ { + id: '[ID]', name: 'arr', value: { type: 'Array', expanded: true, items: [ { - type: 'value', - value: 1 + id: '[ID]', + value: { + type: 'value', + value: 1 + } }, { - type: 'value', - value: 2 + id: '[ID]', + value: { + type: 'value', + value: 2 + } }, { - type: 'Object', - expanded: true, - props: [ - { - name: 'first', - value: { - type: 'value', - value: 3 + id: '[ID]', + value: { + type: 'Object', + expanded: true, + props: [ + { + id: '[ID]', + name: 'first', + value: { + type: 'value', + value: 3 + } + }, + { + id: '[ID]', + name: 'last', + value: { + type: 'value', + value: 4 + } } - }, - { - name: 'last', - value: { - type: 'value', - value: 4 - } - } - ] - }, + ] + } + } ] } } @@ -66,6 +79,7 @@ const JSON_DATA_EXAMPLE = { } }, { + id: '[ID]', name: 'str', value: { type: 'value', @@ -73,6 +87,7 @@ const JSON_DATA_EXAMPLE = { } }, { + id: '[ID]', name: 'nill', value: { type: 'value', @@ -80,6 +95,7 @@ const JSON_DATA_EXAMPLE = { } }, { + id: '[ID]', name: 'bool', value: { type: 'value', @@ -89,50 +105,66 @@ const JSON_DATA_EXAMPLE = { ] } +// TODO: instead of all slightly different copies of JSON_DATA_EXAMPLE, built them up via setIn, updateIn based on JSON_DATA_EXAMPLE + const JSON_DATA_EXAMPLE_COLLAPSED_1 = { type: 'Object', expanded: true, props: [ { + id: '[ID]', name: 'obj', value: { type: 'Object', expanded: true, props: [ { + id: '[ID]', name: 'arr', value: { type: 'Array', expanded: true, items: [ + { - type: 'value', - value: 1 + id: '[ID]', + value: { + type: 'value', + value: 1 + } }, { - type: 'value', - value: 2 + id: '[ID]', + value: { + type: 'value', + value: 2 + } }, { - type: 'Object', - expanded: false, - props: [ - { - name: 'first', - value: { - type: 'value', - value: 3 + id: '[ID]', + value: { + type: 'Object', + expanded: false, + props: [ + { + id: '[ID]', + name: 'first', + value: { + type: 'value', + value: 3 + } + }, + { + id: '[ID]', + name: 'last', + value: { + type: 'value', + value: 4 + } } - }, - { - name: 'last', - value: { - type: 'value', - value: 4 - } - } - ] - }, + ] + } + } ] } } @@ -140,6 +172,7 @@ const JSON_DATA_EXAMPLE_COLLAPSED_1 = { } }, { + id: '[ID]', name: 'str', value: { type: 'value', @@ -147,6 +180,7 @@ const JSON_DATA_EXAMPLE_COLLAPSED_1 = { } }, { + id: '[ID]', name: 'nill', value: { type: 'value', @@ -154,6 +188,7 @@ const JSON_DATA_EXAMPLE_COLLAPSED_1 = { } }, { + id: '[ID]', name: 'bool', value: { type: 'value', @@ -168,45 +203,58 @@ const JSON_DATA_EXAMPLE_COLLAPSED_2 = { expanded: true, props: [ { + id: '[ID]', name: 'obj', value: { type: 'Object', expanded: false, props: [ { + id: '[ID]', name: 'arr', value: { type: 'Array', expanded: false, items: [ { - type: 'value', - value: 1 + id: '[ID]', + value: { + type: 'value', + value: 1 + } }, { - type: 'value', - value: 2 + id: '[ID]', + value: { + type: 'value', + value: 2 + } }, { - type: 'Object', - expanded: false, - props: [ - { - name: 'first', - value: { - type: 'value', - value: 3 + id: '[ID]', + value: { + type: 'Object', + expanded: false, + props: [ + { + id: '[ID]', + name: 'first', + value: { + type: 'value', + value: 3 + } + }, + { + id: '[ID]', + name: 'last', + value: { + type: 'value', + value: 4 + } } - }, - { - name: 'last', - value: { - type: 'value', - value: 4 - } - } - ] - }, + ] + } + } ] } } @@ -214,6 +262,7 @@ const JSON_DATA_EXAMPLE_COLLAPSED_2 = { } }, { + id: '[ID]', name: 'str', value: { type: 'value', @@ -221,6 +270,7 @@ const JSON_DATA_EXAMPLE_COLLAPSED_2 = { } }, { + id: '[ID]', name: 'nill', value: { type: 'value', @@ -228,6 +278,7 @@ const JSON_DATA_EXAMPLE_COLLAPSED_2 = { } }, { + id: '[ID]', name: 'bool', value: { type: 'value', @@ -243,45 +294,58 @@ const JSON_DATA_EXAMPLE_SEARCH_L = { expanded: true, props: [ { + id: '[ID]', name: 'obj', value: { type: 'Object', expanded: true, props: [ { + id: '[ID]', name: 'arr', value: { type: 'Array', expanded: true, items: [ { - type: 'value', - value: 1 + id: '[ID]', + value: { + type: 'value', + value: 1 + } }, { - type: 'value', - value: 2 + id: '[ID]', + value: { + type: 'value', + value: 2 + } }, { - type: 'Object', - expanded: true, - props: [ - { - name: 'first', - value: { - type: 'value', - value: 3 - } - }, - { - name: 'last', - value: { - type: 'value', - value: 4 + id: '[ID]', + value: { + type: 'Object', + expanded: true, + props: [ + { + id: '[ID]', + name: 'first', + value: { + type: 'value', + value: 3 + } }, - searchResult: 'active' - } - ] + { + id: '[ID]', + name: 'last', + value: { + type: 'value', + value: 4 + }, + searchResult: 'active' + } + ] + } } ] } @@ -290,6 +354,7 @@ const JSON_DATA_EXAMPLE_SEARCH_L = { } }, { + id: '[ID]', name: 'str', value: { type: 'value', @@ -298,6 +363,7 @@ const JSON_DATA_EXAMPLE_SEARCH_L = { } }, { + id: '[ID]', name: 'nill', value: { type: 'value', @@ -307,6 +373,7 @@ const JSON_DATA_EXAMPLE_SEARCH_L = { searchResult: 'normal' }, { + id: '[ID]', name: 'bool', value: { type: 'value', @@ -322,11 +389,13 @@ const JSON_DATA_SMALL = { type: 'Object', props: [ { + id: '[ID]', name: 'obj', value: { type: 'Object', props: [ { + id: '[ID]', name: 'a', value: { type: 'value', @@ -337,13 +406,17 @@ const JSON_DATA_SMALL = { } }, { + id: '[ID]', name: 'arr', value: { type: 'Array', items: [ { - type: 'value', - value: 3 + id: '[ID]', + value: { + type: 'value', + value: 3 + } } ] } @@ -362,46 +435,59 @@ const JSON_DATA_EXAMPLE_ERRORS = { expanded: true, props: [ { + id: '[ID]', name: 'obj', value: { type: 'Object', expanded: true, props: [ { + id: '[ID]', name: 'arr', value: { type: 'Array', expanded: true, items: [ { - type: 'value', - value: 1 + id: '[ID]', + value: { + type: 'value', + value: 1 + } }, { - type: 'value', - value: 2 + id: '[ID]', + value: { + type: 'value', + value: 2 + } }, { - type: 'Object', - expanded: true, - props: [ - { - name: 'first', - value: { - type: 'value', - value: 3 + id: '[ID]', + value: { + type: 'Object', + expanded: true, + props: [ + { + id: '[ID]', + name: 'first', + value: { + type: 'value', + value: 3 + } + }, + { + id: '[ID]', + name: 'last', + value: { + type: 'value', + value: 4, + error: JSON_SCHEMA_ERRORS[0] + } } - }, - { - name: 'last', - value: { - type: 'value', - value: 4, - error: JSON_SCHEMA_ERRORS[0] - } - } - ] - }, + ] + } + } ] } } @@ -409,6 +495,7 @@ const JSON_DATA_EXAMPLE_ERRORS = { } }, { + id: '[ID]', name: 'str', value: { type: 'value', @@ -416,6 +503,7 @@ const JSON_DATA_EXAMPLE_ERRORS = { } }, { + id: '[ID]', name: 'nill', value: { type: 'value', @@ -424,6 +512,7 @@ const JSON_DATA_EXAMPLE_ERRORS = { } }, { + id: '[ID]', name: 'bool', value: { type: 'value', @@ -438,7 +527,10 @@ test('jsonToData', t => { return true } - t.deepEqual(jsonToData(JSON_EXAMPLE, expand, []), JSON_DATA_EXAMPLE) + const jsonData = jsonToData(JSON_EXAMPLE, expand, []) + replaceIds(jsonData) + + t.deepEqual(jsonData, JSON_DATA_EXAMPLE) }) test('dataToJson', t => { @@ -622,6 +714,21 @@ test('jsonpatch replace', t => { ]) }) +test('jsonpatch replace (keep ids intact)', t => { + const json = { value: 42 } + const patch = [ + {op: 'replace', path: '/value', value: 100} + ] + + const data = jsonToData(json) + const valueId = data.props[0].id + + const patchedData = patchData(data, patch).data + const patchedValueId = patchedData.props[0].id + + t.is(patchedValueId, valueId) +}) + test('jsonpatch copy', t => { const json = { arr: [1,2,3], @@ -659,6 +766,34 @@ test('jsonpatch copy', t => { ]) }) +// test('jsonpatch copy (create new ids)', t => { +// const json = { foo: { bar: 42 } } +// const patch = [ +// {op: 'copy', from: '/foo', path: '/copied'} +// ] +// +// const data = jsonToData(json) +// const objectId = data.id +// const fooId = data.props[0].value.id +// const barId = data.props[0].value.props[0].value.id +// +// const patchedData = patchData(data, patch).data +// const patchedObjectId = patchedData.id +// const patchedFooId = patchedData.props[0].value.id +// const patchedBarId = patchedData.props[0].value.props[0].value.id +// const patchedCopiedId = patchedData.props[1].value.id +// const patchedCopiedBarId = patchedData.props[1].value.props[0].value.id +// +// t.is(patchedData.props[0].name, 'foo') +// t.is(patchedData.props[1].name, 'copied') +// +// t.is(patchedObjectId, objectId, 'same object id') +// t.is(patchedFooId, fooId, 'same foo id') +// t.is(patchedBarId, barId, 'same bar id') +// t.not(patchedCopiedId, patchedFooId, 'different copied foo id') +// t.not(patchedCopiedBarId, patchedBarId, 'different copied bar id') +// }) +// test('jsonpatch move', t => { const json = { arr: [1,2,3], @@ -732,6 +867,54 @@ test('jsonpatch move before', t => { }) test('jsonpatch move and replace', t => { + const json = { a: 2, b: 3 } + + const patch = [ + {op: 'move', from: '/a', path: '/b'}, + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + replaceIds(patchedData) + t.deepEqual(patchedData, { + "type": "Object", + "expanded": true, + "props": [ + { + "id": "[ID]", + "name": "b", + "value": { + "type": "value", + "value": 2 + } + } + ] + }) + + t.deepEqual(patchedJson, { b : 2 }) + t.deepEqual(revert, [ + {op:'move', from: '/b', path: '/a'}, + {op:'add', path:'/b', value: 3, jsoneditor: {type: 'value', before: 'b'}} + ]) + + // test revert + const data2 = jsonToData(patchedJson) + const result2 = patchData(data2, revert) + const patchedData2 = result2.data + const revert2 = result2.revert + const patchedJson2 = dataToJson(patchedData2) + + t.deepEqual(patchedJson2, json) + t.deepEqual(revert2, [ + {op: 'move', from: '/a', path: '/b'} + ]) +}) + +test('jsonpatch move and replace (nested)', t => { const json = { arr: [1,2,3], obj: {a : 4} @@ -768,6 +951,44 @@ test('jsonpatch move and replace', t => { ]) }) +test('jsonpatch move (keep id intact)', t => { + const json = { value: 42 } + const patch = [ + {op: 'move', from: '/value', path: '/moved'} + ] + + const data = jsonToData(json) + const objectId = data.id + const valueId = data.props[0].value.id + + const patchedData = patchData(data, patch).data + + const patchedObjectId = patchedData.id + const patchedValueId = patchedData.props[0].value.id + + t.is(patchedObjectId, objectId) + t.is(patchedValueId, valueId) +}) + +test('jsonpatch move and replace (keep ids intact)', t => { + const json = { a: 2, b: 3 } + const patch = [ + {op: 'move', from: '/a', path: '/b'} + ] + + const data = jsonToData(json) + const bId = data.props[1].id + + t.is(data.props[0].name, 'a') + t.is(data.props[1].name, 'b') + + const patchedData = patchData(data, patch).data + const patchedBId = patchedData.props[0].id + + t.is(patchedData.props[0].name, 'b') + t.is(patchedBId, bId) +}) + test('jsonpatch test (ok)', t => { const json = { arr: [1,2,3], @@ -855,7 +1076,7 @@ test('add and remove errors', t => { test('transform', t => { // {obj: {a: 2}, arr: [3]} - + let log = [] const transformed = transform(JSON_DATA_SMALL, function (value, path, root) { t.is(root, JSON_DATA_SMALL) @@ -878,18 +1099,18 @@ test('transform', t => { [JSON_DATA_SMALL.props[0].value, ['obj'], JSON_DATA_SMALL], [JSON_DATA_SMALL.props[0].value.props[0].value, ['obj', 'a'], JSON_DATA_SMALL], [JSON_DATA_SMALL.props[1].value, ['arr'], JSON_DATA_SMALL], - [JSON_DATA_SMALL.props[1].value.items[0], ['arr', '0'], JSON_DATA_SMALL], + [JSON_DATA_SMALL.props[1].value.items[0].value, ['arr', '0'], JSON_DATA_SMALL], ] - // log.forEach((row, index) => { - // t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index ) - // }) + log.forEach((row, index) => { + t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index ) + }) t.deepEqual(log, EXPECTED_LOG) t.not(transformed, JSON_DATA_SMALL) t.not(transformed.props[0].value, JSON_DATA_SMALL.props[0].value) t.not(transformed.props[0].value.props[0].value, JSON_DATA_SMALL.props[0].value.props[0].value) t.is(transformed.props[1].value, JSON_DATA_SMALL.props[1].value) - t.is(transformed.props[1].value.items[0], JSON_DATA_SMALL.props[1].value.items[0]) + t.is(transformed.props[1].value.items[0].value, JSON_DATA_SMALL.props[1].value.items[0].value) }) @@ -910,19 +1131,19 @@ test('traverse', t => { [JSON_DATA_SMALL.props[0].value, ['obj'], JSON_DATA_SMALL], [JSON_DATA_SMALL.props[0].value.props[0].value, ['obj', 'a'], JSON_DATA_SMALL], [JSON_DATA_SMALL.props[1].value, ['arr'], JSON_DATA_SMALL], - [JSON_DATA_SMALL.props[1].value.items[0], ['arr', '0'], JSON_DATA_SMALL], + [JSON_DATA_SMALL.props[1].value.items[0].value, ['arr', '0'], JSON_DATA_SMALL], ] - // log.forEach((row, index) => { - // t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index ) - // }) + log.forEach((row, index) => { + t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index ) + }) t.deepEqual(log, EXPECTED_LOG) }) test('search', t => { const searchResults = search(JSON_DATA_EXAMPLE, 'L') - //console.log(JSON.stringify(searchResults, null, 2)) + // printJSON(searchResults) t.deepEqual(searchResults, [ {path: ['obj', 'arr', '2', 'last'], type: 'property'}, @@ -935,7 +1156,7 @@ test('search', t => { const activeSearchResult = searchResults[0] const updatedData = addSearchResults(JSON_DATA_EXAMPLE, searchResults, activeSearchResult) - // console.log(JSON.stringify(updatedData, null, 2)) + // printJSON(updatedData) t.deepEqual(updatedData, JSON_DATA_EXAMPLE_SEARCH_L) }) @@ -995,3 +1216,28 @@ test('previousSearchResult', t => { // return null when searchResults are empty t.deepEqual(previousSearchResult([], {path: ['non', 'existing'], type: 'value'}), null) }) + +// helper function to replace all id properties with a constant value +function replaceIds (data, value = '[ID]') { + if (data.type === 'Object') { + data.props.forEach(prop => { + prop.id = value + replaceIds(prop.value, value) + }) + } + + if (data.type === 'Array') { + data.items.forEach(item => { + item.id = value + replaceIds(item.value, value) + }) + } +} + +// helper function to print JSON in the console +function printJSON (json, message = null) { + if (message) { + console.log(message) + } + console.log(JSON.stringify(json, null, 2)) +} \ No newline at end of file