Merge v189/labsui into release/v1.18.9

Default the labs surface to list view and reuse the medication card header for
tiles; surface confirm-gated biomarker delete on detail and list rows; add
alphabetical sort; align the labs settings page to the canonical SettingsCard.
This commit is contained in:
Marc-André Bombeck committed 2026-06-20 12:00:11 +02:00
commit 09a04f6eb7
11 files changed
+277 -127

No files matched your search

@@ -38,6 +38,27 @@ describe("module-list-prefs", () => {
order: ["x"],
});
});
it("accepts the alphabetical sort directions (#43)", () => {
for (const sortDir of ["alphaAsc", "alphaDesc"]) {
const parsed = parseModuleListPrefs(JSON.stringify({ sortDir }));
expect(parsed.sortDir).toBe(sortDir);
}
});
it("applies a per-call default when no blob is stored (Labs list view, #40)", () => {
const labsDefaults = {
view: "list" as const,
order: [],
sortDir: "recentDesc" as const,
};
expect(parseModuleListPrefs(null, labsDefaults)).toEqual(labsDefaults);
// A stored view still wins over the default.
expect(
parseModuleListPrefs(JSON.stringify({ view: "cards" }), labsDefaults)
.view,
).toBe("cards");
});
});
describe("applyOrder", () => {
+43 -17
View File
@@ -28,10 +28,16 @@ export type ModuleListKey = "vorsorge" | "illness" | "labs";
/**
* Sort choice for the Labs surface (MOD-04): order biomarker groups by the
* recency of their most-recent reading. `manual` defers to the persisted
* `order` array instead.
* recency of their most-recent reading, alphabetically by analyte name
* (#43), or `manual`, which defers to the persisted `order` array.
*/
export const MODULE_SORT_DIRS = ["recentDesc", "recentAsc", "manual"] as const;
export const MODULE_SORT_DIRS = [
"recentDesc",
"recentAsc",
"alphaAsc",
"alphaDesc",
"manual",
] as const;
export type ModuleSortDir = (typeof MODULE_SORT_DIRS)[number];
export interface ModuleListPrefs {
@@ -47,6 +53,21 @@ const DEFAULT_PREFS: ModuleListPrefs = {
sortDir: "recentDesc",
};
/**
* Per-module default overrides applied when no preference blob is stored yet.
* Labs defaults to the compact list view (#40) so the dense reading table is
* the first thing the user sees; Vorsorge and Illness keep the card grid.
*/
const MODULE_DEFAULTS: Partial<
Record<ModuleListKey, Partial<ModuleListPrefs>>
> = {
labs: { view: "list" },
};
function defaultsFor(module: ModuleListKey): ModuleListPrefs {
return { ...DEFAULT_PREFS, ...MODULE_DEFAULTS[module] };
}
function storageKey(module: ModuleListKey): string {
return `healthlog:module-list-prefs:${module}`;
}
@@ -65,20 +86,21 @@ function isSortDir(value: unknown): value is ModuleSortDir {
);
}
export function parseModuleListPrefs(raw: string | null): ModuleListPrefs {
if (!raw) return DEFAULT_PREFS;
export function parseModuleListPrefs(
raw: string | null,
defaults: ModuleListPrefs = DEFAULT_PREFS,
): ModuleListPrefs {
if (!raw) return defaults;
try {
const blob = JSON.parse(raw) as Record<string, unknown>;
const view = isView(blob.view) ? blob.view : DEFAULT_PREFS.view;
const view = isView(blob.view) ? blob.view : defaults.view;
const order = Array.isArray(blob.order)
? blob.order.filter((v): v is string => typeof v === "string")
: [];
const sortDir = isSortDir(blob.sortDir)
? blob.sortDir
: DEFAULT_PREFS.sortDir;
const sortDir = isSortDir(blob.sortDir) ? blob.sortDir : defaults.sortDir;
return { view, order, sortDir };
} catch {
return DEFAULT_PREFS;
return defaults;
}
}
@@ -124,6 +146,7 @@ export function useModuleListPrefs(module: ModuleListKey): {
setSortDir: (sortDir: ModuleSortDir) => void;
} {
const key = storageKey(module);
const defaults = useMemo(() => defaultsFor(module), [module]);
const subscribe = useCallback(
(onChange: () => void) => {
@@ -154,7 +177,10 @@ export function useModuleListPrefs(module: ModuleListKey): {
// changed, so keying the parse on it gives downstream `applyOrder`
// memoisation a stable `prefs` reference instead of a fresh object + a
// JSON.parse on every render.
const prefs = useMemo(() => parseModuleListPrefs(raw), [raw]);
const prefs = useMemo(
() => parseModuleListPrefs(raw, defaults),
[raw, defaults],
);
const write = useCallback(
(next: ModuleListPrefs) => {
@@ -167,18 +193,18 @@ export function useModuleListPrefs(module: ModuleListKey): {
const setView = useCallback(
(view: ModuleListView) =>
write({ ...parseModuleListPrefs(getSnapshot()), view }),
[write, getSnapshot],
write({ ...parseModuleListPrefs(getSnapshot(), defaults), view }),
[write, getSnapshot, defaults],
);
const setOrder = useCallback(
(order: string[]) =>
write({ ...parseModuleListPrefs(getSnapshot()), order }),
[write, getSnapshot],
write({ ...parseModuleListPrefs(getSnapshot(), defaults), order }),
[write, getSnapshot, defaults],
);
const setSortDir = useCallback(
(sortDir: ModuleSortDir) =>
write({ ...parseModuleListPrefs(getSnapshot()), sortDir }),
[write, getSnapshot],
write({ ...parseModuleListPrefs(getSnapshot(), defaults), sortDir }),
[write, getSnapshot, defaults],
);
return { prefs, setView, setOrder, setSortDir };