fix(work-orders): distinguish loading from empty in Unassigned pin (SH-165)

The pinned Unassigned section folded the queue query's pending state into
isFetching only, which the section never read. On first mount, once GET /board
resolved while the queue was still paging GET /board/search, the pin announced
"No unassigned work orders." instead of a loading state.

Expose the queue query's own isLoading as unassignedLoading, thread it through
the table data hook, and render an aria-busy loading row while it is true so the
loading and empty states are separately observable.
This commit is contained in:
Codex Review Integration 2026-08-18 17:22:32 -03:00
parent dc9e146080
commit ac9ce34101
4 changed files with 60 additions and 1 deletions

View file

@ -23,8 +23,19 @@ export function WoTableBody({ tableData, renderRow, renderInlineRow }: WoTableBo
const unassignedRows = tableData.unassigned ?? [];
const showUnscheduled =
(tableData.unscheduled?.length ?? 0) > 0 || tableData.draftDayKey === UNSCHEDULED_DAY_KEY;
// SH-165: "still loading" and "zero unassigned" are distinct states. Without
// this split the pin announced "No unassigned work orders." while the queue
// was still paging board/search on first mount.
const showUnassignedLoading =
tableData.unassignedOpen &&
unassignedRows.length === 0 &&
!tableData.unassignedError &&
tableData.unassignedLoading;
const showUnassignedEmpty =
tableData.unassignedOpen && unassignedRows.length === 0 && !tableData.unassignedError;
tableData.unassignedOpen &&
unassignedRows.length === 0 &&
!tableData.unassignedError &&
!tableData.unassignedLoading;
return (
<tbody>
@ -47,6 +58,15 @@ export function WoTableBody({ tableData, renderRow, renderInlineRow }: WoTableBo
</td>
</tr>
)}
{showUnassignedLoading && (
<tr aria-busy="true">
<td colSpan={WO_TABLE_COLS.length} style={{ padding: "8px 14px" }}>
<Text variant="feedback" tone="muted">
Loading unassigned work orders…
</Text>
</td>
</tr>
)}
{showUnassignedEmpty && (
<tr>
<td colSpan={WO_TABLE_COLS.length} style={{ padding: "8px 14px" }}>

View file

@ -23,6 +23,7 @@ export type WorkOrdersBoardSource = {
data: WorkOrderBoardListResult | undefined;
unassignedQueue: WorkOrderTableRow[];
unassignedError: Error | null;
unassignedLoading: boolean;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
@ -75,6 +76,7 @@ export function useWorkOrdersBoardSource(filters: FiltersReturn): WorkOrdersBoar
} = useWorkOrdersBoardSearch(resolvedSearchParams ?? { datePreset: "ThisWeek" }, advActive);
const {
data: queueData,
isLoading: queueLoading,
isFetching: queueFetching,
error: queueError,
refetch: refetchQueue,
@ -94,6 +96,10 @@ export function useWorkOrdersBoardSource(filters: FiltersReturn): WorkOrdersBoar
data: advActive ? searchData : boardData,
unassignedQueue: advActive ? EMPTY_UNASSIGNED_QUEUE : (queueData ?? EMPTY_UNASSIGNED_QUEUE),
unassignedError: advActive ? null : (queueError ?? null),
// SH-165: the pinned queue has its own first-load state. Folding it into
// isFetching only made "still paging board/search" indistinguishable from
// "zero unassigned", so the pin announced an empty queue while loading.
unassignedLoading: advActive ? false : queueLoading,
isLoading: advActive ? searchLoading : boardLoading,
isFetching: advActive ? searchFetching : boardFetching || queueFetching,
error: advActive ? searchError : boardError,

View file

@ -26,6 +26,7 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
data,
unassignedQueue,
unassignedError,
unassignedLoading,
isLoading,
isFetching,
error,
@ -107,6 +108,7 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
advResults,
unassigned,
unassignedError,
unassignedLoading,
unscheduled,
displayGroups,
searchActive,

View file

@ -76,6 +76,7 @@ function baseTableData(
advResults: [],
unassigned: [],
unassignedError: null,
unassignedLoading: false,
unscheduled: [],
displayGroups: [],
searchActive: false,
@ -308,6 +309,36 @@ describe("WoTableBody Unassigned queue (SH-165)", () => {
expect(screen.queryByText("WED · 07/15")).not.toBeInTheDocument();
});
it("distinguishes a still-loading queue from an empty one", () => {
render(
<table>
<WoTableBody
tableData={baseTableData({ unassignedLoading: true })}
renderRow={() => null}
renderInlineRow={() => null}
/>
</table>,
);
expect(screen.getByText(/Loading unassigned work orders/)).toBeInTheDocument();
expect(screen.queryByText("No unassigned work orders.")).not.toBeInTheDocument();
});
it("announces an empty queue only once loading has settled", () => {
render(
<table>
<WoTableBody
tableData={baseTableData({ unassignedLoading: false })}
renderRow={() => null}
renderInlineRow={() => null}
/>
</table>,
);
expect(screen.getByText("No unassigned work orders.")).toBeInTheDocument();
expect(screen.queryByText(/Loading unassigned work orders/)).not.toBeInTheDocument();
});
it("keeps the Unassigned pin visible when the queue query fails", () => {
render(
<table>