pass layouts throught to MultiJsonForm

Signed-off-by: Paul Cowan <paul.cowan@cutting.scot>
This commit is contained in:
Paul Cowan
2022-08-10 19:47:46 +01:00
committed by blam
parent ff28494249
commit 0fd0385615
18 changed files with 131 additions and 48 deletions
+10 -3
View File
@@ -64,6 +64,7 @@ import {
ScaffolderPage,
NextScaffolderPage,
scaffolderPlugin,
ScaffolderLayouts,
} from '@backstage/plugin-scaffolder';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
@@ -104,6 +105,7 @@ import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
import { PlaylistIndexPage } from '@backstage/plugin-playlist';
import { Customlayout } from './components/scaffolder/customScaffolderLayouts';
const app = createApp({
apis,
@@ -215,9 +217,14 @@ const routes = (
/>
}
>
<ScaffolderFieldExtensions>
<LowerCaseValuePickerFieldExtension />
</ScaffolderFieldExtensions>
<>
<ScaffolderFieldExtensions>
<LowerCaseValuePickerFieldExtension />
</ScaffolderFieldExtensions>
<ScaffolderLayouts>
<Customlayout />
</ScaffolderLayouts>
</>
</Route>
<Route
path="/create/next"
@@ -0,0 +1,43 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
createScaffolderLayout,
ObjectFieldTemplate,
scaffolderPlugin,
} from '@backstage/plugin-scaffolder';
const ALayout: ObjectFieldTemplate = ({ properties, description }) => {
// eslint-disable-next-line no-console
return (
<div>
<h1>CUSTOM LAYOUT!!!!!</h1>
<div>
{properties.map(prop => (
<div key={prop.content.key}>{prop.content}</div>
))}
</div>
{description}
</div>
);
};
export const Customlayout = scaffolderPlugin.provide(
createScaffolderLayout({
name: 'CustomLayout',
component: ALayout,
}),
);
@@ -17,6 +17,7 @@
export const defaultPreviewTemplate = `# Edit the template parameters below to see how they will render in the scaffolder form UI
parameters:
- title: Fill in some steps
ui:ObjectFieldTemplate: 'CustomLayout'
required:
- name
properties:
@@ -36,6 +36,7 @@ import { transformSchemaToProps } from './schema';
import { Content, StructuredMetadataTable } from '@backstage/core-components';
import cloneDeep from 'lodash/cloneDeep';
import * as fieldOverrides from './FieldOverrides';
import { DEFAULT_SCAFFOLDER_LAYOUT, LayoutOptions } from '../../layouts';
const Form = withTheme(MuiTheme);
type Step = {
@@ -55,7 +56,7 @@ type Props = {
widgets?: FormProps<any>['widgets'];
fields?: FormProps<any>['fields'];
finishButtonLabel?: string;
layout?: FormProps<any>['ObjectFieldTemplate'];
layouts: LayoutOptions[];
};
export function getUiSchemasFromSteps(steps: Step[]): UiSchema[] {
@@ -120,7 +121,7 @@ export const MultistepJsonForm = (props: Props) => {
fields,
widgets,
finishButtonLabel,
layout,
layouts,
} = props;
const [activeStep, setActiveStep] = useState(0);
const [disableButtons, setDisableButtons] = useState(false);
@@ -153,6 +154,21 @@ export const MultistepJsonForm = (props: Props) => {
)
: filteredStep.schema.required;
}
const layoutName =
filteredStep.schema['ui:ObjectFieldTemplate'] ??
DEFAULT_SCAFFOLDER_LAYOUT.name;
const LayoutComponent = layouts.find(
layout => layout.name === layoutName,
)?.component;
if (!LayoutComponent) {
throw new Error(`no step layout found for ${layoutName}`);
}
filteredStep.schema['ui:ObjectFieldTemplate'] = LayoutComponent as any;
return filteredStep;
};
@@ -205,7 +221,6 @@ export const MultistepJsonForm = (props: Props) => {
</StepLabel>
<StepContent key={title}>
<Form
ObjectFieldTemplate={layout}
showErrorList={false}
fields={{ ...fieldOverrides, ...fields }}
widgets={widgets}
+9 -3
View File
@@ -122,7 +122,13 @@ export const Router = (props: RouterProps) => {
}),
);
const layout = customLayouts?.[0] ?? DEFAULT_SCAFFOLDER_LAYOUT;
if (
!customLayouts.find(
layout => layout.name === DEFAULT_SCAFFOLDER_LAYOUT.name,
)
) {
customLayouts.push(DEFAULT_SCAFFOLDER_LAYOUT);
}
/**
* This component can be deleted once the older routes have been deprecated.
@@ -163,7 +169,7 @@ export const Router = (props: RouterProps) => {
<SecretsContextProvider>
<TemplatePage
customFieldExtensions={fieldExtensions}
layout={layout}
layouts={customLayouts}
/>
</SecretsContextProvider>
}
@@ -181,7 +187,7 @@ export const Router = (props: RouterProps) => {
<TemplateEditorPage
defaultPreviewTemplate={defaultPreviewTemplate}
customFieldExtensions={fieldExtensions}
layout={layout}
layouts={customLayouts}
/>
</SecretsContextProvider>
}
@@ -16,7 +16,7 @@
import { makeStyles } from '@material-ui/core';
import React, { useState } from 'react';
import { FieldExtensionOptions } from '../../extensions';
import { LayoutOptions } from '../../layouts';
import type { LayoutOptions } from '../../layouts';
import { TemplateDirectoryAccess } from '../../lib/filesystem';
import { DirectoryEditorProvider } from './DirectoryEditorContext';
import { DryRunProvider } from './DryRunContext';
@@ -58,7 +58,7 @@ const useStyles = makeStyles({
export const TemplateEditor = (props: {
directory: TemplateDirectoryAccess;
fieldExtensions?: FieldExtensionOptions<any, any>[];
layout?: LayoutOptions;
layouts?: LayoutOptions[];
onClose?: () => void;
}) => {
const classes = useStyles();
@@ -79,7 +79,7 @@ export const TemplateEditor = (props: {
<TemplateEditorForm.DirectoryEditorDryRun
setErrorText={setErrorText}
fieldExtensions={props.fieldExtensions}
layout={props.layout}
layouts={props.layouts}
/>
</section>
<section className={classes.results}>
@@ -84,7 +84,7 @@ interface TemplateEditorFormProps {
onDryRun?: (data: JsonObject) => Promise<void>;
fieldExtensions?: FieldExtensionOptions<any, any>[];
layout?: LayoutOptions;
layouts?: LayoutOptions[];
}
function isJsonObject(value: JsonValue | undefined): value is JsonObject {
@@ -101,7 +101,7 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
onDryRun,
setErrorText,
fieldExtensions = [],
layout = DEFAULT_SCAFFOLDER_LAYOUT,
layouts = [DEFAULT_SCAFFOLDER_LAYOUT],
} = props;
const classes = useStyles();
const apiHolder = useApiHolder();
@@ -141,6 +141,7 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
}
const { parameters } = rootObj;
if (!Array.isArray(parameters)) {
setErrorText('Template parameters must be an array');
setSteps(undefined);
@@ -191,7 +192,7 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
onReset={() => onUpdate({})}
finishButtonLabel={onDryRun && 'Try It'}
onFinish={onDryRun && (() => onDryRun(data))}
layout={layout.component}
layouts={layouts}
/>
</ErrorBoundary>
</div>
@@ -203,7 +204,7 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
export function TemplateEditorFormDirectoryEditorDryRun(
props: Pick<
TemplateEditorFormProps,
'setErrorText' | 'fieldExtensions' | 'layout'
'setErrorText' | 'fieldExtensions' | 'layouts'
>,
) {
const { setErrorText, fieldExtensions = [] } = props;
@@ -23,7 +23,7 @@ import { TemplateEditorIntro } from './TemplateEditorIntro';
import { TemplateEditor } from './TemplateEditor';
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
import { FieldExtensionOptions } from '../../extensions';
import { LayoutOptions } from '../../layouts';
import type { LayoutOptions } from '../../layouts';
type Selection =
| {
@@ -37,7 +37,7 @@ type Selection =
interface TemplateEditorPageProps {
defaultPreviewTemplate?: string;
customFieldExtensions?: FieldExtensionOptions<any, any>[];
layout?: LayoutOptions;
layouts?: LayoutOptions[];
}
export function TemplateEditorPage(props: TemplateEditorPageProps) {
@@ -50,7 +50,7 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
directory={selection.directory}
fieldExtensions={props.customFieldExtensions}
onClose={() => setSelection(undefined)}
layout={props.layout}
layouts={props.layouts}
/>
);
} else if (selection?.type === 'form') {
@@ -59,7 +59,7 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
defaultPreviewTemplate={props.defaultPreviewTemplate}
customFieldExtensions={props.customFieldExtensions}
onClose={() => setSelection(undefined)}
layout={props.layout}
layouts={props.layouts}
/>
);
} else {
@@ -111,12 +111,12 @@ export const TemplateFormPreviewer = ({
defaultPreviewTemplate = EXAMPLE_TEMPLATE_PARAMS_YAML,
customFieldExtensions = [],
onClose,
layout = DEFAULT_SCAFFOLDER_LAYOUT,
layouts = [DEFAULT_SCAFFOLDER_LAYOUT],
}: {
defaultPreviewTemplate?: string;
customFieldExtensions?: FieldExtensionOptions<any, any>[];
onClose?: () => void;
layout?: LayoutOptions;
layouts?: LayoutOptions[];
}) => {
const classes = useStyles();
const alertApi = useApi(alertApiRef);
@@ -211,7 +211,7 @@ export const TemplateFormPreviewer = ({
data={formState}
onUpdate={setFormState}
setErrorText={setErrorText}
layout={layout}
layouts={layouts}
/>
</div>
</main>
@@ -52,10 +52,10 @@ const useTemplateParameterSchema = (templateRef: string) => {
export const TemplatePage = ({
customFieldExtensions = [],
layout = DEFAULT_SCAFFOLDER_LAYOUT,
layouts = [DEFAULT_SCAFFOLDER_LAYOUT],
}: {
customFieldExtensions?: FieldExtensionOptions<any, any>[];
layout?: LayoutOptions;
layouts?: LayoutOptions[];
}) => {
const apiHolder = useApiHolder();
const secretsContext = useContext(SecretsContext);
@@ -149,7 +149,7 @@ export const TemplatePage = ({
onChange={handleChange}
onReset={handleFormReset}
onFinish={handleCreate}
layout={layout.component}
layouts={layouts}
steps={schema.steps.map(step => {
return {
...step,
@@ -36,7 +36,6 @@ export const DefaultStepFormLayout = ({
}: ObjectFieldTemplateProps) => {
return (
<>
<h1>THIS IS OUR OBJECTFIELDTEMPLATE!!!!!!</h1>
{(uiSchema['ui:title'] || title) && (
<TitleField
id={`${idSchema.$id}-title`}
+2
View File
@@ -48,6 +48,8 @@ export type {
FieldExtensionComponentProps,
FieldExtensionComponent,
} from './extensions';
export { createScaffolderLayout, ScaffolderLayouts } from './layouts';
export type { LayoutOptions, ObjectFieldTemplate } from './layouts';
export {
EntityPickerFieldExtension,
EntityNamePickerFieldExtension,
+8 -5
View File
@@ -15,16 +15,19 @@
*/
import { attachComponentData, Extension } from '@backstage/core-plugin-api';
import type { ObjectFieldTemplateProps } from '@rjsf/core';
import type { LayoutOptions } from './types';
import type { FunctionComponent } from 'react';
export const LAYOUTS_KEY = 'scaffolder.layout.v1';
export const LAYOUTS_WRAPPER_KEY = 'scaffolder.layouts.wrapper.v1';
export function createScaffolderLayout(
export type LayoutComponent<_TReturnValue, _TInputProps> = () => null;
export function createScaffolderLayout<
TFieldReturnValue = unknown,
TInputProps = unknown,
>(
options: LayoutOptions,
): Extension<FunctionComponent<ObjectFieldTemplateProps>> {
): Extension<LayoutComponent<TFieldReturnValue, TInputProps>> {
return {
expose() {
const LayoutDataHolder: any = () => null;
@@ -41,6 +44,6 @@ export const ScaffolderLayouts: React.ComponentType = (): JSX.Element | null =>
attachComponentData(ScaffolderLayouts, LAYOUTS_WRAPPER_KEY, true);
export type { LayoutOptions } from './types';
export type { LayoutOptions, ObjectFieldTemplate } from './types';
export { DEFAULT_SCAFFOLDER_LAYOUT } from './default';
+8 -5
View File
@@ -13,10 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ObjectFieldTemplateProps } from '@rjsf/core';
import type { FunctionComponent } from 'react';
import type { FormProps } from '@rjsf/core';
export type LayoutOptions = {
export type ObjectFieldTemplate = Required<
FormProps<any>
>['ObjectFieldTemplate'];
export interface LayoutOptions {
name: string;
component: FunctionComponent<ObjectFieldTemplateProps>;
};
component: ObjectFieldTemplate;
}
@@ -102,19 +102,17 @@ describe('Router', () => {
it('should extract the custom layout and pass it through', async () => {
const mockLayout = () => null;
const CustomLayout = scaffolderPlugin.provide(
const Customlayout = scaffolderPlugin.provide(
createScaffolderLayout({
name: 'customLayout',
name: 'CustomLayout',
component: mockLayout,
}),
);
const props = {} as ObjectFieldTemplateProps;
await renderInTestApp(
<Router>
<ScaffolderLayouts>
<CustomLayout {...props} />
<Customlayout />
</ScaffolderLayouts>
</Router>,
{ routeEntries: ['/templates/default/foo'] },
@@ -124,7 +122,7 @@ describe('Router', () => {
// eslint-disable-next-line no-console
const [{ layout }] = mock.mock.calls[0];
expect(layout).toEqual({ name: 'customLayout', component: mockLayout });
expect(layout).toEqual({ name: 'CustomLayout', component: mockLayout });
});
});
});
@@ -91,7 +91,13 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
}),
);
const layout = customLayouts?.[0] ?? DEFAULT_SCAFFOLDER_LAYOUT;
if (
!customLayouts.find(
layout => layout.name === DEFAULT_SCAFFOLDER_LAYOUT.name,
)
) {
customLayouts.push(DEFAULT_SCAFFOLDER_LAYOUT);
}
return (
<Routes>
@@ -111,7 +117,7 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
<SecretsContextProvider>
<TemplateWizardPage
customFieldExtensions={fieldExtensions}
layout={layout}
layouts={customLayouts}
/>
</SecretsContextProvider>
}
@@ -26,7 +26,6 @@ import { FieldValidation, withTheme } from '@rjsf/core';
import { Theme as MuiTheme } from '@rjsf/material-ui';
import React, { useMemo, useState } from 'react';
import { FieldExtensionOptions } from '../../../extensions';
import type { LayoutOptions } from '../../../layouts';
import { TemplateParameterSchema } from '../../../types';
import { createAsyncValidators } from './createAsyncValidators';
import { useTemplateSchema } from './useTemplateSchema';
@@ -41,7 +41,7 @@ import type { LayoutOptions } from '../../layouts';
export interface TemplateWizardPageProps {
customFieldExtensions: FieldExtensionOptions<any, any>[];
layout?: LayoutOptions;
layouts: LayoutOptions[];
}
const useStyles = makeStyles<BackstageTheme>(() => ({
@@ -115,7 +115,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
<Stepper
manifest={manifest}
extensions={props.customFieldExtensions}
layout={props.layout}
layouts={props.layouts}
/>
</InfoCard>
)}