From 74dd1292a4e1861186391c50fe0619aff650389f Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 11:52:27 -0300 Subject: [PATCH] fix(workorders): applying a panel filter replaces the exact id selection The server honours only the id set when one is sent, so a facet applied during an ids drill-down showed as an active chip while doing nothing. Applying any facet now drops the selection; applying the panel unchanged keeps it. The ids param is also capped at the server limit whatever built the set. --- .../_hooks/use-work-orders-list-filters.ts | 4 +- .../utils/advanced-filters-utils.ts | 12 ++++ .../work-orders/utils/board-query-params.ts | 5 +- .../work-order-ids-drilldown.test.tsx | 62 ++++++++++++++++++- 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts index 811c9823..9a57c1de 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts @@ -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); }; diff --git a/src/domain/work-orders/utils/advanced-filters-utils.ts b/src/domain/work-orders/utils/advanced-filters-utils.ts index 9bcf6633..fc36045b 100644 --- a/src/domain/work-orders/utils/advanced-filters-utils.ts +++ b/src/domain/work-orders/utils/advanced-filters-utils.ts @@ -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() }; + return next.workOrderIds.size > 0 && countAdvancedFilters(withoutIds) > 0 ? withoutIds : next; +} + function monthStart(y: number, m: number): string { return `${y}-${pad2(m + 1)}-01`; } diff --git a/src/domain/work-orders/utils/board-query-params.ts b/src/domain/work-orders/utils/board-query-params.ts index 27ef48c8..c2e34373 100644 --- a/src/domain/work-orders/utils/board-query-params.ts +++ b/src/domain/work-orders/utils/board-query-params.ts @@ -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 | 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. */ diff --git a/src/test/app/(protected)/workorders/work-order-ids-drilldown.test.tsx b/src/test/app/(protected)/workorders/work-order-ids-drilldown.test.tsx index 24ae3255..43a9a000 100644 --- a/src/test/app/(protected)/workorders/work-order-ids-drilldown.test.tsx +++ b/src/test/app/(protected)/workorders/work-order-ids-drilldown.test.tsx @@ -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))} {location.search} + + {filters.advApplied != null && ( { 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(, { + 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; + 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(, { + 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).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(, { route: "/workorders?ids=5&statuses=Scheduled&dispatchers=u1",