diff --git a/src/hooks/useFilterHandlers.test.ts b/src/hooks/useFilterHandlers.test.ts new file mode 100644 index 000000000..e026ed956 --- /dev/null +++ b/src/hooks/useFilterHandlers.test.ts @@ -0,0 +1,310 @@ +import { renderHook } from "@testing-library/react-hooks"; + +import { useFilterHandlers } from "./useFilterHandlers"; + +jest.mock("./useNavigator", () => () => jest.fn()); + +describe("useFilterHandlers", () => { + describe("resetFilters", () => { + test("should run cleanup function and call createUrl function", () => { + // Arrange + const cleanupFn = jest.fn(); + const createUrl = jest.fn(); + + const { result } = renderHook(() => + useFilterHandlers({ + getFilterQueryParam: jest.fn(), + createUrl, + cleanupFn, + params: { + activeTab: "tab", + asc: true, + sort: "sort", + query: "query", + }, + defaultSortField: "", + keepActiveTab: false, + }), + ); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const resetFilters = result.current[1]; + + // Act + resetFilters(); + + // Assert + + expect(cleanupFn).toHaveBeenCalledTimes(1); + expect(createUrl).toHaveBeenCalledWith({ + asc: true, + sort: "sort", + }); + }); + }); + + describe("changeFilters", () => { + test("should call cleanup function when provided", () => { + const cleanupFn = jest.fn(); + + const { result } = renderHook(() => + useFilterHandlers({ + getFilterQueryParam: jest.fn(), + createUrl: jest.fn(), + cleanupFn, + params: { + activeTab: "tab", + asc: true, + sort: "sort", + query: "query", + }, + defaultSortField: "", + keepActiveTab: false, + }), + ); + + const [changeFilters] = result.current; + + // Act + changeFilters([]); + + // Assert + + expect(cleanupFn).toHaveBeenCalledTimes(1); + }); + + test("should call createUrl function with with proper params when no filters", () => { + const createUrl = jest.fn(); + + const { result } = renderHook(() => + useFilterHandlers({ + getFilterQueryParam: jest.fn(filter => ({ + [filter.name]: filter.value, + })), + createUrl, + params: { + activeTab: "tab", + asc: true, + sort: "sort", + query: "query", + }, + defaultSortField: "", + keepActiveTab: false, + }), + ); + + const [changeFilters] = result.current; + + // Act + changeFilters([]); + + // Assert + expect(createUrl).toHaveBeenCalledWith({ + asc: true, + sort: "sort", + query: "query", + activeTab: undefined, + }); + }); + + test("should call createUrl function with with proper params when filters selected", () => { + const createUrl = jest.fn(); + + const { result } = renderHook(() => + useFilterHandlers({ + getFilterQueryParam: jest.fn(filter => ({ + [filter.name]: filter.value[0], + })), + createUrl, + params: { + activeTab: "tab", + asc: true, + sort: "sort", + query: "query", + }, + defaultSortField: "", + keepActiveTab: false, + }), + ); + + const [changeFilters] = result.current; + + // Act + changeFilters([ + { + name: "filter", + value: ["value"], + label: "test", + active: true, + multiple: false, + }, + { + name: "filterOther", + value: ["valueOther"], + label: "test", + active: true, + multiple: false, + }, + ]); + + // Assert + expect(createUrl).toHaveBeenCalledWith({ + filter: "value", + filterOther: "valueOther", + asc: true, + sort: "sort", + query: "query", + activeTab: undefined, + }); + }); + + test("should call createUrl function with active tab value when keepActiveTab is true", () => { + const createUrl = jest.fn(); + + const { result } = renderHook(() => + useFilterHandlers({ + getFilterQueryParam: jest.fn(filter => ({ + [filter.name]: filter.value[0], + })), + createUrl, + params: { + activeTab: "tab", + asc: true, + sort: "sort", + query: "query", + }, + defaultSortField: "", + keepActiveTab: true, + }), + ); + + const [changeFilters] = result.current; + + // Act + changeFilters([ + { + name: "filter", + value: ["value"], + label: "test", + active: true, + multiple: false, + }, + ]); + + // Assert + expect(createUrl).toHaveBeenCalledWith({ + filter: "value", + asc: true, + sort: "sort", + query: "query", + activeTab: "tab", + }); + }); + }); + + describe("handleSearchChange", () => { + test("should call createUrl function when provided", () => { + const cleanupFn = jest.fn(); + + const { result } = renderHook(() => + useFilterHandlers({ + getFilterQueryParam: jest.fn(), + createUrl: jest.fn(), + cleanupFn, + params: { + activeTab: "tab", + asc: true, + sort: "sort", + query: "query", + }, + defaultSortField: "", + keepActiveTab: false, + }), + ); + + const handleSearchChange = result.current[2]; + + // Act + handleSearchChange("queryTest"); + + // Assert + + expect(cleanupFn).toHaveBeenCalledTimes(1); + }); + + test("should run createUrl function with params and query", () => { + const createUrl = jest.fn(); + + const { result } = renderHook(() => + useFilterHandlers({ + getFilterQueryParam: jest.fn(filter => ({ + [filter.name]: filter.value[0], + })), + createUrl, + params: { + activeTab: "tab", + asc: true, + sort: "sort", + query: "query", + }, + defaultSortField: "", + keepActiveTab: true, + }), + ); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handleSearchChange = result.current[2]; + + // Act + handleSearchChange("queryTest"); + + // Assert + expect(createUrl).toHaveBeenCalledWith({ + after: undefined, + before: undefined, + asc: true, + sort: "sort", + query: "queryTest", + activeTab: undefined, + }); + }); + + test("should run createUrl function with sort rank and asc false when hasSortWithRank is true", () => { + const createUrl = jest.fn(); + + const { result } = renderHook(() => + useFilterHandlers({ + getFilterQueryParam: jest.fn(filter => ({ + [filter.name]: filter.value[0], + })), + createUrl, + params: { + activeTab: "tab", + asc: true, + sort: "sort", + query: "query", + }, + hasSortWithRank: true, + defaultSortField: "", + keepActiveTab: true, + }), + ); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handleSearchChange = result.current[2]; + + // Act + handleSearchChange("queryTest"); + + // Assert + expect(createUrl).toHaveBeenCalledWith({ + after: undefined, + before: undefined, + asc: false, + sort: "rank", + query: "queryTest", + activeTab: undefined, + }); + }); + }); +}); diff --git a/src/hooks/useFilterHandlers.ts b/src/hooks/useFilterHandlers.ts new file mode 100644 index 000000000..2cbbe2180 --- /dev/null +++ b/src/hooks/useFilterHandlers.ts @@ -0,0 +1,141 @@ +import { IFilter } from "@dashboard/components/Filter"; +import { ActiveTab, Pagination, Search, Sort } from "@dashboard/types"; +import { + GetFilterQueryParam, + getFilterQueryParams, +} from "@dashboard/utils/filters"; +import { useEffect, useRef } from "react"; + +import useNavigator from "./useNavigator"; + +type RequiredParams = ActiveTab & + Search & + Sort & + Pagination & { presestesChanged?: string }; +type CreateUrl = (params: RequiredParams) => string; +type CreateFilterHandlers = [ + (filter: IFilter) => void, + () => void, + (query: string) => void, +]; + +export const useFilterHandlers = < + TFilterKeys extends string, + TFilters extends {}, + SortField extends string, +>(opts: { + getFilterQueryParam: GetFilterQueryParam; + createUrl: CreateUrl; + params: RequiredParams; + cleanupFn?: () => void; + keepActiveTab?: boolean; + defaultSortField: SortField; + hasSortWithRank?: boolean; +}): CreateFilterHandlers => { + const { + getFilterQueryParam, + createUrl, + params, + cleanupFn, + keepActiveTab, + defaultSortField, + hasSortWithRank = false, + } = opts; + + const navigate = useNavigator(); + const prevAsc = useRef(null); + + useEffect(() => { + const hasQuery = !!params.query?.trim(); + if (hasQuery || params.sort === "rank") { + prevAsc.current = params.asc; + } + }, [params.asc, params.query, params.sort]); + + const getActiveTabValue = (removeActiveTab: boolean) => { + if (!keepActiveTab || removeActiveTab) { + return undefined; + } + + return params.activeTab; + }; + + const changeFilters = (filters: IFilter) => { + if (!!cleanupFn) { + cleanupFn(); + } + const filtersQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam, + ); + navigate( + createUrl({ + ...params, + ...filtersQueryParams, + activeTab: getActiveTabValue( + checkIfParamsEmpty(filtersQueryParams) && !params.query?.length, + ), + }), + ); + }; + + const resetFilters = () => { + if (!!cleanupFn) { + cleanupFn(); + } + + navigate( + createUrl({ + asc: params.asc, + sort: params.sort, + }), + ); + }; + + const handleSearchChange = (query: string) => { + if (!!cleanupFn) { + cleanupFn(); + } + const trimmedQuery = query?.trim() ?? ""; + const hasQuery = !!trimmedQuery; + const sortWithoutQuery = + params.sort === "rank" ? defaultSortField : params.sort; + const sortWithQuery = "rank" as SortField; + + const getAscParam = () => { + if (hasQuery) { + return false; + } + + if (prevAsc !== null) { + return true; + } + + return params.asc; + }; + + navigate( + createUrl({ + ...params, + after: undefined, + before: undefined, + activeTab: getActiveTabValue(checkIfParamsEmpty(params) && hasQuery), + query: hasQuery ? trimmedQuery : undefined, + ...(hasSortWithRank && { + sort: hasQuery ? sortWithQuery : sortWithoutQuery, + asc: getAscParam(), + }), + }), + ); + }; + + return [changeFilters, resetFilters, handleSearchChange]; +}; + +function checkIfParamsEmpty(params: RequiredParams): boolean { + const paramsToOmit = ["activeTab", "sort", "asc", "query"]; + + return Object.entries(params) + .filter(([name]) => !paramsToOmit.includes(name)) + .every(([_, value]) => value === undefined); +} diff --git a/src/orders/views/OrderList/OrderList.tsx b/src/orders/views/OrderList/OrderList.tsx index e065be1d2..3aed773dc 100644 --- a/src/orders/views/OrderList/OrderList.tsx +++ b/src/orders/views/OrderList/OrderList.tsx @@ -9,6 +9,7 @@ import { useOrderDraftCreateMutation, useOrderListQuery, } from "@dashboard/graphql"; +import { useFilterHandlers } from "@dashboard/hooks/useFilterHandlers"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; import useNotifier from "@dashboard/hooks/useNotifier"; @@ -17,7 +18,6 @@ import usePaginator, { createPaginationState, PaginatorContext, } from "@dashboard/hooks/usePaginator"; -import { useSortRedirects } from "@dashboard/hooks/useSortRedirects"; import { getActiveTabIndexAfterTabDelete, getNextUniqueTabName, @@ -25,7 +25,6 @@ import { import { ListViews } from "@dashboard/types"; import { prepareQs } from "@dashboard/utils/filters/qs"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; -import createFilterHandlers from "@dashboard/utils/handlers/filterHandlers"; import createSortHandler from "@dashboard/utils/handlers/sortHandler"; import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps"; import { getSortParams } from "@dashboard/utils/sort"; @@ -38,7 +37,6 @@ import { orderListUrl, OrderListUrlDialog, OrderListUrlQueryParams, - OrderListUrlSortField, orderSettingsPath, orderUrl, } from "../../urls"; @@ -100,14 +98,13 @@ export const OrderList: React.FC = ({ params }) => { const currentTab = params.activeTab !== undefined ? parseInt(params.activeTab, 10) : undefined; - const [changeFilters, resetFilters, handleSearchChange] = - createFilterHandlers({ - createUrl: orderListUrl, - getFilterQueryParam, - navigate, - params, - keepActiveTab: true, - }); + const [changeFilters, resetFilters, handleSearchChange] = useFilterHandlers({ + createUrl: orderListUrl, + getFilterQueryParam, + params, + defaultSortField: DEFAULT_SORT_KEY, + hasSortWithRank: true, + }); const [openModal, closeModal] = createDialogActionHandlers< OrderListUrlDialog, @@ -196,12 +193,6 @@ export const OrderList: React.FC = ({ params }) => { const handleSort = createSortHandler(navigate, orderListUrl, params); - useSortRedirects({ - params, - defaultSortField: DEFAULT_SORT_KEY, - urlFunc: orderListUrl, - }); - return ( = ({ params }) => { channel => channel.slug === params.channel, ); - useSortRedirects({ - params, - defaultSortField: DEFAULT_SORT_KEY, - urlFunc: productListUrl, - resetToDefault: !canBeSorted(params.sort, !!selectedChannel), - }); - const [openModal, closeModal] = createDialogActionHandlers< ProductListUrlDialog, ProductListUrlQueryParams @@ -263,15 +255,15 @@ export const ProductList: React.FC = ({ params }) => { }, }); - const [changeFilters, resetFilters, handleSearchChange] = - createFilterHandlers({ - cleanupFn: clearRowSelection, - createUrl: productListUrl, - getFilterQueryParam, - navigate, - params, - keepActiveTab: true, - }); + const [changeFilters, resetFilters, handleSearchChange] = useFilterHandlers({ + cleanupFn: clearRowSelection, + createUrl: productListUrl, + getFilterQueryParam, + params, + keepActiveTab: true, + defaultSortField: DEFAULT_SORT_KEY, + hasSortWithRank: true, + }); const handleTabChange = (tab: number) => { clearRowSelection();