Introduce usePresetFilters and useRowSelction hooks (#3836)
This commit is contained in:
parent
5eb6888c42
commit
158b22d1ed
12 changed files with 429 additions and 27 deletions
5
.changeset/loud-bags-behave.md
Normal file
5
.changeset/loud-bags-behave.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-dashboard": minor
|
||||
---
|
||||
|
||||
Introduce two new hook to handle datagrid, useFilterPresets to handle CRUD operation on filter presets and useRowSelection to handle datagrid row selection
|
|
@ -249,7 +249,7 @@ describe("useFilterHandlers", () => {
|
|||
query: "query",
|
||||
},
|
||||
defaultSortField: "",
|
||||
keepActiveTab: true,
|
||||
keepActiveTab: false,
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -287,7 +287,7 @@ describe("useFilterHandlers", () => {
|
|||
},
|
||||
hasSortWithRank: true,
|
||||
defaultSortField: "",
|
||||
keepActiveTab: true,
|
||||
keepActiveTab: false,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -120,7 +120,7 @@ export const useFilterHandlers = <
|
|||
...params,
|
||||
after: undefined,
|
||||
before: undefined,
|
||||
activeTab: getActiveTabValue(checkIfParamsEmpty(params) && hasQuery),
|
||||
activeTab: getActiveTabValue(checkIfParamsEmpty(params) && !hasQuery),
|
||||
query: hasQuery ? trimmedQuery : undefined,
|
||||
...(hasSortWithRank && {
|
||||
sort: hasQuery ? sortWithQuery : sortWithoutQuery,
|
||||
|
|
1
src/hooks/useFilterPresets/index.ts
Normal file
1
src/hooks/useFilterPresets/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./useFilterPresets";
|
232
src/hooks/useFilterPresets/useFilterPresets.test.ts
Normal file
232
src/hooks/useFilterPresets/useFilterPresets.test.ts
Normal file
|
@ -0,0 +1,232 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { useFilterPresets } from "./useFilterPresets";
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
jest.mock("@dashboard/hooks/useNavigator", () => () => mockNavigate);
|
||||
|
||||
const baseUrl = "http://localhost";
|
||||
|
||||
describe("useFilterPresets", () => {
|
||||
const originalWindowLocation = window.location;
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, "location", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: new URL(window.location.href),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, "location", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: originalWindowLocation,
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return saved filter presets from storage", () => {
|
||||
// Arrange && Act
|
||||
const presets = [
|
||||
{ name: "preset1", data: "data1" },
|
||||
{ name: "preset2", data: "data2" },
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFilterPresets({
|
||||
getUrl: jest.fn(),
|
||||
params: {},
|
||||
reset: jest.fn(),
|
||||
storageUtils: {
|
||||
deleteFilterTab: jest.fn(),
|
||||
getFilterTabs: jest.fn(() => presets),
|
||||
saveFilterTab: jest.fn(),
|
||||
updateFilterTab: jest.fn(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.current.presets).toEqual(presets);
|
||||
});
|
||||
|
||||
it("should return selected preset index when activeTab param", () => {
|
||||
// Arrange & Act
|
||||
const { result } = renderHook(() =>
|
||||
useFilterPresets({
|
||||
getUrl: jest.fn(),
|
||||
params: {
|
||||
activeTab: "1",
|
||||
},
|
||||
reset: jest.fn(),
|
||||
storageUtils: {
|
||||
deleteFilterTab: jest.fn(),
|
||||
getFilterTabs: jest.fn(() => []),
|
||||
saveFilterTab: jest.fn(),
|
||||
updateFilterTab: jest.fn(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.current.selectedPreset).toEqual(1);
|
||||
});
|
||||
|
||||
it("should handle active filter preset change", () => {
|
||||
// Arrange
|
||||
const savedPreset = { name: "preset1", data: "query=John" };
|
||||
const { result } = renderHook(() =>
|
||||
useFilterPresets({
|
||||
getUrl: jest.fn(() => baseUrl),
|
||||
params: {},
|
||||
reset: jest.fn(),
|
||||
storageUtils: {
|
||||
deleteFilterTab: jest.fn(),
|
||||
getFilterTabs: jest.fn(() => [savedPreset]),
|
||||
saveFilterTab: jest.fn(),
|
||||
updateFilterTab: jest.fn(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
result.current.onPresetChange(1);
|
||||
|
||||
// Assert
|
||||
expect(mockNavigate).toHaveBeenCalledWith(
|
||||
`${baseUrl}?${savedPreset.data}&activeTab=1`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle preset delete and navigate to base url when active preset is equal deleting preset", () => {
|
||||
// Arrange
|
||||
const mockDeleteStorage = jest.fn();
|
||||
const mockReset = jest.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFilterPresets({
|
||||
getUrl: jest.fn(() => baseUrl),
|
||||
params: {
|
||||
action: "delete",
|
||||
activeTab: "1",
|
||||
},
|
||||
reset: mockReset,
|
||||
storageUtils: {
|
||||
deleteFilterTab: mockDeleteStorage,
|
||||
getFilterTabs: jest.fn(),
|
||||
saveFilterTab: jest.fn(),
|
||||
updateFilterTab: jest.fn(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Act(
|
||||
result.current.setPresetIdToDelete(1);
|
||||
result.current.onPresetDelete();
|
||||
|
||||
// Assert
|
||||
expect(mockDeleteStorage).toHaveBeenCalledWith(1);
|
||||
expect(mockReset).toHaveBeenCalled();
|
||||
expect(mockNavigate).toHaveBeenCalledWith(baseUrl);
|
||||
});
|
||||
|
||||
it("should handle preset delete and navigate to active preset when preset to delete is different that preset to delete", () => {
|
||||
// Arrange
|
||||
const mockDeleteStorage = jest.fn();
|
||||
const mockReset = jest.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFilterPresets({
|
||||
getUrl: jest.fn(() => baseUrl),
|
||||
params: {
|
||||
action: "delete",
|
||||
activeTab: "2",
|
||||
},
|
||||
reset: mockReset,
|
||||
storageUtils: {
|
||||
deleteFilterTab: mockDeleteStorage,
|
||||
getFilterTabs: jest.fn(),
|
||||
saveFilterTab: jest.fn(),
|
||||
updateFilterTab: jest.fn(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Act(
|
||||
result.current.setPresetIdToDelete(1);
|
||||
result.current.onPresetDelete();
|
||||
|
||||
// Assert
|
||||
expect(mockDeleteStorage).toHaveBeenCalledWith(1);
|
||||
expect(mockReset).toHaveBeenCalled();
|
||||
expect(mockNavigate).toHaveBeenCalledWith(baseUrl + "?activeTab=1");
|
||||
});
|
||||
|
||||
it("should handle save new filter preset", () => {
|
||||
// Arrange
|
||||
const mockSaveStorage = jest.fn();
|
||||
window.location.search = "?query=John";
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFilterPresets({
|
||||
getUrl: jest.fn(() => baseUrl),
|
||||
params: {},
|
||||
reset: jest.fn(),
|
||||
storageUtils: {
|
||||
deleteFilterTab: jest.fn(),
|
||||
getFilterTabs: jest.fn(() => []),
|
||||
saveFilterTab: mockSaveStorage,
|
||||
updateFilterTab: jest.fn(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
result.current.onPresetSave({ name: "new-preset" });
|
||||
|
||||
// Assert
|
||||
expect(mockSaveStorage).toHaveBeenCalledWith("new-preset", "query=John");
|
||||
expect(mockNavigate).toHaveBeenCalledWith(`${baseUrl}?activeTab=1`);
|
||||
});
|
||||
|
||||
it("should handle update existing filter preset", () => {
|
||||
// Arrange
|
||||
const mockUpdateStorage = jest.fn();
|
||||
window.location.search = "?query=JoeDoe";
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFilterPresets({
|
||||
getUrl: jest.fn(() => baseUrl),
|
||||
params: {},
|
||||
reset: jest.fn(),
|
||||
storageUtils: {
|
||||
deleteFilterTab: jest.fn(),
|
||||
getFilterTabs: jest.fn(() => [
|
||||
{
|
||||
name: "current-preset",
|
||||
data: "query=John",
|
||||
},
|
||||
]),
|
||||
saveFilterTab: jest.fn(),
|
||||
updateFilterTab: mockUpdateStorage,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
result.current.onPresetUpdate("current-preset");
|
||||
|
||||
// Assert
|
||||
expect(mockUpdateStorage).toHaveBeenCalledWith(
|
||||
"current-preset",
|
||||
"query=JoeDoe",
|
||||
);
|
||||
expect(mockNavigate).toHaveBeenCalledWith(
|
||||
`${baseUrl}?query=John&activeTab=1`,
|
||||
);
|
||||
});
|
||||
});
|
124
src/hooks/useFilterPresets/useFilterPresets.ts
Normal file
124
src/hooks/useFilterPresets/useFilterPresets.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
import { SaveFilterTabDialogFormData } from "@dashboard/components/SaveFilterTabDialog";
|
||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||
import {
|
||||
getActiveTabIndexAfterTabDelete,
|
||||
getNextUniqueTabName,
|
||||
} from "@dashboard/products/views/ProductList/utils";
|
||||
import { StorageUtils } from "@dashboard/utils/filters";
|
||||
import { prepareQs } from "@dashboard/utils/filters/qs";
|
||||
import { stringify } from "qs";
|
||||
import { useState } from "react";
|
||||
|
||||
export const useFilterPresets = <
|
||||
T extends { activeTab?: string; action?: string },
|
||||
>({
|
||||
params,
|
||||
reset,
|
||||
storageUtils,
|
||||
getUrl,
|
||||
}: {
|
||||
params: T;
|
||||
reset: () => void;
|
||||
getUrl: () => string;
|
||||
storageUtils: StorageUtils<string>;
|
||||
}) => {
|
||||
const navigate = useNavigator();
|
||||
const baseUrl = getUrl();
|
||||
const [presetIdToDelete, setPresetIdToDelete] = useState<number | null>(null);
|
||||
|
||||
const presets = storageUtils.getFilterTabs();
|
||||
|
||||
const selectedPreset =
|
||||
params.activeTab !== undefined && typeof params.activeTab === "string"
|
||||
? parseInt(params.activeTab, 10)
|
||||
: undefined;
|
||||
|
||||
const onPresetChange = (index: number) => {
|
||||
reset();
|
||||
const currentPresets = storageUtils.getFilterTabs();
|
||||
const qs = new URLSearchParams(currentPresets[index - 1]?.data ?? "");
|
||||
qs.append("activeTab", index.toString());
|
||||
|
||||
navigate(
|
||||
baseUrl.endsWith("?")
|
||||
? baseUrl + qs.toString()
|
||||
: baseUrl + "?" + qs.toString(),
|
||||
);
|
||||
};
|
||||
|
||||
const onPresetDelete = () => {
|
||||
if (!presetIdToDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
storageUtils.deleteFilterTab(presetIdToDelete);
|
||||
reset();
|
||||
|
||||
// When deleting the current tab, navigate to the All products
|
||||
if (presetIdToDelete === selectedPreset || !selectedPreset) {
|
||||
navigate(baseUrl);
|
||||
} else {
|
||||
const currentParams = { ...params };
|
||||
// When deleting a tab that is not the current one, only remove the action param from the query
|
||||
delete currentParams.action;
|
||||
// When deleting a tab that is before the current one, decrease the activeTab param by 1
|
||||
currentParams.activeTab = getActiveTabIndexAfterTabDelete(
|
||||
selectedPreset,
|
||||
presetIdToDelete,
|
||||
);
|
||||
navigate(
|
||||
baseUrl.endsWith("?")
|
||||
? baseUrl + stringify(currentParams)
|
||||
: baseUrl + "?" + stringify(currentParams),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onPresetSave = (data: SaveFilterTabDialogFormData) => {
|
||||
const { parsedQs } = prepareQs(location.search);
|
||||
|
||||
storageUtils.saveFilterTab(
|
||||
getNextUniqueTabName(
|
||||
data.name,
|
||||
presets.map(tab => tab.name),
|
||||
),
|
||||
stringify(parsedQs),
|
||||
);
|
||||
onPresetChange(presets.length + 1);
|
||||
};
|
||||
|
||||
const onPresetUpdate = (tabName: string) => {
|
||||
const { parsedQs } = prepareQs(location.search);
|
||||
|
||||
storageUtils.updateFilterTab(tabName, stringify(parsedQs));
|
||||
onPresetChange(presets.findIndex(tab => tab.name === tabName) + 1);
|
||||
};
|
||||
|
||||
const hasPresetsChange = () => {
|
||||
const { parsedQs } = prepareQs(location.search);
|
||||
|
||||
if (!selectedPreset) {
|
||||
return location.search !== "" && stringify(parsedQs) !== "";
|
||||
}
|
||||
|
||||
const activeTab = presets[selectedPreset - 1];
|
||||
|
||||
return (
|
||||
activeTab?.data !== stringify(parsedQs) &&
|
||||
location.search !== "" &&
|
||||
stringify(parsedQs) !== ""
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
presetIdToDelete,
|
||||
setPresetIdToDelete,
|
||||
presets,
|
||||
selectedPreset,
|
||||
onPresetChange,
|
||||
onPresetDelete,
|
||||
onPresetSave,
|
||||
onPresetUpdate,
|
||||
hasPresetsChange,
|
||||
};
|
||||
};
|
32
src/hooks/useRowSelection.ts
Normal file
32
src/hooks/useRowSelection.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { Pagination } from "@dashboard/types";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export const useRowSelection = (paginationParams?: Pagination) => {
|
||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||
|
||||
// Keep reference to clear datagrid selection function
|
||||
const clearDatagridRowSelectionCallback = useRef<(() => void) | null>(null);
|
||||
|
||||
const clearRowSelection = () => {
|
||||
setSelectedRowIds([]);
|
||||
if (clearDatagridRowSelectionCallback.current) {
|
||||
clearDatagridRowSelectionCallback.current();
|
||||
}
|
||||
};
|
||||
|
||||
const setClearDatagridRowSelectionCallback = (callback: () => void) => {
|
||||
clearDatagridRowSelectionCallback.current = callback;
|
||||
};
|
||||
|
||||
// Whenever pagination change we need to clear datagrid selection
|
||||
useEffect(() => {
|
||||
clearRowSelection();
|
||||
}, [paginationParams?.after, paginationParams?.before]);
|
||||
|
||||
return {
|
||||
selectedRowIds,
|
||||
setSelectedRowIds,
|
||||
clearRowSelection,
|
||||
setClearDatagridRowSelectionCallback,
|
||||
};
|
||||
};
|
|
@ -114,12 +114,12 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
|||
|
||||
const hasPresetsChanged = () => {
|
||||
const activeTab = tabs[currentTab - 1];
|
||||
const { paresedQs } = prepareQs(location.search);
|
||||
const { parsedQs } = prepareQs(location.search);
|
||||
|
||||
return (
|
||||
activeTab?.data !== stringify(paresedQs) &&
|
||||
activeTab?.data !== stringify(parsedQs) &&
|
||||
location.search !== "" &&
|
||||
stringify(paresedQs) !== ""
|
||||
stringify(parsedQs) !== ""
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -150,23 +150,23 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
|||
};
|
||||
|
||||
const hanleFilterTabUpdate = (tabName: string) => {
|
||||
const { paresedQs, activeTab } = prepareQs(location.search);
|
||||
const { parsedQs, activeTab } = prepareQs(location.search);
|
||||
|
||||
updateFilterTab(tabName, stringify(paresedQs));
|
||||
paresedQs.activeTab = activeTab;
|
||||
updateFilterTab(tabName, stringify(parsedQs));
|
||||
parsedQs.activeTab = activeTab;
|
||||
|
||||
navigate(orderListUrl() + "?" + stringify(paresedQs));
|
||||
navigate(orderListUrl() + "?" + stringify(parsedQs));
|
||||
};
|
||||
|
||||
const handleFilterTabSave = (data: SaveFilterTabDialogFormData) => {
|
||||
const { paresedQs } = prepareQs(location.search);
|
||||
const { parsedQs } = prepareQs(location.search);
|
||||
|
||||
saveFilterTab(
|
||||
getNextUniqueTabName(
|
||||
data.name,
|
||||
tabs.map(tab => tab.name),
|
||||
),
|
||||
stringify(paresedQs),
|
||||
stringify(parsedQs),
|
||||
);
|
||||
handleTabChange(tabs.length + 1);
|
||||
};
|
||||
|
|
|
@ -298,25 +298,25 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
};
|
||||
|
||||
const handleFilterTabSave = (data: SaveFilterTabDialogFormData) => {
|
||||
const { paresedQs } = prepareQs(location.search);
|
||||
const { parsedQs } = prepareQs(location.search);
|
||||
|
||||
saveFilterTab(
|
||||
getNextUniqueTabName(
|
||||
data.name,
|
||||
tabs.map(tab => tab.name),
|
||||
),
|
||||
stringify(paresedQs),
|
||||
stringify(parsedQs),
|
||||
);
|
||||
handleTabChange(tabs.length + 1);
|
||||
};
|
||||
|
||||
const hanleFilterTabUpdate = (tabName: string) => {
|
||||
const { paresedQs, activeTab } = prepareQs(location.search);
|
||||
const { parsedQs, activeTab } = prepareQs(location.search);
|
||||
|
||||
updateFilterTab(tabName, stringify(paresedQs));
|
||||
paresedQs.activeTab = activeTab;
|
||||
updateFilterTab(tabName, stringify(parsedQs));
|
||||
parsedQs.activeTab = activeTab;
|
||||
|
||||
navigate(productListUrl() + stringify(paresedQs));
|
||||
navigate(productListUrl() + stringify(parsedQs));
|
||||
};
|
||||
|
||||
const handleSort = (field: ProductListUrlSortField, attributeId?: string) =>
|
||||
|
@ -442,12 +442,12 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
|
||||
const hasPresetsChanged = () => {
|
||||
const activeTab = tabs[currentTab - 1];
|
||||
const { paresedQs } = prepareQs(location.search);
|
||||
const { parsedQs } = prepareQs(location.search);
|
||||
|
||||
return (
|
||||
activeTab?.data !== stringify(paresedQs) &&
|
||||
activeTab?.data !== stringify(parsedQs) &&
|
||||
location.search !== "" &&
|
||||
stringify(paresedQs) !== ""
|
||||
stringify(parsedQs) !== ""
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ describe("Filters: preapreQS", () => {
|
|||
);
|
||||
expect(qs).toEqual({
|
||||
activeTab: "1",
|
||||
paresedQs: {
|
||||
parsedQs: {
|
||||
category: "1",
|
||||
channel: "usa",
|
||||
},
|
||||
|
|
|
@ -3,17 +3,17 @@ import { parse } from "qs";
|
|||
const paramsToRemove = ["activeTab", "action", "sort", "asc"];
|
||||
|
||||
export const prepareQs = (searchQuery: string) => {
|
||||
const paresedQs = parse(
|
||||
const parsedQs = parse(
|
||||
searchQuery.startsWith("?") ? searchQuery.slice(1) : searchQuery,
|
||||
);
|
||||
const activeTab = paresedQs.activeTab;
|
||||
const activeTab = parsedQs.activeTab;
|
||||
|
||||
paramsToRemove.forEach(param => {
|
||||
delete paresedQs[param];
|
||||
delete parsedQs[param];
|
||||
});
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
paresedQs,
|
||||
parsedQs,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -59,8 +59,16 @@ function deleteFilterTab(id: number, key: string) {
|
|||
JSON.stringify([...userFilters.slice(0, id - 1), ...userFilters.slice(id)]),
|
||||
);
|
||||
}
|
||||
export interface StorageUtils<TUrlFilters> {
|
||||
getFilterTabs: () => GetFilterTabsOutput<TUrlFilters>;
|
||||
deleteFilterTab: (id: number) => void;
|
||||
saveFilterTab: (name: string, data: TUrlFilters) => void;
|
||||
updateFilterTab: (name: string, data: TUrlFilters) => void;
|
||||
}
|
||||
|
||||
function createFilterTabUtils<TUrlFilters>(key: string) {
|
||||
function createFilterTabUtils<TUrlFilters>(
|
||||
key: string,
|
||||
): StorageUtils<TUrlFilters> {
|
||||
return {
|
||||
deleteFilterTab: (id: number) => deleteFilterTab(id, key),
|
||||
getFilterTabs: () => getFilterTabs<TUrlFilters>(key),
|
||||
|
|
Loading…
Reference in a new issue