From 8c2d21109f10c888b6fb9fa5ede1517e544956a8 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Sun, 23 Jan 2022 18:43:11 +0300 Subject: [PATCH] JSON editor Ajv errors (#2597) --- catalog/app/components/JsonEditor/AddRow.tsx | 2 + .../app/components/JsonEditor/JsonEditor.tsx | 7 +- catalog/app/components/JsonEditor/Note.tsx | 37 +++- catalog/app/components/JsonEditor/Row.tsx | 4 + catalog/app/components/JsonEditor/State.js | 32 ++-- .../app/components/JsonEditor/State.spec.js | 4 +- .../app/components/JsonEditor/constants.ts | 5 + .../JsonEditor/mocks/booleans-nulls.js | 36 ++-- .../JsonEditor/mocks/deeply-nested-array.js | 9 +- .../JsonEditor/mocks/deeply-nested-object.js | 164 +++++++++++++----- .../components/JsonEditor/mocks/regular.js | 48 +++-- .../app/components/JsonEditor/mocks/sorted.js | 30 ++-- .../MetadataEditor/MetadataEditor.tsx | 9 +- .../Bucket/PackageDialog/MetaInput.tsx | 10 +- .../containers/Bucket/Queries/QueryViewer.tsx | 4 +- .../app/containers/StoryBook/JsonEditor.tsx | 9 +- catalog/app/utils/reactTools.tsx | 23 +++ 17 files changed, 304 insertions(+), 129 deletions(-) diff --git a/catalog/app/components/JsonEditor/AddRow.tsx b/catalog/app/components/JsonEditor/AddRow.tsx index 1f5659711e2..d7193d39e50 100644 --- a/catalog/app/components/JsonEditor/AddRow.tsx +++ b/catalog/app/components/JsonEditor/AddRow.tsx @@ -30,6 +30,7 @@ const emptyKeyProps = { row: { original: { address: [], + errors: [], required: false, sortIndex: -1, type: 'undefined', @@ -51,6 +52,7 @@ const emptyValueProps = { row: { original: { address: [], + errors: [], required: false, sortIndex: -1, type: 'undefined', diff --git a/catalog/app/components/JsonEditor/JsonEditor.tsx b/catalog/app/components/JsonEditor/JsonEditor.tsx index dc7648469be..9e83ba7ae95 100644 --- a/catalog/app/components/JsonEditor/JsonEditor.tsx +++ b/catalog/app/components/JsonEditor/JsonEditor.tsx @@ -7,7 +7,7 @@ import { EMPTY_SCHEMA, JsonSchema } from 'utils/json-schema' import Column from './Column' import State from './State' -import { JsonValue, RowData } from './constants' +import { JsonValue, RowData, ValidationErrors } from './constants' interface ColumnData { items: RowData[] @@ -194,6 +194,7 @@ interface StateRenderProps { interface JsonEditorWrapperProps { className?: string disabled?: boolean + errors: ValidationErrors multiColumned?: boolean onChange: (value: JsonValue) => void schema?: JsonSchema @@ -202,13 +203,13 @@ interface JsonEditorWrapperProps { export default React.forwardRef( function JsonEditorWrapper( - { className, disabled, multiColumned, onChange, schema: optSchema, value }, + { className, disabled, errors, multiColumned, onChange, schema: optSchema, value }, ref, ) { const schema = optSchema || EMPTY_SCHEMA return ( - + {(stateProps: StateRenderProps) => ( ({ default: { @@ -26,30 +33,42 @@ const useStyles = M.makeStyles((t) => ({ })) interface TypeHelpProps { + errors: ValidationErrors humanReadableSchema: string mismatch: boolean schema?: JsonSchema } -function TypeHelp({ humanReadableSchema, mismatch, schema }: TypeHelpProps) { +function TypeHelp({ errors, humanReadableSchema, mismatch, schema }: TypeHelpProps) { + const validationError = React.useMemo( + () => + RT.join( + errors.map((error) => error.message), +
, + ), + [errors], + ) + if (humanReadableSchema === 'undefined') return <>Key/value is not restricted by schema + const type = `${mismatch ? 'Required type' : 'Type'}: ${humanReadableSchema}` + return (
- {mismatch ? 'Required type: ' : 'Type: '} - {humanReadableSchema} + {validationError || type} {!!schema?.description &&

Description: {schema.description}

}
) } interface NoteValueProps { + errors: ValidationErrors schema?: JsonSchema value: JsonValue } -function NoteValue({ schema, value }: NoteValueProps) { +function NoteValue({ errors, schema, value }: NoteValueProps) { const classes = useStyles() const humanReadableSchema = schemaTypeToHumanString(schema) @@ -58,13 +77,15 @@ function NoteValue({ schema, value }: NoteValueProps) { if (!humanReadableSchema || humanReadableSchema === 'undefined') return null return ( - }> + } + > - {mismatch ? error_outlined : info_outlined} + {info_outlined} ) @@ -78,7 +99,7 @@ interface NoteProps { export default function Note({ columnId, data, value }: NoteProps) { if (columnId === COLUMN_IDS.VALUE) { - return + return } return null diff --git a/catalog/app/components/JsonEditor/Row.tsx b/catalog/app/components/JsonEditor/Row.tsx index 141198a1437..0648aa6c49a 100644 --- a/catalog/app/components/JsonEditor/Row.tsx +++ b/catalog/app/components/JsonEditor/Row.tsx @@ -10,6 +10,9 @@ const useStyles = M.makeStyles((t) => ({ border: `1px solid ${t.palette.grey[400]}`, padding: 0, }, + error: { + borderColor: t.palette.error.main, + }, key: { width: '50%', [t.breakpoints.up('lg')]: { @@ -41,6 +44,7 @@ export default function Row({ cells, columnPath, fresh, onExpand, onRemove }: Ro addressPath.join(', ') +const serializeAddress = (addressPath) => `/${addressPath.join('/')}` const getAddressPath = (key, parentPath) => key === '' ? parentPath : (parentPath || []).concat(key) @@ -124,9 +123,14 @@ function getDefaultValue(jsonDictItem) { return EMPTY_VALUE } -function getJsonDictItem(jsonDict, obj, parentPath, key, sortOrder) { +const NO_ERRORS = [] + +function getJsonDictItem(jsonDict, obj, parentPath, key, sortOrder, allErrors) { const itemAddress = serializeAddress(getAddressPath(key, parentPath)) const item = jsonDict[itemAddress] + const errors = allErrors + ? allErrors.filter((error) => error.instancePath === itemAddress) + : NO_ERRORS // NOTE: can't use R.pathOr, because Ramda thinks `null` is `undefined` too const valuePath = getAddressPath(key, parentPath) const storedValue = R.path(valuePath, obj) @@ -134,6 +138,7 @@ function getJsonDictItem(jsonDict, obj, parentPath, key, sortOrder) { return { [COLUMN_IDS.KEY]: key, [COLUMN_IDS.VALUE]: value, + errors, reactId: calcReactId(valuePath, storedValue), sortIndex: (item && item.sortIndex) || sortOrder.current.dict[itemAddress] || 0, ...(item || {}), @@ -186,11 +191,13 @@ function getSchemaAndObjKeys(obj, jsonDict, objPath, rootKeys) { ]) } -export function iterateJsonDict(jsonDict, obj, fieldPath, rootKeys, sortOrder) { +// TODO: refactor data, decrease number of arguments to three +export function iterateJsonDict(jsonDict, obj, fieldPath, rootKeys, sortOrder, errors) { if (!fieldPath.length) return [ - pipeThru(rootKeys)( - R.map((key) => getJsonDictItem(jsonDict, obj, fieldPath, key, sortOrder)), + FP.function.pipe( + rootKeys, + R.map((key) => getJsonDictItem(jsonDict, obj, fieldPath, key, sortOrder, errors)), R.sortBy(R.prop('sortIndex')), (items) => ({ parent: obj, @@ -203,8 +210,9 @@ export function iterateJsonDict(jsonDict, obj, fieldPath, rootKeys, sortOrder) { const pathPart = R.slice(0, index, fieldPath) const keys = getSchemaAndObjKeys(obj, jsonDict, pathPart, rootKeys) - return pipeThru(keys)( - R.map((key) => getJsonDictItem(jsonDict, obj, pathPart, key, sortOrder)), + return FP.function.pipe( + keys, + R.map((key) => getJsonDictItem(jsonDict, obj, pathPart, key, sortOrder, errors)), R.sortBy(R.prop('sortIndex')), (items) => ({ parent: R.path(pathPart, obj), @@ -220,7 +228,7 @@ export function mergeSchemaAndObjRootKeys(schema, obj) { return R.uniq([...schemaKeys, ...objKeys]) } -export default function JsonEditorState({ children, jsonObject, schema }) { +export default function JsonEditorState({ children, errors, jsonObject, schema }) { // NOTE: fieldPath is like URL for editor columns // `['a', 0, 'b']` means we are focused to `{ a: [ { b: %HERE% }, ... ], ... }` const [fieldPath, setFieldPath] = React.useState([]) @@ -248,8 +256,8 @@ export default function JsonEditorState({ children, jsonObject, schema }) { // NOTE: this data represents table columns shown to user // it's the main source of UI data const columns = React.useMemo( - () => iterateJsonDict(jsonDict, jsonObject, fieldPath, rootKeys, sortOrder), - [jsonObject, jsonDict, fieldPath, rootKeys], + () => iterateJsonDict(jsonDict, jsonObject, fieldPath, rootKeys, sortOrder, errors), + [errors, jsonObject, jsonDict, fieldPath, rootKeys], ) // TODO: Use `sortIndex: -1` to "remove" fields that cannot be removed, diff --git a/catalog/app/components/JsonEditor/State.spec.js b/catalog/app/components/JsonEditor/State.spec.js index 70ccd536594..4f4d2b4de90 100644 --- a/catalog/app/components/JsonEditor/State.spec.js +++ b/catalog/app/components/JsonEditor/State.spec.js @@ -94,7 +94,9 @@ describe('components/JsonEditor/State', () => { }) it('should return one state object utilizing Schema keys and object keys, when input is a flat object', () => { - const sortOrder = { current: { counter: Number.MIN_SAFE_INTEGER, dict: { c: 15 } } } + const sortOrder = { + current: { counter: Number.MIN_SAFE_INTEGER, dict: { '/c': 15 } }, + } const jsonDict = iterateSchema(regular.schema, sortOrder, [], {}) const rootKeys = mergeSchemaAndObjRootKeys(regular.schema, regular.object1) sortOrder.counter = 0 diff --git a/catalog/app/components/JsonEditor/constants.ts b/catalog/app/components/JsonEditor/constants.ts index 0a63da46e11..1e24191df71 100644 --- a/catalog/app/components/JsonEditor/constants.ts +++ b/catalog/app/components/JsonEditor/constants.ts @@ -1,11 +1,16 @@ +import type { ErrorObject } from 'ajv' + import { JsonSchema } from 'utils/json-schema' // TODO: any JSON or EMPTY_VALUE export type JsonValue = $TSFixMe +export type ValidationErrors = (Error | ErrorObject)[] + // TODO: make different types for filled and empty rows export interface RowData { address: string[] + errors: ValidationErrors reactId?: string required: boolean sortIndex: number diff --git a/catalog/app/components/JsonEditor/mocks/booleans-nulls.js b/catalog/app/components/JsonEditor/mocks/booleans-nulls.js index 92a55c75313..f793ac82b28 100644 --- a/catalog/app/components/JsonEditor/mocks/booleans-nulls.js +++ b/catalog/app/components/JsonEditor/mocks/booleans-nulls.js @@ -18,33 +18,33 @@ export const schema = { } export const jsonDict = { - boolValue: { - address: ['boolValue'], + '/nullValue': { + address: ['nullValue'], required: false, - sortIndex: 3, - type: 'boolean', valueSchema: { - type: 'boolean', + type: 'null', }, + sortIndex: 1, + type: 'null', }, - enumBool: { - address: ['enumBool'], + '/boolValue': { + address: ['boolValue'], required: false, - sortIndex: 5, - type: 'boolean', valueSchema: { - enum: [true, false], type: 'boolean', }, + sortIndex: 3, + type: 'boolean', }, - nullValue: { - address: ['nullValue'], + '/enumBool': { + address: ['enumBool'], required: false, - sortIndex: 1, - type: 'null', valueSchema: { - type: 'null', + type: 'boolean', + enum: [true, false], }, + sortIndex: 5, + type: 'boolean', }, } @@ -54,7 +54,7 @@ export const columns = [ items: [ { key: 'nullValue', - reactId: 'nullValue+undefined', + reactId: '/nullValue+undefined', address: ['nullValue'], required: false, valueSchema: { @@ -66,7 +66,7 @@ export const columns = [ }, { key: 'boolValue', - reactId: 'boolValue+undefined', + reactId: '/boolValue+undefined', address: ['boolValue'], required: false, valueSchema: { @@ -78,7 +78,7 @@ export const columns = [ }, { key: 'enumBool', - reactId: 'enumBool+undefined', + reactId: '/enumBool+undefined', address: ['enumBool'], required: false, valueSchema: { diff --git a/catalog/app/components/JsonEditor/mocks/deeply-nested-array.js b/catalog/app/components/JsonEditor/mocks/deeply-nested-array.js index 926bd6a2c9b..a9a270bf87d 100644 --- a/catalog/app/components/JsonEditor/mocks/deeply-nested-array.js +++ b/catalog/app/components/JsonEditor/mocks/deeply-nested-array.js @@ -39,7 +39,7 @@ export const schema = { } export const jsonDict = { - longNestedList: { + '/longNestedList': { address: ['longNestedList'], required: false, valueSchema: { @@ -60,7 +60,12 @@ export const jsonDict = { type: 'array', items: { type: 'array', - items: { type: 'array', items: { type: 'number' } }, + items: { + type: 'array', + items: { + type: 'number', + }, + }, }, }, }, diff --git a/catalog/app/components/JsonEditor/mocks/deeply-nested-object.js b/catalog/app/components/JsonEditor/mocks/deeply-nested-object.js index 603668bacad..683a63b40f5 100644 --- a/catalog/app/components/JsonEditor/mocks/deeply-nested-object.js +++ b/catalog/app/components/JsonEditor/mocks/deeply-nested-object.js @@ -72,7 +72,7 @@ export const schema = { } export const jsonDict = { - a: { + '/a': { address: ['a'], required: false, valueSchema: { @@ -111,7 +111,10 @@ export const jsonDict = { testMaxItems: { type: 'array', items: [ - { type: 'number', maxItems: 3 }, + { + type: 'number', + maxItems: 3, + }, ], }, }, @@ -139,7 +142,7 @@ export const jsonDict = { sortIndex: 1, type: 'object', }, - 'a, b': { + '/a/b': { address: ['a', 'b'], required: false, valueSchema: { @@ -174,7 +177,12 @@ export const jsonDict = { properties: { testMaxItems: { type: 'array', - items: [{ type: 'number', maxItems: 3 }], + items: [ + { + type: 'number', + maxItems: 3, + }, + ], }, }, }, @@ -199,7 +207,7 @@ export const jsonDict = { sortIndex: 3, type: 'object', }, - 'a, b, c': { + '/a/b/c': { address: ['a', 'b', 'c'], required: false, valueSchema: { @@ -231,7 +239,12 @@ export const jsonDict = { properties: { testMaxItems: { type: 'array', - items: [{ type: 'number', maxItems: 3 }], + items: [ + { + type: 'number', + maxItems: 3, + }, + ], }, }, }, @@ -254,7 +267,7 @@ export const jsonDict = { sortIndex: 5, type: 'object', }, - 'a, b, c, d': { + '/a/b/c/d': { address: ['a', 'b', 'c', 'd'], required: false, valueSchema: { @@ -283,7 +296,12 @@ export const jsonDict = { properties: { testMaxItems: { type: 'array', - items: [{ type: 'number', maxItems: 3 }], + items: [ + { + type: 'number', + maxItems: 3, + }, + ], }, }, }, @@ -304,7 +322,7 @@ export const jsonDict = { sortIndex: 7, type: 'object', }, - 'a, b, c, d, e': { + '/a/b/c/d/e': { address: ['a', 'b', 'c', 'd', 'e'], required: false, valueSchema: { @@ -330,7 +348,12 @@ export const jsonDict = { properties: { testMaxItems: { type: 'array', - items: [{ type: 'number', maxItems: 3 }], + items: [ + { + type: 'number', + maxItems: 3, + }, + ], }, }, }, @@ -349,7 +372,7 @@ export const jsonDict = { sortIndex: 9, type: 'object', }, - 'a, b, c, d, e, f': { + '/a/b/c/d/e/f': { address: ['a', 'b', 'c', 'd', 'e', 'f'], required: false, valueSchema: { @@ -372,7 +395,12 @@ export const jsonDict = { properties: { testMaxItems: { type: 'array', - items: [{ type: 'number', maxItems: 3 }], + items: [ + { + type: 'number', + maxItems: 3, + }, + ], }, }, }, @@ -389,7 +417,7 @@ export const jsonDict = { sortIndex: 11, type: 'object', }, - 'a, b, c, d, e, f, g': { + '/a/b/c/d/e/f/g': { address: ['a', 'b', 'c', 'd', 'e', 'f', 'g'], required: false, valueSchema: { @@ -409,7 +437,12 @@ export const jsonDict = { properties: { testMaxItems: { type: 'array', - items: [{ type: 'number', maxItems: 3 }], + items: [ + { + type: 'number', + maxItems: 3, + }, + ], }, }, }, @@ -424,7 +457,7 @@ export const jsonDict = { sortIndex: 13, type: 'object', }, - 'a, b, c, d, e, f, g, h': { + '/a/b/c/d/e/f/g/h': { address: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], required: false, valueSchema: { @@ -441,7 +474,12 @@ export const jsonDict = { properties: { testMaxItems: { type: 'array', - items: [{ type: 'number', maxItems: 3 }], + items: [ + { + type: 'number', + maxItems: 3, + }, + ], }, }, }, @@ -454,7 +492,7 @@ export const jsonDict = { sortIndex: 15, type: 'object', }, - 'a, b, c, d, e, f, g, h, i': { + '/a/b/c/d/e/f/g/h/i': { address: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], required: false, valueSchema: { @@ -466,7 +504,15 @@ export const jsonDict = { k: { type: 'object', properties: { - testMaxItems: { type: 'array', items: [{ type: 'number', maxItems: 3 }] }, + testMaxItems: { + type: 'array', + items: [ + { + type: 'number', + maxItems: 3, + }, + ], + }, }, }, }, @@ -476,7 +522,7 @@ export const jsonDict = { sortIndex: 17, type: 'object', }, - 'a, b, c, d, e, f, g, h, i, j': { + '/a/b/c/d/e/f/g/h/i/j': { address: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'], required: false, valueSchema: { @@ -485,7 +531,15 @@ export const jsonDict = { k: { type: 'object', properties: { - testMaxItems: { type: 'array', items: [{ type: 'number', maxItems: 3 }] }, + testMaxItems: { + type: 'array', + items: [ + { + type: 'number', + maxItems: 3, + }, + ], + }, }, }, }, @@ -493,22 +547,38 @@ export const jsonDict = { sortIndex: 19, type: 'object', }, - 'a, b, c, d, e, f, g, h, i, j, k': { + '/a/b/c/d/e/f/g/h/i/j/k': { address: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'], required: false, valueSchema: { type: 'object', properties: { - testMaxItems: { type: 'array', items: [{ type: 'number', maxItems: 3 }] }, + testMaxItems: { + type: 'array', + items: [ + { + type: 'number', + maxItems: 3, + }, + ], + }, }, }, sortIndex: 21, type: 'object', }, - 'a, b, c, d, e, f, g, h, i, j, k, testMaxItems': { + '/a/b/c/d/e/f/g/h/i/j/k/testMaxItems': { address: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'testMaxItems'], required: false, - valueSchema: { type: 'array', items: [{ type: 'number', maxItems: 3 }] }, + valueSchema: { + type: 'array', + items: [ + { + type: 'number', + maxItems: 3, + }, + ], + }, sortIndex: 23, type: 'array', }, @@ -521,8 +591,9 @@ export const columnsNested = [ parent: {}, items: [ { + errors: [], key: 'a', - reactId: 'a+undefined', + reactId: '/a+undefined', address: ['a'], required: false, valueSchema: schema.properties.a, @@ -536,8 +607,9 @@ export const columnsNested = [ parent: undefined, items: [ { + errors: [], key: 'b', - reactId: 'a, b+undefined', + reactId: '/a/b+undefined', address: ['a', 'b'], required: false, valueSchema: schema.properties.a.properties.b, @@ -551,8 +623,9 @@ export const columnsNested = [ parent: undefined, items: [ { + errors: [], key: 'c', - reactId: 'a, b, c+undefined', + reactId: '/a/b/c+undefined', valueSchema: schema.properties.a.properties.b.properties.c, address: ['a', 'b', 'c'], required: false, @@ -566,8 +639,9 @@ export const columnsNested = [ parent: undefined, items: [ { + errors: [], key: 'd', - reactId: 'a, b, c, d+undefined', + reactId: '/a/b/c/d+undefined', address: ['a', 'b', 'c', 'd'], required: false, valueSchema: schema.properties.a.properties.b.properties.c.properties.d, @@ -604,9 +678,10 @@ export const columns1 = [ parent: { a: { b: [1, 2, { c: [{ d: { e: [1, 2, 3] } }] }] } }, items: [ { + errors: [], key: 'a', value: { b: [1, 2, { c: [{ d: { e: [1, 2, 3] } }] }] }, - reactId: 'a+{"b":[1,2,{"c":[{"d":{"e":[1,2,3]}}]}]}', + reactId: '/a+{"b":[1,2,{"c":[{"d":{"e":[1,2,3]}}]}]}', sortIndex: 0, }, ], @@ -615,9 +690,10 @@ export const columns1 = [ parent: { b: [1, 2, { c: [{ d: { e: [1, 2, 3] } }] }] }, items: [ { + errors: [], key: 'b', value: [1, 2, { c: [{ d: { e: [1, 2, 3] } }] }], - reactId: 'a, b+[1,2,{"c":[{"d":{"e":[1,2,3]}}]}]', + reactId: '/a/b+[1,2,{"c":[{"d":{"e":[1,2,3]}}]}]', sortIndex: 0, }, ], @@ -625,12 +701,13 @@ export const columns1 = [ { parent: [1, 2, { c: [{ d: { e: [1, 2, 3] } }] }], items: [ - { key: 0, value: 1, reactId: 'a, b, 0+1', sortIndex: 0 }, - { key: 1, value: 2, reactId: 'a, b, 1+2', sortIndex: 0 }, + { errors: [], key: 0, value: 1, reactId: '/a/b/0+1', sortIndex: 0 }, + { errors: [], key: 1, value: 2, reactId: '/a/b/1+2', sortIndex: 0 }, { + errors: [], key: 2, value: { c: [{ d: { e: [1, 2, 3] } }] }, - reactId: 'a, b, 2+{"c":[{"d":{"e":[1,2,3]}}]}', + reactId: '/a/b/2+{"c":[{"d":{"e":[1,2,3]}}]}', sortIndex: 0, }, ], @@ -638,8 +715,9 @@ export const columns1 = [ { items: [ { + errors: [], key: 'c', - reactId: 'a, b, 2, c+[{"d":{"e":[1,2,3]}}]', + reactId: '/a/b/2/c+[{"d":{"e":[1,2,3]}}]', sortIndex: 0, value: [ { @@ -664,8 +742,9 @@ export const columns1 = [ { items: [ { + errors: [], key: 0, - reactId: 'a, b, 2, c, 0+{"d":{"e":[1,2,3]}}', + reactId: '/a/b/2/c/0+{"d":{"e":[1,2,3]}}', sortIndex: 0, value: { d: { @@ -685,8 +764,9 @@ export const columns1 = [ { items: [ { + errors: [], key: 'd', - reactId: 'a, b, 2, c, 0, d+{"e":[1,2,3]}', + reactId: '/a/b/2/c/0/d+{"e":[1,2,3]}', sortIndex: 0, value: { e: [1, 2, 3], @@ -702,8 +782,9 @@ export const columns1 = [ { items: [ { + errors: [], key: 'e', - reactId: 'a, b, 2, c, 0, d, e+[1,2,3]', + reactId: '/a/b/2/c/0/d/e+[1,2,3]', sortIndex: 0, value: [1, 2, 3], }, @@ -715,20 +796,23 @@ export const columns1 = [ { items: [ { + errors: [], key: 0, - reactId: 'a, b, 2, c, 0, d, e, 0+1', + reactId: '/a/b/2/c/0/d/e/0+1', sortIndex: 0, value: 1, }, { + errors: [], key: 1, - reactId: 'a, b, 2, c, 0, d, e, 1+2', + reactId: '/a/b/2/c/0/d/e/1+2', sortIndex: 0, value: 2, }, { + errors: [], key: 2, - reactId: 'a, b, 2, c, 0, d, e, 2+3', + reactId: '/a/b/2/c/0/d/e/2+3', sortIndex: 0, value: 3, }, diff --git a/catalog/app/components/JsonEditor/mocks/regular.js b/catalog/app/components/JsonEditor/mocks/regular.js index 3b051ada056..fcc6ee3fcef 100644 --- a/catalog/app/components/JsonEditor/mocks/regular.js +++ b/catalog/app/components/JsonEditor/mocks/regular.js @@ -83,8 +83,9 @@ export const columnsSchemaOnly = [ parent: {}, items: [ { + errors: [], key: 'a', - reactId: 'a+undefined', + reactId: '/a+undefined', address: ['a'], required: true, valueSchema: { default: 1e10, type: 'number' }, @@ -93,8 +94,9 @@ export const columnsSchemaOnly = [ value: 10000000000, }, { + errors: [], key: 'b', - reactId: 'b+undefined', + reactId: '/b+undefined', address: ['b'], required: true, valueSchema: { default: 'Barcelona', type: 'string' }, @@ -103,8 +105,9 @@ export const columnsSchemaOnly = [ value: 'Barcelona', }, { + errors: [], key: 'optList', - reactId: 'optList+undefined', + reactId: '/optList+undefined', address: ['optList'], required: false, valueSchema: { @@ -119,8 +122,9 @@ export const columnsSchemaOnly = [ value: EMPTY_VALUE, }, { + errors: [], key: 'optEnum', - reactId: 'optEnum+undefined', + reactId: '/optEnum+undefined', address: ['optEnum'], required: false, valueSchema: { type: 'string', enum: ['one', 'two', 'three'] }, @@ -129,8 +133,9 @@ export const columnsSchemaOnly = [ value: EMPTY_VALUE, }, { + errors: [], key: 'enumObjects', - reactId: 'enumObjects+undefined', + reactId: '/enumObjects+undefined', address: ['enumObjects'], required: false, valueSchema: { type: 'object', enum: [{ id: 1 }, { id: 2 }, { id: 3 }] }, @@ -139,8 +144,9 @@ export const columnsSchemaOnly = [ value: EMPTY_VALUE, }, { + errors: [], key: 'enumArrays', - reactId: 'enumArrays+undefined', + reactId: '/enumArrays+undefined', address: ['enumArrays'], required: false, valueSchema: { @@ -156,8 +162,9 @@ export const columnsSchemaOnly = [ value: EMPTY_VALUE, }, { + errors: [], key: 'enumArraysAndObjects', - reactId: 'enumArraysAndObjects+undefined', + reactId: '/enumArraysAndObjects+undefined', address: ['enumArraysAndObjects'], required: false, valueSchema: { @@ -183,9 +190,10 @@ export const columnsSchemaAndObject1 = [ parent: object1, items: [ { + errors: [], key: 'a', value: 1, - reactId: 'a+1', + reactId: '/a+1', address: ['a'], required: true, valueSchema: { default: 1e10, type: 'number' }, @@ -193,8 +201,9 @@ export const columnsSchemaAndObject1 = [ type: 'number', }, { + errors: [], key: 'b', - reactId: 'b+undefined', + reactId: '/b+undefined', address: ['b'], required: true, valueSchema: { default: 'Barcelona', type: 'string' }, @@ -203,8 +212,9 @@ export const columnsSchemaAndObject1 = [ value: 'Barcelona', }, { + errors: [], key: 'optList', - reactId: 'optList+undefined', + reactId: '/optList+undefined', address: ['optList'], required: false, valueSchema: { @@ -219,8 +229,9 @@ export const columnsSchemaAndObject1 = [ value: EMPTY_VALUE, }, { + errors: [], key: 'optEnum', - reactId: 'optEnum+undefined', + reactId: '/optEnum+undefined', address: ['optEnum'], required: false, valueSchema: { type: 'string', enum: ['one', 'two', 'three'] }, @@ -229,8 +240,9 @@ export const columnsSchemaAndObject1 = [ value: EMPTY_VALUE, }, { + errors: [], key: 'enumObjects', - reactId: 'enumObjects+undefined', + reactId: '/enumObjects+undefined', address: ['enumObjects'], required: false, valueSchema: { type: 'object', enum: [{ id: 1 }, { id: 2 }, { id: 3 }] }, @@ -239,8 +251,9 @@ export const columnsSchemaAndObject1 = [ value: EMPTY_VALUE, }, { + errors: [], key: 'enumArrays', - reactId: 'enumArrays+undefined', + reactId: '/enumArrays+undefined', address: ['enumArrays'], required: false, valueSchema: { @@ -256,8 +269,9 @@ export const columnsSchemaAndObject1 = [ value: EMPTY_VALUE, }, { + errors: [], key: 'enumArraysAndObjects', - reactId: 'enumArraysAndObjects+undefined', + reactId: '/enumArraysAndObjects+undefined', address: ['enumArraysAndObjects'], required: false, valueSchema: { @@ -272,9 +286,9 @@ export const columnsSchemaAndObject1 = [ type: 'array', value: EMPTY_VALUE, }, - { key: '111', value: 'aaa', reactId: '111+"aaa"', sortIndex: 0 }, - { key: 'd', value: { e: 'f' }, reactId: 'd+{"e":"f"}', sortIndex: 0 }, - { key: 'c', value: [1, 2, 3], reactId: 'c+[1,2,3]', sortIndex: 15 }, + { errors: [], key: '111', value: 'aaa', reactId: '/111+"aaa"', sortIndex: 0 }, + { errors: [], key: 'd', value: { e: 'f' }, reactId: '/d+{"e":"f"}', sortIndex: 0 }, + { errors: [], key: 'c', value: [1, 2, 3], reactId: '/c+[1,2,3]', sortIndex: 15 }, ], }, ] diff --git a/catalog/app/components/JsonEditor/mocks/sorted.js b/catalog/app/components/JsonEditor/mocks/sorted.js index c5206f797b3..a6ecd63fe1a 100644 --- a/catalog/app/components/JsonEditor/mocks/sorted.js +++ b/catalog/app/components/JsonEditor/mocks/sorted.js @@ -6,13 +6,14 @@ export const columns1 = [ { items: [ { + errors: [], key: 'a', - reactId: 'a+{"b":{"123":123,"c":"ccc","d":"ddd"}}', + reactId: '/a+{"b":{"123":123,"c":"ccc","d":"ddd"}}', sortIndex: 1, value: { b: { 123: 123, c: 'ccc', d: 'ddd' } }, }, - { key: '123', reactId: '123+123', sortIndex: 2, value: 123 }, - { key: 'b', reactId: 'b+"bbb"', sortIndex: 3, value: 'bbb' }, + { errors: [], key: '123', reactId: '/123+123', sortIndex: 2, value: 123 }, + { errors: [], key: 'b', reactId: '/b+"bbb"', sortIndex: 3, value: 'bbb' }, ], parent: { 123: 123, a: { b: { 123: 123, c: 'ccc', d: 'ddd' } }, b: 'bbb' }, }, @@ -22,21 +23,23 @@ export const columns2 = [ { items: [ { + errors: [], key: 'a', - reactId: 'a+{"b":{"123":123,"c":"ccc","d":"ddd"}}', + reactId: '/a+{"b":{"123":123,"c":"ccc","d":"ddd"}}', sortIndex: 0, value: { b: { 123: 123, c: 'ccc', d: 'ddd' } }, }, - { key: 'b', reactId: 'b+"bbb"', sortIndex: 0, value: 'bbb' }, - { key: '123', reactId: '123+123', sortIndex: 1, value: 123 }, + { errors: [], key: 'b', reactId: '/b+"bbb"', sortIndex: 0, value: 'bbb' }, + { errors: [], key: '123', reactId: '/123+123', sortIndex: 1, value: 123 }, ], parent: { 123: 123, a: { b: { 123: 123, c: 'ccc', d: 'ddd' } }, b: 'bbb' }, }, { items: [ { + errors: [], key: 'b', - reactId: 'a, b+{"123":123,"c":"ccc","d":"ddd"}', + reactId: '/a/b+{"123":123,"c":"ccc","d":"ddd"}', sortIndex: 0, value: { 123: 123, @@ -56,20 +59,23 @@ export const columns2 = [ { items: [ { + errors: [], key: '123', - reactId: 'a, b, 123+123', + reactId: '/a/b/123+123', sortIndex: 0, value: 123, }, { + errors: [], key: 'd', - reactId: 'a, b, d+"ddd"', + reactId: '/a/b/d+"ddd"', sortIndex: 2, value: 'ddd', }, { + errors: [], key: 'c', - reactId: 'a, b, c+"ccc"', + reactId: '/a/b/c+"ccc"', sortIndex: 3, value: 'ccc', }, @@ -82,8 +88,8 @@ export const columns2 = [ }, ] -export const sortOrder1 = { a: 1, 123: 2, b: 3 } +export const sortOrder1 = { '/a': 1, '/123': 2, '/b': 3 } -export const sortOrder2 = { 123: 1, 'a, b, c': 3, 'a, b, d': 2 } +export const sortOrder2 = { '/123': 1, '/a/b/c': 3, '/a/b/d': 2 } export const object = { a: { b: { c: 'ccc', d: 'ddd', 123: 123 } }, b: 'bbb', 123: 123 } diff --git a/catalog/app/components/MetadataEditor/MetadataEditor.tsx b/catalog/app/components/MetadataEditor/MetadataEditor.tsx index 1dd6aacca7e..c8d0f1f9576 100644 --- a/catalog/app/components/MetadataEditor/MetadataEditor.tsx +++ b/catalog/app/components/MetadataEditor/MetadataEditor.tsx @@ -1,11 +1,11 @@ -import Ajv, { ErrorObject } from 'ajv' +import Ajv from 'ajv' import brace from 'brace' import { JsonEditor as ReactJsonEditor } from 'jsoneditor-react' import * as React from 'react' import * as M from '@material-ui/core' import JsonEditor from 'components/JsonEditor' -import { JsonValue } from 'components/JsonEditor/constants' +import { JsonValue, ValidationErrors } from 'components/JsonEditor/constants' import JsonValidationErrors from 'components/JsonValidationErrors' import { JsonSchema, makeSchemaValidator } from 'utils/json-schema' @@ -47,7 +47,7 @@ export default function MetadataEditor({ }: MetadataEditorProps) { const classes = useStyles() const schemaValidator = React.useMemo(() => makeSchemaValidator(schema), [schema]) - const [errors, setErrors] = React.useState<(Error | ErrorObject)[]>(() => + const [errors, setErrors] = React.useState(() => schemaValidator(value), ) @@ -79,10 +79,11 @@ export default function MetadataEditor({ /> ) : ( )} diff --git a/catalog/app/containers/Bucket/PackageDialog/MetaInput.tsx b/catalog/app/containers/Bucket/PackageDialog/MetaInput.tsx index 95691f53d7d..d178381b4ca 100644 --- a/catalog/app/containers/Bucket/PackageDialog/MetaInput.tsx +++ b/catalog/app/containers/Bucket/PackageDialog/MetaInput.tsx @@ -8,7 +8,7 @@ import { fade } from '@material-ui/core/styles' import * as M from '@material-ui/core' import JsonEditor from 'components/JsonEditor' -import { JsonValue } from 'components/JsonEditor/constants' +import { JsonValue, ValidationErrors } from 'components/JsonEditor/constants' import JsonValidationErrors from 'components/JsonValidationErrors' import MetadataEditor from 'components/MetadataEditor' import * as Notifications from 'containers/Notifications' @@ -230,7 +230,8 @@ export const MetaInput = React.forwardRef( ref, ) { const classes = useMetaInputStyles() - const error = schemaError || ((meta.modified || meta.submitFailed) && meta.error) + const errors: ValidationErrors = + schemaError || ((meta.modified || meta.submitFailed) && meta.error) const disabled = meta.submitting || meta.submitSucceeded const [open, setOpen] = React.useState(false) @@ -318,7 +319,7 @@ export const MetaInput = React.forwardRef(
{/* eslint-disable-next-line no-nested-ternary */} - + Metadata ( value={value} onChange={onChangeInline} schema={schema} + errors={errors} />
- +
{locked && ( diff --git a/catalog/app/containers/Bucket/Queries/QueryViewer.tsx b/catalog/app/containers/Bucket/Queries/QueryViewer.tsx index 7db2b188f1b..ed463a1ed39 100644 --- a/catalog/app/containers/Bucket/Queries/QueryViewer.tsx +++ b/catalog/app/containers/Bucket/Queries/QueryViewer.tsx @@ -1,6 +1,6 @@ import Ajv from 'ajv' import brace from 'brace' -import { JsonEditor } from 'jsoneditor-react' +import { JsonEditor as ReactJsonEditor } from 'jsoneditor-react' import * as React from 'react' import * as M from '@material-ui/core' import * as Lab from '@material-ui/lab' @@ -100,7 +100,7 @@ export default function QueryViewer({ Query body - ({ a: '123', b: 123, }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [errors, setErrors] = React.useState(() => validate(value)) const onChange = React.useCallback((json) => { setErrors(validate(json)) @@ -30,7 +27,7 @@ export default function JsonEditorBook() { return ( - + ) diff --git a/catalog/app/utils/reactTools.tsx b/catalog/app/utils/reactTools.tsx index 1f88d244582..629cb5d0ee9 100644 --- a/catalog/app/utils/reactTools.tsx +++ b/catalog/app/utils/reactTools.tsx @@ -30,3 +30,26 @@ export const mkLazy = ( ) } + +/** + * TODO: add keys, add tests, remove extra fragments + * Acts similar to Array#join: + * `RT.join([, , ], ) -> ` + */ +export const join = ( + list: React.ReactNode[], + separator: React.ReactNode, +): React.ReactNode => { + if (list.length < 2) return <>{list} + return ( + <> + {list.reduce((acc, item) => ( + <> + {item} + {separator} + {acc} + + ))} + + ) +}