Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
787 changes: 393 additions & 394 deletions docs/branch-review-ledger.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/frontend-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Route ownership

- `GlobalSearchShell` owns shared navigation, responsive chrome and URL-backed mode/query/filter state.
- `GlobalSearchShell` owns shared navigation, responsive chrome and URL-backed mode/query/filter state. `search-route-ownership` is the pure routing boundary that decides whether a submitted search stays in a route-owned workflow or renders `ClinicalDashboard`.
- `ClinicalDashboard` owns submitted Answer, Documents and Prescribing workflows.
- `/documents/search` renders live `/api/search` results for submitted queries; `/documents/[id]` is the canonical viewer.
- `/documents/source*` are compatibility redirects. Fixture document journeys live only below `/mockups/document-search/**`.
Expand Down
48 changes: 14 additions & 34 deletions src/components/clinical-dashboard/global-search-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { isLocalNoAuthMode } from "@/lib/client-env";
import { documentsSearchHref } from "@/lib/document-flow-routes";
import { differentialsMobileCompareAddonSlotId, modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
import { readSearchNavigationContext, type SearchNavigationOptions } from "@/lib/search-navigation-context";
import { shouldRenderClinicalDashboard, shouldRenderDashboardSearch } from "@/lib/search-route-ownership";
import type { SearchScopeFilters } from "@/lib/search-scope";
import { useAuthSession } from "@/lib/supabase/client";
import type { ClinicalQueryMode } from "@/lib/types";
Expand Down Expand Up @@ -118,35 +119,23 @@ function GlobalSearchShellClient(props: GlobalSearchShellProps) {
: initialSearchMode;
const requestedQuery = (searchParams.get("q") ?? searchParams.get("query") ?? "").trim();
const hasSubmittedModeSearch = searchParams.get("run") === "1" && requestedQuery.length > 0;
const isHomeRoute = pathname === "/";
const isDocumentSearchMockupRoute = pathname.startsWith("/mockups/document-search");
const shouldRenderDashboardSearch =
hasSubmittedModeSearch &&
resolvedSearchMode !== "services" &&
resolvedSearchMode !== "forms" &&
resolvedSearchMode !== "favourites" &&
resolvedSearchMode !== "differentials" &&
resolvedSearchMode !== "dsm" &&
resolvedSearchMode !== "specifiers" &&
resolvedSearchMode !== "formulation" &&
// Therapy Compass owns its route with an in-tool search; a run-enabled link
// (/therapy-compass?q=…&run=1) must keep rendering the tool, not the dashboard.
resolvedSearchMode !== "therapy-compass" &&
!isDocumentSearchMockupRoute;
const isMedicationDetailRoute = /^\/medications\/[^/]+$/.test(pathname);
const shouldRenderClinicalDashboard = !isMedicationDetailRoute && (isHomeRoute || shouldRenderDashboardSearch);
const rendersClinicalDashboard = shouldRenderClinicalDashboard({
hasSubmittedSearch: hasSubmittedModeSearch,
mode: resolvedSearchMode,
pathname,
});

// Wrap both render paths so the patient-considerations profile is shared
// between the prescribing workspace (ClinicalDashboard) and the medication
// detail pages (standalone shell), backed by sessionStorage across navigation.
return (
<PatientProfileProvider>
{shouldRenderClinicalDashboard ? (
{rendersClinicalDashboard ? (
<ClinicalDashboard
initialSearchMode={resolvedSearchMode}
initialQuery={requestedQuery}
focusSearch={searchParams.get("focus") === "1"}
autoRunSearch={isHomeRoute ? hasSubmittedModeSearch : true}
autoRunSearch={pathname === "/" ? hasSubmittedModeSearch : true}
/>
) : (
<GlobalStandaloneSearchShellClient {...props} />
Expand Down Expand Up @@ -237,7 +226,6 @@ function GlobalStandaloneSearchShellClient({
const auth = useAuthSession();
const sidebarIdentity = useMemo(() => deriveSidebarIdentity(auth.session?.user.email), [auth.session?.user.email]);
const hasSubmittedModeSearch = requestedRun && requestedQuery.length > 0;
const isDocumentSearchMockupRoute = pathname.startsWith("/mockups/document-search");
const isDocumentCommandSearchView = pathname === "/documents/search" && requestedQuery.length > 0;
const useCompactBottomSearch = hasSubmittedModeSearch || isDocumentCommandSearchView;
const differentialsCompareAddonActive =
Expand All @@ -246,22 +234,14 @@ function GlobalStandaloneSearchShellClient({
// standalone routes; the shell must not swap them to the dashboard. On the
// home route the dashboard always renders, so these exclusions only apply
// to the standalone pages.
const shouldRenderDashboardSearch =
hasSubmittedModeSearch &&
resolvedSearchMode !== "services" &&
resolvedSearchMode !== "forms" &&
resolvedSearchMode !== "favourites" &&
resolvedSearchMode !== "differentials" &&
resolvedSearchMode !== "dsm" &&
resolvedSearchMode !== "specifiers" &&
resolvedSearchMode !== "formulation" &&
// Therapy Compass owns its route with an in-tool search; a run-enabled link
// (/therapy-compass?q=…&run=1) must keep rendering the tool, not the dashboard.
resolvedSearchMode !== "therapy-compass" &&
!isDocumentSearchMockupRoute;
const rendersDashboardSearch = shouldRenderDashboardSearch({
hasSubmittedSearch: hasSubmittedModeSearch,
mode: resolvedSearchMode,
pathname,
});
const isStandaloneModeHome =
!hasSubmittedModeSearch &&
!shouldRenderDashboardSearch &&
!rendersDashboardSearch &&
((searchMode === "services" && pathname === "/services") ||
(searchMode === "forms" && pathname === "/forms") ||
(searchMode === "favourites" && pathname === "/favourites") ||
Expand Down
47 changes: 47 additions & 0 deletions src/lib/search-route-ownership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { AppModeId } from "@/lib/app-modes";

/**
* Decides whether a submitted shared-composer search belongs to the dashboard
* or to the route that is already mounted. This is deliberately a pure routing
* boundary: it does not parse URLs, fetch data, or depend on React state.
*/
const routeOwnedSubmittedSearchModes = new Set<AppModeId>([
"services",
"forms",
"favourites",
"differentials",
"dsm",
"specifiers",
"formulation",
"therapy-compass",
]);

export function shouldRenderDashboardSearch({
hasSubmittedSearch,
mode,
pathname,
}: {
hasSubmittedSearch: boolean;
mode: AppModeId;
pathname: string;
}) {
return (
hasSubmittedSearch && !routeOwnedSubmittedSearchModes.has(mode) && !pathname.startsWith("/mockups/document-search")
);
}

export function shouldRenderClinicalDashboard({
hasSubmittedSearch,
mode,
pathname,
}: {
hasSubmittedSearch: boolean;
mode: AppModeId;
pathname: string;
}) {
const isMedicationDetailRoute = /^\/medications\/[^/]+$/.test(pathname);
return (
!isMedicationDetailRoute &&
(pathname === "/" || shouldRenderDashboardSearch({ hasSubmittedSearch, mode, pathname }))
);
}
45 changes: 45 additions & 0 deletions tests/search-route-ownership.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";

import { shouldRenderClinicalDashboard, shouldRenderDashboardSearch } from "@/lib/search-route-ownership";

describe("shared-search route ownership", () => {
it("keeps submitted searches in route-owned mode workflows", () => {
for (const mode of [
"services",
"forms",
"favourites",
"differentials",
"dsm",
"specifiers",
"formulation",
"therapy-compass",
] as const) {
expect(shouldRenderDashboardSearch({ hasSubmittedSearch: true, mode, pathname: `/${mode}` })).toBe(false);
}
});

it("routes dashboard-owned submitted workflows to ClinicalDashboard", () => {
expect(shouldRenderDashboardSearch({ hasSubmittedSearch: true, mode: "answer", pathname: "/" })).toBe(true);
expect(
shouldRenderDashboardSearch({ hasSubmittedSearch: true, mode: "documents", pathname: "/documents/search" }),
).toBe(true);
expect(shouldRenderClinicalDashboard({ hasSubmittedSearch: false, mode: "answer", pathname: "/" })).toBe(true);
});

it("never replaces an explicit medication detail or document-search mockup", () => {
expect(
shouldRenderClinicalDashboard({
hasSubmittedSearch: true,
mode: "prescribing",
pathname: "/medications/acamprosate",
}),
).toBe(false);
expect(
shouldRenderDashboardSearch({
hasSubmittedSearch: true,
mode: "documents",
pathname: "/mockups/document-search/search",
}),
).toBe(false);
});
});
16 changes: 7 additions & 9 deletions tests/therapy-compass-mode-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@ import { existsSync, readFileSync } from "node:fs";

import { describe, expect, it } from "vitest";

import { shouldRenderDashboardSearch } from "@/lib/search-route-ownership";

// Guards the two production-mode wiring invariants for Therapy Compass. Both were
// real breakages caught in review when the mockup was promoted to a live mode.

const loaderSrc = readFileSync(
new URL("../src/components/therapy-compass/data/use-therapy-data.ts", import.meta.url),
"utf8",
);
const shellSrc = readFileSync(
new URL("../src/components/clinical-dashboard/global-search-shell.tsx", import.meta.url),
"utf8",
);
const dataDir = new URL("../public/therapy-compass-data/", import.meta.url);

describe("Therapy Compass production-mode wiring", () => {
Expand All @@ -28,11 +26,11 @@ describe("Therapy Compass production-mode wiring", () => {
}
});

it("excludes therapy-compass from the shell dashboard-search so run-enabled links keep the tool", () => {
// Both shouldRenderDashboardSearch blocks (standalone + non-standalone client) must exclude the mode,
// otherwise /therapy-compass?q=…&run=1 renders ClinicalDashboard over TherapyCompassPage.
const occurrences = shellSrc.match(/resolvedSearchMode !== "therapy-compass"/g) ?? [];
expect(occurrences.length).toBeGreaterThanOrEqual(2);
it("keeps therapy-compass route-owned when the shared composer has a submitted query", () => {
// Otherwise /therapy-compass?q=…&run=1 renders ClinicalDashboard over TherapyCompassPage.
expect(
shouldRenderDashboardSearch({ hasSubmittedSearch: true, mode: "therapy-compass", pathname: "/therapy-compass" }),
).toBe(false);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

it("honors run-enabled deep links by seeding the in-tool search instead of landing on Home", () => {
Expand Down
Loading