fix(ui): a page's sub-tab is remembered per browser tab, not forever
Settings, Configuration, Statistics and Users kept the last sub-tab in localStorage, so every fresh tab opened Configuration on "Plex" as if that were its home. useTabPersistedState keeps it in sessionStorage (account-scoped): a reload and a trip to another page keep it, a new tab starts on the first sub-tab. The pre-selects (a Review link, the Google-link return) write the same store; a value an older build left in localStorage is cleared on first read.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { LS, setAccountRaw } from "./storage";
|
||||
import { LS, setTabRaw } from "./storage";
|
||||
|
||||
// The admin Users page's persisted-tab key + the Access-requests tab id, plus a helper to
|
||||
// pre-select that tab before navigating there (a notification's "Review" link uses it —
|
||||
// useAccountPersistedState reads the key on mount, and the page mounts fresh on navigation).
|
||||
// useTabPersistedState reads the key on mount, and the page mounts fresh on navigation).
|
||||
//
|
||||
// Kept out of AdminUsers.tsx so callers (App, NotificationsPanel) can pre-select the tab
|
||||
// WITHOUT statically importing the — now lazy-loaded — admin page, which would otherwise pull
|
||||
@@ -11,5 +11,5 @@ export const ADMIN_USERS_TAB_KEY = LS.adminUsersTab;
|
||||
const ADMIN_USERS_ACCESS_TAB = "access";
|
||||
|
||||
export function focusAccessRequestsTab(): void {
|
||||
setAccountRaw(ADMIN_USERS_TAB_KEY, ADMIN_USERS_ACCESS_TAB);
|
||||
setTabRaw(ADMIN_USERS_TAB_KEY, ADMIN_USERS_ACCESS_TAB);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ export const LS = {
|
||||
channelDiscoveryCardSize: "siftlode.channelDiscoveryCardSize",
|
||||
channelsTable: "siftlode.channelsTable",
|
||||
channelDiscoveryTable: "siftlode.channelDiscoveryTable",
|
||||
// A page's sub-tab: sessionStorage, per browser tab (useTabPersistedState), like adminUsersTab and
|
||||
// statsTab below.
|
||||
settingsTab: "siftlode.settingsTab",
|
||||
configTab: "siftlode.configTab",
|
||||
auditTable: "siftlode.auditTable",
|
||||
@@ -174,6 +176,61 @@ export function useAccountPersistedState(
|
||||
return [value, set];
|
||||
}
|
||||
|
||||
// --- a page's sub-tab, per browser tab ---------------------------------------------------------
|
||||
|
||||
// Which sub-tab a page was left on (Settings, Configuration, Statistics, Users) is kept for THIS browser
|
||||
// tab only — sessionStorage, account-scoped: it survives a reload and a trip to another page, and a new
|
||||
// tab opens the page on its first sub-tab (the owner, E31: a position remembered forever in
|
||||
// localStorage made Configuration open on "Plex" in every fresh tab, as if that were its home).
|
||||
|
||||
/** The sub-tab stored for this browser tab, or null. */
|
||||
function getTabRaw(base: string): string | null {
|
||||
const k = accountKey(base);
|
||||
try {
|
||||
return k ? sessionStorage.getItem(k) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pre-select a page's sub-tab before navigating there (a "Review" link, the Google-link return). */
|
||||
export function setTabRaw(base: string, value: string): void {
|
||||
const k = accountKey(base);
|
||||
if (!k) return;
|
||||
try {
|
||||
sessionStorage.setItem(k, value);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/** `useAccountPersistedState` for a page's sub-tab, kept per browser tab (see above). */
|
||||
export function useTabPersistedState(
|
||||
base: string,
|
||||
fallback = "",
|
||||
): [string, (value: string) => void] {
|
||||
const [value, setValue] = useState<string>(() => {
|
||||
// Before E31 the position lived in localStorage; a value left there would never be read again.
|
||||
const old = accountKey(base);
|
||||
if (old) {
|
||||
try {
|
||||
localStorage.removeItem(old);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return getTabRaw(base) ?? fallback;
|
||||
});
|
||||
const set = useCallback(
|
||||
(next: string) => {
|
||||
setValue(next);
|
||||
setTabRaw(base, next);
|
||||
},
|
||||
[base],
|
||||
);
|
||||
return [value, set];
|
||||
}
|
||||
|
||||
/** A per-account persisted JSON OBJECT: `patch(partial)` merges + saves, unknown/missing keys fall
|
||||
* back to `defaults` (via readAccountMerged) so adding a field later is safe. Used for bundles of
|
||||
* related per-account settings (e.g. the Plex player prefs: volume, offsets, seek steps, sub style). */
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// E31 (the owner): a page's sub-tab was remembered forever in localStorage, so every fresh browser tab
|
||||
// opened Configuration on "Plex" as if that were its home. The position is now kept per browser tab
|
||||
// (sessionStorage): it survives a reload and a trip to another page, and a new tab starts on the first.
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { ACTIVE_ACCOUNT_KEY } from "./activeAccount";
|
||||
import { setTabRaw, useTabPersistedState } from "./storage";
|
||||
|
||||
beforeEach(() => {
|
||||
sessionStorage.setItem(ACTIVE_ACCOUNT_KEY, "1");
|
||||
});
|
||||
afterEach(() => {
|
||||
sessionStorage.clear();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("a page's sub-tab, per browser tab", () => {
|
||||
it("starts on the fallback, then keeps the picked tab for this browser tab only", () => {
|
||||
const first = renderHook(() => useTabPersistedState("siftlode.configTab", "access"));
|
||||
expect(first.result.current[0]).toBe("access");
|
||||
act(() => first.result.current[1]("plex"));
|
||||
expect(sessionStorage.getItem("siftlode.configTab.1")).toBe("plex");
|
||||
expect(localStorage.getItem("siftlode.configTab.1")).toBeNull();
|
||||
// The page mounts again in this tab (a reload, or back from another page): it is where it was.
|
||||
first.unmount();
|
||||
expect(
|
||||
renderHook(() => useTabPersistedState("siftlode.configTab", "access")).result.current[0],
|
||||
).toBe("plex");
|
||||
});
|
||||
|
||||
it("ignores a position left in localStorage by an older build, and clears it", () => {
|
||||
localStorage.setItem("siftlode.configTab.1", "plex");
|
||||
const { result } = renderHook(() => useTabPersistedState("siftlode.configTab", "access"));
|
||||
expect(result.current[0]).toBe("access");
|
||||
expect(localStorage.getItem("siftlode.configTab.1")).toBeNull();
|
||||
});
|
||||
|
||||
it("a pre-selected tab is the one the page opens on", () => {
|
||||
setTabRaw("siftlode.adminUsersTab", "access");
|
||||
const { result } = renderHook(() => useTabPersistedState("siftlode.adminUsersTab", "roles"));
|
||||
expect(result.current[0]).toBe("access");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user