diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index fd9b2c75f..ec1292b3f 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2235,7 +2235,9 @@ the collapse, so a panel the desktop accordion only folded has its saved and shared state closed too. Clear selected data layers and Context isolation (Contacts, Space Missions) switch the layer off without the user asking: the panel and its share preference stay as they are, and the layer comes back to an -open panel. A panel opened by hand while the layer is off stays open, and its +open panel. Only the switch-off isolation makes counts (`suspendsLayer`, true +while `_clearLayersOutsideContextMode` switches the layer off): the user +switching CCTV off inside Contacts closes the panel as anywhere else. A panel opened by hand while the layer is off stays open, and its meta line reads the catalog count, not the last active camera. ## UI disposal diff --git a/src/contextModePolicy.js b/src/contextModePolicy.js index ef485fe97..6e3238ec5 100644 --- a/src/contextModePolicy.js +++ b/src/contextModePolicy.js @@ -277,7 +277,6 @@ export function recordContextSessionUserChange({ ) { return false; } - snapshot.userSwitched?.add(change.layerId); const isCompanion = CONTEXT_COMPANIONS.has(change.layerId); if (change.enabled) { if (CONTEXT_ENTRY_LAYER_IDS.includes(change.layerId)) return false; diff --git a/src/ui/contextControls.js b/src/ui/contextControls.js index 3c297f277..7acb31ef5 100644 --- a/src/ui/contextControls.js +++ b/src/ui/contextControls.js @@ -51,6 +51,8 @@ export class ContextControls { this._contextRestoreState = null; this._contextLayerReactionPromises = new Set(); this._preservePanelStateDuringLayerClear = false; + /** Layers Context isolation is switching off right now. */ + this._isolatingLayerIds = new Set(); this._userFacingContextNotificationTokens = new Set(); this._clearSelectedLayersPromise = null; this._clearSelectedLayersManagerPromise = null; @@ -60,18 +62,14 @@ export class ContextControls { if (!this.destroyed) this.actions.showToast(...args); } /** - * Whether Context isolation switched this layer off and leaving the mode - * restores it. Such a layer is suspended, not switched off by the user: a - * layer the user switched during the session is theirs again. + * Whether Context isolation is switching this layer off right now: it is + * suspended, and leaving the mode restores it. A switch-off by the user, + * inside the mode or out of it, is not. * @param {string} layerId Layer id. * @returns {boolean} */ suspendsLayer(layerId) { - const snapshot = this._contextSessionSnapshot; - return Boolean( - snapshot?.enabledLayerIds?.has(layerId) && - !snapshot.userSwitched?.has(layerId), - ); + return this._isolatingLayerIds.has(layerId); } setClearBusy(...args) { if (!this.destroyed) this.actions.setClearBusy(...args); diff --git a/src/ui/contextControls.test.mjs b/src/ui/contextControls.test.mjs index caaba2060..2bceb7e75 100644 --- a/src/ui/contextControls.test.mjs +++ b/src/ui/contextControls.test.mjs @@ -1,7 +1,6 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { ContextControls } from './contextControls.js'; -import { recordContextSessionUserChange } from '../contextModePolicy.js'; class Button extends EventTarget { constructor() { @@ -155,7 +154,7 @@ test('a rejected tracked reaction settles without an unhandled rejection', async assert.equal(controls._contextLayerReactionPromises.size, 0); }); -test('a layer Context isolation switches off is suspended until the mode is left', async (t) => { +test('only a layer Context isolation is switching off is suspended', async (t) => { const { controls } = fixture(t); const enabled = new Set(['cctv']); // What the CCTV panel is told at each visibility change of its layer. @@ -168,6 +167,7 @@ test('a layer Context isolation switches off is suspended until the mode is left isEnabled: (layerId) => enabled.has(layerId), isEffectivelyEnabled: (layerId) => enabled.has(layerId), async setEnabled(layerId, next) { + // The layer tells its panel it is off while it is being disabled. if (next) enabled.add(layerId); else enabled.delete(layerId); seen.push([layerId, next, controls.suspendsLayer(layerId)]); @@ -181,47 +181,28 @@ test('a layer Context isolation switches off is suspended until the mode is left controls._captureContextSessionSnapshot(); await controls._clearLayersOutsideContextMode('flights'); assert.deepEqual(seen, [['cctv', false, true]]); - assert.equal(controls.suspendsLayer('cctv'), true); - assert.equal(controls.suspendsLayer('street-level'), false); + assert.equal(controls.suspendsLayer('cctv'), false, 'only while isolating'); + // Inside the mode the user turns CCTV back on, then off: their own. + await controls._dataManager.setEnabled('cctv', true); + await controls._dataManager.setEnabled('cctv', false); + assert.deepEqual(seen.at(-1), ['cctv', false, false]); await controls._restoreContextSession(); assert.equal(enabled.has('cctv'), true); - assert.equal(controls.suspendsLayer('cctv'), false); - // A switch-off after the mode is left is the user's own. + // A switch-off after the mode is left is the user's own too. await controls._dataManager.setEnabled('cctv', false); assert.deepEqual(seen.at(-1), ['cctv', false, false]); }); -test('a layer the user switches inside the mode is no longer suspended', async (t) => { +test('a failed isolation leaves no layer suspended', async (t) => { const { controls } = fixture(t); - const enabled = new Set(['cctv']); controls._dataManager = { layers: new Map([['cctv', {}]]), - getEnabledLayerIds: () => [...enabled], - getLayerParams: () => null, - isEffectivelyEnabled: (layerId) => enabled.has(layerId), - isEnabled: (layerId) => enabled.has(layerId), - async setEnabled(layerId, next) { - if (next) enabled.add(layerId); - else enabled.delete(layerId); - return true; + isEffectivelyEnabled: () => true, + isEnabled: () => true, + setEnabled: async () => { + throw new Error('disable failed'); }, }; - controls._captureContextSessionSnapshot(); - await controls._clearLayersOutsideContextMode('flights'); - assert.equal(controls.suspendsLayer('cctv'), true); - // Inside Contacts the user turns CCTV back on, then off again: the panel - // must close like any other switch-off. - for (const next of [true, false]) { - recordContextSessionUserChange({ - snapshot: controls._contextSessionSnapshot, - change: { - type: 'visibility', - origin: 'user', - layerId: 'cctv', - enabled: next, - }, - effectiveContextMode: 'flights', - }); - } + await assert.rejects(controls._clearLayersOutsideContextMode('flights')); assert.equal(controls.suspendsLayer('cctv'), false); }); diff --git a/src/ui/contextSession.js b/src/ui/contextSession.js index 3f1f2eeb1..936fc2cb1 100644 --- a/src/ui/contextSession.js +++ b/src/ui/contextSession.js @@ -36,8 +36,6 @@ export function _captureContextSessionSnapshot({ excludeLayerIds = [] } = {}) { ), userAdded: new Set(), userRemoved: new Set(), - // Every layer the user switched during the session, either way. - userSwitched: new Set(), params, }; } @@ -151,7 +149,6 @@ export async function _restoreContextSession({ enabledLayerIds: new Set(restoreState.enabledLayerIds), userAdded: new Set(), userRemoved: new Set(), - userSwitched: new Set(), params: snapshot.params, }; } diff --git a/src/ui/contextTransactions.js b/src/ui/contextTransactions.js index e8a69b5fc..1f17faeef 100644 --- a/src/ui/contextTransactions.js +++ b/src/ui/contextTransactions.js @@ -240,25 +240,35 @@ export async function _clearLayersOutsideContextMode( ) { const allowed = contextAllowedLayerIds(mode); const pending = []; - for (const [layerId] of this._dataManager.layers || []) { - // Effective visibility: a disallowed layer still mid-ENABLING must be - // isolated too, or it settles ON inside the exclusive mode. - if ( - !allowed.has(layerId) && - this._dataManager.isEffectivelyEnabled(layerId) - ) { - pending.push({ - layerId, - transition: this._dataManager.setEnabled(layerId, false, { - notificationToken, - ...(signal ? { signal } : {}), - }), - }); + // Panels read this while their layer goes off (suspendsLayer): a layer + // isolation switches off is suspended, not switched off by the user. + const isolating = []; + let results; + try { + for (const [layerId] of this._dataManager.layers || []) { + // Effective visibility: a disallowed layer still mid-ENABLING must be + // isolated too, or it settles ON inside the exclusive mode. + if ( + !allowed.has(layerId) && + this._dataManager.isEffectivelyEnabled(layerId) + ) { + if (!this._isolatingLayerIds.has(layerId)) { + this._isolatingLayerIds.add(layerId); + isolating.push(layerId); + } + pending.push({ + layerId, + transition: this._dataManager.setEnabled(layerId, false, { + notificationToken, + ...(signal ? { signal } : {}), + }), + }); + } } + results = await Promise.all(pending.map(({ transition }) => transition)); + } finally { + for (const layerId of isolating) this._isolatingLayerIds.delete(layerId); } - const results = await Promise.all( - pending.map(({ transition }) => transition), - ); const failed = pending .filter( ({ layerId }, index) =>