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) => {
-