Merge pull request #16670 from acierto/fix-validator-object-field-in-array

Fix the scaffolder validator for arrays when the item is a field in the object
This commit is contained in:
Ben Lambert
2023-03-03 10:22:56 +01:00
committed by GitHub
5 changed files with 170 additions and 48 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Fix the scaffolder validator for arrays when the item is a field in the object
@@ -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();
});
});
@@ -77,31 +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<unknown, unknown>,
) => {
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);
}
};
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)) {
await doValidate(propValue);
}
} else if (isObject(value)) {
formValidation[key] = await validate(formData, path, value);
@@ -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<string, undefined | CustomFieldValidator<unknown>> =
{
@@ -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);
});
});
@@ -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) {