diff --git a/packages/frontend-plugin-api/src/apis/definitions/HeaderActionsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/HeaderActionsApi.ts index 77681ef959..04580a8be8 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/HeaderActionsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/HeaderActionsApi.ts @@ -17,27 +17,14 @@ import { ReactNode } from 'react'; import { createApiRef } from '../system'; -/** - * A single header action with its node ID for ordering. - * - * @public - */ -export interface HeaderAction { - /** The extension node ID, used for ordering across sources */ - nodeId: string; - /** The rendered header action element */ - element: ReactNode; -} - /** * API for retrieving plugin-scoped header actions. * * @remarks * - * Plugin-level header actions are provided via {@link @backstage/frontend-plugin-api#PluginHeaderActionBlueprint} - * and automatically scoped to the providing plugin. The `nodeId` field on each - * action is used together with `AppTreeApi` to determine the display order - * across both page-level and plugin-level actions. + * Header actions are provided via + * {@link @backstage/frontend-plugin-api#HeaderActionBlueprint} + * and automatically scoped to the providing plugin. * * @public */ @@ -45,7 +32,7 @@ export type HeaderActionsApi = { /** * Returns the header actions for a given plugin. */ - getHeaderActions(pluginId: string): HeaderAction[]; + getHeaderActions(pluginId: string): ReactNode[]; }; /** diff --git a/packages/frontend-plugin-api/src/blueprints/PluginHeaderActionBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/HeaderActionBlueprint.tsx similarity index 84% rename from packages/frontend-plugin-api/src/blueprints/PluginHeaderActionBlueprint.tsx rename to packages/frontend-plugin-api/src/blueprints/HeaderActionBlueprint.tsx index b36eae3bf6..cbc6f6ccf5 100644 --- a/packages/frontend-plugin-api/src/blueprints/PluginHeaderActionBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/HeaderActionBlueprint.tsx @@ -20,9 +20,9 @@ import { createExtensionDataRef, } from '../wiring'; -const actionDataRef = createExtensionDataRef< - () => Promise ->().with({ id: 'core.plugin-header-action.loader' }); +const actionDataRef = createExtensionDataRef<() => Promise>().with( + { id: 'core.header-action.loader' }, +); /** * Creates extensions that provide plugin-scoped header actions. @@ -34,8 +34,8 @@ const actionDataRef = createExtensionDataRef< * * @public */ -export const PluginHeaderActionBlueprint = createExtensionBlueprint({ - kind: 'plugin-header-action', +export const HeaderActionBlueprint = createExtensionBlueprint({ + kind: 'header-action', attachTo: { id: 'api:app/header-actions', input: 'actions' }, output: [actionDataRef], dataRefs: { diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index 5406db28fe..daa0f94594 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -26,35 +26,14 @@ import { import { ExtensionBoundary, PageLayout, PageTab } from '../components'; import { useApi } from '../apis/system'; import { headerActionsApiRef } from '../apis/definitions/HeaderActionsApi'; -import { appTreeApiRef } from '../apis/definitions/AppTreeApi'; -import { HeaderAction } from '../apis/definitions/HeaderActionsApi'; -/** - * Hook that collects page-level and plugin-level header actions, then - * sorts them by their position in the global app tree node list. - */ -function useHeaderActions( - pageActions: HeaderAction[], - pluginId: string, -): ReactNode { +function useHeaderActions(pluginId: string): ReactNode { const headerActionsApi = useApi(headerActionsApiRef); - const appTreeApi = useApi(appTreeApiRef); - - const pluginActions = headerActionsApi.getHeaderActions(pluginId); - const allActions = [...pageActions, ...pluginActions]; - - if (allActions.length === 0) { + const actions = headerActionsApi.getHeaderActions(pluginId); + if (actions.length === 0) { return undefined; } - - // Build a position index from the global node ordering - const nodeKeys = [...appTreeApi.getTree().tree.nodes.keys()]; - const orderIndex = new Map(nodeKeys.map((id, i) => [id, i])); - allActions.sort( - (a, b) => (orderIndex.get(a.nodeId) ?? 0) - (orderIndex.get(b.nodeId) ?? 0), - ); - - return <>{allActions.map(a => a.element)}; + return <>{actions}; } /** @@ -72,7 +51,6 @@ export const PageBlueprint = createExtensionBlueprint({ coreExtensionData.reactElement, coreExtensionData.title.optional(), ]), - headerActions: createExtensionInput([coreExtensionData.reactElement]), }, output: [ coreExtensionData.routePath, @@ -109,16 +87,11 @@ export const PageBlueprint = createExtensionBlueprint({ const icon = params.icon ?? node.spec.plugin.icon; const pluginId = node.spec.plugin.pluginId; - const pageActions: HeaderAction[] = inputs.headerActions.map(action => ({ - nodeId: action.node.spec.id, - element: action.get(coreExtensionData.reactElement), - })); - yield coreExtensionData.routePath(config.path ?? params.path); if (params.loader) { const loader = params.loader; const PageContent = () => { - const headerActions = useHeaderActions(pageActions, pluginId); + const headerActions = useHeaderActions(pluginId); return ( {ExtensionBoundary.lazy(node, loader)} @@ -141,7 +114,7 @@ export const PageBlueprint = createExtensionBlueprint({ const PageContent = () => { const firstPagePath = inputs.pages[0]?.get(coreExtensionData.routePath); - const headerActions = useHeaderActions(pageActions, pluginId); + const headerActions = useHeaderActions(pluginId); return ( ); } else { const PageContent = () => { - const headerActions = useHeaderActions(pageActions, pluginId); + const headerActions = useHeaderActions(pluginId); return ( - + ); }; yield coreExtensionData.reactElement(); diff --git a/packages/frontend-plugin-api/src/blueprints/PageHeaderActionBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageHeaderActionBlueprint.tsx deleted file mode 100644 index d6e1cb339a..0000000000 --- a/packages/frontend-plugin-api/src/blueprints/PageHeaderActionBlueprint.tsx +++ /dev/null @@ -1,35 +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 { coreExtensionData, createExtensionBlueprint } from '../wiring'; -import { ExtensionBoundary } from '../components'; - -/** - * Creates extensions that render header actions in a page layout. - * - * @public - */ -export const PageHeaderActionBlueprint = createExtensionBlueprint({ - kind: 'page-header-action', - attachTo: { relative: { kind: 'page' }, input: 'headerActions' }, - output: [coreExtensionData.reactElement], - - *factory(params: { loader: () => Promise }, { node }) { - yield coreExtensionData.reactElement( - ExtensionBoundary.lazy(node, params.loader), - ); - }, -}); diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts index 6640b14e7d..fd55dbb7a5 100644 --- a/packages/frontend-plugin-api/src/blueprints/index.ts +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -23,5 +23,4 @@ export { AppRootElementBlueprint } from './AppRootElementBlueprint'; export { NavItemBlueprint } from './NavItemBlueprint'; export { PageBlueprint } from './PageBlueprint'; export { SubPageBlueprint } from './SubPageBlueprint'; -export { PageHeaderActionBlueprint } from './PageHeaderActionBlueprint'; -export { PluginHeaderActionBlueprint } from './PluginHeaderActionBlueprint'; +export { HeaderActionBlueprint } from './HeaderActionBlueprint'; diff --git a/plugins/app/src/apis/HeaderActionsApi/DefaultHeaderActionsApi.test.tsx b/plugins/app/src/apis/HeaderActionsApi/DefaultHeaderActionsApi.test.tsx index 9cc17f88f9..dd881a786f 100644 --- a/plugins/app/src/apis/HeaderActionsApi/DefaultHeaderActionsApi.test.tsx +++ b/plugins/app/src/apis/HeaderActionsApi/DefaultHeaderActionsApi.test.tsx @@ -32,19 +32,10 @@ describe('DefaultHeaderActionsApi', () => { }, ]); - const actionsA = api.getHeaderActions('plugin-a'); - const actionsB = api.getHeaderActions('plugin-b'); + expect(api.getHeaderActions('plugin-a')).toHaveLength(1); + expect(api.getHeaderActions('plugin-b')).toHaveLength(1); - expect(actionsA).toHaveLength(1); - expect(actionsB).toHaveLength(1); - expect(actionsA[0].nodeId).toBe( - 'plugin-header-action:plugin-a/action-a', - ); - expect(actionsB[0].nodeId).toBe( - 'plugin-header-action:plugin-b/action-b', - ); - - render(<>{actionsA[0].element}); + render(<>{api.getHeaderActions('plugin-a')}); await expect( screen.findByRole('button', { name: 'Action A' }), ).resolves.toBeInTheDocument(); @@ -78,16 +69,8 @@ describe('DefaultHeaderActionsApi', () => { const actions = api.getHeaderActions('plugin-a'); expect(actions).toHaveLength(2); - expect(actions[0].nodeId).toBe('plugin-header-action:plugin-a/first'); - expect(actions[1].nodeId).toBe('plugin-header-action:plugin-a/second'); - render( - <> - {actions.map(a => ( - {a.element} - ))} - , - ); + render(<>{actions}); await expect( screen.findByRole('button', { name: 'First' }), ).resolves.toBeInTheDocument(); diff --git a/plugins/app/src/apis/HeaderActionsApi/DefaultHeaderActionsApi.tsx b/plugins/app/src/apis/HeaderActionsApi/DefaultHeaderActionsApi.tsx index 2fa35c97de..13f94da55b 100644 --- a/plugins/app/src/apis/HeaderActionsApi/DefaultHeaderActionsApi.tsx +++ b/plugins/app/src/apis/HeaderActionsApi/DefaultHeaderActionsApi.tsx @@ -14,11 +14,8 @@ * limitations under the License. */ -import { Suspense, lazy } from 'react'; -import { - type HeaderActionsApi, - type HeaderAction, -} from '@backstage/frontend-plugin-api'; +import { Suspense, ReactNode, lazy } from 'react'; +import { type HeaderActionsApi } from '@backstage/frontend-plugin-api'; type ActionInput = { loader: () => Promise; @@ -32,18 +29,14 @@ type ActionInput = { * @internal */ export class DefaultHeaderActionsApi implements HeaderActionsApi { - constructor( - private readonly actionsByPlugin: Map, - ) {} + constructor(private readonly actionsByPlugin: Map) {} - getHeaderActions(pluginId: string): HeaderAction[] { + getHeaderActions(pluginId: string): ReactNode[] { return this.actionsByPlugin.get(pluginId) ?? []; } - static fromActions( - actions: Array, - ): DefaultHeaderActionsApi { - const actionsByPlugin = new Map(); + static fromActions(actions: Array): DefaultHeaderActionsApi { + const actionsByPlugin = new Map(); for (const action of actions) { let pluginActions = actionsByPlugin.get(action.pluginId); @@ -57,14 +50,11 @@ export class DefaultHeaderActionsApi implements HeaderActionsApi { return { default: () => element }; }); - pluginActions.push({ - nodeId: action.nodeId, - element: ( - - - - ), - }); + pluginActions.push( + + + , + ); } return new DefaultHeaderActionsApi(actionsByPlugin); diff --git a/plugins/app/src/extensions/HeaderActionsApi.ts b/plugins/app/src/extensions/HeaderActionsApi.ts index a64faf3ed8..6768bb3655 100644 --- a/plugins/app/src/extensions/HeaderActionsApi.ts +++ b/plugins/app/src/extensions/HeaderActionsApi.ts @@ -15,7 +15,7 @@ */ import { - PluginHeaderActionBlueprint, + HeaderActionBlueprint, headerActionsApiRef, createExtensionInput, ApiBlueprint, @@ -28,9 +28,7 @@ import { DefaultHeaderActionsApi } from '../apis/HeaderActionsApi'; export const HeaderActionsApi = ApiBlueprint.makeWithOverrides({ name: 'header-actions', inputs: { - actions: createExtensionInput([ - PluginHeaderActionBlueprint.dataRefs.action, - ]), + actions: createExtensionInput([HeaderActionBlueprint.dataRefs.action]), }, factory: (originalFactory, { inputs }) => { return originalFactory(defineParams => @@ -40,9 +38,7 @@ export const HeaderActionsApi = ApiBlueprint.makeWithOverrides({ factory: () => { return DefaultHeaderActionsApi.fromActions( inputs.actions.map(actionInput => ({ - loader: actionInput.get( - PluginHeaderActionBlueprint.dataRefs.action, - ), + loader: actionInput.get(HeaderActionBlueprint.dataRefs.action), pluginId: actionInput.node.spec.plugin.pluginId, nodeId: actionInput.node.spec.id, })),