From 0aae45962960b5a17076e4532d919a0c661c83a9 Mon Sep 17 00:00:00 2001 From: Bogdan Nechyporenko Date: Thu, 2 Mar 2023 14:24:58 +0100 Subject: [PATCH 1/4] Fix the scaffolder validator for arrays when the item is a field in the object Signed-off-by: Bogdan Nechyporenko --- .changeset/silent-dryers-end.md | 5 ++ .../TemplatePage/createValidator.test.ts | 72 +++++++++++++++++++ .../TemplatePage/createValidator.ts | 52 +++++++------- 3 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 .changeset/silent-dryers-end.md diff --git a/.changeset/silent-dryers-end.md b/.changeset/silent-dryers-end.md new file mode 100644 index 0000000000..642a5fb7da --- /dev/null +++ b/.changeset/silent-dryers-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Fix the scaffolder validator for arrays when the item is a field in the object diff --git a/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts b/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts index f5be77811a..1858e40241 100644 --- a/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts +++ b/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts @@ -19,6 +19,12 @@ import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; import { ApiHolder } from '@backstage/core-plugin-api'; import { FieldValidation, FormValidation } from '@rjsf/core'; +type CustomLinkType = { + url: string; + title: string; + icon: string; +}; + describe('createValidator', () => { const validators: Record> = { @@ -31,6 +37,23 @@ describe('createValidator', () => { fieldValidation.addError('Error !'); } }, + CustomLink: ( + values: unknown, + fieldValidation: FieldValidation, + _context: { apiHolder: ApiHolder }, + ) => { + const input = values as CustomLinkType[]; + for (const item of input) { + const validGitlabUrlRegex = + /gitlab\.(?:stg\.)?spotify\.com\?owner=.*&repo=.*/; + + if (!item || !validGitlabUrlRegex.test(item.url)) { + fieldValidation.addError( + `Make sure to put in a valid gitlab clone url.`, + ); + } + } + }, TagPicker: ( values: unknown, fieldValidation: FieldValidation, @@ -123,4 +146,53 @@ describe('createValidator', () => { expect(result).not.toBeNull(); expect(result.tags.addError).toHaveBeenCalledTimes(1); }); + + it('should call validator for array object property from a custom field extension', () => { + /* GIVEN */ + const rootSchema = { + title: 'My links', + properties: { + links: { + title: 'Links', + type: 'array', + items: { + type: 'object', + required: ['url', 'title', 'icon'], + properties: { + url: { + title: 'url', + description: 'url', + type: 'object', + 'ui:field': 'CustomLink', + }, + }, + }, + }, + }, + }; + const validator = createValidator(rootSchema, validators, context); + + const formData = { + links: [ + { + url: 'http://gitlab.spotify.nl/owener=me&repo=test', + icon: 'subject', + title: 'My repository for testing features', + } as CustomLinkType, + ], + }; + const errors = { + addError: jest.fn(), + links: { + addError: jest.fn(), + } as unknown as FormValidation, + } as unknown as FormValidation; + + /* WHEN */ + const result = validator(formData, errors); + + /* THEN */ + expect(result).not.toBeNull(); + expect(result.links.addError).toHaveBeenCalledTimes(1); + }); }); diff --git a/plugins/scaffolder/src/components/TemplatePage/createValidator.ts b/plugins/scaffolder/src/components/TemplatePage/createValidator.ts index 123a7e800e..8bfe8c8313 100644 --- a/plugins/scaffolder/src/components/TemplatePage/createValidator.ts +++ b/plugins/scaffolder/src/components/TemplatePage/createValidator.ts @@ -50,40 +50,42 @@ export const createValidator = ( for (const [key, propData] of Object.entries(formData)) { const propValidation = errors[key]; - const propSchemaProps = schemaProps[key]; - if (isObject(propData)) { - if (isObject(propSchemaProps)) { - validate( - propSchemaProps, - propData as JsonObject, - propValidation as FormValidation, - ); + const doValidate = (item: JsonValue | undefined) => { + if (item && isObject(item)) { + const fieldName = item['ui:field'] as string; + if (fieldName && typeof validators[fieldName] === 'function') { + validators[fieldName]!( + propData as JsonObject[], + propValidation, + context, + ); + } } + }; + + const propSchemaProps = schemaProps[key]; + if (isObject(propData) && isObject(propSchemaProps)) { + validate( + propSchemaProps, + propData as JsonObject, + propValidation as FormValidation, + ); } else if (isArray(propData)) { if (isObject(propSchemaProps)) { const { items } = propSchemaProps; if (isObject(items)) { - const fieldName = items['ui:field'] as string; - if (fieldName && typeof validators[fieldName] === 'function') { - validators[fieldName]!( - propData as JsonObject[], - propValidation, - context, - ); + if (items.type === 'object') { + const properties = (items?.properties ?? []) as JsonObject[]; + for (const [, value] of Object.entries(properties)) { + doValidate(value); + } + } else { + doValidate(items); } } } } else { - const fieldName = - isObject(propSchemaProps) && - (propSchemaProps['ui:field'] as string); - if (fieldName && typeof validators[fieldName] === 'function') { - validators[fieldName]!( - propData as JsonValue, - propValidation, - context, - ); - } + doValidate(propSchemaProps); } } } else if (customObject) { From 6ca13414f25faaac6fcd9a95b0ada1f0ca3af71d Mon Sep 17 00:00:00 2001 From: Bogdan Nechyporenko Date: Thu, 2 Mar 2023 15:37:43 +0100 Subject: [PATCH 2/4] Fix the scaffolder validator for arrays when the item is a field in the object Signed-off-by: Bogdan Nechyporenko --- .../Stepper/createAsyncValidators.test.ts | 35 +++++++++++++++++++ .../Stepper/createAsyncValidators.ts | 20 +++++++++++ 2 files changed, 55 insertions(+) diff --git a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts index 38543b2148..3633410769 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts @@ -406,4 +406,39 @@ describe('createAsyncValidators', () => { expect(validators.TagField).not.toHaveBeenCalled(); }); + + it('should call validator for array object property from a custom field extension', async () => { + const schema: JsonObject = { + type: 'object', + properties: { + links: { + title: 'Links', + type: 'array', + items: { + type: 'object', + required: ['url', 'title', 'icon'], + properties: { + url: { + title: 'url', + description: 'url', + type: 'object', + 'ui:field': 'CustomLinkField', + }, + }, + }, + }, + }, + }; + const validators = { CustomLinkField: jest.fn() }; + + const validate = createAsyncValidators(schema, validators, { + apiHolder: { get: jest.fn() }, + }); + + await validate({ + links: [{ url: 'http://my-url.spotify.com' }], + }); + + expect(validators.CustomLinkField).toHaveBeenCalled(); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts index 31185d1c12..f5f3088c93 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts @@ -103,6 +103,26 @@ export const createAsyncValidators = ( itemsUiSchema, ); } + } else if ( + definitionInSchema && + definitionInSchema.items && + definitionInSchema.items.type === 'object' + ) { + const properties = (definitionInSchema.items?.properties ?? + []) as JsonObject[]; + for (const [, propValue] of Object.entries(properties)) { + if ('ui:field' in propValue) { + const { schema: itemsSchema, uiSchema: itemsUiSchema } = + extractSchemaFromStep(definitionInSchema.items); + await validateForm( + propValue['ui:field'] as string, + key, + value, + itemsSchema, + itemsUiSchema, + ); + } + } } else if (isObject(value)) { formValidation[key] = await validate(formData, path, value); } From 3ce12b6f0c8e61652ac7c7685144d7fadf6a9c78 Mon Sep 17 00:00:00 2001 From: Bogdan Nechyporenko Date: Thu, 2 Mar 2023 15:53:37 +0100 Subject: [PATCH 3/4] Fix the scaffolder validator for arrays when the item is a field in the object Signed-off-by: Bogdan Nechyporenko --- .../Stepper/createAsyncValidators.ts | 66 ++++++++----------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts index f5f3088c93..ceef701255 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts @@ -77,51 +77,39 @@ export const createAsyncValidators = ( const definitionInSchema = parsedSchema.getSchema(path, formData); const { schema, uiSchema } = extractSchemaFromStep(definitionInSchema); - if (definitionInSchema && 'ui:field' in definitionInSchema) { - if ('ui:field' in definitionInSchema) { - await validateForm( - definitionInSchema['ui:field'], - key, - value, - schema, - uiSchema, - ); - } - } else if ( - definitionInSchema && - definitionInSchema.items && - 'ui:field' in definitionInSchema.items - ) { - if ('ui:field' in definitionInSchema.items) { + const hasItems = definitionInSchema && definitionInSchema.items; + + const doValidateItem = async ( + propValue: JsonObject, + itemSchema: JsonObject, + itemUiSchema: NextFieldExtensionUiSchema, + ) => { + await validateForm( + propValue['ui:field'] as string, + key, + value, + itemSchema, + itemUiSchema, + ); + }; + + const doValidate = async (propValue: JsonObject) => { + if ('ui:field' in propValue) { const { schema: itemsSchema, uiSchema: itemsUiSchema } = extractSchemaFromStep(definitionInSchema.items); - await validateForm( - definitionInSchema.items['ui:field'], - key, - value, - itemsSchema, - itemsUiSchema, - ); + await doValidateItem(propValue, itemsSchema, itemsUiSchema); } - } else if ( - definitionInSchema && - definitionInSchema.items && - definitionInSchema.items.type === 'object' - ) { + }; + + if (definitionInSchema && 'ui:field' in definitionInSchema) { + await doValidateItem(definitionInSchema, schema, uiSchema); + } else if (hasItems && 'ui:field' in definitionInSchema.items) { + await doValidate(definitionInSchema.items); + } else if (hasItems && definitionInSchema.items.type === 'object') { const properties = (definitionInSchema.items?.properties ?? []) as JsonObject[]; for (const [, propValue] of Object.entries(properties)) { - if ('ui:field' in propValue) { - const { schema: itemsSchema, uiSchema: itemsUiSchema } = - extractSchemaFromStep(definitionInSchema.items); - await validateForm( - propValue['ui:field'] as string, - key, - value, - itemsSchema, - itemsUiSchema, - ); - } + await doValidate(propValue); } } else if (isObject(value)) { formValidation[key] = await validate(formData, path, value); From 6bc2831d7059d4865a379870c7853eec6f06427f Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Fri, 3 Mar 2023 09:45:51 +0100 Subject: [PATCH 4/4] Update silent-dryers-end.md Signed-off-by: blam --- .changeset/silent-dryers-end.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silent-dryers-end.md b/.changeset/silent-dryers-end.md index 642a5fb7da..d15ea602ae 100644 --- a/.changeset/silent-dryers-end.md +++ b/.changeset/silent-dryers-end.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder': patch --- Fix the scaffolder validator for arrays when the item is a field in the object