diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index be8dc14b0a..9b42ba478b 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -25,7 +25,7 @@ export type CustomFieldValidator = ( data: TFieldReturnValue, field: FieldValidation, context: { apiHolder: ApiHolder }, -) => void; +) => void | Promise; /** * Type for the Custom Field Extension with the diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index 19335d4993..240139ae5c 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { useApiHolder } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { Stepper as MuiStepper, @@ -21,11 +22,12 @@ import { Button, makeStyles, } from '@material-ui/core'; -import { withTheme } from '@rjsf/core'; +import { FieldValidation, FormValidation, withTheme } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import React, { useMemo, useState } from 'react'; import { FieldExtensionOptions } from '../../../extensions'; import { TemplateParameterSchema } from '../../../types'; +import { createAsyncValidator } from './createAsyncValidators'; import { useTemplateSchema } from './useTemplateSchema'; const useStyles = makeStyles(theme => ({ @@ -51,6 +53,7 @@ const Form = withTheme(MuiTheme); export const Stepper = (props: StepperProps) => { const { steps } = useTemplateSchema(props.manifest); + const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); const [formState, setFormState] = useState({}); const styles = useStyles(); @@ -61,11 +64,31 @@ export const Stepper = (props: StepperProps) => { ); }, [props.extensions]); + const validators = useMemo(() => { + return Object.fromEntries( + props.extensions.map(({ name, validation }) => [name, validation]), + ); + }, [props.extensions]); + + const validator = useMemo(() => { + return createAsyncValidator(steps[activeStep].originalSchema, validators, { + apiHolder, + }); + }, [steps, activeStep, validators, apiHolder]); + const handleBack = () => { setActiveStep(prevActiveStep => prevActiveStep - 1); }; - const handleNext = ({ formData }: { formData: JsonObject }) => { + const handleNext = async ({ formData }: { formData: JsonObject }) => { + console.log('running validation'); + + const errors = await validator(formData, { + addError: () => {}, + __errors: [], + } as any); + + console.log('errors'); setActiveStep(prevActiveStep => prevActiveStep + 1); setFormState(current => ({ ...current, ...formData })); }; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.ts new file mode 100644 index 0000000000..613deb57b3 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.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 { FormValidation } from '@rjsf/core'; +import { JsonObject, JsonValue } from '@backstage/types'; +import { ApiHolder } from '@backstage/core-plugin-api'; +import { CustomFieldValidator } from '../../../extensions'; + +function isObject(obj: unknown): obj is JsonObject { + return typeof obj === 'object' && obj !== null && !Array.isArray(obj); +} + +export const createAsyncValidator = ( + rootSchema: JsonObject, + validators: Record>, + context: { + apiHolder: ApiHolder; + }, +) => { + async 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)) { + await 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') { + await validators[fieldName]!( + propData as JsonValue, + propValidation, + context, + ); + } + } + } + } else if (customObject) { + const fieldName = schema['ui:field'] as string; + if (fieldName && typeof validators[fieldName] === 'function') { + await validators[fieldName]!(formData, errors, context); + } + } + } + + return async (formData: JsonObject, errors: FormValidation) => { + await validate(rootSchema, formData, errors); + return errors; + }; +}; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts index d5fe244a25..dac2c7b61c 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts @@ -24,6 +24,7 @@ export const useTemplateSchema = ( ): { steps: { uiSchema: UiSchema; + originalSchema: JsonObject; schema: JsonObject; title: string; description?: string; @@ -33,6 +34,7 @@ export const useTemplateSchema = ( const steps = manifest.steps.map(({ title, description, schema }) => ({ title, description, + originalSchema: schema, ...extractSchemaFromStep(schema), })); @@ -45,6 +47,7 @@ export const useTemplateSchema = ( // Then filter out the properties that are not enabled with feature flag .map(step => ({ ...step, + schema: { ...step.schema, // Title is rendered at the top of the page, so let's ignore this from jsonschemaform