feat(nav): tabs, list views and name searches are named in the address
Settings, Configuration and Users name their tab (stab/cfgtab/utab); a link's tab wins and a tab pre-selected while its page is on screen now switches it. A playlist's sort, filters and grouping (plsort/plf/plgroup), the Files sort (fsort, always written) and the name searches of Channels, Files, Playlists and Plex (cq/fq/plq/pq+pqf) reopen from the address; the searches are written once the typing pauses.
This commit is contained in:
@@ -16,6 +16,8 @@ import SegmentedToggle from "../../components/ui/SegmentedToggle";
|
||||
import FilterSelect from "../../components/ui/FilterSelect";
|
||||
import { DraftSaveBar, type SaveState } from "../../components/ui/DraftSaveBar";
|
||||
import { LS, useTabPersistedState } from "../../lib/storage";
|
||||
import { CONFIG_TAB_KEY } from "../../lib/configTab";
|
||||
import { useUrlMirror } from "../../lib/useUrlMirror";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { CheckRow } from "../../components/ui/NativeControls";
|
||||
@@ -95,7 +97,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] = useTabPersistedState(LS.configTab);
|
||||
const [tab, setTab] = useTabPersistedState(LS.configTab, "", CONFIG_TAB_KEY);
|
||||
|
||||
// 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
|
||||
@@ -181,6 +183,17 @@ export default function ConfigPanel() {
|
||||
setDraft((d) => ({ ...d, plex_libraries: next.join(",") }));
|
||||
};
|
||||
|
||||
// The groups in the registry's sections, in order (sixteen tabs wrapped to three rows at 133%
|
||||
// zoom, so they are a side list under four headings, as Settings has).
|
||||
const groupKeys = data?.sections.flatMap((s) => s.groups) ?? [];
|
||||
// Clamp the persisted tab to a real group (the registry can change between sessions). The
|
||||
// tab pill carries a dot when that group has unsaved edits, so a draft in a hidden tab is
|
||||
// never silently lost behind the global Save bar.
|
||||
const activeGroup = groupKeys.includes(tab) ? tab : groupKeys[0];
|
||||
// The address names the group on screen (always: see SettingsPanel); until the registry is in, the
|
||||
// one it was opened on.
|
||||
useUrlMirror({ [CONFIG_TAB_KEY]: (data ? activeGroup : tab) || null });
|
||||
|
||||
if (q.isError && !data) {
|
||||
return (
|
||||
<StateMessage tone="error" onRetry={() => q.refetch()}>
|
||||
@@ -192,13 +205,6 @@ export default function ConfigPanel() {
|
||||
return <LoadingState label={t("config.loading")} />;
|
||||
}
|
||||
|
||||
// The groups in the registry's sections, in order (sixteen tabs wrapped to three rows at 133%
|
||||
// zoom, so they are a side list under four headings, as Settings has).
|
||||
const groupKeys = data.sections.flatMap((s) => s.groups);
|
||||
// Clamp the persisted tab to a real group (the registry can change between sessions). The
|
||||
// tab pill carries a dot when that group has unsaved edits, so a draft in a hidden tab is
|
||||
// never silently lost behind the global Save bar.
|
||||
const activeGroup = groupKeys.includes(tab) ? tab : groupKeys[0];
|
||||
const dirtyByGroup = new Set(dirtyItems.map((i) => i.group).filter(Boolean));
|
||||
const groupTabs = data.sections.flatMap((s) =>
|
||||
s.groups.map((g) => ({
|
||||
|
||||
Reference in New Issue
Block a user