From 6211dd871a636b4d678e464bf01074401981698d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 20:33:11 +0100 Subject: [PATCH] Move sub-page tab href resolution to PageLayout Move the logic for resolving relative sub-page tab hrefs from PageBlueprint into the app PageLayout component, where it belongs as an app-level rendering concern. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/blueprints/PageBlueprint.tsx | 43 +++++++------------ plugins/app/src/extensions/components.tsx | 12 ++++-- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index 7daf876ac1..bf2e6d8135 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -15,7 +15,7 @@ */ import { JSX } from 'react'; -import { Routes, Route, Navigate, useResolvedPath } from 'react-router-dom'; +import { Routes, Route, Navigate } from 'react-router-dom'; import { IconElement } from '../icons/types'; import { RouteRef } from '../routing'; import { @@ -72,7 +72,6 @@ export const PageBlueprint = createExtensionBlueprint({ { config, node, inputs }, ) { const title = config.title ?? params.title; - const routePath = config.path ?? params.path; const icon = params.icon; const pluginId = node.spec.plugin.pluginId; const noHeader = params.noHeader ?? false; @@ -80,7 +79,7 @@ export const PageBlueprint = createExtensionBlueprint({ title ?? node.spec.plugin.title ?? node.spec.plugin.pluginId; const resolvedIcon = icon ?? node.spec.plugin.icon; - yield coreExtensionData.routePath(routePath); + yield coreExtensionData.routePath(config.path ?? params.path); if (params.loader) { const loader = params.loader; const PageContent = () => { @@ -100,34 +99,24 @@ export const PageBlueprint = createExtensionBlueprint({ }; yield coreExtensionData.reactElement(); } else if (inputs.pages.length > 0) { + // Parent page with sub-pages - render header with tabs + const tabs: PageLayoutTab[] = inputs.pages.map(page => { + const path = page.get(coreExtensionData.routePath); + const tabTitle = page.get(coreExtensionData.title); + const tabIcon = page.get(coreExtensionData.icon); + return { + id: path, + label: tabTitle || path, + icon: tabIcon, + href: path, + }; + }); + const PageContent = () => { const firstPagePath = inputs.pages[0]?.get(coreExtensionData.routePath); + const headerActionsApi = useApi(pluginHeaderActionsApiRef); const headerActions = headerActionsApi.getPluginHeaderActions(pluginId); - const parentPath = useResolvedPath('.').pathname.replace(/\/$/, ''); - const staticParentPath = - routePath.startsWith('/') && - !routePath.includes('/:') && - !routePath.includes('*') - ? routePath.replace(/\/$/, '') - : undefined; - const tabs: PageLayoutTab[] = inputs.pages.map(page => { - const path = page.get(coreExtensionData.routePath); - const tabTitle = page.get(coreExtensionData.title); - const tabIcon = page.get(coreExtensionData.icon); - const tabPath = path.replace(/^\/+/, ''); - const basePath = staticParentPath ?? parentPath ?? ''; - const href = path.startsWith('/') - ? path - : `${basePath}/${tabPath}`.replace(/\/{2,}/g, '/'); - - return { - id: path, - label: tabTitle || path, - icon: tabIcon, - href, - }; - }); return ( (props: PageLayoutProps) => { const { title, icon, noHeader, headerActions, tabs, children } = props; - const tabsWithMatchStrategy = useMemo( + // TODO(Rugvip): Different solution to this path handling would be good + const parentPath = useResolvedPath('.').pathname.replace(/\/$/, ''); + const resolvedTabs = useMemo( () => tabs?.map(tab => ({ ...tab, + href: tab.href.startsWith('/') + ? tab.href + : `${parentPath}/${tab.href}`.replace(/\/{2,}/g, '/'), matchStrategy: 'prefix' as const, })), - [tabs], + [tabs, parentPath], ); if (noHeader) { @@ -93,7 +99,7 @@ export const PageLayout = SwappableComponentBlueprint.make({ {children}