From 42ef5c1e5a134a1433a874220b5207b6d05a2db9 Mon Sep 17 00:00:00 2001 From: slefebvre Date: Mon, 19 Feb 2024 16:36:27 +0100 Subject: [PATCH 01/12] Allow the translation of enum value used for elementLabelProp in ExpandPanelRenderer --- .../src/layouts/ExpandPanelRenderer.tsx | 99 +++++++++++- .../renderers/MaterialArrayLayout.test.tsx | 151 ++++++++++++++++++ 2 files changed, 245 insertions(+), 5 deletions(-) diff --git a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx index f6ad22d71..1239ba2a7 100644 --- a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx +++ b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx @@ -21,6 +21,8 @@ import { findUISchema, JsonFormsRendererRegistryEntry, JsonSchema, + JsonSchema4, + JsonSchema7, moveDown, moveUp, Resolve, @@ -31,6 +33,10 @@ import { createId, removeId, ArrayTranslations, + encode, + enumToEnumOptionMapper, + oneOfToEnumOptionMapper, + getI18nKeyPrefix, } from '@jsonforms/core'; import { Accordion, @@ -296,12 +302,36 @@ export const withContextToExpandPanelProps = ( props, }: JsonFormsStateContext & ExpandPanelProps) { const dispatchProps = ctxDispatchToExpandPanelProps(ctx.dispatch); - const { childLabelProp, schema, path, index, uischemas } = props; + const { + childLabelProp, + schema, + uischema, + rootSchema, + path, + index, + uischemas, + } = props; const childPath = composePaths(path, `${index}`); - const childData = Resolve.data(ctx.core.data, childPath); - const childLabel = childLabelProp - ? get(childData, childLabelProp, '') - : get(childData, getFirstPrimitiveProp(schema), ''); + + const childLabel = useMemo(() => { + return computeChildLabel( + ctx.core.data, + childPath, + childLabelProp, + schema, + rootSchema, + ctx.i18n.translate, + uischema + ); + }, [ + ctx.core.data, + childPath, + childLabelProp, + schema, + rootSchema, + ctx.i18n.translate, + uischema, + ]); return ( e.const === currentValue + ); + + if (oneOfSchema === undefined) return currentValue; + + const oneOfChildLabel = oneOfToEnumOptionMapper( + oneOfSchema, + translateFct, + getI18nKeyPrefix(schema, uiSchema, childPath) + ); + + return oneOfChildLabel.label; + } else { + return currentValue; + } + } else { + return get(childData, getFirstPrimitiveProp(schema), ''); + } +} + export const withJsonFormsExpandPanelProps = ( Component: ComponentType ): ComponentType => diff --git a/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx b/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx index ae1bc86cf..1d9990864 100644 --- a/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx +++ b/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx @@ -52,6 +52,17 @@ const data = [ ]; const emptyData: any[] = []; + +const enumOrOneOfData = [ + { + message: 'El Barto was here', + messageType: 'MSG_TYPE_1', + }, + { + message: 'Yolo', + }, +]; + const schema: JsonSchema7 = { type: 'array', items: { @@ -68,6 +79,49 @@ const schema: JsonSchema7 = { }, }; +const enumSchema: JsonSchema7 = { + type: 'array', + items: { + type: 'object', + properties: { + message: { + type: 'string', + maxLength: 3, + }, + messageType: { + type: 'string', + enum: ['MSG_TYPE_1', 'MSG_TYPE_2'], + }, + }, + }, +}; + +const oneOfSchema = { + type: 'array', + items: { + type: 'object', + properties: { + message: { + type: 'string', + maxLength: 3, + }, + messageType: { + type: 'string', + oneOf: [ + { + const: 'MSG_TYPE_1', + title: 'First message type', + }, + { + const: 'MSG_TYPE_2', + title: 'Second message type', + }, + ], + }, + }, + }, +}; + const nestedSchema: JsonSchema7 = { type: 'array', items: { @@ -140,6 +194,21 @@ const uischemaWithChildLabelProp: ControlElement = { }, }; +const uiSchemaWithEnumOrOneOfChildLabelProp: ControlElement = { + type: 'Control', + scope: '#', + options: { + elementLabelProp: 'messageType', + detail: { + type: 'HorizontalLayout', + elements: [ + { type: 'Control', scope: '#/properties/message' }, + { type: 'Control', scope: '#/properties/messageType' }, + ], + }, + }, +}; + const uischemaOptions: { generate: ControlElement; default: ControlElement; @@ -629,4 +698,86 @@ describe('Material array layout', () => { const noDataLabel = wrapper.find('div>div>p').text(); expect(noDataLabel.includes('Translated')).toBeTruthy(); }); + + it('should render configured enum child label property as translated label', () => { + const core = initCore( + enumSchema, + uiSchemaWithEnumOrOneOfChildLabelProp, + enumOrOneOfData + ); + const translate = () => 'Translated'; + + // Enum Case - No translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe('MSG_TYPE_1'); + + // Enum Case - Translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe('Translated'); + }); + + it('should render configured oneOf child label property as translated label', () => { + const core = initCore( + oneOfSchema, + uiSchemaWithEnumOrOneOfChildLabelProp, + enumOrOneOfData + ); + const translate = () => 'Translated'; + + // OneOf - No translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe('First message type'); + + // OneOf Case - Translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe('Translated'); + }); }); From 4689ffd8ae5e288e051c7f451b35364cf71e4e5e Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Fri, 8 Mar 2024 12:32:19 +0100 Subject: [PATCH 02/12] improve type usage and align function declarations with remaining code base --- .../src/layouts/ExpandPanelRenderer.tsx | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx index e5ae4e823..bc8dcb3f8 100644 --- a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx +++ b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx @@ -21,8 +21,6 @@ import { findUISchema, JsonFormsRendererRegistryEntry, JsonSchema, - JsonSchema4, - JsonSchema7, moveDown, moveUp, Resolve, @@ -37,6 +35,8 @@ import { enumToEnumOptionMapper, oneOfToEnumOptionMapper, getI18nKeyPrefix, + Translator, + UISchemaElement, } from '@jsonforms/core'; import { Accordion, @@ -317,19 +317,33 @@ export const ctxDispatchToExpandPanelProps: ( */ export const withContextToExpandPanelProps = ( Component: ComponentType -): ComponentType => - function WithContextToExpandPanelProps({ +): ComponentType<{ + ctx: JsonFormsStateContext; + props: OwnPropsOfExpandPanel; +}> => { + return function WithContextToExpandPanelProps({ ctx, props, - }: JsonFormsStateContext & ExpandPanelProps) { + }: { + ctx: JsonFormsStateContext; + props: ExpandPanelProps; + }) { const dispatchProps = ctxDispatchToExpandPanelProps(ctx.dispatch); const { + // eslint is unable to detect that these props are "checked" via Typescript already + // eslint-disable-next-line react/prop-types childLabelProp, + // eslint-disable-next-line react/prop-types schema, + // eslint-disable-next-line react/prop-types uischema, + // eslint-disable-next-line react/prop-types rootSchema, + // eslint-disable-next-line react/prop-types path, + // eslint-disable-next-line react/prop-types index, + // eslint-disable-next-line react/prop-types uischemas, } = props; const childPath = composePaths(path, `${index}`); @@ -364,24 +378,25 @@ export const withContextToExpandPanelProps = ( /> ); }; +}; -function hasEnumField(schema: JsonSchema4 | JsonSchema7) { +const hasEnumField = (schema: JsonSchema) => { return schema && (schema.enum !== undefined || schema.const !== undefined); -} +}; -function hasOneOfField(schema: JsonSchema4 | JsonSchema7) { +const hasOneOfField = (schema: JsonSchema) => { return schema && schema.oneOf !== undefined; -} +}; -function computeChildLabel( +const computeChildLabel = ( data: any, childPath: string, - childLabelProp: any, - schema: any, - rootSchema: any, - translateFct: any, - uiSchema: any -) { + childLabelProp: string, + schema: JsonSchema, + rootSchema: JsonSchema, + translateFct: Translator, + uiSchema: UISchemaElement +) => { const childData = Resolve.data(data, childPath); if (childLabelProp) { @@ -402,9 +417,9 @@ function computeChildLabel( return enumChildLabel.label; } else if (hasOneOfField(childSchema)) { - const oneOfArray = childSchema.oneOf as (JsonSchema4 | JsonSchema7)[]; + const oneOfArray = childSchema.oneOf as JsonSchema[]; const oneOfSchema = oneOfArray.find( - (e: JsonSchema4 | JsonSchema7) => e.const === currentValue + (e: JsonSchema) => e.const === currentValue ); if (oneOfSchema === undefined) return currentValue; @@ -422,7 +437,7 @@ function computeChildLabel( } else { return get(childData, getFirstPrimitiveProp(schema), ''); } -} +}; export const withJsonFormsExpandPanelProps = ( Component: ComponentType From 3fd7031edec188bb3c255ad7bd6facc34c1f1331 Mon Sep 17 00:00:00 2001 From: slefebvre Date: Fri, 8 Mar 2024 18:43:39 +0100 Subject: [PATCH 03/12] Fix translation key and deep schema management. Add TU --- .../src/layouts/ExpandPanelRenderer.tsx | 60 ++-- .../renderers/MaterialArrayLayout.test.tsx | 275 +++++++++++++++++- .../material-renderers/test/renderers/util.ts | 3 + 3 files changed, 299 insertions(+), 39 deletions(-) diff --git a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx index bc8dcb3f8..3ef4d38dd 100644 --- a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx +++ b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx @@ -37,6 +37,7 @@ import { getI18nKeyPrefix, Translator, UISchemaElement, + EnumOption, } from '@jsonforms/core'; import { Accordion, @@ -399,44 +400,47 @@ const computeChildLabel = ( ) => { const childData = Resolve.data(data, childPath); - if (childLabelProp) { - const currentValue = get(childData, childLabelProp, ''); + if (!childLabelProp) return get(childData, getFirstPrimitiveProp(schema), ''); - const childSchema = Resolve.schema( - schema, - `#/properties/${encode(childLabelProp)}`, - rootSchema - ); + const currentValue = get(childData, childLabelProp, ''); - if (hasEnumField(childSchema)) { - const enumChildLabel = enumToEnumOptionMapper( - currentValue, - translateFct, - getI18nKeyPrefix(schema, uiSchema, childPath) - ); + const childSchema = Resolve.schema( + schema, + `#/properties/${childLabelProp + .split('.') + .map((p) => encode(p)) + .join('/properties/')}`, + rootSchema + ); - return enumChildLabel.label; - } else if (hasOneOfField(childSchema)) { - const oneOfArray = childSchema.oneOf as JsonSchema[]; - const oneOfSchema = oneOfArray.find( - (e: JsonSchema) => e.const === currentValue - ); + const fallbackI18nKey = + getI18nKeyPrefix(schema, uiSchema, childPath) + + '.' + + getI18nKeyPrefix(schema, uiSchema, childLabelProp); - if (oneOfSchema === undefined) return currentValue; + let enumOption: EnumOption = undefined; + if (hasEnumField(childSchema)) { + enumOption = enumToEnumOptionMapper( + currentValue, + translateFct, + fallbackI18nKey + ); + } else if (hasOneOfField(childSchema)) { + const oneOfArray = childSchema.oneOf as JsonSchema[]; + const oneOfSchema = oneOfArray.find( + (e: JsonSchema) => e.const === currentValue + ); - const oneOfChildLabel = oneOfToEnumOptionMapper( + if (oneOfSchema) { + enumOption = oneOfToEnumOptionMapper( oneOfSchema, translateFct, - getI18nKeyPrefix(schema, uiSchema, childPath) + fallbackI18nKey ); - - return oneOfChildLabel.label; - } else { - return currentValue; } - } else { - return get(childData, getFirstPrimitiveProp(schema), ''); } + + return enumOption ? enumOption.label : currentValue; }; export const withJsonFormsExpandPanelProps = ( diff --git a/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx b/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx index 481698949..d71a9c3ea 100644 --- a/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx +++ b/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx @@ -39,7 +39,7 @@ import Enzyme, { mount, ReactWrapper } from 'enzyme'; import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import { JsonForms, JsonFormsStateProvider } from '@jsonforms/react'; import { Accordion } from '@mui/material'; -import { createTesterContext, initCore } from './util'; +import { createTesterContext, testTranslator, initCore } from './util'; import { checkTooltip, checkTooltipTranslation } from './tooltipChecker'; Enzyme.configure({ adapter: new Adapter() }); @@ -63,6 +63,10 @@ const enumOrOneOfData = [ message: 'El Barto was here', messageType: 'MSG_TYPE_1', }, + { + message: 'El Barto was not here', + messageType: 'MSG_TYPE_2', + }, { message: 'Yolo', }, @@ -115,7 +119,6 @@ const oneOfSchema = { oneOf: [ { const: 'MSG_TYPE_1', - title: 'First message type', }, { const: 'MSG_TYPE_2', @@ -127,6 +130,106 @@ const oneOfSchema = { }, }; +const deepEnumOrOneOfData = { + article: { + title: 'title', + comments: [ + { + author: { + name: 'John', + type: 'WRITER', + role: 'ROLE_1', + }, + }, + { + author: { + name: 'John', + type: 'AUTHOR', + role: 'ROLE_2', + }, + }, + ], + }, +}; + +const deepEnumOrOneOfSchema: JsonSchema7 = { + type: 'object', + properties: { + article: { + type: 'object', + properties: { + title: { + type: 'string', + }, + comments: { + type: 'array', + items: { + type: 'object', + properties: { + author: { + type: 'object', + properties: { + name: { + type: 'string', + }, + type: { + type: 'string', + enum: ['AUTHOR', 'WRITER'], + }, + role: { + type: 'string', + oneOf: [ + { + const: 'ROLE_1', + }, + { + const: 'ROLE_2', + title: 'Second role', + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; + +const deepUiSchemaWithEnumChildLabelProp: ControlElement = { + type: 'Control', + scope: '#/properties/article/properties/comments', + options: { + elementLabelProp: 'author.type', + detail: { + type: 'HorizontalLayout', + elements: [ + { type: 'Control', scope: '#/properties/author/properties/name' }, + { type: 'Control', scope: '#/properties/author/properties/type' }, + { type: 'Control', scope: '#/properties/author/properties/role' }, + ], + }, + }, +}; + +const deepUiSchemaWithOneOfChildLabelProp: ControlElement = { + type: 'Control', + scope: '#/properties/article/properties/comments', + options: { + elementLabelProp: 'author.role', + detail: { + type: 'HorizontalLayout', + elements: [ + { type: 'Control', scope: '#/properties/author/properties/name' }, + { type: 'Control', scope: '#/properties/author/properties/type' }, + { type: 'Control', scope: '#/properties/author/properties/role' }, + ], + }, + }, +}; + const nestedSchema: JsonSchema7 = { type: 'array', items: { @@ -808,13 +911,12 @@ describe('Material array layout', () => { ); }); - it('should render configured enum child label property as translated label', () => { + it('should render configured enum child label property as-is if no translator', () => { const core = initCore( enumSchema, uiSchemaWithEnumOrOneOfChildLabelProp, enumOrOneOfData ); - const translate = () => 'Translated'; // Enum Case - No translation wrapper = mount( @@ -831,11 +933,23 @@ describe('Material array layout', () => { expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); expect(getChildLabel(wrapper, 0)).toBe('MSG_TYPE_1'); + }); + + it('should render configured enum child label property as translated label', () => { + const core = initCore( + enumSchema, + uiSchemaWithEnumOrOneOfChildLabelProp, + enumOrOneOfData + ); // Enum Case - Translation wrapper = mount( { expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('Translated'); + expect(getChildLabel(wrapper, 0)).toBe( + 'translator.root.messageType.MSG_TYPE_1' + ); }); - it('should render configured oneOf child label property as translated label', () => { + it('should render configured oneOf child label property as-is if no translator', () => { const core = initCore( oneOfSchema, uiSchemaWithEnumOrOneOfChildLabelProp, enumOrOneOfData ); - const translate = () => 'Translated'; // OneOf - No translation wrapper = mount( @@ -871,12 +986,25 @@ describe('Material array layout', () => { expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('First message type'); + expect(getChildLabel(wrapper, 0)).toBe('MSG_TYPE_1'); + expect(getChildLabel(wrapper, 1)).toBe('Second message type'); + }); + + it('should render configured oneOf child label property as translated label', () => { + const core = initCore( + oneOfSchema, + uiSchemaWithEnumOrOneOfChildLabelProp, + enumOrOneOfData + ); // OneOf Case - Translation wrapper = mount( { expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('Translated'); + expect(getChildLabel(wrapper, 0)).toBe( + 'translator.root.messageType.MSG_TYPE_1' + ); + // TODO Possible impact to existing jsonForm setup, previously if the translate fonction was setup, + // the enumOf title was written as-is and not pushed to the translation function with a key. + // The oneOf title is pushed for translation over the const value in renderer.ts/oneOfToEnumOptionMapper(), + // seems a bug to me + expect(getChildLabel(wrapper, 1)).toBe( + 'translator.root.messageType.Second message type' + ); + }); + + it('should render configured deep enum child label property as-is if no translator', () => { + const core = initCore( + deepEnumOrOneOfSchema, + deepUiSchemaWithEnumChildLabelProp, + deepEnumOrOneOfData + ); + + // Enum Case - No translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe('WRITER'); + expect(getChildLabel(wrapper, 1)).toBe('AUTHOR'); + }); + + it('should render configured deep enum child label property as translated label', () => { + const core = initCore( + deepEnumOrOneOfSchema, + deepUiSchemaWithEnumChildLabelProp, + deepEnumOrOneOfData + ); + + // Enum Case - Translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe( + 'translator.article.comments.author.type.WRITER' + ); + expect(getChildLabel(wrapper, 1)).toBe( + 'translator.article.comments.author.type.AUTHOR' + ); + }); + + it('should render configured deep oneOf child label property as-is if no translator', () => { + const core = initCore( + deepEnumOrOneOfSchema, + deepUiSchemaWithOneOfChildLabelProp, + deepEnumOrOneOfData + ); + + // OneOf Case - No translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe('ROLE_1'); + expect(getChildLabel(wrapper, 1)).toBe('Second role'); + }); + + it('should render configured deep oneOf child label property as translated label', () => { + const core = initCore( + deepEnumOrOneOfSchema, + deepUiSchemaWithOneOfChildLabelProp, + deepEnumOrOneOfData + ); + + // OneOf Case - Translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe( + 'translator.article.comments.author.role.ROLE_1' + ); + expect(getChildLabel(wrapper, 1)).toBe( + 'translator.article.comments.author.role.Second role' + ); }); }); diff --git a/packages/material-renderers/test/renderers/util.ts b/packages/material-renderers/test/renderers/util.ts index 12836889d..cb5072d57 100644 --- a/packages/material-renderers/test/renderers/util.ts +++ b/packages/material-renderers/test/renderers/util.ts @@ -27,6 +27,7 @@ import { createAjv, JsonSchema, TesterContext, + Translator, UISchemaElement, } from '@jsonforms/core'; import { JsonFormsReactProps, useJsonForms } from '@jsonforms/react'; @@ -55,3 +56,5 @@ export const createTesterContext = ( ): TesterContext => { return { rootSchema, config }; }; + +export const testTranslator: Translator = (key: string) => 'translator.' + key; From 27ad9db083068fe212d765e1b3a07dca750800f8 Mon Sep 17 00:00:00 2001 From: slefebvre Date: Tue, 12 Mar 2024 09:40:11 +0100 Subject: [PATCH 04/12] Fix for enum/oneOf first primitive prop --- .../src/layouts/ExpandPanelRenderer.tsx | 4 +- .../renderers/MaterialArrayLayout.test.tsx | 108 ++++++++++++++++-- 2 files changed, 99 insertions(+), 13 deletions(-) diff --git a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx index 3ef4d38dd..664b9dbf8 100644 --- a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx +++ b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx @@ -400,10 +400,12 @@ const computeChildLabel = ( ) => { const childData = Resolve.data(data, childPath); - if (!childLabelProp) return get(childData, getFirstPrimitiveProp(schema), ''); + childLabelProp ??= getFirstPrimitiveProp(schema); const currentValue = get(childData, childLabelProp, ''); + if (!childLabelProp) return currentValue; + const childSchema = Resolve.schema( schema, `#/properties/${childLabelProp diff --git a/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx b/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx index d71a9c3ea..f8ed5f58b 100644 --- a/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx +++ b/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx @@ -93,10 +93,6 @@ const enumSchema: JsonSchema7 = { items: { type: 'object', properties: { - message: { - type: 'string', - maxLength: 3, - }, messageType: { type: 'string', enum: ['MSG_TYPE_1', 'MSG_TYPE_2'], @@ -110,10 +106,6 @@ const oneOfSchema = { items: { type: 'object', properties: { - message: { - type: 'string', - maxLength: 3, - }, messageType: { type: 'string', oneOf: [ @@ -911,6 +903,52 @@ describe('Material array layout', () => { ); }); + it('should render first simple enum property as-is if no translator', () => { + const lightUiSchema = { ...uiSchemaWithEnumOrOneOfChildLabelProp }; + delete lightUiSchema.options.elementLabelProp; + + const core = initCore(enumSchema, lightUiSchema, enumOrOneOfData); + + // Enum Case - Translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe('MSG_TYPE_1'); + }); + + it('should render first simple enum property as translated child label', () => { + const lightUiSchema = { ...uiSchemaWithEnumOrOneOfChildLabelProp }; + delete lightUiSchema.options.elementLabelProp; + + const core = initCore(enumSchema, lightUiSchema, enumOrOneOfData); + + // Enum Case - Translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe( + 'translator.root.messageType.MSG_TYPE_1' + ); + }); + it('should render configured enum child label property as-is if no translator', () => { const core = initCore( enumSchema, @@ -965,6 +1003,56 @@ describe('Material array layout', () => { ); }); + it('should render first simple oneOf property as-is if no translator', () => { + const lightUiSchema = { ...uiSchemaWithEnumOrOneOfChildLabelProp }; + delete lightUiSchema.options.elementLabelProp; + + const core = initCore(oneOfSchema, lightUiSchema, enumOrOneOfData); + + // Enum Case - Translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe('MSG_TYPE_1'); + expect(getChildLabel(wrapper, 1)).toBe('Second message type'); + }); + + it('should render first simple enum property as translated child label', () => { + const lightUiSchema = { ...uiSchemaWithEnumOrOneOfChildLabelProp }; + delete lightUiSchema.options.elementLabelProp; + + const core = initCore(enumSchema, lightUiSchema, enumOrOneOfData); + + // Enum Case - Translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe( + 'translator.root.messageType.MSG_TYPE_1' + ); + }); + it('should render configured oneOf child label property as-is if no translator', () => { const core = initCore( oneOfSchema, @@ -1018,10 +1106,6 @@ describe('Material array layout', () => { expect(getChildLabel(wrapper, 0)).toBe( 'translator.root.messageType.MSG_TYPE_1' ); - // TODO Possible impact to existing jsonForm setup, previously if the translate fonction was setup, - // the enumOf title was written as-is and not pushed to the translation function with a key. - // The oneOf title is pushed for translation over the const value in renderer.ts/oneOfToEnumOptionMapper(), - // seems a bug to me expect(getChildLabel(wrapper, 1)).toBe( 'translator.root.messageType.Second message type' ); From 72229bfaae733bd993810a8f441039fc6b02d103 Mon Sep 17 00:00:00 2001 From: slefebvre Date: Tue, 12 Mar 2024 10:31:44 +0100 Subject: [PATCH 05/12] Fix for translated i18n on schema --- .../src/layouts/ExpandPanelRenderer.tsx | 13 ++- .../renderers/MaterialArrayLayout.test.tsx | 89 ++++++++++++++++--- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx index 664b9dbf8..cd60902ce 100644 --- a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx +++ b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx @@ -415,17 +415,12 @@ const computeChildLabel = ( rootSchema ); - const fallbackI18nKey = - getI18nKeyPrefix(schema, uiSchema, childPath) + - '.' + - getI18nKeyPrefix(schema, uiSchema, childLabelProp); - let enumOption: EnumOption = undefined; if (hasEnumField(childSchema)) { enumOption = enumToEnumOptionMapper( currentValue, translateFct, - fallbackI18nKey + getI18nKeyPrefix(childSchema, undefined, childPath + '.' + childLabelProp) ); } else if (hasOneOfField(childSchema)) { const oneOfArray = childSchema.oneOf as JsonSchema[]; @@ -437,7 +432,11 @@ const computeChildLabel = ( enumOption = oneOfToEnumOptionMapper( oneOfSchema, translateFct, - fallbackI18nKey + getI18nKeyPrefix( + oneOfSchema, + undefined, + childPath + '.' + childLabelProp + ) ); } } diff --git a/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx b/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx index f8ed5f58b..15b94225b 100644 --- a/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx +++ b/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx @@ -25,7 +25,7 @@ import './MatchMediaMock'; import { ArrayTranslationEnum, - ControlElement, + ControlElement, i18nJsonSchema, JsonSchema7, } from '@jsonforms/core'; import * as React from 'react'; @@ -944,9 +944,7 @@ describe('Material array layout', () => { expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe( - 'translator.root.messageType.MSG_TYPE_1' - ); + expect(getChildLabel(wrapper, 0)).toBe('translator.messageType.MSG_TYPE_1'); }); it('should render configured enum child label property as-is if no translator', () => { @@ -998,9 +996,7 @@ describe('Material array layout', () => { expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe( - 'translator.root.messageType.MSG_TYPE_1' - ); + expect(getChildLabel(wrapper, 0)).toBe('translator.messageType.MSG_TYPE_1'); }); it('should render first simple oneOf property as-is if no translator', () => { @@ -1048,9 +1044,7 @@ describe('Material array layout', () => { expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe( - 'translator.root.messageType.MSG_TYPE_1' - ); + expect(getChildLabel(wrapper, 0)).toBe('translator.messageType.MSG_TYPE_1'); }); it('should render configured oneOf child label property as-is if no translator', () => { @@ -1103,11 +1097,9 @@ describe('Material array layout', () => { expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe( - 'translator.root.messageType.MSG_TYPE_1' - ); + expect(getChildLabel(wrapper, 0)).toBe('translator.messageType.MSG_TYPE_1'); expect(getChildLabel(wrapper, 1)).toBe( - 'translator.root.messageType.Second message type' + 'translator.messageType.Second message type' ); }); @@ -1226,4 +1218,73 @@ describe('Material array layout', () => { 'translator.article.comments.author.role.Second role' ); }); + + it('should render configured enum child label property with schema i18n as translated label', () => { + const i18nSchema = { ...enumSchema }; + const properties = (i18nSchema.items as JsonSchema7).properties; + const childSchema = properties.messageType as i18nJsonSchema; + childSchema.i18n = 'myI18n'; + + const core = initCore( + i18nSchema, + uiSchemaWithEnumOrOneOfChildLabelProp, + enumOrOneOfData + ); + + // Enum Case - Translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe('translator.myI18n.MSG_TYPE_1'); + }); + + it('should render configured oneOf child label property with schema i18n as translated label', () => { + const i18nSchema = { ...oneOfSchema }; + const properties = (i18nSchema.items as JsonSchema7).properties; + const oneOfArray = properties.messageType.oneOf; + for (const oneOfValue of oneOfArray) { + (oneOfValue as i18nJsonSchema).i18n = 'myI18n_' + oneOfValue.const; + } + + const core = initCore( + i18nSchema, + uiSchemaWithEnumOrOneOfChildLabelProp, + enumOrOneOfData + ); + + // OneOf Case - Translation + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + + expect(getChildLabel(wrapper, 0)).toBe('translator.myI18n_MSG_TYPE_1'); + expect(getChildLabel(wrapper, 1)).toBe('translator.myI18n_MSG_TYPE_2'); + }); }); From 3091b2726333eae6b6e93566cf20ae09550c44e5 Mon Sep 17 00:00:00 2001 From: slefebvre Date: Tue, 12 Mar 2024 14:35:04 +0100 Subject: [PATCH 06/12] Fix for translated i18n on UiSchema --- .../src/layouts/ExpandPanelRenderer.tsx | 49 ++- .../renderers/MaterialArrayLayout.test.tsx | 412 +++++++----------- 2 files changed, 194 insertions(+), 267 deletions(-) diff --git a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx index cd60902ce..a7e11227d 100644 --- a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx +++ b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx @@ -38,6 +38,8 @@ import { Translator, UISchemaElement, EnumOption, + isLayout, + isScoped, } from '@jsonforms/core'; import { Accordion, @@ -389,6 +391,42 @@ const hasOneOfField = (schema: JsonSchema) => { return schema && schema.oneOf !== undefined; }; +function getChildPropPath(childLabelProp: string) { + return `/properties/${childLabelProp + .split('.') + .map((p) => encode(p)) + .join('/properties/')}`; +} + +const isControlElement = ( + uiSchema: UISchemaElement +): uiSchema is ControlElement => uiSchema.type === 'Control'; + +const findUiControl = ( + uiSchema: UISchemaElement, + childLabelProp: string +): unknown | undefined => { + if (isControlElement(uiSchema)) { + if ( + isScoped(uiSchema) && + uiSchema.scope.endsWith(getChildPropPath(childLabelProp)) + ) { + return uiSchema; + } else if (uiSchema.options?.detail) { + return findUiControl(uiSchema.options.detail, childLabelProp); + } + } + + if (isLayout(uiSchema)) { + for (const elem of uiSchema.elements) { + const result = findUiControl(elem, childLabelProp); + if (result !== undefined) return result; + } + } + + return undefined; +}; + const computeChildLabel = ( data: any, childPath: string, @@ -408,10 +446,7 @@ const computeChildLabel = ( const childSchema = Resolve.schema( schema, - `#/properties/${childLabelProp - .split('.') - .map((p) => encode(p)) - .join('/properties/')}`, + '#' + getChildPropPath(childLabelProp), rootSchema ); @@ -420,7 +455,11 @@ const computeChildLabel = ( enumOption = enumToEnumOptionMapper( currentValue, translateFct, - getI18nKeyPrefix(childSchema, undefined, childPath + '.' + childLabelProp) + getI18nKeyPrefix( + childSchema, + findUiControl(uiSchema, childLabelProp), + childPath + '.' + childLabelProp + ) ); } else if (hasOneOfField(childSchema)) { const oneOfArray = childSchema.oneOf as JsonSchema[]; diff --git a/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx b/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx index 15b94225b..560b6a0ac 100644 --- a/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx +++ b/packages/material-renderers/test/renderers/MaterialArrayLayout.test.tsx @@ -25,8 +25,10 @@ import './MatchMediaMock'; import { ArrayTranslationEnum, - ControlElement, i18nJsonSchema, + ControlElement, + i18nJsonSchema, JsonSchema7, + Translator, } from '@jsonforms/core'; import * as React from 'react'; @@ -41,6 +43,7 @@ import { JsonForms, JsonFormsStateProvider } from '@jsonforms/react'; import { Accordion } from '@mui/material'; import { createTesterContext, testTranslator, initCore } from './util'; import { checkTooltip, checkTooltipTranslation } from './tooltipChecker'; +import { cloneDeep } from 'lodash'; Enzyme.configure({ adapter: new Adapter() }); @@ -506,13 +509,13 @@ describe('Material array layout', () => { ...schema, title: 'My awesome title', }; - const core = initCore(schema, uischema, data); - wrapper = mount( - - - + + wrapper = arrayLayoutWrapper( + wrapper, + data, + titleSchema, + uischema, + undefined ); const listTitle = wrapper.find('h6').at(0); @@ -708,16 +711,12 @@ describe('Material array layout', () => { .text(); it('should render first simple property as child label', () => { - const core = initCore(schema, uischema, data); - wrapper = mount( - - - + wrapper = arrayLayoutWrapper( + wrapper, + data, + schema, + uischemaWithSortOption, + undefined ); expect(getChildLabel(wrapper, 0)).toBe('El Barto was here'); @@ -907,196 +906,109 @@ describe('Material array layout', () => { const lightUiSchema = { ...uiSchemaWithEnumOrOneOfChildLabelProp }; delete lightUiSchema.options.elementLabelProp; - const core = initCore(enumSchema, lightUiSchema, enumOrOneOfData); - - // Enum Case - Translation - wrapper = mount( - - - + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, + enumSchema, + lightUiSchema, + undefined ); - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('MSG_TYPE_1'); }); it('should render first simple enum property as translated child label', () => { - const lightUiSchema = { ...uiSchemaWithEnumOrOneOfChildLabelProp }; + const lightUiSchema = cloneDeep(uiSchemaWithEnumOrOneOfChildLabelProp); delete lightUiSchema.options.elementLabelProp; - const core = initCore(enumSchema, lightUiSchema, enumOrOneOfData); - - // Enum Case - Translation - wrapper = mount( - - - + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, + enumSchema, + lightUiSchema, + testTranslator ); - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('translator.messageType.MSG_TYPE_1'); }); it('should render configured enum child label property as-is if no translator', () => { - const core = initCore( + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, enumSchema, uiSchemaWithEnumOrOneOfChildLabelProp, - enumOrOneOfData - ); - - // Enum Case - No translation - wrapper = mount( - - - + undefined ); - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('MSG_TYPE_1'); }); it('should render configured enum child label property as translated label', () => { - const core = initCore( + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, enumSchema, uiSchemaWithEnumOrOneOfChildLabelProp, - enumOrOneOfData + testTranslator ); - // Enum Case - Translation - wrapper = mount( - - - - ); - - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('translator.messageType.MSG_TYPE_1'); }); it('should render first simple oneOf property as-is if no translator', () => { - const lightUiSchema = { ...uiSchemaWithEnumOrOneOfChildLabelProp }; + const lightUiSchema = cloneDeep(uiSchemaWithEnumOrOneOfChildLabelProp); delete lightUiSchema.options.elementLabelProp; - const core = initCore(oneOfSchema, lightUiSchema, enumOrOneOfData); - - // Enum Case - Translation - wrapper = mount( - - - + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, + oneOfSchema, + lightUiSchema, + undefined ); - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('MSG_TYPE_1'); expect(getChildLabel(wrapper, 1)).toBe('Second message type'); }); - it('should render first simple enum property as translated child label', () => { - const lightUiSchema = { ...uiSchemaWithEnumOrOneOfChildLabelProp }; + it('should render first simple oneOf property as translated child label', () => { + const lightUiSchema = cloneDeep(uiSchemaWithEnumOrOneOfChildLabelProp); delete lightUiSchema.options.elementLabelProp; - const core = initCore(enumSchema, lightUiSchema, enumOrOneOfData); - - // Enum Case - Translation - wrapper = mount( - - - + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, + oneOfSchema, + lightUiSchema, + testTranslator ); - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('translator.messageType.MSG_TYPE_1'); }); it('should render configured oneOf child label property as-is if no translator', () => { - const core = initCore( + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, oneOfSchema, uiSchemaWithEnumOrOneOfChildLabelProp, - enumOrOneOfData - ); - - // OneOf - No translation - wrapper = mount( - - - + undefined ); - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('MSG_TYPE_1'); expect(getChildLabel(wrapper, 1)).toBe('Second message type'); }); it('should render configured oneOf child label property as translated label', () => { - const core = initCore( + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, oneOfSchema, uiSchemaWithEnumOrOneOfChildLabelProp, - enumOrOneOfData + testTranslator ); - // OneOf Case - Translation - wrapper = mount( - - - - ); - - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('translator.messageType.MSG_TYPE_1'); expect(getChildLabel(wrapper, 1)).toBe( 'translator.messageType.Second message type' @@ -1104,55 +1016,27 @@ describe('Material array layout', () => { }); it('should render configured deep enum child label property as-is if no translator', () => { - const core = initCore( + wrapper = arrayLayoutWrapper( + wrapper, + deepEnumOrOneOfData, deepEnumOrOneOfSchema, deepUiSchemaWithEnumChildLabelProp, - deepEnumOrOneOfData + undefined ); - // Enum Case - No translation - wrapper = mount( - - - - ); - - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('WRITER'); expect(getChildLabel(wrapper, 1)).toBe('AUTHOR'); }); it('should render configured deep enum child label property as translated label', () => { - const core = initCore( + wrapper = arrayLayoutWrapper( + wrapper, + deepEnumOrOneOfData, deepEnumOrOneOfSchema, deepUiSchemaWithEnumChildLabelProp, - deepEnumOrOneOfData + testTranslator ); - // Enum Case - Translation - wrapper = mount( - - - - ); - - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe( 'translator.article.comments.author.type.WRITER' ); @@ -1162,55 +1046,27 @@ describe('Material array layout', () => { }); it('should render configured deep oneOf child label property as-is if no translator', () => { - const core = initCore( + wrapper = arrayLayoutWrapper( + wrapper, + deepEnumOrOneOfData, deepEnumOrOneOfSchema, deepUiSchemaWithOneOfChildLabelProp, - deepEnumOrOneOfData + undefined ); - // OneOf Case - No translation - wrapper = mount( - - - - ); - - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('ROLE_1'); expect(getChildLabel(wrapper, 1)).toBe('Second role'); }); it('should render configured deep oneOf child label property as translated label', () => { - const core = initCore( + wrapper = arrayLayoutWrapper( + wrapper, + deepEnumOrOneOfData, deepEnumOrOneOfSchema, deepUiSchemaWithOneOfChildLabelProp, - deepEnumOrOneOfData - ); - - // OneOf Case - Translation - wrapper = mount( - - - + testTranslator ); - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe( 'translator.article.comments.author.role.ROLE_1' ); @@ -1220,71 +1076,103 @@ describe('Material array layout', () => { }); it('should render configured enum child label property with schema i18n as translated label', () => { - const i18nSchema = { ...enumSchema }; + const i18nSchema = cloneDeep(enumSchema); const properties = (i18nSchema.items as JsonSchema7).properties; const childSchema = properties.messageType as i18nJsonSchema; childSchema.i18n = 'myI18n'; - const core = initCore( + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, i18nSchema, uiSchemaWithEnumOrOneOfChildLabelProp, - enumOrOneOfData + testTranslator ); - // Enum Case - Translation - wrapper = mount( - - - - ); - - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); - expect(getChildLabel(wrapper, 0)).toBe('translator.myI18n.MSG_TYPE_1'); }); it('should render configured oneOf child label property with schema i18n as translated label', () => { - const i18nSchema = { ...oneOfSchema }; + const i18nSchema = cloneDeep(oneOfSchema); const properties = (i18nSchema.items as JsonSchema7).properties; const oneOfArray = properties.messageType.oneOf; for (const oneOfValue of oneOfArray) { (oneOfValue as i18nJsonSchema).i18n = 'myI18n_' + oneOfValue.const; } - const core = initCore( + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, i18nSchema, uiSchemaWithEnumOrOneOfChildLabelProp, - enumOrOneOfData + testTranslator ); - // OneOf Case - Translation - wrapper = mount( - - - + expect(getChildLabel(wrapper, 0)).toBe('translator.myI18n_MSG_TYPE_1'); + expect(getChildLabel(wrapper, 1)).toBe('translator.myI18n_MSG_TYPE_2'); + }); + + it('should render configured enum child label property with UiSchema i18n as translated label', () => { + const i18nUiSchema = cloneDeep(uiSchemaWithEnumOrOneOfChildLabelProp); + const elements = i18nUiSchema.options.detail.elements as ControlElement[]; + const control = elements.find( + (e) => e.scope === '#/properties/messageType' ); + control.i18n = 'myI18n'; - expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + wrapper = arrayLayoutWrapper( + wrapper, + enumOrOneOfData, + enumSchema, + i18nUiSchema, + testTranslator + ); - expect(getChildLabel(wrapper, 0)).toBe('translator.myI18n_MSG_TYPE_1'); - expect(getChildLabel(wrapper, 1)).toBe('translator.myI18n_MSG_TYPE_2'); + expect(getChildLabel(wrapper, 0)).toBe('translator.myI18n.MSG_TYPE_1'); + }); + + it('should render configured deep enum child label property with UiSchema i18n as translated label', () => { + const i18nUiSchema = cloneDeep(deepUiSchemaWithEnumChildLabelProp); + const elements = i18nUiSchema.options.detail.elements as ControlElement[]; + const control = elements.find( + (e) => e.scope === '#/properties/author/properties/type' + ); + control.i18n = 'myI18n'; + + wrapper = arrayLayoutWrapper( + wrapper, + deepEnumOrOneOfData, + deepEnumOrOneOfSchema, + i18nUiSchema, + testTranslator + ); + + expect(getChildLabel(wrapper, 0)).toBe('translator.myI18n.WRITER'); + expect(getChildLabel(wrapper, 1)).toBe('translator.myI18n.AUTHOR'); }); }); + +function arrayLayoutWrapper( + wrapper: ReactWrapper, + data: any, + schema: JsonSchema7, + uiSchema: ControlElement, + translator: Translator +) { + const core = initCore(schema, uiSchema, data); + + wrapper = mount( + + + + ); + + expect(wrapper.find(MaterialArrayLayout).length).toBeTruthy(); + return wrapper; +} From 9478a99b030d650f24a2d7c066a9f7ba497fb234 Mon Sep 17 00:00:00 2001 From: slefebvre Date: Mon, 18 Mar 2024 14:03:05 +0100 Subject: [PATCH 07/12] Move support functions to core --- packages/core/src/models/uischema.ts | 4 + packages/core/src/util/label.ts | 87 +++++++++++++- packages/core/src/util/schema.ts | 8 ++ packages/core/src/util/uischema.ts | 48 +++++++- .../src/layouts/ExpandPanelRenderer.tsx | 113 +----------------- 5 files changed, 146 insertions(+), 114 deletions(-) diff --git a/packages/core/src/models/uischema.ts b/packages/core/src/models/uischema.ts index efdf8d5fa..613356ac9 100644 --- a/packages/core/src/models/uischema.ts +++ b/packages/core/src/models/uischema.ts @@ -309,3 +309,7 @@ export const isLabelable = (obj: unknown): obj is Labelable => export const isLabeled = (obj: unknown): obj is Labeled => isLabelable(obj) && ['string', 'boolean'].includes(typeof obj.label); + +export const isControlElement = ( + uiSchema: UISchemaElement +): uiSchema is ControlElement => uiSchema.type === 'Control'; diff --git a/packages/core/src/util/label.ts b/packages/core/src/util/label.ts index c7592ed98..cd78c2031 100644 --- a/packages/core/src/util/label.ts +++ b/packages/core/src/util/label.ts @@ -25,8 +25,27 @@ import startCase from 'lodash/startCase'; -import type { ControlElement, JsonSchema, LabelDescription } from '../models'; +import { + ControlElement, + JsonSchema, + LabelDescription, + UISchemaElement, +} from '../models'; import { decode } from './path'; +import { getI18nKeyPrefix, Translator } from '../i18n'; +import { Resolve } from './util'; +import { + getFirstPrimitiveProp, + isEnumSchema, + isOneOfEnumSchema, +} from './schema'; +import get from 'lodash/get'; +import { findUiControl, getPropPath } from './uischema'; +import { + EnumOption, + enumToEnumOptionMapper, + oneOfToEnumOptionMapper, +} from './renderer'; const deriveLabel = ( controlElement: ControlElement, @@ -81,3 +100,69 @@ const labelDescription = (text: string, show: boolean): LabelDescription => ({ text: text, show: show, }); + +/** + * Compute the child label title for array based controls + * @param data the data of the control + * @param childPath the child path + * @param childLabelProp the dotted path to the value used as child label + * @param {JsonSchema} schema the json schema for this control + * @param {JsonSchema} rootSchema the root json schema + * @param {Translator} translateFct the translator fonction + * @param {UISchemaElement} uiSchema the uiSchema of the control + */ +export const computeChildLabel = ( + data: any, + childPath: string, + childLabelProp: string, + schema: JsonSchema, + rootSchema: JsonSchema, + translateFct: Translator, + uiSchema: UISchemaElement +): any => { + const childData = Resolve.data(data, childPath); + + childLabelProp ??= getFirstPrimitiveProp(schema); + + const currentValue = get(childData, childLabelProp, ''); + + if (!childLabelProp) return currentValue; + + const childSchema = Resolve.schema( + schema, + '#' + getPropPath(childLabelProp), + rootSchema + ); + + let enumOption: EnumOption = undefined; + if (isEnumSchema(childSchema)) { + enumOption = enumToEnumOptionMapper( + currentValue, + translateFct, + getI18nKeyPrefix( + childSchema, + findUiControl(uiSchema, childLabelProp), + childPath + '.' + childLabelProp + ) + ); + } else if (isOneOfEnumSchema(childSchema)) { + const oneOfArray = childSchema.oneOf as JsonSchema[]; + const oneOfSchema = oneOfArray.find( + (e: JsonSchema) => e.const === currentValue + ); + + if (oneOfSchema) { + enumOption = oneOfToEnumOptionMapper( + oneOfSchema, + translateFct, + getI18nKeyPrefix( + oneOfSchema, + undefined, + childPath + '.' + childLabelProp + ) + ); + } + } + + return enumOption ? enumOption.label : currentValue; +}; diff --git a/packages/core/src/util/schema.ts b/packages/core/src/util/schema.ts index 7ec39ae70..c20a6b23d 100644 --- a/packages/core/src/util/schema.ts +++ b/packages/core/src/util/schema.ts @@ -48,3 +48,11 @@ export const isOneOfEnumSchema = (schema: JsonSchema) => Object.prototype.hasOwnProperty.call(schema, 'oneOf') && schema.oneOf && (schema.oneOf as JsonSchema[]).every((s) => s.const !== undefined); + +/** + * Tests whether the schema has an enum. + */ +export const isEnumSchema = (schema: JsonSchema) => + !!schema && + ((Object.prototype.hasOwnProperty.call(schema, 'enum') && schema.enum) || + (Object.prototype.hasOwnProperty.call(schema, 'const') && schema.const)); diff --git a/packages/core/src/util/uischema.ts b/packages/core/src/util/uischema.ts index 05410bd83..a2fcac335 100644 --- a/packages/core/src/util/uischema.ts +++ b/packages/core/src/util/uischema.ts @@ -24,7 +24,13 @@ */ import isEmpty from 'lodash/isEmpty'; -import { isLayout, UISchemaElement } from '../models'; +import { + isControlElement, + isLayout, + isScoped, + UISchemaElement, +} from '../models'; +import { encode } from './path'; export type IterateCallback = (uischema: UISchemaElement) => void; @@ -55,3 +61,43 @@ export const iterateSchema = ( } toApply(uischema); }; + +/** + * Transform a dotted path to a uiSchema properties path + * @param path a dotted prop path to a schema value (i.e. articles.comment.author) + * @return the uiSchema properties path (i.e. /properties/articles/properties/comment/properties/author) + */ +export const getPropPath = (path: string): string => { + return `/properties/${path + .split('.') + .map((p) => encode(p)) + .join('/properties/')}`; +}; + +/** + * Find a control in a uiSchema, based on the dotted path of the schema value + * @param {UISchemaElement} uiSchema the uiSchema to search from + * @param path a dotted prop path to a schema value (i.e. articles.comment.author) + * @return {UISchemaElement} or undefined if not found + */ +export const findUiControl = ( + uiSchema: UISchemaElement, + path: string +): UISchemaElement | undefined => { + if (isControlElement(uiSchema)) { + if (isScoped(uiSchema) && uiSchema.scope.endsWith(getPropPath(path))) { + return uiSchema; + } else if (uiSchema.options?.detail) { + return findUiControl(uiSchema.options.detail, path); + } + } + + if (isLayout(uiSchema)) { + for (const elem of uiSchema.elements) { + const result = findUiControl(elem, path); + if (result !== undefined) return result; + } + } + + return undefined; +}; diff --git a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx index a7e11227d..0d7213fd1 100644 --- a/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx +++ b/packages/material-renderers/src/layouts/ExpandPanelRenderer.tsx @@ -1,5 +1,4 @@ import merge from 'lodash/merge'; -import get from 'lodash/get'; import React, { ComponentType, Dispatch, @@ -23,23 +22,13 @@ import { JsonSchema, moveDown, moveUp, - Resolve, update, JsonFormsCellRendererRegistryEntry, JsonFormsUISchemaRegistryEntry, - getFirstPrimitiveProp, createId, removeId, ArrayTranslations, - encode, - enumToEnumOptionMapper, - oneOfToEnumOptionMapper, - getI18nKeyPrefix, - Translator, - UISchemaElement, - EnumOption, - isLayout, - isScoped, + computeChildLabel, } from '@jsonforms/core'; import { Accordion, @@ -383,106 +372,6 @@ export const withContextToExpandPanelProps = ( }; }; -const hasEnumField = (schema: JsonSchema) => { - return schema && (schema.enum !== undefined || schema.const !== undefined); -}; - -const hasOneOfField = (schema: JsonSchema) => { - return schema && schema.oneOf !== undefined; -}; - -function getChildPropPath(childLabelProp: string) { - return `/properties/${childLabelProp - .split('.') - .map((p) => encode(p)) - .join('/properties/')}`; -} - -const isControlElement = ( - uiSchema: UISchemaElement -): uiSchema is ControlElement => uiSchema.type === 'Control'; - -const findUiControl = ( - uiSchema: UISchemaElement, - childLabelProp: string -): unknown | undefined => { - if (isControlElement(uiSchema)) { - if ( - isScoped(uiSchema) && - uiSchema.scope.endsWith(getChildPropPath(childLabelProp)) - ) { - return uiSchema; - } else if (uiSchema.options?.detail) { - return findUiControl(uiSchema.options.detail, childLabelProp); - } - } - - if (isLayout(uiSchema)) { - for (const elem of uiSchema.elements) { - const result = findUiControl(elem, childLabelProp); - if (result !== undefined) return result; - } - } - - return undefined; -}; - -const computeChildLabel = ( - data: any, - childPath: string, - childLabelProp: string, - schema: JsonSchema, - rootSchema: JsonSchema, - translateFct: Translator, - uiSchema: UISchemaElement -) => { - const childData = Resolve.data(data, childPath); - - childLabelProp ??= getFirstPrimitiveProp(schema); - - const currentValue = get(childData, childLabelProp, ''); - - if (!childLabelProp) return currentValue; - - const childSchema = Resolve.schema( - schema, - '#' + getChildPropPath(childLabelProp), - rootSchema - ); - - let enumOption: EnumOption = undefined; - if (hasEnumField(childSchema)) { - enumOption = enumToEnumOptionMapper( - currentValue, - translateFct, - getI18nKeyPrefix( - childSchema, - findUiControl(uiSchema, childLabelProp), - childPath + '.' + childLabelProp - ) - ); - } else if (hasOneOfField(childSchema)) { - const oneOfArray = childSchema.oneOf as JsonSchema[]; - const oneOfSchema = oneOfArray.find( - (e: JsonSchema) => e.const === currentValue - ); - - if (oneOfSchema) { - enumOption = oneOfToEnumOptionMapper( - oneOfSchema, - translateFct, - getI18nKeyPrefix( - oneOfSchema, - undefined, - childPath + '.' + childLabelProp - ) - ); - } - } - - return enumOption ? enumOption.label : currentValue; -}; - export const withJsonFormsExpandPanelProps = ( Component: ComponentType ): ComponentType => From f45a4f0139f33a41f29a1cf61845658ac35b640a Mon Sep 17 00:00:00 2001 From: slefebvre Date: Tue, 19 Mar 2024 15:03:20 +0100 Subject: [PATCH 08/12] Adding example page for arrays with translated custom element --- ...ys-with-translated-custom-element-label.ts | 283 ++++++++++++++++++ packages/examples/src/index.ts | 2 + 2 files changed, 285 insertions(+) create mode 100644 packages/examples/src/examples/arrays-with-translated-custom-element-label.ts diff --git a/packages/examples/src/examples/arrays-with-translated-custom-element-label.ts b/packages/examples/src/examples/arrays-with-translated-custom-element-label.ts new file mode 100644 index 000000000..ecd04e234 --- /dev/null +++ b/packages/examples/src/examples/arrays-with-translated-custom-element-label.ts @@ -0,0 +1,283 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +import { registerExamples } from '../register'; +import { JsonSchema7, Translator } from '@jsonforms/core'; + +export const data = { + article: { + title: 'title', + comments: [ + { + visibility: 'PUBLIC', + status: 'NEW', + author: { + name: 'John', + type: 'WRITER', + role: 'ROLE_1', + }, + }, + { + visibility: 'PRIVATE', + status: 'REVIEWED', + author: { + name: 'John', + type: 'AUTHOR', + role: 'ROLE_2', + }, + }, + ], + }, +}; + +const schema: JsonSchema7 = { + type: 'object', + properties: { + article: { + type: 'object', + properties: { + title: { + type: 'string', + }, + comments: { + type: 'array', + items: { + type: 'object', + properties: { + visibility: { + type: 'string', + enum: ['PUBLIC', 'PRIVATE'], + }, + status: { + type: 'string', + oneOf: [ + { + const: 'NEW', + }, + { + const: 'REVIEWED', + title: 'Reviewed comment', + }, + ], + }, + author: { + type: 'object', + properties: { + name: { + type: 'string', + }, + type: { + type: 'string', + enum: ['AUTHOR', 'WRITER'], + }, + role: { + type: 'string', + oneOf: [ + { + const: 'ROLE_1', + }, + { + const: 'ROLE_2', + title: 'Second role', + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; + +const detail = { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/visibility', + }, + { + type: 'Control', + scope: '#/properties/status', + }, + { + type: 'Control', + scope: '#/properties/author/properties/name', + }, + { + type: 'Control', + scope: '#/properties/author/properties/type', + }, + { + type: 'Control', + scope: '#/properties/author/properties/role', + }, + ], +}; + +export const uischema = { + type: 'VerticalLayout', + elements: [ + { + type: 'Group', + label: + 'Standard array control with elementLabelProp pointing on an direct enum (expected translated PUBLIC/PRIVATE)', + elements: [ + { + type: 'Control', + scope: '#/properties/article/properties/comments', + options: { + elementLabelProp: 'visibility', + detail: detail, + }, + }, + ], + }, + { + type: 'Group', + label: + 'Standard array control with elementLabelProp pointing on an direct oneOf (expected translated NEW/Reviewed comment)', + elements: [ + { + type: 'Control', + scope: '#/properties/article/properties/comments', + options: { + elementLabelProp: 'status', + detail: detail, + }, + }, + ], + }, + { + type: 'Group', + label: 'ListWithDetail with elementLabelProp pointing on an direct enum (expected translated PUBLIC/PRIVATE)', + elements: [ + { + type: 'ListWithDetail', + scope: '#/properties/article/properties/comments', + options: { + elementLabelProp: 'visibility', + detail: detail, + }, + }, + ], + }, + { + type: 'Group', + label: + 'ListWithDetail with elementLabelProp pointing on an direct oneOf (expected translated NEW/Reviewed comment)', + elements: [ + { + type: 'ListWithDetail', + scope: '#/properties/article/properties/comments', + options: { + elementLabelProp: 'status', + detail: detail, + }, + }, + ], + }, + { + type: 'Group', + label: + 'Standard array control with elementLabelProp pointing on an deep enum (expected translated WRITER/AUTHOR)', + elements: [ + { + type: 'Control', + scope: '#/properties/article/properties/comments', + options: { + elementLabelProp: 'author.type', + detail: detail, + }, + }, + ], + }, + { + type: 'Group', + label: + 'Standard array control with elementLabelProp pointing on an deep oneOf (expected translated ROLE_1/Second role)', + elements: [ + { + type: 'Control', + scope: '#/properties/article/properties/comments', + options: { + elementLabelProp: 'author.role', + detail: detail, + }, + }, + ], + }, + { + type: 'Group', + label: + 'ListWithDetail with elementLabelProp pointing on an deep enum (expected translated WRITER/AUTHOR)', + elements: [ + { + type: 'ListWithDetail', + scope: '#/properties/article/properties/comments', + options: { + elementLabelProp: 'author.type', + detail: detail, + }, + }, + ], + }, + { + type: 'Group', + label: + 'ListWithDetail with elementLabelProp pointing on an deep oneOf (expected translated ROLE_1/Second role)', + elements: [ + { + type: 'ListWithDetail', + scope: '#/properties/article/properties/comments', + options: { + elementLabelProp: 'author.role', + detail: detail, + }, + }, + ], + }, + ], +}; + +export const translate: Translator = (key: string) => { + return 'translator.' + key; +}; + +registerExamples([ + { + name: 'array-with-translated-custom-element-label', + label: 'Array with translated custom element label', + data, + schema, + uischema, + i18n: { + translate: translate, + locale: 'en', + }, + }, +]); diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 33486d225..0b4071619 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -35,6 +35,7 @@ import * as arrayWithDetail from './examples/arrays-with-detail'; import * as arrayWithDetailAndRule from './examples/arrays-with-detail-and-rule'; import * as arrayWithCustomChildLabel from './examples/arrays-with-custom-element-label'; import * as arrayWithSorting from './examples/arrays-with-sorting'; +import * as arrayWithTranslatedCustomChildLabel from './examples/arrays-with-translated-custom-element-label'; import * as arrayWithDefaults from './examples/arrays-with-defaults'; import * as stringArray from './examples/stringArray'; import * as categorization from './examples/categorization'; @@ -94,6 +95,7 @@ export { arrayWithDetailAndRule, arrayWithCustomChildLabel, arrayWithSorting, + arrayWithTranslatedCustomChildLabel, categorization, stepper, steppershownav, From 1e1cf30ad6d8e1e677a27f000705a7921ae425a9 Mon Sep 17 00:00:00 2001 From: slefebvre Date: Fri, 22 Mar 2024 17:17:25 +0100 Subject: [PATCH 09/12] Fix linting --- .../examples/arrays-with-translated-custom-element-label.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/examples/src/examples/arrays-with-translated-custom-element-label.ts b/packages/examples/src/examples/arrays-with-translated-custom-element-label.ts index ecd04e234..efd39f0ed 100644 --- a/packages/examples/src/examples/arrays-with-translated-custom-element-label.ts +++ b/packages/examples/src/examples/arrays-with-translated-custom-element-label.ts @@ -174,7 +174,8 @@ export const uischema = { }, { type: 'Group', - label: 'ListWithDetail with elementLabelProp pointing on an direct enum (expected translated PUBLIC/PRIVATE)', + label: + 'ListWithDetail with elementLabelProp pointing on an direct enum (expected translated PUBLIC/PRIVATE)', elements: [ { type: 'ListWithDetail', From 6726ac094c2c4b7ea917dd3ab10d36572679c7c6 Mon Sep 17 00:00:00 2001 From: slefebvre Date: Mon, 25 Mar 2024 16:55:30 +0100 Subject: [PATCH 10/12] Fix js syntax failing linting in vue --- packages/core/src/util/label.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/util/label.ts b/packages/core/src/util/label.ts index cd78c2031..a0acee34b 100644 --- a/packages/core/src/util/label.ts +++ b/packages/core/src/util/label.ts @@ -122,7 +122,7 @@ export const computeChildLabel = ( ): any => { const childData = Resolve.data(data, childPath); - childLabelProp ??= getFirstPrimitiveProp(schema); + if (!childLabelProp) childLabelProp = getFirstPrimitiveProp(schema); const currentValue = get(childData, childLabelProp, ''); From 8c0d2a4b012a5c59f8ceac6bd31a28f3a59f526f Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Thu, 18 Apr 2024 14:42:12 +0200 Subject: [PATCH 11/12] apply review comments --- packages/core/src/testers/testers.ts | 10 ++-------- packages/core/src/util/label.ts | 12 +++++++----- packages/core/src/util/schema.ts | 5 +++-- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/core/src/testers/testers.ts b/packages/core/src/testers/testers.ts index 415762ab4..0b993a69c 100644 --- a/packages/core/src/testers/testers.ts +++ b/packages/core/src/testers/testers.ts @@ -41,6 +41,7 @@ import type { import { deriveTypes, hasType, + isEnumSchema, isOneOfEnumSchema, resolveSchema, } from '../util'; @@ -369,14 +370,7 @@ export const isOneOfControl = and( */ export const isEnumControl = and( uiTypeIs('Control'), - or( - schemaMatches((schema) => - Object.prototype.hasOwnProperty.call(schema, 'enum') - ), - schemaMatches((schema) => - Object.prototype.hasOwnProperty.call(schema, 'const') - ) - ) + schemaMatches((schema) => isEnumSchema(schema)) ); /** diff --git a/packages/core/src/util/label.ts b/packages/core/src/util/label.ts index a0acee34b..cd5b517dc 100644 --- a/packages/core/src/util/label.ts +++ b/packages/core/src/util/label.ts @@ -46,6 +46,7 @@ import { enumToEnumOptionMapper, oneOfToEnumOptionMapper, } from './renderer'; +import isEqual from 'lodash/isEqual'; const deriveLabel = ( controlElement: ControlElement, @@ -122,12 +123,13 @@ export const computeChildLabel = ( ): any => { const childData = Resolve.data(data, childPath); - if (!childLabelProp) childLabelProp = getFirstPrimitiveProp(schema); + if (!childLabelProp) { + childLabelProp = getFirstPrimitiveProp(schema); + } const currentValue = get(childData, childLabelProp, ''); - if (!childLabelProp) return currentValue; - + // check whether the value is part of a oneOf or enum and needs to be translated const childSchema = Resolve.schema( schema, '#' + getPropPath(childLabelProp), @@ -147,8 +149,8 @@ export const computeChildLabel = ( ); } else if (isOneOfEnumSchema(childSchema)) { const oneOfArray = childSchema.oneOf as JsonSchema[]; - const oneOfSchema = oneOfArray.find( - (e: JsonSchema) => e.const === currentValue + const oneOfSchema = oneOfArray.find((e: JsonSchema) => + isEqual(e.const, currentValue) ); if (oneOfSchema) { diff --git a/packages/core/src/util/schema.ts b/packages/core/src/util/schema.ts index c20a6b23d..222ac9a7b 100644 --- a/packages/core/src/util/schema.ts +++ b/packages/core/src/util/schema.ts @@ -54,5 +54,6 @@ export const isOneOfEnumSchema = (schema: JsonSchema) => */ export const isEnumSchema = (schema: JsonSchema) => !!schema && - ((Object.prototype.hasOwnProperty.call(schema, 'enum') && schema.enum) || - (Object.prototype.hasOwnProperty.call(schema, 'const') && schema.const)); + typeof schema === 'object' && + (Object.prototype.hasOwnProperty.call(schema, 'enum') || + Object.prototype.hasOwnProperty.call(schema, 'const')); From 4284d6e63209acb801f110771219171cf1ebf7e4 Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Thu, 18 Apr 2024 16:09:14 +0200 Subject: [PATCH 12/12] fix label determination --- packages/core/src/util/label.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/util/label.ts b/packages/core/src/util/label.ts index cd5b517dc..64cd4ef2c 100644 --- a/packages/core/src/util/label.ts +++ b/packages/core/src/util/label.ts @@ -120,13 +120,18 @@ export const computeChildLabel = ( rootSchema: JsonSchema, translateFct: Translator, uiSchema: UISchemaElement -): any => { +): string => { const childData = Resolve.data(data, childPath); if (!childLabelProp) { childLabelProp = getFirstPrimitiveProp(schema); } + // return early in case there is no prop we can query + if (!childLabelProp) { + return ''; + } + const currentValue = get(childData, childLabelProp, ''); // check whether the value is part of a oneOf or enum and needs to be translated