diff --git a/public/index.html b/public/index.html index b492f61..d2a9770 100644 --- a/public/index.html +++ b/public/index.html @@ -95,7 +95,22 @@ target: document.getElementById('testEditorContainer'), props: { doc, - onChangeJson: doc => console.log('onChangeJson', doc) + onChangeJson: doc => console.log('onChangeJson', doc), + validate: doc => { + if ( + doc && typeof doc === 'object' && + doc.object && typeof doc.object === 'object' && + doc.object.a === 'b') { + return [ + { + path: ['object', 'a'], + message: '"a" should not be "b" ;)' + } + ] + } + + return [] + } } }) window.testEditor = testEditor // expose to window for debugging diff --git a/src/components/JSONEditor.svelte b/src/components/JSONEditor.svelte index 56afb5f..191d91b 100644 --- a/src/components/JSONEditor.svelte +++ b/src/components/JSONEditor.svelte @@ -32,13 +32,19 @@ import jump from '../assets/jump.js/src/jump.js' import { expandPath, syncState, patchProps } from '../logic/documentState.js' import Menu from './Menu.svelte' -import { isObjectOrArray } from '../utils/typeUtils.js' + import { isObjectOrArray } from '../utils/typeUtils.js' + import { mapValidationErrors } from '../logic/validation.js' let divContents let domHiddenInput + export let validate = () => [] export let onChangeJson = () => {} + export function setValidator (newValidate) { + validate = newValidate + } + export let doc = {} let state = undefined @@ -46,6 +52,8 @@ import { isObjectOrArray } from '../utils/typeUtils.js' let clipboard = null $: state = syncState(doc, state, [], (path) => path.length < 1) + $: validationErrorsList = validate(doc) + $: validationErrors = mapValidationErrors(validationErrorsList) let showSearch = false let searchText = '' @@ -469,6 +477,7 @@ import { isObjectOrArray } from '../utils/typeUtils.js' path={[]} state={state} searchResult={searchResult && searchResult.itemsWithActive} + validationErrors={validationErrors} onPatch={handlePatch} onUpdateKey={handleUpdateKey} onExpand={handleExpand} diff --git a/src/components/JSONNode.scss b/src/components/JSONNode.scss index acfaf70..1617f61 100644 --- a/src/components/JSONNode.scss +++ b/src/components/JSONNode.scss @@ -235,4 +235,9 @@ div.empty { &.active { background-color: $highlight-active-color; } -} \ No newline at end of file +} + +.validation-error { + color: $warning-color; + padding-left: $input-padding; +} diff --git a/src/components/JSONNode.svelte b/src/components/JSONNode.svelte index df4c1c5..1646d98 100644 --- a/src/components/JSONNode.svelte +++ b/src/components/JSONNode.svelte @@ -10,7 +10,8 @@ STATE_PROPS, STATE_SEARCH_PROPERTY, STATE_SEARCH_VALUE, - INDENTATION_WIDTH + INDENTATION_WIDTH, + VALIDATION_ERROR } from '../constants.js' import { getPlainText, @@ -21,7 +22,7 @@ setPlainText } from '../utils/domUtils.js' import Icon from 'svelte-awesome' - import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons' + import { faCaretDown, faCaretRight, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' import classnames from 'classnames' import { findUniqueName } from '../utils/stringUtils.js' import { isUrl, stringConvert, valueType } from '../utils/typeUtils' @@ -33,6 +34,7 @@ export let path export let state export let searchResult + export let validationErrors export let onPatch export let onUpdateKey export let onExpand @@ -43,6 +45,7 @@ $: expanded = state && state[STATE_EXPANDED] $: limit = state && state[STATE_LIMIT] $: props = state && state[STATE_PROPS] + $: validationError = validationErrors && validationErrors[VALIDATION_ERROR] const escapeUnicode = false // TODO: pass via options @@ -376,6 +379,15 @@
]
{/if} + {#if validationError} + +
+ +
+ {/if} {#if expanded}
@@ -386,6 +398,7 @@ path={path.concat(index)} state={state && state[index]} searchResult={searchResult ? searchResult[index] : undefined} + validationErrors={validationErrors ? validationErrors[index] : undefined} onPatch={onPatch} onUpdateKey={handleUpdateKey} onExpand={onExpand} @@ -444,6 +457,15 @@ } {/if} + {#if validationError} + +
+ +
+ {/if}
{#if expanded}
@@ -454,6 +476,7 @@ path={path.concat(prop.key)} state={state && state[prop.key]} searchResult={searchResult ? searchResult[prop.key] : undefined} + validationErrors={validationErrors ? validationErrors[prop.key] : undefined} onPatch={onPatch} onUpdateKey={handleUpdateKey} onExpand={onExpand} @@ -501,6 +524,12 @@ bind:this={domValue} title={valueIsUrl ? 'Ctrl+Click or Ctrl+Enter to open url in new window' : null} >
+ {#if validationError} + +
+ +
+ {/if} {/if} diff --git a/src/constants.js b/src/constants.js index ec50275..2481eda 100644 --- a/src/constants.js +++ b/src/constants.js @@ -4,6 +4,7 @@ export const STATE_LIMIT = Symbol('limit') export const STATE_PROPS = Symbol('props') export const STATE_SEARCH_PROPERTY = Symbol('search:property') export const STATE_SEARCH_VALUE = Symbol('search:value') +export const VALIDATION_ERROR = Symbol('validation:error') export const SCROLL_DURATION = 300 // ms export const DEBOUNCE_DELAY = 300 diff --git a/src/logic/validation.js b/src/logic/validation.js new file mode 100644 index 0000000..5b1ec3d --- /dev/null +++ b/src/logic/validation.js @@ -0,0 +1,41 @@ +import { initial } from 'lodash-es' +import { VALIDATION_ERROR } from '../constants.js' +import { existsIn, setIn } from '../utils/immutabilityHelpers.js' + +/** + * Create a nested map with validation errors, + * and also create error messages for the parent nodes of the nodes having an error. + * + * @param {ValidationError[]} validationErrors + * @return {Object. | undefined} Returns a nested object containing + */ +export function mapValidationErrors (validationErrors) { + let object = undefined + + validationErrors.forEach(validationError => { + const errorPath = validationError.path.concat([VALIDATION_ERROR]) + object = setIn(object, errorPath, validationError, true) + }) + + // create error entries for all parent nodes + validationErrors.forEach(validationError => { + const path = validationError.path + let parentPath = path + + while (parentPath.length > 0) { + parentPath = initial(parentPath) + + const parentErrorPath = parentPath.concat([VALIDATION_ERROR]) + if (!existsIn(object, parentErrorPath)) { + const error = { + isChildError: true, + path: parentPath, + message: 'Contains invalid data' + } + object = setIn(object, parentErrorPath, error, true) + } + } + }) + + return object +} diff --git a/src/logic/validation.test.js b/src/logic/validation.test.js new file mode 100644 index 0000000..cc782b0 --- /dev/null +++ b/src/logic/validation.test.js @@ -0,0 +1,59 @@ +import assert from 'assert' +import { mapValidationErrors } from './validation.js' +import { VALIDATION_ERROR } from '../constants.js' + +describe('validation', () => { + it('should turn a list with validation errors into a nested object', () => { + const message1 = 'Number expected' + const message2 = 'Year in the past expected' + const message3 = 'Contains invalid data' + + const error1 = { path: ['pupils', 2, 'age'], message: message1 } + const error2 = { path: ['year'], message: message2 } + + const validationErrorsList = [error1, error2] + + const expected = { + pupils: [ + , + , + { + age: { + [VALIDATION_ERROR]: error1 + } + } + ], + year: { + [VALIDATION_ERROR]: error2 + } + } + expected[VALIDATION_ERROR] = { isChildError: true, path: [], message: message3 } + expected.pupils[VALIDATION_ERROR] = { isChildError: true, path: ['pupils'], message: message3 } + expected.pupils[2][VALIDATION_ERROR] = { isChildError: true, path: ['pupils', 2], message: message3 } + + const actual = mapValidationErrors(validationErrorsList) + + assert.deepStrictEqual(actual, expected) + }) + + it('should not override a parent error when creating a validation error object', () => { + const message1 = 'Year in the past expected' + const message2 = 'Missing required property "month"' + + const error1 = { path: ['year'], message: message1 } + const error2 = { path: [], message: message2 } + + const validationErrorsList = [error1, error2] + + const expected = { + year: { + [VALIDATION_ERROR]: error1 + }, + [VALIDATION_ERROR]: error2 + } + + const actual = mapValidationErrors(validationErrorsList) + + assert.deepStrictEqual(actual, expected) + }) +}) \ No newline at end of file diff --git a/src/types.js b/src/types.js index ecf5040..0aa0e31 100644 --- a/src/types.js +++ b/src/types.js @@ -83,3 +83,7 @@ * @property {string} [title=undefined] * @property {boolean} [default=false] */ + + /** + * @typedef {{path: Path, message: string, isChildError?: boolean}} ValidationError + */ \ No newline at end of file