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:
@@ -41,7 +41,7 @@ import AccountPanel from "./AccountPanel";
|
||||
import { useConfirm } from "./confirmContext";
|
||||
import Tabs from "./ui/Tabs";
|
||||
import { useTabPanel } from "./ui/tabPanel";
|
||||
import { useAccountPersistedState } from "../lib/storage";
|
||||
import { useTabPersistedState } from "../lib/storage";
|
||||
import { PageToolbar } from "./ui/PageShell";
|
||||
import { useMe } from "../lib/useMe";
|
||||
import { notifyActionError, notifyKnownFailure } from "../lib/actionErrors";
|
||||
@@ -69,7 +69,7 @@ const ROW_OPEN =
|
||||
export default function AdminUsers() {
|
||||
const me = useMe();
|
||||
const { t } = useTranslation();
|
||||
const [tab, setTab] = useAccountPersistedState(ADMIN_USERS_TAB_KEY, "roles");
|
||||
const [tab, setTab] = useTabPersistedState(ADMIN_USERS_TAB_KEY, "roles");
|
||||
const panel = useTabPanel();
|
||||
const tabs = [
|
||||
{ id: "roles", label: t("users.tabs.roles") },
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Switch } from "./ui/form";
|
||||
import SegmentedToggle from "./ui/SegmentedToggle";
|
||||
import FilterSelect from "./ui/FilterSelect";
|
||||
import { DraftSaveBar, type SaveState } from "./ui/DraftSaveBar";
|
||||
import { LS, useAccountPersistedState } from "../lib/storage";
|
||||
import { LS, useTabPersistedState } from "../lib/storage";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { Button } from "./ui/Button";
|
||||
import { CheckRow } from "./ui/NativeControls";
|
||||
@@ -56,7 +56,7 @@ export default function ConfigPanel() {
|
||||
const [reseedToken, setReseedToken] = useState(0);
|
||||
// One tab per config group (persisted). Called before the loading guard so hook order is
|
||||
// stable; the active id is clamped to a real group at render time once data has loaded.
|
||||
const [tab, setTab] = useAccountPersistedState(LS.configTab);
|
||||
const [tab, setTab] = useTabPersistedState(LS.configTab);
|
||||
|
||||
// A stable signature of the config SHAPE — changes only when keys are added/removed, not when an
|
||||
// unchanged refetch produces a new baseline object. Re-seed the draft on a shape change (initial
|
||||
|
||||
@@ -10,7 +10,7 @@ import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme";
|
||||
import { api, HttpError, type Me } from "../lib/api";
|
||||
import { displayName, roleKey } from "../lib/api/account";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { LS, useAccountPersistedState } from "../lib/storage";
|
||||
import { LS, useAccountPersistedState, useTabPersistedState } from "../lib/storage";
|
||||
import Avatar from "./ui/Avatar";
|
||||
import { notify, type NotifSettings } from "../lib/notifications";
|
||||
import Tooltip from "./ui/Tooltip";
|
||||
@@ -68,7 +68,7 @@ export default function SettingsPanel() {
|
||||
const prefs = usePrefs();
|
||||
// "Connect YouTube" onboarding lives in WizardProvider (the Account tab's reconnect button).
|
||||
const { openWizard } = useWizard();
|
||||
const [tabRaw, setTab] = useAccountPersistedState(LS.settingsTab, "appearance");
|
||||
const [tabRaw, setTab] = useTabPersistedState(LS.settingsTab, "appearance");
|
||||
const panel = useTabPanel();
|
||||
// Clamp at render so a stale/removed tab id falls back to Appearance.
|
||||
const tab: TabId = TABS.some((tabItem) => tabItem.id === tabRaw)
|
||||
|
||||
@@ -8,7 +8,7 @@ import { formatEta, formatNumber, formatUnit, quotaActionLabel } from "../lib/fo
|
||||
import { notify } from "../lib/notifications";
|
||||
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
||||
import { useWizard } from "./wizardContext";
|
||||
import { LS, useAccountPersistedState } from "../lib/storage";
|
||||
import { LS, useTabPersistedState } from "../lib/storage";
|
||||
import Tooltip from "./ui/Tooltip";
|
||||
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||
import FilterChipGroup from "./ui/FilterChipGroup";
|
||||
@@ -34,7 +34,7 @@ export default function Stats() {
|
||||
const me = useMe();
|
||||
const { t } = useTranslation();
|
||||
const isAdmin = me.role === "admin";
|
||||
const [tabRaw, setTab] = useAccountPersistedState(LS.statsTab, "overview");
|
||||
const [tabRaw, setTab] = useTabPersistedState(LS.statsTab, "overview");
|
||||
// Clamp at render: only admins may land on the System tab (covers a stale stored value).
|
||||
const tab: StatsTab = tabRaw === "system" && isAdmin ? "system" : "overview";
|
||||
const panel = useTabPanel();
|
||||
|
||||
@@ -57,7 +57,7 @@ vi.mock("../lib/useMe", () => ({ useMe: () => meMock }));
|
||||
vi.mock("./ui/Tabs", () => ({ default: () => null }));
|
||||
vi.mock("../lib/storage", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../lib/storage")>()),
|
||||
useAccountPersistedState: () => ["access", () => {}],
|
||||
useTabPersistedState: () => ["access", () => {}],
|
||||
}));
|
||||
|
||||
const listMock = vi.fn();
|
||||
|
||||
@@ -48,7 +48,7 @@ vi.mock("../lib/useMe", () => ({ useMe: () => meMock }));
|
||||
vi.mock("./ui/Tabs", () => ({ default: () => null }));
|
||||
vi.mock("../lib/storage", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../lib/storage")>()),
|
||||
useAccountPersistedState: () => ["access", () => {}],
|
||||
useTabPersistedState: () => ["access", () => {}],
|
||||
}));
|
||||
|
||||
const pendingMock = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user