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..d743078d11 100644
--- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx
+++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx
@@ -22,6 +22,7 @@ import type { RJSFValidationError } from '@rjsf/utils';
import { JsonValue } from '@backstage/types';
import { NextFieldExtensionComponentProps } from '../../extensions';
import { LayoutTemplate } from '../../../layouts';
+import { AsyncDependenciesBlock } from 'webpack';
describe('Stepper', () => {
it('should render the step titles for each step of the manifest', async () => {
@@ -237,6 +238,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) => {
-