diff --git a/src/lib/unstable/core/Entity/Entity.tsx b/src/lib/unstable/core/Entity/Entity.tsx new file mode 100644 index 00000000..5aa2744b --- /dev/null +++ b/src/lib/unstable/core/Entity/Entity.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +import {SchemaRendererContext} from '../SchemaRendererContext'; +import type {JsonSchema} from '../types'; +import {useSchemaRendererField} from '../useSchemaRendererField'; + +import {getRenderKit} from './utils'; + +export interface EntityProps { + name: string; + schema: JsonSchema; +} + +const EntityComponent: React.FC = ({name, schema}) => { + const {config, mode} = React.useContext(SchemaRendererContext); + + const renderKit = React.useMemo( + () => getRenderKit({config, mode, schema}), + [config, mode, schema], + ); + + const options = React.useMemo( + () => ({ + data: {schema}, + defaultValue: schema.default, + subscription: { + error: true, + submitFailed: true, + touched: true, + validating: true, + value: true, + }, + }), + [schema], + ); + + const field = useSchemaRendererField(name, options); + + if (!renderKit.View) { + return null; + } + + let content = null; + + if (renderKit.independent) { + content = ( + + ); + } else { + content = ; + + if (renderKit.Wrapper) { + content = ( + + {content} + + ); + } + } + + return
{content}
; +}; + +export const Entity = React.memo(EntityComponent); diff --git a/src/lib/unstable/core/components/Entity/index.ts b/src/lib/unstable/core/Entity/index.ts similarity index 100% rename from src/lib/unstable/core/components/Entity/index.ts rename to src/lib/unstable/core/Entity/index.ts diff --git a/src/lib/unstable/core/Entity/types.ts b/src/lib/unstable/core/Entity/types.ts new file mode 100644 index 00000000..2d597c8c --- /dev/null +++ b/src/lib/unstable/core/Entity/types.ts @@ -0,0 +1,30 @@ +import type {SchemaRendererMode} from '../constants'; +import type { + IndependentView, + JsonSchema, + SchemaRendererConfig, + SimpleView, + Wrapper, +} from '../types'; + +export type GetRenderKitParams = { + config: SchemaRendererConfig; + mode: SchemaRendererMode; + schema: Schema; +}; + +export type GetRenderKitReturn = + | { + View: SimpleView | undefined; + Wrapper: Wrapper | undefined; + independent: false | undefined; + viewProps: Record; + wrapperProps: Record; + } + | { + View: IndependentView; + Wrapper: Wrapper | undefined; + independent: true; + viewProps: Record; + wrapperProps: Record; + }; diff --git a/src/lib/unstable/core/Entity/utils.ts b/src/lib/unstable/core/Entity/utils.ts new file mode 100644 index 00000000..eff211f3 --- /dev/null +++ b/src/lib/unstable/core/Entity/utils.ts @@ -0,0 +1,39 @@ +import get from 'lodash/get'; + +import {EMPTY_OBJECT} from '../constants'; +import type {JsonSchema, View, Wrapper} from '../types'; + +import type {GetRenderKitParams, GetRenderKitReturn} from './types'; + +export const getRenderKit = ({ + config, + mode, + schema, +}: GetRenderKitParams): GetRenderKitReturn => { + const viewType: string | undefined = get(schema, 'entityParameters.viewType'); + const wrapperType: string | undefined = get(schema, 'entityParameters.wrapperType'); + + const viewProps = get(schema, 'entityParameters.viewProps', EMPTY_OBJECT); + const wrapperProps = get(schema, 'entityParameters.wrapperProps', EMPTY_OBJECT); + + const ViewComponent: View | undefined = get( + config, + `${schema.type}.views.${viewType}.${mode}.Component`, + ); + const WrapperComponent: Wrapper | undefined = get( + config, + `${schema.type}.wrappers.${wrapperType}`, + ); + const independent: boolean | undefined = get( + config, + `${schema.type}.views.${viewType}.${mode}.independent`, + ); + + return { + View: ViewComponent, + viewProps, + Wrapper: WrapperComponent, + wrapperProps, + independent, + }; +}; diff --git a/src/lib/unstable/core/SchemaRenderer/SchemaRenderer.tsx b/src/lib/unstable/core/SchemaRenderer/SchemaRenderer.tsx new file mode 100644 index 00000000..5e6a1892 --- /dev/null +++ b/src/lib/unstable/core/SchemaRenderer/SchemaRenderer.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import {Entity} from '../Entity'; +import {SchemaRendererContext, type SchemaRendererContextType} from '../SchemaRendererContext'; +import {SchemaRendererServiceField} from '../SchemaRendererServiceField'; +import type {SchemaRendererMode} from '../constants'; +import type {ErrorMessages, JsonSchema, SchemaRendererConfig} from '../types'; + +export interface SchemaRendererProps { + config: SchemaRendererConfig; + errorMessages?: ErrorMessages; + mode: SchemaRendererMode; + name: string; + schema: JsonSchema; +} + +const SchemaRendererComponent: React.FC = ({ + config, + errorMessages, + mode, + name, + schema, +}) => { + const serviceFieldName = `DYNAMIC_FORMS_SERVICE_FIELD/${name}`; + + const context: SchemaRendererContextType = React.useMemo( + () => ({ + config, + mode, + name, + schema, + serviceFieldName, + }), + [config, mode, name, schema, serviceFieldName], + ); + + return ( + + + + + ); +}; + +export const SchemaRenderer = React.memo(SchemaRendererComponent); diff --git a/src/lib/unstable/core/components/SchemaRenderer/index.ts b/src/lib/unstable/core/SchemaRenderer/index.ts similarity index 55% rename from src/lib/unstable/core/components/SchemaRenderer/index.ts rename to src/lib/unstable/core/SchemaRenderer/index.ts index 58f7aa13..a78a1425 100644 --- a/src/lib/unstable/core/components/SchemaRenderer/index.ts +++ b/src/lib/unstable/core/SchemaRenderer/index.ts @@ -1,2 +1 @@ export * from './SchemaRenderer'; -export * from './context'; diff --git a/src/lib/unstable/core/components/SchemaRenderer/context.ts b/src/lib/unstable/core/SchemaRendererContext/SchemaRendererContext.ts similarity index 73% rename from src/lib/unstable/core/components/SchemaRenderer/context.ts rename to src/lib/unstable/core/SchemaRendererContext/SchemaRendererContext.ts index 81fa4c33..83335032 100644 --- a/src/lib/unstable/core/components/SchemaRenderer/context.ts +++ b/src/lib/unstable/core/SchemaRendererContext/SchemaRendererContext.ts @@ -1,6 +1,6 @@ import React from 'react'; -import type {SchemaRendererContextType} from '../../types'; +import type {SchemaRendererContextType} from './types'; export const SchemaRendererContext = React.createContext( null as unknown as SchemaRendererContextType, diff --git a/src/lib/unstable/core/SchemaRendererContext/index.ts b/src/lib/unstable/core/SchemaRendererContext/index.ts new file mode 100644 index 00000000..9e2483b2 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererContext/index.ts @@ -0,0 +1,2 @@ +export * from './SchemaRendererContext'; +export * from './types'; diff --git a/src/lib/unstable/core/SchemaRendererContext/types.ts b/src/lib/unstable/core/SchemaRendererContext/types.ts new file mode 100644 index 00000000..fc8e074c --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererContext/types.ts @@ -0,0 +1,10 @@ +import type {SchemaRendererMode} from '../constants'; +import type {JsonSchema, SchemaRendererConfig} from '../types'; + +export interface SchemaRendererContextType { + config: SchemaRendererConfig; + mode: SchemaRendererMode; + name: string; + schema: JsonSchema; + serviceFieldName: string; +} diff --git a/src/lib/unstable/core/SchemaRendererServiceField/SchemaRendererServiceField.tsx b/src/lib/unstable/core/SchemaRendererServiceField/SchemaRendererServiceField.tsx new file mode 100644 index 00000000..36388315 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/SchemaRendererServiceField.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import {useField, useForm} from 'react-final-form'; + +import type {ErrorMessages, JsonSchema, SchemaRendererConfig} from '../types'; + +import {getValidate} from './utils'; + +export interface SchemaRendererProps { + config: SchemaRendererConfig; + errorMessages?: ErrorMessages; + name: string; + mainSchema: JsonSchema; + serviceFieldName: string; +} + +const SchemaRendererServiceFieldComponent: React.FC = ({ + config, + errorMessages, + name, + mainSchema, + serviceFieldName, +}) => { + const {setValidationCache, setValidationWaiters} = useForm().mutators; + + const validate = React.useMemo( + () => + getValidate({ + config, + errorMessages, + name, + mainSchema, + serviceFieldName, + setValidationCache, + setValidationWaiters, + }), + [ + config, + errorMessages, + name, + mainSchema, + serviceFieldName, + setValidationCache, + setValidationWaiters, + ], + ); + + useField(serviceFieldName, {data: {schema: mainSchema}, subscription: {}, validate}); + + return null; +}; + +export const SchemaRendererServiceField = React.memo(SchemaRendererServiceFieldComponent); diff --git a/src/lib/unstable/core/SchemaRendererServiceField/index.ts b/src/lib/unstable/core/SchemaRendererServiceField/index.ts new file mode 100644 index 00000000..faf5298a --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/index.ts @@ -0,0 +1 @@ +export * from './SchemaRendererServiceField'; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/types.ts b/src/lib/unstable/core/SchemaRendererServiceField/types.ts new file mode 100644 index 00000000..354b422d --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/types.ts @@ -0,0 +1,53 @@ +import type {ErrorObject, ValidateFunction} from 'ajv'; +import type {FieldValidator} from 'final-form'; + +import type {SetValidationCacheMutator, SetValidationWaitersMutator} from '../mutators'; +import type { + ErrorMessages, + FieldValue, + JsonSchema, + SchemaRendererConfig, + SchemaToValueType, + Validator, +} from '../types'; + +export type EntityParametersError = ErrorObject< + 'entityParameters', + { + schema: JsonSchema; + validator: Validator; + value: FieldValue | null | undefined; + } +>; + +export type GetAjvValidateParams = { + config: SchemaRendererConfig; + mainSchema: JsonSchema; +}; + +export interface GetAjvValidateReturn extends ValidateFunction { + errors?: (ErrorObject | EntityParametersError)[]; +} + +export interface GetAjvErrorMessageParams { + ajvErrorMessage?: string; + errorMessages?: ErrorMessages; + instancePath: string; + keyword: string; + mainSchema: JsonSchema; + schemaPath: string; +} + +export type GetValidateParams = { + config: SchemaRendererConfig; + errorMessages?: ErrorMessages; + name: string; + mainSchema: Schema; + serviceFieldName: string; + setValidationCache: SetValidationCacheMutator; + setValidationWaiters: SetValidationWaitersMutator; +}; + +export type GetValidateReturn = FieldValidator< + SchemaToValueType +>; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils.ts new file mode 100644 index 00000000..3bbe3224 --- /dev/null +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils.ts @@ -0,0 +1,272 @@ +import type {ErrorObject, FuncKeywordDefinition, SchemaValidateFunction} from 'ajv'; +import Ajv from 'ajv'; +import get from 'lodash/get'; +import isEqual from 'lodash/isEqual'; +import isString from 'lodash/isString'; +import omit from 'lodash/omit'; +import set from 'lodash/set'; + +import {ARRAY_AND_OBJECT_ERRORS, EMPTY_OBJECT, JsonSchemaType} from '../constants'; +import type {ValidationState, ValidationWaiter} from '../mutators'; +import type {FieldValue, JsonSchema, ObjectValue, SyncValidateError, Validator} from '../types'; +import {parseFinalFormPath} from '../utils'; + +import type { + EntityParametersError, + GetAjvErrorMessageParams, + GetAjvValidateParams, + GetAjvValidateReturn, + GetValidateParams, + GetValidateReturn, +} from './types'; + +export const getAjvValidate = ({ + config, + mainSchema, +}: GetAjvValidateParams): GetAjvValidateReturn => { + function entityParametersValidate(_: any, value: FieldValue, schema?: JsonSchema) { + if (schema) { + const validatorType: string | undefined = get(schema, 'entityParameters.validatorType'); + const validator: Validator | undefined = get( + config, + `${schema.type}.validators.${validatorType}`, + ); + + if (validator) { + const error: Partial = { + keyword: 'entityParameters', + message: '', + params: {validator, value, schema}, + }; + + (entityParametersValidate as SchemaValidateFunction).errors = [error]; + + return false; + } + } + + return true; + } + + const ajv = new Ajv({ + allErrors: true, + allowMatchingProperties: true, + keywords: [ + { + errors: true, + keyword: 'entityParameters', + validate: entityParametersValidate as FuncKeywordDefinition['validate'], + }, + ], + }); + const ajvValidate = ajv.compile(mainSchema) as GetAjvValidateReturn; + + return ajvValidate; +}; + +const parseSchemaPath = (schemaPath: string): string[] => { + return decodeURIComponent(schemaPath) + .slice('#/'.length) + .split('/') + .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~')) + .slice(0, -1); +}; + +const parseInstancePath = (instancePath: string): string[] => { + return instancePath + .slice('/'.length) + .split('/') + .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~')); +}; + +const getSchemaBySchemaPath = ( + schemaPath: string, + mainSchema: JsonSchema, +): JsonSchema | undefined => { + const pathArr = parseSchemaPath(schemaPath); + + return pathArr.length ? get(mainSchema, pathArr) : mainSchema; +}; + +const getSchemaByInstancePath = ( + instancePath: string, + mainSchema: JsonSchema, +): JsonSchema | undefined => { + if (instancePath.length) { + return parseInstancePath(instancePath).reduce((acc: JsonSchema | undefined, segment) => { + const type = get(acc, 'type'); + + if (type === JsonSchemaType.Object) { + return get(acc, `properties.${segment}`); + } else if (type === JsonSchemaType.Array) { + const items = get(acc, 'items'); + + if (Array.isArray(items)) { + return get(items, `[${segment}]`); + } + + return items; + } + + return undefined; + }, mainSchema); + } + + return mainSchema; +}; + +const getAjvErrorMessage = ({ + ajvErrorMessage, + errorMessages = EMPTY_OBJECT, + instancePath, + keyword, + mainSchema, + schemaPath, +}: GetAjvErrorMessageParams): SyncValidateError => { + const propertyName = instancePath.split('/').pop() as string; + + const getErrorMessageBySchema = (schema: JsonSchema | undefined) => { + const errorOrMap: Record | string | undefined = get( + schema, + `entityParameters.errorMessages.${keyword}`, + ); + const message: string | undefined = isString(errorOrMap) + ? errorOrMap + : get(errorOrMap, propertyName); + + return message; + }; + + return ( + getErrorMessageBySchema(getSchemaBySchemaPath(schemaPath, mainSchema)) || + getErrorMessageBySchema(getSchemaByInstancePath(instancePath, mainSchema)) || + errorMessages[keyword as keyof typeof errorMessages] || + ajvErrorMessage + ); +}; + +export const getValidate = ({ + config, + errorMessages, + name, + mainSchema, + serviceFieldName, + setValidationCache, + setValidationWaiters, +}: GetValidateParams): GetValidateReturn => { + const ajvValidate = getAjvValidate({config, mainSchema}); + + return (_value, allValues, meta) => { + ajvValidate(get(allValues, name)); + + if (!ajvValidate.errors?.length) { + return false; + } + + const waiters: Record = {}; + const ajvErrors: {instancePath: string; error: SyncValidateError}[] = []; + const entityParametersErrors: {instancePath: string; error: SyncValidateError}[] = []; + + ajvValidate.errors.forEach((ajvOrEntityParametersError) => { + if (ajvOrEntityParametersError.keyword === 'entityParameters') { + const entityParametersError = ajvOrEntityParametersError as EntityParametersError; + const validationState: ValidationState | undefined = meta?.data; + + const waiter = validationState?.waiters?.[entityParametersError.instancePath]; + const cache = validationState?.cache?.[entityParametersError.instancePath]; + const cacheItem = cache?.find((item) => + isEqual(entityParametersError.params, omit(item, 'result')), + ); + + if (cacheItem?.result) { + entityParametersErrors.push({ + instancePath: entityParametersError.instancePath, + error: cacheItem.result, + }); + } else if (!waiter || !isEqual(entityParametersError.params, waiter)) { + const errorOrPromise = entityParametersError.params.validator( + entityParametersError.params.value, + allValues as ObjectValue, + ); + + if (errorOrPromise instanceof Promise) { + waiters[entityParametersError.instancePath] = entityParametersError.params; + + errorOrPromise.then((result) => { + setValidationCache({ + name: serviceFieldName, + cache: { + [entityParametersError.instancePath]: { + ...entityParametersError.params, + result, + }, + }, + }); + }); + } else { + entityParametersErrors.push({ + instancePath: entityParametersError.instancePath, + error: errorOrPromise, + }); + } + } + } else { + const ajvError = ajvOrEntityParametersError as ErrorObject; + + let instancePath = ajvError.instancePath; + let keyword = ajvError.keyword; + let schemaPath = ajvError.schemaPath; + + if (keyword === 'required' || keyword === 'dependencies') { + instancePath += `/${ajvError.params.missingProperty}`; + } else if (keyword === 'if') { + keyword = ajvError.params.failingKeyword; + schemaPath = schemaPath.slice(0, -'if'.length) + ajvError.params.failingKeyword; + } + + ajvErrors.push({ + instancePath, + error: getAjvErrorMessage({ + ajvErrorMessage: ajvError.message, + errorMessages, + instancePath, + keyword, + mainSchema, + schemaPath, + }), + }); + } + }); + + if (Object.keys(waiters).length) { + setValidationWaiters({name: serviceFieldName, waiters}); + } + + const result = {[ARRAY_AND_OBJECT_ERRORS]: {}}; + + [...ajvErrors, ...entityParametersErrors].forEach((item) => { + if (item.error) { + const itemSchema = getSchemaByInstancePath(item.instancePath, mainSchema); + + if (itemSchema) { + const arrayOrObjectSchema = + itemSchema.type === JsonSchemaType.Array || + itemSchema.type === JsonSchemaType.Object; + + const path = [ + ...parseFinalFormPath(name), + ...parseInstancePath(item.instancePath), + ]; + + set( + arrayOrObjectSchema ? result[ARRAY_AND_OBJECT_ERRORS] : result, + path, + item.error, + ); + } + } + }); + + return result; + }; +}; diff --git a/src/lib/unstable/core/components/Entity/Entity.tsx b/src/lib/unstable/core/components/Entity/Entity.tsx deleted file mode 100644 index 15c69f09..00000000 --- a/src/lib/unstable/core/components/Entity/Entity.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; - -import {useField} from 'react-final-form'; - -import type {JsonSchema} from '../../types'; -import {SchemaRendererContext} from '../SchemaRenderer/context'; - -import {getRenderKit, getValidate} from './utils'; - -export interface EntityProps { - name: string; - schema: JsonSchema; -} - -const Component: React.FC = ({name, schema}) => { - const { - config, - errorMessages, - mode, - name: headName, - tools: {setValidationCache, setValidationWaiters}, - } = React.useContext(SchemaRendererContext); - - const head = headName === name; - - const renderKit = React.useMemo( - () => getRenderKit({config, mode, schema}), - [config, mode, schema], - ); - - const options = React.useMemo(() => { - const validate = head - ? getValidate({ - config, - errorMessages, - name, - schema, - setValidationCache, - setValidationWaiters, - }) - : undefined; - - return { - data: {schema}, - defaultValue: schema.default, - subscription: {data: true, error: true, validating: true, value: true}, - validate, - }; - }, [config, errorMessages, head, name, schema, setValidationCache, setValidationWaiters]); - - const field = useField(name, options); - - if (!renderKit.View) { - return null; - } - - let content = null; - - if (renderKit.independent) { - content = ( - - ); - } else { - content = ; - - if (renderKit.Wrapper) { - content = ( - - {content} - - ); - } - } - - return
{content}
; -}; - -export const Entity = React.memo(Component); diff --git a/src/lib/unstable/core/components/Entity/types.ts b/src/lib/unstable/core/components/Entity/types.ts deleted file mode 100644 index c7f28350..00000000 --- a/src/lib/unstable/core/components/Entity/types.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type {ErrorObject, ValidateFunction} from 'ajv'; -import type {FieldValidator} from 'final-form'; - -import type {SchemaRendererMode} from '../../constants'; -import type { - ErrorMessages, - FieldValue, - IndependentView, - JsonSchema, - SchemaRendererConfig, - SchemaToValueType, - SetValidationCacheMutator, - SetValidationWaitersMutator, - SimpleView, - Validator, - Wrapper, -} from '../../types'; - -/** - * getRenderKit types start - */ - -export type GetRenderKitParams = { - config: SchemaRendererConfig; - mode: SchemaRendererMode; - schema: Schema; -}; - -export type GetRenderKitReturn = - | { - View: SimpleView | undefined; - Wrapper: Wrapper | undefined; - independent: false | undefined; - viewProps: Record; - wrapperProps: Record; - } - | { - View: IndependentView; - Wrapper: Wrapper | undefined; - independent: true; - viewProps: Record; - wrapperProps: Record; - }; - -/** - * getRenderKit types end - */ - -/** - * getAjvValidate types start - */ - -export type EntityParametersError = ErrorObject< - 'entityParameters', - { - schema: JsonSchema; - validator: Validator; - value: FieldValue | null | undefined; - } ->; - -export type GetAjvValidateParams = { - config: SchemaRendererConfig; - schema: JsonSchema; -}; - -export interface GetAjvValidateReturn extends ValidateFunction { - errors?: (ErrorObject | EntityParametersError)[]; -} - -/** - * getAjvValidate types end - */ - -/** - * getError types start - */ - -export type GetErrorParams = { - errorMessages: Record; - instancePath: string; - keyword: string; - schema: JsonSchema; - schemaPath: string; -}; - -/** - * getError types end - */ -/** - * getValidate types start - */ - -export type GetValidateParams = { - config: SchemaRendererConfig; - errorMessages: ErrorMessages; - name: string; - schema: Schema; - setValidationCache: SetValidationCacheMutator; - setValidationWaiters: SetValidationWaitersMutator; -}; - -export type GetValidateReturn = FieldValidator< - SchemaToValueType ->; - -/** - * getValidate types end - */ diff --git a/src/lib/unstable/core/components/Entity/utils.ts b/src/lib/unstable/core/components/Entity/utils.ts deleted file mode 100644 index 7a0adce4..00000000 --- a/src/lib/unstable/core/components/Entity/utils.ts +++ /dev/null @@ -1,305 +0,0 @@ -import type {ErrorObject, FuncKeywordDefinition, SchemaValidateFunction} from 'ajv'; -import Ajv from 'ajv'; -import {ARRAY_ERROR} from 'final-form'; -import get from 'lodash/get'; -import isEqual from 'lodash/isEqual'; -import isString from 'lodash/isString'; -import omit from 'lodash/omit'; -import set from 'lodash/set'; - -import {EMPTY_OBJECT, JsonSchemaType} from '../../constants'; -import type { - FieldValue, - JsonSchema, - ObjectValue, - SyncValidateError, - ValidationState, - ValidationWaiter, - Validator, - View, - Wrapper, -} from '../../types'; - -import type { - EntityParametersError, - GetAjvValidateParams, - GetAjvValidateReturn, - GetRenderKitParams, - GetRenderKitReturn, - GetValidateParams, - GetValidateReturn, -} from './types'; - -export const getRenderKit = ({ - config, - mode, - schema, -}: GetRenderKitParams): GetRenderKitReturn => { - const viewType: string | undefined = get(schema, 'entityParameters.viewType'); - const wrapperType: string | undefined = get(schema, 'entityParameters.wrapperType'); - - const viewProps = get(schema, 'entityParameters.viewProps', EMPTY_OBJECT); - const wrapperProps = get(schema, 'entityParameters.wrapperProps', EMPTY_OBJECT); - - const ViewComponent: View | undefined = get( - config, - `${schema.type}.views.${viewType}.${mode}.Component`, - ); - const WrapperComponent: Wrapper | undefined = get( - config, - `${schema.type}.wrappers.${wrapperType}`, - ); - const independent: boolean | undefined = get( - config, - `${schema.type}.views.${viewType}.${mode}.independent`, - ); - - return { - View: ViewComponent, - viewProps, - Wrapper: WrapperComponent, - wrapperProps, - independent, - }; -}; - -export const getAjvValidate = ({ - config, - schema: mainSchema, -}: GetAjvValidateParams): GetAjvValidateReturn => { - function entityParametersValidate(_: any, value: FieldValue, schema?: JsonSchema) { - if (schema) { - const validatorType: string | undefined = get(schema, 'entityParameters.validatorType'); - const validator: Validator | undefined = get( - config, - `${schema.type}.validators.${validatorType}`, - ); - - if (validator) { - const error: Partial = { - keyword: 'entityParameters', - message: '', - params: {validator, value, schema}, - }; - - (entityParametersValidate as SchemaValidateFunction).errors = [error]; - - return false; - } - } - - return true; - } - - const ajv = new Ajv({ - allErrors: true, - allowMatchingProperties: true, - keywords: [ - { - errors: true, - keyword: 'entityParameters', - validate: entityParametersValidate as FuncKeywordDefinition['validate'], - }, - ], - }); - const ajvValidate = ajv.compile(mainSchema) as GetAjvValidateReturn; - - return ajvValidate; -}; - -const getSchemaBySchemaPath = ( - schemaPath: string, - keyword: string, - mainSchema: JsonSchema, -): JsonSchema | undefined => { - const sp = decodeURIComponent(schemaPath); - let result = mainSchema; - - if (sp !== `#/${keyword}`) { - const pathArr = sp.substring('#/'.length, sp.length - `/${keyword}`.length).split('/'); - - result = get(mainSchema, pathArr); - } - - return result; -}; - -const getSchemaByInstancePath = ( - instancePath: string, - mainSchema: JsonSchema, -): JsonSchema | undefined => { - let result: JsonSchema | undefined = mainSchema; - - if (instancePath.length) { - const pathArr = instancePath.substring('/'.length).split('/'); - - result = pathArr.reduce((acc: JsonSchema | undefined, subpath) => { - const type = get(acc, 'type'); - - if (type === JsonSchemaType.Object) { - return get(acc, `properties.${subpath}`); - } else if (type === JsonSchemaType.Array) { - const items = get(acc, 'items'); - - if (Array.isArray(items)) { - return get(items, `[${subpath}]`); - } - - return items; - } - - return undefined; - }, mainSchema); - } - - return result; -}; - -const isEntityParametersError = ( - error: ErrorObject | EntityParametersError, -): error is EntityParametersError => error.keyword === 'entityParameters'; - -const getErrorMessageBySchema = (schema: JsonSchema | undefined, keyword: string, name: string) => { - const errorOrMap: Record | string | undefined = get( - schema, - `entityParameters.errorMessages.${keyword}`, - ); - const message: string | undefined = isString(errorOrMap) ? errorOrMap : get(errorOrMap, name); - - return message; -}; - -const getError = (schema: JsonSchema, message: SyncValidateError) => { - if (schema.type === JsonSchemaType.Array) { - const arrayError = set([], ARRAY_ERROR, message); - - return arrayError; - } - - if (schema.type === JsonSchemaType.Object) { - const objectError = set({}, 'FINAL_FORM/object-error', message); - - return objectError; - } - - return message; -}; - -export const getValidate = ({ - config, - errorMessages, - name, - schema, - setValidationCache, - setValidationWaiters, -}: GetValidateParams): GetValidateReturn => { - const ajvValidate = getAjvValidate({config, schema}); - - return (value, allValues, meta) => { - ajvValidate(value); - - if (!ajvValidate.errors?.length) { - return false; - } - - const waiters: Record = {}; - const errors: {instancePath: string; error: SyncValidateError}[] = []; - const priorityErrors: {instancePath: string; error: SyncValidateError}[] = []; - - ajvValidate.errors.forEach((ajvError) => { - if (isEntityParametersError(ajvError)) { - const {instancePath, params} = ajvError; - const validationState: ValidationState | undefined = meta?.data; - - const waiter = validationState?.waiters?.[instancePath]; - const cache = validationState?.cache?.[instancePath]; - const cacheItem = cache?.find((item) => isEqual(params, omit(item, 'result'))); - - if (cacheItem?.result) { - priorityErrors.push({ - instancePath: instancePath, - error: cacheItem.result, - }); - } else if (!waiter || !isEqual(params, waiter)) { - const errorOrPromise = params.validator(params.value, allValues as ObjectValue); - - if (errorOrPromise instanceof Promise) { - waiters[instancePath] = params; - - errorOrPromise.then((result) => { - setValidationCache({ - name, - cache: {[ajvError.instancePath]: {...ajvError.params, result}}, - }); - }); - } else { - priorityErrors.push({ - instancePath: instancePath, - error: errorOrPromise, - }); - } - } - } else { - let instancePath = ajvError.instancePath; - let keyword = ajvError.keyword; - let schemaPath = ajvError.schemaPath; - - if (keyword === 'required' || keyword === 'dependencies') { - instancePath += `/${ajvError.params.missingProperty}`; - } else if (keyword === 'if') { - keyword = ajvError.params.failingKeyword; - schemaPath = schemaPath.slice(0, -'if'.length) + ajvError.params.failingKeyword; - } - - const spSchema: JsonSchema | undefined = getSchemaBySchemaPath( - schemaPath, - keyword, - schema, - ); - const ipSchema: JsonSchema | undefined = getSchemaByInstancePath( - instancePath, - schema, - ); - - const propertyName = instancePath.split('/').pop() as string; - const error: string | undefined = - getErrorMessageBySchema(spSchema, keyword, propertyName) || - getErrorMessageBySchema(ipSchema, keyword, propertyName) || - errorMessages[keyword as keyof typeof errorMessages] || - ajvError.message; - - errors.push({ - instancePath, - error, - }); - } - }); - - if (Object.keys(waiters).length) { - setValidationWaiters({name, waiters: waiters}); - } - - const error = set({}, name, undefined); - - [...errors, ...priorityErrors].forEach((item) => { - if (item.error) { - const path = [ - ...(name.length ? name.split('.') : []), - ...(item.instancePath - ? item.instancePath.substring('/'.length).split('/') - : []), - ]; - - const itemSchema = getSchemaByInstancePath(item.instancePath, schema); - - if (itemSchema) { - set(error, path, getError(itemSchema, item.error)); - } - } - }); - - // сетить ошибки массивов и объектов в отдельное место в сторе формы - - return get(error, name); - }; -}; diff --git a/src/lib/unstable/core/components/SchemaRenderer/SchemaRenderer.tsx b/src/lib/unstable/core/components/SchemaRenderer/SchemaRenderer.tsx deleted file mode 100644 index 351df782..00000000 --- a/src/lib/unstable/core/components/SchemaRenderer/SchemaRenderer.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; - -import {useForm} from 'react-final-form'; - -import type {SchemaRendererMode} from '../../constants'; -import {EMPTY_OBJECT} from '../../constants'; -import type { - ErrorMessages, - JsonSchema, - SchemaRendererConfig, - SchemaRendererContextType, -} from '../../types'; -import {Entity} from '../Entity'; - -import {SchemaRendererContext} from './context'; - -export interface SchemaRendererProps { - config: SchemaRendererConfig; - errorMessages?: ErrorMessages; - mode: SchemaRendererMode; - name: string; - schema: JsonSchema; -} - -const Component: React.FC = ({ - config, - errorMessages = EMPTY_OBJECT, - mode, - name, - schema, -}) => { - const {setValidationCache, setValidationWaiters} = useForm().mutators; - - const context: SchemaRendererContextType = React.useMemo( - () => ({ - config, - errorMessages, - mode, - name, - schema, - tools: {setValidationCache, setValidationWaiters}, - }), - [config, errorMessages, mode, name, schema, setValidationCache, setValidationWaiters], - ); - - return ( - - - - ); -}; - -export const SchemaRenderer = React.memo(Component); diff --git a/src/lib/unstable/core/constants/common.ts b/src/lib/unstable/core/constants.ts similarity index 79% rename from src/lib/unstable/core/constants/common.ts rename to src/lib/unstable/core/constants.ts index d5ec5282..7d3da4bc 100644 --- a/src/lib/unstable/core/constants/common.ts +++ b/src/lib/unstable/core/constants.ts @@ -12,3 +12,5 @@ export enum JsonSchemaType { } export const EMPTY_OBJECT = {}; + +export const ARRAY_AND_OBJECT_ERRORS = 'ARRAY_AND_OBJECT_ERRORS'; diff --git a/src/lib/unstable/core/constants/index.ts b/src/lib/unstable/core/constants/index.ts deleted file mode 100644 index d0b93236..00000000 --- a/src/lib/unstable/core/constants/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './common'; diff --git a/src/lib/unstable/core/index.ts b/src/lib/unstable/core/index.ts new file mode 100644 index 00000000..e2523734 --- /dev/null +++ b/src/lib/unstable/core/index.ts @@ -0,0 +1,6 @@ +export * from './Entity'; +export * from './SchemaRenderer'; +export {JsonSchemaType, SchemaRendererMode} from './constants'; +export {schemaRendererMutators} from './mutators'; +export * from './types'; +export * from './useSchemaRendererField'; diff --git a/src/lib/unstable/core/mutators/async-validation/async-validation.ts b/src/lib/unstable/core/mutators/async-validation/async-validation.ts index fd0bb288..6a813096 100644 --- a/src/lib/unstable/core/mutators/async-validation/async-validation.ts +++ b/src/lib/unstable/core/mutators/async-validation/async-validation.ts @@ -5,7 +5,7 @@ import type { SetValidationCacheFunction, SetValidationWaitersFunction, ValidationState, -} from '../../types'; +} from './types'; export const setValidationWaiters: SetValidationWaitersFunction = ( [{name, waiters}], diff --git a/src/lib/unstable/core/mutators/async-validation/index.ts b/src/lib/unstable/core/mutators/async-validation/index.ts index 0dfe05bd..b8e9f69e 100644 --- a/src/lib/unstable/core/mutators/async-validation/index.ts +++ b/src/lib/unstable/core/mutators/async-validation/index.ts @@ -1 +1,2 @@ export * from './async-validation'; +export * from './types'; diff --git a/src/lib/unstable/core/types/mutators.ts b/src/lib/unstable/core/mutators/async-validation/types.ts similarity index 87% rename from src/lib/unstable/core/types/mutators.ts rename to src/lib/unstable/core/mutators/async-validation/types.ts index dbbdd366..aa7d6644 100644 --- a/src/lib/unstable/core/types/mutators.ts +++ b/src/lib/unstable/core/mutators/async-validation/types.ts @@ -1,12 +1,6 @@ import type {MutableState, Tools} from 'final-form'; -import type {JsonSchema} from './schema'; -import type {SyncValidateError, Validator} from './validation'; -import type {FieldValue} from './values'; - -/** - * async-validation start - */ +import type {FieldValue, JsonSchema, SyncValidateError, Validator} from '../../types'; export interface ValidationWaiter { schema: JsonSchema; @@ -62,7 +56,3 @@ export type SetValidationCacheFunction< ) => void; export type SetValidationCacheMutator = (params: SetValidationCacheParams) => void; - -/** - * async-validation end - */ diff --git a/src/lib/unstable/core/mutators/index.ts b/src/lib/unstable/core/mutators/index.ts index 88115419..fe4a8f90 100644 --- a/src/lib/unstable/core/mutators/index.ts +++ b/src/lib/unstable/core/mutators/index.ts @@ -1,3 +1,8 @@ import {setValidationCache, setValidationWaiters} from './async-validation'; -export const mutators = {setValidationCache, setValidationWaiters}; +export * from './async-validation/types'; + +export const schemaRendererMutators = { + setValidationCache, + setValidationWaiters, +}; diff --git a/src/lib/unstable/core/types/context.ts b/src/lib/unstable/core/types/context.ts deleted file mode 100644 index e775d14d..00000000 --- a/src/lib/unstable/core/types/context.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type {SchemaRendererMode} from '../constants'; - -import type {SchemaRendererConfig} from './config'; -import type {SetValidationCacheMutator, SetValidationWaitersMutator} from './mutators'; -import type {JsonSchema} from './schema'; -import type {ErrorMessages} from './validation'; - -export interface SchemaRendererContextType { - config: SchemaRendererConfig; - errorMessages: ErrorMessages; - mode: SchemaRendererMode; - name: string; - schema: JsonSchema; - tools: { - setValidationCache: SetValidationCacheMutator; - setValidationWaiters: SetValidationWaitersMutator; - }; -} diff --git a/src/lib/unstable/core/types/index.ts b/src/lib/unstable/core/types/index.ts index c81222d6..686dd49d 100644 --- a/src/lib/unstable/core/types/index.ts +++ b/src/lib/unstable/core/types/index.ts @@ -1,8 +1,6 @@ export * from './components'; export * from './config'; -export * from './context'; export * from './helpers'; -export * from './mutators'; export * from './schema'; export * from './validation'; export * from './values'; diff --git a/src/lib/unstable/core/useSchemaRendererField/index.ts b/src/lib/unstable/core/useSchemaRendererField/index.ts new file mode 100644 index 00000000..4a3e8504 --- /dev/null +++ b/src/lib/unstable/core/useSchemaRendererField/index.ts @@ -0,0 +1 @@ +export * from './useSchemaRendererField'; diff --git a/src/lib/unstable/core/useSchemaRendererField/useSchemaRendererField.ts b/src/lib/unstable/core/useSchemaRendererField/useSchemaRendererField.ts new file mode 100644 index 00000000..3b1bdf56 --- /dev/null +++ b/src/lib/unstable/core/useSchemaRendererField/useSchemaRendererField.ts @@ -0,0 +1,41 @@ +import React from 'react'; + +import get from 'lodash/get'; +import {type FieldRenderProps, type UseFieldConfig, useField} from 'react-final-form'; + +import {SchemaRendererContext} from '../SchemaRendererContext'; +import {ARRAY_AND_OBJECT_ERRORS, JsonSchemaType} from '../constants'; +import {getSchemaByFinalFormPath} from '../utils'; + +export const useSchemaRendererField = < + FieldValue = any, + T extends HTMLElement = HTMLElement, + InputValue = FieldValue, +>( + name: string, + config?: UseFieldConfig, +): FieldRenderProps => { + const { + name: headName, + schema: mainSchema, + serviceFieldName, + } = React.useContext(SchemaRendererContext); + + const field = useField(name, config); + const serviceField = useField(serviceFieldName, {subscription: {error: true}}); + + const arrayOrObjectSchema = React.useMemo(() => { + const type = getSchemaByFinalFormPath(name, headName, mainSchema)?.type; + + return type === JsonSchemaType.Array || type === JsonSchemaType.Object; + }, [headName, name, mainSchema]); + + const error = get( + arrayOrObjectSchema + ? get(serviceField.meta.error, ARRAY_AND_OBJECT_ERRORS) + : serviceField.meta.error, + name, + ); + + return error ? {...field, meta: {...field.meta, error}} : field; +}; diff --git a/src/lib/unstable/core/utils.ts b/src/lib/unstable/core/utils.ts new file mode 100644 index 00000000..5257915f --- /dev/null +++ b/src/lib/unstable/core/utils.ts @@ -0,0 +1,50 @@ +import get from 'lodash/get'; + +import {JsonSchemaType} from './constants'; +import type {JsonSchema} from './types'; + +export const parseFinalFormPath = (finalFormPath: string): string[] => { + const result: string[] = []; + const regex = /([^[.\]]+)|\[(\d+)\]/g; + let match; + + while ((match = regex.exec(finalFormPath)) !== null) { + if (match[1] !== undefined) { + result.push(match[1]); + } else if (match[2] !== undefined) { + result.push(match[2]); + } + } + + return result; +}; + +export const getSchemaByFinalFormPath = ( + finalFormPath: string, + finalFormHeadPath: string, + mainSchema: JsonSchema, +): JsonSchema | undefined => { + if (finalFormPath.length) { + return parseFinalFormPath(finalFormPath) + .slice(parseFinalFormPath(finalFormHeadPath).length) + .reduce((acc: JsonSchema | undefined, segment) => { + const type = get(acc, 'type'); + + if (type === JsonSchemaType.Object) { + return get(acc, `properties.${segment}`); + } else if (type === JsonSchemaType.Array) { + const items = get(acc, 'items'); + + if (Array.isArray(items)) { + return get(items, `[${segment}]`); + } + + return items; + } + + return undefined; + }, mainSchema); + } + + return mainSchema; +}; diff --git a/src/lib/unstable/kit/ArrayBase.tsx b/src/lib/unstable/kit/ArrayBase.tsx index 6da78be8..2b2a0c45 100644 --- a/src/lib/unstable/kit/ArrayBase.tsx +++ b/src/lib/unstable/kit/ArrayBase.tsx @@ -20,7 +20,7 @@ import {Button, Icon} from '@gravity-ui/uikit'; // transformArrIn, // } from '../../../../core'; import {block} from '../../kit/utils'; -import {Entity} from '../core/components/Entity'; +import {Entity} from '../core'; import type {JsonSchemaArray, SimpleView} from '../core/types'; import './ArrayBase.scss'; diff --git a/src/lib/unstable/kit/ObjectBase.tsx b/src/lib/unstable/kit/ObjectBase.tsx index 8b09aaa0..d227eba1 100644 --- a/src/lib/unstable/kit/ObjectBase.tsx +++ b/src/lib/unstable/kit/ObjectBase.tsx @@ -7,7 +7,7 @@ import isObjectLike from 'lodash/isObjectLike'; // import {block, filterPropertiesForObjectInline} from '../../../kit/utils'; import {block} from '../../kit/utils'; -import {Entity} from '../core/components/Entity'; +import {Entity} from '../core'; import type {IndependentView, IndependentViewProps, JsonSchemaObject} from '../core/types'; import './ObjectBase.scss'; diff --git a/src/stories/Unstable.stories.tsx b/src/stories/Unstable.stories.tsx index 067e7776..f08a2340 100644 --- a/src/stories/Unstable.stories.tsx +++ b/src/stories/Unstable.stories.tsx @@ -5,9 +5,8 @@ import {noop} from 'lodash'; import {Form} from 'react-final-form'; import {ObjectBase} from '../lib'; -import {SchemaRenderer} from '../lib/unstable/core/components/SchemaRenderer'; +import {SchemaRenderer, schemaRendererMutators} from '../lib/unstable/core'; import {JsonSchemaType, SchemaRendererMode} from '../lib/unstable/core/constants'; -import {mutators} from '../lib/unstable/core/mutators'; import type { JsonSchemaArray, JsonSchemaNumber, @@ -459,7 +458,7 @@ const objectPatternProperties: JsonSchemaObject = { stringConst, stringEnum, }, - title: 'patternProperties', + title: 'objectPatternProperties', default: {}, patternProperties: { '^string': { @@ -652,10 +651,12 @@ const baseSpec: JsonSchemaObject = { const value = { qwe: { test: { - // name: 'bocemb', - age: 13, - nameQ: 'jaja', - obj: {name: 'bocemb', age: 13}, + jajaja: { + // name: 'bocemb', + age: 13, + nameQ: 'jaja', + obj: {name: 'bocemb', age: 13}, + }, }, }, }; @@ -681,10 +682,10 @@ const value = { const template = () => { const Template: StoryFn = (__) => ( -
+ {() => (