mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
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:
1 parent
971d5d32cc
commit
4721914cac
13 files changed
+56
-33
No files matched your search
@@ -393,21 +393,22 @@ describe("submit invalidation — the quick-add is a capture-picker surface, not
|
||||
);
|
||||
|
||||
it("routes the intake submit through the blessed invalidateMedicationReads (source pin)", () => {
|
||||
expect(componentSrc).toContain(
|
||||
"await invalidateMedicationReads(queryClient);",
|
||||
);
|
||||
expect(componentSrc).not.toContain(
|
||||
"invalidateKeys(queryClient, medicationDependentKeys)",
|
||||
);
|
||||
// The submit handler pairs the blessed helper with the inline
|
||||
// compliance-chart fan-out AFTER the intake POST landed.
|
||||
// AFTER the intake POST landed, and after the sheet was told it is done:
|
||||
// the save does not wait for the refetches before it closes.
|
||||
expect(componentSrc).toMatch(
|
||||
/async function handleSubmit\([\s\S]*?apiPost[\s\S]*?await invalidateMedicationReads\(queryClient\);[\s\S]*?complianceChartInline\(\)/,
|
||||
/async function handleSubmit\([\s\S]*?apiPost[\s\S]*?onSuccess\?\.\(\);\s*(?:\/\/[^\n]*\n\s*)*void invalidateMedicationReads\(queryClient\);/,
|
||||
);
|
||||
// The bundle carries every inline compliance chart, so no second sweep.
|
||||
expect(medicationDependentKeys).toContainEqual(
|
||||
queryKeys.complianceChartInline(),
|
||||
);
|
||||
});
|
||||
|
||||
it("forces the inactive Today hero + dashboard reads to refetch after submit", async () => {
|
||||
// The helper the submit path awaits — asserted directly, same shape as
|
||||
// The helper the submit path calls — asserted directly, same shape as
|
||||
// the take-all-due suite: the bundle fans out (N keys), then the two
|
||||
// daily reads are invalidated with `refetchType: "inactive"`, which is
|
||||
// what actually refetches them while unmounted.
|
||||
|
||||
@@ -235,13 +235,6 @@ export function MedicationIntakeQuickAdd({
|
||||
);
|
||||
const eventId = created?.id;
|
||||
|
||||
await invalidateMedicationReads(queryClient);
|
||||
// Fan-out to every inline compliance chart key (one per medication)
|
||||
// so the detail-page tile refreshes when the user re-enters it.
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.complianceChartInline(),
|
||||
});
|
||||
|
||||
// v1.36.x — the shared decision, not a fourth copy of it. This surface
|
||||
// was left out of the collapse that took the other three: it offered a
|
||||
// delegate an Undo hitting `DELETE /api/medications/{id}/intake/{id}`,
|
||||
@@ -272,6 +265,9 @@ export function MedicationIntakeQuickAdd({
|
||||
toast.success(t("common.saved"));
|
||||
}
|
||||
onSuccess?.();
|
||||
// The dose is stored; the sheet does not wait for the refetches. The
|
||||
// bundle covers every inline compliance chart as well.
|
||||
void invalidateMedicationReads(queryClient);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof ApiError && err.message
|
||||
|
||||
@@ -127,15 +127,15 @@ export function useMeasurementEdit({
|
||||
}) => {
|
||||
await apiPut(`/api/measurements/${id}`, { value, measuredAt, notes });
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await invalidateKeys(queryClient, measurementDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
onSuccess: () => {
|
||||
setEditSeed({
|
||||
value: editValue,
|
||||
measuredAt: editMeasuredAt,
|
||||
notes: editNotes,
|
||||
});
|
||||
dismissEdit();
|
||||
void invalidateKeys(queryClient, measurementDependentKeys);
|
||||
void refetchInactiveDailyReads(queryClient);
|
||||
},
|
||||
onError: (err) => {
|
||||
// A 409 names its reason in `meta.errorCode`: the edit collides with
|
||||
|
||||
@@ -528,8 +528,6 @@ export function MeasurementForm({
|
||||
setPulse("");
|
||||
setPairedGlucose("");
|
||||
setNotes("");
|
||||
await invalidateKeys(queryClient, measurementDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
// v1.36.x — a delegate's receipt names the record. "Saved" alone is
|
||||
// the one confirmation somebody acting for another person does not
|
||||
// need, and the reading has just left their own history for good.
|
||||
@@ -544,6 +542,10 @@ export function MeasurementForm({
|
||||
: undefined,
|
||||
);
|
||||
onSuccess?.();
|
||||
// The reading is stored; the sheet does not wait for every read that
|
||||
// shows it to come back.
|
||||
void invalidateKeys(queryClient, measurementDependentKeys);
|
||||
void refetchInactiveDailyReads(queryClient);
|
||||
} catch (err) {
|
||||
setError(localizedApiError(err, t, "measurements.saveError"));
|
||||
} finally {
|
||||
|
||||
@@ -257,13 +257,13 @@ export function MoodForm({
|
||||
if (tagKeys.length > 0) recordUse(tagKeys);
|
||||
resetForm();
|
||||
setMoreTagsOpen(false);
|
||||
await invalidateKeys(queryClient, moodDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
toast.success(t("common.saved"));
|
||||
// The entry landed, but a tag picked here may have been archived or
|
||||
// deleted elsewhere since the picker loaded; the server says so.
|
||||
if (droppedAnyLinkKeys(saved)) toast.warning(t("mood.tagsNotSaved"));
|
||||
onSuccess?.();
|
||||
void invalidateKeys(queryClient, moodDependentKeys);
|
||||
void refetchInactiveDailyReads(queryClient);
|
||||
} catch (err) {
|
||||
setError(localizedApiError(err, t, "mood.saveError"));
|
||||
} finally {
|
||||
|
||||
@@ -137,9 +137,11 @@ export function invalidateSymptomReads(qc: QueryClient) {
|
||||
]);
|
||||
}
|
||||
|
||||
// Not returned, so a save reports as soon as it lands rather than after
|
||||
// every read it touches has refetched.
|
||||
function useInvalidateSymptoms() {
|
||||
const qc = useQueryClient();
|
||||
return () => invalidateSymptomReads(qc);
|
||||
return () => void invalidateSymptomReads(qc);
|
||||
}
|
||||
|
||||
export function useCreateSymptomDefinition() {
|
||||
|
||||
@@ -125,8 +125,10 @@ export function useVaccination(id: string | null) {
|
||||
*/
|
||||
export function useVaccinationMutations() {
|
||||
const qc = useQueryClient();
|
||||
// Not returned: a mutation would wait for every refetch before it settles,
|
||||
// and the sheet would stay open with its spinner until then.
|
||||
const invalidate = () =>
|
||||
Promise.all([
|
||||
void Promise.all([
|
||||
invalidateKeys(qc, vaccinationDependentKeys),
|
||||
invalidateReminderReads(qc),
|
||||
]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -166,10 +166,11 @@ export function useEncounterMutations() {
|
||||
*
|
||||
* So the fan-out is the visit roots PLUS the reminder reads, through the same
|
||||
* helper the Vorsorge surfaces use, rather than a second sweep that could
|
||||
* drift from it.
|
||||
* drift from it. Not returned: a mutation would wait for every refetch
|
||||
* before it settles, and the sheet would stay open until then.
|
||||
*/
|
||||
const invalidate = () =>
|
||||
Promise.all([
|
||||
void Promise.all([
|
||||
invalidateKeys(qc, encounterDependentKeys),
|
||||
invalidateReminderReads(qc),
|
||||
]);
|
||||
|
||||
@@ -117,7 +117,8 @@ export function useMeasurementReminders(enabled = true) {
|
||||
|
||||
export function useMeasurementReminderMutations() {
|
||||
const qc = useQueryClient();
|
||||
const invalidate = () => invalidateReminderReads(qc);
|
||||
// Not returned: a mutation would wait for every refetch before it settles.
|
||||
const invalidate = () => void invalidateReminderReads(qc);
|
||||
|
||||
const create = useMutation({
|
||||
mutationKey: queryKeys.measurementReminderCreate(),
|
||||
|
||||
@@ -62,9 +62,10 @@ export function usePractitionerMutations() {
|
||||
const qc = useQueryClient();
|
||||
// Renaming a practice changes the label a rail appointment renders under,
|
||||
// and the daily reads are unmounted from the address book — pair the
|
||||
// bundle invalidation with the forced inactive refetch.
|
||||
// bundle invalidation with the forced inactive refetch. Not returned, so the
|
||||
// sheet closes without waiting for the refetches.
|
||||
const invalidate = () =>
|
||||
Promise.all([
|
||||
void Promise.all([
|
||||
invalidateKeys(qc, encounterDependentKeys),
|
||||
refetchInactiveDailyReads(qc),
|
||||
]);
|
||||
|
||||
Reference in new issue
Block a user