mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(labs): read odd filter links kindly and match names without accents
A link with the date bounds reversed showed an empty list; it now reads as the range it means. The search ignores accents as well as case, so "hamoglobin" finds "Hämoglobin". The category pill only appears when there are categories to choose between, or when a link already filters by the only one, so the filter can still be cleared where it shows.
This commit is contained in:
1 parent
a40c9e4ea4
commit
cc9cbc4640
7 files changed
+104
-9
No files matched your search
@@ -26,6 +26,13 @@ function props(): LabFilterRailProps {
|
||||
counts: { inRange: 1, below: 0, above: 0, unknown: 0 },
|
||||
status: "in-range",
|
||||
},
|
||||
{
|
||||
key: "cat_lipids",
|
||||
label: "Lipids",
|
||||
markerKeys: ["bm:b2"],
|
||||
counts: { inRange: 1, below: 0, above: 0, unknown: 0 },
|
||||
status: "in-range",
|
||||
},
|
||||
],
|
||||
total: 5,
|
||||
};
|
||||
|
||||
@@ -30,6 +30,13 @@ describe("<LabFilterRail>", () => {
|
||||
counts: { inRange: 1, below: 0, above: 0, unknown: 0 },
|
||||
status: "in-range",
|
||||
},
|
||||
{
|
||||
key: "cat_lipids",
|
||||
label: "Lipids",
|
||||
markerKeys: ["bm:b2"],
|
||||
counts: { inRange: 1, below: 0, above: 0, unknown: 0 },
|
||||
status: "in-range",
|
||||
},
|
||||
]}
|
||||
total={5}
|
||||
/>
|
||||
@@ -137,4 +144,39 @@ describe("<LabFilterRail>", () => {
|
||||
|
||||
expect(html).toContain("Reset");
|
||||
});
|
||||
|
||||
it("offers the category pill only when there is more than one category", () => {
|
||||
const one = [
|
||||
{
|
||||
key: "cat_iron",
|
||||
label: "Iron Panel",
|
||||
markerKeys: ["bm:b1"],
|
||||
counts: { inRange: 1, below: 0, above: 0, unknown: 0 },
|
||||
status: "in-range" as const,
|
||||
},
|
||||
];
|
||||
const render = (category?: string) =>
|
||||
renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<LabFilterRail
|
||||
filters={category ? { category } : {}}
|
||||
searchDraft=""
|
||||
onSearchChange={vi.fn()}
|
||||
searchInputRef={{ current: null }}
|
||||
onStatusChange={vi.fn()}
|
||||
onCategoryChange={vi.fn()}
|
||||
onFromDayChange={vi.fn()}
|
||||
onToDayChange={vi.fn()}
|
||||
onResetFilters={vi.fn()}
|
||||
isFiltered={Boolean(category)}
|
||||
categories={one}
|
||||
total={1}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
expect(render()).not.toContain("Category");
|
||||
// A link that already filters by the one category keeps its pill, so
|
||||
// the filter can be cleared where it shows.
|
||||
expect(render("cat_iron")).toContain("Category: Iron Panel");
|
||||
});
|
||||
});
|
||||
@@ -89,6 +89,14 @@ describe("parseLabListSearchParams & labListFiltersToSearch", () => {
|
||||
expect(parseLabListSearchParams(params)).toEqual({});
|
||||
});
|
||||
|
||||
it("reads reversed date bounds as the range they mean", () => {
|
||||
expect(
|
||||
parseLabListSearchParams(
|
||||
new URLSearchParams("from=2026-09-30&to=2026-09-01"),
|
||||
),
|
||||
).toEqual({ fromDay: "2026-09-01", toDay: "2026-09-30" });
|
||||
});
|
||||
|
||||
it("rejects non-existent calendar dates like 2026-02-30", () => {
|
||||
const params = new URLSearchParams("from=2026-02-30&to=2026-04-31");
|
||||
expect(parseLabListSearchParams(params)).toEqual({});
|
||||
@@ -160,6 +168,22 @@ describe("filterMarkerRows", () => {
|
||||
expect(filterMarkerRows(rows, { q: "Iron" })).toEqual([row1, row2]);
|
||||
});
|
||||
|
||||
it("matches the search regardless of case and accents", () => {
|
||||
const accented = mockMarkerRow({
|
||||
key: "bm:hb",
|
||||
analyte: "Hämoglobin",
|
||||
panel: "Blutbild",
|
||||
});
|
||||
expect(filterMarkerRows([accented], { q: "HAMOGLOBIN" })).toEqual([
|
||||
accented,
|
||||
]);
|
||||
expect(filterMarkerRows([accented], { q: "hämo" })).toEqual([accented]);
|
||||
expect(filterMarkerRows([accented], { q: "blutbild" })).toEqual([
|
||||
accented,
|
||||
]);
|
||||
expect(filterMarkerRows([accented], { q: "ferritin" })).toEqual([]);
|
||||
});
|
||||
|
||||
it("filters by status: abnormal (below or above)", () => {
|
||||
expect(filterMarkerRows(rows, { status: "abnormal" })).toEqual([
|
||||
row2,
|
||||
|
||||
@@ -137,11 +137,12 @@ describe("LabList stale category and group header handling", () => {
|
||||
expect(history.pushes()).toEqual([]);
|
||||
expect(history.replaces()).toEqual([""]);
|
||||
|
||||
// Visible filtering ignores stale category: marker is visible, category pill is inactive
|
||||
// Visible filtering ignores the stale category: the marker is visible,
|
||||
// and the one real category leaves no category pill behind.
|
||||
await expect.element(screen.getByText("Ferritin")).toBeVisible();
|
||||
await expect
|
||||
.element(screen.getByRole("combobox", { name: "Category" }))
|
||||
.toHaveAttribute("data-active", "false");
|
||||
.element(screen.getByRole("combobox", { name: /^Category/ }))
|
||||
.not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("preserves active non-stale filters and scrubs stale category from URL after paint", async () => {
|
||||
|
||||
@@ -385,9 +385,10 @@ describe("<LabList>", () => {
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Stale category is ignored: marker is not filtered out, category dropdown is inactive
|
||||
// Stale category is ignored: the marker is not filtered out, and with a
|
||||
// single category there is no category pill to show it on.
|
||||
expect(html).toContain("Ferritin");
|
||||
expect(html).toContain('aria-label="Category" data-active="false"');
|
||||
expect(html).not.toContain('aria-label="Category');
|
||||
expect(html).not.toContain("Reset");
|
||||
expect(html).not.toContain("stale_unknown_category");
|
||||
});
|
||||
|
||||
@@ -57,6 +57,9 @@ export function LabFilterRail({
|
||||
{ value: "above", label: t("labs.range.above") },
|
||||
];
|
||||
|
||||
// The server's categories in the server's order, so the pill follows the
|
||||
// saved category order. One category is nothing to choose between, unless
|
||||
// a link already filters by it and the pill is the way to clear it.
|
||||
const categoryOptions: FilterBarSelectOption[] = (categories ?? []).map(
|
||||
(c) => ({
|
||||
value: c.key,
|
||||
@@ -142,7 +145,7 @@ export function LabFilterRail({
|
||||
allLabel={t("labs.filter.statusAll")}
|
||||
/>
|
||||
|
||||
{categoryOptions.length > 0 && (
|
||||
{(categoryOptions.length > 1 || filters.category) && (
|
||||
<FilterBarSelect
|
||||
label={t("labs.filter.categoryLabel")}
|
||||
value={filters.category ?? "ALL"}
|
||||
|
||||
@@ -99,6 +99,12 @@ export function parseLabListSearchParams(
|
||||
const toDay = dayOrUndefined(params.get("to"));
|
||||
if (toDay) filters.toDay = toDay;
|
||||
|
||||
// A hand-edited or old link can carry the bounds the wrong way round;
|
||||
// read it as the range it obviously means rather than as an empty one.
|
||||
if (filters.fromDay && filters.toDay && filters.fromDay > filters.toDay) {
|
||||
[filters.fromDay, filters.toDay] = [filters.toDay, filters.fromDay];
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
@@ -144,6 +150,14 @@ export function labReadingDayKey(takenAt: string, tz: string): string {
|
||||
return userDayKey(instant, tz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Case- and accent-insensitive form for the search, so "hamoglobin" finds
|
||||
* "Hämoglobin" and "proteine" finds "Protéine".
|
||||
*/
|
||||
function searchFold(value: string): string {
|
||||
return value.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter rows by text search, reference range status, category, and date bounds.
|
||||
*/
|
||||
@@ -153,7 +167,8 @@ export function filterMarkerRows(
|
||||
categoryOf?: (markerKey: string) => string | undefined,
|
||||
dayKeyOf: (takenAt: string) => string = (iso) => iso.slice(0, 10),
|
||||
): MarkerRow[] {
|
||||
const query = filters.q?.trim().toLowerCase();
|
||||
const trimmed = filters.q?.trim();
|
||||
const query = trimmed ? searchFold(trimmed) : undefined;
|
||||
const status = filters.status;
|
||||
const category = filters.category;
|
||||
const fromDay = filters.fromDay;
|
||||
@@ -162,8 +177,10 @@ export function filterMarkerRows(
|
||||
return rows.filter((row) => {
|
||||
// 1. Text search
|
||||
if (query) {
|
||||
const analyteMatch = row.analyte.toLowerCase().includes(query);
|
||||
const panelMatch = row.panel?.toLowerCase().includes(query) ?? false;
|
||||
const analyteMatch = searchFold(row.analyte).includes(query);
|
||||
const panelMatch = row.panel
|
||||
? searchFold(row.panel).includes(query)
|
||||
: false;
|
||||
if (!analyteMatch && !panelMatch) return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user