fix(search): route the library header search to /search
Typing in the desktop header search bar ran library.search() in place and relied on /library rendering the results inline. On every other /library/** route nothing rendered them, so the search bar looked broken: results were fetched and never shown. Make /search the single surface that renders results. The header bar becomes a navigator — it hands the query and route-derived scope to /search via ?q= and ?scope=, which seed the page and run the search on arrival. The inline result block and the header's scope chips are removed; the chips live on /search, which owns the results. The empty `all` scope is omitted from the URL, and typing while already on /search does not push a history entry per keystroke.
This commit is contained in:
@@ -7,6 +7,8 @@ import {
|
||||
normalizeGroupOrder,
|
||||
reorderGroups,
|
||||
resolveSearchScope,
|
||||
searchRouteUrl,
|
||||
shouldNavigateToSearch,
|
||||
scopeItemTypes,
|
||||
type SearchGroupId,
|
||||
} from "./searchScope";
|
||||
@@ -370,4 +372,42 @@ describe("reorderGroups", () => {
|
||||
expect(reorderGroups(order, -1, 2)).toEqual(order);
|
||||
expect(reorderGroups(order, 0, 9)).toEqual(order);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("searchRouteUrl", () => {
|
||||
it("encodes the query and the scope", () => {
|
||||
expect(searchRouteUrl("miles davis", "music")).toBe("/search?q=miles%20davis&scope=music");
|
||||
});
|
||||
|
||||
it("omits the scope key for the default `all` scope", () => {
|
||||
expect(searchRouteUrl("dune", "all")).toBe("/search?q=dune");
|
||||
});
|
||||
|
||||
it("targets bare /search for an empty query so the page shows its empty state", () => {
|
||||
expect(searchRouteUrl("", "all")).toBe("/search");
|
||||
expect(searchRouteUrl(" ", "music")).toBe("/search");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldNavigateToSearch", () => {
|
||||
it("navigates from any library page, which cannot render results itself", () => {
|
||||
// The bug: the header search bar shows on every /library/** route but only
|
||||
// /library rendered $library.searchResults, so typing did nothing on
|
||||
// /library/music, /library/tv, /library/movies and detail pages.
|
||||
expect(shouldNavigateToSearch("/library", "jazz")).toBe(true);
|
||||
expect(shouldNavigateToSearch("/library/music", "jazz")).toBe(true);
|
||||
expect(shouldNavigateToSearch("/library/tv", "jazz")).toBe(true);
|
||||
expect(shouldNavigateToSearch("/library/movies", "jazz")).toBe(true);
|
||||
expect(shouldNavigateToSearch("/library/abc123", "jazz")).toBe(true);
|
||||
});
|
||||
|
||||
it("stays put when already on /search, so typing does not re-push history", () => {
|
||||
expect(shouldNavigateToSearch("/search", "jazz")).toBe(false);
|
||||
expect(shouldNavigateToSearch("/search?q=old", "jazz")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not navigate on an empty query", () => {
|
||||
expect(shouldNavigateToSearch("/library/music", "")).toBe(false);
|
||||
expect(shouldNavigateToSearch("/library/music", " ")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,42 @@ export function resolveSearchScope(pathname: string): SearchScope {
|
||||
return "all";
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL of the single search surface for a query + scope.
|
||||
*
|
||||
* `/search` is the *only* route that renders results, so every other search
|
||||
* affordance (the desktop header bar) is a navigator to this URL rather than a
|
||||
* second result renderer. The `all` scope is the page's own default, so it is
|
||||
* omitted to keep shared/back-navigated URLs clean.
|
||||
*
|
||||
* TRACES: UR-049 | DR-063
|
||||
*/
|
||||
export function searchRouteUrl(query: string, scope: SearchScope): string {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return "/search";
|
||||
|
||||
const params = new URLSearchParams({ q: trimmed });
|
||||
if (scope !== "all") params.set("scope", scope);
|
||||
// URLSearchParams renders spaces as "+", valid in a query but noisier to
|
||||
// read; %20 is equally valid and matches how the app builds other links.
|
||||
return `/search?${params.toString().replace(/\+/g, "%20")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a search typed on `pathname` must navigate to `/search` to be seen.
|
||||
*
|
||||
* True for every route except `/search` itself: no other page renders
|
||||
* `searchResults`, so a search performed there is invisible. Guarding on
|
||||
* `/search` keeps typing from pushing a history entry per keystroke.
|
||||
*
|
||||
* TRACES: UR-049 | DR-063
|
||||
*/
|
||||
export function shouldNavigateToSearch(pathname: string, query: string): boolean {
|
||||
if (!query.trim()) return false;
|
||||
const path = pathname.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
|
||||
return path !== "/search";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result groups
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user