add ScaffolderLayouts to NextScaffolderPage
Signed-off-by: Paul Cowan <paul.cowan@cutting.scot>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user