From 8dce7d5244510f533adb8d70a6bc17d5ca3e8f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20MORI?= Date: Fri, 29 Apr 2022 17:40:34 +0200 Subject: [PATCH] Allow custom field extension validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane MORI --- .changeset/mean-owls-share.md | 5 ++ .../components/TemplatePage/TemplatePage.tsx | 61 +------------- .../TemplatePage/createValidator.test.ts | 74 +++++++++++++++++ .../TemplatePage/createValidator.ts | 83 +++++++++++++++++++ .../src/components/TemplatePage/index.ts | 3 +- 5 files changed, 167 insertions(+), 59 deletions(-) create mode 100644 .changeset/mean-owls-share.md create mode 100644 plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts create mode 100644 plugins/scaffolder/src/components/TemplatePage/createValidator.ts diff --git a/.changeset/mean-owls-share.md b/.changeset/mean-owls-share.md new file mode 100644 index 0000000000..df02959247 --- /dev/null +++ b/.changeset/mean-owls-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Allow validation for custom field extension with type object diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index fc1c4f222d..22f995eae4 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -13,23 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JsonObject, JsonValue } from '@backstage/types'; import { LinearProgress } from '@material-ui/core'; -import { FormValidation, IChangeEvent } from '@rjsf/core'; +import { IChangeEvent } from '@rjsf/core'; import qs from 'qs'; import React, { useCallback, useContext, useState } from 'react'; import { Navigate, useNavigate } from 'react-router'; import { useParams } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; import { scaffolderApiRef } from '../../api'; -import { CustomFieldValidator, FieldExtensionOptions } from '../../extensions'; +import { FieldExtensionOptions } from '../../extensions'; import { SecretsContext } from '../secrets/SecretsContext'; import { rootRouteRef, scaffolderTaskRouteRef } from '../../routes'; import { MultistepJsonForm } from '../MultistepJsonForm'; +import { createValidator } from './createValidator'; import { Content, Header, InfoCard, Page } from '@backstage/core-components'; import { - ApiHolder, errorApiRef, useApi, useApiHolder, @@ -46,60 +45,6 @@ const useTemplateParameterSchema = (templateRef: string) => { return { schema: value, loading, error }; }; -function isObject(obj: unknown): obj is JsonObject { - return typeof obj === 'object' && obj !== null && !Array.isArray(obj); -} - -export const createValidator = ( - rootSchema: JsonObject, - validators: Record>, - context: { - apiHolder: ApiHolder; - }, -) => { - function validate( - schema: JsonObject, - formData: JsonObject, - errors: FormValidation, - ) { - const schemaProps = schema.properties; - if (!isObject(schemaProps)) { - return; - } - - for (const [key, propData] of Object.entries(formData)) { - const propValidation = errors[key]; - - if (isObject(propData)) { - const propSchemaProps = schemaProps[key]; - if (isObject(propSchemaProps)) { - validate( - propSchemaProps, - propData as JsonObject, - propValidation as FormValidation, - ); - } - } else { - const propSchema = schemaProps[key]; - const fieldName = - isObject(propSchema) && (propSchema['ui:field'] as string); - if (fieldName && typeof validators[fieldName] === 'function') { - validators[fieldName]!( - propData as JsonValue, - propValidation, - context, - ); - } - } - } - } - - return (formData: JsonObject, errors: FormValidation) => { - validate(rootSchema, formData, errors); - return errors; - }; -}; - export const TemplatePage = ({ customFieldExtensions = [], }: { diff --git a/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts b/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts new file mode 100644 index 0000000000..047f22e44e --- /dev/null +++ b/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createValidator } from './createValidator'; +import { CustomFieldValidator } from '../../extensions'; +import { ApiHolder } from '@backstage/core-plugin-api'; +import { FormValidation } from '@rjsf/core'; + +describe('createValidator', () => { + const validators: Record> = + { + CustomPicker: (value, validation, _context) => { + // @ts-ignore + if (!value || !value.value) { + validation.addError('Error !'); + } + }, + }; + + const apiHolderMock: jest.Mocked = { + get: jest.fn().mockImplementation(() => { + return null; + }), + }; + + const context = { + apiHolder: apiHolderMock, + }; + + it('Should call validator for object property from a custom field extension', () => { + /* GIVEN */ + const rootSchema = { + title: 'Title', + properties: { + p1: { + title: 'PropertyOn', + type: 'object', + 'ui:field': 'CustomPicker', + }, + }, + }; + const validator = createValidator(rootSchema, validators, context); + + const formData = { + p1: {}, + }; + const errors = { + addError: jest.fn(), + p1: { + addError: jest.fn(), + } as unknown as FormValidation, + } as unknown as FormValidation; + + /* WHEN */ + const result = validator(formData, errors); + + /* THEN */ + expect(result).not.toBeNull(); + expect(result.p1.addError).toBeCalledTimes(1); + }); +}); diff --git a/plugins/scaffolder/src/components/TemplatePage/createValidator.ts b/plugins/scaffolder/src/components/TemplatePage/createValidator.ts new file mode 100644 index 0000000000..9cd1489a02 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplatePage/createValidator.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CustomFieldValidator } from '../../extensions'; +import { FormValidation } from '@rjsf/core'; +import { JsonObject, JsonValue } from '@backstage/types'; +import { ApiHolder } from '@backstage/core-plugin-api'; + +function isObject(obj: unknown): obj is JsonObject { + return typeof obj === 'object' && obj !== null && !Array.isArray(obj); +} + +export const createValidator = ( + rootSchema: JsonObject, + validators: Record>, + context: { + apiHolder: ApiHolder; + }, +) => { + function validate( + schema: JsonObject, + formData: JsonObject, + errors: FormValidation, + ) { + const schemaProps = schema.properties; + const customObject = schema.type === 'object' && schemaProps === undefined; + + if (!isObject(schemaProps) && !customObject) { + return; + } + + if (schemaProps) { + for (const [key, propData] of Object.entries(formData)) { + const propValidation = errors[key]; + + if (isObject(propData)) { + const propSchemaProps = schemaProps[key]; + if (isObject(propSchemaProps)) { + validate( + propSchemaProps, + propData as JsonObject, + propValidation as FormValidation, + ); + } + } else { + const propSchema = schemaProps[key]; + const fieldName = + isObject(propSchema) && (propSchema['ui:field'] as string); + if (fieldName && typeof validators[fieldName] === 'function') { + validators[fieldName]!( + propData as JsonValue, + propValidation, + context, + ); + } + } + } + } else if (customObject) { + const fieldName = schema['ui:field'] as string; + if (fieldName && typeof validators[fieldName] === 'function') { + validators[fieldName]!(formData, errors, context); + } + } + } + + return (formData: JsonObject, errors: FormValidation) => { + validate(rootSchema, formData, errors); + return errors; + }; +}; diff --git a/plugins/scaffolder/src/components/TemplatePage/index.ts b/plugins/scaffolder/src/components/TemplatePage/index.ts index b01da032e5..d3ae24504b 100644 --- a/plugins/scaffolder/src/components/TemplatePage/index.ts +++ b/plugins/scaffolder/src/components/TemplatePage/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { createValidator, TemplatePage } from './TemplatePage'; +export { TemplatePage } from './TemplatePage'; +export { createValidator } from './createValidator';