scaffolder: deduplicate OngoingTask and pick up page formFields via app tree

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-20 14:10:22 +01:00
parent 3bb9cb5b0a
commit 849f5995b5
2 changed files with 47 additions and 36 deletions
+27 -8
View File
@@ -16,6 +16,7 @@
import {
ApiBlueprint,
appTreeApiRef,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import {
@@ -39,18 +40,18 @@ export const formFieldsApi = ApiBlueprint.makeWithOverrides({
return originalFactory(defineParams =>
defineParams({
api: formFieldsApiRef,
deps: {},
factory: () => ({
deps: { appTreeApi: appTreeApiRef },
factory: ({ appTreeApi }) => ({
async loadFormFields() {
const pageFormFieldLoaders = getPageFormFieldLoaders(appTreeApi);
const formFields = await Promise.all(
formFieldLoaders.map(loader => loader()),
[...formFieldLoaders, ...pageFormFieldLoaders].map(loader =>
loader(),
),
);
const internalFormFields = formFields.map(
OpaqueFormField.toInternal,
);
return internalFormFields;
return formFields.map(OpaqueFormField.toInternal);
},
}),
}),
@@ -58,6 +59,24 @@ export const formFieldsApi = ApiBlueprint.makeWithOverrides({
},
});
function getPageFormFieldLoaders(appTreeApi: typeof appTreeApiRef.T) {
const { tree } = appTreeApi.getTree();
const pageNode = tree.nodes.get('page:scaffolder');
if (!pageNode?.instance) {
return [];
}
const formFieldNodes = pageNode.edges.attachments.get('formFields') ?? [];
return formFieldNodes.flatMap(node => {
if (!node.instance) {
return [];
}
const loader = node.instance.getData(
FormFieldBlueprint.dataRefs.formFieldLoader,
);
return loader ? [loader] : [];
});
}
export {
formFieldsApiRef,
type ScaffolderFormFieldsApi,
@@ -15,6 +15,7 @@
*/
import {
ComponentType,
ReactNode,
useCallback,
useEffect,
useMemo,
@@ -79,15 +80,7 @@ const useStyles = makeStyles(theme => ({
},
}));
/**
* @public
*/
export const OngoingTask = (props: {
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput;
}>;
}) => {
// todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful.
function OngoingTaskAnalyticsContext(props: { children: ReactNode }) {
const { taskId } = useParams();
const taskStream = useTaskEventStream(taskId!);
const { namespace, name } =
@@ -106,10 +99,25 @@ export const OngoingTask = (props: {
taskId,
}}
>
{props.children}
</AnalyticsContext>
);
}
/**
* @public
*/
export const OngoingTask = (props: {
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput;
}>;
}) => {
return (
<OngoingTaskAnalyticsContext>
<Page themeId="website">
<OngoingTaskChrome {...props} />
</Page>
</AnalyticsContext>
</OngoingTaskAnalyticsContext>
);
};
@@ -124,26 +132,10 @@ export function OngoingTaskBody(props: {
output?: ScaffolderTaskOutput;
}>;
}) {
const { taskId } = useParams();
const taskStream = useTaskEventStream(taskId!);
const { namespace, name } =
taskStream.task?.spec.templateInfo?.entity?.metadata ?? {};
return (
<AnalyticsContext
attributes={{
entityRef:
name &&
stringifyEntityRef({
kind: 'template',
namespace,
name,
}),
taskId,
}}
>
<OngoingTaskAnalyticsContext>
<OngoingTaskContent {...props} />
</AnalyticsContext>
</OngoingTaskAnalyticsContext>
);
}