fix(work-orders): toast after assigned-to save on the board

Show the save toast only after the board PATCH succeeds.
This commit is contained in:
Arthur Bassi 2026-09-01 10:20:38 -03:00
parent 0ee2879234
commit 7e1dcb7a36
5 changed files with 219 additions and 3 deletions

View file

@ -1,6 +1,7 @@
import type { CSSProperties } from "react";
import type { LocationOption, WorkOrderUser } from "@/domain/work-orders/types/work-order";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { applyAssignedToTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts";
import { AssignedToCell } from "./cells/assigned-to-cell";
import { DateCell } from "./cells/date-cell";
import { DueDateCell } from "./cells/due-date-cell";
@ -111,9 +112,7 @@ export function WoTableRowCells({
dispatcherId={row.dispatcherId}
users={users}
highlight={!row.dispatcherId}
onSave={(id, name) =>
handlers.onPatch(row.id, { dispatcherId: id, dispatcherName: name })
}
onSave={(id, name) => applyAssignedToTableSave(handlers.onPatch, row.id, id, name)}
/>
</td>
<td

View file

@ -80,3 +80,12 @@ export function applyVendorTableSave(
onSuccess: isAssignedVendorId(fields.vendorId) ? notifyVendorAssigned : notifyWorkOrderSaved,
});
}
export function applyAssignedToTableSave(
onPatch: PersistPatchFn,
id: string | number,
dispatcherId: string,
dispatcherName: string,
) {
onPatch(id, { dispatcherId, dispatcherName }, { onSuccess: notifyWorkOrderSaved });
}

View file

@ -0,0 +1,70 @@
import { fireEvent, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { WoTableRowCells } from "@/app/(protected)/workorders/_components/list/table/wo-table-row-cells";
import type { WoTableRowHandlers } from "@/app/(protected)/workorders/_components/list/table/wo-table-row";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { notifyWorkOrderSaved } from "@/domain/work-orders/utils/work-order-feedback-toasts";
import { renderWithProviders } from "@/test/test-utils";
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/site-cell", () => ({
SiteCell: () => null,
}));
vi.mock(
"@/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell",
() => ({
EditableWONumberCell: () => null,
}),
);
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/type-cell", () => ({
TypeCell: () => null,
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/date-cell", () => ({
DateCell: () => null,
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/due-date-cell", () => ({
DueDateCell: () => null,
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({
AssignedToCell: ({ onSave }: { onSave: (id: string, name: string) => void }) => (
<button type="button" onClick={() => onSave("u2", "Admin test")}>
persist-assigned
</button>
),
}));
const row = { id: 7, dispatcherId: "u1" } as WorkOrderTableRow;
describe("WoTableRowCells assigned-to persist", () => {
it("forwards assigned-to save through patch onSuccess instead of onPatchRow", () => {
const onPatch = vi.fn();
const onPatchRow = vi.fn();
const handlers = { onPatch } as unknown as WoTableRowHandlers;
renderWithProviders(
<table>
<tbody>
<tr>
<WoTableRowCells
row={row}
allRows={[]}
users={[]}
sites={[]}
handlers={handlers}
onPatchRow={onPatchRow}
/>
</tr>
</tbody>
</table>,
{ withAuth: false },
);
fireEvent.click(screen.getByRole("button", { name: "persist-assigned" }));
expect(onPatchRow).not.toHaveBeenCalled();
expect(onPatch).toHaveBeenCalledWith(
7,
{ dispatcherId: "u2", dispatcherName: "Admin test" },
{ onSuccess: notifyWorkOrderSaved },
);
});
});

View file

