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:
1 parent
25d44452c0
commit
b1de0da512
3 files changed
+29
-19
No files matched your search
@@ -172,6 +172,7 @@ templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Customize Dashboard</h2>
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Customize Dashboard</h2>
|
||||||
<p class="text-sm mt-1" style="color: var(--text-secondary)">Drag to reorder, toggle to show or hide.</p>
|
<p class="text-sm mt-1" style="color: var(--text-secondary)">Drag to reorder, toggle to show or hide.</p>
|
||||||
|
<p class="text-xs mt-1 font-medium" id="dashboard-scope-label" style="color: var(--accent)"></p>
|
||||||
</div>
|
</div>
|
||||||
<button data-action="close-dashboard-settings" class="icon-btn" aria-label="Close">
|
<button data-action="close-dashboard-settings" class="icon-btn" aria-label="Close">
|
||||||
@Icon("close", "h-5 w-5")
|
@Icon("close", "h-5 w-5")
|
||||||
|
|||||||
@@ -573,7 +573,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
|||||||
templ_7745c5c3_Var26 = templ.NopComponent
|
templ_7745c5c3_Var26 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div id=\"dashboard-settings-modal\" class=\"hidden fixed inset-0 z-[70] flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-2xl shadow-2xl\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><div><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Customize Dashboard</h2><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">Drag to reorder, toggle to show or hide.</p></div><button data-action=\"close-dashboard-settings\" class=\"icon-btn\" aria-label=\"Close\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div id=\"dashboard-settings-modal\" class=\"hidden fixed inset-0 z-[70] flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-2xl shadow-2xl\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><div><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Customize Dashboard</h2><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">Drag to reorder, toggle to show or hide.</p><p class=\"text-xs mt-1 font-medium\" id=\"dashboard-scope-label\" style=\"color: var(--accent)\"></p></div><button data-action=\"close-dashboard-settings\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -593,7 +593,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
|||||||
var templ_7745c5c3_Var27 string
|
var templ_7745c5c3_Var27 string
|
||||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 184, Col: 37}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 185, Col: 37}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -606,7 +606,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
|||||||
var templ_7745c5c3_Var28 string
|
var templ_7745c5c3_Var28 string
|
||||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem))
|
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 185, Col: 58}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 186, Col: 58}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -627,7 +627,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
|||||||
var templ_7745c5c3_Var29 string
|
var templ_7745c5c3_Var29 string
|
||||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 190, Col: 43}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 191, Col: 43}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -640,7 +640,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
|||||||
var templ_7745c5c3_Var30 string
|
var templ_7745c5c3_Var30 string
|
||||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 192, Col: 84}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 193, Col: 84}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -668,7 +668,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
|||||||
var templ_7745c5c3_Var31 string
|
var templ_7745c5c3_Var31 string
|
||||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 202, Col: 42}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 203, Col: 42}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -701,7 +701,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
|||||||
var templ_7745c5c3_Var32 string
|
var templ_7745c5c3_Var32 string
|
||||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection)
|
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 228, Col: 128}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 229, Col: 128}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -714,7 +714,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
|||||||
var templ_7745c5c3_Var33 string
|
var templ_7745c5c3_Var33 string
|
||||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(itemsPerSection)
|
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(itemsPerSection)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 235, Col: 28}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 236, Col: 28}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
+20
-11
@@ -20,14 +20,13 @@ async function openDashboardSettings(): Promise<void> {
|
|||||||
const librarySelect = document.getElementById(
|
const librarySelect = document.getElementById(
|
||||||
"library-select",
|
"library-select",
|
||||||
) as HTMLSelectElement;
|
) as HTMLSelectElement;
|
||||||
const libraryId = librarySelect?.value;
|
const libraryId = librarySelect?.value || "";
|
||||||
if (!libraryId) {
|
|
||||||
showToast("Select a specific library to customize preferences", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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(
|
const response = await fetch(
|
||||||
`/api/dashboard/preferences?library_id=${libraryId}`,
|
`/api/dashboard/preferences${query}`,
|
||||||
{
|
{
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||||
},
|
},
|
||||||
@@ -36,6 +35,17 @@ async function openDashboardSettings(): Promise<void> {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const prefs = await response.json();
|
const prefs = await response.json();
|
||||||
applyPreferencesToModal(prefs);
|
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 {
|
} else {
|
||||||
showToast("Failed to load library preferences", "error");
|
showToast("Failed to load library preferences", "error");
|
||||||
console.error("API Error:", response.status, response.statusText);
|
console.error("API Error:", response.status, response.statusText);
|
||||||
@@ -142,7 +152,10 @@ async function saveDashboardSettings(): Promise<void> {
|
|||||||
) as HTMLSelectElement;
|
) as HTMLSelectElement;
|
||||||
const libraryId = librarySelect?.value || "";
|
const libraryId = librarySelect?.value || "";
|
||||||
const response = await apiPut("/dashboard/preferences", {
|
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,
|
hidden_collections: hiddenCollections,
|
||||||
collection_order: collectionOrder,
|
collection_order: collectionOrder,
|
||||||
items_per_section: parseInt(itemsPerCollection),
|
items_per_section: parseInt(itemsPerCollection),
|
||||||
@@ -150,10 +163,6 @@ async function saveDashboardSettings(): Promise<void> {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showToast("Dashboard settings saved", "success");
|
showToast("Dashboard settings saved", "success");
|
||||||
closeDashboardSettings();
|
closeDashboardSettings();
|
||||||
if (!libraryId) {
|
|
||||||
showToast("Select a specific library to customize preferences", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await fetchAndRenderSections(libraryId);
|
await fetchAndRenderSections(libraryId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in new issue
Block a user