feat(web): dashboard All Libraries view saves the global scope

The All Libraries entry in the library switcher could render the
cross-library dashboard but rejected its own settings: the modal
refused to open without a library ('Select a specific library...') and
a save sent library_id: '' (server 400). The server now stores a
global scope (separate table; null/absent library_id, empty string
still 400), so:

- openDashboardSettings allows the All view and fetches prefs without
  the library param (reads the global row; previously a loose NULL
  OR-clause matched an arbitrary library's row).
- saveDashboardSettings sends library_id: null from All Libraries.
- The settings modal gains a scope label (#dashboard-scope-label)
  showing which library — or All Libraries — a save will customize
  (user requirement: always know the scope).

Verified end-to-end in headless Chromium: reorder on All Libraries →
save (null) → reload → modal re-opens with the persisted order and the
All Libraries scope label; per-library save lands in its own row
without touching global (SQL-checked).
This commit is contained in:
John O'Keefe committed 2026-10-07 19:51:59 -04:00
1 parent 25d44452c0
commit b1de0da512
3 files changed
+29 -19

No files matched your search

+20 -11
View File
@@ -20,14 +20,13 @@ async function openDashboardSettings(): Promise<void> {
const librarySelect = document.getElementById(
"library-select",
) as HTMLSelectElement;
const libraryId = librarySelect?.value;
if (!libraryId) {
showToast("Select a specific library to customize preferences", "error");
return;
}
const libraryId = librarySelect?.value || "";
// Empty select = the All Libraries view: its preferences are the GLOBAL
// scope — the server returns that row when library_id is absent.
const query = libraryId ? `?library_id=${encodeURIComponent(libraryId)}` : "";
const response = await fetch(
`/api/dashboard/preferences?library_id=${libraryId}`,
`/api/dashboard/preferences${query}`,
{
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
},
@@ -36,6 +35,17 @@ async function openDashboardSettings(): Promise<void> {
if (response.ok) {
const prefs = await response.json();
applyPreferencesToModal(prefs);
// Scope indicator: make it unmistakable what a save will customize.
const scopeEl = document.getElementById("dashboard-scope-label");
if (scopeEl) {
const optionText =
librarySelect?.selectedOptions[0]?.textContent || libraryId;
scopeEl.textContent = (
libraryId ? optionText : optionText || "All Libraries"
)
.replace(/\s*\(\d+\)$/, "")
.trim();
}
} else {
showToast("Failed to load library preferences", "error");
console.error("API Error:", response.status, response.statusText);
@@ -142,7 +152,10 @@ async function saveDashboardSettings(): Promise<void> {
) as HTMLSelectElement;
const libraryId = librarySelect?.value || "";
const response = await apiPut("/dashboard/preferences", {
library_id: libraryId,
// Empty select = the All Libraries view: an explicit JSON null is
// the global scope (empty string is rejected by the server as a
// lost value — never a scope).
library_id: libraryId || null,
hidden_collections: hiddenCollections,
collection_order: collectionOrder,
items_per_section: parseInt(itemsPerCollection),
@@ -150,10 +163,6 @@ async function saveDashboardSettings(): Promise<void> {
if (response.ok) {
showToast("Dashboard settings saved", "success");
closeDashboardSettings();
if (!libraryId) {
showToast("Select a specific library to customize preferences", "error");
return;
}
await fetchAndRenderSections(libraryId);
}
} catch (error) {