>;
+
// @public
export type CustomFieldExtensionSchema = {
returnValue: JSONSchema7;
@@ -116,12 +121,28 @@ export type FieldExtensionOptions<
schema?: CustomFieldExtensionSchema;
};
-// @alpha
+// @public
export type FormProps = Pick<
FormProps_2,
'transformErrors' | 'noHtml5Validate'
>;
+// @public
+export type LayoutComponent<_TInputProps> = () => null;
+
+// @public
+export interface LayoutOptions {
+ // (undocumented)
+ component: LayoutTemplate
;
+ // (undocumented)
+ name: string;
+}
+
+// @public
+export type LayoutTemplate = NonNullable<
+ FormProps_2['uiSchema']
+>['ui:ObjectFieldTemplate'];
+
// @public
export type ListActionsResponse = Array;
@@ -276,6 +297,9 @@ export interface ScaffolderGetIntegrationsListResponse {
}[];
}
+// @public
+export const ScaffolderLayouts: React.ComponentType;
+
// @public (undocumented)
export type ScaffolderOutputLink = {
title?: string;
@@ -361,6 +385,7 @@ export type StepperProps = {
createButtonText?: ReactNode;
reviewButtonText?: ReactNode;
};
+ layouts?: LayoutOptions[];
};
// @alpha
@@ -422,6 +447,11 @@ export const useCustomFieldExtensions: <
outlet: React.ReactNode,
) => TComponentDataType[];
+// @public
+export const useCustomLayouts: >(
+ outlet: React.ReactNode,
+) => TComponentDataType[];
+
// @alpha
export const useFormDataFromQuery: (
initialState?: Record,
@@ -454,7 +484,12 @@ export type WorkflowProps = {
onError(error: Error | undefined): JSX.Element | null;
} & Pick<
StepperProps,
- 'extensions' | 'FormProps' | 'components' | 'onCreate' | 'initialState'
+ | 'extensions'
+ | 'FormProps'
+ | 'components'
+ | 'onCreate'
+ | 'initialState'
+ | 'layouts'
>;
// (No @packageDocumentation comment for this package)
diff --git a/plugins/scaffolder-react/src/extensions/index.tsx b/plugins/scaffolder-react/src/extensions/index.tsx
index 33442f6697..d70f5e36c5 100644
--- a/plugins/scaffolder-react/src/extensions/index.tsx
+++ b/plugins/scaffolder-react/src/extensions/index.tsx
@@ -25,7 +25,8 @@ import { Extension, attachComponentData } from '@backstage/core-plugin-api';
import { FIELD_EXTENSION_KEY, FIELD_EXTENSION_WRAPPER_KEY } from './keys';
/**
- * A type used to wrap up the FieldExtension to embed the ReturnValue and the InputProps
+ * The type used to wrap up the Layout and embed the input props
+ *
* @public
*/
export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null;
diff --git a/plugins/scaffolder-react/src/hooks/index.ts b/plugins/scaffolder-react/src/hooks/index.ts
index 022fe7c38c..af64a9eebc 100644
--- a/plugins/scaffolder-react/src/hooks/index.ts
+++ b/plugins/scaffolder-react/src/hooks/index.ts
@@ -15,3 +15,4 @@
*/
export { useCustomFieldExtensions } from './useCustomFieldExtensions';
+export { useCustomLayouts } from './useCustomLayouts';
diff --git a/plugins/scaffolder-react/src/hooks/useCustomLayouts.ts b/plugins/scaffolder-react/src/hooks/useCustomLayouts.ts
new file mode 100644
index 0000000000..afcc45cd48
--- /dev/null
+++ b/plugins/scaffolder-react/src/hooks/useCustomLayouts.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 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 { useElementFilter } from '@backstage/core-plugin-api';
+import { LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../next/layouts/keys';
+import { type LayoutOptions } from '../next/types';
+
+/**
+ * Hook that returns all custom field extensions from the current outlet.
+ * @public
+ */
+export const useCustomLayouts = (
+ outlet: React.ReactNode,
+) => {
+ return useElementFilter(outlet, elements =>
+ elements
+ .selectByComponentData({
+ key: LAYOUTS_WRAPPER_KEY,
+ })
+ .findComponentData({
+ key: LAYOUTS_KEY,
+ }),
+ );
+};
diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx
index 5dbce3243f..5995d0e419 100644
--- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx
+++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx
@@ -21,6 +21,7 @@ import { act, fireEvent } from '@testing-library/react';
import type { RJSFValidationError } from '@rjsf/utils';
import { JsonValue } from '@backstage/types';
import { NextFieldExtensionComponentProps } from '../../extensions';
+import { LayoutTemplate } from '../../types';
describe('Stepper', () => {
it('should render the step titles for each step of the manifest', async () => {
@@ -392,4 +393,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 }) => (
+ <>
+ A Scaffolder Layout
+ {properties.map((prop, i) => (
+ {prop.content}
+ ))}
+ >
+ );
+
+ 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(
+ ,
+ );
+
+ expect(getByText('A Scaffolder Layout')).toBeInTheDocument();
+ expect(getByRole('textbox', { name: 'field1' })).toBeInTheDocument();
+ });
+ });
});
diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx
index 950c9720de..6189bdc975 100644
--- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx
+++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx
@@ -28,11 +28,12 @@ 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 { 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, LayoutOptions } from '../../types';
+import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps';
const useStyles = makeStyles(theme => ({
backButton: {
@@ -65,6 +66,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,15 +78,13 @@ 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 apiHolder = useApiHolder();
@@ -152,6 +152,8 @@ export const Stepper = (stepperProps: StepperProps) => {
setFormState(current => ({ ...current, ...formData }));
};
+ const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts });
+
return (
<>
@@ -171,8 +173,8 @@ export const Stepper = (stepperProps: StepperProps) => {
extraErrors={errors as unknown as ErrorSchema}
formData={formState}
formContext={{ formData: formState }}
- schema={steps[activeStep].schema}
- uiSchema={steps[activeStep].uiSchema}
+ schema={currentStep.schema}
+ uiSchema={currentStep.uiSchema}
onSubmit={handleNext}
fields={extensions}
showErrorList={false}
diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx
index cca69ab7b3..f6ff12e3a3 100644
--- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx
+++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx
@@ -51,7 +51,12 @@ export type WorkflowProps = {
onError(error: Error | undefined): JSX.Element | null;
} & Pick<
StepperProps,
- 'extensions' | 'FormProps' | 'components' | 'onCreate' | 'initialState'
+ | 'extensions'
+ | 'FormProps'
+ | 'components'
+ | 'onCreate'
+ | 'initialState'
+ | 'layouts'
>;
/**
diff --git a/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.test.tsx b/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.test.tsx
new file mode 100644
index 0000000000..6dba7b9f29
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.test.tsx
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 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 { renderHook } from '@testing-library/react-hooks';
+import { TestApiProvider } from '@backstage/test-utils';
+import { type ParsedTemplateSchema } from './useTemplateSchema';
+import { useTransformSchemaToProps } from './useTransformSchemaToProps';
+
+describe('useTransformSchemaToProps', () => {
+ it('should replace ui:ObjectFieldTemplate with actual component', () => {
+ const layouts = [{ name: 'TwoColumn', component: jest.fn() }];
+
+ const step: ParsedTemplateSchema = {
+ title: 'Fill in some steps',
+ mergedSchema: {},
+ schema: {},
+ uiSchema: {
+ 'ui:ObjectFieldTemplate': 'TwoColumn' as any,
+ name: {
+ 'ui:field': 'EntityNamePicker',
+ 'ui:autofocus': true,
+ },
+ },
+ };
+
+ const { result } = renderHook(
+ () => useTransformSchemaToProps(step, { layouts }),
+ {
+ wrapper: ({ children }) => (
+ {children}
+ ),
+ },
+ );
+
+ const { uiSchema } = result.current;
+
+ expect(uiSchema['ui:ObjectFieldTemplate']).toEqual(layouts[0].component);
+ });
+});
diff --git a/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.ts b/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.ts
new file mode 100644
index 0000000000..ffae1df8bf
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 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 { type ParsedTemplateSchema } from './useTemplateSchema';
+import { type LayoutOptions } from '../types';
+
+interface Options {
+ layouts?: LayoutOptions[];
+}
+
+export const useTransformSchemaToProps = (
+ step: ParsedTemplateSchema,
+ options: Options = {},
+): ParsedTemplateSchema => {
+ const { layouts = [] } = options;
+ const objectFieldTemplate = step?.uiSchema['ui:ObjectFieldTemplate'] as
+ | string
+ | undefined;
+
+ if (typeof objectFieldTemplate !== 'string') {
+ return step;
+ }
+
+ const Layout = layouts.find(
+ layout => layout.name === objectFieldTemplate,
+ )?.component;
+
+ if (!Layout) {
+ return step;
+ }
+
+ return {
+ ...step,
+ uiSchema: {
+ ...step.uiSchema,
+ ['ui:ObjectFieldTemplate']: Layout,
+ },
+ };
+};
diff --git a/plugins/scaffolder-react/src/next/index.ts b/plugins/scaffolder-react/src/next/index.ts
index ea009ea556..4674c1f70a 100644
--- a/plugins/scaffolder-react/src/next/index.ts
+++ b/plugins/scaffolder-react/src/next/index.ts
@@ -15,6 +15,8 @@
*/
export * from './components';
export * from './extensions';
-export type { FormProps } from './types';
+export * from './layouts';
+export * from './types';
export * from './lib';
export * from './hooks';
+export * from './layouts';
diff --git a/plugins/scaffolder/src/layouts/index.tsx b/plugins/scaffolder-react/src/next/layouts/index.tsx
similarity index 80%
rename from plugins/scaffolder/src/layouts/index.tsx
rename to plugins/scaffolder-react/src/next/layouts/index.tsx
index ce1dac9bdf..0b1533c7c8 100644
--- a/plugins/scaffolder/src/layouts/index.tsx
+++ b/plugins/scaffolder-react/src/next/layouts/index.tsx
@@ -1,5 +1,8 @@
+import { LayoutOptions } from '../types';
+import { LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from './keys';
+
/*
- * Copyright 2022 The Backstage Authors
+ * Copyright 2023 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.
@@ -13,16 +16,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import { attachComponentData, Extension } from '@backstage/core-plugin-api';
-import type { LayoutOptions } from './types';
-
-export const LAYOUTS_KEY = 'scaffolder.layout.v1';
-export const LAYOUTS_WRAPPER_KEY = 'scaffolder.layouts.wrapper.v1';
/**
- * The type used to wrap up the Layout and embed the input props
- *
+ * A type used to wrap up the FieldExtension to embed the ReturnValue and the InputProps
* @public
*/
export type LayoutComponent<_TInputProps> = () => null;
@@ -55,5 +52,3 @@ export const ScaffolderLayouts: React.ComponentType = (): JSX.Element | null =>
null;
attachComponentData(ScaffolderLayouts, LAYOUTS_WRAPPER_KEY, true);
-
-export type { LayoutOptions, LayoutTemplate } from './types';
diff --git a/plugins/scaffolder/src/layouts/types.ts b/plugins/scaffolder-react/src/next/layouts/keys.ts
similarity index 54%
rename from plugins/scaffolder/src/layouts/types.ts
rename to plugins/scaffolder-react/src/next/layouts/keys.ts
index 94186d82bf..a82b1597f4 100644
--- a/plugins/scaffolder/src/layouts/types.ts
+++ b/plugins/scaffolder-react/src/next/layouts/keys.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Backstage Authors
+ * Copyright 2023 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.
@@ -13,21 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import type { FormProps } from '@rjsf/core';
-
-/**
- * The field template from \@rjsf/core which is a react component that gets passed \@rjsf/core field related props.
- *
- * @public
- */
-export type LayoutTemplate = FormProps['ObjectFieldTemplate'];
-
-/**
- * The type of layouts that is passed to the TemplateForms
- *
- * @public
- */
-export interface LayoutOptions {
- name: string;
- component: LayoutTemplate
;
-}
+export const LAYOUTS_KEY = 'scaffolder.layout.v1';
+export const LAYOUTS_WRAPPER_KEY = 'scaffolder.layouts.wrapper.v1';
diff --git a/plugins/scaffolder-react/src/next/types.ts b/plugins/scaffolder-react/src/next/types.ts
index 32d0fe85f0..40ba511dbe 100644
--- a/plugins/scaffolder-react/src/next/types.ts
+++ b/plugins/scaffolder-react/src/next/types.ts
@@ -14,13 +14,31 @@
* limitations under the License.
*/
import type { FormProps as SchemaFormProps } from '@rjsf/core-v5';
-
/**
* Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage`
*
- * @alpha
+ * @public
*/
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 = NonNullable<
+ SchemaFormProps['uiSchema']
+>['ui:ObjectFieldTemplate'];
+
+/**
+ * The type of layouts that is passed to the TemplateForms
+ *
+ * @public
+ */
+export interface LayoutOptions {
+ name: string;
+ component: LayoutTemplate
;
+}
diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md
index 82a8dfd8d3..1fb72b0fac 100644
--- a/plugins/scaffolder/api-report.md
+++ b/plugins/scaffolder/api-report.md
@@ -10,21 +10,23 @@ import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { createScaffolderFieldExtension as createScaffolderFieldExtension_2 } from '@backstage/plugin-scaffolder-react';
+import { createScaffolderLayout as createScaffolderLayout_2 } from '@backstage/plugin-scaffolder-react';
import { CustomFieldExtensionSchema as CustomFieldExtensionSchema_2 } from '@backstage/plugin-scaffolder-react';
import { CustomFieldValidator as CustomFieldValidator_2 } from '@backstage/plugin-scaffolder-react';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
-import { Extension } from '@backstage/core-plugin-api';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { FieldExtensionComponent as FieldExtensionComponent_2 } from '@backstage/plugin-scaffolder-react';
import { FieldExtensionComponentProps as FieldExtensionComponentProps_2 } from '@backstage/plugin-scaffolder-react';
import { FieldExtensionOptions as FieldExtensionOptions_2 } from '@backstage/plugin-scaffolder-react';
import { FieldValidation } from '@rjsf/core';
-import type { FormProps as FormProps_2 } from '@rjsf/core';
+import { FormProps as FormProps_2 } from '@backstage/plugin-scaffolder-react';
import type { FormProps as FormProps_3 } from '@rjsf/core-v5';
import { IdentityApi } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
+import { LayoutOptions as LayoutOptions_2 } from '@backstage/plugin-scaffolder-react';
+import { LayoutTemplate as LayoutTemplate_2 } from '@backstage/plugin-scaffolder-react';
import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin-scaffolder-react';
import { LogEvent as LogEvent_2 } from '@backstage/plugin-scaffolder-react';
import { Observable } from '@backstage/types';
@@ -56,10 +58,8 @@ import { z } from 'zod';
// @public @deprecated (undocumented)
export const createScaffolderFieldExtension: typeof createScaffolderFieldExtension_2;
-// @public
-export function createScaffolderLayout(
- options: LayoutOptions,
-): Extension>;
+// @public @deprecated (undocumented)
+export const createScaffolderLayout: typeof createScaffolderLayout_2;
// @public @deprecated (undocumented)
export type CustomFieldExtensionSchema = CustomFieldExtensionSchema_2;
@@ -161,19 +161,11 @@ export type FormProps = Pick<
'transformErrors' | 'noHtml5Validate'
>;
-// @public
-export type LayoutComponent<_TInputProps> = () => null;
+// @public @deprecated (undocumented)
+export type LayoutOptions = LayoutOptions_2;
-// @public
-export interface LayoutOptions {
- // (undocumented)
- component: LayoutTemplate
;
- // (undocumented)
- name: string;
-}
-
-// @public
-export type LayoutTemplate = FormProps_2['ObjectFieldTemplate'];
+// @public @deprecated (undocumented)
+export type LayoutTemplate = LayoutTemplate_2;
// @public @deprecated (undocumented)
export type ListActionsResponse = ListActionsResponse_2;
@@ -207,7 +199,7 @@ export type NextRouterProps = {
TaskPageComponent?: React_2.ComponentType<{}>;
};
groups?: TemplateGroupFilter[];
- FormProps?: FormProps;
+ FormProps?: FormProps_2;
};
// @alpha
@@ -453,8 +445,8 @@ export type ScaffolderGetIntegrationsListOptions =
export type ScaffolderGetIntegrationsListResponse =
ScaffolderGetIntegrationsListResponse_2;
-// @public
-export const ScaffolderLayouts: React.ComponentType;
+// @public @deprecated (undocumented)
+export const ScaffolderLayouts: ComponentType<{}>;
// @public @deprecated (undocumented)
export type ScaffolderOutputlink = ScaffolderOutputLink;
diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx
index 3a1196bc0e..a74b65b885 100644
--- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx
+++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx
@@ -35,10 +35,12 @@ import React, { ComponentType, useState } from 'react';
import { transformSchemaToProps } from './schema';
import cloneDeep from 'lodash/cloneDeep';
import * as fieldOverrides from './FieldOverrides';
-import { LayoutOptions } from '../../layouts';
import { ReviewStepProps } from '../types';
import { ReviewStep } from './ReviewStep';
-import { extractSchemaFromStep } from '@backstage/plugin-scaffolder-react';
+import {
+ extractSchemaFromStep,
+ type LayoutOptions,
+} from '@backstage/plugin-scaffolder-react';
import { selectedTemplateRouteRef } from '../../routes';
const Form = withTheme(MuiTheme);
diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts
index 5d74b0fc81..c3c90ae86a 100644
--- a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts
+++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts
@@ -16,7 +16,7 @@
import { JsonObject } from '@backstage/types';
import { FormProps, UiSchema } from '@rjsf/core';
-import { LayoutOptions } from '../../layouts';
+import type { LayoutOptions } from '@backstage/plugin-scaffolder-react';
function isObject(value: unknown): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -120,7 +120,8 @@ export function transformSchemaToProps(
)?.component;
if (Layout) {
- uiSchema['ui:ObjectFieldTemplate'] = Layout;
+ uiSchema['ui:ObjectFieldTemplate'] =
+ Layout as unknown as FormProps['ObjectFieldTemplate'];
}
}
diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx
index 6a2da406e0..ccc2bd283b 100644
--- a/plugins/scaffolder/src/components/Router.tsx
+++ b/plugins/scaffolder/src/components/Router.tsx
@@ -24,18 +24,14 @@ import { TaskPage } from './TaskPage';
import { ActionsPage } from './ActionsPage';
import { TemplateEditorPage } from './TemplateEditorPage';
import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../extensions/default';
-import {
- useElementFilter,
- useRouteRef,
- useRouteRefParams,
-} from '@backstage/core-plugin-api';
+import { useRouteRef, useRouteRefParams } from '@backstage/core-plugin-api';
import {
FieldExtensionOptions,
SecretsContextProvider,
useCustomFieldExtensions,
+ useCustomLayouts,
} from '@backstage/plugin-scaffolder-react';
import { ListTasksPage } from './ListTasksPage';
-import { LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts';
import { ReviewStepProps } from './types';
import {
actionsRouteRef,
@@ -104,16 +100,7 @@ export const Router = (props: RouterProps) => {
),
] as FieldExtensionOptions[];
- // todo(blam): this should also be moved to a hook in -react
- const customLayouts = useElementFilter(outlet, elements =>
- elements
- .selectByComponentData({
- key: LAYOUTS_WRAPPER_KEY,
- })
- .findComponentData({
- key: LAYOUTS_KEY,
- }),
- );
+ const customLayouts = useCustomLayouts(outlet);
/**
* This component can be deleted once the older routes have been deprecated.
diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx
index dc67f687bb..e07434aff7 100644
--- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx
+++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx
@@ -15,8 +15,10 @@
*/
import { makeStyles } from '@material-ui/core';
import React, { useState } from 'react';
-import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
-import type { LayoutOptions } from '../../layouts';
+import type {
+ FieldExtensionOptions,
+ LayoutOptions,
+} from '@backstage/plugin-scaffolder-react';
import { TemplateDirectoryAccess } from '../../lib/filesystem';
import { DirectoryEditorProvider } from './DirectoryEditorContext';
import { DryRunProvider } from './DryRunContext';
diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx
index c62112a89c..315028219b 100644
--- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx
+++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx
@@ -19,11 +19,11 @@ import { makeStyles } from '@material-ui/core/styles';
import React, { Component, ReactNode, useMemo, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import yaml from 'yaml';
-import {
+import type {
FieldExtensionOptions,
+ LayoutOptions,
TemplateParameterSchema,
} from '@backstage/plugin-scaffolder-react';
-import { LayoutOptions } from '../../layouts';
import { MultistepJsonForm } from '../MultistepJsonForm';
import { createValidator } from '../TemplatePage';
import { useDirectoryEditor } from './DirectoryEditorContext';
diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx
index 20691a8e85..ebb763e145 100644
--- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx
+++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx
@@ -23,8 +23,10 @@ import { CustomFieldExplorer } from './CustomFieldExplorer';
import { TemplateEditorIntro } from './TemplateEditorIntro';
import { TemplateEditor } from './TemplateEditor';
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
-import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
-import type { LayoutOptions } from '../../layouts';
+import {
+ type FieldExtensionOptions,
+ type LayoutOptions,
+} from '@backstage/plugin-scaffolder-react';
type Selection =
| {
diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx
index 884305bf64..a7e3c6a697 100644
--- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx
+++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx
@@ -32,8 +32,10 @@ import CloseIcon from '@material-ui/icons/Close';
import React, { useCallback, useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
import yaml from 'yaml';
-import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
-import { LayoutOptions } from '../../layouts';
+import {
+ type FieldExtensionOptions,
+ type LayoutOptions,
+} from '@backstage/plugin-scaffolder-react';
import { TemplateEditorForm } from './TemplateEditorForm';
import { TemplateEditorTextArea } from './TemplateEditorTextArea';
diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx
index 0f484ff404..c615649f93 100644
--- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx
+++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx
@@ -20,7 +20,8 @@ import React, { ComponentType, useCallback, useState } from 'react';
import { Navigate, useNavigate } from 'react-router-dom';
import useAsync from 'react-use/lib/useAsync';
import {
- FieldExtensionOptions,
+ type FieldExtensionOptions,
+ type LayoutOptions,
scaffolderApiRef,
useTemplateSecrets,
} from '@backstage/plugin-scaffolder-react';
@@ -37,7 +38,6 @@ import {
useRouteRefParams,
} from '@backstage/core-plugin-api';
import { stringifyEntityRef } from '@backstage/catalog-model';
-import { LayoutOptions } from '../../layouts';
import { ReviewStepProps } from '../types';
import {
rootRouteRef,
diff --git a/plugins/scaffolder/src/deprecated.ts b/plugins/scaffolder/src/deprecated.ts
index 201f7ac7d7..f08a83c594 100644
--- a/plugins/scaffolder/src/deprecated.ts
+++ b/plugins/scaffolder/src/deprecated.ts
@@ -25,6 +25,10 @@ import {
ScaffolderFieldExtensions as ScaffolderFieldExtensionsTemp,
useTemplateSecrets as useTemplateSecretsTemp,
scaffolderApiRef as scaffolderApiRefTemp,
+ createScaffolderLayout as createScaffolderLayoutTemp,
+ ScaffolderLayouts as ScaffolderLayoutsTemp,
+ type LayoutOptions as LayoutOptionsTemp,
+ type LayoutTemplate as LayoutTemplateTemp,
type ScaffolderApi as ScaffolderApiTemp,
type ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecretsTemp,
type TemplateParameterSchema as TemplateParameterSchemaTemp,
@@ -188,3 +192,23 @@ export type ScaffolderTaskOutput = ScaffolderTaskOutputTemp;
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTaskStatus} instead as this has now been moved.
*/
export type ScaffolderTaskStatus = ScaffolderTaskStatusTemp;
+/**
+ * @public
+ * @deprecated use import from {@link @backstage/plugin-scaffolder-react#createScaffolderLayout} instead as this has now been moved.
+ */
+export const createScaffolderLayout = createScaffolderLayoutTemp;
+/**
+ * @public
+ * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderLayouts} instead as this has now been moved.
+ */
+export const ScaffolderLayouts = ScaffolderLayoutsTemp;
+/**
+ * @public
+ * @deprecated use import from {@link @backstage/plugin-scaffolder-react#LayoutTemplate} instead as this has now been moved.
+ */
+export type LayoutTemplate = LayoutTemplateTemp;
+/**
+ * @public
+ * @deprecated use import from {@link @backstage/plugin-scaffolder-react#LayoutOptions} instead as this has now been moved.
+ */
+export type LayoutOptions = LayoutOptionsTemp;
diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts
index 0e2b293982..aa78874f9d 100644
--- a/plugins/scaffolder/src/index.ts
+++ b/plugins/scaffolder/src/index.ts
@@ -21,14 +21,6 @@
*/
export { ScaffolderClient } from './api';
-export {
- createScaffolderLayout,
- ScaffolderLayouts,
- type LayoutOptions,
- type LayoutTemplate,
- type LayoutComponent,
-} from './layouts';
-
export {
EntityPickerFieldExtension,
EntityNamePickerFieldExtension,
diff --git a/plugins/scaffolder/src/next/Router/Router.test.tsx b/plugins/scaffolder/src/next/Router/Router.test.tsx
index 4e5535be6c..9565572a2f 100644
--- a/plugins/scaffolder/src/next/Router/Router.test.tsx
+++ b/plugins/scaffolder/src/next/Router/Router.test.tsx
@@ -23,6 +23,10 @@ import {
ScaffolderFieldExtensions,
} from '@backstage/plugin-scaffolder-react';
import { scaffolderPlugin } from '../../plugin';
+import {
+ createScaffolderLayout,
+ ScaffolderLayouts,
+} from '@backstage/plugin-scaffolder-react';
jest.mock('../TemplateListPage', () => ({
TemplateListPage: jest.fn(() => null),
@@ -106,5 +110,36 @@ describe('Router', () => {
]),
);
});
+
+ it('should extract the layouts and pass them through', async () => {
+ const mockLayoutComponent = () => null;
+ const Layout = scaffolderPlugin.provide(
+ createScaffolderLayout({
+ name: 'layout',
+ component: mockLayoutComponent,
+ }),
+ );
+
+ await renderInTestApp(
+
+
+
+
+ ,
+ { routeEntries: ['/templates/default/foo'] },
+ );
+
+ const mock = TemplateWizardPage as jest.Mock;
+ const [{ layouts }] = mock.mock.calls[0];
+
+ expect(layouts).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ name: 'layout',
+ component: mockLayoutComponent,
+ }),
+ ]),
+ );
+ });
});
});
diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx
index f91e56bba7..8dd47b478e 100644
--- a/plugins/scaffolder/src/next/Router/Router.tsx
+++ b/plugins/scaffolder/src/next/Router/Router.tsx
@@ -21,12 +21,13 @@ import {
NextFieldExtensionOptions,
SecretsContextProvider,
useCustomFieldExtensions,
+ useCustomLayouts,
+ type FormProps,
} from '@backstage/plugin-scaffolder-react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups';
import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default';
-import { type FormProps } from '../types';
import { nextSelectedTemplateRouteRef } from '../routes';
/**
@@ -65,6 +66,8 @@ export const Router = (props: PropsWithChildren) => {
),
] as NextFieldExtensionOptions[];
+ const customLayouts = useCustomLayouts(outlet);
+
return (
) => {
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx
index 24c529b094..d24482a583 100644
--- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx
@@ -25,17 +25,19 @@ import {
import {
scaffolderApiRef,
useTemplateSecrets,
- NextFieldExtensionOptions,
+ Workflow,
+ type LayoutOptions,
} from '@backstage/plugin-scaffolder-react';
+import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
import { JsonValue } from '@backstage/types';
import { type FormProps } from '../types';
import { nextRouteRef } from '../routes';
import { scaffolderTaskRouteRef, selectedTemplateRouteRef } from '../../routes';
import { Header, Page } from '@backstage/core-components';
-import { Workflow } from '@backstage/plugin-scaffolder-react';
-type TemplateWizardPageProps = {
+export type TemplateWizardPageProps = {
customFieldExtensions: NextFieldExtensionOptions[];
+ layouts?: LayoutOptions[];
FormProps?: FormProps;
};
@@ -82,6 +84,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
onError={onError}
extensions={props.customFieldExtensions}
FormProps={props.FormProps}
+ layouts={props.layouts}
/>
diff --git a/plugins/scaffolder/src/next/types.ts b/plugins/scaffolder/src/next/types.ts
index e9c77aa00e..c2ee473d54 100644
--- a/plugins/scaffolder/src/next/types.ts
+++ b/plugins/scaffolder/src/next/types.ts
@@ -23,7 +23,7 @@
import type { FormProps as SchemaFormProps } from '@rjsf/core-v5';
/**
- * Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage`
+ * Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderPage`
*
* @alpha
* @deprecated use the import from {@link @backstage/plugin-scaffolder-react/alpha#FormProps} instead