From 1fb196a64e070d4439cbe5f4f79b805cc4fe3d42 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 17:18:41 +0200 Subject: [PATCH] refactor: extract typed navigation routes --- .../assets/web/generated/app.js | 78 +++++++++++-------- .../trx-frontend-http/frontend/src/app.js | 39 ++-------- .../frontend/src/core/decoder-registry.ts | 4 +- .../src/features/navigation/routes.ts | 39 ++++++++++ .../frontend/tests/navigation-routes.test.mjs | 40 ++++++++++ 5 files changed, 134 insertions(+), 66 deletions(-) create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/frontend/src/features/navigation/routes.ts create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/navigation-routes.test.mjs diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js index c4f29569..4e0c1d72 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js @@ -80,7 +80,7 @@ if (decoderRegistry.length > 0) callback(); else readyCallbacks.push(callback); } - function hideUnsupportedDecoderUi() { + function applyDecoderRegistryVisibility() { const knownIds = new Set(decoderRegistry.map(({ id }) => id)); const alwaysShow = /* @__PURE__ */ new Set(["overview", "rds", "sat"]); document.querySelectorAll( @@ -128,7 +128,7 @@ readyCallbacks.splice(0).forEach((callback) => { callback(); }); - hideUnsupportedDecoderUi(); + applyDecoderRegistryVisibility(); onLoaded(); } catch (error) { console.error("Failed to fetch decoder registry:", error); @@ -352,6 +352,44 @@ return value; } + // src/features/navigation/routes.ts + var TAB_ORDER = [ + "main", + "bookmarks", + "digital-modes", + "map", + "statistics", + "recorder", + "settings", + "about" + ]; + var TAB_PATHS = { + main: "/", + bookmarks: "/bookmarks", + "digital-modes": "/digital-modes", + map: "/map", + statistics: "/statistics", + recorder: "/recorder", + settings: "/settings", + about: "/about" + }; + function normalizeTabPath(pathname) { + const raw = pathname.length > 0 ? pathname : "/"; + return raw === "/" ? "/" : raw.replace(/\/+$/, "") || "/"; + } + function tabFromPath(pathname) { + const normalized = normalizeTabPath(pathname); + const match = Object.entries(TAB_PATHS).find(([, path]) => path === normalized); + return match ? match[0] : "main"; + } + function updateTabHistory(name, replace = false) { + const targetPath = TAB_PATHS[name]; + if (normalizeTabPath(window.location.pathname) === targetPath) return; + const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`; + if (replace) window.history.replaceState({}, "", nextUrl); + else window.history.pushState({}, "", nextUrl); + } + // src/app.js void loadDecoderRegistry(refreshOperatorLayoutCapabilities); var authRole = null; @@ -420,7 +458,7 @@ document.querySelectorAll(".tab-bar .tab").forEach((btn) => { btn.classList.remove("active"); }); - navigateToTab(tabFromPath(), { updateHistory: false, replaceHistory: true }); + navigateToTab(tabFromPath2(), { updateHistory: false, replaceHistory: true }); syncTopBarAccess(); } function showAuthError(msg) { @@ -4184,34 +4222,8 @@ ${unsupportedBandSummary()}`; }); } var _activeTab = "main"; - var TAB_ORDER = ["main", "bookmarks", "digital-modes", "map", "statistics", "recorder", "settings", "about"]; - var TAB_PATHS = { - main: "/", - bookmarks: "/bookmarks", - "digital-modes": "/digital-modes", - map: "/map", - recorder: "/recorder", - settings: "/settings", - about: "/about" - }; - function normalizeTabPath(pathname) { - const raw = typeof pathname === "string" && pathname.length > 0 ? pathname : "/"; - if (raw === "/") return "/"; - return raw.replace(/\/+$/, "") || "/"; - } - function tabFromPath(pathname = window.location.pathname) { - const normalized = normalizeTabPath(pathname); - for (const [tabName, tabPath] of Object.entries(TAB_PATHS)) { - if (normalized === tabPath) return tabName; - } - return "main"; - } - function updateTabHistory(name, replaceHistory = false) { - const targetPath = TAB_PATHS[name] || "/"; - if (normalizeTabPath(window.location.pathname) === targetPath) return; - const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`; - const method = replaceHistory ? "replaceState" : "pushState"; - window.history[method]({}, "", nextUrl); + function tabFromPath2(pathname = window.location.pathname) { + return tabFromPath(pathname); } var _mapInitTimer = null; function _initMapWhenReady() { @@ -4265,7 +4277,7 @@ ${unsupportedBandSummary()}`; panel.appendChild(tmpl.content.cloneNode(true)); tmpl.remove(); panel.querySelectorAll(".sub-tab-bar").forEach(_wireSubTabBar); - if (decoderRegistry.length) hideUnsupportedDecoderTabs(); + if (decoderRegistry.length) applyDecoderRegistryVisibility(); } if (updateHistory) { updateTabHistory(name, replaceHistory); @@ -4289,7 +4301,7 @@ ${unsupportedBandSummary()}`; navigateToTab(btn.dataset.tab); }); window.addEventListener("popstate", () => { - navigateToTab(tabFromPath(), { updateHistory: false }); + navigateToTab(tabFromPath2(), { updateHistory: false }); }); (function() { let tx = 0, ty = 0; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js index 122737fb..bdaf449b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js @@ -12,6 +12,7 @@ import { import { escapeHtml as escapeMapHtml } from "./core/dom.js"; import { loadSetting, saveSetting } from "./core/settings.js"; import { + applyDecoderRegistryVisibility, decoderRegistry, loadDecoderRegistry, } from "./core/decoder-registry.js"; @@ -30,6 +31,11 @@ import { parseFrequencyInput, } from "./core/format.js"; import { decodeCbor as decodeCborPayload } from "./core/cbor.js"; +import { + TAB_ORDER, + tabFromPath as tabFromPathname, + updateTabHistory, +} from "./features/navigation/routes.js"; // --- Decoder registry (fetched from /decoders on load) --- void loadDecoderRegistry(refreshOperatorLayoutCapabilities); @@ -4258,37 +4264,8 @@ if (spectrumBwSweetBtn) { // --- Tab navigation --- let _activeTab = "main"; // tracked for render-path tab awareness -const TAB_ORDER = ["main", "bookmarks", "digital-modes", "map", "statistics", "recorder", "settings", "about"]; -const TAB_PATHS = { - main: "/", - bookmarks: "/bookmarks", - "digital-modes": "/digital-modes", - map: "/map", - recorder: "/recorder", - settings: "/settings", - about: "/about", -}; - -function normalizeTabPath(pathname) { - const raw = typeof pathname === "string" && pathname.length > 0 ? pathname : "/"; - if (raw === "/") return "/"; - return raw.replace(/\/+$/, "") || "/"; -} - function tabFromPath(pathname = window.location.pathname) { - const normalized = normalizeTabPath(pathname); - for (const [tabName, tabPath] of Object.entries(TAB_PATHS)) { - if (normalized === tabPath) return tabName; - } - return "main"; -} - -function updateTabHistory(name, replaceHistory = false) { - const targetPath = TAB_PATHS[name] || "/"; - if (normalizeTabPath(window.location.pathname) === targetPath) return; - const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`; - const method = replaceHistory ? "replaceState" : "pushState"; - window.history[method]({}, "", nextUrl); + return tabFromPathname(pathname); } // Initialise the Leaflet map, waiting for both Leaflet (L) and map-core.js @@ -4347,7 +4324,7 @@ function navigateToTab(name, options = {}) { // Wire sub-tab bars inside the freshly cloned content. panel.querySelectorAll(".sub-tab-bar").forEach(_wireSubTabBar); // Re-run decoder visibility for about-tab elements now in the DOM. - if (decoderRegistry.length) hideUnsupportedDecoderTabs(); + if (decoderRegistry.length) applyDecoderRegistryVisibility(); } if (updateHistory) { updateTabHistory(name, replaceHistory); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/decoder-registry.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/decoder-registry.ts index 763c1c65..f0444b8a 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/decoder-registry.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/decoder-registry.ts @@ -26,7 +26,7 @@ export function onDecoderRegistryReady(callback: () => void): void { else readyCallbacks.push(callback); } -function hideUnsupportedDecoderUi(): void { +export function applyDecoderRegistryVisibility(): void { const knownIds = new Set(decoderRegistry.map(({ id }) => id)); const alwaysShow = new Set(["overview", "rds", "sat"]); @@ -87,7 +87,7 @@ export async function loadDecoderRegistry(onLoaded: () => void): Promise { decoderRegistry = decodeRegistry(await response.json()); bridge.decoderRegistry = decoderRegistry; readyCallbacks.splice(0).forEach((callback) => { callback(); }); - hideUnsupportedDecoderUi(); + applyDecoderRegistryVisibility(); onLoaded(); } catch (error: unknown) { console.error("Failed to fetch decoder registry:", error); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/features/navigation/routes.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/features/navigation/routes.ts new file mode 100644 index 00000000..3d8ad7a5 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/features/navigation/routes.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +export const TAB_ORDER = [ + "main", "bookmarks", "digital-modes", "map", "statistics", "recorder", "settings", "about", +] as const; + +export type TabName = typeof TAB_ORDER[number]; + +export const TAB_PATHS: Readonly> = { + main: "/", + bookmarks: "/bookmarks", + "digital-modes": "/digital-modes", + map: "/map", + statistics: "/statistics", + recorder: "/recorder", + settings: "/settings", + about: "/about", +}; + +export function normalizeTabPath(pathname: string): string { + const raw = pathname.length > 0 ? pathname : "/"; + return raw === "/" ? "/" : raw.replace(/\/+$/, "") || "/"; +} + +export function tabFromPath(pathname: string): TabName { + const normalized = normalizeTabPath(pathname); + const match = Object.entries(TAB_PATHS).find(([, path]) => path === normalized); + return match ? match[0] as TabName : "main"; +} + +export function updateTabHistory(name: TabName, replace = false): void { + const targetPath = TAB_PATHS[name]; + if (normalizeTabPath(window.location.pathname) === targetPath) return; + const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`; + if (replace) window.history.replaceState({}, "", nextUrl); + else window.history.pushState({}, "", nextUrl); +} diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/navigation-routes.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/navigation-routes.test.mjs new file mode 100644 index 00000000..2703b784 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/navigation-routes.test.mjs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import assert from "node:assert/strict"; +import test from "node:test"; +import vm from "node:vm"; +import { build } from "esbuild"; + +async function loadRoutes(window) { + const result = await build({ + entryPoints: [new URL("../src/features/navigation/routes.ts", import.meta.url).pathname], + bundle: true, + format: "cjs", + platform: "browser", + target: "es2022", + write: false, + }); + const module = { exports: {} }; + vm.runInNewContext(result.outputFiles[0].text, { module, exports: module.exports, window }); + return module.exports; +} + +test("all top-level tabs have stable round-trip routes", async () => { + const calls = []; + const window = { + location: { pathname: "/", search: "?rig=one", hash: "#panel" }, + history: { + pushState: (...args) => calls.push(["push", ...args]), + replaceState: (...args) => calls.push(["replace", ...args]), + }, + }; + const routes = await loadRoutes(window); + for (const name of routes.TAB_ORDER) { + assert.equal(routes.tabFromPath(routes.TAB_PATHS[name]), name); + } + routes.updateTabHistory("statistics"); + assert.equal(calls[0][0], "push"); + assert.equal(calls[0][3], "/statistics?rig=one#panel"); +});