From f2ba62fe79e180a3e172a94c74f147448dfa37d1 Mon Sep 17 00:00:00 2001 From: npal Date: Mon, 28 Sep 2026 18:30:52 -0500 Subject: [PATCH] fix: stop checklist toggles from dropping fast consecutive updates TanStack Query v5 only runs a mutate() call's onSuccess for the latest call on a shared observer, so checking two items before the first request resolved silently dropped the first item's onItemUpdated call, leaving the portal's checkbox out of sync with the server. Switch to mutateAsync so each call's result is handled independently. Co-Authored-By: Claude Sonnet 5 --- .../_components/checklist-section.tsx | 10 +++++--- src/test/app/v/checklist-section.test.tsx | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/app/v/[token]/dispatch/_components/checklist-section.tsx b/src/app/v/[token]/dispatch/_components/checklist-section.tsx index 850c2b6d..4c1acb2e 100644 --- a/src/app/v/[token]/dispatch/_components/checklist-section.tsx +++ b/src/app/v/[token]/dispatch/_components/checklist-section.tsx @@ -22,10 +22,12 @@ export function ChecklistSection({ const toggle = (item: VendorPortalChecklistItem) => { if (locked) return; - updateItem.mutate( - { itemId: item.id, isCompleted: !item.isCompleted }, - { onSuccess: (updated) => onItemUpdated(updated) }, - ); + // mutateAsync resolves per call, unlike mutate's onSuccess which only fires for the + // latest call on this shared observer — needed so two quick toggles both report back. + void updateItem + .mutateAsync({ itemId: item.id, isCompleted: !item.isCompleted }) + .then((updated) => onItemUpdated(updated)) + .catch(() => undefined); }; if (!items.length) { diff --git a/src/test/app/v/checklist-section.test.tsx b/src/test/app/v/checklist-section.test.tsx index 2c853f7e..e0e1013e 100644 --- a/src/test/app/v/checklist-section.test.tsx +++ b/src/test/app/v/checklist-section.test.tsx @@ -67,4 +67,29 @@ describe("ChecklistSection", () => { await screen.findByText("Unable to update the checklist. Please try again."), ).toBeInTheDocument(); }); + + it("reports every successful toggle when two items are checked before the first responds", async () => { + const a: VendorPortalChecklistItem = { id: 1, itemText: "Check filters", isCompleted: false }; + const b: VendorPortalChecklistItem = { id: 2, itemText: "Check belts", isCompleted: false }; + const resolvers: Array<() => void> = []; + vi.spyOn(vendorPortalApi, "updateChecklistItem").mockImplementation( + (_token, _dispatchId, itemId) => + new Promise((resolve) => { + resolvers.push(() => resolve({ ...(itemId === 1 ? a : b), isCompleted: true })); + }), + ); + const onItemUpdated = vi.fn(); + renderSection({ items: [a, b] }, onItemUpdated); + + const [boxA, boxB] = screen.getAllByRole("checkbox"); + fireEvent.click(boxA); + fireEvent.click(boxB); + + await waitFor(() => expect(resolvers).toHaveLength(2)); + resolvers.forEach((resolve) => resolve()); + + await waitFor(() => + expect(onItemUpdated.mock.calls.map(([updated]) => updated.id)).toEqual([1, 2]), + ); + }); });