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:
John O'Keefe committed 2026-11-02 10:14:35 -05:00
1 parent 12aab156f6
commit ef4de7896a
3 files changed
+38 -8

No files matched your search

+36 -6
View File
@@ -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) {