mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 15:33:12 +00:00
fix(work-orders): show generic copy for unknown edit errors (SH-375)
This commit is contained in:
parent
440a3d0112
commit
fc25d8f7c7
3 changed files with 103 additions and 9 deletions
|
|
@ -24,10 +24,20 @@ export class BoardPatchValidationError extends Error {
|
|||
|
||||
export const DISPATCH_REQUIRED_MESSAGE = "Select a vendor company before saving.";
|
||||
|
||||
/** why: backend field names must never be exposed through user-facing board toasts. */
|
||||
export const BOARD_PATCH_FAILED_MESSAGE = "Unable to save the work order. Please try again.";
|
||||
|
||||
export const NO_PERSISTABLE_FIELDS_MESSAGE =
|
||||
"These changes cannot be saved on the board. Refresh and edit a supported field.";
|
||||
|
||||
const KNOWN_VALIDATION_COPY: ReadonlyMap<string, string> = new Map([
|
||||
["DispatchRequired", DISPATCH_REQUIRED_MESSAGE],
|
||||
["NoPersistableFields", NO_PERSISTABLE_FIELDS_MESSAGE],
|
||||
]);
|
||||
|
||||
/**
|
||||
* why: board PATCH validation text ("vendorId is required…", object ids, backend codes)
|
||||
* must never reach the screen. Unknown codes fall back to generic copy, not `error.message`.
|
||||
*/
|
||||
export function boardPatchValidationMessage(error: BoardPatchValidationError): string {
|
||||
if (error.code === "DispatchRequired") {
|
||||
return DISPATCH_REQUIRED_MESSAGE;
|
||||
}
|
||||
return error.message;
|
||||
return KNOWN_VALIDATION_COPY.get(error.code) ?? BOARD_PATCH_FAILED_MESSAGE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
boardPatchValidationMessage,
|
||||
BoardPatchConflictError,
|
||||
BoardPatchValidationError,
|
||||
NO_PERSISTABLE_FIELDS_MESSAGE,
|
||||
} from "@/domain/work-orders/errors/board-patch-errors";
|
||||
import { mapApiStatusToWizard } from "@/domain/work-orders/mappers/wizard-draft-mapper";
|
||||
import {
|
||||
|
|
@ -58,10 +59,7 @@ async function applyBoardFieldOps(
|
|||
): Promise<WorkOrderTableRow> {
|
||||
const ops = expandTablePatchToBoardFieldOps(patch);
|
||||
if (ops.length === 0) {
|
||||
throw new BoardPatchValidationError(
|
||||
"NoPersistableFields",
|
||||
"These changes cannot be saved on the board. Refresh and edit a supported field.",
|
||||
);
|
||||
throw new BoardPatchValidationError("NoPersistableFields", NO_PERSISTABLE_FIELDS_MESSAGE);
|
||||
}
|
||||
|
||||
const current = getRow?.(id);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
BOARD_PATCH_FAILED_MESSAGE,
|
||||
DISPATCH_REQUIRED_MESSAGE,
|
||||
} from "@/domain/work-orders/errors/board-patch-errors";
|
||||
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 { createTestQueryClient } from "@/test/test-utils";
|
||||
|
||||
vi.mock("react-toastify", () => ({
|
||||
toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn() },
|
||||
}));
|
||||
|
||||
const ROW = { id: 7, rowVersion: "v1" } as WorkOrderTableRow;
|
||||
|
||||
function wrapper({ children }: { children: ReactNode }) {
|
||||
return <QueryClientProvider client={createTestQueryClient()}>{children}</QueryClientProvider>;
|
||||
}
|
||||
|
||||
/** Board PATCH goes through the real ky client; only the network response is faked. */
|
||||
function respondWith(status: number, body: Record<string, unknown>) {
|
||||
globalThis.fetch = vi.fn(
|
||||
async () =>
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
) as typeof fetch;
|
||||
}
|
||||
|
||||
async function patchAndGetToast(): Promise<unknown> {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useWorkOrderTableMutations({
|
||||
onPatch: () => undefined,
|
||||
clearPatch: () => undefined,
|
||||
getRow: () => ROW,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
act(() => {
|
||||
result.current.patchField(7, { woNumber: "WO-100" });
|
||||
});
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalledTimes(1));
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
return vi.mocked(toast.error).mock.calls[0]?.[0];
|
||||
}
|
||||
|
||||
describe("useWorkOrderTableMutations edit error copy", () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(toast.error).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("shows generic copy instead of backend text for an unknown 422 code", async () => {
|
||||
respondWith(422, {
|
||||
code: "InvalidLifecycleStatus",
|
||||
message: "lifecycleStatus cannot move from Completed for workOrderId 7.",
|
||||
});
|
||||
|
||||
const message = await patchAndGetToast();
|
||||
|
||||
expect(message).toBe(BOARD_PATCH_FAILED_MESSAGE);
|
||||
expect(message).not.toMatch(/lifecycleStatus|workOrderId|InvalidLifecycleStatus/);
|
||||
});
|
||||
|
||||
it("keeps specific copy for DispatchRequired", async () => {
|
||||
respondWith(422, {
|
||||
code: "DispatchRequired",
|
||||
message: "vendorId is required when setting appointment fields.",
|
||||
});
|
||||
|
||||
const message = await patchAndGetToast();
|
||||
|
||||
expect(message).toBe(DISPATCH_REQUIRED_MESSAGE);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue