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 <noreply@anthropic.com>
This commit is contained in:
npal 2026-09-28 18:30:52 -05:00
parent 736157467c
commit f2ba62fe79
2 changed files with 31 additions and 4 deletions

View file

@ -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) {

View file

@ -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]),
);
});
});