fix(csb): custom-section book picker search never worked — two latent bugs
Fine-toothed functional verification of the delegation work exposed that the picker's book search has been dead since launch, for two independent reasons: - The builder fetched GET /api/books/search, a route no router has ever registered — every search 404'd (the CSP-dead onclick buttons on the results hid the failure). It now calls the unified JSON search the bookshelf uses (/api/media-items/search?q=&library_id=), maps its rows (id/title/author/cover_image_path) to the picker's BookInfo shape, and treats the endpoint's 404-with-results:[] as empty. - Even with results, the picker container shared id=search-results with the header search dropdown (header.templ renders it on every page); getElementById returns the first match in tree order, so results were written into the invisible topbar dropdown. The picker container is now cs-search-results (templ + all three TS references). Verified end-to-end at 390x844: select library -> search '1984' -> 3 results render -> click adds the selection chip -> chip x removes it. Zero console errors.
This commit is contained in:
1 parent
12aab156f6
commit
ef4de7896a
3 files changed
+38
-8
No files matched your search
@@ -261,7 +261,7 @@ function initCustomSectionBuilder(): void {
|
||||
// Delegated clicks for the generated picker/selection markup: the CSP
|
||||
// build refuses inline onclick= handlers, so the rows carry data
|
||||
// attributes and one listener per container survives innerHTML swaps.
|
||||
const resultsContainer = document.getElementById("search-results");
|
||||
const resultsContainer = document.getElementById("cs-search-results");
|
||||
resultsContainer?.addEventListener("click", (e) => {
|
||||
const row = (e.target as HTMLElement).closest<HTMLElement>("[data-book-id]");
|
||||
if (!row) return;
|
||||
@@ -384,7 +384,11 @@ function onBookSearchInput(): void {
|
||||
async function searchBooks(): Promise<void> {
|
||||
const searchInput = document.getElementById("book-search") as HTMLInputElement;
|
||||
const librarySelect = document.getElementById("section-library") as HTMLSelectElement;
|
||||
const resultsContainer = document.getElementById("search-results") as HTMLElement;
|
||||
// NOTE: cs-search-results, NOT search-results — the header search
|
||||
// dropdown owns the latter on every page, and getElementById returns
|
||||
// the first match in tree order (the picker's results were landing in
|
||||
// the topbar dropdown, which stayed invisible).
|
||||
const resultsContainer = document.getElementById("cs-search-results") as HTMLElement;
|
||||
|
||||
const query = searchInput?.value.trim();
|
||||
const libraryId = librarySelect?.value;
|
||||
@@ -395,22 +399,48 @@ async function searchBooks(): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
// The unified JSON search (same endpoint the bookshelf uses). The
|
||||
// builder was wired to /api/books/search, a route that has never
|
||||
// existed — every picker search 404'd. Rows come back with id/title/
|
||||
// author/cover_image_path; map to the BookInfo shape the picker
|
||||
// renders (conflict/badge metadata is attached server-side later).
|
||||
const response = await fetch(
|
||||
`/api/books/search?q=${encodeURIComponent(query)}&library_id=${libraryId}`,
|
||||
`/api/media-items/search?q=${encodeURIComponent(query)}&library_id=${libraryId}`,
|
||||
{
|
||||
headers: {
|
||||
...authHeaders(),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 404) {
|
||||
// Unified search signals "no results" as a 404 whose body still
|
||||
// carries results: [].
|
||||
displaySearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to search books");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
displaySearchResults(data.books || []);
|
||||
const rows: Array<{
|
||||
id?: string;
|
||||
title?: string;
|
||||
author?: string | null;
|
||||
cover_image_path?: string | null;
|
||||
}> = Array.isArray(data) ? data : data.results || [];
|
||||
const books: BookInfo[] = rows.map((r) => ({
|
||||
media_item_id: r.id ?? "",
|
||||
title: r.title ?? "",
|
||||
author: r.author ?? "",
|
||||
cover_image_path: r.cover_image_path ?? "",
|
||||
has_conflict: false,
|
||||
format_group: "",
|
||||
badge: "",
|
||||
}));
|
||||
displaySearchResults(books);
|
||||
} catch (error) {
|
||||
console.error("Search books error:", error);
|
||||
showToast("Failed to search books", "error");
|
||||
@@ -418,7 +448,7 @@ async function searchBooks(): Promise<void> {
|
||||
}
|
||||
|
||||
function displaySearchResults(books: BookInfo[]): void {
|
||||
const resultsContainer = document.getElementById("search-results") as HTMLElement;
|
||||
const resultsContainer = document.getElementById("cs-search-results") as HTMLElement;
|
||||
if (!resultsContainer) return;
|
||||
|
||||
if (books.length === 0) {
|
||||
|
||||
Reference in new issue
Block a user