diff --git a/src/components/clinical-dashboard/universal-search-also-matches.tsx b/src/components/clinical-dashboard/universal-search-also-matches.tsx index 3234005e6..aacee86ba 100644 --- a/src/components/clinical-dashboard/universal-search-also-matches.tsx +++ b/src/components/clinical-dashboard/universal-search-also-matches.tsx @@ -6,7 +6,7 @@ import { useUniversalSearch } from "@/components/clinical-dashboard/use-universa import { cn } from "@/components/ui-primitives"; import { appModeDefinition, appModeHomeHref, type AppModeId } from "@/lib/app-modes"; import { appModeIcons } from "@/lib/app-mode-icons"; -import { universalSearchModeForDomain } from "@/lib/universal-search-mode-context"; +import { universalSearchModeForDomain, universalSearchPreferredDomains } from "@/lib/universal-search-mode-context"; export function UniversalSearchAlsoMatches({ modeId, @@ -22,6 +22,7 @@ export function UniversalSearchAlsoMatches({ query: trimmedQuery, enabled: trimmedQuery.length >= 2, contextMode: modeId, + excludeDomains: universalSearchPreferredDomains(modeId), limitPerDomain: 2, }); const preferred = new Set(universal.preferredDomains ?? []); diff --git a/src/components/clinical-dashboard/use-universal-search.ts b/src/components/clinical-dashboard/use-universal-search.ts index 8fa2dc967..dabdbbd6d 100644 --- a/src/components/clinical-dashboard/use-universal-search.ts +++ b/src/components/clinical-dashboard/use-universal-search.ts @@ -55,13 +55,13 @@ const resultCache = new Map(); function cacheKeyFor( query: string, contextMode: AppModeId, - excludeDomain: string | undefined, + excludedDomainsKey: string, limitPerDomain: number, authSignature: string, ) { // JSON-array key so no field can collide with another via a shared delimiter (auth header // values and the query itself can contain spaces, commas, etc.). - return JSON.stringify([authSignature, contextMode, excludeDomain ?? "", limitPerDomain, query]); + return JSON.stringify([authSignature, contextMode, excludedDomainsKey, limitPerDomain, query]); } // Non-mutating read used during render (must stay pure — no recency side effect here). @@ -98,7 +98,7 @@ export function useUniversalSearch(args: { query: string; enabled: boolean; contextMode: AppModeId; - excludeDomain?: UniversalSearchDomain; + excludeDomains?: readonly UniversalSearchDomain[]; limitPerDomain?: number; }): UniversalSearchState { const { authorizationHeader } = useAuthSession(); @@ -108,10 +108,10 @@ export function useUniversalSearch(args: { const trimmedQuery = args.query.trim(); const active = args.enabled && trimmedQuery.length >= minQueryLength; const limitPerDomain = args.limitPerDomain ?? 3; - const excludeDomain = args.excludeDomain; + const excludedDomainsKey = universalSearchDomains.filter((domain) => args.excludeDomains?.includes(domain)).join(","); const authSignature = JSON.stringify(authorizationHeader ?? {}); const cacheKey = active - ? cacheKeyFor(trimmedQuery, args.contextMode, excludeDomain, limitPerDomain, authSignature) + ? cacheKeyFor(trimmedQuery, args.contextMode, excludedDomainsKey, limitPerDomain, authSignature) : null; useEffect(() => { @@ -144,7 +144,8 @@ export function useUniversalSearch(args: { // is not enough — abort frees the server/DB work too. const controller = new AbortController(); const timer = window.setTimeout(() => { - const domains = universalSearchDomains.filter((domain) => domain !== excludeDomain); + const excludedDomains = new Set(excludedDomainsKey ? excludedDomainsKey.split(",") : []); + const domains = universalSearchDomains.filter((domain) => !excludedDomains.has(domain)); const params = new URLSearchParams({ q: trimmedQuery, limit: String(limitPerDomain), @@ -185,7 +186,7 @@ export function useUniversalSearch(args: { window.clearTimeout(timer); controller.abort(); }; - }, [active, cacheKey, trimmedQuery, excludeDomain, limitPerDomain, authorizationHeader, args.contextMode]); + }, [active, cacheKey, trimmedQuery, excludedDomainsKey, limitPerDomain, authorizationHeader, args.contextMode]); if (!active) return { groups: [], loading: false, query: "" }; diff --git a/tests/ui-universal-search.spec.ts b/tests/ui-universal-search.spec.ts index 6f2d08cd9..821fa4176 100644 --- a/tests/ui-universal-search.spec.ts +++ b/tests/ui-universal-search.spec.ts @@ -202,11 +202,13 @@ test.describe("universal search typeahead", () => { test("keeps compact cross-mode matches visible after submission", async ({ page }) => { await mockUniversalSearch(page); + const universalRequest = page.waitForRequest(/\/api\/search\/universal(?:\?.*)?$/); await page.goto("/services?q=acamprosate&run=1", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("universal-also-matches")).toBeVisible(); await expect(page.getByText("Also matches in other modes")).toBeVisible(); await expect(page.getByRole("link", { name: "Acamprosate", exact: true })).toBeVisible(); + expect(new URL((await universalRequest).url()).searchParams.get("domains")?.split(",")).not.toContain("services"); }); test("shows submitted cross-mode matches on phones outside hidden desktop headers", async ({ page }) => { @@ -311,7 +313,7 @@ test.describe("universal search smart affordances", () => { await mockSmartSearch(page); const input = await openComposer(page, "/?mode=answer&focus=1"); await input.fill("acamprosat"); - await input.press("Enter"); + await page.getByRole("button", { name: "Generate source-backed answer" }).click(); await expect(page.getByTestId("universal-also-matches")).toBeVisible(); });