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 a380fbc3..98c621c6 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 @@ -637,6 +637,22 @@ function elementById(id) { }; const layoutCapabilities = { broadcast: false, digital: false }; let activeRigId = null; + const layoutSections = [ + { id: "advanced-radio-controls", key: "advanced" }, + { id: "audio-controls", key: "audio" }, + { id: "scheduler-controls", key: "scheduler" } + ]; + const seededSections = /* @__PURE__ */ new Set(); + let appliedLayoutName = null; + function seedLayoutSections(layout, layoutChanged) { + layoutSections.forEach(({ id, key }) => { + const section = document.getElementById(id); + if (!section) return; + if (!layoutChanged && seededSections.has(id)) return; + seededSections.add(id); + section.open = layout[key]; + }); + } function layoutStorageKey() { return activeRigId ? `trxOperatorLayout:${activeRigId}` : "trxOperatorLayout"; } @@ -690,12 +706,9 @@ function elementById(id) { const layout = layouts[permittedName]; document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact"; if (options.persist !== false) localStorage.setItem(layoutStorageKey(), permittedName); - const details = document.getElementById("advanced-radio-controls"); - if (details) details.open = layout.advanced; - const audioDetails = document.getElementById("audio-controls"); - if (audioDetails) audioDetails.open = layout.audio; - const schedulerDetails = document.getElementById("scheduler-controls"); - if (schedulerDetails) schedulerDetails.open = layout.scheduler; + const layoutChanged = appliedLayoutName !== permittedName; + appliedLayoutName = permittedName; + seedLayoutSections(layout, layoutChanged); if (options.navigate && typeof browserWindow.navigateToTab === "function") { browserWindow.navigateToTab(layout.preferredTab); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts index 899e13ef..5b660105 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts @@ -195,6 +195,31 @@ function elementById(id: string): T { const layoutCapabilities: Record = { broadcast: false, digital: false }; let activeRigId: string | null = null; + // A layout seeds the collapsible sections; it does not hold them there. + // applyLayout runs on every rig-state refresh, so re-applying the disclosure + // state unconditionally would reopen or close sections under the operator + // once a second — the reason a section could not be expanded while a layout + // that collapses it was selected. Sections are therefore written only when + // the layout actually changes, or the first time each one appears in the DOM + // (the advanced controls are built after the first applyLayout call). + const layoutSections: { id: string; key: "advanced" | "audio" | "scheduler" }[] = [ + { id: "advanced-radio-controls", key: "advanced" }, + { id: "audio-controls", key: "audio" }, + { id: "scheduler-controls", key: "scheduler" }, + ]; + const seededSections = new Set(); + let appliedLayoutName: LayoutName | null = null; + + function seedLayoutSections(layout: OperatorLayout, layoutChanged: boolean) { + layoutSections.forEach(({ id, key }) => { + const section = document.getElementById(id) as HTMLDetailsElement | null; + if (!section) return; + if (!layoutChanged && seededSections.has(id)) return; + seededSections.add(id); + section.open = layout[key]; + }); + } + function layoutStorageKey() { return activeRigId ? `trxOperatorLayout:${activeRigId}` : "trxOperatorLayout"; } @@ -255,12 +280,9 @@ function elementById(id: string): T { const layout = layouts[permittedName]; document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact"; if (options.persist !== false) localStorage.setItem(layoutStorageKey(), permittedName); - const details = document.getElementById("advanced-radio-controls") as HTMLDetailsElement | null; - if (details) details.open = layout.advanced; - const audioDetails = document.getElementById("audio-controls") as HTMLDetailsElement | null; - if (audioDetails) audioDetails.open = layout.audio; - const schedulerDetails = document.getElementById("scheduler-controls") as HTMLDetailsElement | null; - if (schedulerDetails) schedulerDetails.open = layout.scheduler; + const layoutChanged = appliedLayoutName !== permittedName; + appliedLayoutName = permittedName; + seedLayoutSections(layout, layoutChanged); if (options.navigate && typeof browserWindow.navigateToTab === "function") { browserWindow.navigateToTab(layout.preferredTab); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ui-core.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ui-core.test.mjs index 1828ba97..ecae4e7a 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ui-core.test.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ui-core.test.mjs @@ -143,4 +143,31 @@ ui.applyLayout("digital"); assert.equal(document.body.dataset.operatorLayout, "compact"); assert.equal(localStorage.getItem("trxOperatorLayout:rig-b"), "compact"); +// A layout seeds the collapsible sections but must not hold them there: +// applyLayout runs on every rig-state refresh, so an operator who expands a +// section a layout collapses by default had it shut again about once a second. +const schedulerSection = new Element("details", document); +schedulerSection.id = "scheduler-controls"; +const audioSection = new Element("details", document); +audioSection.id = "audio-controls"; + +ui.setActiveRig("rig-c"); +ui.applyLayout("compact"); +assert.equal(schedulerSection.open, false, "compact seeds the scheduler section closed"); + +schedulerSection.open = true; // operator expands it +ui.applyLayout("compact"); // the next rig-state refresh +assert.equal(schedulerSection.open, true, "re-applying the same layout leaves sections alone"); +ui.setActiveRig("rig-c"); +assert.equal(schedulerSection.open, true, "a rig-state refresh leaves sections alone"); + +// Choosing a different layout is an explicit request for its defaults. +ui.applyLayout("full"); +assert.equal(schedulerSection.open, true, "full seeds the scheduler section open"); +assert.equal(audioSection.open, true, "full seeds the audio section open"); +audioSection.open = false; +ui.applyLayout("compact"); +assert.equal(audioSection.open, false, "compact seeds the audio section closed"); +assert.equal(schedulerSection.open, false, "switching layout reseeds every section"); + console.log("ui-core component tests passed");