diff --git a/src/components/labs/__tests__/lab-filter-rail.browser.test.tsx b/src/components/labs/__tests__/lab-filter-rail.browser.test.tsx index aa2a83931..a6c8ac217 100644 --- a/src/components/labs/__tests__/lab-filter-rail.browser.test.tsx +++ b/src/components/labs/__tests__/lab-filter-rail.browser.test.tsx @@ -14,7 +14,6 @@ function props(): LabFilterRailProps { onCategoryChange: vi.fn(), onFromDayChange: vi.fn(), onToDayChange: vi.fn(), - onSortChange: vi.fn(), onResetFilters: vi.fn(), isFiltered: false, categories: [ @@ -27,7 +26,6 @@ function props(): LabFilterRailProps { }, ], total: 5, - currentSortDir: "recentDesc", }; } @@ -80,11 +78,6 @@ describe(" layout", () => { screen.getByRole("button", { name: "Date range" }).element(), screen.getByRole("combobox", { name: "Status: Out of range" }).element(), screen.getByRole("combobox", { name: "Category" }).element(), - screen - .getByRole("combobox", { - name: "Sort order: Most recent reading first", - }) - .element(), screen.getByRole("button", { name: "Reset" }).element(), ]; @@ -95,7 +88,7 @@ describe(" layout", () => { } }); - it("keeps sort and reset visible without clipping in German at 1536px", async () => { + it("keeps every control and reset visible without clipping in German at 1536px", async () => { await page.viewport(1536, 900); const { screen } = await renderWithApp( layout", () => { }) .element(), screen.getByRole("combobox", { name: "Kategorie" }).element(), - screen - .getByRole("combobox", { - name: "Sortierung: Neueste Messung zuerst", - }) - .element(), screen.getByRole("button", { name: "Zurücksetzen" }).element(), ]; @@ -145,11 +133,6 @@ describe(" layout", () => { screen.getByRole("button", { name: "Date range" }).element(), screen.getByRole("combobox", { name: "Status" }).element(), screen.getByRole("combobox", { name: "Category" }).element(), - screen - .getByRole("combobox", { - name: "Sort order: Most recent reading first", - }) - .element(), ]; const centers = controls.map(centerY); diff --git a/src/components/labs/__tests__/lab-filter-rail.test.tsx b/src/components/labs/__tests__/lab-filter-rail.test.tsx index 224c2a201..f55178fec 100644 --- a/src/components/labs/__tests__/lab-filter-rail.test.tsx +++ b/src/components/labs/__tests__/lab-filter-rail.test.tsx @@ -20,7 +20,6 @@ describe("", () => { onCategoryChange={vi.fn()} onFromDayChange={vi.fn()} onToDayChange={vi.fn()} - onSortChange={vi.fn()} onResetFilters={vi.fn()} isFiltered={false} categories={[ @@ -33,7 +32,6 @@ describe("", () => { }, ]} total={5} - currentSortDir="recentDesc" /> , ); @@ -48,16 +46,13 @@ describe("", () => { const dateRangeIdx = html.indexOf("Date range"); const statusIdx = html.indexOf("Status"); const categoryIdx = html.indexOf("Category"); - const sortIdx = html.indexOf("Sort order"); expect(searchIdx).toBeGreaterThan(-1); expect(dateRangeIdx).toBeGreaterThan(searchIdx); expect(statusIdx).toBeGreaterThan(dateRangeIdx); expect(categoryIdx).toBeGreaterThan(statusIdx); - expect(sortIdx).toBeGreaterThan(categoryIdx); - - // Active sort label is shown directly in the trigger - expect(html).toContain("Sort order: Most recent reading first"); + // The saved sort in Settings > Labs is the only sort; the rail has none. + expect(html).not.toContain("Sort order"); expect(html).toContain("5 biomarkers"); }); @@ -73,11 +68,9 @@ describe("", () => { onCategoryChange={vi.fn()} onFromDayChange={vi.fn()} onToDayChange={vi.fn()} - onSortChange={vi.fn()} onResetFilters={vi.fn()} isFiltered={true} total={1} - currentSortDir="recentDesc" /> , ); @@ -106,11 +99,9 @@ describe("", () => { onCategoryChange={vi.fn()} onFromDayChange={vi.fn()} onToDayChange={vi.fn()} - onSortChange={vi.fn()} - onResetFilters={vi.fn()} + onResetFilters={vi.fn()} isFiltered={false} total={total} - currentSortDir="recentDesc" /> , ); @@ -137,59 +128,13 @@ describe("", () => { onCategoryChange={vi.fn()} onFromDayChange={vi.fn()} onToDayChange={vi.fn()} - onSortChange={vi.fn()} onResetFilters={vi.fn()} isFiltered={true} total={2} - currentSortDir="recentDesc" /> , ); expect(html).toContain("Reset"); }); - - it("renders active sort label in the trigger", () => { - const htmlAlpha = renderToStaticMarkup( - - - , - ); - expect(htmlAlpha).toContain("Sort order: Name (A–Z)"); - - const htmlRecent = renderToStaticMarkup( - - - , - ); - expect(htmlRecent).toContain("Sort order: Most recent reading first"); - }); }); diff --git a/src/components/labs/__tests__/lab-list-filters.test.ts b/src/components/labs/__tests__/lab-list-filters.test.ts index 708941993..2dca96802 100644 --- a/src/components/labs/__tests__/lab-list-filters.test.ts +++ b/src/components/labs/__tests__/lab-list-filters.test.ts @@ -71,7 +71,7 @@ describe("parseLabListSearchParams & labListFiltersToSearch", () => { it("parses valid facets", () => { const params = new URLSearchParams( - "q=iron&status=abnormal&category=cat_iron&from=2026-01-01&to=2026-12-31&sort=alphaAsc", + "q=iron&status=abnormal&category=cat_iron&from=2026-01-01&to=2026-12-31", ); expect(parseLabListSearchParams(params)).toEqual({ q: "iron", @@ -79,13 +79,12 @@ describe("parseLabListSearchParams & labListFiltersToSearch", () => { category: "cat_iron", fromDay: "2026-01-01", toDay: "2026-12-31", - sort: "alphaAsc", }); }); - it("drops invalid status, invalid dates, and invalid sort", () => { + it("drops invalid status and invalid dates", () => { const params = new URLSearchParams( - "status=invalid&category=ALL&from=not-a-date&to=invalid&sort=random", + "status=invalid&category=ALL&from=not-a-date&to=invalid", ); expect(parseLabListSearchParams(params)).toEqual({}); }); @@ -106,14 +105,10 @@ describe("parseLabListSearchParams & labListFiltersToSearch", () => { ).toEqual({}); }); - it("omits default sort from search string", () => { - expect(labListFiltersToSearch({ sort: "recentDesc" }, "recentDesc")).toBe( - "", - ); - expect(labListFiltersToSearch({ sort: "alphaAsc" }, "recentDesc")).toBe( - "sort=alphaAsc", - ); - expect(labListFiltersToSearch({ sort: "alphaAsc" }, "alphaAsc")).toBe(""); + it("ignores a sort param: the list follows the saved sort only", () => { + expect( + parseLabListSearchParams(new URLSearchParams("sort=alphaAsc")), + ).toEqual({}); }); it("roundtrips valid filters to search string", () => { @@ -123,7 +118,6 @@ describe("parseLabListSearchParams & labListFiltersToSearch", () => { category: "lipids", fromDay: "2026-05-01", toDay: "2026-05-31", - sort: "recentAsc", }; const search = labListFiltersToSearch(filters); const parsed = parseLabListSearchParams(new URLSearchParams(search)); diff --git a/src/components/labs/__tests__/lab-list-url-sync.browser.test.tsx b/src/components/labs/__tests__/lab-list-url-sync.browser.test.tsx index 6b591aaf3..0caa69e3d 100644 --- a/src/components/labs/__tests__/lab-list-url-sync.browser.test.tsx +++ b/src/components/labs/__tests__/lab-list-url-sync.browser.test.tsx @@ -3,11 +3,6 @@ import type { QueryClient } from "@tanstack/react-query"; import { describe, expect, it } from "vitest"; import { queryKeys } from "@/lib/query-keys"; -import { - legacyModuleListStorageKey, - resetModuleListCarryOverForTests, -} from "@/lib/module-list-prefs"; -import { stubFetch } from "../../../../tests/browser/fetch-stub"; import { renderWithApp, testQueryClient } from "../../../../tests/browser/render"; import { currentSearch, @@ -165,316 +160,6 @@ describe("LabList stale category and group header handling", () => { expect(history.replaces()).toEqual(["?status=abnormal"]); }); - it("normalizes sort=manual without order in URL via replace after paint", async () => { - setSearch("?sort=manual"); - const history = recordHistory(); - - const queryClient = labQueryClient(); - queryClient.setQueryData(queryKeys.moduleListLayout(), { - version: 1, - lists: { - labs: { - view: "list", - order: [], - sortDir: "recentDesc", - groupBy: "none", - groupsOpen: "expanded", - categoryOrder: [], - }, - }, - }); - queryClient.setQueryData( - queryKeys.labResultsOverview("en"), - overviewWithCategories, - ); - - await renderWithApp(, { queryClient }); - - await expect.poll(currentSearch).toBe(""); - expect(history.pushes()).toEqual([]); - expect(history.replaces()).toEqual([""]); - }); - - it("preserves sort=manual while preferences load and keeps it when manual order arrives", async () => { - setSearch("?sort=manual"); - const history = recordHistory(); - const api = stubFetch(); - const pendingLayout = api.deferred("GET", "/api/settings/module-list-layout"); - - const queryClient = labQueryClient(); - queryClient.setQueryData( - queryKeys.labResultsOverview("en"), - overviewWithCategories, - ); - - const { screen } = await renderWithApp(, { queryClient }); - - // Wait for double-rAF afterPaint to pass; without isLoaded gate, this would scrub prematurely - await new Promise((resolve) => - requestAnimationFrame(() => requestAnimationFrame(resolve)), - ); - expect(currentSearch()).toBe("?sort=manual"); - expect(history.replaces()).toEqual([]); - - // Server responds with persisted manual order - pendingLayout.release({ - data: { - version: 1, - lists: { - labs: { - view: "list", - order: ["bm:b1"], - sortDir: "recentDesc", - groupBy: "none", - groupsOpen: "expanded", - categoryOrder: [], - }, - }, - }, - }); - - // Wait until preference data has rendered and the manual sort option is visible/selected - await expect - .element( - screen.getByRole("combobox", { name: "Sort order: Sort manually" }), - ) - .toBeVisible(); - - // Ensure any afterPaint effects from the render have settled - await new Promise((resolve) => - requestAnimationFrame(() => requestAnimationFrame(resolve)), - ); - - // Valid manual sort remains preserved in URL without triggering replace - expect(currentSearch()).toBe("?sort=manual"); - expect(history.replaces()).toEqual([]); - }); - - it("preserves sort=manual when preferences request fails", async () => { - setSearch("?sort=manual"); - const history = recordHistory(); - const api = stubFetch(); - const pendingLayout = api.deferred("GET", "/api/settings/module-list-layout"); - - const queryClient = labQueryClient(); - queryClient.setQueryData( - queryKeys.labResultsOverview("en"), - overviewWithCategories, - ); - - await renderWithApp(, { queryClient }); - - // While preference query is still pending, sort=manual is preserved - await new Promise((resolve) => - requestAnimationFrame(() => requestAnimationFrame(resolve)), - ); - expect(currentSearch()).toBe("?sort=manual"); - expect(history.replaces()).toEqual([]); - - // Server request fails - pendingLayout.release({ - status: 500, - error: "Internal Server Error", - }); - - // Wait for the query to transition to error state - await expect - .poll(() => queryClient.getQueryState(queryKeys.moduleListLayout())?.status) - .toBe("error"); - - // Wait for double-rAF afterPaint to pass; if readiness was tied to !isLoading, this would scrub - await new Promise((resolve) => - requestAnimationFrame(() => requestAnimationFrame(resolve)), - ); - - // sort=manual remains intact when preference state is unknown due to failure - expect(currentSearch()).toBe("?sort=manual"); - expect(history.replaces()).toEqual([]); - }); - - it("preserves sort=manual while preferences load and normalizes via replace when server confirms empty order", async () => { - setSearch("?sort=manual"); - const history = recordHistory(); - const api = stubFetch(); - const pendingLayout = api.deferred("GET", "/api/settings/module-list-layout"); - - const queryClient = labQueryClient(); - queryClient.setQueryData( - queryKeys.labResultsOverview("en"), - overviewWithCategories, - ); - - await renderWithApp(, { queryClient }); - - // While preference query is still pending, sort=manual is preserved - await new Promise((resolve) => - requestAnimationFrame(() => requestAnimationFrame(resolve)), - ); - expect(currentSearch()).toBe("?sort=manual"); - expect(history.replaces()).toEqual([]); - - // Server responds with confirmed empty manual order - pendingLayout.release({ - data: { - version: 1, - lists: { - labs: { - view: "list", - order: [], - sortDir: "recentDesc", - groupBy: "none", - groupsOpen: "expanded", - categoryOrder: [], - }, - }, - }, - }); - - // Unsupported manual sort is normalized via replace after preferences load - await expect.poll(currentSearch).toBe(""); - expect(history.pushes()).toEqual([]); - expect(history.replaces()).toEqual([""]); - }); - - it("preserves sort=manual while carry-over PUT is pending and keeps it when carry-over succeeds", async () => { - const storageKey = legacyModuleListStorageKey("labs"); - window.localStorage.setItem( - storageKey, - JSON.stringify({ - view: "list", - order: ["bm:b1"], - sortDir: "manual", - }), - ); - resetModuleListCarryOverForTests(); - - try { - setSearch("?sort=manual"); - const history = recordHistory(); - const api = stubFetch(); - const pendingLayout = api.deferred("GET", "/api/settings/module-list-layout"); - const pendingPut = api.deferred("PUT", "/api/settings/module-list-layout"); - - const queryClient = labQueryClient(); - queryClient.setQueryData( - queryKeys.labResultsOverview("en"), - overviewWithCategories, - ); - - const { screen } = await renderWithApp(, { queryClient }); - - // Before layout GET arrives, sort=manual is preserved - await new Promise((resolve) => - requestAnimationFrame(() => requestAnimationFrame(resolve)), - ); - expect(currentSearch()).toBe("?sort=manual"); - expect(history.replaces()).toEqual([]); - - // Release layout GET with saved: false and empty order (unarranged server state) - pendingLayout.release({ - data: { - version: 1, - lists: { - labs: { - view: "list", - order: [], - sortDir: "recentDesc", - groupBy: "none", - groupsOpen: "expanded", - categoryOrder: [], - saved: false, - }, - illness: { - view: "cards", - order: [], - sortDir: "recentDesc", - groupBy: "none", - groupsOpen: "expanded", - categoryOrder: [], - saved: false, - }, - vorsorge: { - view: "cards", - order: [], - sortDir: "recentDesc", - groupBy: "none", - groupsOpen: "expanded", - categoryOrder: [], - saved: false, - }, - }, - }, - }); - - // Wait until carry-over PUT is dispatched - await expect - .poll(() => api.calls("PUT", "/api/settings/module-list-layout").length) - .toBe(1); - - // Wait for double-rAF afterPaint to pass; while carry-over PUT is pending, - // legacy order is preferred so sort=manual is not replaced - await new Promise((resolve) => - requestAnimationFrame(() => requestAnimationFrame(resolve)), - ); - expect(currentSearch()).toBe("?sort=manual"); - expect(history.replaces()).toEqual([]); - - // Release the carry-over PUT with saved manual order - pendingPut.release({ - data: { - version: 1, - lists: { - labs: { - view: "list", - order: ["bm:b1"], - sortDir: "manual", - groupBy: "none", - groupsOpen: "expanded", - categoryOrder: [], - saved: true, - }, - illness: { - view: "cards", - order: [], - sortDir: "recentDesc", - groupBy: "none", - groupsOpen: "expanded", - categoryOrder: [], - saved: false, - }, - vorsorge: { - view: "cards", - order: [], - sortDir: "recentDesc", - groupBy: "none", - groupsOpen: "expanded", - categoryOrder: [], - saved: false, - }, - }, - }, - }); - - // Wait until preference data has rendered and the manual sort option is visible/selected - await expect - .element( - screen.getByRole("combobox", { name: "Sort order: Sort manually" }), - ) - .toBeVisible(); - - await new Promise((resolve) => - requestAnimationFrame(() => requestAnimationFrame(resolve)), - ); - - // URL remains intact and no replace occurred throughout - expect(currentSearch()).toBe("?sort=manual"); - expect(history.replaces()).toEqual([]); - } finally { - window.localStorage.removeItem(storageKey); - resetModuleListCarryOverForTests(); - } - }); - it("preserves valid category in URL without triggering replace", async () => { setSearch("?category=cat_iron"); const history = recordHistory(); diff --git a/src/components/labs/__tests__/lab-list-url-sync.test.tsx b/src/components/labs/__tests__/lab-list-url-sync.test.tsx index 1632f8253..967ae101e 100644 --- a/src/components/labs/__tests__/lab-list-url-sync.test.tsx +++ b/src/components/labs/__tests__/lab-list-url-sync.test.tsx @@ -167,40 +167,15 @@ describe("LabList URL filter sync — interactions write to URL", () => { expect(browser.push).not.toHaveBeenCalled(); }); - it("pushes sort override and does not set isFiltered", () => { - render(""); + it("leaves a sort param out of the filter state", () => { + render("sort=alphaAsc"); expect(capturedRailProps!.isFiltered).toBe(false); - capturedRailProps!.onSortChange("alphaAsc"); - expect(browser.push).toHaveBeenCalledWith("/labs?sort=alphaAsc"); - }); - - it("omits sort from URL when selecting the preference default", () => { - render("sort=alphaAsc"); - capturedRailProps!.onSortChange("recentDesc"); - expect(browser.push).toHaveBeenCalledWith("/labs"); - }); - - it("treats recentDesc as effective default sort when prefs.sortDir is manual without manual order", () => { - mockModulePrefs.sortDir = "manual"; - mockModulePrefs.order = []; - render(""); - expect(capturedRailProps!.currentSortDir).toBe("recentDesc"); - capturedRailProps!.onSortChange("recentDesc"); - // Does not write redundant ?sort=recentDesc to URL - expect(browser.push).not.toHaveBeenCalled(); - }); - - it("clears sort override back to bare pathname when selecting recentDesc under manual prefs without manual order", () => { - mockModulePrefs.sortDir = "manual"; - mockModulePrefs.order = []; - render("sort=alphaAsc"); - expect(capturedRailProps!.currentSortDir).toBe("alphaAsc"); - capturedRailProps!.onSortChange("recentDesc"); - expect(browser.push).toHaveBeenCalledWith("/labs"); + capturedRailProps!.onStatusChange("abnormal"); + expect(browser.push).toHaveBeenCalledWith("/labs?status=abnormal"); }); it("reset clears every facet in one push", () => { - render("status=abnormal&category=cat_iron&from=2026-09-01&sort=alphaAsc"); + render("status=abnormal&category=cat_iron&from=2026-09-01"); expect(capturedRailProps!.isFiltered).toBe(true); capturedRailProps!.onResetFilters(); expect(browser.push).toHaveBeenCalledWith("/labs"); @@ -210,23 +185,21 @@ describe("LabList URL filter sync — interactions write to URL", () => { describe("LabList URL filter sync — mounting restores filters from query", () => { it("restores every facet into the rail controls", () => { render( - "q=ferritin&status=in-range&category=cat_iron&from=2026-09-01&to=2026-09-30&sort=alphaDesc", + "q=ferritin&status=in-range&category=cat_iron&from=2026-09-01&to=2026-09-30", ); expect(capturedRailProps!.filters.q).toBe("ferritin"); expect(capturedRailProps!.filters.status).toBe("in-range"); expect(capturedRailProps!.filters.category).toBe("cat_iron"); expect(capturedRailProps!.filters.fromDay).toBe("2026-09-01"); expect(capturedRailProps!.filters.toDay).toBe("2026-09-30"); - expect(capturedRailProps!.currentSortDir).toBe("alphaDesc"); expect(capturedRailProps!.isFiltered).toBe(true); }); it("drops invalid status and invalid dates without breaking the page", () => { - render("status=INVALID&from=2026-02-30&to=not-a-date&sort=UNKNOWN"); + render("status=INVALID&from=2026-02-30&to=not-a-date"); expect(capturedRailProps!.filters.status).toBeUndefined(); expect(capturedRailProps!.filters.fromDay).toBeUndefined(); expect(capturedRailProps!.filters.toDay).toBeUndefined(); - expect(capturedRailProps!.currentSortDir).toBe("recentDesc"); expect(capturedRailProps!.isFiltered).toBe(false); }); diff --git a/src/components/labs/__tests__/lab-list.test.tsx b/src/components/labs/__tests__/lab-list.test.tsx index fb34bd4c6..e5d9ef1a1 100644 --- a/src/components/labs/__tests__/lab-list.test.tsx +++ b/src/components/labs/__tests__/lab-list.test.tsx @@ -252,12 +252,12 @@ describe("", () => { expect(html).toContain("1 of 1 outside range"); }); - it("handles sort=manual gracefully when manual order is empty", () => { - mockSearch = "sort=manual"; + it("sorts by the saved preference and ignores a sort param in the URL", () => { + mockSearch = "sort=recentDesc"; mockModulePrefs = { view: "list", - order: [], // no manual order - sortDir: "recentDesc", + order: [], + sortDir: "alphaAsc", groupBy: "none", groupsOpen: "expanded", categoryOrder: [], @@ -268,6 +268,12 @@ describe("", () => { }); const overview: LabOverviewResponse = { markers: [ + { + key: "bm:b2", + count: 1, + trend: [5], + latest: mockReading({ id: "r2", biomarkerId: "b2", analyte: "Zinc" }), + }, { key: "bm:b1", count: 1, @@ -288,9 +294,9 @@ describe("", () => { , ); - // Should fall back to recentDesc instead of displaying raw "manual" - expect(html).toContain("Sort order: Most recent reading first"); - expect(html).not.toContain("Sort order: manual"); + expect(html.indexOf("Ferritin")).toBeGreaterThan(-1); + expect(html.indexOf("Zinc")).toBeGreaterThan(html.indexOf("Ferritin")); + expect(html).not.toContain("Sort order"); }); it("filters by local day bounds using display timezone (Europe/Warsaw)", () => { diff --git a/src/components/labs/lab-filter-rail.tsx b/src/components/labs/lab-filter-rail.tsx index c8dce24d4..03519ed5a 100644 --- a/src/components/labs/lab-filter-rail.tsx +++ b/src/components/labs/lab-filter-rail.tsx @@ -7,21 +7,10 @@ import { FilterBar, FilterBarDateRange, FilterBarSelect, - FILTER_BAR_PILL_CLASSES, type FilterBarSelectOption, } from "@/components/ui/filter-bar"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, -} from "@/components/ui/select"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; -import { - isModuleSortDir, - type ModuleSortDir, -} from "@/lib/settings/module-list-layout"; import { isLabStatusFilter, type LabListFilters, @@ -38,13 +27,10 @@ export interface LabFilterRailProps { onCategoryChange: (category: string) => void; onFromDayChange: (from: string) => void; onToDayChange: (to: string) => void; - onSortChange: (sort: ModuleSortDir) => void; onResetFilters: () => void; isFiltered: boolean; categories?: LabOverviewCategoryDto[] | null; total?: number; - currentSortDir: ModuleSortDir; - hasManualOrder?: boolean; } export function LabFilterRail({ @@ -56,13 +42,10 @@ export function LabFilterRail({ onCategoryChange, onFromDayChange, onToDayChange, - onSortChange, onResetFilters, isFiltered, categories, total, - currentSortDir, - hasManualOrder, }: LabFilterRailProps) { const { t, tCount } = useTranslations(); const fmt = useFormatters(); @@ -81,19 +64,6 @@ export function LabFilterRail({ }), ); - const sortOptions: FilterBarSelectOption[] = [ - { value: "recentDesc", label: t("labs.sort.recentDesc") }, - { value: "recentAsc", label: t("labs.sort.recentAsc") }, - { value: "alphaAsc", label: t("labs.sort.alphaAsc") }, - { value: "alphaDesc", label: t("labs.sort.alphaDesc") }, - ...(hasManualOrder - ? [{ value: "manual", label: t("labs.sort.manual") }] - : []), - ]; - - const currentSortLabel = - sortOptions.find((o) => o.value === currentSortDir)?.label ?? currentSortDir; - return (
)} - - -
); diff --git a/src/components/labs/lab-list-filters.ts b/src/components/labs/lab-list-filters.ts index 74795159f..6b921360e 100644 --- a/src/components/labs/lab-list-filters.ts +++ b/src/components/labs/lab-list-filters.ts @@ -2,17 +2,14 @@ /** * Pure URL-facet helpers for the labs overview list, following the - * documents-vault and measurements patterns: the list's filter and sort state - * lives in the page URL (?q&status&category&from&to&sort) so a filtered view is + * documents-vault and measurements patterns: the list's filter state + * lives in the page URL (?q&status&category&from&to) so a filtered view is * deep-linkable and survives navigation, reload and back/forward. Parsing is * lenient — an unknown facet drops silently — and serialisation omits defaults * so the unfiltered list keeps a clean URL. */ import { applyOrder } from "@/lib/module-list-prefs"; -import { - isModuleSortDir, - type ModuleSortDir, -} from "@/lib/settings/module-list-layout"; +import type { ModuleSortDir } from "@/lib/settings/module-list-layout"; import { dateOnlyKey, isCalendarDateKey, @@ -45,8 +42,6 @@ export interface LabListFilters { /** Inclusive day bounds, YYYY-MM-DD for newest reading (latest.takenAt). */ fromDay?: string; toDay?: string; - /** On-page sort override: recentDesc | recentAsc | alphaAsc | alphaDesc | manual. */ - sort?: ModuleSortDir; } const STATUS_SET = new Set(LAB_STATUS_FILTERS); @@ -99,11 +94,6 @@ export function parseLabListSearchParams( const toDay = dayOrUndefined(params.get("to")); if (toDay) filters.toDay = toDay; - const sort = params.get("sort")?.trim(); - if (sort && isModuleSortDir(sort)) { - filters.sort = sort; - } - return filters; } @@ -111,10 +101,7 @@ export function parseLabListSearchParams( * Serialise the filter object back into the page URL query string (no leading `?`). * Defaults/ALL values are omitted. */ -export function labListFiltersToSearch( - filters: LabListFilters, - defaultSort: ModuleSortDir = "recentDesc", -): string { +export function labListFiltersToSearch(filters: LabListFilters): string { const sp = new URLSearchParams(); if (filters.q?.trim()) sp.set("q", filters.q.trim()); if (filters.status && filters.status !== "ALL") @@ -124,7 +111,6 @@ export function labListFiltersToSearch( } if (filters.fromDay) sp.set("from", filters.fromDay); if (filters.toDay) sp.set("to", filters.toDay); - if (filters.sort && filters.sort !== defaultSort) sp.set("sort", filters.sort); return sp.toString(); } @@ -205,7 +191,12 @@ export function filterMarkerRows( } /** - * Sort marker rows according to the chosen sort direction and optional manual order. + * The person's Labs sort (Settings > Labs) over the server's + * most-recent-first order: `recentAsc` reverses, the alpha options sort by + * analyte name (#43), and `manual` applies the persisted biomarker order + * (legacy un-linked rows, which carry no biomarkerId, sort after the ordered + * block). Under categories the same order holds inside each category. The + * list has no sort control of its own; the saved one is the only one. */ export function sortMarkerRows( rows: readonly MarkerRow[], diff --git a/src/components/labs/lab-list.tsx b/src/components/labs/lab-list.tsx index efb6cb379..67680ca2f 100644 --- a/src/components/labs/lab-list.tsx +++ b/src/components/labs/lab-list.tsx @@ -17,7 +17,6 @@ import { queryKeys } from "@/lib/query-keys"; import { useModuleListPrefs, type ModuleListView, - type ModuleSortDir, } from "@/lib/module-list-prefs"; import { LabFilterRail } from "./lab-filter-rail"; @@ -328,7 +327,7 @@ export function LabList({ const { t, locale } = useTranslations(); const { canWriteDomain } = useRecordCapabilities(); const canAddLab = canWriteDomain("labs"); - const { prefs, isLoaded } = useModuleListPrefs("labs"); + const { prefs } = useModuleListPrefs("labs"); const displayTz = useDisplayTimezone(); // v1.22 — a short, factual line under each marker heading describing what the @@ -343,12 +342,6 @@ export function LabList({ queryFn: () => apiGet("/api/labs/overview"), }); - const hasManualOrder = prefs.order.length > 0; - const effectiveDefaultSort: ModuleSortDir = - prefs.sortDir === "manual" && !hasManualOrder - ? "recentDesc" - : prefs.sortDir; - const { filters, searchDraft, @@ -358,15 +351,8 @@ export function LabList({ setCategoryFilter, setFromDay, setToDay, - setSortFilter, resetFilters, - } = useLabListFilters({ - defaultSort: effectiveDefaultSort, - }); - - const rawSort = filters.sort ?? effectiveDefaultSort; - const effectiveSortDir = - rawSort === "manual" && !hasManualOrder ? "recentDesc" : rawSort; + } = useLabListFilters(); const rawRows = useMemo( () => toMarkerRows(data?.markers ?? [], t), @@ -395,14 +381,6 @@ export function LabList({ const groupsOpen = useGroupsOpen(prefs.groupsOpen); - // Scrub unsupported manual sort if preferences confirm no manual order exists. - // afterPaint protects mount writes from router history initialization. - useEffect(() => { - if (isLoaded && filters.sort === "manual" && !hasManualOrder) { - setSortFilter(effectiveDefaultSort, { mode: "replace", afterPaint: true }); - } - }, [isLoaded, filters.sort, hasManualOrder, effectiveDefaultSort, setSortFilter]); - // Scrub stale category filter when categories load. // afterPaint protects mount writes from router history initialization. useEffect(() => { @@ -436,8 +414,8 @@ export function LabList({ ); const rows = useMemo( - () => sortMarkerRows(filteredRows, effectiveSortDir, prefs.order), - [filteredRows, effectiveSortDir, prefs.order], + () => sortMarkerRows(filteredRows, prefs.sortDir, prefs.order), + [filteredRows, prefs.sortDir, prefs.order], ); // Categories only when the person asked for them and the server says they @@ -483,13 +461,10 @@ export function LabList({ onCategoryChange={setCategoryFilter} onFromDayChange={setFromDay} onToDayChange={setToDay} - onSortChange={setSortFilter} onResetFilters={resetFilters} isFiltered={effectiveIsFiltered} categories={categories} total={rows.length} - currentSortDir={effectiveSortDir} - hasManualOrder={hasManualOrder} /> ); diff --git a/src/components/labs/use-lab-list-filters.ts b/src/components/labs/use-lab-list-filters.ts index c42b89357..5140488fe 100644 --- a/src/components/labs/use-lab-list-filters.ts +++ b/src/components/labs/use-lab-list-filters.ts @@ -5,7 +5,6 @@ import { useUrlFilterSync, type UrlFilterSyncOptions, } from "@/hooks/use-url-filter-sync"; -import type { ModuleSortDir } from "@/lib/settings/module-list-layout"; import { labListFiltersToSearch, parseLabListSearchParams, @@ -13,20 +12,11 @@ import { type LabStatusFilter, } from "./lab-list-filters"; -export function useLabListFilters({ - defaultSort = "recentDesc", -}: { - defaultSort?: ModuleSortDir; -} = {}) { - const serialise = useCallback( - (f: LabListFilters) => labListFiltersToSearch(f, defaultSort), - [defaultSort], - ); - +export function useLabListFilters() { const { filters, applyFilters, patchFilters } = useUrlFilterSync({ parse: parseLabListSearchParams, - serialise, + serialise: labListFiltersToSearch, }); // Debounced search draft @@ -115,21 +105,6 @@ export function useLabListFilters({ [patchFilters], ); - const setSortFilter = useCallback( - ( - value: ModuleSortDir, - modeOrOptions: "push" | "replace" | UrlFilterSyncOptions = "push", - ) => { - patchFilters( - { - sort: value === defaultSort ? undefined : value, - }, - modeOrOptions, - ); - }, - [defaultSort, patchFilters], - ); - const resetFilters = useCallback(() => { setSearchDraft(""); applyFilters({}); @@ -144,7 +119,6 @@ export function useLabListFilters({ setCategoryFilter, setFromDay, setToDay, - setSortFilter, resetFilters, }; } diff --git a/src/components/ui/filter-bar.tsx b/src/components/ui/filter-bar.tsx index fb4b1eb3a..abacb0576 100644 --- a/src/components/ui/filter-bar.tsx +++ b/src/components/ui/filter-bar.tsx @@ -57,11 +57,8 @@ import { cn } from "@/lib/utils"; * ui-guidelines §5). */ -/** - * Shared pill chrome for filter-bar triggers and active chips. - * Exported for custom controls (such as select triggers) that participate in the rail. - */ -export const FILTER_BAR_PILL_CLASSES = +/** Shared pill chrome — trigger and chip in one element. */ +const PILL_CLASSES = "border-border bg-card text-foreground inline-flex min-h-11 shrink-0 items-center gap-1.5 rounded-full border px-3 text-sm whitespace-nowrap shadow-xs transition-all duration-150 ease-out sm:min-h-9 h-auto py-1"; const PILL_ACTIVE_CLASSES = "border-primary/40 bg-primary/10"; @@ -186,7 +183,7 @@ export function FilterBarSelect({ data-slot="filter-bar-pill" data-active={active ? "true" : "false"} className={cn( - FILTER_BAR_PILL_CLASSES, + PILL_CLASSES, active && cn(PILL_ACTIVE_CLASSES, "pr-8"), )} > @@ -265,7 +262,7 @@ export function FilterBarDateRange({ data-slot="filter-bar-pill" data-active={active ? "true" : "false"} className={cn( - FILTER_BAR_PILL_CLASSES, + PILL_CLASSES, "outline-none", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", active && cn(PILL_ACTIVE_CLASSES, "pr-8"), @@ -360,7 +357,7 @@ export function FilterBarNumberRange({ data-slot="filter-bar-pill" data-active={active ? "true" : "false"} className={cn( - FILTER_BAR_PILL_CLASSES, + PILL_CLASSES, "outline-none", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", active && cn(PILL_ACTIVE_CLASSES, "pr-8"),