fix(work-orders): hide unfinished affordances and route View to detail

This commit is contained in:
Arthur Bassi 2026-07-22 17:10:27 -03:00 • committed by Alexandre Brandizzi
parent 0709bd1172
commit becc4913e3
3 changed files with 174 additions and 32 deletions

View file

@ -1,36 +1,25 @@
import { CircularProgress } from "@mui/material";
import { Plus, SlidersHorizontal } from "lucide-react";
import { SearchInput } from "@/components/ui/search-input";
import { ToolbarButton } from "@/components/ui/toolbar-button";
type WorkOrdersListHeaderProps = {
search: string;
onSearchChange: (value: string) => void;
onOpenAdvancedFilters: () => void;
onNewWorkOrder: () => void;
/** Omit until Advanced Search slice mounts the sheet. */
onOpenAdvancedFilters?: () => void;
/** Omit until wizard shell slice mounts New WO. */
onNewWorkOrder?: () => void;
activeFilterCount: number;
isFetching?: boolean;
};
export function WorkOrdersListHeader({
search,
onSearchChange,
onOpenAdvancedFilters,
onNewWorkOrder,
activeFilterCount,
isFetching = false,
}: WorkOrdersListHeaderProps) {
return (
@ -42,20 +31,24 @@ export function WorkOrdersListHeader({
aria-label="Search work orders"
/>
<ToolbarButton
variant="outline"
icon={<SlidersHorizontal size={13} />}
onClick={onOpenAdvancedFilters}
aria-label="Advanced Filters"
title="Advanced Filters"
badge={activeFilterCount}
>
Filters
</ToolbarButton>
{onOpenAdvancedFilters && (
<ToolbarButton
variant="outline"
icon={<SlidersHorizontal size={13} />}
onClick={onOpenAdvancedFilters}
aria-label="Advanced Filters"
title="Advanced Filters"
badge={activeFilterCount}
>
Filters
</ToolbarButton>
)}
<ToolbarButton variant="primary" icon={<Plus size={13} />} onClick={onNewWorkOrder}>
New WO
</ToolbarButton>
{onNewWorkOrder && (
<ToolbarButton variant="primary" icon={<Plus size={13} />} onClick={onNewWorkOrder}>
New WO
</ToolbarButton>
)}
{isFetching && <CircularProgress size={14} />}
</>

View file

@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import { Box } from "@mui/material";
import { useNavigate } from "react-router";
import { PageHeader } from "@/components/ui/page-header";
import { MediaUploadModal } from "@/app/(protected)/workorders/_components/media/media-upload-modal";
import { WorkOrdersAdvancedFiltersBanner } from "@/app/(protected)/workorders/_components/list/work-orders-advanced-filters-banner";
@ -22,6 +23,7 @@ import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-w
import { useAuthContext } from "@/providers/auth-context";
export default function WorkOrdersListPage() {
const navigate = useNavigate();
const { user } = useAuthContext();
const [mediaRow, setMediaRow] = useState<WorkOrderTableRow | null>(null);
const [mediaFiles, setMediaFiles] = useState<Record<string, MediaFile[]>>({});
@ -60,8 +62,6 @@ export default function WorkOrdersListPage() {
<WorkOrdersListHeader
search={filters.search}
onSearchChange={filters.handleSearchChange}
onOpenAdvancedFilters={() => filters.setAdvOpen(true)}
onNewWorkOrder={() => undefined}
activeFilterCount={filters.activeFilterCount}
isFetching={tableData.isFetching && !tableData.isLoading}
/>
@ -98,11 +98,13 @@ export default function WorkOrdersListPage() {
sites={locations}
vendors={vendors}
pmTypes={pmTypes}
onOpenSlideOver={(row) => tableData.setSelectedId(row.id)}
onOpenSlideOver={(row, edit) => {
tableData.setSelectedId(row.id);
navigate(edit ? `/workorders/edit/${row.id}` : `/workorders/${row.id}`);
}}
onOpenCompDoc={() => undefined}
onOpenMedia={setMediaRow}
onClearSearch={() => filters.handleSearchChange("")}
onOpenAdvancedFilters={() => filters.setAdvOpen(true)}
/>
{activeMediaRow && (

View file

@ -0,0 +1,147 @@
import { fireEvent, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { WorkOrdersListHeader } from "@/app/(protected)/workorders/_components/list/work-orders-list-header";
import WorkOrdersListPage from "@/app/(protected)/workorders/index";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { renderWithProviders } from "@/test/test-utils";
const navigate = vi.fn();
vi.mock("react-router", async () => {
const actual = await vi.importActual<typeof import("react-router")>("react-router");
return {
...actual,
useNavigate: () => navigate,
};
});
vi.mock("@/providers/auth-context", () => ({
useAuthContext: () => ({ user: { id: "u1" } }),
}));
vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({
useWorkOrdersListFilters: () => ({
search: "",
handleSearchChange: vi.fn(),
setAdvOpen: vi.fn(),
activeFilterCount: 0,
advApplied: null,
clearAdvancedFilterChip: vi.fn(),
clearAdvancedFilters: vi.fn(),
weekMonday: "2026-07-20",
setWeekMonday: vi.fn(),
dispatcherIds: new Set<string>(),
setDispatcherIds: vi.fn(),
typeFilter: "All",
setTypeFilter: vi.fn(),
}),
}));
vi.mock("@/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar", () => ({
WorkOrdersFilterBar: () => null,
}));
vi.mock(
"@/app/(protected)/workorders/_components/list/work-orders-advanced-filters-banner",
() => ({
WorkOrdersAdvancedFiltersBanner: () => null,
}),
);
vi.mock("@/app/(protected)/workorders/_components/media/media-upload-modal", () => ({
MediaUploadModal: () => null,
}));
const canceledRow = {
id: 77,
status: "Canceled",
woNumber: "WO-77",
} as WorkOrderTableRow;
vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-table-data", () => ({
useWorkOrdersTableData: () => ({
rows: [canceledRow],
totalCount: 1,
scopeTotalCount: 1,
isFetching: false,
isLoading: false,
setSelectedId: vi.fn(),
patchRow: vi.fn(),
}),
}));
vi.mock("@/domain/work-orders/use-cases/use-work-order-table-mutations", () => ({
useWorkOrderTableMutations: () => ({ patchField: vi.fn() }),
}));
vi.mock("@/domain/work-orders/use-cases/use-work-orders-list", () => ({
useWorkOrderUsers: () => ({ data: [] }),
}));
vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({
useWorkOrderLocations: () => ({ data: [] }),
useWorkOrderVendors: () => ({ data: [] }),
}));
vi.mock("@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category", () => ({
useDropdownOptionsByCategory: () => ({ data: [] }),
}));
vi.mock("@/domain/work-orders/mocks/is-wo-table-mock-enabled", () => ({
isWoTableMockEnabled: () => false,
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/wo-table", () => ({
WoTable: ({
onOpenSlideOver,
}: {
onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void;
}) => (
<button type="button" onClick={() => onOpenSlideOver(canceledRow, false)}>
View canceled row
</button>
),
}));
describe("WorkOrdersListHeader slice affordances", () => {
it("hides New WO and Advanced Filters when callbacks are omitted", () => {
renderWithProviders(
<WorkOrdersListHeader search="" onSearchChange={vi.fn()} activeFilterCount={0} />,
{ withAuth: false },
);
expect(screen.queryByRole("button", { name: /new wo/i })).not.toBeInTheDocument();
expect(screen.queryByLabelText("Advanced Filters")).not.toBeInTheDocument();
expect(screen.getByLabelText("Search work orders")).toBeInTheDocument();
});
it("shows New WO and Advanced Filters when callbacks are provided", () => {
renderWithProviders(
<WorkOrdersListHeader
search=""
onSearchChange={vi.fn()}
onNewWorkOrder={vi.fn()}
onOpenAdvancedFilters={vi.fn()}
activeFilterCount={2}
/>,
{ withAuth: false },
);
expect(screen.getByRole("button", { name: /new wo/i })).toBeInTheDocument();
expect(screen.getByLabelText("Advanced Filters")).toBeInTheDocument();
});
});
describe("WorkOrdersListPage board-core public interface", () => {
it("does not render New WO or Advanced Filters, and View navigates to detail", () => {
navigate.mockReset();
renderWithProviders(<WorkOrdersListPage />, { route: "/workorders", withAuth: false });
expect(screen.queryByRole("button", { name: /new wo/i })).not.toBeInTheDocument();
expect(screen.queryByLabelText("Advanced Filters")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /view canceled row/i }));
expect(navigate).toHaveBeenCalledWith("/workorders/77");
});
});