Skip to content

Commit

Permalink
Merge pull request janus-idp#69 from parodos-dev/pc/horizontal-stepper
Browse files Browse the repository at this point in the history
horizontal dynamic form stepper
  • Loading branch information
dagda1 authored Mar 27, 2023
2 parents d45a732 + 7ddb1a7 commit d7ebd6f
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 118 deletions.
2 changes: 1 addition & 1 deletion plugins/parodos/src/components/Form/Form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('<Form />', () => {
});

await act(async () => {
await fireEvent.click(getByRole('button', { name: 'Submit' }));
await fireEvent.click(getByRole('button', { name: 'NEXT' }));
});

expect(onSubmit).toHaveBeenCalled();
Expand Down
58 changes: 26 additions & 32 deletions plugins/parodos/src/components/Form/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useCallback, type ReactNode } from 'react';
import React, { useState, useCallback, type ReactNode, useRef } from 'react';
import validator from '@rjsf/validator-ajv8';
import { Form as JsonForm } from './RJSF';
import {
Expand All @@ -9,14 +9,16 @@ import type { FormSchema } from '../types';
import { JsonValue } from '@backstage/types';
import {
Step,
StepContent,
StepLabel,
Stepper,
Button,
makeStyles,
ButtonGroup,
} from '@material-ui/core';
import { FluidObjectFieldTemplate } from '../layouts/FluidObjectFieldTemplate';
import { OutlinedBaseInputTemplate } from './widgets/TextAreaWidget';
import ArrayFieldTemplate from './Templates/ArrayFieldTemplate';
import { default as CoreForm } from '@rjsf/core-v5';
import { useStyles } from './styles';

type FormProps = Pick<
JsonFormProps,
Expand All @@ -26,30 +28,10 @@ type FormProps = Pick<
formSchema: FormSchema;
title?: string;
hideTitle?: boolean;
stepLess?: boolean;
children?: ReactNode;
};

const useStyles = makeStyles(theme => ({
stepLabel: {
'& span': {
fontSize: '1.25rem',
},
},
previous: {
border: `1px solid ${theme.palette.primary.main}`,
color: theme.palette.text.primary,
marginRight: theme.spacing(1),
textTransform: 'uppercase',
'&:disabled': {
border: `1px solid ${theme.palette.text.disabled}`,
},
},
next: {
paddingRight: theme.spacing(4),
paddingLeft: theme.spacing(4),
},
}));

