mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
fix(medications): let the custom dose-window toggle stay on
The per-dose "custom window" switch could never be turned on. Switching it on seeded the range from the default band and emitted immediately, but the serialiser drops a range equal to the default band, so the persisted value round-tripped straight back to empty, the controlled `checked` recomputed false, and the switch snapped off. The explicit range fields only appear once the switch is on, so there was no way to edit the range into a non-default one first — a deadlock. Track the opened-switch intent in local editor state, decoupled from serialisation. A toggled-on row renders its default band immediately and the switch stays on; an explicit range persists only once the user edits it away from the default. Toggling off drops any stored range back to the default derivation.
This commit is contained in:
1 parent
a0032ba7ed
commit
7c4b54e3ea
2 files changed
+52
-9
No files matched your search
@@ -158,6 +158,21 @@ describe("dose-window helpers", () => {
|
||||
];
|
||||
expect(rowsToEntries(rows)).toEqual([]);
|
||||
});
|
||||
|
||||
// v1.25.13 regression — a row toggled custom but still sitting at the
|
||||
// default band serialises to `[]`. This is WHY <DoseWindowEditor> tracks
|
||||
// the "opened" switch state locally instead of reconstructing it from the
|
||||
// persisted value: a value round-trip would drop the row and snap the
|
||||
// switch back off, so the toggle could never turn on until an explicit
|
||||
// range existed — which the editor won't reveal until the toggle is on.
|
||||
it("round-trips a default-band custom row to empty (the toggle-deadlock trap)", () => {
|
||||
const seeded = buildRows(["08:00"], []); // custom:false, band 07:00–09:00
|
||||
const openedAtDefault: DoseWindowRow[] = seeded.map((r) => ({
|
||||
...r,
|
||||
custom: true,
|
||||
}));
|
||||
expect(rowsToEntries(openedAtDefault)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isOrderedRange", () => {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* section chrome the other Zeitplan / Erinnerung rows use.
|
||||
*/
|
||||
|
||||
import { useCallback, useId, useMemo } from "react";
|
||||
import { useCallback, useId, useMemo, useState } from "react";
|
||||
|
||||
import { TimeField } from "@/components/ui/time-field";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
@@ -26,7 +26,6 @@ import { Label } from "@/components/ui/label";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import {
|
||||
buildRows,
|
||||
defaultBandForTime,
|
||||
isOrderedRange,
|
||||
isValidHhmm,
|
||||
lateTailDays,
|
||||
@@ -68,7 +67,31 @@ export function DoseWindowEditor({
|
||||
const generatedId = useId();
|
||||
const prefix = idPrefix ?? generatedId;
|
||||
|
||||
const rows = useMemo(() => buildRows(timesOfDay, value), [timesOfDay, value]);
|
||||
// v1.25.13 — the switch reflects the user's INTENT to customise, held in
|
||||
// local state, because it cannot be reconstructed from `value` alone: the
|
||||
// persisted contract stores ONLY ranges that differ from the default band
|
||||
// (`rowsToEntries` drops a default-equal range so the column stays minimal).
|
||||
// The former editor seeded the range from the default band on toggle-ON and
|
||||
// emitted immediately, so serialisation dropped it right back to `[]`, the
|
||||
// controlled `checked={row.custom}` recomputed `false`, and the switch
|
||||
// snapped off — the toggle could never turn on without first editing a range
|
||||
// the editor wouldn't reveal until the toggle was on (a deadlock). Tracking
|
||||
// the opened set here decouples the toggle from serialisation: the row shows
|
||||
// its default band, and an explicit range only persists once it differs.
|
||||
const [opened, setOpened] = useState<ReadonlySet<string>>(
|
||||
() => new Set<string>(),
|
||||
);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const base = buildRows(timesOfDay, value);
|
||||
if (opened.size === 0) return base;
|
||||
// A row the user opened reads as custom even while its range still equals
|
||||
// the default band (nothing persisted yet); a row already carrying a
|
||||
// stored explicit range is custom via `value` regardless.
|
||||
return base.map((r) =>
|
||||
opened.has(r.timeOfDay) ? { ...r, custom: true } : r,
|
||||
);
|
||||
}, [timesOfDay, value, opened]);
|
||||
|
||||
// Rebuild the persisted set from the next row state and bubble it up.
|
||||
const emit = useCallback(
|
||||
@@ -89,12 +112,17 @@ export function DoseWindowEditor({
|
||||
|
||||
const toggleCustom = useCallback(
|
||||
(row: DoseWindowRow, custom: boolean) => {
|
||||
if (custom) {
|
||||
// Switching ON seeds the range from the current default band so
|
||||
// the user starts from the same band the engine would derive.
|
||||
const def = defaultBandForTime(row.timeOfDay);
|
||||
setRow(row.timeOfDay, { custom: true, start: def.start, end: def.end });
|
||||
} else {
|
||||
setOpened((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (custom) next.add(row.timeOfDay);
|
||||
else next.delete(row.timeOfDay);
|
||||
return next;
|
||||
});
|
||||
// Switching OFF drops any persisted explicit range for the slot (back to
|
||||
// the default derivation). Switching ON needs no emit — the row already
|
||||
// renders the default band and the opened set keeps the switch on; a
|
||||
// range only persists once the user edits it away from the default.
|
||||
if (!custom) {
|
||||
setRow(row.timeOfDay, { custom: false });
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user