Merge pull request #15402 from dagda1/onchange-handler-for-stepper
add onChange handler to next/Stepper
This commit is contained in:
@@ -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,98 @@ describe('Stepper', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge nested formData correctly in multiple steps', async () => {
|
||||
const Repo = ({
|
||||
onChange,
|
||||
}: NextFieldExtensionComponentProps<{ repository: string }, any>) => (
|
||||
<input
|
||||
aria-label="repo"
|
||||
type="text"
|
||||
onChange={e => onChange({ repository: e.target.value })}
|
||||
defaultValue=""
|
||||
/>
|
||||
);
|
||||
|
||||
const Owner = ({
|
||||
onChange,
|
||||
}: NextFieldExtensionComponentProps<{ owner: string }, any>) => (
|
||||
<input
|
||||
aria-label="owner"
|
||||
type="text"
|
||||
onChange={e => 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<string, JsonValue>) => {
|
||||
expect(values).toEqual({
|
||||
first: { repository: 'Repo' },
|
||||
second: { owner: 'Owner' },
|
||||
});
|
||||
});
|
||||
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Stepper
|
||||
manifest={manifest}
|
||||
onComplete={onComplete}
|
||||
extensions={[
|
||||
{ name: 'Repo', component: Repo },
|
||||
{ name: 'Owner', component: Owner },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
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' }));
|
||||
});
|
||||
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render custom field extensions properly', async () => {
|
||||
const MockComponent = () => {
|
||||
return <h1>im a custom field extension</h1>;
|
||||
|
||||
@@ -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,12 @@ export const Stepper = (props: StepperProps) => {
|
||||
setActiveStep(prevActiveStep => prevActiveStep - 1);
|
||||
};
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: IChangeEvent) =>
|
||||
setFormState(current => ({ ...current, ...e.formData })),
|
||||
[setFormState],
|
||||
);
|
||||
|
||||
const handleNext = async ({
|
||||
formData,
|
||||
}: {
|
||||
@@ -111,18 +116,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 +132,7 @@ export const Stepper = (props: StepperProps) => {
|
||||
return stepNum;
|
||||
});
|
||||
}
|
||||
setFormState(current => ({ ...current, ...newFormData }));
|
||||
setFormState(current => ({ ...current, ...formData }));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -165,6 +159,7 @@ export const Stepper = (props: StepperProps) => {
|
||||
onSubmit={handleNext}
|
||||
fields={extensions}
|
||||
showErrorList={false}
|
||||
onChange={handleChange}
|
||||
{...(props.FormProps ?? {})}
|
||||
>
|
||||
<div className={styles.footer}>
|
||||
|
||||
Reference in New Issue
Block a user