From 5eed5ff4a14e0a6580ef8c135c10ceb5791a282f Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 23 Jul 2026 11:40:14 -0300 Subject: [PATCH] fix(vendors): scope facets to directory status --- src/app/(protected)/vendors/index.tsx | 2 +- src/domain/vendors/api/vendors-api.ts | 18 +++- .../vendors/use-cases/use-vendor-facets.ts | 9 +- src/infra/query-key/query-key.ts | 2 +- .../domain/vendors/api/vendors-api.test.ts | 18 +++- .../use-cases/use-vendor-facets.test.tsx | 86 +++++++++++++++++++ 6 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 src/test/domain/vendors/use-cases/use-vendor-facets.test.tsx diff --git a/src/app/(protected)/vendors/index.tsx b/src/app/(protected)/vendors/index.tsx index 68abe1a0..62430d7d 100644 --- a/src/app/(protected)/vendors/index.tsx +++ b/src/app/(protected)/vendors/index.tsx @@ -105,7 +105,7 @@ export default function VendorsListPage() { const debouncedSearch = useDebounce(search, 300); const deleteVendor = useDeleteVendor(); - const { data: facets } = useVendorFacets(); + const { data: facets } = useVendorFacets(isActive); const { data: trades = [] } = useDropdownOptionsByCategory("Trade"); const tradeOptions = useMemo(() => trades.map((trade) => trade.value), [trades]); diff --git a/src/domain/vendors/api/vendors-api.ts b/src/domain/vendors/api/vendors-api.ts index 3d9dd695..f375e86c 100644 --- a/src/domain/vendors/api/vendors-api.ts +++ b/src/domain/vendors/api/vendors-api.ts @@ -28,6 +28,18 @@ function pushRepeated( }); } +export interface VendorFacetsParams { + isActive?: boolean; +} + +function buildFacetsParams(params?: VendorFacetsParams): Record { + const searchParams: Record = {}; + if (params?.isActive !== undefined) { + searchParams.isActive = String(params.isActive); + } + return searchParams; +} + function buildListParams(params: VendorsListParams): Record { const searchParams: Record = { search: params.search ?? "", @@ -61,8 +73,10 @@ export const vendorsApi = { }; }, - getFacets: async (): Promise => { - const data = await apiGet(API_PATHS.vendor.facets); + getFacets: async (params?: VendorFacetsParams): Promise => { + const data = await apiGet(API_PATHS.vendor.facets, { + searchParams: buildFacetsParams(params), + }); return mapVendorFacets(handleApiResponse(data)); }, diff --git a/src/domain/vendors/use-cases/use-vendor-facets.ts b/src/domain/vendors/use-cases/use-vendor-facets.ts index 23921d36..2aef7d19 100644 --- a/src/domain/vendors/use-cases/use-vendor-facets.ts +++ b/src/domain/vendors/use-cases/use-vendor-facets.ts @@ -1,12 +1,13 @@ import { useQuery, type UseQueryResult } from "@tanstack/react-query"; -import { vendorsApi } from "@/domain/vendors/api/vendors-api"; +import { vendorsApi, type VendorFacetsParams } from "@/domain/vendors/api/vendors-api"; import type { VendorFacets } from "@/domain/vendors/types/vendor"; import { queryKeys } from "@/infra/query-key/query-key"; -export function useVendorFacets(): UseQueryResult { +export function useVendorFacets(isActive?: boolean): UseQueryResult { + const params: VendorFacetsParams | undefined = isActive !== undefined ? { isActive } : undefined; return useQuery({ - queryKey: queryKeys.vendors.facets(), - queryFn: () => vendorsApi.getFacets(), + queryKey: queryKeys.vendors.facets(isActive), + queryFn: () => vendorsApi.getFacets(params), staleTime: 60_000, }); } diff --git a/src/infra/query-key/query-key.ts b/src/infra/query-key/query-key.ts index cea259ad..a5daa242 100644 --- a/src/infra/query-key/query-key.ts +++ b/src/infra/query-key/query-key.ts @@ -52,7 +52,7 @@ export const queryKeys = { list: (params?: Record) => [...queryKeys.vendors.all, "list", params ?? {}] as const, detail: (id: string | number) => [...queryKeys.vendors.all, "detail", id] as const, - facets: () => [...queryKeys.vendors.all, "facets"] as const, + facets: (isActive?: boolean) => [...queryKeys.vendors.all, "facets", isActive ?? null] as const, portalToken: (vendorId: string | number) => [...queryKeys.vendors.all, "portalToken", vendorId] as const, }, diff --git a/src/test/domain/vendors/api/vendors-api.test.ts b/src/test/domain/vendors/api/vendors-api.test.ts index 559aab48..554ffcc6 100644 --- a/src/test/domain/vendors/api/vendors-api.test.ts +++ b/src/test/domain/vendors/api/vendors-api.test.ts @@ -88,7 +88,7 @@ describe("vendorsApi.getList", () => { const facets = await vendorsApi.getFacets(); - expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.facets); + expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.facets, { searchParams: {} }); expect(facets.companies[0]).toMatchObject({ name: "Gateway Plumbing", companyPhone: "314-555-0100", @@ -98,6 +98,22 @@ describe("vendorsApi.getList", () => { expect(facets.jobBuckets[0]).toEqual({ id: "50-99", label: "50–99" }); }); + it("serializes the requested facet scope as an isActive query parameter", async () => { + apiGet.mockResolvedValue({ + data: { companies: [], trades: [], locations: [], jobBuckets: [] }, + }); + + await vendorsApi.getFacets({ isActive: true }); + await vendorsApi.getFacets({ isActive: false }); + + expect(apiGet).toHaveBeenNthCalledWith(1, API_PATHS.vendor.facets, { + searchParams: { isActive: "true" }, + }); + expect(apiGet).toHaveBeenNthCalledWith(2, API_PATHS.vendor.facets, { + searchParams: { isActive: "false" }, + }); + }); + it("sends all editable prototype fields when creating and updating", async () => { const values = { companyName: "Gateway Plumbing", diff --git a/src/test/domain/vendors/use-cases/use-vendor-facets.test.tsx b/src/test/domain/vendors/use-cases/use-vendor-facets.test.tsx new file mode 100644 index 00000000..c732e116 --- /dev/null +++ b/src/test/domain/vendors/use-cases/use-vendor-facets.test.tsx @@ -0,0 +1,86 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { ReactNode } from "react"; + +const getFacets = vi.fn(); + +vi.mock("@/domain/vendors/api/vendors-api", () => ({ + vendorsApi: { getFacets: (...args: unknown[]) => getFacets(...args) }, +})); + +import { useVendorFacets } from "@/domain/vendors/use-cases/use-vendor-facets"; +import { queryKeys } from "@/infra/query-key/query-key"; + +const emptyFacets = { companies: [], trades: [], locations: [], jobBuckets: [] }; +const activeFacets = { ...emptyFacets, companies: [{ name: "Active Co" }] }; +const inactiveFacets = { ...emptyFacets, companies: [{ name: "Inactive Co" }] }; + +function createClient(): QueryClient { + return new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); +} + +function makeWrapper(client: QueryClient) { + return function Wrapper({ children }: { children: ReactNode }) { + return {children}; + }; +} + +describe("useVendorFacets", () => { + beforeEach(() => { + getFacets.mockReset(); + }); + + it("switches facet requests true/false as the tab scope changes", async () => { + getFacets.mockResolvedValue(activeFacets); + const { rerender } = renderHook(({ isActive }) => useVendorFacets(isActive), { + initialProps: { isActive: true as boolean | undefined }, + wrapper: makeWrapper(createClient()), + }); + + await waitFor(() => expect(getFacets).toHaveBeenCalledWith({ isActive: true })); + + getFacets.mockResolvedValue(inactiveFacets); + rerender({ isActive: false }); + await waitFor(() => expect(getFacets).toHaveBeenCalledWith({ isActive: false })); + + getFacets.mockResolvedValue(activeFacets); + rerender({ isActive: undefined }); + await waitFor(() => expect(getFacets).toHaveBeenCalledWith(undefined)); + }); + + it("scopes the cache so active, inactive, and unscoped keys cannot collide", () => { + const active = queryKeys.vendors.facets(true); + const inactive = queryKeys.vendors.facets(false); + const unscoped = queryKeys.vendors.facets(); + + expect(active).not.toEqual(inactive); + expect(active).not.toEqual(unscoped); + expect(inactive).not.toEqual(unscoped); + expect(active[active.length - 1]).toBe(true); + expect(inactive[inactive.length - 1]).toBe(false); + expect(unscoped[unscoped.length - 1]).toBeNull(); + }); + + it("reuses the cached facet entry when returning to a previous scope", async () => { + getFacets.mockResolvedValue(activeFacets); + const client = createClient(); + const { result, rerender } = renderHook(({ isActive }) => useVendorFacets(isActive), { + initialProps: { isActive: true as boolean | undefined }, + wrapper: makeWrapper(client), + }); + + await waitFor(() => expect(getFacets).toHaveBeenCalledTimes(1)); + + getFacets.mockResolvedValue(inactiveFacets); + rerender({ isActive: false }); + await waitFor(() => expect(getFacets).toHaveBeenCalledTimes(2)); + + getFacets.mockClear(); + rerender({ isActive: true }); + await waitFor(() => expect(result.current.data).toEqual(activeFacets)); + expect(getFacets).not.toHaveBeenCalled(); + }); +});