From 3f4b9f4c756e8d723055ccd90c10ceca784a7bb5 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Thu, 29 Aug 2024 16:00:47 +0200 Subject: [PATCH 01/12] Add support for omitExtraData and liveOmit properties. Signed-off-by: Jasper Boeijenga --- .../scaffolder-react/src/components/types.ts | 7 ++++- .../src/next/components/Stepper/Stepper.tsx | 30 ++++++++++++++----- .../src/components/Router/Router.tsx | 1 + .../TemplateEditorPage/TemplateEditor.tsx | 7 ++++- .../TemplateEditorPage/TemplateEditorForm.tsx | 6 +++- .../TemplateEditorPage/TemplateEditorPage.tsx | 4 +++ .../TemplateFormPreviewer.tsx | 4 +++ 7 files changed, 48 insertions(+), 11 deletions(-) diff --git a/plugins/scaffolder-react/src/components/types.ts b/plugins/scaffolder-react/src/components/types.ts index d5d317015d..3df6209cad 100644 --- a/plugins/scaffolder-react/src/components/types.ts +++ b/plugins/scaffolder-react/src/components/types.ts @@ -32,7 +32,12 @@ export type TemplateGroupFilter = { */ export type FormProps = Pick< SchemaFormProps, - 'transformErrors' | 'noHtml5Validate' | 'uiSchema' | 'formContext' + | 'transformErrors' + | 'noHtml5Validate' + | 'uiSchema' + | 'formContext' + | 'omitExtraData' + | 'liveOmit' >; /** diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 3a8858144b..16a3bb0761 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -155,9 +155,13 @@ export const Stepper = (stepperProps: StepperProps) => { }; const handleChange = useCallback( - (e: IChangeEvent) => - setFormState(current => ({ ...current, ...e.formData })), - [setFormState], + (e: IChangeEvent) => { + setFormState(current => ({ + ...current, + [`step${activeStep}`]: e.formData, + })); + }, + [setFormState, activeStep], ); const handleCreate = useCallback(() => { @@ -191,7 +195,7 @@ export const Stepper = (stepperProps: StepperProps) => { return stepNum; }); } - setFormState(current => ({ ...current, ...formData })); + setFormState(current => ({ ...current, [`step${activeStep}`]: formData })); }; const { @@ -202,6 +206,16 @@ export const Stepper = (stepperProps: StepperProps) => { const mergedUiSchema = merge({}, propUiSchema, currentStep?.uiSchema); + const mergedState = useMemo(() => { + const { [`step${activeStep}`]: activeState, ...historicalState } = + formState; + const chronologicalState = { + ...historicalState, + [`step${activeStep}`]: activeState, + }; + return merge({}, ...Object.values(chronologicalState)); + }, [formState, activeStep]); + return ( <> {isValidating && } @@ -237,8 +251,8 @@ export const Stepper = (stepperProps: StepperProps) => {
{ ReviewStepComponent ? ( {}} steps={steps} @@ -282,7 +296,7 @@ export const Stepper = (stepperProps: StepperProps) => { /> ) : ( <> - +
@@ -206,7 +209,7 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) { export function TemplateEditorFormDirectoryEditorDryRun( props: Pick< TemplateEditorFormProps, - 'setErrorText' | 'fieldExtensions' | 'layouts' + 'setErrorText' | 'fieldExtensions' | 'layouts' | 'formProps' >, ) { const { setErrorText, fieldExtensions = [], layouts } = props; @@ -245,6 +248,7 @@ export function TemplateEditorFormDirectoryEditorDryRun( setErrorText={setErrorText} content={content} layouts={layouts} + formProps={props.formProps} /> ); } diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorPage.tsx index 6e7c921e91..03d656c98d 100644 --- a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorPage.tsx +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorPage.tsx @@ -24,6 +24,7 @@ import { TemplateEditor } from './TemplateEditor'; import { TemplateFormPreviewer } from './TemplateFormPreviewer'; import { FieldExtensionOptions, + FormProps, type LayoutOptions, } from '@backstage/plugin-scaffolder-react'; import { TemplateEditorIntro } from './TemplateEditorIntro'; @@ -54,6 +55,7 @@ interface TemplateEditorPageProps { defaultPreviewTemplate?: string; customFieldExtensions?: FieldExtensionOptions[]; layouts?: LayoutOptions[]; + formProps?: FormProps; } export function TemplateEditorPage(props: TemplateEditorPageProps) { @@ -79,6 +81,7 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) { fieldExtensions={props.customFieldExtensions} onClose={() => setSelection(undefined)} layouts={props.layouts} + formProps={props.formProps} /> ); } else if (selection?.type === 'form') { @@ -88,6 +91,7 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) { customFieldExtensions={props.customFieldExtensions} onClose={() => setSelection(undefined)} layouts={props.layouts} + formProps={props.formProps} /> ); } else if (selection?.type === 'field-explorer') { diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx index 07f2fd7f07..610d2a22f0 100644 --- a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx @@ -33,6 +33,7 @@ import yaml from 'yaml'; import { LayoutOptions, FieldExtensionOptions, + FormProps, } from '@backstage/plugin-scaffolder-react'; import { TemplateEditorForm } from './TemplateEditorForm'; import { TemplateEditorTextArea } from './TemplateEditorTextArea'; @@ -114,11 +115,13 @@ export const TemplateFormPreviewer = ({ customFieldExtensions = [], onClose, layouts = [], + formProps, }: { defaultPreviewTemplate?: string; customFieldExtensions?: FieldExtensionOptions[]; onClose?: () => void; layouts?: LayoutOptions[]; + formProps?: FormProps; }) => { const classes = useStyles(); const { t } = useTranslationRef(scaffolderTranslationRef); @@ -213,6 +216,7 @@ export const TemplateFormPreviewer = ({ fieldExtensions={customFieldExtensions} setErrorText={setErrorText} layouts={layouts} + formProps={formProps} /> From 4baad345f1d90a270f2c1f724f5c69998cfc98b3 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Thu, 29 Aug 2024 16:08:24 +0200 Subject: [PATCH 02/12] Added changeset Signed-off-by: Jasper Boeijenga --- .changeset/funny-rocks-pull.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/funny-rocks-pull.md diff --git a/.changeset/funny-rocks-pull.md b/.changeset/funny-rocks-pull.md new file mode 100644 index 0000000000..7fc6ea7fa3 --- /dev/null +++ b/.changeset/funny-rocks-pull.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': minor +'@backstage/plugin-scaffolder': minor +--- + +Added support for omitExtraData and liveOmit for rjsf in the scaffolder From 474a568987b29216fe26dbb726ebab53e29fb967 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Thu, 29 Aug 2024 16:10:26 +0200 Subject: [PATCH 03/12] Update funny-rocks-pull.md Signed-off-by: Jasper Boeijenga --- .changeset/funny-rocks-pull.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/funny-rocks-pull.md b/.changeset/funny-rocks-pull.md index 7fc6ea7fa3..9145464137 100644 --- a/.changeset/funny-rocks-pull.md +++ b/.changeset/funny-rocks-pull.md @@ -3,4 +3,4 @@ '@backstage/plugin-scaffolder': minor --- -Added support for omitExtraData and liveOmit for rjsf in the scaffolder +Added support for `omitExtraData` and `liveOmit` for rjsf in the scaffolder From 909b613598a3cce6df65e3aa68be488f7fc124b6 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Mon, 2 Sep 2024 08:10:58 +0200 Subject: [PATCH 04/12] Added changed api-report file Signed-off-by: Jasper Boeijenga --- plugins/scaffolder-react/api-report.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index a19a656d17..f73365f0ac 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -128,7 +128,12 @@ export interface FieldExtensionUiSchema // @public export type FormProps = Pick< FormProps_2, - 'transformErrors' | 'noHtml5Validate' | 'uiSchema' | 'formContext' + | 'transformErrors' + | 'noHtml5Validate' + | 'uiSchema' + | 'formContext' + | 'omitExtraData' + | 'liveOmit' >; // @public From f11bc722f266c9298bb4a1593a63b357159c7ca5 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Mon, 2 Sep 2024 08:35:07 +0200 Subject: [PATCH 05/12] Fix failing test Signed-off-by: Jasper Boeijenga --- .../src/next/TemplateWizardPage/TemplateWizardPage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx index 2639e8149c..2b1522cac2 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -140,7 +140,7 @@ describe('TemplateWizardPage', () => { // And the "Create" button should have fired an event expect(analyticsMock.getEvents()[1]).toMatchObject({ action: 'create', - subject: 'expected-name', + subject: 'test', context: { entityRef: 'template:default/test' }, value: 120, }); From 5e2d399f0599191900cc2d1432c80b9fde77fee1 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Mon, 2 Sep 2024 09:39:33 +0200 Subject: [PATCH 06/12] Fix handleCreate and initialState cases Signed-off-by: Jasper Boeijenga --- .../src/next/components/Stepper/Stepper.tsx | 18 ++++++++----- .../src/next/hooks/useFormDataFromQuery.ts | 26 ++++++++----------- .../TemplateWizardPage.test.tsx | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 16a3bb0761..394fe517ce 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -115,7 +115,8 @@ export const Stepper = (stepperProps: StepperProps) => { const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); const [isValidating, setIsValidating] = useState(false); - const [formState, setFormState] = useFormDataFromQuery(props.initialState); + const initialState = useFormDataFromQuery(props.initialState); + const [formState, setFormState] = useState<{ [step: string]: any }>(); const [errors, setErrors] = useState(); const styles = useStyles(); @@ -164,11 +165,6 @@ export const Stepper = (stepperProps: StepperProps) => { [setFormState, activeStep], ); - const handleCreate = useCallback(() => { - props.onCreate(formState); - analytics.captureEvent('click', `${createLabel}`); - }, [props, formState, analytics, createLabel]); - const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts }); const handleNext = async ({ @@ -207,6 +203,9 @@ export const Stepper = (stepperProps: StepperProps) => { const mergedUiSchema = merge({}, propUiSchema, currentStep?.uiSchema); const mergedState = useMemo(() => { + if (!formState) { + return initialState; + } const { [`step${activeStep}`]: activeState, ...historicalState } = formState; const chronologicalState = { @@ -214,7 +213,12 @@ export const Stepper = (stepperProps: StepperProps) => { [`step${activeStep}`]: activeState, }; return merge({}, ...Object.values(chronologicalState)); - }, [formState, activeStep]); + }, [formState, activeStep, initialState]); + + const handleCreate = useCallback(() => { + props.onCreate(mergedState); + analytics.captureEvent('click', `${createLabel}`); + }, [props, mergedState, analytics, createLabel]); return ( <> diff --git a/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts b/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts index 966a8fe8f4..9f9f4ed9a2 100644 --- a/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts +++ b/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts @@ -13,10 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { JsonValue } from '@backstage/types'; import qs from 'qs'; -import { useState } from 'react'; /** * This hook is used to get the formData from the query string. @@ -25,19 +23,17 @@ import { useState } from 'react'; export const useFormDataFromQuery = ( initialState?: Record, ) => { - return useState>(() => { - if (initialState) { - return initialState; - } + if (initialState) { + return initialState; + } - const query = qs.parse(window.location.search, { - ignoreQueryPrefix: true, - }); - - try { - return JSON.parse(query.formData as string); - } catch (e) { - return {}; - } + const query = qs.parse(window.location.search, { + ignoreQueryPrefix: true, }); + + try { + return JSON.parse(query.formData as string); + } catch (e) { + return {}; + } }; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx index 2b1522cac2..2639e8149c 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -140,7 +140,7 @@ describe('TemplateWizardPage', () => { // And the "Create" button should have fired an event expect(analyticsMock.getEvents()[1]).toMatchObject({ action: 'create', - subject: 'test', + subject: 'expected-name', context: { entityRef: 'template:default/test' }, value: 120, }); From 0dfe21016fedc018be73cb0ad73c1cb66066e352 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Mon, 2 Sep 2024 09:56:59 +0200 Subject: [PATCH 07/12] Updated api reports Signed-off-by: Jasper Boeijenga --- plugins/scaffolder-react/api-report-alpha.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/scaffolder-react/api-report-alpha.md b/plugins/scaffolder-react/api-report-alpha.md index 545cc02e9e..5b5a37d6b3 100644 --- a/plugins/scaffolder-react/api-report-alpha.md +++ b/plugins/scaffolder-react/api-report-alpha.md @@ -3,12 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { ApiHolder } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; -import { Dispatch } from 'react'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import { FieldValidation } from '@rjsf/utils'; import { FormProps } from '@backstage/plugin-scaffolder-react'; @@ -25,7 +22,6 @@ import { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; import { ScaffolderRJSFFormProps } from '@backstage/plugin-scaffolder-react'; import { ScaffolderStep } from '@backstage/plugin-scaffolder-react'; import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; -import { SetStateAction } from 'react'; import { StyleRules } from '@material-ui/core/styles/withStyles'; import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; @@ -280,7 +276,7 @@ export const useFilteredSchemaProperties: ( // @alpha export const useFormDataFromQuery: ( initialState?: Record, -) => [Record, Dispatch>>]; +) => any; // @alpha (undocumented) export const useTemplateParameterSchema: (templateRef: string) => { From 8be7cbf8a999e47964a528a46bca2a4b3a308488 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Mon, 2 Sep 2024 10:22:44 +0200 Subject: [PATCH 08/12] Improve typings Signed-off-by: Jasper Boeijenga --- plugins/scaffolder-react/api-report-alpha.md | 2 +- .../scaffolder-react/src/next/components/Stepper/Stepper.tsx | 4 +++- .../scaffolder-react/src/next/hooks/useFormDataFromQuery.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-react/api-report-alpha.md b/plugins/scaffolder-react/api-report-alpha.md index 5b5a37d6b3..57f97f2012 100644 --- a/plugins/scaffolder-react/api-report-alpha.md +++ b/plugins/scaffolder-react/api-report-alpha.md @@ -276,7 +276,7 @@ export const useFilteredSchemaProperties: ( // @alpha export const useFormDataFromQuery: ( initialState?: Record, -) => any; +) => Record; // @alpha (undocumented) export const useTemplateParameterSchema: (templateRef: string) => { diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 394fe517ce..069512da5a 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -116,7 +116,9 @@ export const Stepper = (stepperProps: StepperProps) => { const [activeStep, setActiveStep] = useState(0); const [isValidating, setIsValidating] = useState(false); const initialState = useFormDataFromQuery(props.initialState); - const [formState, setFormState] = useState<{ [step: string]: any }>(); + const [formState, setFormState] = useState<{ + [step: string]: Record; + }>(); const [errors, setErrors] = useState(); const styles = useStyles(); diff --git a/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts b/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts index 9f9f4ed9a2..777c615cd7 100644 --- a/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts +++ b/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts @@ -22,7 +22,7 @@ import qs from 'qs'; */ export const useFormDataFromQuery = ( initialState?: Record, -) => { +): Record => { if (initialState) { return initialState; } From 8d9a54c7d50f9130823c0aec79e47232424ea251 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Tue, 3 Sep 2024 13:35:43 +0200 Subject: [PATCH 09/12] processed feedback Signed-off-by: Jasper Boeijenga --- plugins/scaffolder-react/api-report-alpha.md | 6 +++- .../src/next/components/Stepper/Stepper.tsx | 15 ++++++---- .../src/next/hooks/useFormDataFromQuery.ts | 28 +++++++++++-------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/plugins/scaffolder-react/api-report-alpha.md b/plugins/scaffolder-react/api-report-alpha.md index 57f97f2012..545cc02e9e 100644 --- a/plugins/scaffolder-react/api-report-alpha.md +++ b/plugins/scaffolder-react/api-report-alpha.md @@ -3,9 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { ApiHolder } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; +import { Dispatch } from 'react'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import { FieldValidation } from '@rjsf/utils'; import { FormProps } from '@backstage/plugin-scaffolder-react'; @@ -22,6 +25,7 @@ import { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; import { ScaffolderRJSFFormProps } from '@backstage/plugin-scaffolder-react'; import { ScaffolderStep } from '@backstage/plugin-scaffolder-react'; import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; +import { SetStateAction } from 'react'; import { StyleRules } from '@material-ui/core/styles/withStyles'; import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; @@ -276,7 +280,7 @@ export const useFilteredSchemaProperties: ( // @alpha export const useFormDataFromQuery: ( initialState?: Record, -) => Record; +) => [Record, Dispatch>>]; // @alpha (undocumented) export const useTemplateParameterSchema: (templateRef: string) => { diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 069512da5a..a1b31d0089 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -115,7 +115,7 @@ export const Stepper = (stepperProps: StepperProps) => { const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); const [isValidating, setIsValidating] = useState(false); - const initialState = useFormDataFromQuery(props.initialState); + const [initialState] = useFormDataFromQuery(props.initialState); const [formState, setFormState] = useState<{ [step: string]: Record; }>(); @@ -123,6 +123,8 @@ export const Stepper = (stepperProps: StepperProps) => { const [errors, setErrors] = useState(); const styles = useStyles(); + const makeStepKey = (step: string | number) => `step-${step}`; + const backLabel = presentation?.buttonLabels?.backButtonText ?? backButtonText; const createLabel = @@ -161,7 +163,7 @@ export const Stepper = (stepperProps: StepperProps) => { (e: IChangeEvent) => { setFormState(current => ({ ...current, - [`step${activeStep}`]: e.formData, + [makeStepKey(activeStep)]: e.formData, })); }, [setFormState, activeStep], @@ -193,7 +195,10 @@ export const Stepper = (stepperProps: StepperProps) => { return stepNum; }); } - setFormState(current => ({ ...current, [`step${activeStep}`]: formData })); + setFormState(current => ({ + ...current, + [makeStepKey(activeStep)]: formData, + })); }; const { @@ -208,11 +213,11 @@ export const Stepper = (stepperProps: StepperProps) => { if (!formState) { return initialState; } - const { [`step${activeStep}`]: activeState, ...historicalState } = + const { [makeStepKey(activeStep)]: activeState, ...historicalState } = formState; const chronologicalState = { ...historicalState, - [`step${activeStep}`]: activeState, + [makeStepKey(activeStep)]: activeState, }; return merge({}, ...Object.values(chronologicalState)); }, [formState, activeStep, initialState]); diff --git a/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts b/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts index 777c615cd7..966a8fe8f4 100644 --- a/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts +++ b/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { JsonValue } from '@backstage/types'; import qs from 'qs'; +import { useState } from 'react'; /** * This hook is used to get the formData from the query string. @@ -22,18 +24,20 @@ import qs from 'qs'; */ export const useFormDataFromQuery = ( initialState?: Record, -): Record => { - if (initialState) { - return initialState; - } +) => { + return useState>(() => { + if (initialState) { + return initialState; + } - const query = qs.parse(window.location.search, { - ignoreQueryPrefix: true, + const query = qs.parse(window.location.search, { + ignoreQueryPrefix: true, + }); + + try { + return JSON.parse(query.formData as string); + } catch (e) { + return {}; + } }); - - try { - return JSON.parse(query.formData as string); - } catch (e) { - return {}; - } }; From 3b0e1d4c580edf165544638e711ce028cb8ec30d Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Wed, 4 Sep 2024 16:29:47 +0200 Subject: [PATCH 10/12] Implemented test case for omitExtraData Signed-off-by: Jasper Boeijenga --- .../next/components/Stepper/Stepper.test.tsx | 192 +++++++++++++----- 1 file changed, 139 insertions(+), 53 deletions(-) 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 78aef3428f..d6882c7403 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -13,16 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { renderInTestApp } from '@backstage/test-utils'; +import { JsonValue } from '@backstage/types'; +import { act, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; + +import { FieldExtensionComponentProps } from '../../../extensions'; +import { LayoutTemplate } from '../../../layouts'; +import { SecretsContextProvider } from '../../../secrets'; import { TemplateParameterSchema } from '../../../types'; import { Stepper } from './Stepper'; -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 { FieldExtensionComponentProps } from '../../../extensions'; -import { SecretsContextProvider } from '../../../secrets'; -import { LayoutTemplate } from '../../../layouts'; describe('Stepper', () => { it('should render the step titles for each step of the manifest', async () => { @@ -63,7 +65,7 @@ describe('Stepper', () => { expect(getByRole('button', { name: 'Next' })).toBeInTheDocument(); await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Next' })); + fireEvent.click(getByRole('button', { name: 'Next' })); }); expect(getByRole('button', { name: 'Review' })).toBeInTheDocument(); @@ -102,16 +104,15 @@ describe('Stepper', () => { , ); - await fireEvent.change(getByRole('textbox', { name: 'name' }), { - target: { value: 'im a test value' }, + await act(async () => { + fireEvent.change(getByRole('textbox', { name: 'name' }), { + target: { value: 'im a test value' }, + }); + fireEvent.click(getByRole('button', { name: 'Next' })); }); await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Next' })); - }); - - await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Back' })); + fireEvent.click(getByRole('button', { name: 'Back' })); }); expect(getByRole('textbox', { name: 'name' })).toHaveValue( @@ -152,16 +153,15 @@ describe('Stepper', () => { , ); - await fireEvent.change(getByRole('textbox', { name: 'name' }), { - target: { value: 'im a test value' }, + await act(async () => { + fireEvent.change(getByRole('textbox', { name: 'name' }), { + target: { value: 'im a test value' }, + }); + fireEvent.click(getByRole('button', { name: 'Next' })); }); await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Next' })); - }); - - await act(async () => { - await fireEvent.click(getByLabelText('Step 1')); + fireEvent.click(getByLabelText('Step 1')); }); expect(getByRole('textbox', { name: 'name' })).toHaveValue( @@ -169,6 +169,97 @@ describe('Stepper', () => { ); }); + it('should omit properties that are no longer pertinent to the current step', async () => { + const manifest: TemplateParameterSchema = { + title: 'Conditional Input Form', + steps: [ + { + title: 'Conditional Input step', + schema: { + type: 'object', + properties: { + moreInfo: { + type: 'boolean', + title: 'More info', + }, + }, + dependencies: { + moreInfo: { + oneOf: [ + { + properties: { + moreInfo: { + const: true, + }, + description: { + type: 'string', + title: 'Description', + }, + }, + required: ['description'], + }, + { + properties: { + moreInfo: { + not: { + const: true, + }, + }, + }, + }, + ], + }, + }, + }, + }, + ], + }; + + const onCreate = jest.fn(); + + const { getByRole, queryByRole } = await renderInTestApp( + + + , + ); + + await act(async () => { + fireEvent.click(getByRole('checkbox', { name: 'More info' })); + fireEvent.change(getByRole('textbox', { name: 'Description' }), { + target: { value: 'My Test Description' }, + }); + fireEvent.click(getByRole('button', { name: 'Review' })); + }); + + expect( + getByRole('cell', { name: 'My Test Description' }), + ).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(getByRole('button', { name: 'Back' })); + }); + + await act(async () => { + fireEvent.click(getByRole('checkbox', { name: 'More info' })); + fireEvent.click(getByRole('button', { name: 'Review' })); + }); + + expect( + queryByRole('cell', { name: 'My Test Description' }), + ).not.toBeInTheDocument(); + + await act(async () => { + fireEvent.click(getByRole('button', { name: 'Create' })); + }); + + expect(onCreate).toHaveBeenCalledWith({ moreInfo: false }); + }); + it('should merge nested formData correctly in multiple steps', async () => { const Repo = ({ onChange, @@ -240,24 +331,22 @@ describe('Stepper', () => { , ); - await fireEvent.change(getByRole('textbox', { name: 'repo' }), { - target: { value: 'Repo' }, + await act(async () => { + fireEvent.change(getByRole('textbox', { name: 'repo' }), { + target: { value: 'Repo' }, + }); + fireEvent.click(getByRole('button', { name: 'Next' })); }); await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Next' })); - }); - - await fireEvent.change(getByRole('textbox', { name: 'owner' }), { - target: { value: 'Owner' }, + fireEvent.change(getByRole('textbox', { name: 'owner' }), { + target: { value: 'Owner' }, + }); + fireEvent.click(getByRole('button', { name: 'Review' })); }); await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Review' })); - }); - - await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Create' })); + fireEvent.click(getByRole('button', { name: 'Create' })); }); expect(onCreate).toHaveBeenCalled(); @@ -335,13 +424,11 @@ describe('Stepper', () => { /> , ); - - act(() => { - fireEvent.click(getByRole('button', { name: 'Review' })); + fireEvent.click(getByRole('button', { name: 'Review' })); + await waitFor(() => { + expect(getByRole('progressbar')).toBeInTheDocument(); // Check if progress bar is rendered + expect(getByRole('button', { name: 'Review' })).toBeDisabled(); // Check if the button is disabled }); - - expect(getByRole('progressbar')).toBeInTheDocument(); - expect(getByRole('button', { name: 'Review' })).toBeDisabled(); }); it('should transform default error message', async () => { @@ -381,12 +468,11 @@ describe('Stepper', () => { , ); - await fireEvent.change(getByRole('textbox', { name: 'postcode' }), { - target: { value: 'invalid' }, - }); - await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Review' })); + fireEvent.change(getByRole('textbox', { name: 'postcode' }), { + target: { value: 'invalid' }, + }); + fireEvent.click(getByRole('button', { name: 'Review' })); }); expect(getByText('invalid postcode')).toBeInTheDocument(); @@ -421,12 +507,12 @@ describe('Stepper', () => { , ); - await fireEvent.change(getByRole('textbox', { name: 'postcode' }), { - target: { value: 'invalid' }, - }); - await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Review' })); + fireEvent.change(getByRole('textbox', { name: 'postcode' }), { + target: { value: 'invalid' }, + }); + + fireEvent.click(getByRole('button', { name: 'Review' })); }); expect(getByText('invalid postcode')).toBeInTheDocument(); @@ -494,13 +580,13 @@ describe('Stepper', () => { ); await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Review' })); + fireEvent.click(getByRole('button', { name: 'Review' })); }); expect(getByRole('button', { name: 'Create' })).toBeInTheDocument(); await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Create' })); + fireEvent.click(getByRole('button', { name: 'Create' })); }); // flush promises @@ -539,13 +625,13 @@ describe('Stepper', () => { ); await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Inspect' })); + fireEvent.click(getByRole('button', { name: 'Inspect' })); }); expect(getByRole('button', { name: 'Make' })).toBeInTheDocument(); await act(async () => { - await fireEvent.click(getByRole('button', { name: 'Make' })); + fireEvent.click(getByRole('button', { name: 'Make' })); }); }); From 94d80975057b21c4edd067f0011edf45d93a9524 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Wed, 4 Sep 2024 16:54:37 +0200 Subject: [PATCH 11/12] Added extra case to test overwriting formdata in different steps Signed-off-by: Jasper Boeijenga --- .../next/components/Stepper/Stepper.test.tsx | 72 +++++++------------ 1 file changed, 24 insertions(+), 48 deletions(-) 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 d6882c7403..9d661aeb7e 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -260,38 +260,16 @@ describe('Stepper', () => { expect(onCreate).toHaveBeenCalledWith({ moreInfo: false }); }); - it('should merge nested formData correctly in multiple steps', async () => { - const Repo = ({ - onChange, - }: FieldExtensionComponentProps<{ repository: string }, any>) => ( - onChange({ repository: e.target.value })} - defaultValue="" - /> - ); - - const Owner = ({ - onChange, - }: FieldExtensionComponentProps<{ owner: string }, any>) => ( - onChange({ owner: e.target.value })} - defaultValue="" - /> - ); - + it('should merge and overwrite nested formData correctly', async () => { const manifest: TemplateParameterSchema = { steps: [ { title: 'Step 1', schema: { properties: { - first: { - type: 'object', - 'ui:field': 'Repo', + foo: { + type: 'string', + title: 'Foo - 1', }, }, }, @@ -300,9 +278,13 @@ describe('Stepper', () => { title: 'Step 2', schema: { properties: { - second: { - type: 'object', - 'ui:field': 'Owner', + foo: { + type: 'string', + title: 'Foo - 2', + }, + bar: { + type: 'string', + title: 'Bar - 2', }, }, }, @@ -311,36 +293,27 @@ describe('Stepper', () => { title: 'React JSON Schema Form Test', }; - const onCreate = jest.fn(async (values: Record) => { - expect(values).toEqual({ - first: { repository: 'Repo' }, - second: { owner: 'Owner' }, - }); - }); + const onCreate = jest.fn(); const { getByRole } = await renderInTestApp( - + , ); await act(async () => { - fireEvent.change(getByRole('textbox', { name: 'repo' }), { - target: { value: 'Repo' }, + fireEvent.change(getByRole('textbox', { name: 'Foo - 1' }), { + target: { value: 'value 1' }, }); fireEvent.click(getByRole('button', { name: 'Next' })); }); await act(async () => { - fireEvent.change(getByRole('textbox', { name: 'owner' }), { - target: { value: 'Owner' }, + fireEvent.change(getByRole('textbox', { name: 'Foo - 2' }), { + target: { value: 'value 2' }, + }); + fireEvent.change(getByRole('textbox', { name: 'Bar - 2' }), { + target: { value: 'value 2' }, }); fireEvent.click(getByRole('button', { name: 'Review' })); }); @@ -349,7 +322,10 @@ describe('Stepper', () => { fireEvent.click(getByRole('button', { name: 'Create' })); }); - expect(onCreate).toHaveBeenCalled(); + expect(onCreate).toHaveBeenCalledWith({ + foo: 'value 2', + bar: 'value 2', + }); }); it('should render custom field extensions properly', async () => { From a6ce155b8dea5f6b4ceda05ba3c396dcaedb40af Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Wed, 4 Sep 2024 18:10:49 +0200 Subject: [PATCH 12/12] Remove unused import Signed-off-by: Jasper Boeijenga --- .../src/next/components/Stepper/Stepper.test.tsx | 1 - 1 file changed, 1 deletion(-) 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 9d661aeb7e..b2ec74c41a 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -18,7 +18,6 @@ import { JsonValue } from '@backstage/types'; import { act, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; -import { FieldExtensionComponentProps } from '../../../extensions'; import { LayoutTemplate } from '../../../layouts'; import { SecretsContextProvider } from '../../../secrets'; import { TemplateParameterSchema } from '../../../types';