From b4cdbf3d07c8a431d26bf7dce5bb82996656097c Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 2 Nov 2026 14:47:24 +1100 Subject: [PATCH] [UI] Improved notification links (#13159) Handle internal notification links which are stored as a fully qualified URL. These can now be routed internally rather than reloading the whole page --- .../src/components/nav/NotificationDrawer.tsx | 18 ++++++++++ src/frontend/tests/pui_notifications.spec.ts | 36 +++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/components/nav/NotificationDrawer.tsx b/src/frontend/src/components/nav/NotificationDrawer.tsx index f220d99e7a..8fb7fda2d3 100644 --- a/src/frontend/src/components/nav/NotificationDrawer.tsx +++ b/src/frontend/src/components/nav/NotificationDrawer.tsx @@ -63,6 +63,24 @@ function NotificationEntry({ } const base = `/${getBaseUrl()}`; + + // The backend may provide an absolute URL (e.g. https://host/web/part/1/) + // If it points to a page within this frontend, reduce it to a path, + // so that it can be routed internally rather than reloading the page + if (link && !link.startsWith('/')) { + try { + const url = new URL(link); + if ( + url.origin === window.location.origin && + url.pathname.startsWith(`${base}/`) + ) { + link = `${url.pathname}${url.search}${url.hash}`; + } + } catch { + // Not a valid URL - leave as-is + } + } + const href = link?.startsWith('/') && !link.startsWith(base) ? `${base}${link}` : link; diff --git a/src/frontend/tests/pui_notifications.spec.ts b/src/frontend/tests/pui_notifications.spec.ts index add5675db5..f94241d103 100644 --- a/src/frontend/tests/pui_notifications.spec.ts +++ b/src/frontend/tests/pui_notifications.spec.ts @@ -62,14 +62,32 @@ test('Notifications - link targets', async ({ page }) => { return; } + // Absolute link to a page on the same server (as provided by the backend) + const origin = new URL(page.url()).origin; + + const results = [ + ...notifications, + { + ...notifications[1], + pk: 4, + target: { + ...notifications[1].target, + link: `${origin}/web/part/category/index/` + }, + category: 'test_absolute', + name: 'Absolute Notification', + message: 'Absolute notification link' + } + ]; + await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ - count: notifications.length, + count: results.length, next: null, previous: null, - results: notifications + results: results }) }); }); @@ -89,4 +107,18 @@ test('Notifications - link targets', async ({ page }) => { await expect( page.getByRole('link', { name: 'Base Notification' }) ).toHaveAttribute('href', '/web/settings/admin/'); + + await expect( + page.getByRole('link', { name: 'Absolute Notification' }) + ).toHaveAttribute('href', '/web/part/category/index/'); + + // Clicking an internal link should route within the app (no page reload) + await page.evaluate(() => { + (window as any).__noReload = true; + }); + + await page.getByRole('link', { name: 'Absolute Notification' }).click(); + await page.waitForURL('**/web/part/category/index/**'); + + expect(await page.evaluate(() => (window as any).__noReload)).toBe(true); });