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.
This commit is contained in:
Arthur Bassi 2026-08-20 10:28:04 -03:00
parent 1a54cbd2af
commit be975c792c
5 changed files with 175 additions and 6 deletions

View file

@ -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 (
<WoTableEmptyShell
message="No work orders match your search"

View file

@ -8,6 +8,7 @@ import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
import {
buildSelectedDayGroup,
isDayScopedSearchEmpty,
isUnassignedQueueReady,
} from "@/domain/work-orders/utils/week-day-nav-utils";
import {
composeBoardSections,
@ -94,6 +95,7 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
searchActive,
unassignedCount: unassigned.length,
dayItemCount: dayVisibleCount,
unassignedQueueReady: isUnassignedQueueReady(unassignedLoading, unassignedError),
});
const isEmptyWeek =
!advActive && scheduled.length === 0 && unassigned.length === 0 && !searchActive;

View file

@ -27,12 +27,25 @@ export function scheduledDayTab(dateIso: string): { weekMonday: string; selected
};
}
export function isUnassignedQueueReady(
unassignedLoading: boolean,
unassignedError: Error | null,
): boolean {
return !unassignedLoading && unassignedError == null;
}
export function isDayScopedSearchEmpty(args: {
searchActive: boolean;
unassignedCount: number;
dayItemCount: number;
unassignedQueueReady: boolean;
}): boolean {
return args.searchActive && args.unassignedCount === 0 && args.dayItemCount === 0;
return (
args.searchActive &&
args.unassignedQueueReady &&
args.unassignedCount === 0 &&
args.dayItemCount === 0
);
}
export function initialSelectedDay(weekMonday: string, today = todayIso()): string {

View file

@ -0,0 +1,113 @@
import { describe, expect, it, vi } from "vitest";
import { WoTable } from "@/app/(protected)/workorders/_components/list/table/wo-table";
import type { ReturnTypeOfUseWorkOrdersTableData } from "@/app/(protected)/workorders/_components/list/table/wo-table-types";
import { renderWithProviders } from "@/test/test-utils";
vi.mock("@/domain/locations/use-cases/use-location-sites", () => ({
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> = {},
): 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(
<WoTable
tableData={data}
users={[]}
sites={[]}
vendors={[]}
pmTypes={[]}
weekMonday="2026-05-18"
onNavigateToDay={vi.fn()}
onOpenSlideOver={vi.fn()}
onOpenMedia={vi.fn()}
onClearSearch={vi.fn()}
onPatch={vi.fn()}
/>,
{ 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();
});
});

View file

@ -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);
});
});