From b5c97062b85c6ba0a1e85709acd87996d2f877c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 11 Nov 2026 21:40:02 +0100 Subject: [PATCH] fix(timeline): close the life event sheet before the timeline refetch The life event hooks returned the timeline and day refresh from onSuccess, which a mutation waits for, so saving or removing an event kept the sheet spinning until the whole timeline had reloaded. The refresh is now started and not returned, like the other record sheets. --- .../use-life-event-mutations.test.ts | 42 +++++++++++++++++++ src/components/timeline/use-timeline.ts | 4 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/components/timeline/__tests__/use-life-event-mutations.test.ts diff --git a/src/components/timeline/__tests__/use-life-event-mutations.test.ts b/src/components/timeline/__tests__/use-life-event-mutations.test.ts new file mode 100644 index 000000000..8a973de6c --- /dev/null +++ b/src/components/timeline/__tests__/use-life-event-mutations.test.ts @@ -0,0 +1,42 @@ +/** + * A life event save settles once the server stored it. + * + * The hooks returned the timeline refresh from `onSuccess`, which a mutation + * waits for, so the sheet kept its spinner until the timeline had refetched. + */ +import { describe, expect, it, vi } from "vitest"; + +const client = vi.hoisted(() => ({ + // A refetch that never answers, as on a slow connection. + invalidateQueries: vi.fn(() => new Promise(() => {})), + getQueryCache: () => ({ findAll: () => [] }), +})); + +vi.mock("react", async (importOriginal) => ({ + ...(await importOriginal()), + useRef: (initial: T) => ({ current: initial }), +})); + +vi.mock("@tanstack/react-query", () => ({ + useQuery: (options: unknown) => options, + // The hook's options are what is under test; return them as they are. + useMutation: (options: unknown) => options, + useQueryClient: () => client, +})); + +import { useLifeEventMutations } from "../use-timeline"; + +describe("life event saves", () => { + it.each(["create", "update", "remove"] as const)( + "%s starts the refresh and does not wait for it", + (name) => { + client.invalidateQueries.mockClear(); + const options = useLifeEventMutations()[name] as unknown as { + onSuccess: () => unknown; + }; + + expect(options.onSuccess()).toBeUndefined(); + expect(client.invalidateQueries).toHaveBeenCalled(); + }, + ); +}); diff --git a/src/components/timeline/use-timeline.ts b/src/components/timeline/use-timeline.ts index ab0aaae45..0db31301f 100644 --- a/src/components/timeline/use-timeline.ts +++ b/src/components/timeline/use-timeline.ts @@ -73,8 +73,10 @@ export function useLifeEvents(enabled = true) { export function useLifeEventMutations() { const qc = useQueryClient(); + // Not returned, so the sheet closes without waiting for the timeline to + // refetch. const invalidate = () => - invalidateKeys(qc, [queryKeys.timelineRoot(), queryKeys.dayRoot()]); + void invalidateKeys(qc, [queryKeys.timelineRoot(), queryKeys.dayRoot()]); // One key until a save lands, so a retry after a timeout is not a second // event.