pass layouts throught to MultiJsonForm
Signed-off-by: Paul Cowan <paul.cowan@cutting.scot>
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
-1
@@ -36,7 +36,6 @@ export const DefaultStepFormLayout = ({
|
||||
}: ObjectFieldTemplateProps) => {
|
||||
return (
|
||||
<>
|
||||
<h1>THIS IS OUR OBJECTFIELDTEMPLATE!!!!!!</h1>
|
||||
{(uiSchema['ui:title'] || title) && (
|
||||
<TitleField
|
||||
id={`${idSchema.$id}-title`}
|
||||
|
||||
@@ -48,6 +48,8 @@ export type {
|
||||
FieldExtensionComponentProps,
|
||||
FieldExtensionComponent,
|
||||
} from './extensions';
|
||||
export { createScaffolderLayout, ScaffolderLayouts } from './layouts';
|
||||
export type { LayoutOptions, ObjectFieldTemplate } from './layouts';
|
||||
export {
|
||||
EntityPickerFieldExtension,
|
||||
EntityNamePickerFieldExtension,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user