scaffolder: attach form field extensions to page instead

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-06 17:13:59 +01:00
parent 8dee48768c
commit b3b7c9c99b
14 changed files with 208 additions and 130 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-react': patch
---
Deprecated the alpha `ScaffolderFormFieldsApi` and `formFieldsApiRef` as these are being replaced with a different solution.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Updated the alpha `page:scaffolder` extension to accept `formFields` input, matching the updated `FormFieldBlueprint`.
+21 -21
View File
@@ -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<undefined>;
},
'core.nav-item.target',
{}
>;
inputs: {};
params: {
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
};
}>;
'page:app-visualizer': ExtensionDefinition<{
kind: 'page';
name: undefined;
@@ -46,27 +67,6 @@ const visualizerPlugin: FrontendPlugin<
routeRef?: RouteRef<AnyRouteRefParams> | undefined;
};
}>;
'nav-item:app-visualizer': ExtensionDefinition<{
kind: 'nav-item';
name: undefined;
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
{
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
},
'core.nav-item.target',
{}
>;
inputs: {};
params: {
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
};
}>;
}
>;
export default visualizerPlugin;
+15 -15
View File
@@ -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<AnyRouteRefParams> | undefined;
};
}>;
'api:catalog-import': ExtensionDefinition<{
kind: 'api';
name: undefined;
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
params: {
factory: AnyApiFactory;
};
}>;
}
>;
export default _default;
+30 -30
View File
@@ -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<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>;
inputs: {};
params: {
defaultPath: string;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef<AnyRouteRefParams> | 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<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>;
inputs: {};
params: {
defaultPath: string;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef<AnyRouteRefParams> | undefined;
};
}>;
}
>;
export default _default;
+15 -15
View File
@@ -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<AnyRouteRefParams> | 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';
+2 -2
View File
@@ -224,7 +224,7 @@ export const formFieldsApi: ExtensionDefinition<{
};
}>;
// @alpha (undocumented)
// @alpha @deprecated (undocumented)
export const formFieldsApiRef: ApiRef<ScaffolderFormFieldsApi>;
// @alpha (undocumented)
@@ -301,7 +301,7 @@ export type ScaffolderFormDecoratorContext<
) => void;
};
// @alpha (undocumented)
// @alpha @deprecated (undocumented)
export interface ScaffolderFormFieldsApi {
// (undocumented)
getFormFields(): Promise<FormFieldExtensionData[]>;
+4 -1
View File
@@ -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<ScaffolderFormFieldsApi>({
id: 'plugin.scaffolder.form-fields',
});
@@ -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<FormFieldExtensionData[]>;
}
@@ -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,
},
+15 -3
View File
@@ -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<FormField>,
'scaffolder.form-field-loader',
{}
>,
{
singleton: false;
optional: false;
}
>;
};
kind: 'page';
name: undefined;
params: {
defaultPath: string;
loader: () => Promise<JSX.Element>;
+21 -6
View File
@@ -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(<m.Router />)),
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(
<m.InternalRouter formFieldLoaders={formFieldLoaders} />,
),
),
});
},
});
@@ -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<RouterProps>) => {
export const InternalRouter = (
props: PropsWithChildren<
RouterProps & { formFieldLoaders?: Array<() => Promise<FormField>> }
>,
) => {
const {
components: {
TemplateCardComponent,
@@ -123,13 +130,15 @@ export const Router = (props: PropsWithChildren<RouterProps>) => {
} = {},
} = props;
const outlet = useOutlet() || props.children;
const customFieldExtensions =
useCustomFieldExtensions<FieldExtensionOptions>(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<RouterProps>) => {
</Routes>
);
};
/**
* The Scaffolder Router
*
* @public
*/
export const Router = (props: PropsWithChildren<RouterProps>) => {
return <InternalRouter {...props} />;
};
function useFormFieldLoaders(
formFieldLoaders?: Array<() => Promise<FormField>>,
) {
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;
}
+30 -30
View File
@@ -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<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>;
inputs: {};
params: {
defaultPath: string;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef<AnyRouteRefParams> | 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<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>;
inputs: {};
params: {
defaultPath: string;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef<AnyRouteRefParams> | undefined;
};
}>;
'search-result-list-item:techdocs': ExtensionDefinition<{
config: {
title: string | undefined;