From 04caf617eeadc2e6ed311b23f245c5a4e8cdf1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Tue, 29 Oct 2024 14:52:12 +0100 Subject: [PATCH] fix(Forms): fix schema validation for required paths with matching name (#4189) Fixes #4179 --- .../Form/Section/__tests__/Section.test.tsx | 92 ++++++++++++++++++- .../extensions/forms/hooks/useFieldProps.ts | 80 ++++++++++------ .../src/extensions/forms/hooks/usePath.ts | 1 + 3 files changed, 143 insertions(+), 30 deletions(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Section/__tests__/Section.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Section/__tests__/Section.test.tsx index 0c786f38e54..e2274c0ac01 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Section/__tests__/Section.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Section/__tests__/Section.test.tsx @@ -948,7 +948,7 @@ describe('Form.Section', () => { ).toHaveAttribute('aria-required', 'true') }) - it('should set "required" for firstName with nested schema', () => { + it('should set "required" in schema with section', () => { const schema: JSONSchema = { type: 'object', properties: { @@ -978,6 +978,96 @@ describe('Form.Section', () => { ).toHaveAttribute('aria-required', 'true') }) + it('should set "required" in schema with section in nested object', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + myObject: { + type: 'object', + properties: { + mySection: { + type: 'object', + properties: { + firstName: { + type: 'string', + }, + }, + required: ['firstName'], + }, + }, + }, + }, + } + + render( + + + + ) + + expect( + document.querySelector('input[name="firstName"]') + ).toHaveAttribute('aria-required', 'true') + expect( + document.querySelector('input[name="lastName"]') + ).toHaveAttribute('aria-required', 'true') + }) + + it('should set "required" in schema with section of the same name', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + firstName: { + type: 'object', + properties: { + firstName: { + type: 'string', + }, + }, + required: ['firstName'], + }, + }, + } + + render( + + + + ) + + expect( + document.querySelector('input[name="firstName"]') + ).toHaveAttribute('aria-required', 'true') + expect( + document.querySelector('input[name="lastName"]') + ).toHaveAttribute('aria-required', 'true') + }) + + it('should not set "required" for field path that matches a schema path', () => { + const schema: JSONSchema = { + type: 'object', + required: ['longPath_with_firstName_inside'], + properties: { + longPath_with_firstName_inside: { + type: 'string', + }, + }, + } + + render( + + + + ) + + expect( + document.querySelector('input[name="firstName"]') + ).not.toHaveAttribute('aria-required') + expect( + document.querySelector('input[name="lastName"]') + ).toHaveAttribute('aria-required', 'true') + }) + it('should overwrite minLength', () => { const schema: JSONSchema = { type: 'object', diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts index 51995d2f86e..dad895c1d99 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts @@ -227,11 +227,12 @@ export default function useFieldProps( const hasPath = Boolean(pathProp) const hasItemPath = Boolean(itemPath) - const { path, identifier, makeIteratePath } = usePath({ - id, - path: pathProp, - itemPath, - }) + const { path, identifier, makeIteratePath, joinPath, cleanPath } = + usePath({ + id, + path: pathProp, + itemPath, + }) const defaultValueRef = useRef(defaultValue) useLayoutEffect(() => { @@ -264,34 +265,55 @@ export default function useFieldProps( return requiredProp } - const paths = identifier.split('/') - if (paths.length > 0 && (schema || dataContext?.schema)) { - const requiredList = [schema?.['required']] - - if (paths.length > 1) { - const schema = dataContext.schema - const schemaPath = paths.slice(0, -1).join('/properties/') - const schemaPart = pointer.has(schema, schemaPath) - ? pointer.get(schema, schemaPath) - : schema - - requiredList.push(schemaPart?.['required']) - } - - if (sectionPath) { - paths.push(sectionPath.substring(1)) - } + if (schema || dataContext?.schema) { + const paths = identifier.split('/') + if (paths.length > 0) { + const requiredInSchema = [schema?.['required']] + + // - Handle context schema + if (paths.length > 1) { + const schema = dataContext.schema + const pathWithoutLast = paths.slice(0, -1).join('/properties/') + const schemaPart = pointer.has(schema, pathWithoutLast) + ? pointer.get(schema, pathWithoutLast) + : schema + + const requiredSchemaList = schemaPart?.['required'] + if (Array.isArray(requiredSchemaList)) { + const rootPath = pathWithoutLast.replace(/properties\//g, '') + const requiredList = requiredSchemaList.map((path) => { + path = cleanPath('/' + path) + return sectionPath && path.startsWith(sectionPath) + ? path + : joinPath([sectionPath || rootPath, path]) + }) + requiredInSchema.push(requiredList) + } + } - const collected = requiredList.flatMap((v) => v).filter(Boolean) - if ( - paths + const collected = requiredInSchema + .flatMap((value) => value) .filter(Boolean) - .some((p) => collected.some((c) => c.includes(p))) - ) { - return true + + if ( + collected.filter(Boolean).some((path) => { + path = cleanPath('/' + path) + return identifier === path || sectionPath === path + }) + ) { + return true + } } } - }, [requiredProp, identifier, schema, dataContext.schema, sectionPath]) + }, [ + cleanPath, + dataContext.schema, + identifier, + joinPath, + requiredProp, + schema, + sectionPath, + ]) // Error handling // - Should errors received through validation be shown initially. Assume that providing a direct prop to diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/usePath.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/usePath.ts index c9f32f2566c..b977cffffae 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/usePath.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/usePath.ts @@ -100,6 +100,7 @@ export default function usePath(props: Props = {}) { makePath, makeIteratePath, makeSectionPath, + cleanPath, } }