mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
Merge branch 'feat/ab/wo-ids-filter' into feat/ab/sh-295-sla-alerts
This commit is contained in:
commit
7de34d359e
4 changed files with 79 additions and 4 deletions
|
|
@ -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 type { WorkOrderBoardQueryParams } from "@/domain/work-orders/types/work-order-board";
|
||||||
import {
|
import {
|
||||||
clearAdvancedFilterGroup,
|
clearAdvancedFilterGroup,
|
||||||
cloneAdvancedFilters,
|
|
||||||
countAdvancedFilters,
|
countAdvancedFilters,
|
||||||
getInitialAdvancedFilters,
|
getInitialAdvancedFilters,
|
||||||
|
resolveAppliedAdvancedFilters,
|
||||||
} from "@/domain/work-orders/utils/advanced-filters-utils";
|
} from "@/domain/work-orders/utils/advanced-filters-utils";
|
||||||
import { toBoardQueryParams } from "@/domain/work-orders/utils/board-query-params";
|
import { toBoardQueryParams } from "@/domain/work-orders/utils/board-query-params";
|
||||||
import { asDayPageSize, DEFAULT_DAY_PAGE_SIZE } from "@/domain/work-orders/utils/day-pagination";
|
import { asDayPageSize, DEFAULT_DAY_PAGE_SIZE } from "@/domain/work-orders/utils/day-pagination";
|
||||||
|
|
@ -90,7 +90,7 @@ export function useWorkOrdersListFilters() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyAdvancedFilters = (filters: WorkOrderAdvancedFilters) => {
|
const applyAdvancedFilters = (filters: WorkOrderAdvancedFilters) => {
|
||||||
setAdvApplied(cloneAdvancedFilters(filters));
|
setAdvApplied(resolveAppliedAdvancedFilters(filters));
|
||||||
setPage(0);
|
setPage(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,18 @@ export function countAdvancedFilters(filters: WorkOrderAdvancedFilters): number
|
||||||
return ADVANCED_FILTER_ACTIVE_SIGNALS(filters).filter(Boolean).length;
|
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 {
|
function monthStart(y: number, m: number): string {
|
||||||
return `${y}-${pad2(m + 1)}-01`;
|
return `${y}-${pad2(m + 1)}-01`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from "@/domain/work-orders/types/work-order-board";
|
} from "@/domain/work-orders/types/work-order-board";
|
||||||
import { mapWizardTypeToApi } from "@/domain/work-orders/mappers/work-order-board-mapper";
|
import { mapWizardTypeToApi } from "@/domain/work-orders/mappers/work-order-board-mapper";
|
||||||
import { weekRange } from "@/domain/work-orders/utils/list-week-utils";
|
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 { CompletionDocFilter } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||||
import type { WizardWOStatus, WOType } from "@/domain/work-orders/types/work-order-wizard";
|
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. */
|
/** Sent as one comma-separated `ids` value; the server then ignores every other filter. */
|
||||||
function mapWorkOrderIds(ids: Set<number> | undefined): string | undefined {
|
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. */
|
/** Build GET /board query from weekly filter bar. */
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,13 @@ import { WorkOrdersAdvancedFiltersBanner } from "@/app/(protected)/workorders/_c
|
||||||
import { buildResolvedBoardSearchParams } from "@/app/(protected)/workorders/_hooks/build-resolved-board-search-params";
|
import { buildResolvedBoardSearchParams } from "@/app/(protected)/workorders/_hooks/build-resolved-board-search-params";
|
||||||
import { useDashboardDrilldownFilters } from "@/app/(protected)/workorders/_hooks/use-dashboard-drilldown-filters";
|
import { useDashboardDrilldownFilters } from "@/app/(protected)/workorders/_hooks/use-dashboard-drilldown-filters";
|
||||||
import { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-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 { 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";
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
/** The Work Orders page's filter wiring, without the table: URL → applied filters → board/search params. */
|
/** 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))}
|
{params == null ? "weekly board" : JSON.stringify(buildBoardSearchParamsRecord(params))}
|
||||||
</output>
|
</output>
|
||||||
<output aria-label="Current search">{location.search}</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 && (
|
{filters.advApplied != null && (
|
||||||
<WorkOrdersAdvancedFiltersBanner
|
<WorkOrdersAdvancedFiltersBanner
|
||||||
applied={filters.advApplied}
|
applied={filters.advApplied}
|
||||||
|
|
@ -85,6 +99,52 @@ describe("Work Orders exact id filter", () => {
|
||||||
expect(screen.queryByText(/selected work order/)).not.toBeInTheDocument();
|
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 () => {
|
it("ignores other drill-down params sent alongside the id set", async () => {
|
||||||
renderWithProviders(<BoardFilterHarness />, {
|
renderWithProviders(<BoardFilterHarness />, {
|
||||||
route: "/workorders?ids=5&statuses=Scheduled&dispatchers=u1",
|
route: "/workorders?ids=5&statuses=Scheduled&dispatchers=u1",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue