From 2f4da62c4602a1e33dfdfd14530df18839a62e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 11 Nov 2026 00:07:58 +0100 Subject: [PATCH] refactor(labs): drop the sort pill from the filter rail Settings > Labs already holds the list's sort order. The pill added a second, URL-only one that was lost on the next visit, and Reset cleared it while the rail counted it as no filter at all. The list now sorts the way Settings says, and a ?sort param is ignored. --- .../lab-filter-rail.browser.test.tsx | 19 +- .../labs/__tests__/lab-filter-rail.test.tsx | 61 +--- .../labs/__tests__/lab-list-filters.test.ts | 20 +- .../lab-list-url-sync.browser.test.tsx | 315 ------------------ .../labs/__tests__/lab-list-url-sync.test.tsx | 41 +-- .../labs/__tests__/lab-list.test.tsx | 20 +- src/components/labs/lab-filter-rail.tsx | 56 ---- src/components/labs/lab-list-filters.ts | 29 +- src/components/labs/lab-list.tsx | 33 +- src/components/labs/use-lab-list-filters.ts | 30 +- src/components/ui/filter-bar.tsx | 13 +- 11 files changed, 52 insertions(+), 585 deletions(-) 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"),