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:
@@ -43,7 +43,9 @@ test('Should render the home page', async ({ page }) => {
|
||||
await enterButton.click();
|
||||
|
||||
// Wait for sign-in to complete
|
||||
await expect(page.getByRole('link', { name: 'Catalog' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('navigation').getByRole('link', { name: 'Catalog' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.goto('/home');
|
||||
// The home page should render with the custom homepage grid
|
||||
|
||||
@@ -24,7 +24,9 @@ test('the results are rendered as expected', async ({ page }) => {
|
||||
await enterButton.click();
|
||||
|
||||
// Wait for sign-in to complete before navigating
|
||||
await expect(page.getByRole('link', { name: 'Catalog' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('navigation').getByRole('link', { name: 'Catalog' }),
|
||||
).toBeVisible();
|
||||
|
||||
// Set up route interception BEFORE navigating to the search page
|
||||
await page.route(`**/api/search/query?term=*`, async route => {
|
||||
|
||||
@@ -24,6 +24,8 @@ test('App should render the welcome page', async ({ page }) => {
|
||||
await enterButton.click();
|
||||
|
||||
// Verify the sidebar navigation is visible after sign-in
|
||||
await expect(page.getByRole('link', { name: 'Catalog' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('navigation').getByRole('link', { name: 'Catalog' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'APIs' })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ test('App should render the welcome page', async ({ page }) => {
|
||||
await expect(enterButton).toBeVisible();
|
||||
await enterButton.click();
|
||||
|
||||
await expect(page.getByRole('link', { name: 'Catalog' })).toBeVisible();
|
||||
const nav = page.getByRole('navigation');
|
||||
await expect(nav.getByRole('link', { name: 'Catalog' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'APIs' })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1924,6 +1924,8 @@ export interface PageLayoutProps {
|
||||
tabs?: PageLayoutTab[];
|
||||
// (undocumented)
|
||||
title?: string;
|
||||
// (undocumented)
|
||||
titleLink?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -25,7 +25,24 @@ import {
|
||||
} from '../wiring';
|
||||
import { ExtensionBoundary, PageLayout, PageLayoutTab } from '../components';
|
||||
import { useApi } from '../apis/system';
|
||||
import { routeResolutionApiRef } from '../apis/definitions/RouteResolutionApi';
|
||||
import { pluginHeaderActionsApiRef } from '../apis/definitions/PluginHeaderActionsApi';
|
||||
import { RouteResolutionApi } from '../apis/definitions/RouteResolutionApi';
|
||||
|
||||
function resolveTitleLink(
|
||||
routeResolutionApi: RouteResolutionApi,
|
||||
routeRef: RouteRef | undefined,
|
||||
): string | undefined {
|
||||
if (!routeRef) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return routeResolutionApi.resolve(routeRef)?.();
|
||||
} catch {
|
||||
// Route ref may require params not available in the current context
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates extensions that are routable React page components.
|
||||
@@ -78,11 +95,15 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
const resolvedTitle =
|
||||
title ?? node.spec.plugin.title ?? node.spec.plugin.pluginId;
|
||||
const resolvedIcon = icon ?? node.spec.plugin.icon;
|
||||
const titleRouteRef =
|
||||
(node.spec.plugin.routes as { root?: RouteRef }).root ?? params.routeRef;
|
||||
|
||||
yield coreExtensionData.routePath(config.path ?? params.path);
|
||||
if (params.loader) {
|
||||
const loader = params.loader;
|
||||
const PageContent = () => {
|
||||
const routeResolutionApi = useApi(routeResolutionApiRef);
|
||||
const titleLink = resolveTitleLink(routeResolutionApi, titleRouteRef);
|
||||
const headerActionsApi = useApi(pluginHeaderActionsApiRef);
|
||||
const headerActions = headerActionsApi.getPluginHeaderActions(pluginId);
|
||||
|
||||
@@ -91,6 +112,7 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
title={resolvedTitle}
|
||||
icon={resolvedIcon}
|
||||
noHeader={noHeader}
|
||||
titleLink={titleLink}
|
||||
headerActions={headerActions}
|
||||
>
|
||||
{ExtensionBoundary.lazy(node, loader)}
|
||||
@@ -114,6 +136,8 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
|
||||
const PageContent = () => {
|
||||
const firstPagePath = inputs.pages[0]?.get(coreExtensionData.routePath);
|
||||
const routeResolutionApi = useApi(routeResolutionApiRef);
|
||||
const titleLink = resolveTitleLink(routeResolutionApi, titleRouteRef);
|
||||
|
||||
const headerActionsApi = useApi(pluginHeaderActionsApiRef);
|
||||
const headerActions = headerActionsApi.getPluginHeaderActions(pluginId);
|
||||
@@ -123,6 +147,7 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
title={resolvedTitle}
|
||||
icon={resolvedIcon}
|
||||
tabs={tabs}
|
||||
titleLink={titleLink}
|
||||
headerActions={headerActions}
|
||||
>
|
||||
<Routes>
|
||||
@@ -147,12 +172,15 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
yield coreExtensionData.reactElement(<PageContent />);
|
||||
} else {
|
||||
const PageContent = () => {
|
||||
const routeResolutionApi = useApi(routeResolutionApiRef);
|
||||
const titleLink = resolveTitleLink(routeResolutionApi, titleRouteRef);
|
||||
const headerActionsApi = useApi(pluginHeaderActionsApiRef);
|
||||
const headerActions = headerActionsApi.getPluginHeaderActions(pluginId);
|
||||
return (
|
||||
<PageLayout
|
||||
title={resolvedTitle}
|
||||
icon={resolvedIcon}
|
||||
titleLink={titleLink}
|
||||
headerActions={headerActions}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface PageLayoutProps {
|
||||
title?: string;
|
||||
icon?: IconElement;
|
||||
noHeader?: boolean;
|
||||
titleLink?: string;
|
||||
headerActions?: Array<JSX.Element | null>;
|
||||
tabs?: PageLayoutTab[];
|
||||
children?: ReactNode;
|
||||
|
||||
Reference in New Issue
Block a user