mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-03 11:33:22 +00:00
fix(workorders): carry each saved version into the panel before the next save
A save from the detail panel left the cached detail on the pre-save version until its refetch settled, so a quick second save, Cancel WO or Complete could conflict. The saved row's version now updates the detail cache before the patch queue releases, and panel patches read their base at run time.
This commit is contained in:
parent
e6548ffa17
commit
522fea4db7
2 changed files with 107 additions and 7 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { useMemo } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { type QueryClient, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "react-toastify";
|
||||
import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
|
||||
import {
|
||||
|
|
@ -17,6 +17,7 @@ import {
|
|||
type BoardOp,
|
||||
} from "@/domain/work-orders/mappers/work-order-board-patch-mapper";
|
||||
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-mapper";
|
||||
import type { WorkOrderBoardDetail } from "@/domain/work-orders/types/work-order-board-detail";
|
||||
import type {
|
||||
WorkOrderTablePatch,
|
||||
WorkOrderTableRow,
|
||||
|
|
@ -126,6 +127,51 @@ export async function runWorkOrderTablePatch(
|
|||
return { kind: "board", row: boardRow };
|
||||
}
|
||||
|
||||
function readDetailRow(
|
||||
queryClient: QueryClient,
|
||||
id: string | number,
|
||||
): WorkOrderTableRow | undefined {
|
||||
return queryClient.getQueryData<WorkOrderBoardDetail>(queryKeys.workOrders.boardDetail(id))?.info;
|
||||
}
|
||||
|
||||
/** Carry a saved row's version into the open panel's detail before the next queued patch runs. */
|
||||
function syncDetailRowVersion(
|
||||
queryClient: QueryClient,
|
||||
id: string | number,
|
||||
row: WorkOrderTableRow,
|
||||
) {
|
||||
queryClient.setQueryData<WorkOrderBoardDetail>(queryKeys.workOrders.boardDetail(id), (detail) =>
|
||||
detail && row.rowVersion
|
||||
? {
|
||||
...detail,
|
||||
info: {
|
||||
...detail.info,
|
||||
rowVersion: row.rowVersion,
|
||||
dispatchRowVersion: row.dispatchRowVersion || detail.info.dispatchRowVersion,
|
||||
},
|
||||
}
|
||||
: detail,
|
||||
);
|
||||
}
|
||||
|
||||
async function runPatchAndSyncDetail(
|
||||
queryClient: QueryClient,
|
||||
id: string | number,
|
||||
patch: WorkOrderTablePatch,
|
||||
readBase: (id: string | number) => WorkOrderTableRow | undefined,
|
||||
): Promise<PatchResult> {
|
||||
try {
|
||||
const result = await runWorkOrderTablePatch(id, patch, readBase);
|
||||
if (result.kind === "board") syncDetailRowVersion(queryClient, id, result.row);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof BoardPatchPartialSaveError) {
|
||||
syncDetailRowVersion(queryClient, id, error.partialRow);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function useWorkOrderTableMutations(
|
||||
onPatchOrHandlers:
|
||||
((id: string | number, patch: WorkOrderTablePatch) => void) | WorkOrderTablePatchHandlers,
|
||||
|
|
@ -154,15 +200,25 @@ export function useWorkOrderTableMutations(
|
|||
id,
|
||||
persistPatch,
|
||||
baseRow,
|
||||
fallbackRow,
|
||||
}: {
|
||||
id: string | number;
|
||||
patch: WorkOrderTablePatch;
|
||||
persistPatch: WorkOrderTablePatch;
|
||||
/** Server row from a just-settled patch; its rowVersion is fresher than the table snapshot. */
|
||||
baseRow?: WorkOrderTableRow;
|
||||
/** Used when neither the table nor the detail cache holds the work order. */
|
||||
fallbackRow?: WorkOrderTableRow;
|
||||
}) =>
|
||||
enqueuePatch(id, () =>
|
||||
runWorkOrderTablePatch(id, persistPatch, baseRow ? () => baseRow : getRow),
|
||||
runPatchAndSyncDetail(
|
||||
queryClient,
|
||||
id,
|
||||
persistPatch,
|
||||
baseRow
|
||||
? () => baseRow
|
||||
: (rowId) => getRow?.(rowId) ?? readDetailRow(queryClient, rowId) ?? fallbackRow,
|
||||
),
|
||||
),
|
||||
onMutate: async ({ id, persistPatch }) => {
|
||||
onPatch(id, persistPatch);
|
||||
|
|
@ -196,9 +252,9 @@ export function useWorkOrderTableMutations(
|
|||
mergeAutoSchedulePatch(getRow?.(id), patch);
|
||||
|
||||
const patchField: WorkOrderTablePatchFn = (id, patch, options) => {
|
||||
const baseRow = getRow?.(id) ? undefined : options?.fallbackRow;
|
||||
const persistPatch = baseRow ? mergeAutoSchedulePatch(baseRow, patch) : resolvePatch(id, patch);
|
||||
void patchMutation.mutateAsync({ id, patch, persistPatch, baseRow }).then(
|
||||
const fallbackRow = options?.fallbackRow;
|
||||
const persistPatch = mergeAutoSchedulePatch(getRow?.(id) ?? fallbackRow, patch);
|
||||
void patchMutation.mutateAsync({ id, patch, persistPatch, fallbackRow }).then(
|
||||
() => options?.onSuccess?.(),
|
||||
(error: Error) => options?.onError?.(error),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ 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 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";
|
||||
|
||||
const patchBoardField = vi.fn();
|
||||
|
||||
|
|
@ -22,10 +24,13 @@ import {
|
|||
useWorkOrderTableMutations,
|
||||
} from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
||||
|
||||
function makeWrapper() {
|
||||
const client = new QueryClient({
|
||||
function makeClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
}
|
||||
|
||||
function makeWrapper(client = makeClient()) {
|
||||
return function Wrapper({ children }: { children: ReactNode }) {
|
||||
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
|
||||
};
|
||||
|
|
@ -79,6 +84,45 @@ describe("useWorkOrderTableMutations for a work order outside the loaded list",
|
|||
);
|
||||
});
|
||||
|
||||
it("runs a queued second save on the first save's version, not the stale panel row", async () => {
|
||||
const client = makeClient();
|
||||
client.setQueryData<WorkOrderBoardDetail>(queryKeys.workOrders.boardDetail(9), {
|
||||
info: DETAIL_ROW,
|
||||
} as WorkOrderBoardDetail);
|
||||
let version = 0;
|
||||
patchBoardField.mockImplementation(async () => {
|
||||
version += 1;
|
||||
return { ...DETAIL_ROW, rowVersion: `saved-${version}` };
|
||||
});
|
||||
const onSecondSuccess = 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 });
|
||||
result.current.patchField(
|
||||
9,
|
||||
{ dueDate: "2026-10-16" },
|
||||
{ fallbackRow: DETAIL_ROW, onSuccess: onSecondSuccess },
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(onSecondSuccess).toHaveBeenCalled());
|
||||
expect(patchBoardField.mock.calls[0][1]).toMatchObject({ workOrderVersion: "AAAAAAAAB9E=" });
|
||||
expect(patchBoardField.mock.calls[1][1]).toMatchObject({ workOrderVersion: "saved-1" });
|
||||
expect(
|
||||
client.getQueryData<WorkOrderBoardDetail>(queryKeys.workOrders.boardDetail(9))?.info
|
||||
.rowVersion,
|
||||
).toBe("saved-2");
|
||||
});
|
||||
|
||||
it("prefers the table row when it still holds the work order", async () => {
|
||||
const tableRow = { ...DETAIL_ROW, rowVersion: "table-version" };
|
||||
const onSuccess = vi.fn();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue