fix(workorders): retry off-list saves on the conflict's current version

This commit is contained in:
Alexandre Brandizzi 2026-10-02 00:18:43 -03:00
parent 47cdca55c2
commit 3e849f47de
2 changed files with 47 additions and 0 deletions

View file

@ -181,6 +181,9 @@ async function runPatchAndSyncDetail(
} catch (error) {
if (error instanceof BoardPatchPartialSaveError) {
recordSavedRow(sync, id, error.partialRow);
} else if (error instanceof BoardPatchConflictError && error.currentState) {
// A retry before the refetch lands must patch the server's current version.
recordSavedRow(sync, id, error.currentState);
}
throw error;
}

View file

@ -2,6 +2,7 @@ import type { ReactNode } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, renderHook, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { BoardPatchConflictError } from "@/domain/work-orders/errors/board-patch-errors";
import type { WorkOrderBoardDetail } from "@/domain/work-orders/types/work-order-board-detail";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { queryKeys } from "@/infra/query-key/query-key";
@ -195,6 +196,49 @@ describe("useWorkOrderTableMutations for a work order outside the loaded list",
expect(patchBoardField.mock.calls[1][1]).toMatchObject({ workOrderVersion: "saved-1" });
});
it("retries an off-list save on the conflict's current version, not the stale panel row", async () => {
const client = makeClient();
client.setQueryData<WorkOrderBoardDetail>(queryKeys.workOrders.boardDetail(9), {
info: DETAIL_ROW,
} as WorkOrderBoardDetail);
patchBoardField.mockRejectedValueOnce(
new BoardPatchConflictError("Work order was modified.", {
...DETAIL_ROW,
rowVersion: "server-current",
}),
);
const onRetrySuccess = vi.fn();
const onFirstError = vi.fn();
const { result } = renderHook(
() =>
useWorkOrderTableMutations({
onPatch: vi.fn(),
clearPatch: vi.fn(),
getRow: () => undefined,
}),
{ wrapper: makeWrapper(client) },
);
act(() => {
result.current.patchField(
9,
{ dueDate: "2026-10-09" },
{ fallbackRow: DETAIL_ROW, onError: onFirstError },
);
});
await waitFor(() => expect(onFirstError).toHaveBeenCalled());
act(() => {
result.current.patchField(
9,
{ dueDate: "2026-10-09" },
{ fallbackRow: DETAIL_ROW, onSuccess: onRetrySuccess },
);
});
await waitFor(() => expect(onRetrySuccess).toHaveBeenCalled());
expect(patchBoardField.mock.calls[1][1]).toMatchObject({ workOrderVersion: "server-current" });
});
it("prefers the table row when it still holds the work order", async () => {
const tableRow = { ...DETAIL_ROW, rowVersion: "table-version" };
const onSuccess = vi.fn();