feat(frontend-plugin-api): add titleRouteRef to PageBlueprint

Add `titleRouteRef` to `PageLayoutProps` so the plugin header title
links back to the plugin root. `PageBlueprint` resolves it from
`plugin.routes.root` with fallback to `params.routeRef`.

- PageLayout swap resolves the title link via a conditional child
  component that calls `useRouteRef` only when a route ref exists
- Header actions get stable React keys via `cloneElement`

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-03-16 17:27:46 +01:00
parent 364d4fe187
commit d66a3ec9ab
11 changed files with 71 additions and 7 deletions
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JSX } from 'react';
import { cloneElement, JSX } from 'react';
import { type PluginHeaderActionsApi } from '@backstage/frontend-plugin-api';
// Stable reference
@@ -51,7 +51,14 @@ export class DefaultPluginHeaderActionsApi implements PluginHeaderActionsApi {
actionsByPlugin.set(action.pluginId, pluginActions);
}
pluginActions.push(action.element);
const index = pluginActions.length;
pluginActions.push(
cloneElement(action.element, {
key:
action.element.key ??
`plugin-header-action-${action.pluginId}-${index}`,
}),
);
}
return new DefaultPluginHeaderActionsApi(actionsByPlugin);
+10 -1
View File
@@ -75,7 +75,15 @@ export const PageLayout = SwappableComponentBlueprint.make({
define({
component: SwappablePageLayout,
loader: () => (props: PageLayoutProps) => {
const { title, icon, noHeader, headerActions, tabs, children } = props;
const {
title,
icon,
noHeader,
titleLink,
headerActions,
tabs,
children,
} = props;
// TODO(Rugvip): Different solution to this path handling would be good
const parentPath = useResolvedPath('.').pathname.replace(/\/$/, '');
const resolvedTabs = useMemo(
@@ -99,6 +107,7 @@ export const PageLayout = SwappableComponentBlueprint.make({
<PluginHeader
title={title}
icon={icon}
titleLink={titleLink}
tabs={resolvedTabs}
customActions={headerActions}
/>