Merge branch 'feat/ab/wo-ids-filter' into feat/ab/sh-295-sla-alerts

This commit is contained in:
Alexandre Brandizzi 2026-09-25 11:52:57 -03:00
commit 7de34d359e
4 changed files with 79 additions and 4 deletions

View file

@ -6,9 +6,9 @@ import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
import type { WorkOrderBoardQueryParams } from "@/domain/work-orders/types/work-order-board";
import {
clearAdvancedFilterGroup,
cloneAdvancedFilters,
countAdvancedFilters,
getInitialAdvancedFilters,
resolveAppliedAdvancedFilters,
} from "@/domain/work-orders/utils/advanced-filters-utils";
import { toBoardQueryParams } from "@/domain/work-orders/utils/board-query-params";
import { asDayPageSize, DEFAULT_DAY_PAGE_SIZE } from "@/domain/work-orders/utils/day-pagination";
@ -90,7 +90,7 @@ export function useWorkOrdersListFilters() {
};
const applyAdvancedFilters = (filters: WorkOrderAdvancedFilters) => {
setAdvApplied(cloneAdvancedFilters(filters));
setAdvApplied(resolveAppliedAdvancedFilters(filters));
setPage(0);
};

View file

@ -65,6 +65,18 @@ export function countAdvancedFilters(filters: WorkOrderAdvancedFilters): number
return ADVANCED_FILTER_ACTIVE_SIGNALS(filters).filter(Boolean).length;
}
/**
* The server honours only the id set when one is sent, so an id selection and panel facets never
* combine: applying any facet replaces the selection with a normal filtered board.
*/
export function resolveAppliedAdvancedFilters(
filters: WorkOrderAdvancedFilters,
): WorkOrderAdvancedFilters {
const next = cloneAdvancedFilters(filters);
const withoutIds = { ...next, workOrderIds: new Set<number>() };
return next.workOrderIds.size > 0 && countAdvancedFilters(withoutIds) > 0 ? withoutIds : next;
}
function monthStart(y: number, m: number): string {
return `${y}-${pad2(m + 1)}-01`;
}

View file

@ -12,6 +12,7 @@ import {
} from "@/domain/work-orders/types/work-order-board";
import { mapWizardTypeToApi } from "@/domain/work-orders/mappers/work-order-board-mapper";
import { weekRange } from "@/domain/work-orders/utils/list-week-utils";
import { normalizeWorkOrderIds } from "@/domain/work-orders/utils/work-order-ids-filter";
import type { CompletionDocFilter } from "@/domain/work-orders/types/work-order-advanced-filters";
import type { WizardWOStatus, WOType } from "@/domain/work-orders/types/work-order-wizard";
@ -135,7 +136,9 @@ function mapSearchFacetParams(
/** Sent as one comma-separated `ids` value; the server then ignores every other filter. */
function mapWorkOrderIds(ids: Set<number> | undefined): string | undefined {
return ids?.size ? [...ids].join(",") : undefined;
// Normalized here too, so a caller that fills the set directly still stays within the server's limit.
const normalized = ids?.size ? normalizeWorkOrderIds(ids) : [];
return normalized.length > 0 ? normalized.join(",") : undefined;
}
/** Build GET /board query from weekly filter bar. */

View file

@ -6,8 +6,13 @@ import { WorkOrdersAdvancedFiltersBanner } from "@/app/(protected)/workorders/_c
import { buildResolvedBoardSearchParams } from "@/app/(protected)/workorders/_hooks/build-resolved-board-search-params";
import { useDashboardDrilldownFilters } from "@/app/(protected)/workorders/_hooks/use-dashboard-drilldown-filters";
import { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
import { cloneAdvancedFilters } from "@/domain/work-orders/utils/advanced-filters-utils";
import { buildBoardSearchParamsRecord } from "@/domain/work-orders/utils/board-query-params";
import { buildWorkOrderIdsHref } from "@/domain/work-orders/utils/work-order-ids-filter";
import {
buildWorkOrderIdsHref,
WORK_ORDER_IDS_MAX,
} from "@/domain/work-orders/utils/work-order-ids-filter";
import { renderWithProviders } from "@/test/test-utils";
/** The Work Orders page's filter wiring, without the table: URL → applied filters → board/search params. */
@ -32,6 +37,15 @@ function BoardFilterHarness() {
{params == null ? "weekly board" : JSON.stringify(buildBoardSearchParamsRecord(params))}
</output>
<output aria-label="Current search">{location.search}</output>
<button
type="button"
onClick={() => filters.applyAdvancedFilters({ ...filters.advInitial, rescheduled: true })}
>
Apply rescheduled only
</button>
<button type="button" onClick={() => filters.applyAdvancedFilters(filters.advInitial)}>
Apply unchanged
</button>
{filters.advApplied != null && (
<WorkOrdersAdvancedFiltersBanner
applied={filters.advApplied}
@ -85,6 +99,52 @@ describe("Work Orders exact id filter", () => {
expect(screen.queryByText(/selected work order/)).not.toBeInTheDocument();
});
it("replaces the selection when a panel filter is applied, so no chip shows a filter the server ignores", async () => {
const user = userEvent.setup();
renderWithProviders(<BoardFilterHarness />, {
route: "/workorders?ids=4,9",
withAuth: false,
});
expect(await screen.findByText("Showing 2 selected work orders")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Apply rescheduled only" }));
const params = searchParams() as Record<string, unknown>;
expect(params.ids).toBeUndefined();
expect(params.rescheduled).toBe(true);
expect(screen.queryByText(/selected work order/)).not.toBeInTheDocument();
});
it("keeps the selection when the panel is applied without a filter", async () => {
const user = userEvent.setup();
renderWithProviders(<BoardFilterHarness />, {
route: "/workorders?ids=4,9",
withAuth: false,
});
expect(await screen.findByText("Showing 2 selected work orders")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Apply unchanged" }));
expect((searchParams() as Record<string, unknown>).ids).toBe("4,9");
expect(screen.getByText("Showing 2 selected work orders")).toBeInTheDocument();
});
it("never sends more ids than the server accepts, however the selection was built", () => {
const ids = new Set(Array.from({ length: WORK_ORDER_IDS_MAX + 30 }, (_, index) => index + 1));
const params = buildResolvedBoardSearchParams({
advApplied: { ...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS), workOrderIds: ids },
search: "",
page: 0,
pageSize: 200,
sortBy: "",
sortDir: "",
locations: [],
vendors: [],
});
expect(params?.ids?.split(",")).toHaveLength(WORK_ORDER_IDS_MAX);
});
it("ignores other drill-down params sent alongside the id set", async () => {
renderWithProviders(<BoardFilterHarness />, {
route: "/workorders?ids=5&statuses=Scheduled&dispatchers=u1",