consolidate props in Stepper and Workflow

Signed-off-by: Paul Cowan <paul.cowan@cutting.scot>
This commit is contained in:
Paul Cowan
2023-01-17 20:05:25 +00:00
parent 7e6fdc0146
commit 72ea4f64b3
6 changed files with 84 additions and 73 deletions
+2 -2
View File
@@ -355,7 +355,7 @@ export type StepperProps = {
templateName?: string;
FormProps?: FormProps;
initialState?: Record<string, JsonValue>;
onComplete: (values: Record<string, JsonValue>) => Promise<void>;
onCreate: (values: Record<string, JsonValue>) => Promise<void>;
ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element;
createButtonText?: string;
reviewButtonText?: string;
@@ -456,7 +456,7 @@ export interface WorkflowProps {
// (undocumented)
namespace: string;
// (undocumented)
onComplete: (values: Record<string, JsonValue>) => Promise<void>;
onCreate: (values: Record<string, JsonValue>) => Promise<void>;
// (undocumented)
onError(error: Error | undefined): JSX.Element | null;
// (undocumented)
@@ -33,7 +33,7 @@ describe('Stepper', () => {
};
const { getByText } = await renderInTestApp(
<Stepper manifest={manifest} extensions={[]} onComplete={jest.fn()} />,
<Stepper manifest={manifest} extensions={[]} onCreate={jest.fn()} />,
);
for (const step of manifest.steps) {
@@ -51,7 +51,7 @@ describe('Stepper', () => {
};
const { getByRole } = await renderInTestApp(
<Stepper manifest={manifest} extensions={[]} onComplete={jest.fn()} />,
<Stepper manifest={manifest} extensions={[]} onCreate={jest.fn()} />,
);
expect(getByRole('button', { name: 'Next' })).toBeInTheDocument();
@@ -91,7 +91,7 @@ describe('Stepper', () => {
};
const { getByRole } = await renderInTestApp(
<Stepper manifest={manifest} extensions={[]} onComplete={jest.fn()} />,
<Stepper manifest={manifest} extensions={[]} onCreate={jest.fn()} />,
);
await fireEvent.change(getByRole('textbox', { name: 'name' }), {
@@ -162,7 +162,7 @@ describe('Stepper', () => {
title: 'React JSON Schema Form Test',
};
const onComplete = jest.fn(async (values: Record<string, JsonValue>) => {
const onCreate = jest.fn(async (values: Record<string, JsonValue>) => {
expect(values).toEqual({
first: { repository: 'Repo' },
second: { owner: 'Owner' },
@@ -172,7 +172,7 @@ describe('Stepper', () => {
const { getByRole } = await renderInTestApp(
<Stepper
manifest={manifest}
onComplete={onComplete}
onCreate={onCreate}
extensions={[
{ name: 'Repo', component: Repo },
{ name: 'Owner', component: Owner },
@@ -200,7 +200,7 @@ describe('Stepper', () => {
await fireEvent.click(getByRole('button', { name: 'Create' }));
});
expect(onComplete).toHaveBeenCalled();
expect(onCreate).toHaveBeenCalled();
});
it('should render custom field extensions properly', async () => {
@@ -229,7 +229,7 @@ describe('Stepper', () => {
<Stepper
manifest={manifest}
extensions={[{ name: 'Mock', component: MockComponent }]}
onComplete={jest.fn()}
onCreate={jest.fn()}
/>,
);
@@ -266,7 +266,7 @@ describe('Stepper', () => {
<Stepper
manifest={manifest}
extensions={[]}
onComplete={jest.fn()}
onCreate={jest.fn()}
FormProps={{ transformErrors }}
/>,
);
@@ -308,7 +308,7 @@ describe('Stepper', () => {
});
const { getByRole } = await renderInTestApp(
<Stepper manifest={manifest} extensions={[]} onComplete={jest.fn()} />,
<Stepper manifest={manifest} extensions={[]} onCreate={jest.fn()} />,
);
expect(getByRole('textbox', { name: 'firstName' })).toHaveValue('John');
@@ -331,12 +331,12 @@ describe('Stepper', () => {
title: 'initialize formData',
};
const onComplete = jest.fn(async (values: Record<string, JsonValue>) => {
const onCreate = jest.fn(async (values: Record<string, JsonValue>) => {
expect(values).toHaveProperty('firstName');
});
const { getByRole } = await renderInTestApp(
<Stepper manifest={manifest} extensions={[]} onComplete={onComplete} />,
<Stepper manifest={manifest} extensions={[]} onCreate={onCreate} />,
);
await act(async () => {
@@ -373,10 +373,12 @@ describe('Stepper', () => {
const { getByRole } = await renderInTestApp(
<Stepper
manifest={manifest}
onComplete={jest.fn()}
onCreate={jest.fn()}
extensions={[]}
createButtonText="Make"
reviewButtonText="Inspect"
components={{
createButtonText: <b>Make</b>,
reviewButtonText: <i>Inspect</i>,
}}
/>,
);
@@ -24,7 +24,7 @@ import {
} from '@material-ui/core';
import { type IChangeEvent, withTheme } from '@rjsf/core-v5';
import { ErrorSchema, FieldValidation } from '@rjsf/utils';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState, type ReactNode } from 'react';
import { NextFieldExtensionOptions } from '../../extensions';
import { TemplateParameterSchema } from '../../../types';
import { createAsyncValidators } from './createAsyncValidators';
@@ -59,11 +59,12 @@ export type StepperProps = {
templateName?: string;
FormProps?: FormProps;
initialState?: Record<string, JsonValue>;
onComplete: (values: Record<string, JsonValue>) => Promise<void>;
ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element;
createButtonText?: string;
reviewButtonText?: string;
onCreate: (values: Record<string, JsonValue>) => Promise<void>;
components?: {
ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element;
createButtonText?: ReactNode;
reviewButtonText?: ReactNode;
};
};
// TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly
@@ -77,12 +78,12 @@ const Form = withTheme(require('@rjsf/material-ui-v5').Theme);
*/
export const Stepper = (stepperProps: StepperProps) => {
const { components = {}, ...props } = stepperProps;
const {
ReviewStateComponent = ReviewState,
createButtonText = 'Create',
reviewButtonText = 'Review',
...props
} = stepperProps;
} = components;
const analytics = useAnalytics();
const { steps } = useTemplateSchema(props.manifest);
@@ -205,7 +206,7 @@ export const Stepper = (stepperProps: StepperProps) => {
<Button
variant="contained"
onClick={() => {
props.onComplete(formState);
props.onCreate(formState);
const name =
typeof formState.name === 'string'
? formState.name
@@ -45,7 +45,7 @@ const apis = TestApiRegistry.from(
describe('<Workflow />', () => {
it('should complete a workflow', async () => {
const onComplete = jest.fn();
const onCreate = jest.fn();
const onError = jest.fn();
scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' });
@@ -61,6 +61,16 @@ describe('<Workflow />', () => {
},
},
},
{
title: 'Step 2',
schema: {
properties: {
age: {
type: 'string',
},
},
},
},
],
title: 'React JSON Schema Form Test',
});
@@ -73,17 +83,22 @@ describe('<Workflow />', () => {
## This is markdown
- overriding the template description
`}
onComplete={onComplete}
onCreate={onCreate}
onError={onError}
namespace="default"
templateName="docs-template"
initialFormState={{
initialState={{
name: 'prefilled-name',
age: '53',
}}
ReviewStateComponent={() => (
<h1>This is a different wrapper for the review page</h1>
)}
customFieldExtensions={[]}
components={{
ReviewStateComponent: () => (
<h1>This is a different wrapper for the review page</h1>
),
reviewButtonText: <i>Onwards</i>,
createButtonText: <b>Make</b>,
}}
extensions={[]}
/>
</ApiProvider>,
);
@@ -93,14 +108,24 @@ describe('<Workflow />', () => {
'Different title than template',
);
const input = getByRole('textbox') as HTMLInputElement;
const nameInput = getByRole('textbox', {
name: 'name',
}) as HTMLInputElement;
expect(input).toBeInTheDocument();
expect(nameInput).toBeInTheDocument();
expect(input.value).toBe('prefilled-name');
expect(nameInput.value).toBe('prefilled-name');
await act(async () => {
fireEvent.click(getByRole('button', { name: 'Review' }));
fireEvent.click(getByRole('button', { name: 'Next' }));
});
const ageInput = getByRole('textbox', { name: 'age' }) as HTMLInputElement;
expect(ageInput.value).toBe('53');
await act(async () => {
fireEvent.click(getByRole('button', { name: 'Onwards' }));
});
expect(
@@ -111,6 +136,9 @@ describe('<Workflow />', () => {
fireEvent.click(getAllByRole('button')[1] as HTMLButtonElement);
});
expect(onComplete).toHaveBeenCalledWith({ name: 'prefilled-name' });
expect(onCreate).toHaveBeenCalledWith({
name: 'prefilled-name',
age: '53',
});
});
});
@@ -21,16 +21,11 @@ import {
Progress,
} from '@backstage/core-components';
import { stringifyEntityRef } from '@backstage/catalog-model';
import type { ErrorTransformer } from '@rjsf/utils';
import type { JsonValue } from '@backstage/types';
import { makeStyles } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { type NextFieldExtensionOptions } from '../../extensions/types';
import { type FormProps } from '../../types';
import { ReviewState, ReviewStateProps } from '../ReviewState/ReviewState';
import { useTemplateParameterSchema } from '../../hooks/useTemplateParameterSchema';
import { Stepper } from '../Stepper/Stepper';
import { Stepper, type StepperProps } from '../Stepper/Stepper';
import { SecretsContextProvider } from '../../../secrets/SecretsContext';
const useStyles = makeStyles<BackstageTheme>(() => ({
@@ -48,35 +43,29 @@ const useStyles = makeStyles<BackstageTheme>(() => ({
/**
* @alpha
*/
export interface WorkflowProps {
export type WorkflowProps = {
title?: string;
description?: string;
namespace: string;
templateName: string;
customFieldExtensions: NextFieldExtensionOptions<any, any>[];
transformErrors?: ErrorTransformer;
onComplete: (values: Record<string, JsonValue>) => Promise<void>;
onError(error: Error | undefined): JSX.Element | null;
initialFormState?: Record<string, JsonValue>;
FormProps?: FormProps;
ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element;
}
} & Pick<
StepperProps,
'extensions' | 'FormProps' | 'components' | 'onCreate' | 'initialState'
>;
/**
* @alpha
*/
export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => {
const {
ReviewStateComponent = ReviewState,
FormProps = {},
...props
} = workflowProps;
const { title, description, namespace, templateName, ...props } =
workflowProps;
const styles = useStyles();
const templateRef = stringifyEntityRef({
kind: 'Template',
namespace: props.namespace,
name: props.templateName,
namespace: namespace,
name: templateName,
});
const errorApi = useApi(errorApiRef);
@@ -98,26 +87,17 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => {
{loading && <Progress />}
{manifest && (
<InfoCard
title={props.title ?? manifest.title}
title={title ?? manifest.title}
subheader={
<MarkdownContent
className={styles.markdown}
content={
props.description ?? manifest.description ?? 'No description'
}
content={description ?? manifest.description ?? 'No description'}
/>
}
noPadding
titleTypographyProps={{ component: 'h2' }}
>
<Stepper
manifest={manifest}
extensions={props.customFieldExtensions}
onComplete={props.onComplete}
FormProps={FormProps}
initialState={props.initialFormState}
ReviewStateComponent={ReviewStateComponent}
/>
<Stepper manifest={manifest} {...props} />
</InfoCard>
)}
</Content>
@@ -55,7 +55,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
name: templateName,
});
const onComplete = async (values: Record<string, JsonValue>) => {
const onCreate = async (values: Record<string, JsonValue>) => {
const { taskId } = await scaffolderApi.scaffold({
templateRef,
values,
@@ -78,9 +78,9 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
<Workflow
namespace={namespace}
templateName={templateName}
onComplete={onComplete}
onCreate={onCreate}
onError={onError}
customFieldExtensions={props.customFieldExtensions}
extensions={props.customFieldExtensions}
FormProps={props.FormProps}
/>
</Page>