diff --git a/.changeset/fluffy-ghosts-drop.md b/.changeset/fluffy-ghosts-drop.md new file mode 100644 index 0000000000..1b70e3977d --- /dev/null +++ b/.changeset/fluffy-ghosts-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fix field extension validation not working when field is in dependencies in an array field 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 f4dab376bc..dc9512ac07 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts @@ -17,6 +17,12 @@ import { JsonObject } from '@backstage/types'; import { CustomFieldValidator } from '../../../extensions'; import { createAsyncValidators } from './createAsyncValidators'; +type UrlItem = { + icon?: string; + title?: string; + url: string; +}; + describe('createAsyncValidators', () => { it('should call the correct functions for validation', async () => { const schema: JsonObject = { @@ -402,6 +408,9 @@ describe('createAsyncValidators', () => { }); it('should call validator for array object property from a custom field extension', async () => { + let validatorTriggered = false; + const validationErrors = [] as string[]; + const schema: JsonObject = { type: 'object', properties: { @@ -423,7 +432,23 @@ describe('createAsyncValidators', () => { }, }, }; - const validators = { CustomLinkField: jest.fn() }; + + const validators = { + CustomLinkField: (input: unknown) => { + const regex = /https?:\/\/(\S+)/; + const items = input instanceof Array ? input : [input]; + + items.forEach((item: UrlItem) => { + if (!regex.test(item.url)) { + validationErrors.push( + `Url ${item.url} is invalid. Urls must start with http:// or https://`, + ); + } + }); + + validatorTriggered = true; + }, + }; const validate = createAsyncValidators(schema, validators, { apiHolder: { get: jest.fn() }, @@ -433,6 +458,84 @@ describe('createAsyncValidators', () => { links: [{ url: 'http://my-url.spotify.com' }], }); - expect(validators.CustomLinkField).toHaveBeenCalled(); + expect(validatorTriggered).toBe(true); + expect(validationErrors).toEqual([]); + }); + + it('should validate field in the dependencies in an array field', async () => { + const schema: JsonObject = { + title: 'Make a choice', + properties: { + myArray: { + type: 'array', + title: 'Array', + items: { + type: 'object', + required: ['selector'], + properties: { + selector: { + title: 'Selector', + type: 'string', + enum: ['Choice 1', 'Choice 2'], + }, + }, + dependencies: { + selector: { + oneOf: [ + { properties: { selector: { enum: ['Choice 1'] } } }, + { + properties: { + selector: { enum: ['Choice 2'] }, + customValidatedField: { + title: 'Custom validated field', + type: 'string', + 'ui:field': 'ValidateKebabCase', + }, + }, + }, + ], + }, + }, + }, + }, + }, + }; + + const validatorsForChoice1 = { ValidateKebabCase: jest.fn() }; + + const validateChoice1 = createAsyncValidators( + schema, + validatorsForChoice1, + { + apiHolder: { get: jest.fn() }, + }, + ); + + await validateChoice1({ + myArray: [{ selector: 'Choice 1' }], + }); + + expect(validatorsForChoice1.ValidateKebabCase).not.toHaveBeenCalled(); + + const validatorsForChoice2 = { ValidateKebabCase: jest.fn() }; + + const validateChoice2 = createAsyncValidators( + schema, + validatorsForChoice2, + { + apiHolder: { get: jest.fn() }, + }, + ); + + await validateChoice2({ + myArray: [ + { + selector: 'Choice 2', + customValidatedField: 'apple', + }, + ], + }); + + expect(validatorsForChoice2.ValidateKebabCase).toHaveBeenCalled(); }); }); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts index d11d0b9b3b..9682c28baf 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts @@ -125,6 +125,30 @@ export const createAsyncValidators = ( } }; + const doValidateDependency = async (propValue: JsonObject) => { + const { schema: itemsSchema, uiSchema: itemsUiSchema } = + extractSchemaFromStep(propValue); + await doValidateItem(propValue, itemsSchema, itemsUiSchema); + + const iterable = Array.isArray(value) ? value : [value]; + for (const item of iterable) { + if (item && isObject(item)) { + const keys = Object.keys(item); + for (const k of keys) { + if (itemsUiSchema[k] && 'ui:field' in itemsUiSchema[k]) { + await validateForm( + itemsUiSchema[k]['ui:field'], + k, + item[k], + itemsSchema, + itemsUiSchema, + ); + } + } + } + } + }; + if ('ui:field' in definitionInSchema) { await doValidateItem(definitionInSchema, schema, uiSchema); } else if (hasItems && 'ui:field' in definitionInSchema.items) { @@ -135,6 +159,12 @@ export const createAsyncValidators = ( for (const [, propValue] of Object.entries(properties)) { await doValidate(propValue); } + + const dependencies = (definitionInSchema.items?.dependencies ?? + []) as JsonObject[]; + for (const [, propValue] of Object.entries(dependencies)) { + await doValidateDependency(propValue); + } } else if (isObject(value)) { formValidation[key] = await validate(formData, pointer, value); }