@ -0,0 +1,97 @@
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 { toast } from "react-toastify";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
import {
applyAssignedToTableSave,
workOrderSavedMessage,
} from "@/domain/work-orders/utils/work-order-feedback-toasts";
const patchBoardField = vi.fn();
vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
workOrdersApi: {
patchBoardField: (...args: unknown[]) => patchBoardField(...args),
},
}));
vi.mock("react-toastify", () => ({
toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn() },
}));
const ROW = {
id: 7,
dispatcherId: "u1",
dispatcherName: "Admin",
rowVersion: "v1",
} as WorkOrderTableRow;
function makeWrapper() {
const client = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return function Wrapper({ children }: { children: ReactNode }) {
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
};
}
describe("useWorkOrderTableMutations assigned-to toast", () => {
beforeEach(() => {
patchBoardField.mockReset();
vi.mocked(toast.success).mockClear();
vi.mocked(toast.error).mockClear();
});
it("toasts assigned-to save only after the board PATCH succeeds", async () => {
let release: () => void = () => undefined;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
patchBoardField.mockImplementation(async () => {
await gate;
return { ...ROW, dispatcherId: "u2", dispatcherName: "Admin test", rowVersion: "v2" };
});
const { result } = renderHook(
() =>
useWorkOrderTableMutations({
onPatch: () => undefined,
clearPatch: () => undefined,
getRow: () => ROW,
}),
{ wrapper: makeWrapper() },
);
act(() => {
applyAssignedToTableSave(result.current.patchField, 7, "u2", "Admin test");
});
expect(toast.success).not.toHaveBeenCalled();
release();
await waitFor(() => expect(toast.success).toHaveBeenCalledWith(workOrderSavedMessage()));
});
it("does not toast assigned-to save when the board PATCH fails", async () => {
patchBoardField.mockRejectedValue(new Error("Failed to update work order"));
const { result } = renderHook(
() =>
useWorkOrderTableMutations({
onPatch: () => undefined,
clearPatch: () => undefined,
getRow: () => ROW,
}),
{ wrapper: makeWrapper() },
);
act(() => {
applyAssignedToTableSave(result.current.patchField, 7, "u2", "Admin test");
});
await waitFor(() => expect(toast.error).toHaveBeenCalled());
expect(toast.success).not.toHaveBeenCalled();
});
});

View file

@ -2,10 +2,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { toast } from "react-toastify";
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
import {
applyAssignedToTableSave,
applyVendorTableSave,
isVendorAssignmentPatch,
notifySlideOverSaveSuccess,
notifyVendorAssigned,
notifyWorkOrderSaved,
patchWorkOrderAsCanceled,
patchWorkOrderAsCompleted,
vendorAssignedMessage,
@ -137,6 +139,45 @@ describe("work-order-feedback-toasts", () => {
expect(toast.success).not.toHaveBeenCalledWith(vendorAssignedMessage());
});
it("toasts generic save after assigned-to persist onSuccess", () => {
const onPatch = vi.fn();
applyAssignedToTableSave(onPatch, 7, "u2", "Admin test");
expect(onPatch).toHaveBeenCalledWith(
7,
{ dispatcherId: "u2", dispatcherName: "Admin test" },
{ onSuccess: notifyWorkOrderSaved },
);
expect(toast.success).not.toHaveBeenCalled();
const { onSuccess } = onPatch.mock.calls[0][2] as { onSuccess: () => void };
onSuccess();
expect(toast.success).toHaveBeenCalledWith(workOrderSavedMessage());
expect(toast.warning).not.toHaveBeenCalled();
});
it("toasts the same save copy after unassign persist onSuccess", () => {
const onPatch = vi.fn();
applyAssignedToTableSave(onPatch, 7, "", "");
expect(onPatch).toHaveBeenCalledWith(
7,
{ dispatcherId: "", dispatcherName: "" },
{ onSuccess: notifyWorkOrderSaved },
);
expect(toast.success).not.toHaveBeenCalled();
const { onSuccess } = onPatch.mock.calls[0][2] as { onSuccess: () => void };
onSuccess();
expect(toast.success).toHaveBeenCalledWith(workOrderSavedMessage());
});
it("does not toast assigned-to save when persist onSuccess is skipped", () => {
const onPatch = vi.fn();
applyAssignedToTableSave(onPatch, 7, "u2", "Admin test");
expect(toast.success).not.toHaveBeenCalled();
});
it("completes a work order without a success toast", () => {
const patchField = vi.fn();
patchWorkOrderAsCompleted(patchField, 42);