chore: starting to add async validation to the next scaffolder

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2022-07-29 15:22:11 +02:00
parent d326058eb9
commit 6c8c1a4140
4 changed files with 112 additions and 3 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ export type CustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidation,
context: { apiHolder: ApiHolder },
) => void;
) => void | Promise<void>;
/**
* Type for the Custom Field Extension with the
@@ -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 }));
};
@@ -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<string, undefined | CustomFieldValidator<unknown>>,
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;
};
};
@@ -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