From 2eeca031c4e2576420a7d5564ae7c6412803766a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 26 Jan 2026 11:10:06 +0100 Subject: [PATCH] scaffolder: migrate nfs form fields to utility API Signed-off-by: Patrik Oldsberg --- ...ffolder-form-fields-api-migration-react.md | 11 +++ .../scaffolder-form-fields-api-migration.md | 10 +++ plugins/scaffolder-react/report-alpha.api.md | 55 ++------------ .../hooks/useCustomFieldExtensions.test.tsx | 75 +------------------ .../src/hooks/useCustomFieldExtensions.ts | 26 +------ .../scaffolder-react/src/next/api/index.ts | 4 +- plugins/scaffolder-react/src/next/api/ref.ts | 26 ------- .../scaffolder-react/src/next/api/types.ts | 10 --- .../next/blueprints/FormFieldBlueprint.tsx | 5 +- plugins/scaffolder/report-alpha.api.md | 6 -- plugins/scaffolder/src/alpha/extensions.tsx | 29 +++++-- .../src/alpha/formFieldsApi.ts} | 48 ++++++------ plugins/scaffolder/src/alpha/index.ts | 5 -- plugins/scaffolder/src/alpha/plugin.tsx | 2 +- .../src/components/Router/Router.test.tsx | 34 +++------ .../src/components/Router/Router.tsx | 22 +----- plugins/scaffolder/src/plugin.tsx | 6 -- 17 files changed, 95 insertions(+), 279 deletions(-) create mode 100644 .changeset/scaffolder-form-fields-api-migration-react.md create mode 100644 .changeset/scaffolder-form-fields-api-migration.md delete mode 100644 plugins/scaffolder-react/src/next/api/ref.ts rename plugins/{scaffolder-react/src/next/api/FormFieldsApi.ts => scaffolder/src/alpha/formFieldsApi.ts} (62%) diff --git a/.changeset/scaffolder-form-fields-api-migration-react.md b/.changeset/scaffolder-form-fields-api-migration-react.md new file mode 100644 index 0000000000..4d640c58fc --- /dev/null +++ b/.changeset/scaffolder-form-fields-api-migration-react.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Scaffolder form fields in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `FormFieldBlueprint` now uses this new approach, and while form fields created with older versions still work, they will produce a deprecation warning and will stop working in a future release. + +As part of this change, the following alpha exports were removed: + +- `formFieldsApi` +- `formFieldsApiRef` +- `ScaffolderFormFieldsApi` diff --git a/.changeset/scaffolder-form-fields-api-migration.md b/.changeset/scaffolder-form-fields-api-migration.md new file mode 100644 index 0000000000..cfbed3b749 --- /dev/null +++ b/.changeset/scaffolder-form-fields-api-migration.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Scaffolder form fields in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `FormFieldBlueprint` now uses this new approach, and while form fields created with older versions still work, they will produce a deprecation warning and will stop working in a future release. + +As part of this change, the following alpha exports were removed: + +- `formFieldsApiRef` +- `ScaffolderFormFieldsApi` diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 1befe9ca46..5074e7b1e9 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -3,19 +3,14 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyApiRef } from '@backstage/core-plugin-api'; -import { ApiFactory } from '@backstage/frontend-plugin-api'; import { ApiHolder } from '@backstage/core-plugin-api'; -import { ApiRef } from '@backstage/frontend-plugin-api'; import { ComponentType } from 'react'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; import { Dispatch } from 'react'; import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; -import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import { FieldSchema } from '@backstage/plugin-scaffolder-react'; @@ -26,7 +21,6 @@ import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; -import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { Overrides } from '@material-ui/core/styles/overrides'; import { PropsWithChildren } from 'react'; import { ReactElement } from 'react'; @@ -204,39 +198,6 @@ export type FormFieldExtensionData< schema?: FieldSchema, z.output>; }; -// @alpha (undocumented) -export const formFieldsApi: OverridableExtensionDefinition<{ - config: {}; - configInput: {}; - output: ExtensionDataRef; - inputs: { - formFields: ExtensionInput< - ConfigurableExtensionDataRef< - () => Promise, - 'scaffolder.form-field-loader', - {} - >, - { - singleton: false; - optional: false; - internal: false; - } - >; - }; - kind: 'api'; - name: 'form-fields'; - params: < - TApi, - TImpl extends TApi, - TDeps extends { [name in string]: unknown }, - >( - params: ApiFactory, - ) => ExtensionBlueprintParams; -}>; - -// @alpha @deprecated (undocumented) -export const formFieldsApiRef: ApiRef; - // @alpha (undocumented) export type FormValidation = { [name: string]: FieldValidation | FormValidation; @@ -311,12 +272,6 @@ export type ScaffolderFormDecoratorContext< ) => void; }; -// @alpha @deprecated (undocumented) -export interface ScaffolderFormFieldsApi { - // (undocumented) - getFormFields(): Promise; -} - // @alpha (undocumented) export function ScaffolderPageContextMenu( props: ScaffolderPageContextMenuProps, @@ -345,6 +300,11 @@ export const scaffolderReactTranslationRef: TranslationRef< 'scaffolder-react', { readonly 'workflow.noDescription': 'No description'; + readonly 'stepper.backButtonText': 'Back'; + readonly 'stepper.createButtonText': 'Create'; + readonly 'stepper.reviewButtonText': 'Review'; + readonly 'stepper.nextButtonText': 'Next'; + readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; readonly 'passwordWidget.content': 'This widget is insecure. Please use [`ui:field: Secret`](https://backstage.io/docs/features/software-templates/writing-templates/#using-secrets) instead of `ui:widget: password`'; readonly 'scaffolderPageContextMenu.createLabel': 'Create'; readonly 'scaffolderPageContextMenu.moreLabel': 'more'; @@ -352,11 +312,6 @@ export const scaffolderReactTranslationRef: TranslationRef< readonly 'scaffolderPageContextMenu.actionsLabel': 'Installed Actions'; readonly 'scaffolderPageContextMenu.tasksLabel': 'Task List'; readonly 'scaffolderPageContextMenu.templatingExtensionsLabel': 'Templating Extensions'; - readonly 'stepper.backButtonText': 'Back'; - readonly 'stepper.createButtonText': 'Create'; - readonly 'stepper.reviewButtonText': 'Review'; - readonly 'stepper.nextButtonText': 'Next'; - readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; readonly 'templateCategoryPicker.title': 'Categories'; readonly 'templateCard.noDescription': 'No description'; readonly 'templateCard.chooseButtonText': 'Choose'; diff --git a/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.test.tsx b/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.test.tsx index 3be482d684..8b8251d4eb 100644 --- a/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.test.tsx +++ b/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.test.tsx @@ -16,9 +16,8 @@ import { PropsWithChildren } from 'react'; import { createPlugin } from '@backstage/core-plugin-api'; -import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; -import { renderHook, waitFor } from '@testing-library/react'; -import { ScaffolderFormFieldsApi, formFieldsApiRef } from '../alpha'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { renderHook } from '@testing-library/react'; import { useCustomFieldExtensions } from './useCustomFieldExtensions'; import { ScaffolderFieldExtensions, @@ -33,22 +32,10 @@ const plugin = createPlugin({ }); describe('useCustomFieldExtensions', () => { - const mockFormFieldsApi: jest.Mocked = { - getFormFields: jest.fn(), - }; const wrapper = ({ children }: PropsWithChildren<{}>) => - wrapInTestApp( - - {children} - , - ); - - beforeEach(() => { - jest.resetAllMocks(); - }); + wrapInTestApp(<>{children}); it('should return field extensions from the React tree', async () => { - mockFormFieldsApi.getFormFields.mockResolvedValue([]); const CustomFieldExtension = plugin.provide( createScaffolderFieldExtension({ name: 'test', @@ -70,60 +57,4 @@ describe('useCustomFieldExtensions', () => { expect(result.current).toEqual([expect.objectContaining({ name: 'test' })]); }); - - it('should return field extensions from formFieldsApi', async () => { - mockFormFieldsApi.getFormFields.mockResolvedValue([ - { - name: 'blueprint', - component: () =>
Test
, - }, - ]); - - const { result } = renderHook(() => useCustomFieldExtensions(
), { - wrapper, - }); - - await waitFor(() => { - expect(result.current.length).toBeGreaterThan(0); - }); - - expect(result.current).toEqual([ - expect.objectContaining({ name: 'blueprint' }), - ]); - }); - - it('should return field extensions from both sources', async () => { - mockFormFieldsApi.getFormFields.mockResolvedValue([ - { - name: 'blueprint', - component: () =>
Test
, - }, - ]); - - const CustomFieldExtension = plugin.provide( - createScaffolderFieldExtension({ - name: 'test', - component: () =>
Test
, - }), - ); - - const { result } = renderHook( - () => - useCustomFieldExtensions( - - - , - ), - { - wrapper, - }, - ); - - await waitFor(() => { - expect(result.current).toHaveLength(2); - }); - - const fieldNames = result.current.map(field => field.name); - expect(fieldNames).toEqual(expect.arrayContaining(['test', 'blueprint'])); - }); }); diff --git a/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts b/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts index be24e4186d..47a91e5d63 100644 --- a/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts +++ b/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts @@ -13,9 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useAsync, useMountEffect } from '@react-hookz/web'; -import { useApi, useElementFilter } from '@backstage/core-plugin-api'; -import { formFieldsApiRef } from '../next'; +import { useElementFilter } from '@backstage/core-plugin-api'; import { FieldExtensionOptions } from '../extensions'; import { FIELD_EXTENSION_KEY, @@ -32,14 +30,6 @@ export const useCustomFieldExtensions = < >( outlet: React.ReactNode, ) => { - // Get custom fields created with FormFieldBlueprint - const formFieldsApi = useApi(formFieldsApiRef); - const [{ result: blueprintFields }, { execute }] = useAsync( - () => formFieldsApi.getFormFields(), - [], - ); - useMountEffect(execute); - // Get custom fields created with ScaffolderFieldExtensions const outletFields = useElementFilter(outlet, elements => elements @@ -51,17 +41,5 @@ export const useCustomFieldExtensions = < }), ); - // This should really be a different type moving forward, but we do this to keep type compatibility. - // should probably also move the defaults into the API eventually too, but that will come with the move - // to the new frontend system. - const blueprintsToLegacy: FieldExtensionOptions[] = blueprintFields?.map( - field => ({ - component: field.component, - name: field.name, - validation: field.validation, - schema: field.schema?.schema, - }), - ); - - return [...blueprintsToLegacy, ...outletFields] as TComponentDataType[]; + return outletFields as TComponentDataType[]; }; diff --git a/plugins/scaffolder-react/src/next/api/index.ts b/plugins/scaffolder-react/src/next/api/index.ts index 52c40ad99a..9988f4dd82 100644 --- a/plugins/scaffolder-react/src/next/api/index.ts +++ b/plugins/scaffolder-react/src/next/api/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -export { formFieldsApi } from './FormFieldsApi'; -export { formFieldsApiRef } from './ref'; -export type { ScaffolderFormFieldsApi, FormField } from './types'; +export { type FormField } from './types'; diff --git a/plugins/scaffolder-react/src/next/api/ref.ts b/plugins/scaffolder-react/src/next/api/ref.ts deleted file mode 100644 index 6832b91a3d..0000000000 --- a/plugins/scaffolder-react/src/next/api/ref.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2024 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 { createApiRef } from '@backstage/frontend-plugin-api'; -import { ScaffolderFormFieldsApi } from './types'; - -/** - * @alpha - * @deprecated This API is no longer necessary and will be removed - */ -export const formFieldsApiRef = createApiRef({ - id: 'plugin.scaffolder.form-fields', -}); diff --git a/plugins/scaffolder-react/src/next/api/types.ts b/plugins/scaffolder-react/src/next/api/types.ts index 9e028567f6..38929ee189 100644 --- a/plugins/scaffolder-react/src/next/api/types.ts +++ b/plugins/scaffolder-react/src/next/api/types.ts @@ -14,16 +14,6 @@ * limitations under the License. */ -import { FormFieldExtensionData } from '../blueprints'; - -/** - * @alpha - * @deprecated This API is no longer necessary and will be removed - */ -export interface ScaffolderFormFieldsApi { - getFormFields(): Promise; -} - /** @alpha */ export interface FormField { readonly $$type: '@backstage/scaffolder/FormField'; diff --git a/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx b/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx index afbf2b501d..8b5a88f7df 100644 --- a/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx +++ b/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx @@ -35,10 +35,7 @@ const formFieldExtensionDataRef = createExtensionDataRef< * */ export const FormFieldBlueprint = createExtensionBlueprint({ kind: 'scaffolder-form-field', - attachTo: [ - { id: 'page:scaffolder', input: 'formFields' }, - { id: 'api:scaffolder/form-fields', input: 'formFields' }, - ], + attachTo: { id: 'api:scaffolder/form-fields', input: 'formFields' }, dataRefs: { formFieldLoader: formFieldExtensionDataRef, }, diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index b9aa82782f..b90bc4c693 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -17,7 +17,6 @@ import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import { FormField } from '@backstage/plugin-scaffolder-react/alpha'; -import { formFieldsApiRef } from '@backstage/plugin-scaffolder-react/alpha'; import type { FormProps as FormProps_2 } from '@rjsf/core'; import { FormProps as FormProps_3 } from '@backstage/plugin-scaffolder-react'; import { IconComponent } from '@backstage/frontend-plugin-api'; @@ -31,7 +30,6 @@ import { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha'; -import { ScaffolderFormFieldsApi } from '@backstage/plugin-scaffolder-react/alpha'; import { SubRouteRef } from '@backstage/core-plugin-api'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react'; @@ -432,8 +430,6 @@ export const formDecoratorsApi: OverridableExtensionDefinition<{ // @alpha (undocumented) export const formDecoratorsApiRef: ApiRef; -export { formFieldsApiRef }; - // @alpha @deprecated export type FormProps = Pick< FormProps_2, @@ -453,8 +449,6 @@ export interface ScaffolderFormDecoratorsApi { getFormDecorators(): Promise; } -export { ScaffolderFormFieldsApi }; - // @public (undocumented) export type ScaffolderTemplateEditorClassKey = | 'root' diff --git a/plugins/scaffolder/src/alpha/extensions.tsx b/plugins/scaffolder/src/alpha/extensions.tsx index 5215fc0968..8c17f8cc1c 100644 --- a/plugins/scaffolder/src/alpha/extensions.tsx +++ b/plugins/scaffolder/src/alpha/extensions.tsx @@ -29,6 +29,7 @@ import { FormFieldBlueprint } from '@backstage/plugin-scaffolder-react/alpha'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import { ScaffolderClient } from '../api'; +import { formFieldsApiRef } from './formFieldsApi'; export const scaffolderPage = PageBlueprint.makeWithOverrides({ inputs: { @@ -36,17 +37,29 @@ export const scaffolderPage = PageBlueprint.makeWithOverrides({ FormFieldBlueprint.dataRefs.formFieldLoader, ]), }, - factory(originalFactory, { inputs }) { - const formFieldLoaders = inputs.formFields.map(i => - i.get(FormFieldBlueprint.dataRefs.formFieldLoader), - ); + factory(originalFactory, { apis, inputs }) { + const formFieldsApi = apis.get(formFieldsApiRef); + return originalFactory({ routeRef: rootRouteRef, path: '/create', - loader: () => - import('../components/Router/Router').then(m => ( - - )), + loader: async () => { + // Merge form fields from the API with old-style direct attachments + const apiFormFields = (await formFieldsApi?.loadFormFields()) ?? []; + const formFieldLoaders = inputs.formFields.map(output => + output.get(FormFieldBlueprint.dataRefs.formFieldLoader), + ); + + // Resolve direct attachments and combine with API form fields + const loadedFormFields = await Promise.all( + formFieldLoaders.map(loader => loader()), + ); + const formFields = [...apiFormFields, ...loadedFormFields]; + + return import('../components/Router/Router').then(m => ( + + )); + }, }); }, }); diff --git a/plugins/scaffolder-react/src/next/api/FormFieldsApi.ts b/plugins/scaffolder/src/alpha/formFieldsApi.ts similarity index 62% rename from plugins/scaffolder-react/src/next/api/FormFieldsApi.ts rename to plugins/scaffolder/src/alpha/formFieldsApi.ts index 08ccc662d4..df88645eb4 100644 --- a/plugins/scaffolder-react/src/next/api/FormFieldsApi.ts +++ b/plugins/scaffolder/src/alpha/formFieldsApi.ts @@ -16,32 +16,24 @@ import { ApiBlueprint, + createApiRef, createExtensionInput, } from '@backstage/frontend-plugin-api'; -import { formFieldsApiRef } from './ref'; -import { FormField, ScaffolderFormFieldsApi } from './types'; -import { FormFieldBlueprint } from '../blueprints'; +import { FormFieldBlueprint } from '@backstage/plugin-scaffolder-react/alpha'; import { OpaqueFormField } from '@internal/scaffolder'; -class DefaultScaffolderFormFieldsApi implements ScaffolderFormFieldsApi { - private readonly formFieldLoaders: Array<() => Promise>; - - constructor(formFieldLoaders: Array<() => Promise> = []) { - this.formFieldLoaders = formFieldLoaders; - } - - async getFormFields() { - const formFields = await Promise.all( - this.formFieldLoaders.map(loader => loader()), - ); - - const internalFormFields = formFields.map(OpaqueFormField.toInternal); - - return internalFormFields; - } +interface FormField { + readonly $$type: '@backstage/scaffolder/FormField'; } -/** @alpha */ +interface ScaffolderFormFieldsApi { + loadFormFields(): Promise; +} + +const formFieldsApiRef = createApiRef({ + id: 'plugin.scaffolder.form-fields-loader', +}); + export const formFieldsApi = ApiBlueprint.makeWithOverrides({ name: 'form-fields', inputs: { @@ -58,8 +50,22 @@ export const formFieldsApi = ApiBlueprint.makeWithOverrides({ defineParams({ api: formFieldsApiRef, deps: {}, - factory: () => new DefaultScaffolderFormFieldsApi(formFieldLoaders), + factory: () => ({ + async loadFormFields() { + const formFields = await Promise.all( + formFieldLoaders.map(loader => loader()), + ); + + const internalFormFields = formFields.map( + OpaqueFormField.toInternal, + ); + + return internalFormFields; + }, + }), }), ); }, }); + +export { formFieldsApiRef }; diff --git a/plugins/scaffolder/src/alpha/index.ts b/plugins/scaffolder/src/alpha/index.ts index d239f44b4f..53e2b8f0bf 100644 --- a/plugins/scaffolder/src/alpha/index.ts +++ b/plugins/scaffolder/src/alpha/index.ts @@ -25,9 +25,4 @@ export { export { scaffolderTranslationRef } from '../translation'; export * from './api'; -export { - formFieldsApiRef, - type ScaffolderFormFieldsApi, -} from '@backstage/plugin-scaffolder-react/alpha'; - export { default } from './plugin'; diff --git a/plugins/scaffolder/src/alpha/plugin.tsx b/plugins/scaffolder/src/alpha/plugin.tsx index 502aab0bbc..9b7698250a 100644 --- a/plugins/scaffolder/src/alpha/plugin.tsx +++ b/plugins/scaffolder/src/alpha/plugin.tsx @@ -42,7 +42,7 @@ import { scaffolderPage, } from './extensions'; import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { formFieldsApi } from '@backstage/plugin-scaffolder-react/alpha'; +import { formFieldsApi } from './formFieldsApi'; import { formDecoratorsApi } from './api'; import { EntityIconLinkBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { useScaffolderTemplateIconLinkProps } from './hooks/useScaffolderTemplateIconLinkProps'; diff --git a/plugins/scaffolder/src/components/Router/Router.test.tsx b/plugins/scaffolder/src/components/Router/Router.test.tsx index 20b21fabc7..04f6cac690 100644 --- a/plugins/scaffolder/src/components/Router/Router.test.tsx +++ b/plugins/scaffolder/src/components/Router/Router.test.tsx @@ -13,13 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ReactElement } from 'react'; import { Router } from './Router'; -import { - renderInTestApp, - TestApiProvider, - TestAppOptions, -} from '@backstage/test-utils'; +import { renderInTestApp } from '@backstage/test-utils'; import { createScaffolderFieldExtension, ScaffolderFieldExtensions, @@ -30,23 +25,12 @@ import { ScaffolderLayouts, } from '@backstage/plugin-scaffolder-react'; import { TemplateListPage, TemplateWizardPage } from '../../alpha/components'; -import { formFieldsApiRef } from '@backstage/plugin-scaffolder-react/alpha'; jest.mock('../../alpha/components', () => ({ TemplateWizardPage: jest.fn(() => null), TemplateListPage: jest.fn(() => null), })); -const wrapInApisAndRender = (element: ReactElement, opts?: TestAppOptions) => - renderInTestApp( - [] }]]} - > - {element} - , - opts, - ); - describe('Router', () => { beforeEach(() => { (TemplateWizardPage as jest.Mock).mockClear(); @@ -54,13 +38,13 @@ describe('Router', () => { }); describe('/', () => { it('should render the TemplateListPage', async () => { - await wrapInApisAndRender(); + await renderInTestApp(); expect(TemplateListPage).toHaveBeenCalled(); }); it('should render user-provided TemplateListPage', async () => { - const { getByText } = await wrapInApisAndRender( + const { getByText } = await renderInTestApp( <>foobar, @@ -76,7 +60,7 @@ describe('Router', () => { it('should render not found error page', async () => { await expect( - wrapInApisAndRender(, { + renderInTestApp(, { routeEntries: ['/foonotfounderror'], }), ).rejects.toThrow('Reached NotFound Page'); @@ -85,7 +69,7 @@ describe('Router', () => { describe('/templates/:templateName', () => { it('should render the TemplateWizard page', async () => { - await wrapInApisAndRender(, { + await renderInTestApp(, { routeEntries: ['/templates/default/foo'], }); @@ -93,7 +77,7 @@ describe('Router', () => { }); it('should render user-provided TemplateWizardPage', async () => { - const { getByText } = await wrapInApisAndRender( + const { getByText } = await renderInTestApp( <>foobar, @@ -110,7 +94,7 @@ describe('Router', () => { it('should pass through the FormProps property', async () => { const transformErrorsMock = jest.fn(); - await wrapInApisAndRender( + await renderInTestApp( { }), ); - await wrapInApisAndRender( + await renderInTestApp( @@ -170,7 +154,7 @@ describe('Router', () => { }), ); - await wrapInApisAndRender( + await renderInTestApp( diff --git a/plugins/scaffolder/src/components/Router/Router.tsx b/plugins/scaffolder/src/components/Router/Router.tsx index 7a2496c046..0fa43901bf 100644 --- a/plugins/scaffolder/src/components/Router/Router.tsx +++ b/plugins/scaffolder/src/components/Router/Router.tsx @@ -63,7 +63,6 @@ import { RequirePermission } from '@backstage/plugin-permission-react'; import { templateManagementPermission } from '@backstage/plugin-scaffolder-common/alpha'; import { useApp } from '@backstage/core-plugin-api'; import { OpaqueFormField } from '@internal/scaffolder'; -import { useAsync, useMountEffect } from '@react-hookz/web'; import { TemplatingExtensionsPage } from '../TemplatingExtensionsPage'; import { FormField } from '@backstage/plugin-scaffolder-react/alpha'; @@ -116,7 +115,9 @@ export type RouterProps = { */ export const InternalRouter = ( props: PropsWithChildren< - RouterProps & { formFieldLoaders?: Array<() => Promise> } + RouterProps & { + formFields?: Array; + } >, ) => { const { @@ -133,14 +134,13 @@ export const InternalRouter = ( } = props; const outlet = useOutlet() || props.children; const customFieldExtensions = useCustomFieldExtensions(outlet); - const loadedFieldExtensions = useFormFieldLoaders(props.formFieldLoaders); const app = useApp(); const { NotFoundErrorPage } = app.getComponents(); const fieldExtensions = [ ...customFieldExtensions, - ...loadedFieldExtensions, + ...(props.formFields?.map(OpaqueFormField.toInternal) ?? []), ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( ({ name }) => !customFieldExtensions.some( @@ -261,17 +261,3 @@ export const InternalRouter = ( export const Router = (props: PropsWithChildren) => { return ; }; - -function useFormFieldLoaders( - formFieldLoaders?: Array<() => Promise>, -) { - const [{ result: loadedFieldExtensions }, { execute }] = - useAsync(async () => { - const loaded = await Promise.all( - (formFieldLoaders ?? []).map(loader => loader()), - ); - return loaded.map(f => OpaqueFormField.toInternal(f)); - }, []); - useMountEffect(execute); - return loadedFieldExtensions; -} diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index dabcb683c7..50a5d29280 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -81,7 +81,6 @@ import { RepoBranchPicker } from './components/fields/RepoBranchPicker/RepoBranc import { RepoBranchPickerSchema } from './components/fields/RepoBranchPicker/schema'; import { formDecoratorsApiRef } from './alpha/api/ref'; import { DefaultScaffolderFormDecoratorsApi } from './alpha/api/FormDecoratorsApi'; -import { formFieldsApiRef } from '@backstage/plugin-scaffolder-react/alpha'; import { RepoOwnerPicker, RepoOwnerPickerSchema, @@ -115,11 +114,6 @@ export const scaffolderPlugin = createPlugin({ deps: {}, factory: () => DefaultScaffolderFormDecoratorsApi.create(), }), - createApiFactory({ - api: formFieldsApiRef, - deps: {}, - factory: () => ({ getFormFields: async () => [] }), - }), ], routes: { root: rootRouteRef,