diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx index d916f884..5a00ed3f 100644 --- a/artifacts/tx-os/src/pages/executive-meetings.tsx +++ b/artifacts/tx-os/src/pages/executive-meetings.tsx @@ -349,7 +349,9 @@ const ROW_COLOR_OPTIONS: { key: string; bg: string; border: string; label: strin const COLS_STORAGE_KEY = "em-schedule-cols-v1"; const ROW_COLORS_STORAGE_KEY = "em-schedule-row-colors-v1"; const HIGHLIGHT_STORAGE_KEY = "em-current-meeting-highlight-v1"; -const EDIT_MODE_STORAGE_KEY = "em-schedule-edit-mode-v1"; +// #484: edit-mode is intentionally NOT persisted. It always starts off +// when the Meetings page mounts so users never come back to a stuck +// half-open editor state after navigating away. type HighlightPrefs = { enabled: boolean; color: string }; const DEFAULT_HIGHLIGHT_PREFS: HighlightPrefs = { @@ -1075,46 +1077,24 @@ function ScheduleSection({ // (no edit affordances). When a user with edit permission flips it on, // every per-cell editor and per-row action (merge, color swatch, drag // handle, add/delete row, inline edit, column drag, column resize) is - // re-enabled. We persist the user's last choice in localStorage so the - // toggle survives reloads + date changes, but we *also* always start in - // view mode for users without permission so they never see a stale "on" - // state from a previous session where they had access. Hydrating the - // localStorage value happens inside an effect to keep SSR-safe and to - // avoid reading from a non-mutator's namespace. + // re-enabled. // - // The storage key is namespaced by the current user's id so a shared - // browser profile can't leak one editor's last toggle state into the - // next account that signs in. When userId is null (no /me yet) we - // fall back to view mode without touching storage. - const editModeStorageKey = useMemo( - () => (userId != null ? `${EDIT_MODE_STORAGE_KEY}:${userId}` : null), - [userId], - ); + // #484: the toggle is NOT persisted. Every fresh page mount — whether + // from a reload or from navigating back to Meetings from another app — + // starts in view mode so the user never returns to a stuck half-open + // editor state. The in-memory state still flips normally during a + // single visit. We also force back to false the moment a user loses + // mutate permission so they never see a stale "on". const [editMode, setEditModeState] = useState(false); useEffect(() => { - if (!canMutate || !editModeStorageKey) { - setEditModeState(false); - return; - } - try { - const raw = window.localStorage.getItem(editModeStorageKey); - setEditModeState(raw === "true"); - } catch { - // localStorage may be unavailable (private mode); fall through to - // the default false. - } - }, [canMutate, editModeStorageKey]); + if (!canMutate) setEditModeState(false); + }, [canMutate]); const setEditMode = useCallback( (next: boolean) => { - if (!canMutate || !editModeStorageKey) return; + if (!canMutate) return; setEditModeState(next); - try { - window.localStorage.setItem(editModeStorageKey, next ? "true" : "false"); - } catch { - // Persistence is best-effort; the in-memory state still flips. - } }, - [canMutate, editModeStorageKey], + [canMutate], ); // Anything the user can mutate must AND with editMode. We pass this // derived flag down to every row + cell so they don't each have to diff --git a/artifacts/tx-os/tests/executive-meetings-edit-toggle.spec.mjs b/artifacts/tx-os/tests/executive-meetings-edit-toggle.spec.mjs index b5cf7bab..80c827c4 100644 --- a/artifacts/tx-os/tests/executive-meetings-edit-toggle.spec.mjs +++ b/artifacts/tx-os/tests/executive-meetings-edit-toggle.spec.mjs @@ -12,7 +12,7 @@ async function loginViaUi(page, username, password) { ]); } -test("Schedule: Edit toggle hides editing affordances by default and reveals them when on", async ({ +test("Schedule: Edit toggle hides editing affordances by default, reveals them when on, and resets to view mode on reload / re-navigation (#484)", async ({ page, }) => { await page.addInitScript(() => { @@ -24,27 +24,7 @@ test("Schedule: Edit toggle hides editing affordances by default and reveals the }); await loginViaUi(page, "admin", "admin123"); - // Make sure we always start the test in view mode regardless of any - // previously-persisted toggle state from another run. We clear AFTER - // login so the cleanup isn't undone by the initScript on every nav. await page.goto("/executive-meetings"); - // The toggle's storage key is namespaced per-user - // (`em-schedule-edit-mode-v1:`), so we wipe every entry that - // starts with the base key to make sure we begin in view mode no - // matter who admin's userId resolves to in this run. - await page.evaluate(() => { - try { - const keys = Object.keys(window.localStorage); - for (const k of keys) { - if (k.startsWith("em-schedule-edit-mode-v1")) { - window.localStorage.removeItem(k); - } - } - } catch { - /* ignore */ - } - }); - await page.reload(); // Toggle is rendered (admin has edit permission), and starts in // pressed=false / view mode. @@ -68,16 +48,30 @@ test("Schedule: Edit toggle hides editing affordances by default and reveals the // The "+ Add meeting" row button is back. await expect(page.getByTestId("em-add-row-button")).toBeVisible(); - // Persist + survive a reload: the toggle should still be on. + // #484: edit mode is intentionally NOT persisted. Reloading the + // page must drop the toggle back to view mode. await page.reload(); const toggleAfterReload = page.getByTestId("em-edit-mode-toggle"); - await expect(toggleAfterReload).toHaveAttribute("aria-pressed", "true"); - await expect(page.getByTestId("em-add-row-button")).toBeVisible(); - - // Toggle back off — affordances disappear again. - await toggleAfterReload.click(); await expect(toggleAfterReload).toHaveAttribute("aria-pressed", "false"); await expect(page.getByTestId("em-add-row-button")).toHaveCount(0); + + // #484: navigating away to another app and coming back also resets + // the toggle (the page unmounts on route change). + await toggleAfterReload.click(); + await expect(toggleAfterReload).toHaveAttribute("aria-pressed", "true"); + await page.goto("/"); + await page.goto("/executive-meetings"); + const toggleAfterRenav = page.getByTestId("em-edit-mode-toggle"); + await expect(toggleAfterRenav).toHaveAttribute("aria-pressed", "false"); + await expect(page.getByTestId("em-add-row-button")).toHaveCount(0); + + // Within a single visit the toggle still works normally. + await toggleAfterRenav.click(); + await expect(toggleAfterRenav).toHaveAttribute("aria-pressed", "true"); + await expect(page.getByTestId("em-add-row-button")).toBeVisible(); + await toggleAfterRenav.click(); + await expect(toggleAfterRenav).toHaveAttribute("aria-pressed", "false"); + await expect(page.getByTestId("em-add-row-button")).toHaveCount(0); }); test("Schedule: turning edit mode OFF cancels any open inline editor and discards the draft", async ({