Implement option `validate` and method `setValidator` (WIP)
This commit is contained in:
parent
300e46b149
commit
9356f44c95
|
@ -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
|
||||
|
|
|
@ -33,12 +33,18 @@
|
|||
import { expandPath, syncState, patchProps } from '../logic/documentState.js'
|
||||
import Menu from './Menu.svelte'
|
||||
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}
|
||||
|
|
|
@ -236,3 +236,8 @@ div.empty {
|
|||
background-color: $highlight-active-color;
|
||||
}
|
||||
}
|
||||
|
||||
.validation-error {
|
||||
color: $warning-color;
|
||||
padding-left: $input-padding;
|
||||
}
|
||||
|
|
|
@ -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 @@
|
|||
<button class="tag" on:click={handleExpand}>{value.length} items</button>
|
||||
<div class="delimiter">]</div>
|
||||
{/if}
|
||||
{#if validationError}
|
||||
<!-- FIXME: implement proper tooltip -->
|
||||
<div
|
||||
class='validation-error'
|
||||
title={validationError.isChildError ? 'Contains invalid items' : validationError.message}
|
||||
>
|
||||
<Icon data={faExclamationTriangle} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if expanded}
|
||||
<div class="items">
|
||||
|
@ -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 @@
|
|||
<button class="tag" on:click={handleExpand}>{Object.keys(value).length} props</button>
|
||||
<span class="delimiter">}</span>
|
||||
{/if}
|
||||
{#if validationError}
|
||||
<!-- FIXME: implement proper tooltip -->
|
||||
<div
|
||||
class='validation-error'
|
||||
title={validationError.isChildError ? 'Contains invalid properties' : validationError.message}
|
||||
>
|
||||
<Icon data={faExclamationTriangle} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if expanded}
|
||||
<div class="props">
|
||||
|
@ -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}
|
||||
></div>
|
||||
{#if validationError}
|
||||
<!-- FIXME: implement proper tooltip -->
|
||||
<div class='validation-error' title={validationError.message}>
|
||||
<Icon data={faExclamationTriangle} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.<string, string> | 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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -83,3 +83,7 @@
|
|||
* @property {string} [title=undefined]
|
||||
* @property {boolean} [default=false]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{path: Path, message: string, isChildError?: boolean}} ValidationError
|
||||
*/
|
Loading…
Reference in New Issue