diff --git a/.changeset/poor-horses-approve.md b/.changeset/poor-horses-approve.md new file mode 100644 index 0000000000..7214b6432a --- /dev/null +++ b/.changeset/poor-horses-approve.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Add indication that the validators are running diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index 0b17c550a0..6183f32a6c 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -237,6 +237,50 @@ describe('Stepper', () => { expect(getByText('im a custom field extension')).toBeInTheDocument(); }); + it('should disable the form with progress when async validators are running', async () => { + const MockComponent = () => { + return

im a custom field extension

; + }; + + const manifest: TemplateParameterSchema = { + title: 'Custom Fields', + steps: [ + { + title: 'Test', + schema: { + properties: { + name: { + type: 'string', + 'ui:field': 'Mock', + }, + }, + }, + }, + ], + }; + + const { getByRole } = await renderInTestApp( + new Promise(r => setTimeout(r, 1000)), + }, + ]} + onCreate={jest.fn()} + />, + ); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Review' })); + + expect(getByRole('progressbar')).toBeInTheDocument(); + expect(getByRole('button', { name: 'Review' })).toBeDisabled(); + }); + }); + it('should transform default error message', async () => { const manifest: TemplateParameterSchema = { steps: [ diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 51a00dfa64..37d0b0baef 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -21,6 +21,7 @@ import { StepLabel as MuiStepLabel, Button, makeStyles, + LinearProgress, } from '@material-ui/core'; import { type IChangeEvent } from '@rjsf/core-v5'; import { ErrorSchema } from '@rjsf/utils'; @@ -93,6 +94,7 @@ export const Stepper = (stepperProps: StepperProps) => { const { steps } = useTemplateSchema(props.manifest); const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); + const [isValidating, setIsValidating] = useState(false); const [formState, setFormState] = useFormDataFromQuery(props.initialState); const [errors, setErrors] = useState(); @@ -133,12 +135,13 @@ export const Stepper = (stepperProps: StepperProps) => { }: { formData?: Record; }) => { - // TODO(blam): What do we do about loading states, does each field extension get a chance - // to display it's own loading? Or should we grey out the entire form. setErrors(undefined); + setIsValidating(true); const returnedValidation = await validation(formData); + setIsValidating(false); + if (hasErrors(returnedValidation)) { setErrors(returnedValidation); } else { @@ -154,6 +157,7 @@ export const Stepper = (stepperProps: StepperProps) => { return ( <> + {isValidating && } {steps.map((step, index) => ( @@ -183,11 +187,16 @@ export const Stepper = (stepperProps: StepperProps) => { -