Skip to content

Commit

Permalink
JSON editor Ajv errors (#2597)
Browse files Browse the repository at this point in the history
  • Loading branch information
fiskus committed Jan 23, 2022
1 parent 2345aaa commit 8c2d211
Show file tree
Hide file tree
Showing 17 changed files with 304 additions and 129 deletions.
2 changes: 2 additions & 0 deletions catalog/app/components/JsonEditor/AddRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const emptyKeyProps = {
row: {
original: {
address: [],
errors: [],
required: false,
sortIndex: -1,
type: 'undefined',
Expand All @@ -51,6 +52,7 @@ const emptyValueProps = {
row: {
original: {
address: [],
errors: [],
required: false,
sortIndex: -1,
type: 'undefined',
Expand Down
7 changes: 4 additions & 3 deletions catalog/app/components/JsonEditor/JsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -194,6 +194,7 @@ interface StateRenderProps {
interface JsonEditorWrapperProps {
className?: string
disabled?: boolean
errors: ValidationErrors
multiColumned?: boolean
onChange: (value: JsonValue) => void
schema?: JsonSchema
Expand All @@ -202,13 +203,13 @@ interface JsonEditorWrapperProps {

export default React.forwardRef<HTMLDivElement, JsonEditorWrapperProps>(
function JsonEditorWrapper(
{ className, disabled, multiColumned, onChange, schema: optSchema, value },
{ className, disabled, errors, multiColumned, onChange, schema: optSchema, value },
ref,
) {
const schema = optSchema || EMPTY_SCHEMA

return (
<State jsonObject={value} schema={schema}>
<State errors={errors} jsonObject={value} schema={schema}>
{(stateProps: StateRenderProps) => (
<JsonEditor
{...{
Expand Down
37 changes: 29 additions & 8 deletions catalog/app/components/JsonEditor/Note.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ import {
doesTypeMatchSchema,
schemaTypeToHumanString,
} from 'utils/json-schema'
import * as RT from 'utils/reactTools'

import { JsonValue, COLUMN_IDS, EMPTY_VALUE, RowData } from './constants'
import {
JsonValue,
COLUMN_IDS,
EMPTY_VALUE,
RowData,
ValidationErrors,
} from './constants'

const useStyles = M.makeStyles((t) => ({
default: {
Expand All @@ -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),
<br />,
),
[errors],
)

if (humanReadableSchema === 'undefined')
return <>Key/value is not restricted by schema</>

const type = `${mismatch ? 'Required type' : 'Type'}: ${humanReadableSchema}`

return (
<div>
{mismatch ? 'Required type: ' : 'Type: '}
{humanReadableSchema}
{validationError || type}
{!!schema?.description && <p>Description: {schema.description}</p>}
</div>
)
}

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)
Expand All @@ -58,13 +77,15 @@ function NoteValue({ schema, value }: NoteValueProps) {
if (!humanReadableSchema || humanReadableSchema === 'undefined') return null

return (
<M.Tooltip title={<TypeHelp {...{ humanReadableSchema, mismatch, schema }} />}>
<M.Tooltip
title={<TypeHelp {...{ errors, humanReadableSchema, mismatch, schema }} />}
>
<span
className={cx(classes.default, {
[classes.mismatch]: mismatch,
})}
>
{mismatch ? <M.Icon>error_outlined</M.Icon> : <M.Icon>info_outlined</M.Icon>}
{<M.Icon>info_outlined</M.Icon>}
</span>
</M.Tooltip>
)
Expand All @@ -78,7 +99,7 @@ interface NoteProps {

export default function Note({ columnId, data, value }: NoteProps) {
if (columnId === COLUMN_IDS.VALUE) {
return <NoteValue value={value} schema={data.valueSchema} />
return <NoteValue errors={data.errors} schema={data.valueSchema} value={value} />
}

return null
Expand Down
4 changes: 4 additions & 0 deletions catalog/app/components/JsonEditor/Row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')]: {
Expand Down Expand Up @@ -41,6 +44,7 @@ export default function Row({ cells, columnPath, fresh, onExpand, onRemove }: Ro
<M.TableCell
{...cell.getCellProps()}
className={cx(classes.cell, {
[classes.error]: cell.row.original.errors.length,
[classes.key]: cell.column.id === COLUMN_IDS.KEY,
[classes.value]: cell.column.id === COLUMN_IDS.VALUE,
})}
Expand Down
32 changes: 20 additions & 12 deletions catalog/app/components/JsonEditor/State.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import * as dateFns from 'date-fns'
import * as FP from 'fp-ts'
import * as R from 'ramda'
import * as React from 'react'

import pipeThru from 'utils/pipeThru'

import { COLUMN_IDS, EMPTY_VALUE } from './constants'

const serializeAddress = (addressPath) => addressPath.join(', ')
const serializeAddress = (addressPath) => `/${addressPath.join('/')}`

const getAddressPath = (key, parentPath) =>
key === '' ? parentPath : (parentPath || []).concat(key)
Expand Down Expand Up @@ -124,16 +123,22 @@ 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)
const value = storedValue === undefined ? getDefaultValue(item) : storedValue
return {
[COLUMN_IDS.KEY]: key,
[COLUMN_IDS.VALUE]: value,
errors,
reactId: calcReactId(valuePath, storedValue),
sortIndex: (item && item.sortIndex) || sortOrder.current.dict[itemAddress] || 0,
...(item || {}),
Expand Down Expand Up @@ -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,
Expand All @@ -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),
Expand All @@ -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([])
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion catalog/app/components/JsonEditor/State.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions catalog/app/components/JsonEditor/constants.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
36 changes: 18 additions & 18 deletions catalog/app/components/JsonEditor/mocks/booleans-nulls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
}

Expand All @@ -54,7 +54,7 @@ export const columns = [
items: [
{
key: 'nullValue',
reactId: 'nullValue+undefined',
reactId: '/nullValue+undefined',
address: ['nullValue'],
required: false,
valueSchema: {
Expand All @@ -66,7 +66,7 @@ export const columns = [
},
{
key: 'boolValue',
reactId: 'boolValue+undefined',
reactId: '/boolValue+undefined',
address: ['boolValue'],
required: false,
valueSchema: {
Expand All @@ -78,7 +78,7 @@ export const columns = [
},
{
key: 'enumBool',
reactId: 'enumBool+undefined',
reactId: '/enumBool+undefined',
address: ['enumBool'],
required: false,
valueSchema: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const schema = {
}

export const jsonDict = {
longNestedList: {
'/longNestedList': {
address: ['longNestedList'],
required: false,
valueSchema: {
Expand All @@ -60,7 +60,12 @@ export const jsonDict = {
type: 'array',
items: {
type: 'array',
items: { type: 'array', items: { type: 'number' } },
items: {
type: 'array',
items: {
type: 'number',
},
},
},
},
},
Expand Down
Loading

0 comments on commit 8c2d211

Please sign in to comment.