From 223e2c5f0328aa86140f815d592fd67c0e95aa09 Mon Sep 17 00:00:00 2001 From: Paul Cowan Date: Thu, 22 Dec 2022 21:21:33 +0000 Subject: [PATCH 1/5] add onChange handler to stepper Signed-off-by: Paul Cowan --- .changeset/metal-hotels-deliver.md | 5 ++++ .../TemplateWizardPage/Stepper/Stepper.tsx | 26 +++++++------------ 2 files changed, 15 insertions(+), 16 deletions(-) create mode 100644 .changeset/metal-hotels-deliver.md diff --git a/.changeset/metal-hotels-deliver.md b/.changeset/metal-hotels-deliver.md new file mode 100644 index 0000000000..f2ce925bc2 --- /dev/null +++ b/.changeset/metal-hotels-deliver.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +'add onChange handler`to`Stepper` component diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index 3d5cb1bfd2..d59fb7f9c1 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -26,9 +26,9 @@ import { Button, makeStyles, } from '@material-ui/core'; -import { withTheme } from '@rjsf/core-v5'; +import { type IChangeEvent, withTheme } from '@rjsf/core-v5'; import { ErrorSchema, FieldValidation } from '@rjsf/utils'; -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { NextFieldExtensionOptions } from '../../../extensions'; import { TemplateParameterSchema } from '../../../types'; import { createAsyncValidators } from './createAsyncValidators'; @@ -36,7 +36,6 @@ import { useTemplateSchema } from './useTemplateSchema'; import { ReviewState } from './ReviewState'; import validator from '@rjsf/validator-ajv6'; import { selectedTemplateRouteRef } from '../../../routes'; -import { getDefaultFormState } from '@rjsf/utils'; import { useFormData } from './useFormData'; import { FormProps } from '../../types'; @@ -102,6 +101,11 @@ export const Stepper = (props: StepperProps) => { setActiveStep(prevActiveStep => prevActiveStep - 1); }; + const handleChange = useCallback( + (e: IChangeEvent) => setFormState(e.formData), + [setFormState], + ); + const handleNext = async ({ formData, }: { @@ -111,18 +115,7 @@ export const Stepper = (props: StepperProps) => { // to display it's own loading? Or should we grey out the entire form. setErrors(undefined); - const schema = steps[activeStep]?.schema; - const rootSchema = steps[activeStep]?.mergedSchema; - - const newFormData = getDefaultFormState( - validator, - schema, - formData, - rootSchema, - true, - ); - - const returnedValidation = await validation(newFormData); + const returnedValidation = await validation(formData); const hasErrors = Object.values(returnedValidation).some( i => i.__errors?.length, @@ -138,7 +131,7 @@ export const Stepper = (props: StepperProps) => { return stepNum; }); } - setFormState(current => ({ ...current, ...newFormData })); + setFormState(current => ({ ...current, ...formData })); }; return ( @@ -165,6 +158,7 @@ export const Stepper = (props: StepperProps) => { onSubmit={handleNext} fields={extensions} showErrorList={false} + onChange={handleChange} {...(props.FormProps ?? {})} >
From 4e5bd3124207964981120edc93c4e3fede6cec29 Mon Sep 17 00:00:00 2001 From: Paul Cowan Date: Thu, 22 Dec 2022 21:37:56 +0000 Subject: [PATCH 2/5] update changeset Signed-off-by: Paul Cowan --- .changeset/metal-hotels-deliver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/metal-hotels-deliver.md b/.changeset/metal-hotels-deliver.md index f2ce925bc2..6b7f9c109b 100644 --- a/.changeset/metal-hotels-deliver.md +++ b/.changeset/metal-hotels-deliver.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': patch --- -'add onChange handler`to`Stepper` component +add `onChange` handler to`Stepper` component From 0b0bd3379bdbfef88944a55c54827d320114e0d2 Mon Sep 17 00:00:00 2001 From: Paul Cowan Date: Fri, 23 Dec 2022 10:57:31 +0000 Subject: [PATCH 3/5] merge formData with current in Stepper onChange Signed-off-by: Paul Cowan --- .../scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index d59fb7f9c1..d4d57eed22 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -102,7 +102,8 @@ export const Stepper = (props: StepperProps) => { }; const handleChange = useCallback( - (e: IChangeEvent) => setFormState(e.formData), + (e: IChangeEvent) => + setFormState(current => ({ ...current, ...e.formData })), [setFormState], ); From 89faf7c9b5c9d75f40d4dd05c416787c54588bf8 Mon Sep 17 00:00:00 2001 From: Paul Cowan Date: Fri, 23 Dec 2022 12:11:07 +0000 Subject: [PATCH 4/5] test nested form data in multiple steps Signed-off-by: Paul Cowan --- .../Stepper/Stepper.test.tsx | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx index af86abf1c6..f40e467a14 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx @@ -20,6 +20,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; import type { RJSFValidationError } from '@rjsf/utils'; import { JsonValue } from '@backstage/types'; +import { NextFieldExtensionComponentProps } from '../../../extensions/types'; describe('Stepper', () => { it('should render the step titles for each step of the manifest', async () => { @@ -110,6 +111,96 @@ describe('Stepper', () => { ); }); + it('should merge nested formData correctly in multiple steps', async () => { + const Repo = ({ + onChange, + }: NextFieldExtensionComponentProps<{ repository: string }, any>) => ( + onChange({ repository: e.target.value })} + defaultValue="" + /> + ); + + const Owner = ({ + onChange, + }: NextFieldExtensionComponentProps<{ owner: string }, any>) => ( + onChange({ owner: e.target.value })} + defaultValue="" + /> + ); + + const manifest: TemplateParameterSchema = { + steps: [ + { + title: 'Step 1', + schema: { + properties: { + first: { + type: 'object', + 'ui:field': 'Repo', + }, + }, + }, + }, + { + title: 'Step 2', + schema: { + properties: { + second: { + type: 'object', + 'ui:field': 'Owner', + }, + }, + }, + }, + ], + title: 'React JSON Schema Form Test', + }; + + const onComplete = jest.fn(async (values: Record) => { + expect(values).toEqual({ + first: { repository: 'Repo' }, + second: { owner: 'Owner' }, + }); + }); + + const { getByRole } = await renderInTestApp( + , + ); + + await fireEvent.change(getByRole('textbox', { name: 'repo' }), { + target: { value: 'Repo' }, + }); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Next' })); + }); + + await fireEvent.change(getByRole('textbox', { name: 'owner' }), { + target: { value: 'Owner' }, + }); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Review' })); + }); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Create' })); + }); + }); + it('should render custom field extensions properly', async () => { const MockComponent = () => { return

im a custom field extension

; From c4d2226ef2b7a9ea3f5241db2f71302461be2b28 Mon Sep 17 00:00:00 2001 From: Paul Cowan Date: Fri, 23 Dec 2022 12:15:14 +0000 Subject: [PATCH 5/5] ensure mock is called in Stepper test Signed-off-by: Paul Cowan --- .../src/next/TemplateWizardPage/Stepper/Stepper.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx index f40e467a14..2660eed47d 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx @@ -199,6 +199,8 @@ describe('Stepper', () => { await act(async () => { await fireEvent.click(getByRole('button', { name: 'Create' })); }); + + expect(onComplete).toHaveBeenCalled(); }); it('should render custom field extensions properly', async () => {