diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index db1a59cf79..118b061c2a 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -618,3 +618,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-04 | pull/1588 | d79d19c97a918edf785c5a066e691435a3f31518 | Run PR sweep full changed scope | fixed; exact-head CI required | PASS: conflict resolved from current main; combined lock regenerated with 0 vulnerabilities; prior jsdom exact-head build, coverage, static, Lighthouse, container and scans passed. Final merged-tree CI required. | | 2026-08-04 | pull/1602 | 9b2c45c3f10ee7440e7450da7179a390dfbdf4c0 | Run PR sweep full changed scope | merged | PASS: dependency audit, build, coverage, static, Lighthouse, provider-free container smoke, HIGH/CRITICAL scan, SAST, Secret Scan and PR required. | | 2026-08-04 | pull/1603 | dab117d474224964da40af2a1edc016e5dd7ffdf | Run PR sweep full changed scope | merged | PASS: dependency audit, Knip check, typecheck, build, full unit coverage, Lighthouse, provider-free container verification, scans and PR required. | +| 2026-08-04 | claude/top-search-design-mockups-w53znc | b432448e4893a42d07558aff0dc04be797971231 | PR #1611 — results-band shelf Clear filter-only, memo deps, restored tests | Fixed two Qodo findings from merged #1555; mutation-tested guard added | tsc 0; eslint 0; vitest 4 files/59 tests; verify:pr-local blocked by lock parity (node 24.13 vs jsdom@30) | diff --git a/src/components/clinical-dashboard/document-search-results.tsx b/src/components/clinical-dashboard/document-search-results.tsx index e57a658ab2..122791750c 100644 --- a/src/components/clinical-dashboard/document-search-results.tsx +++ b/src/components/clinical-dashboard/document-search-results.tsx @@ -1111,15 +1111,20 @@ function DocumentSearchResultsPanelImpl({ const recordMatchCount = recordMatches.length; const shouldShowHome = showHome || !trimmedQuery; - function toggleTagFacet(facet: SmartDocumentTagFacet) { - setActiveFacetState((current) => { - const keys = current.query === query ? current.keys : []; - return { - query, - keys: keys.includes(facet.key) ? keys.filter((key) => key !== facet.key) : [...keys, facet.key], - }; - }); - } + // Stable per query so the applied-filter shelf can depend on it honestly + // rather than suppressing the dependency check. + const toggleTagFacet = useCallback( + (facet: SmartDocumentTagFacet) => { + setActiveFacetState((current) => { + const keys = current.query === query ? current.keys : []; + return { + query, + keys: keys.includes(facet.key) ? keys.filter((key) => key !== facet.key) : [...keys, facet.key], + }; + }); + }, + [query], + ); const unavailable = deriveDocumentSearchUnavailable({ apiUnavailable, @@ -1182,10 +1187,7 @@ function DocumentSearchResultsPanelImpl({ } } return chips; - // `toggleTagFacet` is a stable closure over `query`, which is already a - // dependency of `activeFacetKeys`. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tagFacetGroups, activeFacetKeys, effectiveResultType, resultTabs]); + }, [tagFacetGroups, activeFacetKeys, effectiveResultType, resultTabs, toggleTagFacet]); const clearAllFilters = () => { setActiveFacetState({ query, keys: [] }); setActiveResultType("all"); diff --git a/src/components/therapy-compass/bindings.tsx b/src/components/therapy-compass/bindings.tsx index f9ef94f783..c6f2b118e5 100644 --- a/src/components/therapy-compass/bindings.tsx +++ b/src/components/therapy-compass/bindings.tsx @@ -106,6 +106,7 @@ export type TcBindings = { toggleSheetOnly: () => void; toggleReviewedOnly: () => void; clearSearch: () => void; + clearSearchFilters: () => void; // ---- compare -------------------------------------------------------- compareSlugs: string[]; @@ -434,6 +435,10 @@ export function TcProvider({ children }: { children: ReactNode }) { toggleSheetOnly: () => setSearch((prev) => ({ ...prev, sheetOnly: !prev.sheetOnly })), toggleReviewedOnly: () => setSearch((prev) => ({ ...prev, reviewedOnly: !prev.reviewedOnly })), clearSearch: () => setSearch(EMPTY_SEARCH), + // Filter-only clear. The results-band shelf lists filters and says so + // ("Filtered by"), so its Clear must not delete the search term the user + // is reading — that is a control doing more than it advertises. + clearSearchFilters: () => setSearch((prev) => ({ ...EMPTY_SEARCH, query: prev.query })), compareSlugs, compareTherapies, diff --git a/src/components/therapy-compass/screens/search-screen.tsx b/src/components/therapy-compass/screens/search-screen.tsx index 6cd0627eff..62450feff0 100644 --- a/src/components/therapy-compass/screens/search-screen.tsx +++ b/src/components/therapy-compass/screens/search-screen.tsx @@ -31,7 +31,9 @@ export function SearchScreen() { const activeFilterCount = b.search.tags.length + availabilityFilterCount; // Topics and availability both narrow the same list, so both belong on the // shelf. The query is deliberately absent: it is stated in the composer and - // removing it is not a filter operation. + // removing it is not a filter operation. The shelf's trailing Clear therefore + // uses `clearSearchFilters`, not `clearSearch` — a row labelled "Filtered by" + // must not delete the search term the user is reading. const appliedFilters = [ ...b.search.tags.map((tag) => ({ id: `topic-${tag}`, label: tag, onRemove: () => b.toggleTag(tag) })), ...(b.search.reviewedOnly ? [{ id: "reviewed", label: "Reviewed only", onRemove: b.toggleReviewedOnly }] : []), @@ -51,7 +53,7 @@ export function SearchScreen() { onRetry={b.retryData} headingLevel={1} appliedFilters={appliedFilters} - onClearFilters={b.clearSearch} + onClearFilters={b.clearSearchFilters} filterLabel="Filter therapy results" mobileControls={ { ); expect(filteredSuggestions(config, "missing topic")).toEqual([]); }); + + it("exposes differential red-flag search terms", () => { + expect(differentialRedFlagTerms).toContain("confusion"); + expect(differentialRedFlagTerms.length).toBeGreaterThan(3); + }); + + it("keeps one sm max-height cap per command dropdown placement", () => { + const source = readFileSync( + new URL("../src/components/clinical-dashboard/universal-search-command-surface.tsx", import.meta.url), + "utf8", + ); + + expect(source).not.toContain("sm:max-h-[min(55dvh,26rem)]"); + expect(source).toContain('opensUpward ? "sm:max-h-[min(38dvh,20rem)]" : "sm:max-h-[min(42dvh,24rem)]"'); + }); }); diff --git a/tests/therapy-compass-mode-wiring.test.ts b/tests/therapy-compass-mode-wiring.test.ts index 1b249fcce1..3d9b2837e9 100644 --- a/tests/therapy-compass-mode-wiring.test.ts +++ b/tests/therapy-compass-mode-wiring.test.ts @@ -228,4 +228,25 @@ describe("Therapy Compass production-mode wiring", () => { expect(shellSrc).toContain("isStandaloneModeHomePath(pathname)"); expect(isStandaloneModeHomePath("/therapy-compass")).toBe(true); }); + + it("keeps the results-band shelf Clear filter-only so it cannot delete the query", () => { + // Shipped defect (PR #1555): the shelf is labelled "Filtered by" and its + // trailing Clear was wired to `clearSearch`, which resets EMPTY_SEARCH — + // including `query: ""`. So Clear silently deleted the search term the user + // was reading, while the code comment beside it claimed the query was + // deliberately excluded. The sheet's own "Clear all" still resets + // everything on purpose; only the shelf is filter-only. + const searchScreenSrc = readFileSync( + new URL("../src/components/therapy-compass/screens/search-screen.tsx", import.meta.url), + "utf8", + ); + expect(searchScreenSrc).toContain("onClearFilters={b.clearSearchFilters}"); + expect(searchScreenSrc).not.toContain("onClearFilters={b.clearSearch}"); + // The binding must preserve the query rather than reset the whole shape. + expect(bindingsSrc).toContain( + "clearSearchFilters: () => setSearch((prev) => ({ ...EMPTY_SEARCH, query: prev.query }))", + ); + // The sheet keeps the full reset. + expect(searchScreenSrc).toContain("onClear={b.clearSearch}"); + }); });