export function Form({
formSchema,
title,
Expand All @@ -59,12 +41,14 @@ export function Form({
className,
transformErrors,
hideTitle = false,
stepLess = false,
children,
...props
}: FormProps): JSX.Element {
const [activeStep, setActiveStep] = useState(0);
const [formState, setFormState] = useState<Record<string, JsonValue>>({});
const styles = useStyles();
const formRef = useRef<CoreForm>(null);

const currentStep = formSchema.steps[activeStep];

Expand Down Expand Up @@ -92,20 +76,22 @@ export function Form({

const TheForm = (
<JsonForm
ref={formRef}
idPrefix=""
className={className}
validator={validator}
noHtml5Validate
showErrorList={false}
onChange={handleChange}
formData={formState}
formContext={{ formData: formState }}
formContext={{ formData: formState, form: formRef }}
onSubmit={handleNext}
schema={currentStep.schema}
disabled={disabled}
templates={{
ObjectFieldTemplate: FluidObjectFieldTemplate,
BaseInputTemplate: OutlinedBaseInputTemplate as any,
ArrayFieldTemplate: ArrayFieldTemplate,
}}
uiSchema={{
...currentStep.uiSchema,
Expand All @@ -115,10 +101,10 @@ export function Form({
transformErrors={transformErrors}
{...props}
>
{formSchema.steps.length === 1 ? (
{stepLess ? (
children
) : (
<div>
<ButtonGroup className={styles.buttonContainer}>
<Button
disabled={activeStep === 0}
className={styles.previous}
Expand All @@ -134,28 +120,36 @@ export function Form({
>
NEXT
</Button>
</div>
</ButtonGroup>
)}
</JsonForm>
);

if (formSchema.steps.length === 1) {
if (stepLess) {
return TheForm;
}

return (
<>
<Stepper activeStep={activeStep} orientation="vertical">
<Stepper
activeStep={activeStep}
orientation="horizontal"
className={styles.stepper}
>
{formSchema.steps.map((step, index) => (
<Step key={index}>
{hideTitle === false && (
<StepLabel className={styles.stepLabel}>{step.title}</StepLabel>
)}
<StepContent key={index}>{TheForm}</StepContent>
</Step>
))}
</Stepper>
{children}
<div className={styles.formWrapper}>
<>
{TheForm}
{children}
</>
</div>
</>
);
}
163 changes: 163 additions & 0 deletions plugins/parodos/src/components/Form/Templates/ArrayFieldTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/* eslint-disable no-console */
import React, { useCallback, useState } from 'react';
import {
getTemplate,
getUiOptions,
ArrayFieldTemplateProps,
ArrayFieldTemplateItemType,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
} from '@rjsf/utils';
import {
Button,
ButtonGroup,
makeStyles,
Step,
StepContent,
StepLabel,
Stepper,
} from '@material-ui/core';
import { assert } from 'assert-ts';
import { default as Form } from '@rjsf/core-v5';
import { useStyles as useFormStyles } from '../styles';

const useStyles = makeStyles(theme => ({
stepper: {
marginBottom: theme.spacing(2),
background: theme.palette.background.default,
'& div[class^="MuiPaper-root"]': {
boxShadow: 'none',
background: theme.palette.background.default,
'& input': {
background: theme.palette.background.paper,
},
},
},
}));

/** The `ArrayFieldTemplate` component is the template used to render all items in an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(props: ArrayFieldTemplateProps<T, S, F>) {
const {
idSchema,
uiSchema,
items,
registry,
required,
schema,
title,
formContext,
} = props;
const uiOptions = getUiOptions(uiSchema);

const ArrayFieldTitleTemplate = getTemplate<
'ArrayFieldTitleTemplate',
T,
S,
F
>('ArrayFieldTitleTemplate', registry, uiOptions);

const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>(
'ArrayFieldItemTemplate',
registry,
uiOptions,
);
const [activeItem, setActiveItem] = useState(0);

const form = formContext?.form?.current as Form;

const handleNext = useCallback(() => {
assert(!!form, 'no form in ArrayFieldTemplate');

const isValid = form.validateForm();

setTimeout(() => {
if (isValid) {
setActiveItem(prev => prev + 1);

return;
}

// find the current array item index to see if we can progress or not
const indexes = form.state.errors.map(error =>
Number(error.property?.match(/(\d+)/g)?.[0]),
);

if (!indexes.includes(activeItem)) {
setActiveItem(prev => prev + 1);
}
});
}, [activeItem, form]);

const styles = useStyles();
const formStyles = useFormStyles();

return (
<>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || title}
schema={schema}
uiSchema={uiSchema}
required={required}
registry={registry}
/>
<Stepper
activeStep={activeItem}
orientation="vertical"
className={styles.stepper}
>
{items &&
items.map(
(
{ key, ...itemProps }: ArrayFieldTemplateItemType<T, S, F>,
index,
) => {
return (
<Step key={key}>
<StepLabel
StepIconProps={{ icon: String.fromCharCode(65 + index) }}
className={formStyles.stepLabel}
>
{uiOptions.title || itemProps.schema.title}
</StepLabel>
<StepContent key={key}>
<>
<ArrayFieldItemTemplate key={key} {...itemProps} />
<ButtonGroup>
<Button
disabled={activeItem === 0}
className={formStyles.previous}
onClick={() =>
setActiveItem(a => (activeItem === 0 ? 0 : a - 1))
}
>
PREVIOUS
</Button>
<Button
variant="contained"
type="button"
color="primary"
onClick={handleNext}
className={formStyles.next}
>
NEXT
</Button>
</ButtonGroup>
</>
</StepContent>
</Step>
);
},
)}
</Stepper>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FormControl, TextField } from '@material-ui/core';
import { Autocomplete, UseAutocompleteProps } from '@material-ui/lab';
import { Autocomplete, type UseAutocompleteProps } from '@material-ui/lab';
import React, { useCallback } from 'react';
import { type PickerFieldExtensionProps } from './types';

Expand Down
35 changes: 35 additions & 0 deletions plugins/parodos/src/components/Form/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { makeStyles } from '@material-ui/core';

export const useStyles = makeStyles(theme => ({
stepLabel: {
'& span': {
fontSize: '1.25rem',
},
},
previous: {
border: `1px solid ${theme.palette.primary.main}`,
color: theme.palette.text.primary,
marginRight: theme.spacing(1),
textTransform: 'uppercase',
'&:disabled': {
border: `1px solid ${theme.palette.text.disabled}`,
},
},
next: {
paddingRight: theme.spacing(4),
paddingLeft: theme.spacing(4),
},
backButton: {
marginRight: theme.spacing(1),
},
buttonContainer: {
marginBottom: theme.spacing(2),
},
formWrapper: {
padding: theme.spacing(2),
},
stepper: {
margin: 0,
paddingLeft: theme.spacing(1),
},
}));
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,6 @@ export function FluidObjectFieldTemplate<
) : (
<Grid container spacing={2} className={styles.container}>
{properties.map((element, index) => {
const container =
element.content.props.uiSchema['ui:hidden'] === true;

if (container) {
return (
<Grid item xs={12} className={styles.item}>
{element.content}
</Grid>
);
}

return element.hidden ? (
element.content
) : (
Expand Down
1 change: 1 addition & 0 deletions plugins/parodos/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export interface Step {
schema: JsonObject;
title: string;
description?: string;
parent?: Step;
}

export interface FormSchema {
Expand Down
Loading

0 comments on commit d7ebd6f

Please sign in to comment.