diff --git a/src/components/labs/__tests__/lab-form-keep-open.test.ts b/src/components/labs/__tests__/lab-form-keep-open.test.ts new file mode 100644 index 000000000..389df8ecc --- /dev/null +++ b/src/components/labs/__tests__/lab-form-keep-open.test.ts @@ -0,0 +1,44 @@ +/** + * "Save & add another" keeps the report and the marker, clears the reading + * (#1029). + * + * It used to clear the biomarker too. The picker then fell out of its + * controlled state and went on showing the marker it last held while the + * form's own value was empty, so the second save answered "Pick a biomarker + * first" under a visibly picked marker. Keeping the marker removes the reset + * that caused the disagreement; the picker itself is now always controlled, + * so an empty pick shows the placeholder instead of a stale marker. + */ +import { describe, it, expect } from "vitest"; + +import { nextEntryAfterKeepOpen, type LabEntryDraft } from "../lab-form"; + +const saved: LabEntryDraft = { + biomarkerId: "bm-vitamin-d", + value: "42", + valueText: "", + takenAt: "2026-09-01T08:30", + note: "fasting", + sourceRange: "30 - 100", + visitId: "visit-1", +}; + +describe("nextEntryAfterKeepOpen", () => { + it("keeps the picked biomarker so the next save does not ask for it again", () => { + expect(nextEntryAfterKeepOpen(saved).biomarkerId).toBe("bm-vitamin-d"); + }); + + it("keeps what the report shares: its draw date and its visit", () => { + const next = nextEntryAfterKeepOpen(saved); + expect(next.takenAt).toBe("2026-09-01T08:30"); + expect(next.visitId).toBe("visit-1"); + }); + + it("clears the reading itself and its printed window", () => { + const next = nextEntryAfterKeepOpen({ ...saved, valueText: "negative" }); + expect(next.value).toBe(""); + expect(next.valueText).toBe(""); + expect(next.note).toBe(""); + expect(next.sourceRange).toBe(""); + }); +}); diff --git a/src/components/labs/lab-form.tsx b/src/components/labs/lab-form.tsx index f22d1efe8..a27babb65 100644 --- a/src/components/labs/lab-form.tsx +++ b/src/components/labs/lab-form.tsx @@ -1,7 +1,7 @@ "use client"; import { useActiveRecordName } from "@/hooks/use-record-capabilities"; -import { useId, useMemo, useRef, useState } from "react"; +import { useId, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2 } from "lucide-react"; @@ -57,6 +57,40 @@ function parseDecimal(raw: string): number | null { return Number.isFinite(n) ? n : null; } +/** The per-reading fields "Save & add another" decides to keep or clear. */ +export interface LabEntryDraft { + biomarkerId: string; + value: string; + valueText: string; + takenAt: string; + note: string; + sourceRange: string; + visitId: string | null; +} + +/** + * The form after "Save & add another": the reading is cleared, everything that + * describes the report it came from stays. The draw date and the visit are + * shared by every value on one report, and the biomarker stays because a run + * of entries is as often one marker over several dates as several markers on + * one date; re-picking it each time is what the button exists to avoid. + * + * Clearing the biomarker here used to drop the picker out of its controlled + * state. It kept showing the marker it last held while the form value was + * empty, so the next save refused with "Pick a biomarker first" under a + * visibly picked marker. + */ +export function nextEntryAfterKeepOpen(draft: LabEntryDraft): LabEntryDraft { + return { + ...draft, + value: "", + valueText: "", + note: "", + // The next reading has its own printed window. + sourceRange: "", + }; +} + interface LabFormProps { /** When set, the biomarker is pre-selected and locked (add-from-detail). */ lockedBiomarkerId?: string; @@ -157,7 +191,6 @@ export function LabForm({ const [defineFooterEl, setDefineFooterEl] = useState( null, ); - const biomarkerTriggerRef = useRef(null); const allMarkers = catalog?.biomarkers ?? []; const selected = allMarkers.find((m) => m.id === biomarkerId); @@ -256,30 +289,31 @@ export function LabForm({ : undefined, ); if (keepOpen) { - // A real lab report shares one blood-draw date across every - // analyte — clear the reading but deliberately KEEP `takenAt` so - // the next row doesn't need it re-set away from "now" again. - // The biomarker resets too (unless locked to one detail page) - // since the whole point is entering the NEXT analyte. - if (!lockedBiomarkerId) setBiomarkerId(""); - setValue(""); - setValueText(""); - setNote(""); - // The next analyte off the same report has its own printed window. - setSourceRange(""); + const next = nextEntryAfterKeepOpen({ + biomarkerId, + value, + valueText, + takenAt, + note, + sourceRange, + visitId, + }); + setBiomarkerId(next.biomarkerId); + setValue(next.value); + setValueText(next.valueText); + setTakenAt(next.takenAt); + setNote(next.note); + setSourceRange(next.sourceRange); + setVisitId(next.visitId); setError(null); onSavedKeepOpen?.(created); - // Return focus to wherever the next entry starts so the flow - // stays keyboard/screen-reader friendly across repeated saves. - if (!lockedBiomarkerId) { - biomarkerTriggerRef.current?.focus(); - } else { - document - .getElementById( - resultType === "numeric" ? "lab-value" : "lab-valueText", - ) - ?.focus(); - } + // The next entry starts at the value, so focus goes there and keeps + // the flow keyboard/screen-reader friendly across repeated saves. + document + .getElementById( + resultType === "numeric" ? "lab-value" : "lab-valueText", + ) + ?.focus(); } else { onSuccess?.(created); } @@ -355,16 +389,16 @@ export function LabForm({
+ {/* Always controlled: `""` shows the placeholder. Passing + `undefined` for an empty pick switched the picker to its own + internal state, which kept displaying the last marker while the + form held none. */}