From b3b7c9c99bdc91c9a254dc0eba7e6aae50278982 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Feb 2025 17:13:59 +0100 Subject: [PATCH] scaffolder: attach form field extensions to page instead Signed-off-by: Patrik Oldsberg --- .changeset/lazy-apricots-whisper.md | 5 ++ .changeset/swift-worms-behave.md | 5 ++ plugins/app-visualizer/report.api.md | 42 ++++++------- plugins/catalog-import/report-alpha.api.md | 30 +++++----- plugins/devtools/report-alpha.api.md | 60 +++++++++---------- plugins/kubernetes/report-alpha.api.md | 30 +++++----- plugins/scaffolder-react/report-alpha.api.md | 4 +- plugins/scaffolder-react/src/next/api/ref.ts | 5 +- .../scaffolder-react/src/next/api/types.ts | 5 +- .../next/blueprints/FormFieldBlueprint.tsx | 5 +- plugins/scaffolder/report-alpha.api.md | 18 +++++- plugins/scaffolder/src/alpha/extensions.tsx | 27 +++++++-- .../src/components/Router/Router.tsx | 42 +++++++++++-- plugins/techdocs/report-alpha.api.md | 60 +++++++++---------- 14 files changed, 208 insertions(+), 130 deletions(-) create mode 100644 .changeset/lazy-apricots-whisper.md create mode 100644 .changeset/swift-worms-behave.md diff --git a/.changeset/lazy-apricots-whisper.md b/.changeset/lazy-apricots-whisper.md new file mode 100644 index 0000000000..3489f5bdef --- /dev/null +++ b/.changeset/lazy-apricots-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Deprecated the alpha `ScaffolderFormFieldsApi` and `formFieldsApiRef` as these are being replaced with a different solution. diff --git a/.changeset/swift-worms-behave.md b/.changeset/swift-worms-behave.md new file mode 100644 index 0000000000..16d25e8737 --- /dev/null +++ b/.changeset/swift-worms-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Updated the alpha `page:scaffolder` extension to accept `formFields` input, matching the updated `FormFieldBlueprint`. diff --git a/plugins/app-visualizer/report.api.md b/plugins/app-visualizer/report.api.md index 48eca04ed8..f0855c9531 100644 --- a/plugins/app-visualizer/report.api.md +++ b/plugins/app-visualizer/report.api.md @@ -16,6 +16,27 @@ const visualizerPlugin: FrontendPlugin< {}, {}, { + 'nav-item:app-visualizer': ExtensionDefinition<{ + kind: 'nav-item'; + name: undefined; + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >; + inputs: {}; + params: { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }; + }>; 'page:app-visualizer': ExtensionDefinition<{ kind: 'page'; name: undefined; @@ -46,27 +67,6 @@ const visualizerPlugin: FrontendPlugin< routeRef?: RouteRef | undefined; }; }>; - 'nav-item:app-visualizer': ExtensionDefinition<{ - kind: 'nav-item'; - name: undefined; - config: {}; - configInput: {}; - output: ConfigurableExtensionDataRef< - { - title: string; - icon: IconComponent; - routeRef: RouteRef; - }, - 'core.nav-item.target', - {} - >; - inputs: {}; - params: { - title: string; - icon: IconComponent; - routeRef: RouteRef; - }; - }>; } >; export default visualizerPlugin; diff --git a/plugins/catalog-import/report-alpha.api.md b/plugins/catalog-import/report-alpha.api.md index c13f7a1a4a..1706404b94 100644 --- a/plugins/catalog-import/report-alpha.api.md +++ b/plugins/catalog-import/report-alpha.api.md @@ -18,6 +18,21 @@ const _default: FrontendPlugin< }, {}, { + 'api:catalog-import': ExtensionDefinition<{ + kind: 'api'; + name: undefined; + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef< + AnyApiFactory, + 'core.api.factory', + {} + >; + inputs: {}; + params: { + factory: AnyApiFactory; + }; + }>; 'page:catalog-import': ExtensionDefinition<{ kind: 'page'; name: undefined; @@ -48,21 +63,6 @@ const _default: FrontendPlugin< routeRef?: RouteRef | undefined; }; }>; - 'api:catalog-import': ExtensionDefinition<{ - kind: 'api'; - name: undefined; - config: {}; - configInput: {}; - output: ConfigurableExtensionDataRef< - AnyApiFactory, - 'core.api.factory', - {} - >; - inputs: {}; - params: { - factory: AnyApiFactory; - }; - }>; } >; export default _default; diff --git a/plugins/devtools/report-alpha.api.md b/plugins/devtools/report-alpha.api.md index e3261e0f05..abef9dc11f 100644 --- a/plugins/devtools/report-alpha.api.md +++ b/plugins/devtools/report-alpha.api.md @@ -19,36 +19,6 @@ const _default: FrontendPlugin< }, {}, { - 'page:devtools': ExtensionDefinition<{ - kind: 'page'; - name: undefined; - config: { - path: string | undefined; - }; - configInput: { - path?: string | undefined; - }; - output: - | ConfigurableExtensionDataRef< - React_2.JSX.Element, - 'core.reactElement', - {} - > - | ConfigurableExtensionDataRef - | ConfigurableExtensionDataRef< - RouteRef, - 'core.routing.ref', - { - optional: true; - } - >; - inputs: {}; - params: { - defaultPath: string; - loader: () => Promise; - routeRef?: RouteRef | undefined; - }; - }>; 'nav-item:devtools': ExtensionDefinition<{ kind: 'nav-item'; name: undefined; @@ -85,6 +55,36 @@ const _default: FrontendPlugin< factory: AnyApiFactory; }; }>; + 'page:devtools': ExtensionDefinition<{ + kind: 'page'; + name: undefined; + config: { + path: string | undefined; + }; + configInput: { + path?: string | undefined; + }; + output: + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >; + inputs: {}; + params: { + defaultPath: string; + loader: () => Promise; + routeRef?: RouteRef | undefined; + }; + }>; } >; export default _default; diff --git a/plugins/kubernetes/report-alpha.api.md b/plugins/kubernetes/report-alpha.api.md index 19f0554291..c2b954022e 100644 --- a/plugins/kubernetes/report-alpha.api.md +++ b/plugins/kubernetes/report-alpha.api.md @@ -22,6 +22,21 @@ const _default: FrontendPlugin< }, {}, { + 'api:kubernetes': ExtensionDefinition<{ + kind: 'api'; + name: undefined; + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef< + AnyApiFactory, + 'core.api.factory', + {} + >; + inputs: {}; + params: { + factory: AnyApiFactory; + }; + }>; 'page:kubernetes': ExtensionDefinition<{ kind: 'page'; name: undefined; @@ -48,21 +63,6 @@ const _default: FrontendPlugin< routeRef?: RouteRef | undefined; }; }>; - 'api:kubernetes': ExtensionDefinition<{ - kind: 'api'; - name: undefined; - config: {}; - configInput: {}; - output: ConfigurableExtensionDataRef< - AnyApiFactory, - 'core.api.factory', - {} - >; - inputs: {}; - params: { - factory: AnyApiFactory; - }; - }>; 'entity-content:kubernetes/kubernetes': ExtensionDefinition<{ kind: 'entity-content'; name: 'kubernetes'; diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 4cddb48602..9af439f15a 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -224,7 +224,7 @@ export const formFieldsApi: ExtensionDefinition<{ }; }>; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const formFieldsApiRef: ApiRef; // @alpha (undocumented) @@ -301,7 +301,7 @@ export type ScaffolderFormDecoratorContext< ) => void; }; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export interface ScaffolderFormFieldsApi { // (undocumented) getFormFields(): Promise; diff --git a/plugins/scaffolder-react/src/next/api/ref.ts b/plugins/scaffolder-react/src/next/api/ref.ts index eaa6f49c21..6832b91a3d 100644 --- a/plugins/scaffolder-react/src/next/api/ref.ts +++ b/plugins/scaffolder-react/src/next/api/ref.ts @@ -17,7 +17,10 @@ import { createApiRef } from '@backstage/frontend-plugin-api'; import { ScaffolderFormFieldsApi } from './types'; -/** @alpha */ +/** + * @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 2c98111b1c..ea93ba86b2 100644 --- a/plugins/scaffolder-react/src/next/api/types.ts +++ b/plugins/scaffolder-react/src/next/api/types.ts @@ -16,7 +16,10 @@ import { FormFieldExtensionData } from '../blueprints'; -/** @alpha */ +/** + * @alpha + * @deprecated This API is no longer necessary and will be removed + */ export interface ScaffolderFormFieldsApi { getFormFields(): Promise; } diff --git a/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx b/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx index e99adcba82..4324a080f6 100644 --- a/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx +++ b/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx @@ -34,7 +34,10 @@ const formFieldExtensionDataRef = createExtensionDataRef< * */ export const FormFieldBlueprint = createExtensionBlueprint({ kind: 'scaffolder-form-field', - attachTo: { id: 'api:scaffolder/form-fields', input: 'formFields' }, + attachTo: [ + { id: 'page:scaffolder', input: 'formFields' }, + { 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 7623350d96..74a6ae276a 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -108,8 +108,6 @@ const _default: FrontendPlugin< }; }>; 'page:scaffolder': ExtensionDefinition<{ - kind: 'page'; - name: undefined; config: { path: string | undefined; }; @@ -126,7 +124,21 @@ const _default: FrontendPlugin< optional: true; } >; - inputs: {}; + inputs: { + formFields: ExtensionInput< + ConfigurableExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >, + { + singleton: false; + optional: false; + } + >; + }; + kind: 'page'; + name: undefined; params: { defaultPath: string; loader: () => Promise; diff --git a/plugins/scaffolder/src/alpha/extensions.tsx b/plugins/scaffolder/src/alpha/extensions.tsx index 493b0d7114..db1a12e464 100644 --- a/plugins/scaffolder/src/alpha/extensions.tsx +++ b/plugins/scaffolder/src/alpha/extensions.tsx @@ -26,6 +26,7 @@ import { discoveryApiRef, fetchApiRef, identityApiRef, + createExtensionInput, } from '@backstage/frontend-plugin-api'; import React from 'react'; import { rootRouteRef } from '../routes'; @@ -35,12 +36,26 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import { ScaffolderClient } from '../api'; -export const scaffolderPage = PageBlueprint.make({ - params: { - routeRef: convertLegacyRouteRef(rootRouteRef), - defaultPath: '/create', - loader: () => - import('../components/Router').then(m => compatWrapper()), +export const scaffolderPage = PageBlueprint.makeWithOverrides({ + inputs: { + formFields: createExtensionInput([ + FormFieldBlueprint.dataRefs.formFieldLoader, + ]), + }, + factory(originalFactory, { inputs }) { + const formFieldLoaders = inputs.formFields.map(i => + i.get(FormFieldBlueprint.dataRefs.formFieldLoader), + ); + return originalFactory({ + routeRef: convertLegacyRouteRef(rootRouteRef), + defaultPath: '/create', + loader: () => + import('../components/Router/Router').then(m => + compatWrapper( + , + ), + ), + }); }, }); diff --git a/plugins/scaffolder/src/components/Router/Router.tsx b/plugins/scaffolder/src/components/Router/Router.tsx index 0853ea9438..2e081d835a 100644 --- a/plugins/scaffolder/src/components/Router/Router.tsx +++ b/plugins/scaffolder/src/components/Router/Router.tsx @@ -64,6 +64,8 @@ import { templateManagementPermission, } from '@backstage/plugin-scaffolder-common/alpha'; import { useApp } from '@backstage/core-plugin-api'; +import { FormField, OpaqueFormField } from '@internal/scaffolder'; +import { useAsync, useMountEffect } from '@react-hookz/web'; /** * The Props for the Scaffolder Router @@ -105,11 +107,16 @@ export type RouterProps = { }; /** - * The Scaffolder Router + * Internal router with additional props that aren't available in the public API + * for the old frontend system. * - * @public + * @internal */ -export const Router = (props: PropsWithChildren) => { +export const InternalRouter = ( + props: PropsWithChildren< + RouterProps & { formFieldLoaders?: Array<() => Promise> } + >, +) => { const { components: { TemplateCardComponent, @@ -123,13 +130,15 @@ export const Router = (props: PropsWithChildren) => { } = {}, } = props; const outlet = useOutlet() || props.children; - const customFieldExtensions = - useCustomFieldExtensions(outlet); + const customFieldExtensions = useCustomFieldExtensions(outlet); + const loadedFieldExtensions = useFormFieldLoaders(props.formFieldLoaders); + const app = useApp(); const { NotFoundErrorPage } = app.getComponents(); const fieldExtensions = [ ...customFieldExtensions, + ...loadedFieldExtensions, ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( ({ name }) => !customFieldExtensions.some( @@ -243,3 +252,26 @@ export const Router = (props: PropsWithChildren) => { ); }; + +/** + * The Scaffolder Router + * + * @public + */ +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/techdocs/report-alpha.api.md b/plugins/techdocs/report-alpha.api.md index d39c9d31ed..2af641d12c 100644 --- a/plugins/techdocs/report-alpha.api.md +++ b/plugins/techdocs/report-alpha.api.md @@ -31,36 +31,6 @@ const _default: FrontendPlugin< }, {}, { - 'page:techdocs': ExtensionDefinition<{ - kind: 'page'; - name: undefined; - config: { - path: string | undefined; - }; - configInput: { - path?: string | undefined; - }; - output: - | ConfigurableExtensionDataRef< - React_2.JSX.Element, - 'core.reactElement', - {} - > - | ConfigurableExtensionDataRef - | ConfigurableExtensionDataRef< - RouteRef, - 'core.routing.ref', - { - optional: true; - } - >; - inputs: {}; - params: { - defaultPath: string; - loader: () => Promise; - routeRef?: RouteRef | undefined; - }; - }>; 'nav-item:techdocs': ExtensionDefinition<{ kind: 'nav-item'; name: undefined; @@ -112,6 +82,36 @@ const _default: FrontendPlugin< factory: AnyApiFactory; }; }>; + 'page:techdocs': ExtensionDefinition<{ + kind: 'page'; + name: undefined; + config: { + path: string | undefined; + }; + configInput: { + path?: string | undefined; + }; + output: + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >; + inputs: {}; + params: { + defaultPath: string; + loader: () => Promise; + routeRef?: RouteRef | undefined; + }; + }>; 'search-result-list-item:techdocs': ExtensionDefinition<{ config: { title: string | undefined;