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 Tabs, { usePersistedTab } from "./Tabs";
|
||||
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
|
||||
// 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.
|
||||
// Email/SMTP) stay together.
|
||||
const GROUP_ORDER = ["access", "email", "youtube", "google", "quota", "backfill", "shorts", "batch", "downloads", "plex"];
|
||||
type SaveState = "idle" | "saving" | "saved" | "error";
|
||||
|
||||
export default function ConfigPanel() {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -40,7 +40,7 @@ export default function ConfigPanel() {
|
|||
const [saveState, setSaveState] = useState<SaveState>("idle");
|
||||
// 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] = usePersistedTab("siftlode.configTab");
|
||||
const [tab, setTab] = usePersistedTab(LS.configTab);
|
||||
|
||||
// Re-seed the draft whenever the server data changes (initial load + after a save/reset).
|
||||
useEffect(() => {
|
||||
|
|
@ -110,9 +110,14 @@ export default function ConfigPanel() {
|
|||
});
|
||||
const plexLibs = (draft["plex_libraries"] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
||||
const togglePlexLib = (key: string) => {
|
||||
const next = plexLibs.includes(key)
|
||||
? plexLibs.filter((k) => k !== key)
|
||||
: [...plexLibs, key];
|
||||
// An empty list means "all libraries" — every box renders checked. Unchecking one from that
|
||||
// state must select all-except-this, so expand "all" to the explicit section set first;
|
||||
// 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(",") }));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import { api, type SchedulerJob, type SchedulerStatus } from "../lib/api";
|
||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
import { relativeTime } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
|
@ -309,6 +310,7 @@ function Stat({
|
|||
export default function Scheduler() {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const confirm = useConfirm();
|
||||
// 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).
|
||||
const q = useLiveQuery<SchedulerStatus>(["scheduler"], api.schedulerStatus, {
|
||||
|
|
@ -411,7 +413,15 @@ export default function Scheduler() {
|
|||
</Tooltip>
|
||||
<Tooltip hint={t("scheduler.purgeDiscoveryHint")}>
|
||||
<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}
|
||||
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 Tooltip from "./Tooltip";
|
||||
import { Section, SettingRow, Switch } from "./ui/form";
|
||||
import { DraftSaveBar } from "./ui/DraftSaveBar";
|
||||
import { DraftSaveBar, type SaveState } from "./ui/DraftSaveBar";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
|
||||
// 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).
|
||||
// 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.
|
||||
export type PrefsSaveState = "idle" | "saving" | "saved" | "error";
|
||||
export type PrefsSaveState = SaveState;
|
||||
export interface PrefsController {
|
||||
theme: ThemePrefs;
|
||||
setTheme: (t: ThemePrefs) => void;
|
||||
|
|
|
|||
|
|
@ -103,5 +103,6 @@
|
|||
},
|
||||
"purgeDiscovery": "Entdeckung leeren",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,5 +103,6 @@
|
|||
},
|
||||
"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.",
|
||||
"purgeConfirmTitle": "Purge discovery content?",
|
||||
"purgedDiscovery": "Removed {{count}} discovery items"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,5 +103,6 @@
|
|||
},
|
||||
"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.",
|
||||
"purgeConfirmTitle": "Felfedezés-tartalom törlése?",
|
||||
"purgedDiscovery": "{{count}} felfedezés-elem eltávolítva"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const LS = {
|
|||
channelsTable: "siftlode.channelsTable",
|
||||
channelDiscoveryTable: "siftlode.channelDiscoveryTable",
|
||||
settingsTab: "siftlode.settingsTab",
|
||||
configTab: "siftlode.configTab",
|
||||
statsTab: "siftlode.statsTab",
|
||||
adminUsersTab: "siftlode.adminUsersTab",
|
||||
navCollapsed: "siftlode.navCollapsed",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue