diff --git a/.changeset/friendly-scissors-shop.md b/.changeset/friendly-scissors-shop.md new file mode 100644 index 0000000000..03c67ba491 --- /dev/null +++ b/.changeset/friendly-scissors-shop.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +- **Deprecation** - Deprecated the following exports, please import them directly from `@backstage/plugin-scaffolder-react` instead + + ``` + createScaffolderLayout + ScaffolderLayouts + LayoutOptions + LayoutTemplate + ``` diff --git a/.changeset/silly-turkeys-hang.md b/.changeset/silly-turkeys-hang.md new file mode 100644 index 0000000000..9f712a43c8 --- /dev/null +++ b/.changeset/silly-turkeys-hang.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-react': patch +--- + +Add `ScaffolderLayouts` to `NextScaffolderPage` diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c5410c4f80..8984a69259 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -244,6 +244,9 @@ const routes = ( + + + } /> , ): Extension>; +// @public +export function createScaffolderLayout( + options: LayoutOptions, +): Extension>; + // @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