fix(vendors): scope facets to directory status

This commit is contained in:
Alexandre Brandizzi 2026-07-23 11:40:14 -03:00
parent db3ae2ca3f
commit 5eed5ff4a1
6 changed files with 126 additions and 9 deletions

View file

@ -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]);

View file

@ -28,6 +28,18 @@ function pushRepeated(
});
}
export interface VendorFacetsParams {
isActive?: boolean;
}
function buildFacetsParams(params?: VendorFacetsParams): Record<string, string> {
const searchParams: Record<string, string> = {};
if (params?.isActive !== undefined) {
searchParams.isActive = String(params.isActive);
}
return searchParams;
}
function buildListParams(params: VendorsListParams): Record<string, string | number> {
const searchParams: Record<string, string | number> = {
search: params.search ?? "",
@ -61,8 +73,10 @@ export const vendorsApi = {
};
},
getFacets: async (): Promise<VendorFacets> => {
const data = await apiGet<unknown>(API_PATHS.vendor.facets);
getFacets: async (params?: VendorFacetsParams): Promise<VendorFacets> => {
const data = await apiGet<unknown>(API_PATHS.vendor.facets, {
searchParams: buildFacetsParams(params),
});
return mapVendorFacets(handleApiResponse(data));
},

View file

@ -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<VendorFacets, Error> {
export function useVendorFacets(isActive?: boolean): UseQueryResult<VendorFacets, Error> {
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,
});
}

View file

@ -52,7 +52,7 @@ export const queryKeys = {
list: (params?: Record<string, unknown>) =>
[...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,
},

View file

@ -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",

View file

@ -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 <QueryClientProvider client={client}>{children}</QueryClientProvider>;
};
}
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();
});
});