diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index ca565af748f..9c5633665ad 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1440,6 +1440,7 @@ "ux_editor.component_properties.minNumberOfAttachments": "Minimum antall vedlegg", "ux_editor.component_properties.mode": "Modus", "ux_editor.component_properties.openInNewTab": "Lenken skal åpnes i ny fane", + "ux_editor.component_properties.optionalIndicator": "Vis valgfri-indikator på ledetekst", "ux_editor.component_properties.options": "Alternativer", "ux_editor.component_properties.optionsId": "Kodeliste", "ux_editor.component_properties.pageBreak": "PDF-innstillinger (pageBreak)", diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx index 1b7112297e8..8d812199f11 100644 --- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx +++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx @@ -96,6 +96,87 @@ describe('FormComponentConfig', () => { expect(screen.queryByText('nullProperty')).not.toBeInTheDocument(); }); + it('should render nothing if schema is undefined', () => { + render({ + props: { + schema: undefined, + }, + }); + expect( + screen.queryByText(textMock(`ux_editor.component_properties.grid`)), + ).not.toBeInTheDocument(); + }); + + it('should render nothing if schema properties are undefined', () => { + render({ + props: { + schema: { + properties: undefined, + }, + }, + }); + expect( + screen.queryByText(textMock(`ux_editor.component_properties.grid`)), + ).not.toBeInTheDocument(); + }); + + it('should not render property if it is unsupported', () => { + render({ + props: { + schema: { + ...InputSchema, + properties: { + ...InputSchema.properties, + unsupportedProperty: { + type: 'object', + properties: {}, + additionalProperties: { + type: 'string', + }, + }, + }, + }, + }, + }); + expect( + screen.queryByText(textMock(`ux_editor.component_properties.unsupportedProperty`)), + ).not.toBeInTheDocument(); + }); + + it('should only render array properties with items of type string AND enum values', () => { + render({ + props: { + schema: { + ...InputSchema, + properties: { + ...InputSchema.properties, + supportedArrayProperty: { + type: 'array', + items: { + type: 'string', + enum: ['option1', 'option2'], + }, + }, + unsupportedArrayProperty: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + }); + expect( + screen.getByRole('combobox', { + name: textMock(`ux_editor.component_properties.supportedArrayProperty`), + }), + ).toBeInTheDocument(); + expect( + screen.queryByLabelText(textMock(`ux_editor.component_properties.unsupportedArrayProperty`)), + ).not.toBeInTheDocument(); + }); + const render = ({ props = {}, queries = {}, diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx index 329b5573fc5..dee2fe7d7e2 100644 --- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx +++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import { Alert, Heading } from '@digdir/design-system-react'; +import { Alert, Card, Heading, Paragraph } from '@digdir/design-system-react'; import type { FormComponent } from '../../types/FormComponent'; import { EditBooleanValue } from './editModal/EditBooleanValue'; import { EditNumberValue } from './editModal/EditNumberValue'; import { EditStringValue } from './editModal/EditStringValue'; -import { useText } from '../../hooks'; +import { useComponentPropertyLabel, useText } from '../../hooks'; import { - ExpressionSchemaBooleanDefinitionReference, - getUnsupportedPropertyTypes, + PropertyTypes, + propertyKeysToExcludeFromComponentConfig, + getSupportedPropertyKeysForPropertyType, } from '../../utils/component'; import { EditGrid } from './editModal/EditGrid'; import type { FormItem } from '../../types/FormItem'; @@ -22,6 +23,7 @@ export interface FormComponentConfigProps extends IEditFormComponentProps { schema: any; hideUnsupported?: boolean; } + export const FormComponentConfig = ({ schema, editFormId, @@ -30,26 +32,53 @@ export const FormComponentConfig = ({ hideUnsupported, }: FormComponentConfigProps) => { const t = useText(); + const componentPropertyLabel = useComponentPropertyLabel(); if (!schema?.properties) return null; - const { - children, - dataModelBindings, - required, - readOnly, - id, - textResourceBindings, - type, - options, - optionsId, - hasCustomFileEndings, - validFileEndings, - grid, - ...rest - } = schema.properties; + const { properties } = schema; + const { hasCustomFileEndings, validFileEndings, grid } = properties; + + // Add any properties that have a custom implementation to this list so they are not duplicated in the generic view + const customProperties = ['hasCustomFileEndings', 'validFileEndings', 'grid', 'children']; + + const booleanPropertyKeys: string[] = getSupportedPropertyKeysForPropertyType( + schema.properties, + [PropertyTypes.boolean], + customProperties, + ); + const stringPropertyKeys: string[] = getSupportedPropertyKeysForPropertyType( + schema.properties, + [PropertyTypes.string], + customProperties, + ); + const numberPropertyKeys: string[] = getSupportedPropertyKeysForPropertyType( + schema.properties, + [PropertyTypes.number, PropertyTypes.integer], + customProperties, + ); + const arrayPropertyKeys: string[] = getSupportedPropertyKeysForPropertyType( + schema.properties, + [PropertyTypes.array], + customProperties, + ); + const objectPropertyKeys: string[] = getSupportedPropertyKeysForPropertyType( + schema.properties, + [PropertyTypes.object], + [...customProperties, 'source'], + ); - const unsupportedPropertyKeys: string[] = getUnsupportedPropertyTypes(rest); + const unsupportedPropertyKeys: string[] = Object.keys(properties).filter((key) => { + return ( + !booleanPropertyKeys.includes(key) && + !stringPropertyKeys.includes(key) && + !numberPropertyKeys.includes(key) && + !arrayPropertyKeys.includes(key) && + !objectPropertyKeys.includes(key) && + !customProperties.includes(key) && + !propertyKeysToExcludeFromComponentConfig.includes(key) + ); + }); return ( <> @@ -71,6 +100,20 @@ export const FormComponentConfig = ({ )} + {/** Boolean fields, incl. expression type */} + {booleanPropertyKeys.map((propertyKey) => { + return ( + + ); + })} + + {/** Custom logic for custom file endings */} {hasCustomFileEndings && ( <> )} - {readOnly && ( - - )} - {required && ( - - )} + {/** String properties */} + {stringPropertyKeys.map((propertyKey) => { + return ( + + ); + })} - {Object.keys(rest).map((propertyKey) => { - if (!rest[propertyKey]) return null; - if ( - rest[propertyKey].type === 'boolean' || - rest[propertyKey].$ref?.endsWith(ExpressionSchemaBooleanDefinitionReference) - ) { - return ( - - ); - } - if (rest[propertyKey].type === 'number' || rest[propertyKey].type === 'integer') { - return ( - - ); - } - if (rest[propertyKey].type === 'string') { - return ( - - ); - } - if (rest[propertyKey].type === 'array' && rest[propertyKey].items?.type === 'string') { - return ( - { + return ( + + ); + })} + + {/** Array properties with enum values) */} + {arrayPropertyKeys.map((propertyKey) => { + return ( + + ); + })} + + {/** Object properties */} + {objectPropertyKeys.map((propertyKey) => { + return ( + + + {componentPropertyLabel(propertyKey)} + + {properties[propertyKey]?.description && ( + {properties[propertyKey].description} + )} + { + handleComponentUpdate({ + ...component, + [propertyKey]: updatedComponent, + }); + }} + editFormId={editFormId} + hideUnsupported /> - ); - } - return null; + + ); })} {/* Show information about unsupported properties if there are any */} {unsupportedPropertyKeys.length > 0 && !hideUnsupported && ( diff --git a/frontend/packages/ux-editor/src/hooks/index.ts b/frontend/packages/ux-editor/src/hooks/index.ts index 6e3184f1882..e0ca80c4929 100644 --- a/frontend/packages/ux-editor/src/hooks/index.ts +++ b/frontend/packages/ux-editor/src/hooks/index.ts @@ -9,3 +9,4 @@ export { useText } from './useText'; export { useTextResourcesSelector } from './useTextResourcesSelector'; export type { ComponentValidationResult, ErrorCode } from './useValidateComponent'; export { useValidateComponent } from './useValidateComponent'; +export { useComponentPropertyLabel } from './useComponentPropertyLabel'; diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Address.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Address.schema.v1.json index f5b63dc9724..f044bad4956 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Address.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Address.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Checkboxes.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Checkboxes.schema.v1.json index 1eb0fb17ae6..1b42a5d2716 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Checkboxes.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Checkboxes.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Custom.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Custom.schema.v1.json index 1fc24d2823a..0645193d91f 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Custom.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Custom.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Datepicker.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Datepicker.schema.v1.json index 74650b91972..a77cf3bd0cb 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Datepicker.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Datepicker.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Dropdown.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Dropdown.schema.v1.json index bec256a7946..eccd8e5c282 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Dropdown.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Dropdown.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/FileUpload.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/FileUpload.schema.v1.json index 2ab4af18e4f..d4ddf60e01f 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/FileUpload.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/FileUpload.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/FileUploadWithTag.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/FileUploadWithTag.schema.v1.json index 37981018c93..31be3d04567 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/FileUploadWithTag.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/FileUploadWithTag.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Input.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Input.schema.v1.json index 6e4fd54acfa..45b0aa78a64 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Input.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Input.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { @@ -170,6 +171,7 @@ "currency": { "title": "Language-sensitive currency formatting", "description": "Enables currency to be language sensitive based on selected app language. Note: parts that already exist in number property are not overridden by this prop.", + "type": "string", "enum": [ "AED", "AFN", diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Likert.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Likert.schema.v1.json index de171823129..c3b49abf956 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Likert.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Likert.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/LikertItem.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/LikertItem.schema.v1.json index 4d40e589715..874eb7e94db 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/LikertItem.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/LikertItem.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/List.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/List.schema.v1.json index a20b22eac7d..836bc54b2d7 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/List.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/List.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Map.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Map.schema.v1.json index 411ae99872c..1b42456f7e0 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Map.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Map.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/MultipleSelect.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/MultipleSelect.schema.v1.json index 71ab6cfcbff..4c66ce2e2f0 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/MultipleSelect.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/MultipleSelect.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/RadioButtons.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/RadioButtons.schema.v1.json index 2cb3d268ceb..86acb101d06 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/RadioButtons.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/RadioButtons.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/RepeatingGroup.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/RepeatingGroup.schema.v1.json index 2a968b95a47..e8a9bc23ca5 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/RepeatingGroup.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/RepeatingGroup.schema.v1.json @@ -140,7 +140,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "edit": { diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/TextArea.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/TextArea.schema.v1.json index 3ecda35cb04..644605bc6c8 100644 --- a/frontend/packages/ux-editor/src/testing/schemas/json/component/TextArea.schema.v1.json +++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/TextArea.schema.v1.json @@ -72,7 +72,8 @@ "Required", "AllExceptRequired", "All" - ] + ], + "type": "string" } }, "renderAsSummary": { diff --git a/frontend/packages/ux-editor/src/utils/component.test.ts b/frontend/packages/ux-editor/src/utils/component.test.ts index 6b235109ab1..8bf2c65bea5 100644 --- a/frontend/packages/ux-editor/src/utils/component.test.ts +++ b/frontend/packages/ux-editor/src/utils/component.test.ts @@ -3,11 +3,14 @@ import { addOptionToComponent, changeComponentOptionLabel, changeTextResourceBinding, - ExpressionSchemaBooleanDefinitionReference, + getExpressionSchemaDefinitionReference, generateFormItem, - getUnsupportedPropertyTypes, isPropertyTypeSupported, setComponentProperty, + EXPRESSION_SCHEMA_BASE_DEFINITION_REFERENCE, + PropertyTypes, + propertyTypeMatcher, + getSupportedPropertyKeysForPropertyType, } from './component'; import { ComponentType } from 'app-shared/types/ComponentType'; import type { @@ -200,113 +203,233 @@ describe('Component utils', () => { }); }); - describe('getUnsupportedPropertyTypes', () => { - it('Returns empty array when only properties are provided', () => { - const properties = { - testProperty1: { - type: 'string', - }, - testProperty2: { - type: 'number', - }, - testProperty3: { - type: 'array', - items: { - type: 'string', - }, - }, - testProperty4: { - $ref: ExpressionSchemaBooleanDefinitionReference, - }, - testProperty5: { - type: 'integer', - }, - testProperty6: { - type: 'object', - }, - testProperty7: { - type: 'boolean', - }, - }; - expect(getUnsupportedPropertyTypes(properties)).toEqual(['testProperty6']); + describe('isPropertyTypeSupported', () => { + it('should return true if property type is supported', () => { + [ + PropertyTypes.boolean, + PropertyTypes.number, + PropertyTypes.integer, + PropertyTypes.string, + PropertyTypes.object, + PropertyTypes.array, + ].forEach((type) => { + expect( + isPropertyTypeSupported({ + type, + }), + ).toBe(true); + }); }); - it('Returns empty array when no properties are provided', () => { - const properties = {}; - expect(getUnsupportedPropertyTypes(properties)).toEqual([]); + + it('should return true if property ref is supported', () => { + [ + PropertyTypes.boolean, + PropertyTypes.number, + PropertyTypes.integer, + PropertyTypes.string, + ].forEach((type) => { + expect( + isPropertyTypeSupported({ + $ref: getExpressionSchemaDefinitionReference(type), + }), + ).toBe(true); + }); }); - it('Returns array of unsupported property keys when known unsupported property keys are provided', () => { - const properties = { - children: 'testValue', - }; - expect(getUnsupportedPropertyTypes(properties, ['children'])).toEqual(['children']); + + it('should return false if property ref is not supported', () => { + [PropertyTypes.object, PropertyTypes.array].forEach((type) => { + expect( + isPropertyTypeSupported({ + $ref: getExpressionSchemaDefinitionReference(type), + }), + ).toBe(false); + }); }); - it('Returns array of unsupported property keys when unsupported property keys are given', () => { - const properties = { - testProperty1: { - $ref: 'testRef', - }, - testProperty2: { - type: 'array', - items: { - type: 'object', - }, - }, - testProperty3: { - type: 'string', - }, - }; - const result = getUnsupportedPropertyTypes(properties); - expect(result).toEqual(['testProperty1', 'testProperty2']); + + it('should return false if property ref is not supported', () => { + expect( + isPropertyTypeSupported({ + $ref: 'test', + }), + ).toBe(false); }); - }); - describe('isPropertyTypeSupported', () => { - it('should return true if property type is supported', () => { + it('should return true if property type is supported and propertyKey is undefined', () => { expect( isPropertyTypeSupported({ type: 'string', }), ).toBe(true); }); + }); + + describe('getExpressionSchemaDefinitionReference', () => { + it('should return correct reference for given type', () => { + expect(getExpressionSchemaDefinitionReference(PropertyTypes.array)).toBe( + `${EXPRESSION_SCHEMA_BASE_DEFINITION_REFERENCE}array`, + ); + }); + }); - it('should return true if property ref is supported', () => { + describe('propertyTypeMatcher', () => { + it('should return false if property does not exist', () => { + expect(propertyTypeMatcher(undefined, PropertyTypes.string)).toBe(false); + }); + + it('should return true if property type matches', () => { + expect(propertyTypeMatcher({ type: 'string' }, PropertyTypes.string)).toBe(true); + }); + + it('should return false if property type does not match', () => { + expect(propertyTypeMatcher({ type: 'number' }, PropertyTypes.string)).toBe(false); + }); + + it('should return true if property has a supported ref', () => { expect( - isPropertyTypeSupported({ - $ref: ExpressionSchemaBooleanDefinitionReference, - }), + propertyTypeMatcher( + { + $ref: getExpressionSchemaDefinitionReference(PropertyTypes.string), + }, + PropertyTypes.string, + ), ).toBe(true); }); - it('should return true for property of array type with items that are type string', () => { + + it('should return true for a property of string type with enum even if type: string is not defined explicitly', () => { expect( - isPropertyTypeSupported({ - type: 'array', - items: { - type: 'string', + propertyTypeMatcher( + { + enum: ['test'], }, - }), + PropertyTypes.string, + ), ).toBe(true); }); - it('should return false for property type object', () => { + + it('should return false for a property with no type defined and no enum defined', () => { + expect(propertyTypeMatcher({ something: 'test' }, PropertyTypes.string)).toBe(false); + }); + + it('should return true for a property of array type with items that have enum value', () => { expect( - isPropertyTypeSupported({ - type: 'object', - }), + propertyTypeMatcher( + { + type: 'array', + items: { + enum: ['test'], + }, + }, + PropertyTypes.array, + ), + ).toBe(true); + }); + + it('should return false for a property of array type with items that have no enum value', () => { + expect( + propertyTypeMatcher( + { + type: 'array', + items: { + type: 'string', + }, + }, + PropertyTypes.array, + ), ).toBe(false); }); - it('should return false if property ref is not supported', () => { + + it('should return false for a property of array type with no items defined', () => { + expect(propertyTypeMatcher({ type: 'array' }, PropertyTypes.array)).toBe(false); + }); + + it('should return false for a property of object type with no properties defined', () => { + expect(propertyTypeMatcher({ type: 'object' }, PropertyTypes.object)).toBe(false); + }); + + it('should return false for a property of object type with additionalProperties defined', () => { expect( - isPropertyTypeSupported({ - $ref: 'test', - }), + propertyTypeMatcher( + { + type: 'object', + properties: {}, + additionalProperties: { + type: 'string', + }, + }, + PropertyTypes.object, + ), ).toBe(false); }); - it('should return true if property type is supported and propertyKey is undefined', () => { + it('should return true for a property of object type with defined properties and no additionalProperties', () => { expect( - isPropertyTypeSupported({ - type: 'string', - }), + propertyTypeMatcher( + { + type: 'object', + properties: { + testProperty: { + type: 'string', + }, + }, + }, + PropertyTypes.object, + ), ).toBe(true); }); }); + + describe('getSupportedPropertyKeysForPropertyType', () => { + it('should return empty array if no properties are provided', () => { + expect(getSupportedPropertyKeysForPropertyType({}, [PropertyTypes.string])).toEqual([]); + }); + + it('should return empty array if no property keys are of the expected property types', () => { + expect( + getSupportedPropertyKeysForPropertyType( + { + testProperty: { + type: 'number', + }, + }, + [PropertyTypes.string], + ), + ).toEqual([]); + }); + + it('should return array of property keys of the expected property types', () => { + expect( + getSupportedPropertyKeysForPropertyType( + { + testProperty: { + type: 'string', + }, + testProperty2: { + type: 'number', + }, + }, + [PropertyTypes.string], + ), + ).toEqual(['testProperty']); + }); + + it('should only return property keys that are not in the excludeKeys array', () => { + expect( + getSupportedPropertyKeysForPropertyType( + { + testProperty: { + type: 'string', + }, + testProperty1: { + type: 'string', + }, + testProperty2: { + type: 'number', + }, + }, + [PropertyTypes.string], + ['testProperty'], + ), + ).toEqual(['testProperty1']); + }); + }); }); diff --git a/frontend/packages/ux-editor/src/utils/component.ts b/frontend/packages/ux-editor/src/utils/component.ts index 1f4d755acf6..8b057346628 100644 --- a/frontend/packages/ux-editor/src/utils/component.ts +++ b/frontend/packages/ux-editor/src/utils/component.ts @@ -42,6 +42,25 @@ export enum AddressKeys { houseNumber = 'houseNumber', } +export enum PropertyTypes { + boolean = 'boolean', + number = 'number', + integer = 'integer', + string = 'string', + object = 'object', + array = 'array', +} + +// Add any properties that are rendered elsewhere to this list so they are not duplicated in the generic view +export const propertyKeysToExcludeFromComponentConfig = [ + 'id', + 'type', + 'dataModelBindings', + 'textResourceBindings', + 'options', + 'optionsId', +]; + export const changeTextResourceBinding = ( component: FormComponent, bindingKey: string, @@ -54,6 +73,60 @@ export const changeTextResourceBinding = ( }, }); +/** + * Function that returns true if the given property matches the required + * conditions for the provided property type. + * @param propertyKey The key of the property to check. + * @param propertyType The expected property type to check for. + * @param properties The properties to check. + * @returns + */ +export const propertyTypeMatcher = (property: KeyValuePairs, propertyType: PropertyTypes) => { + if (!property) return false; + const baseMatch = + property.type === propertyType || + property.$ref?.endsWith(getExpressionSchemaDefinitionReference(propertyType)); + + switch (propertyType) { + case PropertyTypes.string: + // Not all schemas with enum value explicitly specifies type as string + return baseMatch || !!property.enum; + case PropertyTypes.array: + // Currently only supporting array of strings with specified enum values + return baseMatch && !!property.items?.enum; + case PropertyTypes.object: + // Currently only supporting object with specifiec properties and no additional properties + return baseMatch && !!property.properties && !property.additionalProperties; + default: + return baseMatch; + } +}; + +/** + * Function that returns an array of supported property keys for the given property type(s). + * @param properties The properties to check. + * @param propertyTypes The expected property types to check for. + * @param excludeKeys Property keys that should be excluded from the result. + * @returns An array of supported property keys. + */ +export const getSupportedPropertyKeysForPropertyType = ( + properties: KeyValuePairs, + propertyTypes: PropertyTypes[], + excludeKeys: string[] = [], +) => { + return Object.keys(properties).filter((key) => { + if ( + !properties[key] || + !isPropertyTypeSupported(properties[key]) || + excludeKeys.includes(key) || + propertyKeysToExcludeFromComponentConfig.includes(key) + ) + return false; + + return propertyTypes.find((propertyType) => propertyTypeMatcher(properties[key], propertyType)); + }); +}; + export const changeTitleBinding = (component: FormComponent, resourceKey: string): FormComponent => changeTextResourceBinding(component, 'title', resourceKey); @@ -116,37 +189,24 @@ export const setComponentProperty = < [propertyKey]: value, }); -export const ExpressionSchemaBooleanDefinitionReference = - 'expression.schema.v1.json#/definitions/boolean'; +export const EXPRESSION_SCHEMA_BASE_DEFINITION_REFERENCE = + 'expression.schema.v1.json#/definitions/' as const; -/** - * Gets an array of unsupported property keys - * @param properties The properties object to check. - * @param knownUnsupportedPropertyKeys An array of additional known unsupported property keys. - * @returns An array of unsupported property keys. - */ -export const getUnsupportedPropertyTypes = ( - properties: KeyValuePairs, - knownUnsupportedPropertyKeys?: string[], -) => { - const propertyKeys = Object.keys(properties); - let unsupportedPropertyKeys = propertyKeys - .filter((key) => - knownUnsupportedPropertyKeys ? !knownUnsupportedPropertyKeys.includes(key) : true, - ) - .filter((key) => { - return !isPropertyTypeSupported(properties[key]); - }); - - if (knownUnsupportedPropertyKeys) { - unsupportedPropertyKeys = unsupportedPropertyKeys.concat(knownUnsupportedPropertyKeys); - } - - return unsupportedPropertyKeys; +export const getExpressionSchemaDefinitionReference = (type: PropertyTypes) => { + return `${EXPRESSION_SCHEMA_BASE_DEFINITION_REFERENCE}${type}`; }; -const supportedPropertyTypes = ['boolean', 'number', 'integer', 'string']; -const supportedPropertyRefs = [ExpressionSchemaBooleanDefinitionReference]; +const supportedPropertyTypes = [ + PropertyTypes.boolean, + PropertyTypes.number, + PropertyTypes.integer, + PropertyTypes.string, + PropertyTypes.object, + PropertyTypes.array, +]; +const supportedPropertyRefs = supportedPropertyTypes + .filter((p) => p !== PropertyTypes.object && p !== PropertyTypes.array) + .map((type) => getExpressionSchemaDefinitionReference(type)); /** * Checks if a given property with optional property key is supported by component config view. @@ -157,8 +217,6 @@ export const isPropertyTypeSupported = (property: KeyValuePairs) => { if (property?.$ref) { return supportedPropertyRefs.includes(property.$ref); } - if (property?.type === 'array' && property?.items?.type === 'string') { - return true; - } + return supportedPropertyTypes.includes(property?.type); }; diff --git a/frontend/scripts/componentSchemas/schemaUtils.ts b/frontend/scripts/componentSchemas/schemaUtils.ts index e07d6863472..f3fde6b083b 100644 --- a/frontend/scripts/componentSchemas/schemaUtils.ts +++ b/frontend/scripts/componentSchemas/schemaUtils.ts @@ -68,6 +68,8 @@ export const expandRef = (ref: string, layoutSchema: any) => { export const ensureStringTypeWithEnums = (schema: any) => { if (schema.enum) { schema.type = 'string'; + } else if (schema.items?.enum) { + schema.items.type = 'string'; } };