From 5555e1731349e55060b87d688f7ba6fa23077e88 Mon Sep 17 00:00:00 2001 From: Paul Cowan Date: Mon, 6 Feb 2023 14:34:09 +0000 Subject: [PATCH] refactor createAsyncValidators to be recursive Signed-off-by: Paul Cowan --- .changeset/shy-buses-mix.md | 5 +++ .../scaffolder/customScaffolderExtensions.tsx | 4 +-- .../src/next/components/Stepper/Stepper.tsx | 7 ++-- .../Stepper/createAsyncValidators.test.ts | 16 ++++++--- .../Stepper/createAsyncValidators.ts | 35 +++++++++++++----- .../src/next/components/Stepper/guards.ts | 36 +++++++++++++++++++ 6 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 .changeset/shy-buses-mix.md create mode 100644 plugins/scaffolder-react/src/next/components/Stepper/guards.ts diff --git a/.changeset/shy-buses-mix.md b/.changeset/shy-buses-mix.md new file mode 100644 index 0000000000..868da1cc93 --- /dev/null +++ b/.changeset/shy-buses-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': minor +--- + +refactor createAsyncValidators to be recursive diff --git a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx index c521bcf86c..fc37ff9e96 100644 --- a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx +++ b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx @@ -68,7 +68,7 @@ export const LowerCaseValuePickerFieldExtension = scaffolderPlugin.provide( const MockDelayComponent = ( props: NextFieldExtensionComponentProps<{ test?: string }>, ) => { - const { onChange, formData, rawErrors } = props; + const { onChange, formData, rawErrors = [] } = props; return ( onChange({ test: value })} margin="normal" - error={rawErrors?.length > 0 && !formData} + error={rawErrors.length > 0 && !formData} /> ); }; diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index c97d31c487..bd298fcbae 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -35,6 +35,7 @@ import { useFormDataFromQuery } from '../../hooks'; import { FormProps } from '../../types'; import { LayoutOptions } from '../../../layouts'; import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps'; +import { isInvalid } from './guards'; const useStyles = makeStyles(theme => ({ backButton: { @@ -136,11 +137,7 @@ export const Stepper = (stepperProps: StepperProps) => { const returnedValidation = await validation(formData); - const hasErrors = Object.values(returnedValidation).some( - i => i.__errors?.length, - ); - - if (hasErrors) { + if (isInvalid(returnedValidation)) { setErrors(returnedValidation); } else { setErrors(undefined); 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 ff7060222d..15fa944b0f 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts @@ -224,13 +224,19 @@ describe('createAsyncValidators', () => { apiHolder: { get: jest.fn() }, }); - await validate({ - actionType: 'newThing', + await expect( + validate({ + actionType: 'newThing', + general: { + name: undefined, + }, + }), + ).resolves.toEqual({ general: { - name: undefined, + name: expect.objectContaining({ + __errors: ['something is broken here!'], + }), }, }); - - expect(validators.NameField).toHaveBeenCalled(); }); }); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts index 79466ee602..299a2451ce 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts @@ -15,12 +15,21 @@ */ import { FieldValidation } from '@rjsf/utils'; -import { JsonObject } from '@backstage/types'; +import type { JsonObject, JsonValue } from '@backstage/types'; import { ApiHolder } from '@backstage/core-plugin-api'; import { Draft07 as JSONSchema } from 'json-schema-library'; import { createFieldValidation } from '../../lib'; import { NextCustomFieldValidator } from '../../extensions'; +function isObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +type FormValidation = Record< + string, + FieldValidation | Record +>; + export const createAsyncValidators = ( rootSchema: JsonObject, validators: Record>, @@ -28,14 +37,17 @@ export const createAsyncValidators = ( apiHolder: ApiHolder; }, ) => { - async function validate(formData: JsonObject, pathPrefix: string = '#') { + async function validate( + formData: JsonObject, + pathPrefix: string = '#', + current: JsonObject = formData, + ): Promise> { const parsedSchema = new JSONSchema(rootSchema); - const formValidation: Record = {}; - for (const [key, value] of Object.entries(formData)) { - const definitionInSchema = parsedSchema.getSchema( - `${pathPrefix}/${key}`, - formData, - ); + const formValidation: FormValidation = {}; + + for (const [key, value] of Object.entries(current)) { + const path = `${pathPrefix}/${key}`; + const definitionInSchema = parsedSchema.getSchema(path, formData); if (definitionInSchema && 'ui:field' in definitionInSchema) { const validator = validators[definitionInSchema['ui:field']]; @@ -48,10 +60,15 @@ export const createAsyncValidators = ( } formValidation[key] = fieldValidation; } + } else if (isObject(value)) { + formValidation[key] = (await validate(formData, path, value)) as Record< + string, + FieldValidation + >; } } - return formValidation; + return formValidation as Record; } return async (formData: JsonObject) => { diff --git a/plugins/scaffolder-react/src/next/components/Stepper/guards.ts b/plugins/scaffolder-react/src/next/components/Stepper/guards.ts new file mode 100644 index 0000000000..8dfedabd77 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Stepper/guards.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2023 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 type { FieldValidation } from '@rjsf/utils'; + +function isFieldValidation(error: any): error is FieldValidation { + return !!error && '__errors' in error; +} + +export function isInvalid(errors?: Record): boolean { + if (!errors) { + return false; + } + + for (const error of Object.values(errors)) { + if (isFieldValidation(error)) { + return (error.__errors ?? []).length > 0; + } + + return isInvalid(error); + } + + return false; +}