From be975c792c0d950c34b9815b279168a6488e310a Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Thu, 20 Aug 2026 10:28:04 -0300 Subject: [PATCH] fix(work-orders): wait for Unassigned queue before empty search (SH-240) Keep SH-165 loading and error visible instead of a premature empty-search shell. --- .../_components/list/table/wo-table.tsx | 8 +- .../_hooks/use-work-orders-table-data.ts | 2 + .../work-orders/utils/week-day-nav-utils.ts | 15 ++- .../workorders/wo-table-search-empty.test.tsx | 113 ++++++++++++++++++ .../utils/week-day-nav-utils.test.ts | 43 ++++++- 5 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 src/test/app/(protected)/workorders/wo-table-search-empty.test.tsx diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx index 0ac11f1a..4706ce02 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx @@ -9,6 +9,7 @@ import type { WorkOrderTablePatch, WorkOrderTableRow, } from "@/domain/work-orders/types/work-order-table-row"; +import { isUnassignedQueueReady } from "@/domain/work-orders/utils/week-day-nav-utils"; import { DuplicateWoNumberDialog } from "@/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog"; import { WoTableSkeleton } from "@/app/(protected)/workorders/_components/loading/wo-table-skeleton"; import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types"; @@ -170,7 +171,12 @@ export function WoTable({ ); } - if (!tableData.advActive && tableData.noSearchResults) { + const showSearchEmpty = + !tableData.advActive && + tableData.noSearchResults && + isUnassignedQueueReady(tableData.unassignedLoading, tableData.unassignedError); + + if (showSearchEmpty) { return ( ({ + useLocationSites: () => ({ data: [] }), +})); + +vi.mock("@/app/(protected)/workorders/_components/list/table/use-wo-table-inline-create", () => ({ + useWoTableInlineCreate: () => ({ handleInlineSave: vi.fn(), saving: false }), +})); + +function tableData( + overrides: Partial = {}, +): ReturnTypeOfUseWorkOrdersTableData { + return { + rows: [], + isLoading: false, + isFetching: false, + error: null, + refetch: vi.fn(), + advActive: false, + advResults: [], + unassigned: [], + unassignedError: null, + unassignedLoading: false, + unscheduled: [], + displayGroups: [{ key: "05/18", label: "Mon ยท 05/18", date: "2026-05-18", items: [] }], + searchActive: true, + noSearchResults: true, + isEmptyWeek: false, + todayKey: null, + draftDayKey: null, + draft: { locationId: "", site: "" }, + setDraft: vi.fn(), + unassignedOpen: true, + setUnassignedOpen: vi.fn(), + unschedOpen: true, + setUnschedOpen: vi.fn(), + selectedId: null, + setSelectedId: vi.fn(), + selectedRow: null, + flashId: null, + triggerFlash: vi.fn(), + dragViz: null, + setDragViz: vi.fn(), + dragState: { current: { dragId: null, overId: null } }, + patchRow: vi.fn(), + clearRowPatch: vi.fn(), + reorderRows: vi.fn(), + endDrag: vi.fn(), + startDayDraft: vi.fn(), + cancelDraft: vi.fn(), + addRow: vi.fn(), + scopeTotalCount: 0, + totalCount: 0, + search: "no-match", + ...overrides, + } as ReturnTypeOfUseWorkOrdersTableData; +} + +function renderTable(data: ReturnTypeOfUseWorkOrdersTableData) { + return renderWithProviders( + , + { withAuth: false }, + ); +} + +describe("WoTable search empty vs Unassigned queue (SH-165/SH-240)", () => { + it("shows empty search only after the Unassigned queue has settled empty", () => { + const { getByText, queryByText } = renderTable(tableData()); + + expect(getByText("No work orders match your search")).toBeInTheDocument(); + expect(queryByText(/Loading unassigned work orders/)).not.toBeInTheDocument(); + }); + + it("keeps Unassigned loading visible instead of a premature empty search", () => { + const { getByText, queryByText } = renderTable( + tableData({ + noSearchResults: true, + unassignedLoading: true, + }), + ); + + expect(queryByText("No work orders match your search")).not.toBeInTheDocument(); + expect(getByText(/Loading unassigned work orders/)).toBeInTheDocument(); + }); + + it("keeps Unassigned error visible instead of hiding it behind empty search", () => { + const { getByText, queryByText } = renderTable( + tableData({ + noSearchResults: true, + unassignedError: new Error("queue unavailable"), + }), + ); + + expect(queryByText("No work orders match your search")).not.toBeInTheDocument(); + expect(getByText("queue unavailable")).toBeInTheDocument(); + }); +}); diff --git a/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts b/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts index bc56591d..0439dc61 100644 --- a/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts +++ b/src/test/domain/work-orders/utils/week-day-nav-utils.test.ts @@ -7,6 +7,7 @@ import { formatDayTabLabel, initialSelectedDay, isDayScopedSearchEmpty, + isUnassignedQueueReady, rowMatchesSelectedDay, scheduledDayTab, weekdaysMonFri, @@ -131,16 +132,50 @@ describe("week-day-nav-utils (SH-240)", () => { it("treats search as empty only when the selected day and Unassigned have no hits", () => { expect( - isDayScopedSearchEmpty({ searchActive: true, unassignedCount: 0, dayItemCount: 0 }), + isDayScopedSearchEmpty({ + searchActive: true, + unassignedCount: 0, + dayItemCount: 0, + unassignedQueueReady: true, + }), ).toBe(true); expect( - isDayScopedSearchEmpty({ searchActive: true, unassignedCount: 0, dayItemCount: 1 }), + isDayScopedSearchEmpty({ + searchActive: true, + unassignedCount: 0, + dayItemCount: 1, + unassignedQueueReady: true, + }), ).toBe(false); expect( - isDayScopedSearchEmpty({ searchActive: true, unassignedCount: 2, dayItemCount: 0 }), + isDayScopedSearchEmpty({ + searchActive: true, + unassignedCount: 2, + dayItemCount: 0, + unassignedQueueReady: true, + }), ).toBe(false); expect( - isDayScopedSearchEmpty({ searchActive: false, unassignedCount: 0, dayItemCount: 0 }), + isDayScopedSearchEmpty({ + searchActive: false, + unassignedCount: 0, + dayItemCount: 0, + unassignedQueueReady: true, + }), + ).toBe(false); + }); + + it("does not treat search as empty while the Unassigned queue is loading or failed", () => { + expect(isUnassignedQueueReady(true, null)).toBe(false); + expect(isUnassignedQueueReady(false, new Error("queue unavailable"))).toBe(false); + expect(isUnassignedQueueReady(false, null)).toBe(true); + expect( + isDayScopedSearchEmpty({ + searchActive: true, + unassignedCount: 0, + dayItemCount: 0, + unassignedQueueReady: false, + }), ).toBe(false); }); });