Select filter presets (#3412)
This commit is contained in:
parent
42584cdfc2
commit
63b98a08bf
49 changed files with 27090 additions and 276 deletions
|
@ -764,6 +764,9 @@
|
|||
"3DGvA/": {
|
||||
"string": "Remember this will also unpin all products assigned to this category, making them unavailable in storefront."
|
||||
},
|
||||
"3PVGWj": {
|
||||
"string": "Filter preset"
|
||||
},
|
||||
"3Sz1/t": {
|
||||
"context": "dialog header",
|
||||
"string": "Delete Pages"
|
||||
|
@ -1283,10 +1286,6 @@
|
|||
"7NFfmz": {
|
||||
"string": "Products"
|
||||
},
|
||||
"7NfoiJ": {
|
||||
"context": "custom search delete, dialog header",
|
||||
"string": "Delete Search"
|
||||
},
|
||||
"7Oorx5": {
|
||||
"context": "navigator section header",
|
||||
"string": "Search in Catalog"
|
||||
|
@ -1595,6 +1594,9 @@
|
|||
"context": "used staff users counter",
|
||||
"string": "{count}/{max} members"
|
||||
},
|
||||
"A+g/VP": {
|
||||
"string": "Unsaved preset"
|
||||
},
|
||||
"A0Wlg7": {
|
||||
"context": "product discount removed title",
|
||||
"string": "{productName} discount was removed by"
|
||||
|
@ -1786,6 +1788,9 @@
|
|||
"BUKMzM": {
|
||||
"string": "Variant removed"
|
||||
},
|
||||
"BWpuKl": {
|
||||
"string": "Update"
|
||||
},
|
||||
"BXMSl4": {
|
||||
"context": "currency channel",
|
||||
"string": "There is no available channel to move order information to. Please create a channel with same currency so that information can be moved to it."
|
||||
|
@ -3688,6 +3693,10 @@
|
|||
"P7PLVj": {
|
||||
"string": "Date"
|
||||
},
|
||||
"P9YktI": {
|
||||
"context": "save preset, header",
|
||||
"string": "Save view preset"
|
||||
},
|
||||
"PAqicb": {
|
||||
"context": "button",
|
||||
"string": "Cancel order"
|
||||
|
@ -3923,10 +3932,6 @@
|
|||
"context": "dialog header",
|
||||
"string": "Delete Channel"
|
||||
},
|
||||
"QcIFCs": {
|
||||
"context": "save search tab",
|
||||
"string": "Search Name"
|
||||
},
|
||||
"QclvqG": {
|
||||
"context": "input label",
|
||||
"string": "Checkout line limit"
|
||||
|
@ -4378,6 +4383,9 @@
|
|||
"U2mOqA": {
|
||||
"string": "No vouchers found"
|
||||
},
|
||||
"U5CH0u": {
|
||||
"string": "Are you sure you want to delete {name} preset?"
|
||||
},
|
||||
"U5Da30": {
|
||||
"string": "Warehouses"
|
||||
},
|
||||
|
@ -4442,9 +4450,6 @@
|
|||
"context": "filters error messages unknown error",
|
||||
"string": "Unknown error occurred"
|
||||
},
|
||||
"UaYJJ8": {
|
||||
"string": "Are you sure you want to delete {name} search tab?"
|
||||
},
|
||||
"Uf3oHA": {
|
||||
"context": "add new menu item",
|
||||
"string": "Create new item"
|
||||
|
@ -5838,6 +5843,9 @@
|
|||
"eUjFjW": {
|
||||
"string": "Permission group created"
|
||||
},
|
||||
"eW36Jx": {
|
||||
"string": "Saved search queries will appear here"
|
||||
},
|
||||
"eWV760": {
|
||||
"string": "Attribute with this slug already exists"
|
||||
},
|
||||
|
@ -6751,10 +6759,6 @@
|
|||
"context": "export items as csv file",
|
||||
"string": "Plain CSV file"
|
||||
},
|
||||
"liLrVs": {
|
||||
"context": "save filter tab, header",
|
||||
"string": "Save Custom Search"
|
||||
},
|
||||
"lit2zF": {
|
||||
"string": "Search Discounts"
|
||||
},
|
||||
|
@ -7638,6 +7642,9 @@
|
|||
"context": "button",
|
||||
"string": "Assign product"
|
||||
},
|
||||
"scTuDZ": {
|
||||
"string": "Save search as preset"
|
||||
},
|
||||
"sdA14A": {
|
||||
"context": "Status label when object is published in a channel",
|
||||
"string": "Published"
|
||||
|
@ -7718,6 +7725,10 @@
|
|||
"context": "webhook input help text",
|
||||
"string": "secret key is used to create a hash signature with each payload. *optional field"
|
||||
},
|
||||
"tCLTCb": {
|
||||
"context": "tab name",
|
||||
"string": "All products"
|
||||
},
|
||||
"tEdFmZ": {
|
||||
"context": "notification title",
|
||||
"string": "System update required"
|
||||
|
@ -8351,6 +8362,10 @@
|
|||
"xxQxLE": {
|
||||
"string": "Email Address"
|
||||
},
|
||||
"xy66ru": {
|
||||
"context": "custom preset delete, dialog header",
|
||||
"string": "Delete preset"
|
||||
},
|
||||
"y/UWBR": {
|
||||
"string": "There is no address to show for this customer"
|
||||
},
|
||||
|
@ -8572,6 +8587,10 @@
|
|||
"context": "attribute list",
|
||||
"string": "Attribute {name}"
|
||||
},
|
||||
"zhnwl6": {
|
||||
"context": "save preset name",
|
||||
"string": "Preset name"
|
||||
},
|
||||
"zjHH6b": {
|
||||
"context": "sale start date",
|
||||
"string": "Started"
|
||||
|
|
26282
package-lock.json
generated
26282
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -33,7 +33,7 @@
|
|||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@material-ui/styles": "^4.11.4",
|
||||
"@reach/auto-id": "^0.16.0",
|
||||
"@saleor/macaw-ui": "^0.8.0-pre.50",
|
||||
"@saleor/macaw-ui": "^0.8.0-pre.64",
|
||||
"@saleor/sdk": "^0.4.6",
|
||||
"@sentry/react": "^6.0.0",
|
||||
"@types/faker": "^5.1.6",
|
||||
|
|
|
@ -14,6 +14,7 @@ export interface ActionDialogProps extends DialogProps {
|
|||
maxWidth?: Size | false;
|
||||
title: string;
|
||||
variant?: ActionDialogVariant;
|
||||
backButtonText?: string;
|
||||
onConfirm();
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ interface DialogButtonsProps {
|
|||
variant?: ActionDialogVariant;
|
||||
children?: React.ReactNode;
|
||||
showBackButton?: boolean;
|
||||
backButtonText?: string;
|
||||
onConfirm();
|
||||
}
|
||||
|
||||
|
@ -29,6 +30,7 @@ const DialogButtons: React.FC<DialogButtonsProps> = props => {
|
|||
onClose,
|
||||
children,
|
||||
showBackButton = true,
|
||||
backButtonText,
|
||||
} = props;
|
||||
|
||||
const intl = useIntl();
|
||||
|
@ -36,7 +38,9 @@ const DialogButtons: React.FC<DialogButtonsProps> = props => {
|
|||
return (
|
||||
<DialogActions>
|
||||
{children}
|
||||
{showBackButton && <BackButton onClick={onClose} />}
|
||||
{showBackButton && (
|
||||
<BackButton onClick={onClose}>{backButtonText}</BackButton>
|
||||
)}
|
||||
{variant !== "info" && (
|
||||
<ConfirmButton
|
||||
disabled={disabled}
|
||||
|
|
|
@ -98,7 +98,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
|
|||
borderColor="neutralPlain"
|
||||
__maxWidth={contentMaxWidth}
|
||||
margin="auto"
|
||||
zIndex="3"
|
||||
zIndex="2"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import { Box, DropdownButton } from "@saleor/macaw-ui/next";
|
||||
import React, { MouseEventHandler } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
interface FilterButtonProps {
|
||||
isFilterActive: boolean;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
selectedFilterAmount: number;
|
||||
}
|
||||
|
||||
export const FilterButton = ({
|
||||
isFilterActive,
|
||||
onClick,
|
||||
selectedFilterAmount,
|
||||
}: FilterButtonProps) => (
|
||||
<DropdownButton data-test-id="show-filters-button" onClick={onClick}>
|
||||
<FormattedMessage
|
||||
id="FNpv6K"
|
||||
defaultMessage="Filters"
|
||||
description="button"
|
||||
/>
|
||||
{isFilterActive && (
|
||||
<>
|
||||
<Box
|
||||
as="span"
|
||||
backgroundColor="interactiveNeutralDefault"
|
||||
height={6}
|
||||
width={1}
|
||||
marginX={3}
|
||||
/>
|
||||
{selectedFilterAmount}
|
||||
</>
|
||||
)}
|
||||
</DropdownButton>
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
export * from "./FilterBar";
|
|
@ -3,10 +3,10 @@ import { FilterProps, SearchPageProps } from "@dashboard/types";
|
|||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import { Filter } from "./Filter";
|
||||
import SearchInput from "./SearchInput";
|
||||
import { FiltersSelect } from "./components/FiltersSelect";
|
||||
import SearchInput from "./components/SearchInput";
|
||||
|
||||
export interface FilterBarProps<TKeys extends string = string>
|
||||
export interface ListFiltersProps<TKeys extends string = string>
|
||||
extends FilterProps<TKeys>,
|
||||
SearchPageProps {
|
||||
searchPlaceholder: string;
|
||||
|
@ -15,7 +15,7 @@ export interface FilterBarProps<TKeys extends string = string>
|
|||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
export const FilterBar: React.FC<FilterBarProps> = ({
|
||||
export const ListFilters = ({
|
||||
currencySymbol,
|
||||
filterStructure,
|
||||
initialSearch,
|
||||
|
@ -25,11 +25,11 @@ export const FilterBar: React.FC<FilterBarProps> = ({
|
|||
onFilterAttributeFocus,
|
||||
errorMessages,
|
||||
actions,
|
||||
}: FilterBarProps) => (
|
||||
}: ListFiltersProps) => (
|
||||
<>
|
||||
<Box
|
||||
display="grid"
|
||||
__gridTemplateColumns="1fr 1fr"
|
||||
gridTemplateColumns={2}
|
||||
gap={7}
|
||||
paddingBottom={5}
|
||||
paddingX={9}
|
||||
|
@ -38,13 +38,14 @@ export const FilterBar: React.FC<FilterBarProps> = ({
|
|||
borderBottomWidth={1}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={7}>
|
||||
<Filter
|
||||
<FiltersSelect
|
||||
errorMessages={errorMessages}
|
||||
menu={filterStructure}
|
||||
currencySymbol={currencySymbol}
|
||||
onFilterAdd={onFilterChange}
|
||||
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||
/>
|
||||
|
||||
<Box __width="320px">
|
||||
<SearchInput
|
||||
initialSearch={initialSearch}
|
||||
|
@ -59,4 +60,4 @@ export const FilterBar: React.FC<FilterBarProps> = ({
|
|||
</Box>
|
||||
</>
|
||||
);
|
||||
FilterBar.displayName = "FilterBar";
|
||||
ListFilters.displayName = "FilterBar";
|
|
@ -8,11 +8,11 @@ import {
|
|||
import useFilter from "@dashboard/components/Filter/useFilter";
|
||||
import { extractInvalidFilters } from "@dashboard/components/Filter/utils";
|
||||
import { ClickAwayListener, Grow, Popper } from "@material-ui/core";
|
||||
import { sprinkles } from "@saleor/macaw-ui/next";
|
||||
import { DropdownButton, sprinkles } from "@saleor/macaw-ui/next";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { FilterButton } from "./FilterButton";
|
||||
import { getSelectedFilterAmount } from "./utils";
|
||||
import { getSelectedFilterAmount } from "../utils";
|
||||
|
||||
export interface FilterProps<TFilterKeys extends string = string> {
|
||||
currencySymbol?: string;
|
||||
|
@ -22,7 +22,7 @@ export interface FilterProps<TFilterKeys extends string = string> {
|
|||
onFilterAttributeFocus?: (id?: string) => void;
|
||||
}
|
||||
|
||||
export const Filter = ({
|
||||
export const FiltersSelect = ({
|
||||
currencySymbol,
|
||||
menu,
|
||||
onFilterAdd,
|
||||
|
@ -68,11 +68,19 @@ export const Filter = ({
|
|||
mouseEvent="onMouseUp"
|
||||
>
|
||||
<div ref={anchor}>
|
||||
<FilterButton
|
||||
isFilterActive={isFilterActive}
|
||||
<DropdownButton
|
||||
data-test-id="show-filters-button"
|
||||
onClick={() => setFilterMenuOpened(!isFilterMenuOpened)}
|
||||
selectedFilterAmount={selectedFilterAmount}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="FNpv6K"
|
||||
defaultMessage="Filters"
|
||||
description="button"
|
||||
/>
|
||||
{isFilterActive && selectedFilterAmount > 0 && (
|
||||
<>({selectedFilterAmount})</>
|
||||
)}
|
||||
</DropdownButton>
|
||||
<Popper
|
||||
className={sprinkles({
|
||||
backgroundColor: "surfaceNeutralPlain",
|
||||
|
@ -123,4 +131,4 @@ export const Filter = ({
|
|||
);
|
||||
};
|
||||
|
||||
Filter.displayName = "Filter";
|
||||
FiltersSelect.displayName = "Filter";
|
1
src/components/AppLayout/ListFilters/index.ts
Normal file
1
src/components/AppLayout/ListFilters/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./ListFilters";
|
|
@ -10,12 +10,14 @@ interface TopNavProps {
|
|||
title: string | React.ReactNode;
|
||||
href?: string;
|
||||
withoutBorder?: boolean;
|
||||
isAlignToRight?: boolean;
|
||||
}
|
||||
|
||||
export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
||||
title,
|
||||
href,
|
||||
withoutBorder = false,
|
||||
isAlignToRight = true,
|
||||
children,
|
||||
}) => {
|
||||
const { availableChannels, channel, isPickerActive, setChannel } =
|
||||
|
@ -24,10 +26,16 @@ export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
|||
return (
|
||||
<TopNavWrapper withoutBorder={withoutBorder}>
|
||||
{href && <TopNavLink to={href} />}
|
||||
<Box __flex={1}>
|
||||
<Text variant="title">{title}</Text>
|
||||
<Box __flex={isAlignToRight ? 1 : 0}>
|
||||
<Text variant="title" size="small">
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" flexWrap="nowrap">
|
||||
<Box
|
||||
display="flex"
|
||||
flexWrap="nowrap"
|
||||
__flex={isAlignToRight ? "initial" : 1}
|
||||
>
|
||||
{isPickerActive && (
|
||||
<AppChannelSelect
|
||||
channels={availableChannels}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import { buttonMessages } from "@dashboard/intl";
|
||||
import { ButtonProps } from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const BackButton: React.FC<ButtonProps> = props => (
|
||||
interface BackButtonProps extends ButtonProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const BackButton: React.FC<BackButtonProps> = ({ children, ...props }) => (
|
||||
<Button data-test-id="back" variant="secondary" color="text" {...props}>
|
||||
<FormattedMessage {...buttonMessages.back} />
|
||||
{children ?? <FormattedMessage {...buttonMessages.back} />}
|
||||
</Button>
|
||||
);
|
||||
|
||||
|
|
|
@ -88,6 +88,7 @@ export interface DatagridProps {
|
|||
onColumnMoved?: (startIndex: number, endIndex: number) => void;
|
||||
onColumnResize?: (column: GridColumn, newSize: number) => void;
|
||||
readonly?: boolean;
|
||||
hasRowHover?: boolean;
|
||||
rowMarkers?: DataEditorProps["rowMarkers"];
|
||||
freezeColumns?: DataEditorProps["freezeColumns"];
|
||||
verticalBorder?: DataEditorProps["verticalBorder"];
|
||||
|
@ -117,6 +118,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
columnSelect = "none",
|
||||
onColumnMoved,
|
||||
onColumnResize,
|
||||
hasRowHover = false,
|
||||
...datagridProps
|
||||
}): ReactElement => {
|
||||
const classes = useStyles();
|
||||
|
@ -219,11 +221,11 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
|
||||
const handleRowHover = useCallback(
|
||||
(args: GridMouseEventArgs) => {
|
||||
if (readonly) {
|
||||
if (hasRowHover) {
|
||||
setHoverRow(args.kind !== "cell" ? undefined : args.location[1]);
|
||||
}
|
||||
},
|
||||
[readonly],
|
||||
[hasRowHover],
|
||||
);
|
||||
|
||||
const handleGridSelectionChange = (gridSelection: GridSelection) => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { buttonMessages } from "@dashboard/intl";
|
||||
import { DialogContentText } from "@material-ui/core";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
|
@ -26,19 +27,20 @@ const DeleteFilterTabDialog: React.FC<DeleteFilterTabDialogProps> = ({
|
|||
<ActionDialog
|
||||
open={open}
|
||||
confirmButtonState={confirmButtonState}
|
||||
backButtonText={intl.formatMessage(buttonMessages.cancel)}
|
||||
onClose={onClose}
|
||||
onConfirm={onSubmit}
|
||||
title={intl.formatMessage({
|
||||
id: "7NfoiJ",
|
||||
defaultMessage: "Delete Search",
|
||||
description: "custom search delete, dialog header",
|
||||
id: "xy66ru",
|
||||
defaultMessage: "Delete preset",
|
||||
description: "custom preset delete, dialog header",
|
||||
})}
|
||||
variant="delete"
|
||||
>
|
||||
<DialogContentText>
|
||||
<FormattedMessage
|
||||
id="UaYJJ8"
|
||||
defaultMessage="Are you sure you want to delete {name} search tab?"
|
||||
id="U5CH0u"
|
||||
defaultMessage="Are you sure you want to delete {name} preset?"
|
||||
values={{
|
||||
name: <strong>{tabName}</strong>,
|
||||
}}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { alpha } from "@material-ui/core/styles";
|
|||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
||||
import { vars } from "@saleor/macaw-ui/next";
|
||||
import clsx from "clsx";
|
||||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { FilterContent } from ".";
|
||||
|
@ -14,7 +14,7 @@ import {
|
|||
InvalidFilters,
|
||||
} from "./types";
|
||||
import useFilter from "./useFilter";
|
||||
import { extractInvalidFilters } from "./utils";
|
||||
import { extractInvalidFilters, getSelectedFiltersAmount } from "./utils";
|
||||
|
||||
export interface FilterProps<TFilterKeys extends string = string> {
|
||||
currencySymbol?: string;
|
||||
|
@ -103,6 +103,11 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
|
||||
const isFilterActive = menu.some(filterElement => filterElement.active);
|
||||
|
||||
const selectedFilterAmount = useMemo(
|
||||
() => getSelectedFiltersAmount(menu, data),
|
||||
[data, menu],
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const invalidFilters = extractInvalidFilters(data, menu);
|
||||
|
||||
|
@ -147,23 +152,8 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
description="button"
|
||||
/>
|
||||
</Typography>
|
||||
{isFilterActive && (
|
||||
<>
|
||||
<span className={classes.separator} />
|
||||
<Typography className={classes.addFilterText}>
|
||||
{menu.reduce((acc, filterElement) => {
|
||||
const dataFilterElement = data.find(
|
||||
({ name }) => name === filterElement.name,
|
||||
);
|
||||
|
||||
if (!dataFilterElement) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc + (dataFilterElement.active ? 1 : 0);
|
||||
}, 0)}
|
||||
</Typography>
|
||||
</>
|
||||
{isFilterActive && selectedFilterAmount > 0 && (
|
||||
<>({selectedFilterAmount})</>
|
||||
)}
|
||||
</Button>
|
||||
<Popper
|
||||
|
|
|
@ -16,31 +16,11 @@ export interface FilterReducerAction<K extends string, T extends FieldType> {
|
|||
}>;
|
||||
}
|
||||
export type UpdateStateFunction<K extends string = string> = <
|
||||
T extends FieldType
|
||||
T extends FieldType,
|
||||
>(
|
||||
value: FilterReducerAction<K, T>,
|
||||
) => void;
|
||||
|
||||
function merge<T extends string>(
|
||||
prevState: IFilter<T>,
|
||||
newState: IFilter<T>,
|
||||
): IFilter<T> {
|
||||
return newState.map(newFilter => {
|
||||
const prevFilter = prevState.find(
|
||||
prevFilter => prevFilter.name === newFilter.name,
|
||||
);
|
||||
if (!!prevFilter) {
|
||||
return {
|
||||
...newFilter,
|
||||
active: prevFilter.active,
|
||||
value: prevFilter.value,
|
||||
};
|
||||
}
|
||||
|
||||
return newFilter;
|
||||
});
|
||||
}
|
||||
|
||||
function setProperty<K extends string, T extends FieldType>(
|
||||
prevState: IFilter<K, T>,
|
||||
filter: K,
|
||||
|
@ -66,8 +46,6 @@ function reduceFilter<K extends string, T extends FieldType>(
|
|||
action.payload.name,
|
||||
action.payload.update,
|
||||
);
|
||||
case "merge":
|
||||
return merge(prevState, action.payload.new);
|
||||
case "reset":
|
||||
return action.payload.new;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import reduceFilter, { FilterReducerAction } from "./reducer";
|
|||
import { FieldType, FilterElement, IFilter } from "./types";
|
||||
|
||||
export type FilterDispatchFunction<K extends string = string> = <
|
||||
T extends FieldType
|
||||
T extends FieldType,
|
||||
>(
|
||||
value: FilterReducerAction<K, T>,
|
||||
) => void;
|
||||
|
@ -53,7 +53,7 @@ function useFilter<K extends string>(initialFilter: IFilter<K>): UseFilter<K> {
|
|||
payload: {
|
||||
new: parsedInitialFilter,
|
||||
},
|
||||
type: "merge",
|
||||
type: "reset",
|
||||
});
|
||||
|
||||
useEffect(refresh, [initialFilter]);
|
||||
|
|
|
@ -3,6 +3,7 @@ import compact from "lodash/compact";
|
|||
import {
|
||||
FieldType,
|
||||
FilterElement,
|
||||
IFilter,
|
||||
InvalidFilters,
|
||||
ValidationErrorCode,
|
||||
} from "./types";
|
||||
|
@ -10,13 +11,13 @@ import {
|
|||
export const getByName = (nameToCompare: string) => (obj: { name: string }) =>
|
||||
obj.name === nameToCompare;
|
||||
|
||||
export const isAutocompleteFilterFieldValid = function<T extends string>({
|
||||
export const isAutocompleteFilterFieldValid = function <T extends string>({
|
||||
value,
|
||||
}: FilterElement<T>) {
|
||||
return !!compact(value).length;
|
||||
};
|
||||
|
||||
export const isNumberFilterFieldValid = function<T extends string>({
|
||||
export const isNumberFilterFieldValid = function <T extends string>({
|
||||
value,
|
||||
}: FilterElement<T>) {
|
||||
const [min, max] = value;
|
||||
|
@ -28,7 +29,7 @@ export const isNumberFilterFieldValid = function<T extends string>({
|
|||
return true;
|
||||
};
|
||||
|
||||
export const isFilterFieldValid = function<T extends string>(
|
||||
export const isFilterFieldValid = function <T extends string>(
|
||||
filter: FilterElement<T>,
|
||||
) {
|
||||
const { type } = filter;
|
||||
|
@ -47,7 +48,7 @@ export const isFilterFieldValid = function<T extends string>(
|
|||
}
|
||||
};
|
||||
|
||||
export const isFilterValid = function<T extends string>(
|
||||
export const isFilterValid = function <T extends string>(
|
||||
filter: FilterElement<T>,
|
||||
) {
|
||||
const { required, active } = filter;
|
||||
|
@ -59,7 +60,7 @@ export const isFilterValid = function<T extends string>(
|
|||
return isFilterFieldValid(filter);
|
||||
};
|
||||
|
||||
export const extractInvalidFilters = function<T extends string>(
|
||||
export const extractInvalidFilters = function <T extends string>(
|
||||
filtersData: Array<FilterElement<T>>,
|
||||
filtersDataStructure: Array<FilterElement<T>>,
|
||||
): InvalidFilters<T> {
|
||||
|
@ -118,3 +119,19 @@ export const extractInvalidFilters = function<T extends string>(
|
|||
{} as InvalidFilters<T>,
|
||||
);
|
||||
};
|
||||
|
||||
export const getSelectedFiltersAmount = <TFilterKeys extends string = string>(
|
||||
menu: IFilter<TFilterKeys>,
|
||||
data: Array<FilterElement<string>>,
|
||||
) =>
|
||||
menu.reduce((acc, filterElement) => {
|
||||
const dataFilterElement = data.find(
|
||||
({ name }) => name === filterElement.name,
|
||||
);
|
||||
|
||||
if (!dataFilterElement) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc + (dataFilterElement.active ? 1 : 0);
|
||||
}, 0);
|
||||
|
|
61
src/components/FilterPresetsSelect/FilterPresetItem.tsx
Normal file
61
src/components/FilterPresetsSelect/FilterPresetItem.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { Box, Dropdown, List, RemoveIcon, Text } from "@saleor/macaw-ui/next";
|
||||
import React, { MouseEvent } from "react";
|
||||
|
||||
interface FilterPresetItemProps {
|
||||
onSelect: (e: MouseEvent<HTMLLIElement>) => void;
|
||||
onRemove: () => void;
|
||||
isActive?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FilterPresetItem = ({
|
||||
children,
|
||||
onSelect,
|
||||
isActive,
|
||||
onRemove,
|
||||
}: FilterPresetItemProps) => {
|
||||
const [hasHover, setHasHover] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Dropdown.Item>
|
||||
<List.Item
|
||||
paddingLeft={4}
|
||||
paddingRight={11}
|
||||
paddingY={3}
|
||||
gap={6}
|
||||
borderRadius={2}
|
||||
position="relative"
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
onClick={onSelect}
|
||||
onMouseOver={() => setHasHover(true)}
|
||||
onMouseLeave={() => setHasHover(false)}
|
||||
>
|
||||
<Text ellipsis variant={isActive ? "bodyStrong" : "body"}>
|
||||
{children}
|
||||
</Text>
|
||||
{hasHover && (
|
||||
<Box
|
||||
cursor="pointer"
|
||||
zIndex="2"
|
||||
position="absolute"
|
||||
__top="50%"
|
||||
right={4}
|
||||
__transform="translateY(-50%)"
|
||||
onClick={onRemove}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<RemoveIcon
|
||||
color={{
|
||||
default: "iconNeutralSubdued",
|
||||
hover: "iconNeutralPlain",
|
||||
focusVisible: "iconNeutralPlain",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</List.Item>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
};
|
196
src/components/FilterPresetsSelect/FilterPresetsSelect.tsx
Normal file
196
src/components/FilterPresetsSelect/FilterPresetsSelect.tsx
Normal file
|
@ -0,0 +1,196 @@
|
|||
import { commonMessages } from "@dashboard/intl";
|
||||
import { Tooltip } from "@saleor/macaw-ui";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
List,
|
||||
PlusIcon,
|
||||
sprinkles,
|
||||
Text,
|
||||
vars,
|
||||
} from "@saleor/macaw-ui/next";
|
||||
import React, { MouseEvent } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { FilterPresetItem } from "./FilterPresetItem";
|
||||
import { messages } from "./messages";
|
||||
import { getSeparatorWidth } from "./utils";
|
||||
|
||||
interface FilterPresetsSelectProps {
|
||||
activePreset?: number;
|
||||
savedPresets: string[];
|
||||
selectAllLabel: string;
|
||||
isOpen?: boolean;
|
||||
presetsChanged?: boolean;
|
||||
onSave: () => void;
|
||||
onSelectAll: () => void;
|
||||
onRemove: (filterIndex: number) => void;
|
||||
onUpdate: (tabName: string) => void;
|
||||
onSelect: (filterIndex: number) => void;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const FilterPresetsSelect = ({
|
||||
onSelect,
|
||||
onRemove,
|
||||
onSave,
|
||||
savedPresets,
|
||||
activePreset,
|
||||
onSelectAll,
|
||||
selectAllLabel,
|
||||
isOpen,
|
||||
onUpdate,
|
||||
onOpenChange,
|
||||
presetsChanged,
|
||||
}: FilterPresetsSelectProps) => {
|
||||
const intl = useIntl();
|
||||
const showUpdateButton =
|
||||
presetsChanged && savedPresets.length > 0 && activePreset;
|
||||
const showSaveButton = presetsChanged;
|
||||
|
||||
const getLabel = () => {
|
||||
if (!activePreset) {
|
||||
return selectAllLabel;
|
||||
}
|
||||
|
||||
return savedPresets[activePreset - 1];
|
||||
};
|
||||
|
||||
const handleSelectPreset = (e: MouseEvent<HTMLElement>, index: number) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// Prevent run onSelect when click on remove button
|
||||
if (!["LI", "SPAN"].includes(target.tagName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(index);
|
||||
};
|
||||
|
||||
const renderDropdown = () => {
|
||||
if (!savedPresets?.length) {
|
||||
return (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Tooltip title={intl.formatMessage(messages.noPresets)}>
|
||||
<Text variant="title" size="small">
|
||||
{selectAllLabel}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
open={isOpen}
|
||||
onOpenChange={open => {
|
||||
if (onOpenChange) {
|
||||
onOpenChange(open);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dropdown.Trigger>
|
||||
<DropdownButton
|
||||
variant="text"
|
||||
size="medium"
|
||||
data-test-id="show-saved-filters-button"
|
||||
style={{
|
||||
borderColor: isOpen ? vars.colors.border.brandDefault : undefined,
|
||||
}}
|
||||
>
|
||||
<Box __maxWidth="200px">
|
||||
<Text ellipsis variant="title" size="small" display="block">
|
||||
{getLabel()}
|
||||
</Text>
|
||||
</Box>
|
||||
</DropdownButton>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Content align="start">
|
||||
<List
|
||||
__maxWidth={250}
|
||||
__minWidth={175}
|
||||
__maxHeight={400}
|
||||
overflowY="auto"
|
||||
padding={3}
|
||||
borderRadius={3}
|
||||
boxShadow="overlay"
|
||||
borderColor="neutralHighlight"
|
||||
borderStyle="solid"
|
||||
borderWidth={1}
|
||||
width="100%"
|
||||
marginTop={2}
|
||||
backgroundColor="surfaceNeutralPlain"
|
||||
>
|
||||
<Dropdown.Item>
|
||||
<List.Item
|
||||
paddingX={4}
|
||||
paddingY={3}
|
||||
gap={6}
|
||||
borderRadius={3}
|
||||
onClick={onSelectAll}
|
||||
>
|
||||
<Text variant={activePreset === 0 ? "bodyStrong" : "body"}>
|
||||
{selectAllLabel}
|
||||
</Text>
|
||||
</List.Item>
|
||||
</Dropdown.Item>
|
||||
{savedPresets.length > 0 && (
|
||||
<Box
|
||||
height={1}
|
||||
marginY={3}
|
||||
__backgroundColor={vars.colors.border.neutralHighlight}
|
||||
__marginLeft={-4}
|
||||
__width={getSeparatorWidth("4px")}
|
||||
/>
|
||||
)}
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
{savedPresets.map((preset, index) => (
|
||||
<FilterPresetItem
|
||||
isActive={activePreset === index + 1}
|
||||
onSelect={e => handleSelectPreset(e, index + 1)}
|
||||
onRemove={() => {
|
||||
onRemove(index + 1);
|
||||
}}
|
||||
>
|
||||
{preset}
|
||||
</FilterPresetItem>
|
||||
))}
|
||||
</Box>
|
||||
</List>
|
||||
</Dropdown.Content>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center">
|
||||
{renderDropdown()}
|
||||
{showUpdateButton && (
|
||||
<Button
|
||||
className={sprinkles({
|
||||
marginLeft: 6,
|
||||
})}
|
||||
onClick={() => onUpdate(savedPresets[activePreset - 1])}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
>
|
||||
<FormattedMessage {...commonMessages.update} />
|
||||
</Button>
|
||||
)}
|
||||
{showSaveButton && (
|
||||
<Tooltip title={intl.formatMessage(messages.savePreset)}>
|
||||
<Button
|
||||
className={sprinkles({
|
||||
marginLeft: 6,
|
||||
})}
|
||||
icon={<PlusIcon />}
|
||||
onClick={onSave}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
1
src/components/FilterPresetsSelect/index.ts
Normal file
1
src/components/FilterPresetsSelect/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./FilterPresetsSelect";
|
20
src/components/FilterPresetsSelect/messages.ts
Normal file
20
src/components/FilterPresetsSelect/messages.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
unsavedPreset: {
|
||||
defaultMessage: "Unsaved preset",
|
||||
id: "A+g/VP",
|
||||
},
|
||||
filterPreset: {
|
||||
defaultMessage: "Filter preset",
|
||||
id: "3PVGWj",
|
||||
},
|
||||
savePreset: {
|
||||
defaultMessage: "Save search as preset",
|
||||
id: "scTuDZ",
|
||||
},
|
||||
noPresets: {
|
||||
defaultMessage: "Saved search queries will appear here",
|
||||
id: "eW36Jx",
|
||||
},
|
||||
});
|
5
src/components/FilterPresetsSelect/utils.ts
Normal file
5
src/components/FilterPresetsSelect/utils.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const getSeparatorWidth = (paddingValue: string) =>
|
||||
// Separator should cover the whole width of the container,
|
||||
// as the container has padding we have to add it to the width
|
||||
// because container is moved to the left by the padding value
|
||||
`calc(100% + ${paddingValue})`;
|
|
@ -38,7 +38,7 @@ const SaveFilterTabDialog: React.FC<SaveFilterTabDialogProps> = ({
|
|||
const intl = useIntl();
|
||||
const [errors, setErrors] = React.useState(false);
|
||||
const handleErrors = data => {
|
||||
if (data.name.length) {
|
||||
if (data.name.trim().length) {
|
||||
onSubmit(data);
|
||||
setErrors(false);
|
||||
} else {
|
||||
|
@ -50,9 +50,9 @@ const SaveFilterTabDialog: React.FC<SaveFilterTabDialogProps> = ({
|
|||
<Dialog onClose={onClose} open={open} fullWidth maxWidth="sm">
|
||||
<DialogTitle disableTypography>
|
||||
<FormattedMessage
|
||||
id="liLrVs"
|
||||
defaultMessage="Save Custom Search"
|
||||
description="save filter tab, header"
|
||||
id="P9YktI"
|
||||
defaultMessage="Save view preset"
|
||||
description="save preset, header"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<Form initial={initialForm} onSubmit={handleErrors}>
|
||||
|
@ -63,9 +63,9 @@ const SaveFilterTabDialog: React.FC<SaveFilterTabDialogProps> = ({
|
|||
autoFocus
|
||||
fullWidth
|
||||
label={intl.formatMessage({
|
||||
id: "QcIFCs",
|
||||
defaultMessage: "Search Name",
|
||||
description: "save search tab",
|
||||
id: "zhnwl6",
|
||||
defaultMessage: "Preset name",
|
||||
description: "save preset name",
|
||||
})}
|
||||
name={"name" as keyof SaveFilterTabDialogFormData}
|
||||
value={data.name}
|
||||
|
@ -75,7 +75,9 @@ const SaveFilterTabDialog: React.FC<SaveFilterTabDialogProps> = ({
|
|||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<BackButton onClick={onClose} />
|
||||
<BackButton onClick={onClose}>
|
||||
<FormattedMessage {...buttonMessages.cancel} />
|
||||
</BackButton>
|
||||
<ConfirmButton
|
||||
transitionState={confirmButtonState}
|
||||
onClick={submit}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||
import { Sort } from "@dashboard/types";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export type SortByRankUrlQueryParams<T extends string> = Sort<T> & {
|
||||
query?: string;
|
||||
|
@ -24,17 +24,35 @@ export function useSortRedirects<SortField extends string>({
|
|||
resetToDefault,
|
||||
}: UseSortRedirectsOpts<SortField>) {
|
||||
const navigate = useNavigator();
|
||||
const prevAscParams = useRef<boolean | null>(null);
|
||||
|
||||
const hasQuery = !!params.query?.trim();
|
||||
|
||||
const getAscParam = () => {
|
||||
if (hasQuery) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasQuery && prevAscParams.current !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return params.asc;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const sortWithQuery = "rank" as SortField;
|
||||
const sortWithoutQuery =
|
||||
params.sort === "rank" ? defaultSortField : params.sort;
|
||||
|
||||
if (hasQuery || params.sort === "rank") {
|
||||
prevAscParams.current = params.asc;
|
||||
}
|
||||
|
||||
navigate(
|
||||
urlFunc({
|
||||
...params,
|
||||
asc: hasQuery ? false : params.asc,
|
||||
asc: getAscParam(),
|
||||
sort: hasQuery ? sortWithQuery : sortWithoutQuery,
|
||||
}),
|
||||
{ replace: true },
|
||||
|
|
|
@ -131,6 +131,10 @@ export const commonMessages = defineMessages({
|
|||
id: "JqiqNj",
|
||||
defaultMessage: "Something went wrong",
|
||||
},
|
||||
update: {
|
||||
defaultMessage: "Update",
|
||||
id: "BWpuKl",
|
||||
},
|
||||
startDate: {
|
||||
id: "QirE3M",
|
||||
defaultMessage: "Start Date",
|
||||
|
|
|
@ -33,7 +33,8 @@ const props: OrderListPageProps = {
|
|||
},
|
||||
channel: {
|
||||
active: false,
|
||||
value: [
|
||||
value: [],
|
||||
choices: [
|
||||
{
|
||||
label: "Channel PLN",
|
||||
value: "channelId",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { IFilter } from "@dashboard/components/Filter";
|
||||
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
|
||||
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";
|
||||
import { OrderStatusFilter, PaymentChargeStatusEnum } from "@dashboard/graphql";
|
||||
import {
|
||||
commonMessages,
|
||||
|
@ -39,7 +39,7 @@ export interface OrderListFilterOpts {
|
|||
customer: FilterOpts<string>;
|
||||
status: FilterOpts<OrderStatusFilter[]>;
|
||||
paymentStatus: FilterOpts<PaymentChargeStatusEnum[]>;
|
||||
channel?: FilterOpts<MultiAutocompleteChoiceType[]>;
|
||||
channel: FilterOpts<string[]> & { choices: SingleAutocompleteChoiceType[] };
|
||||
clickAndCollect: FilterOpts<boolean>;
|
||||
preorder: FilterOpts<boolean>;
|
||||
giftCard: FilterOpts<OrderFilterGiftCard[]>;
|
||||
|
@ -247,15 +247,15 @@ export function createFilterStructure(
|
|||
),
|
||||
active: opts.metadata.active,
|
||||
},
|
||||
...(opts?.channel?.value.length
|
||||
...(opts?.channel?.choices?.length
|
||||
? [
|
||||
{
|
||||
...createOptionsField(
|
||||
OrderFilterKeys.channel,
|
||||
intl.formatMessage(messages.channel),
|
||||
[],
|
||||
true,
|
||||
opts.channel.value,
|
||||
true,
|
||||
opts.channel.choices,
|
||||
),
|
||||
active: opts.channel.active,
|
||||
},
|
||||
|
|
|
@ -51,7 +51,8 @@ describe("Filtering URL params", () => {
|
|||
},
|
||||
channel: {
|
||||
active: false,
|
||||
value: [
|
||||
value: [],
|
||||
choices: [
|
||||
{
|
||||
label: "Channel PLN",
|
||||
value: "channelId",
|
||||
|
|
|
@ -53,7 +53,8 @@ export function getFilterOpts(
|
|||
channel: channels
|
||||
? {
|
||||
active: params?.channel !== undefined,
|
||||
value: channels,
|
||||
choices: channels,
|
||||
value: params?.channel ?? [],
|
||||
}
|
||||
: null,
|
||||
created: {
|
||||
|
|
|
@ -58,6 +58,7 @@ interface ProductListDatagridProps
|
|||
>;
|
||||
onColumnQueryChange: (query: string) => void;
|
||||
isAttributeLoading?: boolean;
|
||||
hasRowHover?: boolean;
|
||||
}
|
||||
|
||||
export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
|
||||
|
@ -80,6 +81,7 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
|
|||
onColumnQueryChange,
|
||||
activeAttributeSortId,
|
||||
filterDependency,
|
||||
hasRowHover,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const searchProductType = useSearchProductTypes();
|
||||
|
@ -230,6 +232,7 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
|
|||
rowMarkers="none"
|
||||
columnSelect="single"
|
||||
freezeColumns={2}
|
||||
hasRowHover={hasRowHover}
|
||||
onColumnMoved={handleColumnMoved}
|
||||
onColumnResize={handleColumnResize}
|
||||
verticalBorder={col => (col > 1 ? true : false)}
|
||||
|
|
|
@ -36,6 +36,10 @@ const props: ProductListPageProps = {
|
|||
},
|
||||
channels: [],
|
||||
columnQuery: "",
|
||||
currentTab: 0,
|
||||
hasPresetsChanged: false,
|
||||
onTabSave: () => undefined,
|
||||
onTabUpdate: () => undefined,
|
||||
availableInGridAttributes: [],
|
||||
onColumnQueryChange: () => undefined,
|
||||
},
|
||||
|
|
|
@ -4,10 +4,11 @@ import {
|
|||
mapToMenuItemsForProductOverviewActions,
|
||||
useExtensions,
|
||||
} from "@dashboard/apps/hooks/useExtensions";
|
||||
import { FilterBar } from "@dashboard/components/AppLayout/FilterBar";
|
||||
import { ListFilters } from "@dashboard/components/AppLayout/ListFilters";
|
||||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||
import { ButtonWithDropdown } from "@dashboard/components/ButtonWithDropdown";
|
||||
import { getByName } from "@dashboard/components/Filter/utils";
|
||||
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
|
||||
import { ListPageLayout } from "@dashboard/components/Layouts";
|
||||
import LimitReachedAlert from "@dashboard/components/LimitReachedAlert";
|
||||
import { TopNavMenu } from "@dashboard/components/TopNavMenu";
|
||||
|
@ -32,8 +33,8 @@ import {
|
|||
} from "@dashboard/types";
|
||||
import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
|
||||
import { Card } from "@material-ui/core";
|
||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { Box, Button, ChevronRightIcon, Text } from "@saleor/macaw-ui/next";
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { ProductListUrlSortField, productUrl } from "../../urls";
|
||||
|
@ -49,7 +50,10 @@ import {
|
|||
export interface ProductListPageProps
|
||||
extends PageListProps<ProductListColumns>,
|
||||
ListActions,
|
||||
Omit<
|
||||
FilterPageProps<ProductFilterKeys, ProductListFilterOpts>,
|
||||
"onTabDelete"
|
||||
>,
|
||||
FetchMoreProps,
|
||||
SortPage<ProductListUrlSortField>,
|
||||
ChannelProps {
|
||||
|
@ -63,9 +67,12 @@ export interface ProductListPageProps
|
|||
limits: RefreshLimitsQuery["shop"]["limits"];
|
||||
products: RelayToFlat<ProductListQuery["products"]>;
|
||||
selectedProductIds: string[];
|
||||
hasPresetsChanged: boolean;
|
||||
onAdd: () => void;
|
||||
onExport: () => void;
|
||||
onColumnQueryChange: (query: string) => void;
|
||||
onTabUpdate: (tabName: string) => void;
|
||||
onTabDelete: (tabIndex: number) => void;
|
||||
}
|
||||
|
||||
export type ProductListViewType = "datagrid" | "tile";
|
||||
|
@ -95,11 +102,20 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
selectedChannelId,
|
||||
selectedProductIds,
|
||||
activeAttributeSortId,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
onTabSave,
|
||||
onAll,
|
||||
currentTab,
|
||||
tabs,
|
||||
onTabUpdate,
|
||||
hasPresetsChanged,
|
||||
...listProps
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigator();
|
||||
const filterStructure = createFilterStructure(intl, filterOpts);
|
||||
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
|
||||
|
||||
const filterDependency = filterStructure.find(getByName("channel"));
|
||||
|
||||
|
@ -122,7 +138,40 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
|
||||
return (
|
||||
<ListPageLayout>
|
||||
<TopNav withoutBorder title={intl.formatMessage(sectionNames.products)}>
|
||||
<TopNav
|
||||
withoutBorder
|
||||
isAlignToRight={false}
|
||||
title={intl.formatMessage(sectionNames.products)}
|
||||
>
|
||||
<Box
|
||||
__flex={1}
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box display="flex">
|
||||
<Box marginX={6} display="flex" alignItems="center">
|
||||
<ChevronRightIcon />
|
||||
</Box>
|
||||
|
||||
<FilterPresetsSelect
|
||||
presetsChanged={hasPresetsChanged}
|
||||
onSelect={onTabChange}
|
||||
onRemove={onTabDelete}
|
||||
onUpdate={onTabUpdate}
|
||||
savedPresets={tabs}
|
||||
activePreset={currentTab}
|
||||
onSelectAll={onAll}
|
||||
onSave={onTabSave}
|
||||
isOpen={isFilterPresetOpen}
|
||||
onOpenChange={setFilterPresetOpen}
|
||||
selectAllLabel={intl.formatMessage({
|
||||
id: "tCLTCb",
|
||||
defaultMessage: "All products",
|
||||
description: "tab name",
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={5}>
|
||||
{hasLimits(limits, "productVariants") && (
|
||||
<Text variant="caption">
|
||||
|
@ -176,6 +225,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</TopNav>
|
||||
{limitReached && (
|
||||
<LimitReachedAlert
|
||||
|
@ -199,7 +249,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
alignItems="stretch"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<FilterBar
|
||||
<ListFilters
|
||||
currencySymbol={currencySymbol}
|
||||
initialSearch={initialSearch}
|
||||
onFilterChange={onFilterChange}
|
||||
|
@ -221,6 +271,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
{isDatagridView ? (
|
||||
<ProductListDatagrid
|
||||
{...listProps}
|
||||
hasRowHover={!isFilterPresetOpen}
|
||||
filterDependency={filterDependency}
|
||||
activeAttributeSortId={activeAttributeSortId}
|
||||
columnQuery={columnQuery}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { IFilter } from "@dashboard/components/Filter";
|
|||
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";
|
||||
import { AttributeInputTypeEnum, StockAvailability } from "@dashboard/graphql";
|
||||
import { commonMessages, sectionNames } from "@dashboard/intl";
|
||||
import { parseBoolean } from "@dashboard/misc";
|
||||
import { ProductListUrlFiltersAsDictWithMultipleValues } from "@dashboard/products/urls";
|
||||
import {
|
||||
AutocompleteFilterOpts,
|
||||
|
@ -32,7 +33,8 @@ export const ProductFilterKeys = {
|
|||
channel: "channel",
|
||||
productKind: "productKind",
|
||||
} as const;
|
||||
export type ProductFilterKeys = typeof ProductFilterKeys[keyof typeof ProductFilterKeys];
|
||||
export type ProductFilterKeys =
|
||||
(typeof ProductFilterKeys)[keyof typeof ProductFilterKeys];
|
||||
|
||||
export type AttributeFilterOpts = FilterOpts<string[]> & {
|
||||
id: string;
|
||||
|
@ -105,9 +107,9 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
const filterByType = (type: AttributeInputTypeEnum) => (
|
||||
attribute: AttributeFilterOpts,
|
||||
) => attribute.inputType === type;
|
||||
const filterByType =
|
||||
(type: AttributeInputTypeEnum) => (attribute: AttributeFilterOpts) =>
|
||||
attribute.inputType === type;
|
||||
|
||||
export function createFilterStructure(
|
||||
intl: IntlShape,
|
||||
|
@ -254,7 +256,7 @@ export function createFilterStructure(
|
|||
attr.slug,
|
||||
attr.name,
|
||||
Array.isArray(attr.value)
|
||||
? undefined
|
||||
? parseBoolean(attr.value[0], undefined)
|
||||
: (attr.value as unknown) === "true",
|
||||
{
|
||||
positive: intl.formatMessage(commonMessages.yes),
|
||||
|
|
|
@ -76,6 +76,7 @@ export interface ProductListUrlQueryParams
|
|||
Pagination,
|
||||
ActiveTab {
|
||||
attributeId?: string;
|
||||
presestesChanged?: string;
|
||||
}
|
||||
export const productListUrl = (params?: ProductListUrlQueryParams): string =>
|
||||
productListPath + "?" + stringifyQs(params);
|
||||
|
|
|
@ -58,12 +58,14 @@ import useCategorySearch from "@dashboard/searches/useCategorySearch";
|
|||
import useCollectionSearch from "@dashboard/searches/useCollectionSearch";
|
||||
import useProductTypeSearch from "@dashboard/searches/useProductTypeSearch";
|
||||
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 { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps";
|
||||
import { getSortUrlVariables } from "@dashboard/utils/sort";
|
||||
import { DialogContentText } from "@material-ui/core";
|
||||
import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
|
||||
import { stringify } from "qs";
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -71,16 +73,20 @@ import { useSortRedirects } from "../../../hooks/useSortRedirects";
|
|||
import ProductListPage from "../../components/ProductListPage";
|
||||
import {
|
||||
deleteFilterTab,
|
||||
getActiveFilters,
|
||||
getFilterOpts,
|
||||
getFilterQueryParam,
|
||||
getFiltersCurrentTab,
|
||||
getFilterTabs,
|
||||
getFilterVariables,
|
||||
saveFilterTab,
|
||||
updateFilterTab,
|
||||
} from "./filters";
|
||||
import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
|
||||
import { getAvailableProductKinds, getProductKindOpts } from "./utils";
|
||||
import {
|
||||
getActiveTabIndexAfterTabDelete,
|
||||
getAvailableProductKinds,
|
||||
getNextUniqueTabName,
|
||||
getProductKindOpts,
|
||||
} from "./utils";
|
||||
|
||||
interface ProductListProps {
|
||||
params: ProductListUrlQueryParams;
|
||||
|
@ -91,6 +97,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
const notify = useNotifier();
|
||||
const { queue } = useBackgroundTask();
|
||||
|
||||
const [tabIndexToDelete, setTabIndexToDelete] = useState<number | null>(null);
|
||||
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
||||
[],
|
||||
);
|
||||
|
@ -190,7 +197,8 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
|
||||
const tabs = getFilterTabs();
|
||||
|
||||
const currentTab = getFiltersCurrentTab(params, tabs);
|
||||
const currentTab =
|
||||
params.activeTab !== undefined ? parseInt(params.activeTab, 10) : undefined;
|
||||
|
||||
const countAllProducts = useProductCountQuery({
|
||||
skip: params.action !== "export",
|
||||
|
@ -227,29 +235,59 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
getFilterQueryParam,
|
||||
navigate,
|
||||
params,
|
||||
keepActiveTab: true,
|
||||
});
|
||||
|
||||
const handleTabChange = (tab: number) => {
|
||||
reset();
|
||||
navigate(
|
||||
productListUrl({
|
||||
activeTab: tab.toString(),
|
||||
...getFilterTabs()[tab - 1].data,
|
||||
}),
|
||||
);
|
||||
const qs = new URLSearchParams(getFilterTabs()[tab - 1]?.data ?? "");
|
||||
qs.append("activeTab", tab.toString());
|
||||
|
||||
navigate(productListUrl() + qs.toString());
|
||||
};
|
||||
|
||||
const handleFilterTabDelete = () => {
|
||||
deleteFilterTab(currentTab);
|
||||
deleteFilterTab(tabIndexToDelete);
|
||||
reset();
|
||||
|
||||
// When deleting the current tab, navigate to the All products
|
||||
if (tabIndexToDelete === currentTab) {
|
||||
navigate(productListUrl());
|
||||
} 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(
|
||||
currentTab,
|
||||
tabIndexToDelete,
|
||||
);
|
||||
navigate(productListUrl() + stringify(currentParams));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterTabSave = (data: SaveFilterTabDialogFormData) => {
|
||||
saveFilterTab(data.name, getActiveFilters(params));
|
||||
const { paresedQs } = prepareQs(location.search);
|
||||
|
||||
saveFilterTab(
|
||||
getNextUniqueTabName(
|
||||
data.name,
|
||||
tabs.map(tab => tab.name),
|
||||
),
|
||||
stringify(paresedQs),
|
||||
);
|
||||
handleTabChange(tabs.length + 1);
|
||||
};
|
||||
|
||||
const hanleFilterTabUpdate = (tabName: string) => {
|
||||
const { paresedQs, activeTab } = prepareQs(location.search);
|
||||
|
||||
updateFilterTab(tabName, stringify(paresedQs));
|
||||
paresedQs.activeTab = activeTab;
|
||||
|
||||
navigate(productListUrl() + stringify(paresedQs));
|
||||
};
|
||||
|
||||
const handleSort = (field: ProductListUrlSortField, attributeId?: string) =>
|
||||
navigate(
|
||||
productListUrl({
|
||||
|
@ -355,6 +393,17 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
channelOpts,
|
||||
);
|
||||
|
||||
const hasPresetsChanged = () => {
|
||||
const activeTab = tabs[currentTab - 1];
|
||||
const { paresedQs } = prepareQs(location.search);
|
||||
|
||||
return (
|
||||
activeTab?.data !== stringify(paresedQs) &&
|
||||
location.search !== "" &&
|
||||
stringify(paresedQs) !== ""
|
||||
);
|
||||
};
|
||||
|
||||
const paginationValues = usePaginator({
|
||||
pageInfo: data?.products?.pageInfo,
|
||||
paginationState,
|
||||
|
@ -416,10 +465,15 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
onFilterChange={changeFilters}
|
||||
onFilterAttributeFocus={setFocusedAttribute}
|
||||
onTabSave={() => openModal("save-search")}
|
||||
onTabDelete={() => openModal("delete-search")}
|
||||
onTabUpdate={hanleFilterTabUpdate}
|
||||
onTabDelete={(tabIndex: number) => {
|
||||
setTabIndexToDelete(tabIndex);
|
||||
openModal("delete-search");
|
||||
}}
|
||||
onTabChange={handleTabChange}
|
||||
hasPresetsChanged={hasPresetsChanged()}
|
||||
initialSearch={params.query || ""}
|
||||
tabs={getFilterTabs().map(tab => tab.name)}
|
||||
tabs={tabs.map(tab => tab.name)}
|
||||
onExport={() => openModal("export")}
|
||||
selectedChannelId={selectedChannel?.id}
|
||||
columnQuery={availableInGridAttributesOpts.query}
|
||||
|
@ -498,7 +552,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
confirmButtonState="default"
|
||||
onClose={closeModal}
|
||||
onSubmit={handleFilterTabDelete}
|
||||
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
|
||||
tabName={tabs[tabIndexToDelete - 1]?.name ?? "..."}
|
||||
/>
|
||||
<ProductTypePickerDialog
|
||||
confirmButtonState="success"
|
||||
|
|
|
@ -56,7 +56,7 @@ import {
|
|||
ProductListUrlQueryParams,
|
||||
} from "../../urls";
|
||||
import { getProductGiftCardFilterParam } from "./utils";
|
||||
export const PRODUCT_FILTERS_KEY = "productFilters";
|
||||
export const PRODUCT_FILTERS_KEY = "productPresets";
|
||||
|
||||
function getAttributeFilterParamType(inputType: AttributeInputTypeEnum) {
|
||||
switch (inputType) {
|
||||
|
@ -468,14 +468,12 @@ export const {
|
|||
deleteFilterTab,
|
||||
getFilterTabs,
|
||||
saveFilterTab,
|
||||
} = createFilterTabUtils<ProductListUrlFilters>(PRODUCT_FILTERS_KEY);
|
||||
updateFilterTab,
|
||||
} = createFilterTabUtils<string>(PRODUCT_FILTERS_KEY);
|
||||
|
||||
export const {
|
||||
areFiltersApplied,
|
||||
getActiveFilters,
|
||||
getFiltersCurrentTab,
|
||||
} = createFilterUtils<ProductListUrlQueryParams, ProductListUrlFilters>({
|
||||
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
|
||||
createFilterUtils<ProductListUrlQueryParams, ProductListUrlFilters>({
|
||||
...ProductListUrlFiltersEnum,
|
||||
...ProductListUrlFiltersWithMultipleValues,
|
||||
...ProductListUrlFiltersAsDictWithMultipleValues,
|
||||
});
|
||||
});
|
||||
|
|
49
src/products/views/ProductList/utils.test.ts
Normal file
49
src/products/views/ProductList/utils.test.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { getActiveTabIndexAfterTabDelete, getNextUniqueTabName } from "./utils";
|
||||
|
||||
describe("ProductList utils", () => {
|
||||
describe("getNextUniqueTabName", () => {
|
||||
it("should return unique name", () => {
|
||||
// Arrange
|
||||
const name = "test";
|
||||
const availableNames = ["test", "test 1", "test 2"];
|
||||
|
||||
// Act
|
||||
const result = getNextUniqueTabName(name, availableNames);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual("test 3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getActiveTabIndexAfterDelete", () => {
|
||||
it("should return active tab index descread by one when delete index before current tab index", () => {
|
||||
// Arrange
|
||||
const currentTab = 5;
|
||||
const tabIndexToDelete = 1;
|
||||
|
||||
// Act
|
||||
const result = getActiveTabIndexAfterTabDelete(
|
||||
currentTab,
|
||||
tabIndexToDelete,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual("4");
|
||||
});
|
||||
|
||||
it("should return active tab same active tab index when delete tab index higher than current tab", () => {
|
||||
// Arrange
|
||||
const currentTab = 5;
|
||||
const tabIndexToDelete = 7;
|
||||
|
||||
// Act
|
||||
const result = getActiveTabIndexAfterTabDelete(
|
||||
currentTab,
|
||||
tabIndexToDelete,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual("5");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -44,3 +44,21 @@ export const getProductGiftCardFilterParam = (productKind?: string) => {
|
|||
|
||||
return productKind === ProductTypeKindEnum.GIFT_CARD;
|
||||
};
|
||||
|
||||
export const getNextUniqueTabName = (name: string, avialabeNames: string[]) => {
|
||||
let uniqueName = name;
|
||||
let i = 1;
|
||||
|
||||
while (avialabeNames.includes(uniqueName)) {
|
||||
uniqueName = `${name} ${i}`;
|
||||
i++;
|
||||
}
|
||||
|
||||
return uniqueName;
|
||||
};
|
||||
|
||||
export const getActiveTabIndexAfterTabDelete = (
|
||||
currentTab: number,
|
||||
tabIndexToDelete: number,
|
||||
): string =>
|
||||
tabIndexToDelete < currentTab ? `${currentTab - 1}` : `${currentTab}`;
|
||||
|
|
|
@ -83,9 +83,8 @@ export interface ListActionsWithoutToolbar {
|
|||
isChecked: (id: string) => boolean;
|
||||
selected: number;
|
||||
}
|
||||
export type TabListActions<
|
||||
TToolbars extends string
|
||||
> = ListActionsWithoutToolbar &
|
||||
export type TabListActions<TToolbars extends string> =
|
||||
ListActionsWithoutToolbar &
|
||||
Record<TToolbars, React.ReactNode | React.ReactNodeArray>;
|
||||
export interface ListActions extends ListActionsWithoutToolbar {
|
||||
toolbar: React.ReactNode | React.ReactNodeArray;
|
||||
|
@ -129,7 +128,7 @@ export interface ChannelProps {
|
|||
|
||||
export interface PartialMutationProviderOutput<
|
||||
TData extends {} = {},
|
||||
TVariables extends {} = {}
|
||||
TVariables extends {} = {},
|
||||
> {
|
||||
opts: MutationResult<TData> & MutationResultAdditionalProps;
|
||||
mutate: (variables: TVariables) => Promise<FetchResult<TData>>;
|
||||
|
|
|
@ -13,7 +13,7 @@ function createFilterUtils<
|
|||
>(filters: {}) {
|
||||
function getActiveFilters(params: TQueryParams): TFilters {
|
||||
return Object.keys(params)
|
||||
.filter(key => Object.keys(filters).includes(key))
|
||||
.filter(key => Object.values(filters).includes(key))
|
||||
.reduce((acc, key) => {
|
||||
acc[key] = params[key];
|
||||
return acc;
|
||||
|
|
16
src/utils/filters/qs.test.ts
Normal file
16
src/utils/filters/qs.test.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { prepareQs } from "./qs";
|
||||
|
||||
describe("Filters: preapreQS", () => {
|
||||
it("should remove activeTab, action, sort, asc from query string", () => {
|
||||
const qs = prepareQs(
|
||||
"?category=1&activeTab=1&channel=usa&action=save-search&sort=name&asc=4",
|
||||
);
|
||||
expect(qs).toEqual({
|
||||
activeTab: "1",
|
||||
paresedQs: {
|
||||
category: "1",
|
||||
channel: "usa",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
19
src/utils/filters/qs.ts
Normal file
19
src/utils/filters/qs.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { parse } from "qs";
|
||||
|
||||
const paramsToRemove = ["activeTab", "action", "sort", "asc"];
|
||||
|
||||
export const prepareQs = (searchQuery: string) => {
|
||||
const paresedQs = parse(
|
||||
searchQuery.startsWith("?") ? searchQuery.slice(1) : searchQuery,
|
||||
);
|
||||
const activeTab = paresedQs.activeTab;
|
||||
|
||||
paramsToRemove.forEach(param => {
|
||||
delete paresedQs[param];
|
||||
});
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
paresedQs,
|
||||
};
|
||||
};
|
|
@ -31,6 +31,26 @@ function saveFilterTab<TUrlFilters>(
|
|||
);
|
||||
}
|
||||
|
||||
function updateFilterTab<TUrlFilters>(
|
||||
tabName: string,
|
||||
data: TUrlFilters,
|
||||
key: string,
|
||||
) {
|
||||
const userFilters = getFilterTabs<TUrlFilters>(key);
|
||||
const updatedFilters = userFilters.map(tab => {
|
||||
if (tab.name === tabName) {
|
||||
return {
|
||||
data,
|
||||
name: tabName,
|
||||
};
|
||||
}
|
||||
|
||||
return tab;
|
||||
});
|
||||
|
||||
localStorage.setItem(key, JSON.stringify([...updatedFilters]));
|
||||
}
|
||||
|
||||
function deleteFilterTab(id: number, key: string) {
|
||||
const userFilters = getFilterTabs(key);
|
||||
|
||||
|
@ -46,6 +66,8 @@ function createFilterTabUtils<TUrlFilters>(key: string) {
|
|||
getFilterTabs: () => getFilterTabs<TUrlFilters>(key),
|
||||
saveFilterTab: (name: string, data: TUrlFilters) =>
|
||||
saveFilterTab<TUrlFilters>(name, data, key),
|
||||
updateFilterTab: (tabName: string, data: TUrlFilters) =>
|
||||
updateFilterTab<TUrlFilters>(tabName, data, key),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,10 @@ import { ActiveTab, Pagination, Search, Sort } from "@dashboard/types";
|
|||
|
||||
import { GetFilterQueryParam, getFilterQueryParams } from "../filters";
|
||||
|
||||
type RequiredParams = ActiveTab & Search & Sort<any> & Pagination;
|
||||
type RequiredParams = ActiveTab &
|
||||
Search &
|
||||
Sort<any> &
|
||||
Pagination & { presestesChanged?: string };
|
||||
type CreateUrl = (params: RequiredParams) => string;
|
||||
type CreateFilterHandlers<TFilterKeys extends string> = [
|
||||
(filter: IFilter<TFilterKeys>) => void,
|
||||
|
@ -21,19 +24,40 @@ function createFilterHandlers<
|
|||
createUrl: CreateUrl;
|
||||
params: RequiredParams;
|
||||
cleanupFn?: () => void;
|
||||
keepActiveTab?: boolean;
|
||||
}): CreateFilterHandlers<TFilterKeys> {
|
||||
const { getFilterQueryParam, navigate, createUrl, params, cleanupFn } = opts;
|
||||
const {
|
||||
getFilterQueryParam,
|
||||
navigate,
|
||||
createUrl,
|
||||
params,
|
||||
cleanupFn,
|
||||
keepActiveTab,
|
||||
} = opts;
|
||||
|
||||
const getActiveTabValue = (removeActiveTab: boolean) => {
|
||||
if (!keepActiveTab || removeActiveTab) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return params.activeTab;
|
||||
};
|
||||
|
||||
const changeFilters = (filters: IFilter<TFilterKeys>) => {
|
||||
if (!!cleanupFn) {
|
||||
cleanupFn();
|
||||
}
|
||||
|
||||
const filtersQueryParams = getFilterQueryParams(
|
||||
filters,
|
||||
getFilterQueryParam,
|
||||
);
|
||||
navigate(
|
||||
createUrl({
|
||||
...params,
|
||||
...getFilterQueryParams(filters, getFilterQueryParam),
|
||||
activeTab: undefined,
|
||||
...filtersQueryParams,
|
||||
activeTab: getActiveTabValue(
|
||||
checkIfParamsEmpty(filtersQueryParams) && !params.query?.length,
|
||||
),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
@ -55,14 +79,17 @@ function createFilterHandlers<
|
|||
if (!!cleanupFn) {
|
||||
cleanupFn();
|
||||
}
|
||||
const trimmedQuery = query?.trim() ?? "";
|
||||
|
||||
navigate(
|
||||
createUrl({
|
||||
...params,
|
||||
after: undefined,
|
||||
before: undefined,
|
||||
activeTab: undefined,
|
||||
query: query?.trim(),
|
||||
activeTab: getActiveTabValue(
|
||||
checkIfParamsEmpty(params) && trimmedQuery === "",
|
||||
),
|
||||
query: trimmedQuery !== "" ? trimmedQuery : undefined,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
@ -70,4 +97,12 @@ function createFilterHandlers<
|
|||
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);
|
||||
}
|
||||
|
||||
export default createFilterHandlers;
|
||||
|
|
Loading…
Reference in a new issue