Implement option `validate` and method `setValidator` (WIP)

This commit is contained in:
Jos de Jong 2020-07-29 21:52:46 +02:00
parent 300e46b149
commit 9356f44c95
8 changed files with 168 additions and 5 deletions

View File

@ -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

View File

@ -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}

View File

@ -236,3 +236,8 @@ div.empty {
background-color: $highlight-active-color;
}
}
.validation-error {
color: $warning-color;
padding-left: $input-padding;
}

View File

@ -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">&rbrace;</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>

View File

@ -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

41
src/logic/validation.js Normal file
View File

@ -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
}

View File

@ -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)
})
})

View File

@ -83,3 +83,7 @@
* @property {string} [title=undefined]
* @property {boolean} [default=false]
*/
/**
* @typedef {{path: Path, message: string, isChildError?: boolean}} ValidationError
*/