add ScaffolderLayouts to NextScaffolderPage

Signed-off-by: Paul Cowan <paul.cowan@cutting.scot>
This commit is contained in:
Paul Cowan
2022-12-23 16:36:52 +00:00
parent cd63a3d39f
commit d2ddde2108
8 changed files with 138 additions and 12 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Add `ScaffolderLayouts` to `NextScaffolderPage`
@@ -392,4 +392,47 @@ describe('Stepper', () => {
await fireEvent.click(getByRole('button', { name: 'Make' }));
});
});
describe('Scaffolder Layouts', () => {
it('should render the step in the scaffolder layout', async () => {
const ScaffolderLayout: LayoutTemplate = ({ properties }) => (
<>
<h1>A Scaffolder Layout</h1>
{properties.map((prop, i) => (
<div key={i}>{prop.content}</div>
))}
</>
);
const manifest: TemplateParameterSchema = {
steps: [
{
title: 'Step 1',
schema: {
type: 'object',
'ui:ObjectFieldTemplate': 'Layout',
properties: {
field1: {
type: 'string',
},
},
},
},
],
title: 'scaffolder layouts',
};
const { getByText, getByRole } = await renderInTestApp(
<Stepper
manifest={manifest}
extensions={[]}
onComplete={jest.fn()}
layouts={[{ name: 'Layout', component: ScaffolderLayout }]}
/>,
);
expect(getByText('A Scaffolder Layout')).toBeInTheDocument();
expect(getByRole('textbox', { name: 'field1' })).toBeInTheDocument();
});
});
});
@@ -28,11 +28,14 @@ import React, { useCallback, useMemo, useState, type ReactNode } from 'react';
import { NextFieldExtensionOptions } from '../../extensions';
import { TemplateParameterSchema } from '../../../types';
import { createAsyncValidators } from './createAsyncValidators';
import type { FormProps } from '../../types';
import type { FormProps, LayoutOptions } from '../../types';
import { ReviewState, type ReviewStateProps } from '../ReviewState';
import { useTemplateSchema } from '../../hooks/useTemplateSchema';
import { useFormDataFromQuery } from '../../hooks/useFormDataFromQuery';
import validator from '@rjsf/validator-ajv6';
import { useFormDataFromQuery } from '../../hooks';
import type { FormProps } from '../../types';
import { selectedTemplateRouteRef } from '../../../routes';
const useStyles = makeStyles(theme => ({
backButton: {
@@ -65,6 +68,7 @@ export type StepperProps = {
createButtonText?: ReactNode;
reviewButtonText?: ReactNode;
};
layouts?: LayoutOptions[];
};
// TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly
@@ -76,17 +80,15 @@ const Form = withTheme(require('@rjsf/material-ui-v5').Theme);
* The `Stepper` component is the Wizard that is rendered when a user selects a template
* @alpha
*/
export const Stepper = (stepperProps: StepperProps) => {
const { components = {}, ...props } = stepperProps;
const { layouts = [], components = {}, ...props } = stepperProps;
const {
ReviewStateComponent = ReviewState,
createButtonText = 'Create',
reviewButtonText = 'Review',
} = components;
const analytics = useAnalytics();
const { steps } = useTemplateSchema(props.manifest);
const { steps } = useTemplateSchema(props.manifest, layouts);
const apiHolder = useApiHolder();
const [activeStep, setActiveStep] = useState(0);
const [formState, setFormState] = useFormDataFromQuery(props.initialState);
@@ -177,7 +179,7 @@ export const Stepper = (stepperProps: StepperProps) => {
fields={extensions}
showErrorList={false}
onChange={handleChange}
{...(props.FormProps ?? {})}
{...(formProps ?? {})}
>
<div className={styles.footer}>
<Button
@@ -233,4 +233,42 @@ describe('useTemplateSchema', () => {
});
});
});
it('should replace ui:ObjectFieldTemplate with actual component', () => {
const layouts = [{ name: 'TwoColumn', component: jest.fn() }];
const manifest: TemplateParameterSchema = {
title: 'Test Template',
description: 'Test Template Description',
steps: [
{
title: 'Step 1',
description: 'Step 1 Description',
schema: {
type: 'object',
'ui:ObjectFieldTemplate': 'TwoColumn',
properties: {
field1: {
type: 'string',
},
},
},
},
],
};
const { result } = renderHook(() => useTemplateSchema(manifest, layouts), {
wrapper: ({ children }) => (
<TestApiProvider
apis={[[featureFlagsApiRef, { isActive: () => true }]]}
>
{children}
</TestApiProvider>
),
});
const [{ uiSchema }] = result.current.steps;
expect(uiSchema['ui:ObjectFieldTemplate']).toEqual(layouts[0].component);
});
});
@@ -24,3 +24,22 @@ export type FormProps = Pick<
SchemaFormProps,
'transformErrors' | 'noHtml5Validate'
>;
/**
* The field template from `@rjsf/core` which is a react component that gets passed `@rjsf/core` field related props.
*
* @public
*/
export type LayoutTemplate<T = any> = NonNullable<
SchemaFormProps<T>['uiSchema']
>['ui:ObjectFieldTemplate'];
/**
* The type of layouts that is passed to the TemplateForms
*
* @public
*/
export interface LayoutOptions<P = any> {
name: string;
component: LayoutTemplate<P>;
}
@@ -76,6 +76,7 @@ describe('Router', () => {
expect(FormProps).toEqual({
transformErrors: transformErrorsMock,
noHtml5Validate: true,
layouts: [],
});
});
@@ -27,8 +27,8 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups';
import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default';
import { nextSelectedTemplateRouteRef } from '../routes';
import { type LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../../layouts';
import type { FormProps } from '../types';
import { LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../../layouts';
import type { FormProps, NextLayoutOptions } from '../types';
/**
* The Props for the Scaffolder Router
@@ -71,7 +71,7 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
.selectByComponentData({
key: LAYOUTS_WRAPPER_KEY,
})
.findComponentData<LayoutOptions>({
.findComponentData<NextLayoutOptions>({
key: LAYOUTS_KEY,
}),
);
@@ -95,7 +95,7 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
customFieldExtensions={fieldExtensions}
FormProps={{
...props.FormProps,
layouts: customLayouts
layouts: customLayouts,
}}
/>
</SecretsContextProvider>
+20 -2
View File
@@ -21,7 +21,25 @@
*/
import type { FormProps as SchemaFormProps } from '@rjsf/core-v5';
import { LayoutOptions } from '../layouts/types';
/**
* The field template from \@rjsf/core which is a react component that gets passed \@rjsf/core field related props.
*
* @public
*/
export type NextLayoutTemplate<T = any> = NonNullable<
SchemaFormProps<T>['uiSchema']
>['ui:ObjectFieldTemplate'];
/**
* The type of layouts that is passed to the TemplateForms
*
* @public
*/
export interface NextLayoutOptions<P = any> {
name: string;
component: NextLayoutTemplate<P>;
}
/**
* Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage`
@@ -33,5 +51,5 @@ export type FormProps = Pick<
SchemaFormProps,
'transformErrors' | 'noHtml5Validate'
> & {
layouts?: LayoutOptions[];
layouts?: NextLayoutOptions[];
};