fix(forms): close the sheet as soon as a save lands

Saving a weight, a mood, a workout, a measurement edit or a quick-add
dose emptied the form and kept the button spinning until every read the
write touches had refetched (snapshot, digest, charts, insights). On a
slow connection that looked like nothing had been saved. The vaccination,
visit, address-book, symptom and check-up hooks did the same through an
onSuccess that returned the refetch, which a mutation waits for.

The forms now report and close first and start the refresh without
waiting for it; the hooks no longer return it. The quick-add loses a
second sweep of the inline compliance charts that the medication bundle
already covers, and its source pin follows.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 19:46:17 +01:00
1 parent 971d5d32cc
commit 4721914cac
13 files changed
+56 -33

No files matched your search

@@ -105,6 +105,22 @@ describe("saveManualWorkout — the wire", () => {
expect(keys).toContain(JSON.stringify(queryKeys.timelineRoot()));
});
it("returns once the entry is stored, without waiting for the refetches", async () => {
// The sheet closes on this answer; on a slow connection the reads it
// refreshes can take seconds, and the person would watch a spinner.
fetchSpy.mockResolvedValue(
envelope({ entries: [{ index: 0, status: "inserted" }] }),
);
const client = {
...fakeClient(),
invalidateQueries: vi.fn(() => new Promise(() => {})),
};
await expect(
saveManualWorkout(ENTRY, client as unknown as QueryClient),
).resolves.toBe("inserted");
});
it("treats a duplicate (the same form submitted twice) as saved", async () => {
fetchSpy.mockResolvedValue(
envelope({ entries: [{ index: 0, status: "duplicate" }] }),
@@ -53,8 +53,8 @@ export async function deleteManualWorkout(
): Promise<void> {
await apiDelete(`/api/workouts/${encodeURIComponent(id)}`);
queryClient.removeQueries({ queryKey: queryKeys.workoutDetail(id) });
await invalidateKeys(queryClient, workoutDependentKeys);
await refetchInactiveDailyReads(queryClient);
void invalidateKeys(queryClient, workoutDependentKeys);
void refetchInactiveDailyReads(queryClient);
}
/**
@@ -94,7 +94,8 @@ interface BatchResult {
export type ManualWorkoutSaveOutcome = "inserted" | "duplicate" | "updated";
/**
* Post the entry and refresh what shows workouts. A `duplicate` is a success:
* Post the entry and start refreshing what shows workouts; the caller reports
* the save without waiting for those reads. A `duplicate` is a success:
* the same form already landed this session (a second tap, a retry), so the
* row exists exactly once. A `skipped` entry was refused and throws.
*/
@@ -112,8 +113,8 @@ export async function saveManualWorkout(
// The form also opens from the capture picker on any page, where the
// Today hero and the dashboard snapshot are unmounted, so those two are
// refetched outright.
await invalidateKeys(queryClient, workoutDependentKeys);
await refetchInactiveDailyReads(queryClient);
void invalidateKeys(queryClient, workoutDependentKeys);
void refetchInactiveDailyReads(queryClient);
return status;
}