fix(admin-ui): Plex library toggle inversion + Purge-discovery confirm (+ cleanups)
Bugs: - ConfigPanel (CB1): unchecking a Plex library from the "all" (empty) state made that library the ONLY selected one instead of all-except-it, because the toggle ADDED the key to an empty list. Expand "all" to the explicit section set before toggling. E2E-verified: uncheck from all → all-except-one. - Scheduler (SB1): "Purge discovery" bulk-deleted search videos/videos/channels immediately with no confirm (every AdminUsers destructive action uses useConfirm). Wrapped in a danger useConfirm dialog (+ trilingual purgeConfirmTitle key). E2E-verified: dialog appears, Cancel aborts. Cleanups (behavior-neutral): - Register LS.configTab; ConfigPanel uses it instead of the bare "siftlode.configTab". - ConfigPanel + SettingsPanel import SaveState from ui/DraftSaveBar instead of redeclaring the union (PrefsSaveState is now an alias). tsc green, re-review clean, localdev boots.
This commit is contained in:
parent
a6ffcf9577
commit
3d00d75863
7 changed files with 28 additions and 9 deletions
|
|
@ -7,7 +7,8 @@ import { notify } from "../lib/notifications";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
import Tabs, { usePersistedTab } from "./Tabs";
|
import Tabs, { usePersistedTab } from "./Tabs";
|
||||||
import { Switch } from "./ui/form";
|
import { Switch } from "./ui/form";
|
||||||
import { DraftSaveBar } from "./ui/DraftSaveBar";
|
import { DraftSaveBar, type SaveState } from "./ui/DraftSaveBar";
|
||||||
|
import { LS } from "../lib/storage";
|
||||||
|
|
||||||
// Admin Configuration page: edit DB-overridable operational settings (registry-driven from the
|
// Admin Configuration page: edit DB-overridable operational settings (registry-driven from the
|
||||||
// backend — adding a key there makes it appear here automatically). Edits are drafted and
|
// backend — adding a key there makes it appear here automatically). Edits are drafted and
|
||||||
|
|
@ -15,7 +16,6 @@ import { DraftSaveBar } from "./ui/DraftSaveBar";
|
||||||
// the server until Save. Group order is fixed where known so logically related settings (e.g.
|
// the server until Save. Group order is fixed where known so logically related settings (e.g.
|
||||||
// Email/SMTP) stay together.
|
// Email/SMTP) stay together.
|
||||||
const GROUP_ORDER = ["access", "email", "youtube", "google", "quota", "backfill", "shorts", "batch", "downloads", "plex"];
|
const GROUP_ORDER = ["access", "email", "youtube", "google", "quota", "backfill", "shorts", "batch", "downloads", "plex"];
|
||||||
type SaveState = "idle" | "saving" | "saved" | "error";
|
|
||||||
|
|
||||||
export default function ConfigPanel() {
|
export default function ConfigPanel() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -40,7 +40,7 @@ export default function ConfigPanel() {
|
||||||
const [saveState, setSaveState] = useState<SaveState>("idle");
|
const [saveState, setSaveState] = useState<SaveState>("idle");
|
||||||
// One tab per config group (persisted). Called before the loading guard so hook order is
|
// 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.
|
// stable; the active id is clamped to a real group at render time once data has loaded.
|
||||||
const [tab, setTab] = usePersistedTab("siftlode.configTab");
|
const [tab, setTab] = usePersistedTab(LS.configTab);
|
||||||
|
|
||||||
// Re-seed the draft whenever the server data changes (initial load + after a save/reset).
|
// Re-seed the draft whenever the server data changes (initial load + after a save/reset).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -110,9 +110,14 @@ export default function ConfigPanel() {
|
||||||
});
|
});
|
||||||
const plexLibs = (draft["plex_libraries"] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
const plexLibs = (draft["plex_libraries"] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
||||||
const togglePlexLib = (key: string) => {
|
const togglePlexLib = (key: string) => {
|
||||||
const next = plexLibs.includes(key)
|
// An empty list means "all libraries" — every box renders checked. Unchecking one from that
|
||||||
? plexLibs.filter((k) => k !== key)
|
// state must select all-except-this, so expand "all" to the explicit section set first;
|
||||||
: [...plexLibs, key];
|
// otherwise the toggle would ADD the key and leave it as the ONLY selected library.
|
||||||
|
const allKeys = plexResult?.sections?.map((s) => s.key) ?? [];
|
||||||
|
const current = plexLibs.length === 0 ? allKeys : plexLibs;
|
||||||
|
const next = current.includes(key)
|
||||||
|
? current.filter((k) => k !== key)
|
||||||
|
: [...current, key];
|
||||||
setDraft((d) => ({ ...d, plex_libraries: next.join(",") }));
|
setDraft((d) => ({ ...d, plex_libraries: next.join(",") }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { api, type SchedulerJob, type SchedulerStatus } from "../lib/api";
|
import { api, type SchedulerJob, type SchedulerStatus } from "../lib/api";
|
||||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||||
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
import { relativeTime } from "../lib/format";
|
import { relativeTime } from "../lib/format";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
|
|
@ -309,6 +310,7 @@ function Stat({
|
||||||
export default function Scheduler() {
|
export default function Scheduler() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
const confirm = useConfirm();
|
||||||
// Poll faster while any job is running so live progress is smooth, then ease back when
|
// Poll faster while any job is running so live progress is smooth, then ease back when
|
||||||
// idle. Derived from the freshest data by react-query itself (see useLiveQuery).
|
// idle. Derived from the freshest data by react-query itself (see useLiveQuery).
|
||||||
const q = useLiveQuery<SchedulerStatus>(["scheduler"], api.schedulerStatus, {
|
const q = useLiveQuery<SchedulerStatus>(["scheduler"], api.schedulerStatus, {
|
||||||
|
|
@ -411,7 +413,15 @@ export default function Scheduler() {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip hint={t("scheduler.purgeDiscoveryHint")}>
|
<Tooltip hint={t("scheduler.purgeDiscoveryHint")}>
|
||||||
<button
|
<button
|
||||||
onClick={() => purgeMut.mutate()}
|
onClick={async () => {
|
||||||
|
const ok = await confirm({
|
||||||
|
title: t("scheduler.purgeConfirmTitle"),
|
||||||
|
message: t("scheduler.purgeDiscoveryHint"),
|
||||||
|
confirmLabel: t("scheduler.purgeDiscovery"),
|
||||||
|
danger: true,
|
||||||
|
});
|
||||||
|
if (ok) purgeMut.mutate();
|
||||||
|
}}
|
||||||
disabled={purgeMut.isPending}
|
disabled={purgeMut.isPending}
|
||||||
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
|
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ import Avatar from "./Avatar";
|
||||||
import { notify, type NotifSettings } from "../lib/notifications";
|
import { notify, type NotifSettings } from "../lib/notifications";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
import { Section, SettingRow, Switch } from "./ui/form";
|
import { Section, SettingRow, Switch } from "./ui/form";
|
||||||
import { DraftSaveBar } from "./ui/DraftSaveBar";
|
import { DraftSaveBar, type SaveState } from "./ui/DraftSaveBar";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
|
|
||||||
// The Settings page edits server-persisted preferences as a draft: changes apply locally
|
// The Settings page edits server-persisted preferences as a draft: changes apply locally
|
||||||
// for instant preview but reach the server only on an explicit Save (or revert on Discard).
|
// for instant preview but reach the server only on an explicit Save (or revert on Discard).
|
||||||
// App owns the draft + baseline (so the leave-the-page guard can see it); this controller is
|
// App owns the draft + baseline (so the leave-the-page guard can see it); this controller is
|
||||||
// how the panel reads/writes it. See App.tsx.
|
// how the panel reads/writes it. See App.tsx.
|
||||||
export type PrefsSaveState = "idle" | "saving" | "saved" | "error";
|
export type PrefsSaveState = SaveState;
|
||||||
export interface PrefsController {
|
export interface PrefsController {
|
||||||
theme: ThemePrefs;
|
theme: ThemePrefs;
|
||||||
setTheme: (t: ThemePrefs) => void;
|
setTheme: (t: ThemePrefs) => void;
|
||||||
|
|
|
||||||
|
|
@ -103,5 +103,6 @@
|
||||||
},
|
},
|
||||||
"purgeDiscovery": "Entdeckung leeren",
|
"purgeDiscovery": "Entdeckung leeren",
|
||||||
"purgeDiscoveryHint": "Alle nicht behaltenen Entdeckungsinhalte jetzt entfernen (von niemandem angesehene/gespeicherte Suchergebnisse und erkundete, nicht abonnierte Kanäle) — ohne Karenzzeit.",
|
"purgeDiscoveryHint": "Alle nicht behaltenen Entdeckungsinhalte jetzt entfernen (von niemandem angesehene/gespeicherte Suchergebnisse und erkundete, nicht abonnierte Kanäle) — ohne Karenzzeit.",
|
||||||
|
"purgeConfirmTitle": "Entdeckungsinhalte löschen?",
|
||||||
"purgedDiscovery": "{{count}} Entdeckungselemente entfernt"
|
"purgedDiscovery": "{{count}} Entdeckungselemente entfernt"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,5 +103,6 @@
|
||||||
},
|
},
|
||||||
"purgeDiscovery": "Purge discovery",
|
"purgeDiscovery": "Purge discovery",
|
||||||
"purgeDiscoveryHint": "Remove all un-kept discovery content now (search results nobody watched/saved, and explored-but-unsubscribed channels) — ignoring the grace period.",
|
"purgeDiscoveryHint": "Remove all un-kept discovery content now (search results nobody watched/saved, and explored-but-unsubscribed channels) — ignoring the grace period.",
|
||||||
|
"purgeConfirmTitle": "Purge discovery content?",
|
||||||
"purgedDiscovery": "Removed {{count}} discovery items"
|
"purgedDiscovery": "Removed {{count}} discovery items"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,5 +103,6 @@
|
||||||
},
|
},
|
||||||
"purgeDiscovery": "Felfedezés ürítése",
|
"purgeDiscovery": "Felfedezés ürítése",
|
||||||
"purgeDiscoveryHint": "Most eltávolítja az összes meg nem tartott felfedezés-tartalmat (senki által meg nem nézett/mentett keresési találatok és felfedezett, de nem követett csatornák) — a türelmi időt figyelmen kívül hagyva.",
|
"purgeDiscoveryHint": "Most eltávolítja az összes meg nem tartott felfedezés-tartalmat (senki által meg nem nézett/mentett keresési találatok és felfedezett, de nem követett csatornák) — a türelmi időt figyelmen kívül hagyva.",
|
||||||
|
"purgeConfirmTitle": "Felfedezés-tartalom törlése?",
|
||||||
"purgedDiscovery": "{{count}} felfedezés-elem eltávolítva"
|
"purgedDiscovery": "{{count}} felfedezés-elem eltávolítva"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export const LS = {
|
||||||
channelsTable: "siftlode.channelsTable",
|
channelsTable: "siftlode.channelsTable",
|
||||||
channelDiscoveryTable: "siftlode.channelDiscoveryTable",
|
channelDiscoveryTable: "siftlode.channelDiscoveryTable",
|
||||||
settingsTab: "siftlode.settingsTab",
|
settingsTab: "siftlode.settingsTab",
|
||||||
|
configTab: "siftlode.configTab",
|
||||||
statsTab: "siftlode.statsTab",
|
statsTab: "siftlode.statsTab",
|
||||||
adminUsersTab: "siftlode.adminUsersTab",
|
adminUsersTab: "siftlode.adminUsersTab",
|
||||||
navCollapsed: "siftlode.navCollapsed",
|
navCollapsed: "siftlode.navCollapsed",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue