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/": {
|
"3DGvA/": {
|
||||||
"string": "Remember this will also unpin all products assigned to this category, making them unavailable in storefront."
|
"string": "Remember this will also unpin all products assigned to this category, making them unavailable in storefront."
|
||||||
},
|
},
|
||||||
|
"3PVGWj": {
|
||||||
|
"string": "Filter preset"
|
||||||
|
},
|
||||||
"3Sz1/t": {
|
"3Sz1/t": {
|
||||||
"context": "dialog header",
|
"context": "dialog header",
|
||||||
"string": "Delete Pages"
|
"string": "Delete Pages"
|
||||||
|
@ -1283,10 +1286,6 @@
|
||||||
"7NFfmz": {
|
"7NFfmz": {
|
||||||
"string": "Products"
|
"string": "Products"
|
||||||
},
|
},
|
||||||
"7NfoiJ": {
|
|
||||||
"context": "custom search delete, dialog header",
|
|
||||||
"string": "Delete Search"
|
|
||||||
},
|
|
||||||
"7Oorx5": {
|
"7Oorx5": {
|
||||||
"context": "navigator section header",
|
"context": "navigator section header",
|
||||||
"string": "Search in Catalog"
|
"string": "Search in Catalog"
|
||||||
|
@ -1595,6 +1594,9 @@
|
||||||
"context": "used staff users counter",
|
"context": "used staff users counter",
|
||||||
"string": "{count}/{max} members"
|
"string": "{count}/{max} members"
|
||||||
},
|
},
|
||||||
|
"A+g/VP": {
|
||||||
|
"string": "Unsaved preset"
|
||||||
|
},
|
||||||
"A0Wlg7": {
|
"A0Wlg7": {
|
||||||
"context": "product discount removed title",
|
"context": "product discount removed title",
|
||||||
"string": "{productName} discount was removed by"
|
"string": "{productName} discount was removed by"
|
||||||
|
@ -1786,6 +1788,9 @@
|
||||||
"BUKMzM": {
|
"BUKMzM": {
|
||||||
"string": "Variant removed"
|
"string": "Variant removed"
|
||||||
},
|
},
|
||||||
|
"BWpuKl": {
|
||||||
|
"string": "Update"
|
||||||
|
},
|
||||||
"BXMSl4": {
|
"BXMSl4": {
|
||||||
"context": "currency channel",
|
"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."
|
"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": {
|
"P7PLVj": {
|
||||||
"string": "Date"
|
"string": "Date"
|
||||||
},
|
},
|
||||||
|
"P9YktI": {
|
||||||
|
"context": "save preset, header",
|
||||||
|
"string": "Save view preset"
|
||||||
|
},
|
||||||
"PAqicb": {
|
"PAqicb": {
|
||||||
"context": "button",
|
"context": "button",
|
||||||
"string": "Cancel order"
|
"string": "Cancel order"
|
||||||
|
@ -3923,10 +3932,6 @@
|
||||||
"context": "dialog header",
|
"context": "dialog header",
|
||||||
"string": "Delete Channel"
|
"string": "Delete Channel"
|
||||||
},
|
},
|
||||||
"QcIFCs": {
|
|
||||||
"context": "save search tab",
|
|
||||||
"string": "Search Name"
|
|
||||||
},
|
|
||||||
"QclvqG": {
|
"QclvqG": {
|
||||||
"context": "input label",
|
"context": "input label",
|
||||||
"string": "Checkout line limit"
|
"string": "Checkout line limit"
|
||||||
|
@ -4378,6 +4383,9 @@
|
||||||
"U2mOqA": {
|
"U2mOqA": {
|
||||||
"string": "No vouchers found"
|
"string": "No vouchers found"
|
||||||
},
|
},
|
||||||
|
"U5CH0u": {
|
||||||
|
"string": "Are you sure you want to delete {name} preset?"
|
||||||
|
},
|
||||||
"U5Da30": {
|
"U5Da30": {
|
||||||
"string": "Warehouses"
|
"string": "Warehouses"
|
||||||
},
|
},
|
||||||
|
@ -4442,9 +4450,6 @@
|
||||||
"context": "filters error messages unknown error",
|
"context": "filters error messages unknown error",
|
||||||
"string": "Unknown error occurred"
|
"string": "Unknown error occurred"
|
||||||
},
|
},
|
||||||
"UaYJJ8": {
|
|
||||||
"string": "Are you sure you want to delete {name} search tab?"
|
|
||||||
},
|
|
||||||
"Uf3oHA": {
|
"Uf3oHA": {
|
||||||
"context": "add new menu item",
|
"context": "add new menu item",
|
||||||
"string": "Create new item"
|
"string": "Create new item"
|
||||||
|
@ -5838,6 +5843,9 @@
|
||||||
"eUjFjW": {
|
"eUjFjW": {
|
||||||
"string": "Permission group created"
|
"string": "Permission group created"
|
||||||
},
|
},
|
||||||
|
"eW36Jx": {
|
||||||
|
"string": "Saved search queries will appear here"
|
||||||
|
},
|
||||||
"eWV760": {
|
"eWV760": {
|
||||||
"string": "Attribute with this slug already exists"
|
"string": "Attribute with this slug already exists"
|
||||||
},
|
},
|
||||||
|
@ -6751,10 +6759,6 @@
|
||||||
"context": "export items as csv file",
|
"context": "export items as csv file",
|
||||||
"string": "Plain CSV file"
|
"string": "Plain CSV file"
|
||||||
},
|
},
|
||||||
"liLrVs": {
|
|
||||||
"context": "save filter tab, header",
|
|
||||||
"string": "Save Custom Search"
|
|
||||||
},
|
|
||||||
"lit2zF": {
|
"lit2zF": {
|
||||||
"string": "Search Discounts"
|
"string": "Search Discounts"
|
||||||
},
|
},
|
||||||
|
@ -7638,6 +7642,9 @@
|
||||||
"context": "button",
|
"context": "button",
|
||||||
"string": "Assign product"
|
"string": "Assign product"
|
||||||
},
|
},
|
||||||
|
"scTuDZ": {
|
||||||
|
"string": "Save search as preset"
|
||||||
|
},
|
||||||
"sdA14A": {
|
"sdA14A": {
|
||||||
"context": "Status label when object is published in a channel",
|
"context": "Status label when object is published in a channel",
|
||||||
"string": "Published"
|
"string": "Published"
|
||||||
|
@ -7718,6 +7725,10 @@
|
||||||
"context": "webhook input help text",
|
"context": "webhook input help text",
|
||||||
"string": "secret key is used to create a hash signature with each payload. *optional field"
|
"string": "secret key is used to create a hash signature with each payload. *optional field"
|
||||||
},
|
},
|
||||||
|
"tCLTCb": {
|
||||||
|
"context": "tab name",
|
||||||
|
"string": "All products"
|
||||||
|
},
|
||||||
"tEdFmZ": {
|
"tEdFmZ": {
|
||||||
"context": "notification title",
|
"context": "notification title",
|
||||||
"string": "System update required"
|
"string": "System update required"
|
||||||
|
@ -8351,6 +8362,10 @@
|
||||||
"xxQxLE": {
|
"xxQxLE": {
|
||||||
"string": "Email Address"
|
"string": "Email Address"
|
||||||
},
|
},
|
||||||
|
"xy66ru": {
|
||||||
|
"context": "custom preset delete, dialog header",
|
||||||
|
"string": "Delete preset"
|
||||||
|
},
|
||||||
"y/UWBR": {
|
"y/UWBR": {
|
||||||
"string": "There is no address to show for this customer"
|
"string": "There is no address to show for this customer"
|
||||||
},
|
},
|
||||||
|
@ -8572,6 +8587,10 @@
|
||||||
"context": "attribute list",
|
"context": "attribute list",
|
||||||
"string": "Attribute {name}"
|
"string": "Attribute {name}"
|
||||||
},
|
},
|
||||||
|
"zhnwl6": {
|
||||||
|
"context": "save preset name",
|
||||||
|
"string": "Preset name"
|
||||||
|
},
|
||||||
"zjHH6b": {
|
"zjHH6b": {
|
||||||
"context": "sale start date",
|
"context": "sale start date",
|
||||||
"string": "Started"
|
"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/lab": "^4.0.0-alpha.61",
|
||||||
"@material-ui/styles": "^4.11.4",
|
"@material-ui/styles": "^4.11.4",
|
||||||
"@reach/auto-id": "^0.16.0",
|
"@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",
|
"@saleor/sdk": "^0.4.6",
|
||||||
"@sentry/react": "^6.0.0",
|
"@sentry/react": "^6.0.0",
|
||||||
"@types/faker": "^5.1.6",
|
"@types/faker": "^5.1.6",
|
||||||
|
|
|
@ -14,6 +14,7 @@ export interface ActionDialogProps extends DialogProps {
|
||||||
maxWidth?: Size | false;
|
maxWidth?: Size | false;
|
||||||
title: string;
|
title: string;
|
||||||
variant?: ActionDialogVariant;
|
variant?: ActionDialogVariant;
|
||||||
|
backButtonText?: string;
|
||||||
onConfirm();
|
onConfirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ interface DialogButtonsProps {
|
||||||
variant?: ActionDialogVariant;
|
variant?: ActionDialogVariant;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
showBackButton?: boolean;
|
showBackButton?: boolean;
|
||||||
|
backButtonText?: string;
|
||||||
onConfirm();
|
onConfirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ const DialogButtons: React.FC<DialogButtonsProps> = props => {
|
||||||
onClose,
|
onClose,
|
||||||
children,
|
children,
|
||||||
showBackButton = true,
|
showBackButton = true,
|
||||||
|
backButtonText,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
@ -36,7 +38,9 @@ const DialogButtons: React.FC<DialogButtonsProps> = props => {
|
||||||
return (
|
return (
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
{children}
|
{children}
|
||||||
{showBackButton && <BackButton onClick={onClose} />}
|
{showBackButton && (
|
||||||
|
<BackButton onClick={onClose}>{backButtonText}</BackButton>
|
||||||
|
)}
|
||||||
{variant !== "info" && (
|
{variant !== "info" && (
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -98,7 +98,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
|
||||||
borderColor="neutralPlain"
|
borderColor="neutralPlain"
|
||||||
__maxWidth={contentMaxWidth}
|
__maxWidth={contentMaxWidth}
|
||||||
margin="auto"
|
margin="auto"
|
||||||
zIndex="3"
|
zIndex="2"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</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 { Box } from "@saleor/macaw-ui/next";
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
import { Filter } from "./Filter";
|
import { FiltersSelect } from "./components/FiltersSelect";
|
||||||
import SearchInput from "./SearchInput";
|
import SearchInput from "./components/SearchInput";
|
||||||
|
|
||||||
export interface FilterBarProps<TKeys extends string = string>
|
export interface ListFiltersProps<TKeys extends string = string>
|
||||||
extends FilterProps<TKeys>,
|
extends FilterProps<TKeys>,
|
||||||
SearchPageProps {
|
SearchPageProps {
|
||||||
searchPlaceholder: string;
|
searchPlaceholder: string;
|
||||||
|
@ -15,7 +15,7 @@ export interface FilterBarProps<TKeys extends string = string>
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilterBar: React.FC<FilterBarProps> = ({
|
export const ListFilters = ({
|
||||||
currencySymbol,
|
currencySymbol,
|
||||||
filterStructure,
|
filterStructure,
|
||||||
initialSearch,
|
initialSearch,
|
||||||
|
@ -25,11 +25,11 @@ export const FilterBar: React.FC<FilterBarProps> = ({
|
||||||
onFilterAttributeFocus,
|
onFilterAttributeFocus,
|
||||||
errorMessages,
|
errorMessages,
|
||||||
actions,
|
actions,
|
||||||
}: FilterBarProps) => (
|
}: ListFiltersProps) => (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
display="grid"
|
display="grid"
|
||||||
__gridTemplateColumns="1fr 1fr"
|
gridTemplateColumns={2}
|
||||||
gap={7}
|
gap={7}
|
||||||
paddingBottom={5}
|
paddingBottom={5}
|
||||||
paddingX={9}
|
paddingX={9}
|
||||||
|
@ -38,13 +38,14 @@ export const FilterBar: React.FC<FilterBarProps> = ({
|
||||||
borderBottomWidth={1}
|
borderBottomWidth={1}
|
||||||
>
|
>
|
||||||
<Box display="flex" alignItems="center" gap={7}>
|
<Box display="flex" alignItems="center" gap={7}>
|
||||||
<Filter
|
<FiltersSelect
|
||||||
errorMessages={errorMessages}
|
errorMessages={errorMessages}
|
||||||
menu={filterStructure}
|
menu={filterStructure}
|
||||||
currencySymbol={currencySymbol}
|
currencySymbol={currencySymbol}
|
||||||
onFilterAdd={onFilterChange}
|
onFilterAdd={onFilterChange}
|
||||||
onFilterAttributeFocus={onFilterAttributeFocus}
|
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box __width="320px">
|
<Box __width="320px">
|
||||||
<SearchInput
|
<SearchInput
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
|
@ -59,4 +60,4 @@ export const FilterBar: React.FC<FilterBarProps> = ({
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
FilterBar.displayName = "FilterBar";
|
ListFilters.displayName = "FilterBar";
|
|
@ -8,11 +8,11 @@ import {
|
||||||
import useFilter from "@dashboard/components/Filter/useFilter";
|
import useFilter from "@dashboard/components/Filter/useFilter";
|
||||||
import { extractInvalidFilters } from "@dashboard/components/Filter/utils";
|
import { extractInvalidFilters } from "@dashboard/components/Filter/utils";
|
||||||
import { ClickAwayListener, Grow, Popper } from "@material-ui/core";
|
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 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> {
|
export interface FilterProps<TFilterKeys extends string = string> {
|
||||||
currencySymbol?: string;
|
currencySymbol?: string;
|
||||||
|
@ -22,7 +22,7 @@ export interface FilterProps<TFilterKeys extends string = string> {
|
||||||
onFilterAttributeFocus?: (id?: string) => void;
|
onFilterAttributeFocus?: (id?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Filter = ({
|
export const FiltersSelect = ({
|
||||||
currencySymbol,
|
currencySymbol,
|
||||||
menu,
|
menu,
|
||||||
onFilterAdd,
|
onFilterAdd,
|
||||||
|
@ -68,11 +68,19 @@ export const Filter = ({
|
||||||
mouseEvent="onMouseUp"
|
mouseEvent="onMouseUp"
|
||||||
>
|
>
|
||||||
<div ref={anchor}>
|
<div ref={anchor}>
|
||||||
<FilterButton
|
<DropdownButton
|
||||||
isFilterActive={isFilterActive}
|
data-test-id="show-filters-button"
|
||||||
onClick={() => setFilterMenuOpened(!isFilterMenuOpened)}
|
onClick={() => setFilterMenuOpened(!isFilterMenuOpened)}
|
||||||
selectedFilterAmount={selectedFilterAmount}
|
>
|
||||||
/>
|
<FormattedMessage
|
||||||
|
id="FNpv6K"
|
||||||
|
defaultMessage="Filters"
|
||||||
|
description="button"
|
||||||
|
/>
|
||||||
|
{isFilterActive && selectedFilterAmount > 0 && (
|
||||||
|
<>({selectedFilterAmount})</>
|
||||||
|
)}
|
||||||
|
</DropdownButton>
|
||||||
<Popper
|
<Popper
|
||||||
className={sprinkles({
|
className={sprinkles({
|
||||||
backgroundColor: "surfaceNeutralPlain",
|
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;
|
title: string | React.ReactNode;
|
||||||
href?: string;
|
href?: string;
|
||||||
withoutBorder?: boolean;
|
withoutBorder?: boolean;
|
||||||
|
isAlignToRight?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
||||||
title,
|
title,
|
||||||
href,
|
href,
|
||||||
withoutBorder = false,
|
withoutBorder = false,
|
||||||
|
isAlignToRight = true,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { availableChannels, channel, isPickerActive, setChannel } =
|
const { availableChannels, channel, isPickerActive, setChannel } =
|
||||||
|
@ -24,10 +26,16 @@ export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
||||||
return (
|
return (
|
||||||
<TopNavWrapper withoutBorder={withoutBorder}>
|
<TopNavWrapper withoutBorder={withoutBorder}>
|
||||||
{href && <TopNavLink to={href} />}
|
{href && <TopNavLink to={href} />}
|
||||||
<Box __flex={1}>
|
<Box __flex={isAlignToRight ? 1 : 0}>
|
||||||
<Text variant="title">{title}</Text>
|
<Text variant="title" size="small">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" flexWrap="nowrap">
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexWrap="nowrap"
|
||||||
|
__flex={isAlignToRight ? "initial" : 1}
|
||||||
|
>
|
||||||
{isPickerActive && (
|
{isPickerActive && (
|
||||||
<AppChannelSelect
|
<AppChannelSelect
|
||||||
channels={availableChannels}
|
channels={availableChannels}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { Button } from "@dashboard/components/Button";
|
import { Button } from "@dashboard/components/Button";
|
||||||
import { buttonMessages } from "@dashboard/intl";
|
import { buttonMessages } from "@dashboard/intl";
|
||||||
import { ButtonProps } from "@saleor/macaw-ui";
|
import { ButtonProps } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
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}>
|
<Button data-test-id="back" variant="secondary" color="text" {...props}>
|
||||||
<FormattedMessage {...buttonMessages.back} />
|
{children ?? <FormattedMessage {...buttonMessages.back} />}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,7 @@ export interface DatagridProps {
|
||||||
onColumnMoved?: (startIndex: number, endIndex: number) => void;
|
onColumnMoved?: (startIndex: number, endIndex: number) => void;
|
||||||
onColumnResize?: (column: GridColumn, newSize: number) => void;
|
onColumnResize?: (column: GridColumn, newSize: number) => void;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
hasRowHover?: boolean;
|
||||||
rowMarkers?: DataEditorProps["rowMarkers"];
|
rowMarkers?: DataEditorProps["rowMarkers"];
|
||||||
freezeColumns?: DataEditorProps["freezeColumns"];
|
freezeColumns?: DataEditorProps["freezeColumns"];
|
||||||
verticalBorder?: DataEditorProps["verticalBorder"];
|
verticalBorder?: DataEditorProps["verticalBorder"];
|
||||||
|
@ -117,6 +118,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
columnSelect = "none",
|
columnSelect = "none",
|
||||||
onColumnMoved,
|
onColumnMoved,
|
||||||
onColumnResize,
|
onColumnResize,
|
||||||
|
hasRowHover = false,
|
||||||
...datagridProps
|
...datagridProps
|
||||||
}): ReactElement => {
|
}): ReactElement => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
@ -219,11 +221,11 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
|
|
||||||
const handleRowHover = useCallback(
|
const handleRowHover = useCallback(
|
||||||
(args: GridMouseEventArgs) => {
|
(args: GridMouseEventArgs) => {
|
||||||
if (readonly) {
|
if (hasRowHover) {
|
||||||
setHoverRow(args.kind !== "cell" ? undefined : args.location[1]);
|
setHoverRow(args.kind !== "cell" ? undefined : args.location[1]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[readonly],
|
[hasRowHover],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGridSelectionChange = (gridSelection: GridSelection) => {
|
const handleGridSelectionChange = (gridSelection: GridSelection) => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { buttonMessages } from "@dashboard/intl";
|
||||||
import { DialogContentText } from "@material-ui/core";
|
import { DialogContentText } from "@material-ui/core";
|
||||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
@ -26,19 +27,20 @@ const DeleteFilterTabDialog: React.FC<DeleteFilterTabDialogProps> = ({
|
||||||
<ActionDialog
|
<ActionDialog
|
||||||
open={open}
|
open={open}
|
||||||
confirmButtonState={confirmButtonState}
|
confirmButtonState={confirmButtonState}
|
||||||
|
backButtonText={intl.formatMessage(buttonMessages.cancel)}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onConfirm={onSubmit}
|
onConfirm={onSubmit}
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage({
|
||||||
id: "7NfoiJ",
|
id: "xy66ru",
|
||||||
defaultMessage: "Delete Search",
|
defaultMessage: "Delete preset",
|
||||||
description: "custom search delete, dialog header",
|
description: "custom preset delete, dialog header",
|
||||||
})}
|
})}
|
||||||
variant="delete"
|
variant="delete"
|
||||||
>
|
>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="UaYJJ8"
|
id="U5CH0u"
|
||||||
defaultMessage="Are you sure you want to delete {name} search tab?"
|
defaultMessage="Are you sure you want to delete {name} preset?"
|
||||||
values={{
|
values={{
|
||||||
name: <strong>{tabName}</strong>,
|
name: <strong>{tabName}</strong>,
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { alpha } from "@material-ui/core/styles";
|
||||||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
import { Button, makeStyles } from "@saleor/macaw-ui";
|
||||||
import { vars } from "@saleor/macaw-ui/next";
|
import { vars } from "@saleor/macaw-ui/next";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import React, { useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { FilterContent } from ".";
|
import { FilterContent } from ".";
|
||||||
|
@ -14,7 +14,7 @@ import {
|
||||||
InvalidFilters,
|
InvalidFilters,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import useFilter from "./useFilter";
|
import useFilter from "./useFilter";
|
||||||
import { extractInvalidFilters } from "./utils";
|
import { extractInvalidFilters, getSelectedFiltersAmount } from "./utils";
|
||||||
|
|
||||||
export interface FilterProps<TFilterKeys extends string = string> {
|
export interface FilterProps<TFilterKeys extends string = string> {
|
||||||
currencySymbol?: string;
|
currencySymbol?: string;
|
||||||
|
@ -103,6 +103,11 @@ const Filter: React.FC<FilterProps> = props => {
|
||||||
|
|
||||||
const isFilterActive = menu.some(filterElement => filterElement.active);
|
const isFilterActive = menu.some(filterElement => filterElement.active);
|
||||||
|
|
||||||
|
const selectedFilterAmount = useMemo(
|
||||||
|
() => getSelectedFiltersAmount(menu, data),
|
||||||
|
[data, menu],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const invalidFilters = extractInvalidFilters(data, menu);
|
const invalidFilters = extractInvalidFilters(data, menu);
|
||||||
|
|
||||||
|
@ -147,23 +152,8 @@ const Filter: React.FC<FilterProps> = props => {
|
||||||
description="button"
|
description="button"
|
||||||
/>
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
{isFilterActive && (
|
{isFilterActive && selectedFilterAmount > 0 && (
|
||||||
<>
|
<>({selectedFilterAmount})</>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Popper
|
<Popper
|
||||||
|
|
|
@ -16,31 +16,11 @@ export interface FilterReducerAction<K extends string, T extends FieldType> {
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
export type UpdateStateFunction<K extends string = string> = <
|
export type UpdateStateFunction<K extends string = string> = <
|
||||||
T extends FieldType
|
T extends FieldType,
|
||||||
>(
|
>(
|
||||||
value: FilterReducerAction<K, T>,
|
value: FilterReducerAction<K, T>,
|
||||||
) => void;
|
) => 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>(
|
function setProperty<K extends string, T extends FieldType>(
|
||||||
prevState: IFilter<K, T>,
|
prevState: IFilter<K, T>,
|
||||||
filter: K,
|
filter: K,
|
||||||
|
@ -66,8 +46,6 @@ function reduceFilter<K extends string, T extends FieldType>(
|
||||||
action.payload.name,
|
action.payload.name,
|
||||||
action.payload.update,
|
action.payload.update,
|
||||||
);
|
);
|
||||||
case "merge":
|
|
||||||
return merge(prevState, action.payload.new);
|
|
||||||
case "reset":
|
case "reset":
|
||||||
return action.payload.new;
|
return action.payload.new;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import reduceFilter, { FilterReducerAction } from "./reducer";
|
||||||
import { FieldType, FilterElement, IFilter } from "./types";
|
import { FieldType, FilterElement, IFilter } from "./types";
|
||||||
|
|
||||||
export type FilterDispatchFunction<K extends string = string> = <
|
export type FilterDispatchFunction<K extends string = string> = <
|
||||||
T extends FieldType
|
T extends FieldType,
|
||||||
>(
|
>(
|
||||||
value: FilterReducerAction<K, T>,
|
value: FilterReducerAction<K, T>,
|
||||||
) => void;
|
) => void;
|
||||||
|
@ -53,7 +53,7 @@ function useFilter<K extends string>(initialFilter: IFilter<K>): UseFilter<K> {
|
||||||
payload: {
|
payload: {
|
||||||
new: parsedInitialFilter,
|
new: parsedInitialFilter,
|
||||||
},
|
},
|
||||||
type: "merge",
|
type: "reset",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(refresh, [initialFilter]);
|
useEffect(refresh, [initialFilter]);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import compact from "lodash/compact";
|
||||||
import {
|
import {
|
||||||
FieldType,
|
FieldType,
|
||||||
FilterElement,
|
FilterElement,
|
||||||
|
IFilter,
|
||||||
InvalidFilters,
|
InvalidFilters,
|
||||||
ValidationErrorCode,
|
ValidationErrorCode,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
@ -10,13 +11,13 @@ import {
|
||||||
export const getByName = (nameToCompare: string) => (obj: { name: string }) =>
|
export const getByName = (nameToCompare: string) => (obj: { name: string }) =>
|
||||||
obj.name === nameToCompare;
|
obj.name === nameToCompare;
|
||||||
|
|
||||||
export const isAutocompleteFilterFieldValid = function<T extends string>({
|
export const isAutocompleteFilterFieldValid = function <T extends string>({
|
||||||
value,
|
value,
|
||||||
}: FilterElement<T>) {
|
}: FilterElement<T>) {
|
||||||
return !!compact(value).length;
|
return !!compact(value).length;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isNumberFilterFieldValid = function<T extends string>({
|
export const isNumberFilterFieldValid = function <T extends string>({
|
||||||
value,
|
value,
|
||||||
}: FilterElement<T>) {
|
}: FilterElement<T>) {
|
||||||
const [min, max] = value;
|
const [min, max] = value;
|
||||||
|
@ -28,7 +29,7 @@ export const isNumberFilterFieldValid = function<T extends string>({
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFilterFieldValid = function<T extends string>(
|
export const isFilterFieldValid = function <T extends string>(
|
||||||
filter: FilterElement<T>,
|
filter: FilterElement<T>,
|
||||||
) {
|
) {
|
||||||
const { type } = filter;
|
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>,
|
filter: FilterElement<T>,
|
||||||
) {
|
) {
|
||||||
const { required, active } = filter;
|
const { required, active } = filter;
|
||||||
|
@ -59,7 +60,7 @@ export const isFilterValid = function<T extends string>(
|
||||||
return isFilterFieldValid(filter);
|
return isFilterFieldValid(filter);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extractInvalidFilters = function<T extends string>(
|
export const extractInvalidFilters = function <T extends string>(
|
||||||
filtersData: Array<FilterElement<T>>,
|
filtersData: Array<FilterElement<T>>,
|
||||||
filtersDataStructure: Array<FilterElement<T>>,
|
filtersDataStructure: Array<FilterElement<T>>,
|
||||||
): InvalidFilters<T> {
|
): InvalidFilters<T> {
|
||||||
|
@ -118,3 +119,19 @@ export const extractInvalidFilters = function<T extends string>(
|
||||||
{} as InvalidFilters<T>,
|
{} 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 intl = useIntl();
|
||||||
const [errors, setErrors] = React.useState(false);
|
const [errors, setErrors] = React.useState(false);
|
||||||
const handleErrors = data => {
|
const handleErrors = data => {
|
||||||
if (data.name.length) {
|
if (data.name.trim().length) {
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
setErrors(false);
|
setErrors(false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -50,9 +50,9 @@ const SaveFilterTabDialog: React.FC<SaveFilterTabDialogProps> = ({
|
||||||
<Dialog onClose={onClose} open={open} fullWidth maxWidth="sm">
|
<Dialog onClose={onClose} open={open} fullWidth maxWidth="sm">
|
||||||
<DialogTitle disableTypography>
|
<DialogTitle disableTypography>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="liLrVs"
|
id="P9YktI"
|
||||||
defaultMessage="Save Custom Search"
|
defaultMessage="Save view preset"
|
||||||
description="save filter tab, header"
|
description="save preset, header"
|
||||||
/>
|
/>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<Form initial={initialForm} onSubmit={handleErrors}>
|
<Form initial={initialForm} onSubmit={handleErrors}>
|
||||||
|
@ -63,9 +63,9 @@ const SaveFilterTabDialog: React.FC<SaveFilterTabDialogProps> = ({
|
||||||
autoFocus
|
autoFocus
|
||||||
fullWidth
|
fullWidth
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: "QcIFCs",
|
id: "zhnwl6",
|
||||||
defaultMessage: "Search Name",
|
defaultMessage: "Preset name",
|
||||||
description: "save search tab",
|
description: "save preset name",
|
||||||
})}
|
})}
|
||||||
name={"name" as keyof SaveFilterTabDialogFormData}
|
name={"name" as keyof SaveFilterTabDialogFormData}
|
||||||
value={data.name}
|
value={data.name}
|
||||||
|
@ -75,7 +75,9 @@ const SaveFilterTabDialog: React.FC<SaveFilterTabDialogProps> = ({
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<BackButton onClick={onClose} />
|
<BackButton onClick={onClose}>
|
||||||
|
<FormattedMessage {...buttonMessages.cancel} />
|
||||||
|
</BackButton>
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
transitionState={confirmButtonState}
|
transitionState={confirmButtonState}
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||||
import { Sort } from "@dashboard/types";
|
import { Sort } from "@dashboard/types";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
export type SortByRankUrlQueryParams<T extends string> = Sort<T> & {
|
export type SortByRankUrlQueryParams<T extends string> = Sort<T> & {
|
||||||
query?: string;
|
query?: string;
|
||||||
|
@ -24,17 +24,35 @@ export function useSortRedirects<SortField extends string>({
|
||||||
resetToDefault,
|
resetToDefault,
|
||||||
}: UseSortRedirectsOpts<SortField>) {
|
}: UseSortRedirectsOpts<SortField>) {
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
|
const prevAscParams = useRef<boolean | null>(null);
|
||||||
|
|
||||||
const hasQuery = !!params.query?.trim();
|
const hasQuery = !!params.query?.trim();
|
||||||
|
|
||||||
|
const getAscParam = () => {
|
||||||
|
if (hasQuery) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasQuery && prevAscParams.current !== null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.asc;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sortWithQuery = "rank" as SortField;
|
const sortWithQuery = "rank" as SortField;
|
||||||
const sortWithoutQuery =
|
const sortWithoutQuery =
|
||||||
params.sort === "rank" ? defaultSortField : params.sort;
|
params.sort === "rank" ? defaultSortField : params.sort;
|
||||||
|
|
||||||
|
if (hasQuery || params.sort === "rank") {
|
||||||
|
prevAscParams.current = params.asc;
|
||||||
|
}
|
||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
urlFunc({
|
urlFunc({
|
||||||
...params,
|
...params,
|
||||||
asc: hasQuery ? false : params.asc,
|
asc: getAscParam(),
|
||||||
sort: hasQuery ? sortWithQuery : sortWithoutQuery,
|
sort: hasQuery ? sortWithQuery : sortWithoutQuery,
|
||||||
}),
|
}),
|
||||||
{ replace: true },
|
{ replace: true },
|
||||||
|
|
|
@ -131,6 +131,10 @@ export const commonMessages = defineMessages({
|
||||||
id: "JqiqNj",
|
id: "JqiqNj",
|
||||||
defaultMessage: "Something went wrong",
|
defaultMessage: "Something went wrong",
|
||||||
},
|
},
|
||||||
|
update: {
|
||||||
|
defaultMessage: "Update",
|
||||||
|
id: "BWpuKl",
|
||||||
|
},
|
||||||
startDate: {
|
startDate: {
|
||||||
id: "QirE3M",
|
id: "QirE3M",
|
||||||
defaultMessage: "Start Date",
|
defaultMessage: "Start Date",
|
||||||
|
|
|
@ -33,7 +33,8 @@ const props: OrderListPageProps = {
|
||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
active: false,
|
active: false,
|
||||||
value: [
|
value: [],
|
||||||
|
choices: [
|
||||||
{
|
{
|
||||||
label: "Channel PLN",
|
label: "Channel PLN",
|
||||||
value: "channelId",
|
value: "channelId",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { IFilter } from "@dashboard/components/Filter";
|
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 { OrderStatusFilter, PaymentChargeStatusEnum } from "@dashboard/graphql";
|
||||||
import {
|
import {
|
||||||
commonMessages,
|
commonMessages,
|
||||||
|
@ -39,7 +39,7 @@ export interface OrderListFilterOpts {
|
||||||
customer: FilterOpts<string>;
|
customer: FilterOpts<string>;
|
||||||
status: FilterOpts<OrderStatusFilter[]>;
|
status: FilterOpts<OrderStatusFilter[]>;
|
||||||
paymentStatus: FilterOpts<PaymentChargeStatusEnum[]>;
|
paymentStatus: FilterOpts<PaymentChargeStatusEnum[]>;
|
||||||
channel?: FilterOpts<MultiAutocompleteChoiceType[]>;
|
channel: FilterOpts<string[]> & { choices: SingleAutocompleteChoiceType[] };
|
||||||
clickAndCollect: FilterOpts<boolean>;
|
clickAndCollect: FilterOpts<boolean>;
|
||||||
preorder: FilterOpts<boolean>;
|
preorder: FilterOpts<boolean>;
|
||||||
giftCard: FilterOpts<OrderFilterGiftCard[]>;
|
giftCard: FilterOpts<OrderFilterGiftCard[]>;
|
||||||
|
@ -247,15 +247,15 @@ export function createFilterStructure(
|
||||||
),
|
),
|
||||||
active: opts.metadata.active,
|
active: opts.metadata.active,
|
||||||
},
|
},
|
||||||
...(opts?.channel?.value.length
|
...(opts?.channel?.choices?.length
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
...createOptionsField(
|
...createOptionsField(
|
||||||
OrderFilterKeys.channel,
|
OrderFilterKeys.channel,
|
||||||
intl.formatMessage(messages.channel),
|
intl.formatMessage(messages.channel),
|
||||||
[],
|
|
||||||
true,
|
|
||||||
opts.channel.value,
|
opts.channel.value,
|
||||||
|
true,
|
||||||
|
opts.channel.choices,
|
||||||
),
|
),
|
||||||
active: opts.channel.active,
|
active: opts.channel.active,
|
||||||
},
|
},
|
||||||
|
|
|
@ -51,7 +51,8 @@ describe("Filtering URL params", () => {
|
||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
active: false,
|
active: false,
|
||||||
value: [
|
value: [],
|
||||||
|
choices: [
|
||||||
{
|
{
|
||||||
label: "Channel PLN",
|
label: "Channel PLN",
|
||||||
value: "channelId",
|
value: "channelId",
|
||||||
|
|
|
@ -53,7 +53,8 @@ export function getFilterOpts(
|
||||||
channel: channels
|
channel: channels
|
||||||
? {
|
? {
|
||||||
active: params?.channel !== undefined,
|
active: params?.channel !== undefined,
|
||||||
value: channels,
|
choices: channels,
|
||||||
|
value: params?.channel ?? [],
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
created: {
|
created: {
|
||||||
|
|
|
@ -58,6 +58,7 @@ interface ProductListDatagridProps
|
||||||
>;
|
>;
|
||||||
onColumnQueryChange: (query: string) => void;
|
onColumnQueryChange: (query: string) => void;
|
||||||
isAttributeLoading?: boolean;
|
isAttributeLoading?: boolean;
|
||||||
|
hasRowHover?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
|
export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
|
||||||
|
@ -80,6 +81,7 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
|
||||||
onColumnQueryChange,
|
onColumnQueryChange,
|
||||||
activeAttributeSortId,
|
activeAttributeSortId,
|
||||||
filterDependency,
|
filterDependency,
|
||||||
|
hasRowHover,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const searchProductType = useSearchProductTypes();
|
const searchProductType = useSearchProductTypes();
|
||||||
|
@ -230,6 +232,7 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
|
||||||
rowMarkers="none"
|
rowMarkers="none"
|
||||||
columnSelect="single"
|
columnSelect="single"
|
||||||
freezeColumns={2}
|
freezeColumns={2}
|
||||||
|
hasRowHover={hasRowHover}
|
||||||
onColumnMoved={handleColumnMoved}
|
onColumnMoved={handleColumnMoved}
|
||||||
onColumnResize={handleColumnResize}
|
onColumnResize={handleColumnResize}
|
||||||
verticalBorder={col => (col > 1 ? true : false)}
|
verticalBorder={col => (col > 1 ? true : false)}
|
||||||
|
|
|
@ -36,6 +36,10 @@ const props: ProductListPageProps = {
|
||||||
},
|
},
|
||||||
channels: [],
|
channels: [],
|
||||||
columnQuery: "",
|
columnQuery: "",
|
||||||
|
currentTab: 0,
|
||||||
|
hasPresetsChanged: false,
|
||||||
|
onTabSave: () => undefined,
|
||||||
|
onTabUpdate: () => undefined,
|
||||||
availableInGridAttributes: [],
|
availableInGridAttributes: [],
|
||||||
onColumnQueryChange: () => undefined,
|
onColumnQueryChange: () => undefined,
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,10 +4,11 @@ import {
|
||||||
mapToMenuItemsForProductOverviewActions,
|
mapToMenuItemsForProductOverviewActions,
|
||||||
useExtensions,
|
useExtensions,
|
||||||
} from "@dashboard/apps/hooks/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 { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||||
import { ButtonWithDropdown } from "@dashboard/components/ButtonWithDropdown";
|
import { ButtonWithDropdown } from "@dashboard/components/ButtonWithDropdown";
|
||||||
import { getByName } from "@dashboard/components/Filter/utils";
|
import { getByName } from "@dashboard/components/Filter/utils";
|
||||||
|
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
|
||||||
import { ListPageLayout } from "@dashboard/components/Layouts";
|
import { ListPageLayout } from "@dashboard/components/Layouts";
|
||||||
import LimitReachedAlert from "@dashboard/components/LimitReachedAlert";
|
import LimitReachedAlert from "@dashboard/components/LimitReachedAlert";
|
||||||
import { TopNavMenu } from "@dashboard/components/TopNavMenu";
|
import { TopNavMenu } from "@dashboard/components/TopNavMenu";
|
||||||
|
@ -32,8 +33,8 @@ import {
|
||||||
} from "@dashboard/types";
|
} from "@dashboard/types";
|
||||||
import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
|
import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
|
||||||
import { Card } from "@material-ui/core";
|
import { Card } from "@material-ui/core";
|
||||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
import { Box, Button, ChevronRightIcon, Text } from "@saleor/macaw-ui/next";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { ProductListUrlSortField, productUrl } from "../../urls";
|
import { ProductListUrlSortField, productUrl } from "../../urls";
|
||||||
|
@ -49,7 +50,10 @@ import {
|
||||||
export interface ProductListPageProps
|
export interface ProductListPageProps
|
||||||
extends PageListProps<ProductListColumns>,
|
extends PageListProps<ProductListColumns>,
|
||||||
ListActions,
|
ListActions,
|
||||||
FilterPageProps<ProductFilterKeys, ProductListFilterOpts>,
|
Omit<
|
||||||
|
FilterPageProps<ProductFilterKeys, ProductListFilterOpts>,
|
||||||
|
"onTabDelete"
|
||||||
|
>,
|
||||||
FetchMoreProps,
|
FetchMoreProps,
|
||||||
SortPage<ProductListUrlSortField>,
|
SortPage<ProductListUrlSortField>,
|
||||||
ChannelProps {
|
ChannelProps {
|
||||||
|
@ -63,9 +67,12 @@ export interface ProductListPageProps
|
||||||
limits: RefreshLimitsQuery["shop"]["limits"];
|
limits: RefreshLimitsQuery["shop"]["limits"];
|
||||||
products: RelayToFlat<ProductListQuery["products"]>;
|
products: RelayToFlat<ProductListQuery["products"]>;
|
||||||
selectedProductIds: string[];
|
selectedProductIds: string[];
|
||||||
|
hasPresetsChanged: boolean;
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
onColumnQueryChange: (query: string) => void;
|
onColumnQueryChange: (query: string) => void;
|
||||||
|
onTabUpdate: (tabName: string) => void;
|
||||||
|
onTabDelete: (tabIndex: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductListViewType = "datagrid" | "tile";
|
export type ProductListViewType = "datagrid" | "tile";
|
||||||
|
@ -95,11 +102,20 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
selectedChannelId,
|
selectedChannelId,
|
||||||
selectedProductIds,
|
selectedProductIds,
|
||||||
activeAttributeSortId,
|
activeAttributeSortId,
|
||||||
|
onTabChange,
|
||||||
|
onTabDelete,
|
||||||
|
onTabSave,
|
||||||
|
onAll,
|
||||||
|
currentTab,
|
||||||
|
tabs,
|
||||||
|
onTabUpdate,
|
||||||
|
hasPresetsChanged,
|
||||||
...listProps
|
...listProps
|
||||||
} = props;
|
} = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
const filterStructure = createFilterStructure(intl, filterOpts);
|
const filterStructure = createFilterStructure(intl, filterOpts);
|
||||||
|
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
|
||||||
|
|
||||||
const filterDependency = filterStructure.find(getByName("channel"));
|
const filterDependency = filterStructure.find(getByName("channel"));
|
||||||
|
|
||||||
|
@ -122,59 +138,93 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListPageLayout>
|
<ListPageLayout>
|
||||||
<TopNav withoutBorder title={intl.formatMessage(sectionNames.products)}>
|
<TopNav
|
||||||
<Box display="flex" alignItems="center" gap={5}>
|
withoutBorder
|
||||||
{hasLimits(limits, "productVariants") && (
|
isAlignToRight={false}
|
||||||
<Text variant="caption">
|
title={intl.formatMessage(sectionNames.products)}
|
||||||
{intl.formatMessage(
|
>
|
||||||
|
<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">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "Kw0jHS",
|
||||||
|
defaultMessage: "{count}/{max} SKUs used",
|
||||||
|
description: "created products counter",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: limits.currentUsage.productVariants,
|
||||||
|
max: limits.allowedUsage.productVariants,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<TopNavMenu
|
||||||
|
dataTestId="menu"
|
||||||
|
items={[
|
||||||
{
|
{
|
||||||
id: "Kw0jHS",
|
label: intl.formatMessage({
|
||||||
defaultMessage: "{count}/{max} SKUs used",
|
id: "7FL+WZ",
|
||||||
description: "created products counter",
|
defaultMessage: "Export Products",
|
||||||
|
description: "export products to csv file, button",
|
||||||
|
}),
|
||||||
|
onSelect: onExport,
|
||||||
|
testId: "export",
|
||||||
},
|
},
|
||||||
{
|
...extensionMenuItems,
|
||||||
count: limits.currentUsage.productVariants,
|
]}
|
||||||
max: limits.allowedUsage.productVariants,
|
/>
|
||||||
},
|
{extensionCreateButtonItems.length > 0 ? (
|
||||||
)}
|
<ButtonWithDropdown
|
||||||
</Text>
|
onClick={onAdd}
|
||||||
)}
|
testId={"add-product"}
|
||||||
<TopNavMenu
|
options={extensionCreateButtonItems}
|
||||||
dataTestId="menu"
|
>
|
||||||
items={[
|
<FormattedMessage
|
||||||
{
|
id="JFmOfi"
|
||||||
label: intl.formatMessage({
|
defaultMessage="Create Product"
|
||||||
id: "7FL+WZ",
|
description="button"
|
||||||
defaultMessage: "Export Products",
|
/>
|
||||||
description: "export products to csv file, button",
|
</ButtonWithDropdown>
|
||||||
}),
|
) : (
|
||||||
onSelect: onExport,
|
<Button data-test-id="add-product" onClick={onAdd}>
|
||||||
testId: "export",
|
<FormattedMessage
|
||||||
},
|
id="JFmOfi"
|
||||||
...extensionMenuItems,
|
defaultMessage="Create Product"
|
||||||
]}
|
description="button"
|
||||||
/>
|
/>
|
||||||
{extensionCreateButtonItems.length > 0 ? (
|
</Button>
|
||||||
<ButtonWithDropdown
|
)}
|
||||||
onClick={onAdd}
|
</Box>
|
||||||
testId={"add-product"}
|
|
||||||
options={extensionCreateButtonItems}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id="JFmOfi"
|
|
||||||
defaultMessage="Create Product"
|
|
||||||
description="button"
|
|
||||||
/>
|
|
||||||
</ButtonWithDropdown>
|
|
||||||
) : (
|
|
||||||
<Button data-test-id="add-product" onClick={onAdd}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="JFmOfi"
|
|
||||||
defaultMessage="Create Product"
|
|
||||||
description="button"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</TopNav>
|
</TopNav>
|
||||||
{limitReached && (
|
{limitReached && (
|
||||||
|
@ -199,7 +249,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
alignItems="stretch"
|
alignItems="stretch"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
>
|
>
|
||||||
<FilterBar
|
<ListFilters
|
||||||
currencySymbol={currencySymbol}
|
currencySymbol={currencySymbol}
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
onFilterChange={onFilterChange}
|
onFilterChange={onFilterChange}
|
||||||
|
@ -221,6 +271,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
{isDatagridView ? (
|
{isDatagridView ? (
|
||||||
<ProductListDatagrid
|
<ProductListDatagrid
|
||||||
{...listProps}
|
{...listProps}
|
||||||
|
hasRowHover={!isFilterPresetOpen}
|
||||||
filterDependency={filterDependency}
|
filterDependency={filterDependency}
|
||||||
activeAttributeSortId={activeAttributeSortId}
|
activeAttributeSortId={activeAttributeSortId}
|
||||||
columnQuery={columnQuery}
|
columnQuery={columnQuery}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { IFilter } from "@dashboard/components/Filter";
|
||||||
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";
|
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";
|
||||||
import { AttributeInputTypeEnum, StockAvailability } from "@dashboard/graphql";
|
import { AttributeInputTypeEnum, StockAvailability } from "@dashboard/graphql";
|
||||||
import { commonMessages, sectionNames } from "@dashboard/intl";
|
import { commonMessages, sectionNames } from "@dashboard/intl";
|
||||||
|
import { parseBoolean } from "@dashboard/misc";
|
||||||
import { ProductListUrlFiltersAsDictWithMultipleValues } from "@dashboard/products/urls";
|
import { ProductListUrlFiltersAsDictWithMultipleValues } from "@dashboard/products/urls";
|
||||||
import {
|
import {
|
||||||
AutocompleteFilterOpts,
|
AutocompleteFilterOpts,
|
||||||
|
@ -32,7 +33,8 @@ export const ProductFilterKeys = {
|
||||||
channel: "channel",
|
channel: "channel",
|
||||||
productKind: "productKind",
|
productKind: "productKind",
|
||||||
} as const;
|
} as const;
|
||||||
export type ProductFilterKeys = typeof ProductFilterKeys[keyof typeof ProductFilterKeys];
|
export type ProductFilterKeys =
|
||||||
|
(typeof ProductFilterKeys)[keyof typeof ProductFilterKeys];
|
||||||
|
|
||||||
export type AttributeFilterOpts = FilterOpts<string[]> & {
|
export type AttributeFilterOpts = FilterOpts<string[]> & {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -105,9 +107,9 @@ const messages = defineMessages({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterByType = (type: AttributeInputTypeEnum) => (
|
const filterByType =
|
||||||
attribute: AttributeFilterOpts,
|
(type: AttributeInputTypeEnum) => (attribute: AttributeFilterOpts) =>
|
||||||
) => attribute.inputType === type;
|
attribute.inputType === type;
|
||||||
|
|
||||||
export function createFilterStructure(
|
export function createFilterStructure(
|
||||||
intl: IntlShape,
|
intl: IntlShape,
|
||||||
|
@ -254,7 +256,7 @@ export function createFilterStructure(
|
||||||
attr.slug,
|
attr.slug,
|
||||||
attr.name,
|
attr.name,
|
||||||
Array.isArray(attr.value)
|
Array.isArray(attr.value)
|
||||||
? undefined
|
? parseBoolean(attr.value[0], undefined)
|
||||||
: (attr.value as unknown) === "true",
|
: (attr.value as unknown) === "true",
|
||||||
{
|
{
|
||||||
positive: intl.formatMessage(commonMessages.yes),
|
positive: intl.formatMessage(commonMessages.yes),
|
||||||
|
|
|
@ -76,6 +76,7 @@ export interface ProductListUrlQueryParams
|
||||||
Pagination,
|
Pagination,
|
||||||
ActiveTab {
|
ActiveTab {
|
||||||
attributeId?: string;
|
attributeId?: string;
|
||||||
|
presestesChanged?: string;
|
||||||
}
|
}
|
||||||
export const productListUrl = (params?: ProductListUrlQueryParams): string =>
|
export const productListUrl = (params?: ProductListUrlQueryParams): string =>
|
||||||
productListPath + "?" + stringifyQs(params);
|
productListPath + "?" + stringifyQs(params);
|
||||||
|
|
|
@ -58,12 +58,14 @@ import useCategorySearch from "@dashboard/searches/useCategorySearch";
|
||||||
import useCollectionSearch from "@dashboard/searches/useCollectionSearch";
|
import useCollectionSearch from "@dashboard/searches/useCollectionSearch";
|
||||||
import useProductTypeSearch from "@dashboard/searches/useProductTypeSearch";
|
import useProductTypeSearch from "@dashboard/searches/useProductTypeSearch";
|
||||||
import { ListViews } from "@dashboard/types";
|
import { ListViews } from "@dashboard/types";
|
||||||
|
import { prepareQs } from "@dashboard/utils/filters/qs";
|
||||||
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
|
||||||
import createFilterHandlers from "@dashboard/utils/handlers/filterHandlers";
|
import createFilterHandlers from "@dashboard/utils/handlers/filterHandlers";
|
||||||
import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps";
|
import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps";
|
||||||
import { getSortUrlVariables } from "@dashboard/utils/sort";
|
import { getSortUrlVariables } from "@dashboard/utils/sort";
|
||||||
import { DialogContentText } from "@material-ui/core";
|
import { DialogContentText } from "@material-ui/core";
|
||||||
import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
|
import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
|
||||||
|
import { stringify } from "qs";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
|
@ -71,16 +73,20 @@ import { useSortRedirects } from "../../../hooks/useSortRedirects";
|
||||||
import ProductListPage from "../../components/ProductListPage";
|
import ProductListPage from "../../components/ProductListPage";
|
||||||
import {
|
import {
|
||||||
deleteFilterTab,
|
deleteFilterTab,
|
||||||
getActiveFilters,
|
|
||||||
getFilterOpts,
|
getFilterOpts,
|
||||||
getFilterQueryParam,
|
getFilterQueryParam,
|
||||||
getFiltersCurrentTab,
|
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
getFilterVariables,
|
getFilterVariables,
|
||||||
saveFilterTab,
|
saveFilterTab,
|
||||||
|
updateFilterTab,
|
||||||
} from "./filters";
|
} from "./filters";
|
||||||
import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
|
import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
|
||||||
import { getAvailableProductKinds, getProductKindOpts } from "./utils";
|
import {
|
||||||
|
getActiveTabIndexAfterTabDelete,
|
||||||
|
getAvailableProductKinds,
|
||||||
|
getNextUniqueTabName,
|
||||||
|
getProductKindOpts,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
interface ProductListProps {
|
interface ProductListProps {
|
||||||
params: ProductListUrlQueryParams;
|
params: ProductListUrlQueryParams;
|
||||||
|
@ -91,6 +97,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
const { queue } = useBackgroundTask();
|
const { queue } = useBackgroundTask();
|
||||||
|
|
||||||
|
const [tabIndexToDelete, setTabIndexToDelete] = useState<number | null>(null);
|
||||||
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
@ -190,7 +197,8 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
|
|
||||||
const tabs = getFilterTabs();
|
const tabs = getFilterTabs();
|
||||||
|
|
||||||
const currentTab = getFiltersCurrentTab(params, tabs);
|
const currentTab =
|
||||||
|
params.activeTab !== undefined ? parseInt(params.activeTab, 10) : undefined;
|
||||||
|
|
||||||
const countAllProducts = useProductCountQuery({
|
const countAllProducts = useProductCountQuery({
|
||||||
skip: params.action !== "export",
|
skip: params.action !== "export",
|
||||||
|
@ -227,29 +235,59 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
getFilterQueryParam,
|
getFilterQueryParam,
|
||||||
navigate,
|
navigate,
|
||||||
params,
|
params,
|
||||||
|
keepActiveTab: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTabChange = (tab: number) => {
|
const handleTabChange = (tab: number) => {
|
||||||
reset();
|
reset();
|
||||||
navigate(
|
const qs = new URLSearchParams(getFilterTabs()[tab - 1]?.data ?? "");
|
||||||
productListUrl({
|
qs.append("activeTab", tab.toString());
|
||||||
activeTab: tab.toString(),
|
|
||||||
...getFilterTabs()[tab - 1].data,
|
navigate(productListUrl() + qs.toString());
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterTabDelete = () => {
|
const handleFilterTabDelete = () => {
|
||||||
deleteFilterTab(currentTab);
|
deleteFilterTab(tabIndexToDelete);
|
||||||
reset();
|
reset();
|
||||||
navigate(productListUrl());
|
|
||||||
|
// 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) => {
|
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);
|
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) =>
|
const handleSort = (field: ProductListUrlSortField, attributeId?: string) =>
|
||||||
navigate(
|
navigate(
|
||||||
productListUrl({
|
productListUrl({
|
||||||
|
@ -355,6 +393,17 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
channelOpts,
|
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({
|
const paginationValues = usePaginator({
|
||||||
pageInfo: data?.products?.pageInfo,
|
pageInfo: data?.products?.pageInfo,
|
||||||
paginationState,
|
paginationState,
|
||||||
|
@ -416,10 +465,15 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
onFilterChange={changeFilters}
|
onFilterChange={changeFilters}
|
||||||
onFilterAttributeFocus={setFocusedAttribute}
|
onFilterAttributeFocus={setFocusedAttribute}
|
||||||
onTabSave={() => openModal("save-search")}
|
onTabSave={() => openModal("save-search")}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabUpdate={hanleFilterTabUpdate}
|
||||||
|
onTabDelete={(tabIndex: number) => {
|
||||||
|
setTabIndexToDelete(tabIndex);
|
||||||
|
openModal("delete-search");
|
||||||
|
}}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
|
hasPresetsChanged={hasPresetsChanged()}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
tabs={getFilterTabs().map(tab => tab.name)}
|
tabs={tabs.map(tab => tab.name)}
|
||||||
onExport={() => openModal("export")}
|
onExport={() => openModal("export")}
|
||||||
selectedChannelId={selectedChannel?.id}
|
selectedChannelId={selectedChannel?.id}
|
||||||
columnQuery={availableInGridAttributesOpts.query}
|
columnQuery={availableInGridAttributesOpts.query}
|
||||||
|
@ -498,7 +552,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
confirmButtonState="default"
|
confirmButtonState="default"
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
onSubmit={handleFilterTabDelete}
|
onSubmit={handleFilterTabDelete}
|
||||||
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
|
tabName={tabs[tabIndexToDelete - 1]?.name ?? "..."}
|
||||||
/>
|
/>
|
||||||
<ProductTypePickerDialog
|
<ProductTypePickerDialog
|
||||||
confirmButtonState="success"
|
confirmButtonState="success"
|
||||||
|
|
|
@ -56,7 +56,7 @@ import {
|
||||||
ProductListUrlQueryParams,
|
ProductListUrlQueryParams,
|
||||||
} from "../../urls";
|
} from "../../urls";
|
||||||
import { getProductGiftCardFilterParam } from "./utils";
|
import { getProductGiftCardFilterParam } from "./utils";
|
||||||
export const PRODUCT_FILTERS_KEY = "productFilters";
|
export const PRODUCT_FILTERS_KEY = "productPresets";
|
||||||
|
|
||||||
function getAttributeFilterParamType(inputType: AttributeInputTypeEnum) {
|
function getAttributeFilterParamType(inputType: AttributeInputTypeEnum) {
|
||||||
switch (inputType) {
|
switch (inputType) {
|
||||||
|
@ -468,14 +468,12 @@ export const {
|
||||||
deleteFilterTab,
|
deleteFilterTab,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
saveFilterTab,
|
saveFilterTab,
|
||||||
} = createFilterTabUtils<ProductListUrlFilters>(PRODUCT_FILTERS_KEY);
|
updateFilterTab,
|
||||||
|
} = createFilterTabUtils<string>(PRODUCT_FILTERS_KEY);
|
||||||
|
|
||||||
export const {
|
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
|
||||||
areFiltersApplied,
|
createFilterUtils<ProductListUrlQueryParams, ProductListUrlFilters>({
|
||||||
getActiveFilters,
|
...ProductListUrlFiltersEnum,
|
||||||
getFiltersCurrentTab,
|
...ProductListUrlFiltersWithMultipleValues,
|
||||||
} = createFilterUtils<ProductListUrlQueryParams, ProductListUrlFilters>({
|
...ProductListUrlFiltersAsDictWithMultipleValues,
|
||||||
...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;
|
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,10 +83,9 @@ export interface ListActionsWithoutToolbar {
|
||||||
isChecked: (id: string) => boolean;
|
isChecked: (id: string) => boolean;
|
||||||
selected: number;
|
selected: number;
|
||||||
}
|
}
|
||||||
export type TabListActions<
|
export type TabListActions<TToolbars extends string> =
|
||||||
TToolbars extends string
|
ListActionsWithoutToolbar &
|
||||||
> = ListActionsWithoutToolbar &
|
Record<TToolbars, React.ReactNode | React.ReactNodeArray>;
|
||||||
Record<TToolbars, React.ReactNode | React.ReactNodeArray>;
|
|
||||||
export interface ListActions extends ListActionsWithoutToolbar {
|
export interface ListActions extends ListActionsWithoutToolbar {
|
||||||
toolbar: React.ReactNode | React.ReactNodeArray;
|
toolbar: React.ReactNode | React.ReactNodeArray;
|
||||||
}
|
}
|
||||||
|
@ -129,7 +128,7 @@ export interface ChannelProps {
|
||||||
|
|
||||||
export interface PartialMutationProviderOutput<
|
export interface PartialMutationProviderOutput<
|
||||||
TData extends {} = {},
|
TData extends {} = {},
|
||||||
TVariables extends {} = {}
|
TVariables extends {} = {},
|
||||||
> {
|
> {
|
||||||
opts: MutationResult<TData> & MutationResultAdditionalProps;
|
opts: MutationResult<TData> & MutationResultAdditionalProps;
|
||||||
mutate: (variables: TVariables) => Promise<FetchResult<TData>>;
|
mutate: (variables: TVariables) => Promise<FetchResult<TData>>;
|
||||||
|
|
|
@ -13,7 +13,7 @@ function createFilterUtils<
|
||||||
>(filters: {}) {
|
>(filters: {}) {
|
||||||
function getActiveFilters(params: TQueryParams): TFilters {
|
function getActiveFilters(params: TQueryParams): TFilters {
|
||||||
return Object.keys(params)
|
return Object.keys(params)
|
||||||
.filter(key => Object.keys(filters).includes(key))
|
.filter(key => Object.values(filters).includes(key))
|
||||||
.reduce((acc, key) => {
|
.reduce((acc, key) => {
|
||||||
acc[key] = params[key];
|
acc[key] = params[key];
|
||||||
return acc;
|
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) {
|
function deleteFilterTab(id: number, key: string) {
|
||||||
const userFilters = getFilterTabs(key);
|
const userFilters = getFilterTabs(key);
|
||||||
|
|
||||||
|
@ -46,6 +66,8 @@ function createFilterTabUtils<TUrlFilters>(key: string) {
|
||||||
getFilterTabs: () => getFilterTabs<TUrlFilters>(key),
|
getFilterTabs: () => getFilterTabs<TUrlFilters>(key),
|
||||||
saveFilterTab: (name: string, data: TUrlFilters) =>
|
saveFilterTab: (name: string, data: TUrlFilters) =>
|
||||||
saveFilterTab<TUrlFilters>(name, data, key),
|
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";
|
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 CreateUrl = (params: RequiredParams) => string;
|
||||||
type CreateFilterHandlers<TFilterKeys extends string> = [
|
type CreateFilterHandlers<TFilterKeys extends string> = [
|
||||||
(filter: IFilter<TFilterKeys>) => void,
|
(filter: IFilter<TFilterKeys>) => void,
|
||||||
|
@ -21,19 +24,40 @@ function createFilterHandlers<
|
||||||
createUrl: CreateUrl;
|
createUrl: CreateUrl;
|
||||||
params: RequiredParams;
|
params: RequiredParams;
|
||||||
cleanupFn?: () => void;
|
cleanupFn?: () => void;
|
||||||
|
keepActiveTab?: boolean;
|
||||||
}): CreateFilterHandlers<TFilterKeys> {
|
}): 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>) => {
|
const changeFilters = (filters: IFilter<TFilterKeys>) => {
|
||||||
if (!!cleanupFn) {
|
if (!!cleanupFn) {
|
||||||
cleanupFn();
|
cleanupFn();
|
||||||
}
|
}
|
||||||
|
const filtersQueryParams = getFilterQueryParams(
|
||||||
|
filters,
|
||||||
|
getFilterQueryParam,
|
||||||
|
);
|
||||||
navigate(
|
navigate(
|
||||||
createUrl({
|
createUrl({
|
||||||
...params,
|
...params,
|
||||||
...getFilterQueryParams(filters, getFilterQueryParam),
|
...filtersQueryParams,
|
||||||
activeTab: undefined,
|
activeTab: getActiveTabValue(
|
||||||
|
checkIfParamsEmpty(filtersQueryParams) && !params.query?.length,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -55,14 +79,17 @@ function createFilterHandlers<
|
||||||
if (!!cleanupFn) {
|
if (!!cleanupFn) {
|
||||||
cleanupFn();
|
cleanupFn();
|
||||||
}
|
}
|
||||||
|
const trimmedQuery = query?.trim() ?? "";
|
||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
createUrl({
|
createUrl({
|
||||||
...params,
|
...params,
|
||||||
after: undefined,
|
after: undefined,
|
||||||
before: undefined,
|
before: undefined,
|
||||||
activeTab: undefined,
|
activeTab: getActiveTabValue(
|
||||||
query: query?.trim(),
|
checkIfParamsEmpty(params) && trimmedQuery === "",
|
||||||
|
),
|
||||||
|
query: trimmedQuery !== "" ? trimmedQuery : undefined,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -70,4 +97,12 @@ function createFilterHandlers<
|
||||||
return [changeFilters, resetFilters, handleSearchChange];
|
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;
|
export default createFilterHandlers;
|
||||||
|
|
Loading…
Reference in a new issue