Refactor filter components
This commit is contained in:
parent
87b94f47e1
commit
aafa6b62dc
22 changed files with 408 additions and 1088 deletions
|
@ -9,14 +9,14 @@ import classNames from "classnames";
|
|||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { FilterContentSubmitData } from "./FilterContent";
|
||||
import { IFilter } from "./types";
|
||||
import { IFilter, IFilterElement } from "./types";
|
||||
import useFilter from "./useFilter";
|
||||
import { FilterContent } from ".";
|
||||
|
||||
export interface FilterProps<TFilterKeys = string> {
|
||||
export interface FilterProps<TFilterKeys extends string = string> {
|
||||
currencySymbol: string;
|
||||
menu: IFilter<TFilterKeys>;
|
||||
onFilterAdd: (filter: FilterContentSubmitData) => void;
|
||||
onFilterAdd: (filter: Array<IFilterElement<string>>) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
|
@ -71,7 +71,8 @@ const useStyles = makeStyles(
|
|||
width: 240
|
||||
},
|
||||
popover: {
|
||||
zIndex: 1
|
||||
width: 376,
|
||||
zIndex: 3
|
||||
},
|
||||
rotate: {
|
||||
transform: "rotate(180deg)"
|
||||
|
@ -85,6 +86,7 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
|
||||
const anchor = React.useRef<HTMLDivElement>();
|
||||
const [isFilterMenuOpened, setFilterMenuOpened] = React.useState(false);
|
||||
const [data, dispatch, reset] = useFilter(menu);
|
||||
|
||||
return (
|
||||
<div ref={anchor}>
|
||||
|
@ -122,8 +124,10 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
>
|
||||
<FilterContent
|
||||
currencySymbol={currencySymbol}
|
||||
filters={menu}
|
||||
onSubmit={data => {
|
||||
filters={data}
|
||||
onClear={reset}
|
||||
onFilterPropertyChange={dispatch}
|
||||
onSubmit={() => {
|
||||
onFilterAdd(data);
|
||||
setFilterMenuOpened(false);
|
||||
}}
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import TextField, { TextFieldProps } from "@material-ui/core/TextField";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { FilterContentSubmitData, IFilter } from "../Filter";
|
||||
import Filter from "./Filter";
|
||||
|
||||
const useInputStyles = makeStyles(
|
||||
{
|
||||
input: {
|
||||
padding: "10.5px 12px"
|
||||
},
|
||||
root: {
|
||||
flex: 1
|
||||
}
|
||||
},
|
||||
{ name: "FilterActions" }
|
||||
);
|
||||
|
||||
const Search: React.FC<TextFieldProps> = props => {
|
||||
const classes = useInputStyles({});
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
className={classes.root}
|
||||
inputProps={{
|
||||
className: classes.input
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
actionContainer: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
padding: theme.spacing(1, 3)
|
||||
},
|
||||
searchOnly: {
|
||||
paddingBottom: theme.spacing(1.5)
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "FilterActions"
|
||||
}
|
||||
);
|
||||
|
||||
export interface FilterActionsPropsSearch {
|
||||
placeholder: string;
|
||||
search: string;
|
||||
onSearchChange: (event: React.ChangeEvent<any>) => void;
|
||||
}
|
||||
export interface FilterActionsPropsFilters<TKeys = string> {
|
||||
currencySymbol: string;
|
||||
menu: IFilter<TKeys>;
|
||||
onFilterAdd: (filter: FilterContentSubmitData<TKeys>) => void;
|
||||
}
|
||||
|
||||
export const FilterActionsOnlySearch: React.FC<FilterActionsPropsSearch> = props => {
|
||||
const { onSearchChange, placeholder, search } = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
return (
|
||||
<div className={classNames(classes.actionContainer, classes.searchOnly)}>
|
||||
<Search
|
||||
fullWidth
|
||||
placeholder={placeholder}
|
||||
value={search}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type FilterActionsProps = FilterActionsPropsSearch &
|
||||
FilterActionsPropsFilters;
|
||||
const FilterActions: React.FC<FilterActionsProps> = props => {
|
||||
const {
|
||||
currencySymbol,
|
||||
menu,
|
||||
onFilterAdd,
|
||||
onSearchChange,
|
||||
placeholder,
|
||||
search
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
return (
|
||||
<div className={classes.actionContainer}>
|
||||
<Filter
|
||||
currencySymbol={currencySymbol}
|
||||
menu={menu}
|
||||
onFilterAdd={onFilterAdd}
|
||||
/>
|
||||
<Search
|
||||
fullWidth
|
||||
placeholder={placeholder}
|
||||
value={search}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FilterActions.displayName = "FilterActions";
|
||||
export default FilterActions;
|
|
@ -19,10 +19,6 @@ import { IFilter, FieldType, FilterType } from "./types";
|
|||
import Arrow from "./Arrow";
|
||||
import { FilterReducerAction } from "./reducer";
|
||||
|
||||
export interface FilterContentSubmitData<TKeys = string> {
|
||||
name: TKeys;
|
||||
value: string[];
|
||||
}
|
||||
export interface FilterContentProps<T extends string = string> {
|
||||
currencySymbol: string;
|
||||
filters: IFilter<T>;
|
||||
|
@ -31,17 +27,6 @@ export interface FilterContentProps<T extends string = string> {
|
|||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
function checkFilterValue(value: string[]): boolean {
|
||||
return value.some(v => !!v);
|
||||
}
|
||||
|
||||
function getFilterChoices(items: IFilter<string>) {
|
||||
return items.map(filterItem => ({
|
||||
label: filterItem.label,
|
||||
value: filterItem.value.toString()
|
||||
}));
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
actionBar: {
|
||||
|
|
|
@ -1,229 +0,0 @@
|
|||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import Calendar from "../../icons/Calendar";
|
||||
import FormSpacer from "../FormSpacer";
|
||||
import PriceField from "../PriceField";
|
||||
import SingleSelectField from "../SingleSelectField";
|
||||
import { FieldType, IFilterItem } from "./types";
|
||||
|
||||
export interface FilterElementProps<TFilterKeys = string> {
|
||||
className?: string;
|
||||
filter: IFilterItem<TFilterKeys>;
|
||||
value: string | string[];
|
||||
onChange: (value: string | string[]) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
calendar: {
|
||||
margin: 8
|
||||
},
|
||||
input: {
|
||||
padding: "20px 12px 17px"
|
||||
}
|
||||
},
|
||||
{ name: "FilterElement" }
|
||||
);
|
||||
|
||||
export interface FilterElementProps<TFilterKeys = string> {
|
||||
className?: string;
|
||||
currencySymbol: string;
|
||||
filter: IFilterItem<TFilterKeys>;
|
||||
value: string | string[];
|
||||
onChange: (value: string | string[]) => void;
|
||||
}
|
||||
|
||||
const FilterElement: React.FC<FilterElementProps> = ({
|
||||
currencySymbol,
|
||||
className,
|
||||
filter,
|
||||
onChange,
|
||||
value
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
|
||||
if (filter.data.type === FieldType.date) {
|
||||
return (
|
||||
<TextField
|
||||
className={className}
|
||||
fullWidth
|
||||
type="date"
|
||||
onChange={event => onChange(event.target.value)}
|
||||
value={value}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
startAdornment: <Calendar className={classes.calendar} />
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (filter.data.type === FieldType.rangeDate) {
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
<FormattedMessage defaultMessage="from" />
|
||||
</Typography>
|
||||
<TextField
|
||||
className={className}
|
||||
fullWidth
|
||||
type="date"
|
||||
value={value[0]}
|
||||
onChange={event => onChange([event.target.value, value[1]])}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
startAdornment: <Calendar className={classes.calendar} />
|
||||
}}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<Typography>
|
||||
<FormattedMessage defaultMessage="to" />
|
||||
</Typography>
|
||||
<TextField
|
||||
className={className}
|
||||
fullWidth
|
||||
type="date"
|
||||
value={value[1]}
|
||||
onChange={event => onChange([value[0], event.target.value])}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
startAdornment: <Calendar className={classes.calendar} />
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else if (filter.data.type === FieldType.range) {
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
<FormattedMessage defaultMessage="from" />
|
||||
</Typography>
|
||||
<TextField
|
||||
className={className}
|
||||
fullWidth
|
||||
value={value[0]}
|
||||
onChange={event => onChange([event.target.value, value[1]])}
|
||||
type="number"
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<Typography>
|
||||
<FormattedMessage defaultMessage="to" />
|
||||
</Typography>
|
||||
<TextField
|
||||
className={className}
|
||||
fullWidth
|
||||
value={value[1]}
|
||||
onChange={event => onChange([value[0], event.target.value])}
|
||||
type="number"
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else if (filter.data.type === FieldType.rangePrice) {
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
<FormattedMessage defaultMessage="from" />
|
||||
</Typography>
|
||||
<PriceField
|
||||
currencySymbol={currencySymbol}
|
||||
className={className}
|
||||
value={value[0]}
|
||||
onChange={event => onChange([event.target.value, value[1]])}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<Typography>
|
||||
<FormattedMessage defaultMessage="to" />
|
||||
</Typography>
|
||||
<PriceField
|
||||
currencySymbol={currencySymbol}
|
||||
className={className}
|
||||
value={value[1]}
|
||||
onChange={event => onChange([value[0], event.target.value])}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else if (filter.data.type === FieldType.select) {
|
||||
return (
|
||||
<SingleSelectField
|
||||
choices={filter.data.options.map(option => ({
|
||||
...option,
|
||||
value: option.value.toString()
|
||||
}))}
|
||||
selectProps={{
|
||||
className,
|
||||
inputProps: {
|
||||
className: classes.input
|
||||
}
|
||||
}}
|
||||
value={value as string}
|
||||
placeholder={intl.formatMessage({
|
||||
defaultMessage: "Select Filter..."
|
||||
})}
|
||||
onChange={event => onChange(event.target.value)}
|
||||
/>
|
||||
);
|
||||
} else if (filter.data.type === FieldType.price) {
|
||||
return (
|
||||
<PriceField
|
||||
currencySymbol={currencySymbol}
|
||||
className={className}
|
||||
label={filter.data.fieldLabel}
|
||||
onChange={event => onChange(event.target.value)}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: !filter.data.fieldLabel && classes.input
|
||||
}
|
||||
}}
|
||||
value={value as string}
|
||||
/>
|
||||
);
|
||||
} else if (filter.data.type === FieldType.hidden) {
|
||||
onChange(filter.data.value);
|
||||
return <input type="hidden" value={value} />;
|
||||
}
|
||||
return (
|
||||
<TextField
|
||||
className={className}
|
||||
fullWidth
|
||||
label={filter.data.fieldLabel}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: !filter.data.fieldLabel && classes.input
|
||||
}
|
||||
}}
|
||||
onChange={event => onChange(event.target.value)}
|
||||
value={value as string}
|
||||
/>
|
||||
);
|
||||
};
|
||||
FilterElement.displayName = "FilterElement";
|
||||
export default FilterElement;
|
|
@ -30,14 +30,14 @@ function reduceFilter<T extends string>(
|
|||
action: FilterReducerAction<T>
|
||||
): IFilter<T> {
|
||||
switch (action.type) {
|
||||
case "clear":
|
||||
return prevState;
|
||||
case "set-property":
|
||||
return setProperty(prevState, action.payload.name, action.payload.update);
|
||||
case "reset":
|
||||
return action.payload.reset;
|
||||
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
return prevState;
|
||||
}
|
||||
|
||||
export default reduceFilter;
|
||||
|
|
|
@ -19,7 +19,6 @@ export interface IFilterElement<T extends string = string>
|
|||
extends Partial<FetchMoreProps>,
|
||||
IFilterElementMutableData {
|
||||
autocomplete?: boolean;
|
||||
currencySymbol?: string;
|
||||
label: string;
|
||||
name: T;
|
||||
type: FieldType;
|
||||
|
|
|
@ -1,37 +1,66 @@
|
|||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
|
||||
import { FilterProps } from "../../types";
|
||||
import Debounce from "../Debounce";
|
||||
import { IFilter } from "../Filter/types";
|
||||
import FilterTabs, { FilterChips, FilterTab } from "../TableFilter";
|
||||
import FilterTabs, { FilterTab } from "../TableFilter";
|
||||
import { SearchBarProps } from "../SearchBar";
|
||||
import SearchInput from "../SearchBar/SearchInput";
|
||||
import Filter from "../Filter";
|
||||
import Link from "../Link";
|
||||
import Hr from "../Hr";
|
||||
|
||||
export interface FilterBarProps<TKeys = string> extends FilterProps {
|
||||
filterMenu: IFilter<TKeys>;
|
||||
export interface FilterBarProps<TKeys extends string = string>
|
||||
extends FilterProps<TKeys>,
|
||||
SearchBarProps {
|
||||
filterStructure: IFilter<TKeys>;
|
||||
}
|
||||
|
||||
const FilterBar: React.FC<FilterBarProps> = ({
|
||||
allTabLabel,
|
||||
currencySymbol,
|
||||
filterLabel,
|
||||
filtersList,
|
||||
filterMenu,
|
||||
currentTab,
|
||||
initialSearch,
|
||||
searchPlaceholder,
|
||||
tabs,
|
||||
onAll,
|
||||
onSearchChange,
|
||||
onFilterAdd,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
onTabSave
|
||||
}) => {
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
padding: theme.spacing(1, 3)
|
||||
},
|
||||
tabActions: {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
padding: theme.spacing(1, 3, 2),
|
||||
textAlign: "right"
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "FilterBar"
|
||||
}
|
||||
);
|
||||
|
||||
const FilterBar: React.FC<FilterBarProps> = props => {
|
||||
const {
|
||||
allTabLabel,
|
||||
currencySymbol,
|
||||
filterStructure,
|
||||
currentTab,
|
||||
initialSearch,
|
||||
searchPlaceholder,
|
||||
tabs,
|
||||
onAll,
|
||||
onSearchChange,
|
||||
onFilterChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
onTabSave
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
const [search, setSearch] = React.useState(initialSearch);
|
||||
React.useEffect(() => setSearch(initialSearch), [currentTab, initialSearch]);
|
||||
|
||||
const isCustom = currentTab === tabs.length + 1;
|
||||
const displayTabAction = isCustom
|
||||
? "save"
|
||||
: currentTab === 0
|
||||
? null
|
||||
: "delete";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -53,34 +82,41 @@ const FilterBar: React.FC<FilterBarProps> = ({
|
|||
/>
|
||||
)}
|
||||
</FilterTabs>
|
||||
<Debounce debounceFn={onSearchChange}>
|
||||
{debounceSearchChange => {
|
||||
const handleSearchChange = (event: React.ChangeEvent<any>) => {
|
||||
const value = event.target.value;
|
||||
setSearch(value);
|
||||
debounceSearchChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<FilterChips
|
||||
currencySymbol={currencySymbol}
|
||||
displayTabAction={
|
||||
!!initialSearch ? (isCustom ? "save" : "delete") : null
|
||||
}
|
||||
menu={filterMenu}
|
||||
filtersList={filtersList}
|
||||
filterLabel={filterLabel}
|
||||
placeholder={searchPlaceholder}
|
||||
search={search}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterAdd={onFilterAdd}
|
||||
onFilterSave={onTabSave}
|
||||
isCustomSearch={isCustom}
|
||||
onFilterDelete={onTabDelete}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Debounce>
|
||||
<div className={classes.root}>
|
||||
<Filter
|
||||
currencySymbol={currencySymbol}
|
||||
menu={filterStructure}
|
||||
onFilterAdd={onFilterChange}
|
||||
/>
|
||||
<SearchInput
|
||||
initialSearch={initialSearch}
|
||||
placeholder={searchPlaceholder}
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
</div>
|
||||
{displayTabAction === null ? (
|
||||
<Hr />
|
||||
) : (
|
||||
<div className={classes.tabActions}>
|
||||
{displayTabAction === "save" ? (
|
||||
<Link onClick={onTabSave}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Custom Search"
|
||||
description="button"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
displayTabAction === "delete" && (
|
||||
<Link onClick={onTabDelete}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete Search"
|
||||
description="button"
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
|
||||
import { SearchPageProps, TabPageProps } from "@saleor/types";
|
||||
import FilterTabs, { FilterTab } from "../TableFilter";
|
||||
import Link from "../Link";
|
||||
import Hr from "../Hr";
|
||||
import SearchInput from "./SearchInput";
|
||||
|
||||
export interface SearchBarProps extends SearchPageProps, TabPageProps {
|
||||
|
@ -10,6 +13,24 @@ export interface SearchBarProps extends SearchPageProps, TabPageProps {
|
|||
searchPlaceholder: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
padding: theme.spacing(1, 3)
|
||||
},
|
||||
tabActions: {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
padding: theme.spacing(1, 3, 2),
|
||||
textAlign: "right"
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "SearchBar"
|
||||
}
|
||||
);
|
||||
|
||||
const SearchBar: React.FC<SearchBarProps> = props => {
|
||||
const {
|
||||
allTabLabel,
|
||||
|
@ -23,9 +44,16 @@ const SearchBar: React.FC<SearchBarProps> = props => {
|
|||
onTabDelete,
|
||||
onTabSave
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
const isCustom = currentTab === tabs.length + 1;
|
||||
const displayTabAction = isCustom
|
||||
? "save"
|
||||
: currentTab === 0
|
||||
? null
|
||||
: "delete";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -47,16 +75,36 @@ const SearchBar: React.FC<SearchBarProps> = props => {
|
|||
/>
|
||||
)}
|
||||
</FilterTabs>
|
||||
<SearchInput
|
||||
displaySearchAction={
|
||||
!!initialSearch ? (isCustom ? "save" : "delete") : null
|
||||
}
|
||||
initialSearch={initialSearch}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
onSearchChange={onSearchChange}
|
||||
onSearchDelete={onTabDelete}
|
||||
onSearchSave={onTabSave}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
<SearchInput
|
||||
initialSearch={initialSearch}
|
||||
placeholder={searchPlaceholder}
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
</div>
|
||||
{displayTabAction === null ? (
|
||||
<Hr />
|
||||
) : (
|
||||
<div className={classes.tabActions}>
|
||||
{displayTabAction === "save" ? (
|
||||
<Link onClick={onTabSave}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Custom Search"
|
||||
description="button"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
displayTabAction === "delete" && (
|
||||
<Link onClick={onTabDelete}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete Search"
|
||||
description="button"
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,47 +1,31 @@
|
|||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
|
||||
import { SearchPageProps } from "../../types";
|
||||
import Debounce from "../Debounce";
|
||||
import { FilterActionsOnlySearch } from "../Filter/FilterActions";
|
||||
import Hr from "../Hr";
|
||||
import Link from "../Link";
|
||||
|
||||
export interface SearchInputProps extends SearchPageProps {
|
||||
displaySearchAction: "save" | "delete" | null;
|
||||
searchPlaceholder: string;
|
||||
onSearchDelete?: () => void;
|
||||
onSearchSave?: () => void;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
tabAction: {
|
||||
display: "inline-block"
|
||||
{
|
||||
input: {
|
||||
padding: "10.5px 12px"
|
||||
},
|
||||
tabActionContainer: {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
marginTop: theme.spacing(),
|
||||
padding: theme.spacing(0, 1, 3, 1)
|
||||
root: {
|
||||
flex: 1
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "SearchInput"
|
||||
}
|
||||
);
|
||||
|
||||
const SearchInput: React.FC<SearchInputProps> = props => {
|
||||
const {
|
||||
displaySearchAction,
|
||||
initialSearch,
|
||||
onSearchChange,
|
||||
onSearchDelete,
|
||||
onSearchSave,
|
||||
searchPlaceholder
|
||||
} = props;
|
||||
const { initialSearch, onSearchChange, placeholder } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const [search, setSearch] = React.useState(initialSearch);
|
||||
React.useEffect(() => setSearch(initialSearch), [initialSearch]);
|
||||
|
@ -56,37 +40,15 @@ const SearchInput: React.FC<SearchInputProps> = props => {
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterActionsOnlySearch
|
||||
{...props}
|
||||
placeholder={searchPlaceholder}
|
||||
search={search}
|
||||
onSearchChange={handleSearchChange}
|
||||
/>
|
||||
{!!displaySearchAction ? (
|
||||
<div className={classes.tabActionContainer}>
|
||||
<div className={classes.tabAction}>
|
||||
{displaySearchAction === "save" ? (
|
||||
<Link onClick={onSearchSave}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Custom Search"
|
||||
description="button"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<Link onClick={onSearchDelete}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete Search"
|
||||
description="button"
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Hr />
|
||||
)}
|
||||
</>
|
||||
<TextField
|
||||
className={classes.root}
|
||||
inputProps={{
|
||||
className: classes.input,
|
||||
placeholder
|
||||
}}
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Debounce>
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
import ButtonBase from "@material-ui/core/ButtonBase";
|
||||
import { makeStyles, useTheme } from "@material-ui/core/styles";
|
||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import ClearIcon from "@material-ui/icons/Clear";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import FilterActions, { FilterActionsProps } from "../Filter/FilterActions";
|
||||
import Hr from "../Hr";
|
||||
import Link from "../Link";
|
||||
|
||||
export interface Filter {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
filterButton: {
|
||||
alignItems: "center",
|
||||
backgroundColor: fade(theme.palette.primary.main, 0.8),
|
||||
borderRadius: "19px",
|
||||
display: "flex",
|
||||
height: "38px",
|
||||
justifyContent: "space-around",
|
||||
margin: theme.spacing(0, 1, 2),
|
||||
marginLeft: 0,
|
||||
padding: theme.spacing(0, 2)
|
||||
},
|
||||
filterChipContainer: {
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
flexWrap: "wrap",
|
||||
paddingTop: theme.spacing(2)
|
||||
},
|
||||
filterContainer: {
|
||||
"& a": {
|
||||
paddingBottom: 10,
|
||||
paddingTop: theme.spacing(1)
|
||||
},
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
marginTop: -theme.spacing(1),
|
||||
padding: theme.spacing(0, 2)
|
||||
},
|
||||
filterIcon: {
|
||||
color: theme.palette.common.white,
|
||||
height: 16,
|
||||
width: 16
|
||||
},
|
||||
filterIconContainer: {
|
||||
WebkitAppearance: "none",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
borderRadius: "100%",
|
||||
cursor: "pointer",
|
||||
height: 32,
|
||||
marginRight: -13,
|
||||
padding: 8,
|
||||
width: 32
|
||||
},
|
||||
filterLabel: {
|
||||
marginBottom: theme.spacing(1)
|
||||
},
|
||||
filterText: {
|
||||
color: theme.palette.common.white,
|
||||
fontSize: 14,
|
||||
fontWeight: 400 as 400,
|
||||
lineHeight: "38px"
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "FilterChips"
|
||||
}
|
||||
);
|
||||
|
||||
interface FilterChipProps extends FilterActionsProps {
|
||||
displayTabAction: "save" | "delete" | null;
|
||||
filtersList: Filter[];
|
||||
search: string;
|
||||
isCustomSearch: boolean;
|
||||
onFilterDelete: () => void;
|
||||
onFilterSave: () => void;
|
||||
}
|
||||
|
||||
export const FilterChips: React.FC<FilterChipProps> = ({
|
||||
currencySymbol,
|
||||
displayTabAction,
|
||||
filtersList,
|
||||
menu,
|
||||
filterLabel,
|
||||
placeholder,
|
||||
onSearchChange,
|
||||
search,
|
||||
onFilterAdd,
|
||||
onFilterSave,
|
||||
onFilterDelete
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const classes = useStyles({ theme });
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterActions
|
||||
currencySymbol={currencySymbol}
|
||||
menu={menu}
|
||||
filterLabel={filterLabel}
|
||||
placeholder={placeholder}
|
||||
search={search}
|
||||
onSearchChange={onSearchChange}
|
||||
onFilterAdd={onFilterAdd}
|
||||
/>
|
||||
{search || (filtersList && filtersList.length > 0) ? (
|
||||
<div className={classes.filterContainer}>
|
||||
<div className={classes.filterChipContainer}>
|
||||
{filtersList.map(filter => (
|
||||
<div className={classes.filterButton} key={filter.label}>
|
||||
<Typography className={classes.filterText}>
|
||||
{filter.label}
|
||||
</Typography>
|
||||
<ButtonBase
|
||||
className={classes.filterIconContainer}
|
||||
onClick={filter.onClick}
|
||||
>
|
||||
<ClearIcon className={classes.filterIcon} />
|
||||
</ButtonBase>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{displayTabAction === "save" ? (
|
||||
<Link onClick={onFilterSave}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Custom Search"
|
||||
description="button"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
displayTabAction === "delete" && (
|
||||
<Link onClick={onFilterDelete}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete Search"
|
||||
description="button"
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Hr />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterChips;
|
|
@ -1,4 +1,3 @@
|
|||
export { default } from "./FilterTabs";
|
||||
export * from "./FilterTabs";
|
||||
export * from "./FilterTab";
|
||||
export * from "./FilterChips";
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { FieldType, IFilter } from "@saleor/components/Filter";
|
||||
import FilterBar from "@saleor/components/FilterBar";
|
||||
import { FilterProps } from "@saleor/types";
|
||||
import { StockAvailability } from "@saleor/types/globalTypes";
|
||||
|
||||
type ProductListFilterProps = Omit<
|
||||
FilterProps,
|
||||
"allTabLabel" | "filterLabel" | "searchPlaceholder"
|
||||
>;
|
||||
|
||||
export enum ProductFilterKeys {
|
||||
published = "published",
|
||||
price = "price",
|
||||
priceEqual = "priceEqual",
|
||||
priceRange = "priceRange",
|
||||
stock = "stock"
|
||||
}
|
||||
|
||||
const ProductListFilter: React.FC<ProductListFilterProps> = props => {
|
||||
const intl = useIntl();
|
||||
|
||||
const filterMenu: IFilter<ProductFilterKeys> = [
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
additionalText: intl.formatMessage({
|
||||
defaultMessage: "is set as",
|
||||
description: "product status is set as"
|
||||
}),
|
||||
fieldLabel: intl.formatMessage({
|
||||
defaultMessage: "Status",
|
||||
description: "product status"
|
||||
}),
|
||||
options: [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Visible",
|
||||
description: "product is visible"
|
||||
}),
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Hidden",
|
||||
description: "product is hidden"
|
||||
}),
|
||||
value: false
|
||||
}
|
||||
],
|
||||
type: FieldType.select
|
||||
},
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Visibility",
|
||||
description: "product visibility"
|
||||
}),
|
||||
value: ProductFilterKeys.published
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
fieldLabel: intl.formatMessage({
|
||||
defaultMessage: "Stock quantity"
|
||||
}),
|
||||
options: [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Available",
|
||||
description: "product status"
|
||||
}),
|
||||
value: StockAvailability.IN_STOCK
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Out Of Stock",
|
||||
description: "product status"
|
||||
}),
|
||||
value: StockAvailability.OUT_OF_STOCK
|
||||
}
|
||||
],
|
||||
type: FieldType.select
|
||||
},
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Stock",
|
||||
description: "product stock"
|
||||
}),
|
||||
value: ProductFilterKeys.stock
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
additionalText: intl.formatMessage({
|
||||
defaultMessage: "equals",
|
||||
description: "product price"
|
||||
}),
|
||||
fieldLabel: null,
|
||||
type: FieldType.price
|
||||
},
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Specific Price"
|
||||
}),
|
||||
value: ProductFilterKeys.priceEqual
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
fieldLabel: intl.formatMessage({
|
||||
defaultMessage: "Range"
|
||||
}),
|
||||
type: FieldType.rangePrice
|
||||
},
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Range"
|
||||
}),
|
||||
value: ProductFilterKeys.priceRange
|
||||
}
|
||||
],
|
||||
data: {
|
||||
fieldLabel: intl.formatMessage({
|
||||
defaultMessage: "Price"
|
||||
}),
|
||||
type: FieldType.range
|
||||
},
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Price"
|
||||
}),
|
||||
value: ProductFilterKeys.price
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<FilterBar
|
||||
{...props}
|
||||
allTabLabel={intl.formatMessage({
|
||||
defaultMessage: "All Products",
|
||||
description: "tab name"
|
||||
})}
|
||||
filterMenu={filterMenu}
|
||||
filterLabel={intl.formatMessage({
|
||||
defaultMessage: "Select all products where:"
|
||||
})}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
defaultMessage: "Search Products..."
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
ProductListFilter.displayName = "ProductListFilter";
|
||||
export default ProductListFilter;
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from "./ProductListFilter";
|
||||
export * from "./ProductListFilter";
|
|
@ -23,9 +23,13 @@ import {
|
|||
PageListProps,
|
||||
SortPage
|
||||
} from "@saleor/types";
|
||||
import FilterBar from "@saleor/components/FilterBar";
|
||||
import {
|
||||
createFilterStructure,
|
||||
ProductFilterKeys
|
||||
} from "@saleor/products/views/ProductList/filters";
|
||||
import { ProductListUrlSortField } from "../../urls";
|
||||
import ProductList from "../ProductList";
|
||||
import ProductListFilter, { ProductFilterKeys } from "../ProductListFilter";
|
||||
|
||||
export interface ProductListPageProps
|
||||
extends PageListProps<ProductListColumns>,
|
||||
|
@ -55,7 +59,6 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
currencySymbol,
|
||||
currentTab,
|
||||
defaultSettings,
|
||||
filtersList,
|
||||
gridAttributes,
|
||||
availableInGridAttributes,
|
||||
hasMore,
|
||||
|
@ -67,8 +70,8 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
onAdd,
|
||||
onAll,
|
||||
onFetchMore,
|
||||
onFilterChange,
|
||||
onSearchChange,
|
||||
onFilterAdd,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
onTabSave,
|
||||
|
@ -81,6 +84,8 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
const handleSave = (columns: ProductListColumns[]) =>
|
||||
onUpdateListSettings("columns", columns);
|
||||
|
||||
const filterStructure = createFilterStructure(intl);
|
||||
|
||||
const columns: ColumnPickerChoice[] = [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
|
@ -140,18 +145,28 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
</Button>
|
||||
</PageHeader>
|
||||
<Card>
|
||||
<ProductListFilter
|
||||
<FilterBar
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filtersList={filtersList}
|
||||
initialSearch={initialSearch}
|
||||
onAll={onAll}
|
||||
onFilterAdd={onFilterAdd}
|
||||
onFilterChange={onFilterChange}
|
||||
onSearchChange={onSearchChange}
|
||||
onTabChange={onTabChange}
|
||||
onTabDelete={onTabDelete}
|
||||
onTabSave={onTabSave}
|
||||
tabs={tabs}
|
||||
allTabLabel={intl.formatMessage({
|
||||
defaultMessage: "All Products",
|
||||
description: "tab name"
|
||||
})}
|
||||
filterStructure={filterStructure}
|
||||
filterLabel={intl.formatMessage({
|
||||
defaultMessage: "Select all products where:"
|
||||
})}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
defaultMessage: "Search Products..."
|
||||
})}
|
||||
/>
|
||||
<ProductList
|
||||
{...listProps}
|
||||
|
|
|
@ -24,10 +24,10 @@ export type ProductListUrlDialog =
|
|||
| "delete"
|
||||
| TabActionDialog;
|
||||
export enum ProductListUrlFiltersEnum {
|
||||
isPublished = "isPublished",
|
||||
priceFrom = "priceFrom",
|
||||
priceTo = "priceTo",
|
||||
status = "status",
|
||||
stockStatus = "stockStatus",
|
||||
query = "query"
|
||||
}
|
||||
export type ProductListUrlFilters = Filters<ProductListUrlFiltersEnum>;
|
||||
|
|
|
@ -13,7 +13,6 @@ import SaveFilterTabDialog, {
|
|||
import { defaultListSettings, ProductListColumns } from "@saleor/config";
|
||||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||
import useListSettings from "@saleor/hooks/useListSettings";
|
||||
import useLocale from "@saleor/hooks/useLocale";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import usePaginator, {
|
||||
|
@ -26,6 +25,7 @@ import { ProductListVariables } from "@saleor/products/types/ProductList";
|
|||
import { ListViews } from "@saleor/types";
|
||||
import { getSortUrlVariables } from "@saleor/utils/sort";
|
||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
import { IFilter } from "@saleor/components/Filter";
|
||||
import ProductListPage from "../../components/ProductListPage";
|
||||
import {
|
||||
TypedProductBulkDeleteMutation,
|
||||
|
@ -48,13 +48,13 @@ import {
|
|||
} from "../../urls";
|
||||
import {
|
||||
areFiltersApplied,
|
||||
createFilter,
|
||||
createFilterChips,
|
||||
deleteFilterTab,
|
||||
getActiveFilters,
|
||||
getFilterTabs,
|
||||
getFilterVariables,
|
||||
saveFilterTab
|
||||
saveFilterTab,
|
||||
ProductFilterKeys,
|
||||
createFilterQueryParams
|
||||
} from "./filters";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
|
||||
|
@ -63,7 +63,6 @@ interface ProductListProps {
|
|||
}
|
||||
|
||||
export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||
const { locale } = useLocale();
|
||||
const navigate = useNavigator();
|
||||
const notify = useNotifier();
|
||||
const paginate = usePaginator();
|
||||
|
@ -108,17 +107,28 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
navigate(productListUrl(filters));
|
||||
};
|
||||
|
||||
const changeFilterField = (filter: ProductListUrlFilters) => {
|
||||
const changeFilterField = (filter: IFilter<ProductFilterKeys>) => {
|
||||
reset();
|
||||
navigate(
|
||||
productListUrl({
|
||||
...getActiveFilters(params),
|
||||
...filter,
|
||||
...params,
|
||||
...createFilterQueryParams(filter),
|
||||
activeTab: undefined
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSearchChange = (query: string) => {
|
||||
reset();
|
||||
navigate(
|
||||
productListUrl({
|
||||
...params,
|
||||
activeTab: undefined,
|
||||
query
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleTabChange = (tab: number) => {
|
||||
reset();
|
||||
navigate(
|
||||
|
@ -241,15 +251,6 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
.hasNextPage,
|
||||
false
|
||||
)}
|
||||
filtersList={createFilterChips(
|
||||
params,
|
||||
{
|
||||
currencySymbol,
|
||||
locale
|
||||
},
|
||||
changeFilterField,
|
||||
intl
|
||||
)}
|
||||
onAdd={() => navigate(productAddUrl)}
|
||||
disabled={loading}
|
||||
products={maybe(() =>
|
||||
|
@ -337,10 +338,8 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
selected={listElements.length}
|
||||
toggle={toggle}
|
||||
toggleAll={toggleAll}
|
||||
onSearchChange={query => changeFilterField({ query })}
|
||||
onFilterAdd={filter =>
|
||||
changeFilterField(createFilter(filter))
|
||||
}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={filter => changeFilterField(filter)}
|
||||
onTabSave={() => openModal("save-search")}
|
||||
onTabDelete={() => openModal("delete-search")}
|
||||
onTabChange={handleTabChange}
|
||||
|
|
|
@ -1,76 +1,12 @@
|
|||
import { createIntl } from "react-intl";
|
||||
|
||||
import { ProductFilterKeys } from "@saleor/products/components/ProductListFilter";
|
||||
import { StockAvailability } from "@saleor/types/globalTypes";
|
||||
import { createFilter, createFilterChips, getFilterVariables } from "./filters";
|
||||
|
||||
const mockIntl = createIntl({
|
||||
locale: "en"
|
||||
});
|
||||
|
||||
describe("Create filter object", () => {
|
||||
it("with price", () => {
|
||||
const filter = createFilter({
|
||||
name: ProductFilterKeys.priceEqual,
|
||||
value: "10"
|
||||
});
|
||||
|
||||
expect(filter).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("with price range", () => {
|
||||
const filter = createFilter({
|
||||
name: ProductFilterKeys.priceEqual,
|
||||
value: ["10", "20"]
|
||||
});
|
||||
|
||||
expect(filter).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("with publication status", () => {
|
||||
const filter = createFilter({
|
||||
name: ProductFilterKeys.published,
|
||||
value: "false"
|
||||
});
|
||||
|
||||
expect(filter).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("with stock status", () => {
|
||||
const filter = createFilter({
|
||||
name: ProductFilterKeys.stock,
|
||||
value: StockAvailability.OUT_OF_STOCK
|
||||
});
|
||||
|
||||
expect(filter).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test("Crate filter chips", () => {
|
||||
const chips = createFilterChips(
|
||||
{
|
||||
isPublished: "true",
|
||||
priceFrom: "10",
|
||||
priceTo: "20",
|
||||
status: StockAvailability.IN_STOCK
|
||||
},
|
||||
{
|
||||
currencySymbol: "USD",
|
||||
locale: "en"
|
||||
},
|
||||
jest.fn(),
|
||||
mockIntl as any
|
||||
);
|
||||
|
||||
expect(chips).toMatchSnapshot();
|
||||
});
|
||||
import { getFilterVariables } from "./filters";
|
||||
|
||||
test("Get filter variables", () => {
|
||||
const filter = getFilterVariables({
|
||||
isPublished: "true",
|
||||
priceFrom: "10",
|
||||
priceTo: "20",
|
||||
status: StockAvailability.IN_STOCK
|
||||
status: "true",
|
||||
stockStatus: StockAvailability.IN_STOCK
|
||||
});
|
||||
|
||||
expect(filter).toMatchSnapshot();
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { defineMessages, IntlShape } from "react-intl";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
import { FilterContentSubmitData } from "../../../components/Filter";
|
||||
import { Filter } from "../../../components/TableFilter";
|
||||
import { findInEnum, maybe } from "@saleor/misc";
|
||||
import {
|
||||
createOptionsField,
|
||||
createPriceField
|
||||
} from "@saleor/utils/filters/fields";
|
||||
import { IFilterElement, IFilter } from "../../../components/Filter";
|
||||
import {
|
||||
ProductFilterInput,
|
||||
StockAvailability
|
||||
|
@ -10,204 +14,141 @@ import {
|
|||
createFilterTabUtils,
|
||||
createFilterUtils
|
||||
} from "../../../utils/filters";
|
||||
import { ProductFilterKeys } from "../../components/ProductListFilter";
|
||||
import {
|
||||
ProductListUrlFilters,
|
||||
ProductListUrlFiltersEnum,
|
||||
ProductListUrlQueryParams
|
||||
} from "../../urls";
|
||||
import messages from "./messages";
|
||||
|
||||
export const PRODUCT_FILTERS_KEY = "productFilters";
|
||||
|
||||
export enum ProductFilterKeys {
|
||||
status = "status",
|
||||
price = "price",
|
||||
stock = "stock"
|
||||
}
|
||||
|
||||
export enum ProductStatus {
|
||||
PUBLISHED = "published",
|
||||
HIDDEN = "hidden"
|
||||
}
|
||||
|
||||
export function createFilterStructure(
|
||||
intl: IntlShape,
|
||||
params: ProductListUrlFilters
|
||||
): IFilter<ProductFilterKeys> {
|
||||
return [
|
||||
{
|
||||
...createOptionsField(
|
||||
ProductFilterKeys.status,
|
||||
intl.formatMessage(messages.visibility),
|
||||
[ProductStatus.PUBLISHED],
|
||||
false,
|
||||
[
|
||||
{
|
||||
label: intl.formatMessage(messages.visible),
|
||||
value: ProductStatus.PUBLISHED
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(messages.hidden),
|
||||
value: ProductStatus.HIDDEN
|
||||
}
|
||||
]
|
||||
),
|
||||
active: maybe(() => params.status !== undefined, false)
|
||||
},
|
||||
{
|
||||
...createOptionsField(
|
||||
ProductFilterKeys.stock,
|
||||
intl.formatMessage(messages.quantity),
|
||||
[StockAvailability.IN_STOCK],
|
||||
false,
|
||||
[
|
||||
{
|
||||
label: intl.formatMessage(messages.available),
|
||||
value: StockAvailability.IN_STOCK
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(messages.outOfStock),
|
||||
value: StockAvailability.OUT_OF_STOCK
|
||||
}
|
||||
]
|
||||
),
|
||||
active: maybe(() => params.stockStatus !== undefined, false)
|
||||
},
|
||||
{
|
||||
...createPriceField(
|
||||
ProductFilterKeys.price,
|
||||
intl.formatMessage(messages.price),
|
||||
{
|
||||
max: maybe(() => params.priceTo, "0"),
|
||||
min: maybe(() => params.priceFrom, "0")
|
||||
}
|
||||
),
|
||||
active: maybe(
|
||||
() =>
|
||||
[params.priceFrom, params.priceTo].some(field => field !== undefined),
|
||||
false
|
||||
)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export function getFilterVariables(
|
||||
params: ProductListUrlFilters
|
||||
): ProductFilterInput {
|
||||
return {
|
||||
isPublished:
|
||||
params.isPublished !== undefined ? params.isPublished === "true" : null,
|
||||
params.status !== undefined
|
||||
? params.status === ProductStatus.PUBLISHED
|
||||
: null,
|
||||
price: {
|
||||
gte: parseFloat(params.priceFrom),
|
||||
lte: parseFloat(params.priceTo)
|
||||
},
|
||||
search: params.query,
|
||||
stockAvailability: StockAvailability[params.status]
|
||||
stockAvailability: StockAvailability[params.stockStatus]
|
||||
};
|
||||
}
|
||||
|
||||
export function createFilter(
|
||||
filter: FilterContentSubmitData<ProductFilterKeys>
|
||||
export function getFilterQueryParam(
|
||||
filter: IFilterElement<ProductFilterKeys>
|
||||
): ProductListUrlFilters {
|
||||
const filterName = filter.name;
|
||||
if (filterName === ProductFilterKeys.priceEqual) {
|
||||
const value = filter.value as string;
|
||||
return {
|
||||
priceFrom: value,
|
||||
priceTo: value
|
||||
};
|
||||
} else if (filterName === ProductFilterKeys.priceRange) {
|
||||
const { value } = filter;
|
||||
return {
|
||||
priceFrom: value[0],
|
||||
priceTo: value[1]
|
||||
};
|
||||
} else if (filterName === ProductFilterKeys.published) {
|
||||
return {
|
||||
isPublished: filter.value as string
|
||||
};
|
||||
} else if (filterName === ProductFilterKeys.stock) {
|
||||
const value = filter.value as string;
|
||||
return {
|
||||
status: StockAvailability[value]
|
||||
};
|
||||
}
|
||||
}
|
||||
const { active, name, value } = filter;
|
||||
|
||||
const filterMessages = defineMessages({
|
||||
available: {
|
||||
defaultMessage: "Available",
|
||||
description: "filter products by stock"
|
||||
},
|
||||
hidden: {
|
||||
defaultMessage: "Hidden",
|
||||
description: "filter products by visibility"
|
||||
},
|
||||
outOfStock: {
|
||||
defaultMessage: "Out of stock",
|
||||
description: "filter products by stock"
|
||||
},
|
||||
priceFrom: {
|
||||
defaultMessage: "Price from {price}",
|
||||
description: "filter by price"
|
||||
},
|
||||
priceIs: {
|
||||
defaultMessage: "Price is {price}",
|
||||
description: "filter by price"
|
||||
},
|
||||
priceTo: {
|
||||
defaultMessage: "Price to {price}",
|
||||
description: "filter by price"
|
||||
},
|
||||
published: {
|
||||
defaultMessage: "Published",
|
||||
description: "filter products by visibility"
|
||||
}
|
||||
});
|
||||
if (active) {
|
||||
switch (name) {
|
||||
case ProductFilterKeys.price:
|
||||
return {
|
||||
priceFrom: value[0],
|
||||
priceTo: value[1]
|
||||
};
|
||||
|
||||
interface ProductListChipFormatData {
|
||||
currencySymbol: string;
|
||||
locale: string;
|
||||
}
|
||||
export function createFilterChips(
|
||||
filters: ProductListUrlFilters,
|
||||
formatData: ProductListChipFormatData,
|
||||
onFilterDelete: (filters: ProductListUrlFilters) => void,
|
||||
intl: IntlShape
|
||||
): Filter[] {
|
||||
let filterChips: Filter[] = [];
|
||||
case ProductFilterKeys.status:
|
||||
return {
|
||||
status: (
|
||||
findInEnum(value[0], ProductStatus) === ProductStatus.PUBLISHED
|
||||
).toString()
|
||||
};
|
||||
|
||||
if (!!filters.priceFrom || !!filters.priceTo) {
|
||||
if (filters.priceFrom === filters.priceTo) {
|
||||
filterChips = [
|
||||
...filterChips,
|
||||
{
|
||||
label: intl.formatMessage(filterMessages.priceIs, {
|
||||
price: parseFloat(filters.priceFrom).toLocaleString(
|
||||
formatData.locale,
|
||||
{
|
||||
currency: formatData.currencySymbol,
|
||||
style: "currency"
|
||||
}
|
||||
)
|
||||
}),
|
||||
onClick: () =>
|
||||
onFilterDelete({
|
||||
...filters,
|
||||
priceFrom: undefined,
|
||||
priceTo: undefined
|
||||
})
|
||||
}
|
||||
];
|
||||
} else {
|
||||
if (!!filters.priceFrom) {
|
||||
filterChips = [
|
||||
...filterChips,
|
||||
{
|
||||
label: intl.formatMessage(filterMessages.priceFrom, {
|
||||
price: parseFloat(filters.priceFrom).toLocaleString(
|
||||
formatData.locale,
|
||||
{
|
||||
currency: formatData.currencySymbol,
|
||||
style: "currency"
|
||||
}
|
||||
)
|
||||
}),
|
||||
onClick: () =>
|
||||
onFilterDelete({
|
||||
...filters,
|
||||
priceFrom: undefined
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (!!filters.priceTo) {
|
||||
filterChips = [
|
||||
...filterChips,
|
||||
{
|
||||
label: intl.formatMessage(filterMessages.priceTo, {
|
||||
price: parseFloat(filters.priceTo).toLocaleString(
|
||||
formatData.locale,
|
||||
{
|
||||
currency: formatData.currencySymbol,
|
||||
style: "currency"
|
||||
}
|
||||
)
|
||||
}),
|
||||
onClick: () =>
|
||||
onFilterDelete({
|
||||
...filters,
|
||||
priceTo: undefined
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
case ProductFilterKeys.stock:
|
||||
return {
|
||||
status: findInEnum(value[0], StockAvailability)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!!filters.status) {
|
||||
filterChips = [
|
||||
...filterChips,
|
||||
{
|
||||
label:
|
||||
filters.status === StockAvailability.IN_STOCK.toString()
|
||||
? intl.formatMessage(filterMessages.available)
|
||||
: intl.formatMessage(filterMessages.outOfStock),
|
||||
onClick: () =>
|
||||
onFilterDelete({
|
||||
...filters,
|
||||
status: undefined
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (!!filters.isPublished) {
|
||||
filterChips = [
|
||||
...filterChips,
|
||||
{
|
||||
label: !!filters.isPublished
|
||||
? intl.formatMessage(filterMessages.published)
|
||||
: intl.formatMessage(filterMessages.hidden),
|
||||
onClick: () =>
|
||||
onFilterDelete({
|
||||
...filters,
|
||||
isPublished: undefined
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return filterChips;
|
||||
}
|
||||
export function createFilterQueryParams(
|
||||
filter: IFilter<ProductFilterKeys>
|
||||
): ProductListUrlFilters {
|
||||
return filter.reduce(
|
||||
(acc, filterField) => ({
|
||||
...acc,
|
||||
...getFilterQueryParam(filterField)
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export const {
|
||||
|
|
33
src/products/views/ProductList/messages.ts
Normal file
33
src/products/views/ProductList/messages.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
const messages = defineMessages({
|
||||
available: {
|
||||
defaultMessage: "Available",
|
||||
description: "product status"
|
||||
},
|
||||
hidden: {
|
||||
defaultMessage: "Hidden",
|
||||
description: "product is hidden"
|
||||
},
|
||||
outOfStock: {
|
||||
defaultMessage: "Out Of Stock",
|
||||
description: "product status"
|
||||
},
|
||||
price: {
|
||||
defaultMessage: "Price"
|
||||
},
|
||||
quantity: {
|
||||
defaultMessage: "Stock quantity",
|
||||
description: "product"
|
||||
},
|
||||
visibility: {
|
||||
defaultMessage: "Visibility",
|
||||
description: "product visibility"
|
||||
},
|
||||
visible: {
|
||||
defaultMessage: "Visible",
|
||||
description: "product is visible"
|
||||
}
|
||||
});
|
||||
|
||||
export default messages;
|
|
@ -1,6 +1,7 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import React from "react";
|
||||
import { IntlShape, useIntl } from "react-intl";
|
||||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
|
||||
import AppHeader from "@saleor/components/AppHeader";
|
||||
import Container from "@saleor/components/Container";
|
||||
|
@ -30,6 +31,24 @@ export interface TranslationsEntitiesFilters {
|
|||
onProductTypesTabClick: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
padding: theme.spacing(1, 3)
|
||||
},
|
||||
tabActions: {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
padding: theme.spacing(1, 3, 2),
|
||||
textAlign: "right"
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "FilterActions"
|
||||
}
|
||||
);
|
||||
|
||||
export type TranslationsEntitiesListFilterTab = keyof typeof TranslatableEntities;
|
||||
|
||||
function getSearchPlaceholder(
|
||||
|
@ -87,13 +106,10 @@ const tabs: TranslationsEntitiesListFilterTab[] = [
|
|||
"productTypes"
|
||||
];
|
||||
|
||||
const TranslationsEntitiesListPage: React.FC<TranslationsEntitiesListPageProps> = ({
|
||||
filters,
|
||||
language,
|
||||
onBack,
|
||||
children,
|
||||
...searchProps
|
||||
}) => {
|
||||
const TranslationsEntitiesListPage: React.FC<TranslationsEntitiesListPageProps> = props => {
|
||||
const { filters, language, onBack, children, ...searchProps } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
const currentTab = tabs.indexOf(filters.current);
|
||||
|
||||
|
@ -160,11 +176,12 @@ const TranslationsEntitiesListPage: React.FC<TranslationsEntitiesListPageProps>
|
|||
onClick={filters.onProductTypesTabClick}
|
||||
/>
|
||||
</FilterTabs>
|
||||
<SearchInput
|
||||
displaySearchAction={null}
|
||||
searchPlaceholder={getSearchPlaceholder(filters.current, intl)}
|
||||
{...searchProps}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
<SearchInput
|
||||
placeholder={getSearchPlaceholder(filters.current, intl)}
|
||||
{...searchProps}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</Card>
|
||||
</Container>
|
||||
|
|
13
src/types.ts
13
src/types.ts
|
@ -1,9 +1,8 @@
|
|||
import { MutationResult } from "react-apollo";
|
||||
|
||||
import { User_permissions } from "./auth/types/User";
|
||||
import { FilterContentSubmitData } from "./components/Filter";
|
||||
import { Filter } from "./components/TableFilter";
|
||||
import { ConfirmButtonTransitionState } from "./components/ConfirmButton";
|
||||
import { IFilter } from "./components/Filter";
|
||||
|
||||
export interface UserError {
|
||||
field: string;
|
||||
|
@ -83,22 +82,20 @@ export interface SearchPageProps {
|
|||
initialSearch: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
}
|
||||
export interface FilterPageProps<TKeys = string>
|
||||
export interface FilterPageProps<TKeys extends string>
|
||||
extends SearchPageProps,
|
||||
TabPageProps {
|
||||
currencySymbol: string;
|
||||
filtersList: Filter[];
|
||||
onFilterAdd: (filter: FilterContentSubmitData<TKeys>) => void;
|
||||
onFilterChange: (filter: IFilter<TKeys>) => void;
|
||||
}
|
||||
|
||||
export interface SearchProps {
|
||||
searchPlaceholder: string;
|
||||
}
|
||||
export interface FilterProps<TKeys = string>
|
||||
export interface FilterProps<TKeys extends string>
|
||||
extends FilterPageProps<TKeys>,
|
||||
SearchProps {
|
||||
allTabLabel: string;
|
||||
filterLabel: string;
|
||||
currencySymbol: string;
|
||||
}
|
||||
|
||||
export interface TabPageProps {
|
||||
|
|
|
@ -6,14 +6,12 @@ type MinMax = Record<"min" | "max", string>;
|
|||
export function createPriceField<T extends string>(
|
||||
name: T,
|
||||
label: string,
|
||||
currencySymbol: string,
|
||||
defaultValue: MinMax
|
||||
): IFilterElement<T> {
|
||||
return {
|
||||
active: false,
|
||||
currencySymbol,
|
||||
label,
|
||||
multiple: true,
|
||||
multiple: defaultValue.min !== defaultValue.max,
|
||||
name,
|
||||
type: FieldType.price,
|
||||
value: [defaultValue.min, defaultValue.max]
|
||||
|
|
Loading…
Reference in a new issue