Refactor filter components
This commit is contained in:
parent
d58b1046ff
commit
87b94f47e1
16 changed files with 626 additions and 286 deletions
22
src/components/Filter/Arrow.tsx
Normal file
22
src/components/Filter/Arrow.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React from "react";
|
||||
|
||||
const Arrow: React.FC<React.SVGProps<SVGSVGElement>> = props => (
|
||||
<svg
|
||||
width="18"
|
||||
height="21"
|
||||
viewBox="0 0 18 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.5858 17.1357L-1.37065e-07 17.1357L-1.37065e-07 15L-1.37064e-07 0L2 -8.74228e-08L2 15.1357L13.5858 15.1357L11.8643 13.4142L13.2785 12L17.4142 16.1357L13.2785 20.2714L11.8643 18.8571L13.5858 17.1357Z"
|
||||
fill="#3D3D3D"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
Arrow.displayName = "Arrow";
|
||||
export default Arrow;
|
|
@ -1,6 +1,5 @@
|
|||
import ButtonBase from "@material-ui/core/ButtonBase";
|
||||
import Grow from "@material-ui/core/Grow";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Popper from "@material-ui/core/Popper";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||
|
@ -17,7 +16,6 @@ import { FilterContent } from ".";
|
|||
export interface FilterProps<TFilterKeys = string> {
|
||||
currencySymbol: string;
|
||||
menu: IFilter<TFilterKeys>;
|
||||
filterLabel: string;
|
||||
onFilterAdd: (filter: FilterContentSubmitData) => void;
|
||||
}
|
||||
|
||||
|
@ -82,7 +80,7 @@ const useStyles = makeStyles(
|
|||
{ name: "Filter" }
|
||||
);
|
||||
const Filter: React.FC<FilterProps> = props => {
|
||||
const { currencySymbol, filterLabel, menu, onFilterAdd } = props;
|
||||
const { currencySymbol, menu, onFilterAdd } = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
const anchor = React.useRef<HTMLDivElement>();
|
||||
|
@ -122,17 +120,14 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
placement === "bottom" ? "right top" : "right bottom"
|
||||
}}
|
||||
>
|
||||
<Paper className={classes.paper}>
|
||||
<Typography>{filterLabel}</Typography>
|
||||
<FilterContent
|
||||
currencySymbol={currencySymbol}
|
||||
filters={menu}
|
||||
onSubmit={data => {
|
||||
onFilterAdd(data);
|
||||
setFilterMenuOpened(false);
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
<FilterContent
|
||||
currencySymbol={currencySymbol}
|
||||
filters={menu}
|
||||
onSubmit={data => {
|
||||
onFilterAdd(data);
|
||||
setFilterMenuOpened(false);
|
||||
}}
|
||||
/>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
|
|
|
@ -56,13 +56,10 @@ export interface FilterActionsPropsSearch {
|
|||
export interface FilterActionsPropsFilters<TKeys = string> {
|
||||
currencySymbol: string;
|
||||
menu: IFilter<TKeys>;
|
||||
filterLabel: string;
|
||||
onFilterAdd: (filter: FilterContentSubmitData<TKeys>) => void;
|
||||
}
|
||||
|
||||
export const FilterActionsOnlySearch: React.FC<
|
||||
FilterActionsPropsSearch
|
||||
> = props => {
|
||||
export const FilterActionsOnlySearch: React.FC<FilterActionsPropsSearch> = props => {
|
||||
const { onSearchChange, placeholder, search } = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
|
@ -83,7 +80,6 @@ export type FilterActionsProps = FilterActionsPropsSearch &
|
|||
const FilterActions: React.FC<FilterActionsProps> = props => {
|
||||
const {
|
||||
currencySymbol,
|
||||
filterLabel,
|
||||
menu,
|
||||
onFilterAdd,
|
||||
onSearchChange,
|
||||
|
@ -97,7 +93,6 @@ const FilterActions: React.FC<FilterActionsProps> = props => {
|
|||
<Filter
|
||||
currencySymbol={currencySymbol}
|
||||
menu={menu}
|
||||
filterLabel={filterLabel}
|
||||
onFilterAdd={onFilterAdd}
|
||||
/>
|
||||
<Search
|
||||
|
|
|
@ -1,29 +1,37 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { FormattedMessage, useIntl, IntlShape } from "react-intl";
|
||||
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { getMenuItemByValue, isLeaf, walkToRoot } from "../../utils/menu";
|
||||
import FormSpacer from "../FormSpacer";
|
||||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||
import { buttonMessages } from "@saleor/intl";
|
||||
import { TextField } from "@material-ui/core";
|
||||
import { toggle } from "@saleor/utils/lists";
|
||||
import Hr from "../Hr";
|
||||
import Checkbox from "../Checkbox";
|
||||
import SingleSelectField from "../SingleSelectField";
|
||||
import FilterElement from "./FilterElement";
|
||||
import { IFilter } from "./types";
|
||||
import { SingleAutocompleteChoiceType } from "../SingleAutocompleteSelectField";
|
||||
import FormSpacer from "../FormSpacer";
|
||||
import { IFilter, FieldType, FilterType } from "./types";
|
||||
import Arrow from "./Arrow";
|
||||
import { FilterReducerAction } from "./reducer";
|
||||
|
||||
export interface FilterContentSubmitData<TKeys = string> {
|
||||
name: TKeys;
|
||||
value: string | string[];
|
||||
value: string[];
|
||||
}
|
||||
export interface FilterContentProps {
|
||||
export interface FilterContentProps<T extends string = string> {
|
||||
currencySymbol: string;
|
||||
filters: IFilter<string>;
|
||||
onSubmit: (data: FilterContentSubmitData) => void;
|
||||
filters: IFilter<T>;
|
||||
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
||||
onClear: () => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
function checkFilterValue(value: string | string[]): boolean {
|
||||
if (typeof value === "string") {
|
||||
return !!value;
|
||||
}
|
||||
function checkFilterValue(value: string[]): boolean {
|
||||
return value.some(v => !!v);
|
||||
}
|
||||
|
||||
|
@ -35,113 +43,333 @@ function getFilterChoices(items: IFilter<string>) {
|
|||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
theme => ({
|
||||
actionBar: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: theme.spacing(1, 3)
|
||||
},
|
||||
andLabel: {
|
||||
margin: theme.spacing(0, 2)
|
||||
},
|
||||
arrow: {
|
||||
marginRight: theme.spacing(2)
|
||||
},
|
||||
clear: {
|
||||
marginRight: theme.spacing(1)
|
||||
},
|
||||
filterFieldBar: {
|
||||
"&:not(:last-of-type)": {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`
|
||||
},
|
||||
padding: theme.spacing(1, 2.5)
|
||||
},
|
||||
filterSettings: {
|
||||
background: fade(theme.palette.primary.main, 0.2),
|
||||
padding: theme.spacing(2, 3)
|
||||
},
|
||||
input: {
|
||||
padding: "20px 12px 17px"
|
||||
padding: "12px 0 9px 12px"
|
||||
},
|
||||
inputRange: {
|
||||
alignItems: "center",
|
||||
display: "flex"
|
||||
},
|
||||
label: {
|
||||
fontWeight: 600
|
||||
},
|
||||
option: {
|
||||
left: -theme.spacing(0.5),
|
||||
position: "relative"
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ name: "FilterContent" }
|
||||
);
|
||||
|
||||
function getIsFilterMultipleChoices(
|
||||
intl: IntlShape
|
||||
): SingleAutocompleteChoiceType[] {
|
||||
return [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "is equal to",
|
||||
description: "is filter range or value"
|
||||
}),
|
||||
value: FilterType.SINGULAR
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "is between",
|
||||
description: "is filter range or value"
|
||||
}),
|
||||
value: FilterType.MULTIPLE
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const FilterContent: React.FC<FilterContentProps> = ({
|
||||
currencySymbol,
|
||||
filters,
|
||||
onClear,
|
||||
onFilterPropertyChange,
|
||||
onSubmit
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [menuValue, setMenuValue] = React.useState<string>(null);
|
||||
const [filterValue, setFilterValue] = React.useState<string | string[]>("");
|
||||
const classes = useStyles({});
|
||||
|
||||
const activeMenu = menuValue
|
||||
? getMenuItemByValue(filters, menuValue)
|
||||
: undefined;
|
||||
const menus = menuValue
|
||||
? walkToRoot(filters, menuValue).slice(-1)
|
||||
: undefined;
|
||||
|
||||
const onMenuChange = (event: React.ChangeEvent<any>) => {
|
||||
setMenuValue(event.target.value);
|
||||
setFilterValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SingleSelectField
|
||||
choices={getFilterChoices(filters)}
|
||||
onChange={onMenuChange}
|
||||
selectProps={{
|
||||
classes: {
|
||||
selectMenu: classes.input
|
||||
}
|
||||
<Paper>
|
||||
<form
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
value={menus ? menus[0].value : menuValue}
|
||||
placeholder={intl.formatMessage({
|
||||
defaultMessage: "Select Filter..."
|
||||
})}
|
||||
/>
|
||||
{menus &&
|
||||
menus.map(
|
||||
(filterItem, filterItemIndex) =>
|
||||
!isLeaf(filterItem) && (
|
||||
<React.Fragment
|
||||
key={filterItem.label.toString() + ":" + filterItem.value}
|
||||
>
|
||||
<FormSpacer />
|
||||
<SingleSelectField
|
||||
choices={getFilterChoices(filterItem.children)}
|
||||
onChange={onMenuChange}
|
||||
selectProps={{
|
||||
classes: {
|
||||
selectMenu: classes.input
|
||||
}
|
||||
}}
|
||||
value={
|
||||
filterItemIndex === menus.length - 1
|
||||
? menuValue.toString()
|
||||
: menus[filterItemIndex - 1].label.toString()
|
||||
>
|
||||
<div className={classes.actionBar}>
|
||||
<Typography className={classes.label}>
|
||||
<FormattedMessage defaultMessage="Filters" />
|
||||
</Typography>
|
||||
<div>
|
||||
<Button className={classes.clear} onClick={onClear}>
|
||||
<FormattedMessage {...buttonMessages.clear} />
|
||||
</Button>
|
||||
<Button color="primary" variant="contained" type="submit">
|
||||
<FormattedMessage {...buttonMessages.done} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Hr />
|
||||
{filters
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
.map(filterField => (
|
||||
<React.Fragment key={filterField.name}>
|
||||
<div className={classes.filterFieldBar}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={filterField.active} />}
|
||||
label={filterField.label}
|
||||
onChange={() =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
active: !filterField.active
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
placeholder={intl.formatMessage({
|
||||
defaultMessage: "Select Filter..."
|
||||
})}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
{activeMenu && isLeaf(activeMenu) && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
{activeMenu.data.additionalText && (
|
||||
<Typography>{activeMenu.data.additionalText}</Typography>
|
||||
)}
|
||||
<FilterElement
|
||||
currencySymbol={currencySymbol}
|
||||
filter={activeMenu}
|
||||
value={filterValue}
|
||||
onChange={value => setFilterValue(value)}
|
||||
/>
|
||||
{checkFilterValue(filterValue) && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
name: activeMenu.value,
|
||||
value: filterValue
|
||||
})
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add filter"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
{filterField.active && (
|
||||
<div className={classes.filterSettings}>
|
||||
{[FieldType.date, FieldType.price, FieldType.number].includes(
|
||||
filterField.type
|
||||
) && (
|
||||
<>
|
||||
<SingleSelectField
|
||||
choices={getIsFilterMultipleChoices(intl)}
|
||||
value={
|
||||
filterField.multiple
|
||||
? FilterType.MULTIPLE
|
||||
: FilterType.SINGULAR
|
||||
}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
multiple:
|
||||
event.target.value === FilterType.MULTIPLE
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<div className={classes.inputRange}>
|
||||
<div>
|
||||
<Arrow className={classes.arrow} />
|
||||
</div>
|
||||
{filterField.multiple ? (
|
||||
<>
|
||||
<TextField
|
||||
fullWidth
|
||||
name={filterField.name + "_min"}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
endAdornment:
|
||||
filterField.type === FieldType.price &&
|
||||
currencySymbol,
|
||||
type:
|
||||
filterField.type === FieldType.date
|
||||
? "date"
|
||||
: "number"
|
||||
}}
|
||||
value={filterField.value[0]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: [
|
||||
event.target.value,
|
||||
filterField.value[1]
|
||||
]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className={classes.andLabel}>
|
||||
<FormattedMessage
|
||||
defaultMessage="and"
|
||||
description="filter range separator"
|
||||
/>
|
||||
</span>
|
||||
<TextField
|
||||
fullWidth
|
||||
name={filterField.name + "_max"}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
endAdornment:
|
||||
filterField.type === FieldType.price &&
|
||||
currencySymbol,
|
||||
type:
|
||||
filterField.type === FieldType.date
|
||||
? "date"
|
||||
: "number"
|
||||
}}
|
||||
value={filterField.value[1]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: [
|
||||
filterField.value[0],
|
||||
event.target.value
|
||||
]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<TextField
|
||||
fullWidth
|
||||
name={filterField.name}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
endAdornment:
|
||||
filterField.type === FieldType.price &&
|
||||
currencySymbol,
|
||||
type:
|
||||
filterField.type === FieldType.date
|
||||
? "date"
|
||||
: [
|
||||
FieldType.number,
|
||||
FieldType.price
|
||||
].includes(filterField.type)
|
||||
? "number"
|
||||
: "text"
|
||||
}}
|
||||
value={filterField.value[0]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: [
|
||||
event.target.value,
|
||||
filterField.value[1]
|
||||
]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{filterField.type === FieldType.options &&
|
||||
(filterField.multiple ? (
|
||||
filterField.options.map(option => (
|
||||
<div className={classes.option} key={option.value}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={filterField.value.includes(
|
||||
option.value
|
||||
)}
|
||||
/>
|
||||
}
|
||||
label={option.label}
|
||||
name={filterField.name}
|
||||
onChange={() =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: toggle(
|
||||
option.value,
|
||||
filterField.value,
|
||||
(a, b) => a === b
|
||||
)
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<SingleSelectField
|
||||
choices={filterField.options}
|
||||
name={filterField.name}
|
||||
value={filterField.value[0]}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: [event.target.value]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</form>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
FilterContent.displayName = "FilterContent";
|
||||
|
|
0
src/components/Filter/reducer.test.ts
Normal file
0
src/components/Filter/reducer.test.ts
Normal file
43
src/components/Filter/reducer.ts
Normal file
43
src/components/Filter/reducer.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { update } from "@saleor/utils/lists";
|
||||
import { IFilter, IFilterElementMutableData } from "./types";
|
||||
|
||||
export type FilterReducerActionType = "clear" | "reset" | "set-property";
|
||||
export interface FilterReducerAction<T extends string> {
|
||||
type: FilterReducerActionType;
|
||||
payload: Partial<{
|
||||
name: T;
|
||||
update: Partial<IFilterElementMutableData>;
|
||||
reset: IFilter<T>;
|
||||
}>;
|
||||
}
|
||||
|
||||
function setProperty<T extends string>(
|
||||
prevState: IFilter<T>,
|
||||
filter: T,
|
||||
updateData: Partial<IFilterElementMutableData>
|
||||
): IFilter<T> {
|
||||
const field = prevState.find(f => f.name === filter);
|
||||
const updatedField = {
|
||||
...field,
|
||||
...updateData
|
||||
};
|
||||
|
||||
return update(updatedField, prevState, (a, b) => a.name === b.name);
|
||||
}
|
||||
|
||||
function reduceFilter<T extends string>(
|
||||
prevState: IFilter<T>,
|
||||
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;
|
||||
}
|
||||
return prevState;
|
||||
}
|
||||
|
||||
export default reduceFilter;
|
|
@ -1,30 +1,33 @@
|
|||
import { IMenu, IMenuItem } from "../../utils/menu";
|
||||
import { FetchMoreProps } from "@saleor/types";
|
||||
import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
|
||||
|
||||
export enum FieldType {
|
||||
date,
|
||||
hidden,
|
||||
number,
|
||||
price,
|
||||
range,
|
||||
rangeDate,
|
||||
rangePrice,
|
||||
select,
|
||||
options,
|
||||
text
|
||||
}
|
||||
|
||||
export interface FilterChoice {
|
||||
export interface IFilterElementMutableData<T extends string = string> {
|
||||
active: boolean;
|
||||
multiple: boolean;
|
||||
options?: MultiAutocompleteChoiceType[];
|
||||
value: T[];
|
||||
}
|
||||
export interface IFilterElement<T extends string = string>
|
||||
extends Partial<FetchMoreProps>,
|
||||
IFilterElementMutableData {
|
||||
autocomplete?: boolean;
|
||||
currencySymbol?: string;
|
||||
label: string;
|
||||
value: string | boolean;
|
||||
}
|
||||
|
||||
export interface FilterData {
|
||||
additionalText?: string;
|
||||
fieldLabel: string;
|
||||
options?: FilterChoice[];
|
||||
name: T;
|
||||
type: FieldType;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export type IFilterItem<TKeys> = IMenuItem<FilterData, TKeys>;
|
||||
export type IFilter<T extends string = string> = Array<IFilterElement<T>>;
|
||||
|
||||
export type IFilter<TKeys> = IMenu<FilterData, TKeys>;
|
||||
export enum FilterType {
|
||||
MULTIPLE = "MULTIPLE",
|
||||
SINGULAR = "SINGULAR"
|
||||
}
|
||||
|
|
37
src/components/Filter/useFilter.ts
Normal file
37
src/components/Filter/useFilter.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { useReducer, useEffect, Dispatch } from "react";
|
||||
|
||||
import reduceFilter, { FilterReducerAction } from "./reducer";
|
||||
import { IFilter, IFilterElement } from "./types";
|
||||
|
||||
function createInitialFilter<T extends string>(
|
||||
initialFilter: IFilter<T>
|
||||
): IFilter<T> {
|
||||
return initialFilter;
|
||||
}
|
||||
|
||||
export type UseFilter<T extends string> = [
|
||||
Array<IFilterElement<T>>,
|
||||
Dispatch<FilterReducerAction<T>>,
|
||||
() => void
|
||||
];
|
||||
|
||||
function useFilter<T extends string>(initialFilter: IFilter<T>): UseFilter<T> {
|
||||
const [data, dispatchFilterAction] = useReducer(
|
||||
reduceFilter,
|
||||
createInitialFilter(initialFilter)
|
||||
);
|
||||
|
||||
const reset = () =>
|
||||
dispatchFilterAction({
|
||||
payload: {
|
||||
reset: initialFilter
|
||||
},
|
||||
type: "reset"
|
||||
});
|
||||
|
||||
useEffect(reset, [initialFilter]);
|
||||
|
||||
return [data, dispatchFilterAction, reset];
|
||||
}
|
||||
|
||||
export default useFilter;
|
|
@ -2,8 +2,8 @@ import React from "react";
|
|||
import { useIntl } from "react-intl";
|
||||
|
||||
import { SearchPageProps, TabPageProps } from "@saleor/types";
|
||||
import FilterSearch from "../Filter/FilterSearch";
|
||||
import FilterTabs, { FilterTab } from "../TableFilter";
|
||||
import SearchInput from "./SearchInput";
|
||||
|
||||
export interface SearchBarProps extends SearchPageProps, TabPageProps {
|
||||
allTabLabel: string;
|
||||
|
@ -47,7 +47,7 @@ const SearchBar: React.FC<SearchBarProps> = props => {
|
|||
/>
|
||||
)}
|
||||
</FilterTabs>
|
||||
<FilterSearch
|
||||
<SearchInput
|
||||
displaySearchAction={
|
||||
!!initialSearch ? (isCustom ? "save" : "delete") : null
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { FilterActionsOnlySearch } from "../Filter/FilterActions";
|
|||
import Hr from "../Hr";
|
||||
import Link from "../Link";
|
||||
|
||||
export interface FilterSearchProps extends SearchPageProps {
|
||||
export interface SearchInputProps extends SearchPageProps {
|
||||
displaySearchAction: "save" | "delete" | null;
|
||||
searchPlaceholder: string;
|
||||
onSearchDelete?: () => void;
|
||||
|
@ -29,11 +29,11 @@ const useStyles = makeStyles(
|
|||
}
|
||||
}),
|
||||
{
|
||||
name: "FilterSearch"
|
||||
name: "SearchInput"
|
||||
}
|
||||
);
|
||||
|
||||
const FilterSearch: React.FC<FilterSearchProps> = props => {
|
||||
const SearchInput: React.FC<SearchInputProps> = props => {
|
||||
const {
|
||||
displaySearchAction,
|
||||
initialSearch,
|
||||
|
@ -93,5 +93,5 @@ const FilterSearch: React.FC<FilterSearchProps> = props => {
|
|||
);
|
||||
};
|
||||
|
||||
FilterSearch.displayName = "FilterSearch";
|
||||
export default FilterSearch;
|
||||
SearchInput.displayName = "SearchInput";
|
||||
export default SearchInput;
|
|
@ -8,6 +8,7 @@ import { makeStyles } from "@material-ui/core/styles";
|
|||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { InputProps } from "@material-ui/core/Input";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
|
@ -38,13 +39,13 @@ interface SingleSelectFieldProps {
|
|||
selectProps?: SelectProps;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
InputProps?: InputProps;
|
||||
onChange(event: any);
|
||||
}
|
||||
|
||||
export const SingleSelectField: React.FC<SingleSelectFieldProps> = props => {
|
||||
const {
|
||||
className,
|
||||
|
||||
disabled,
|
||||
error,
|
||||
label,
|
||||
|
@ -54,7 +55,8 @@ export const SingleSelectField: React.FC<SingleSelectFieldProps> = props => {
|
|||
name,
|
||||
hint,
|
||||
selectProps,
|
||||
placeholder
|
||||
placeholder,
|
||||
InputProps
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
|
@ -90,6 +92,7 @@ export const SingleSelectField: React.FC<SingleSelectFieldProps> = props => {
|
|||
}}
|
||||
name={name}
|
||||
labelWidth={180}
|
||||
{...InputProps}
|
||||
/>
|
||||
}
|
||||
{...selectProps}
|
||||
|
|
|
@ -1,124 +1,92 @@
|
|||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { FilterContent, FilterContentProps } from "@saleor/components/Filter";
|
||||
import {
|
||||
FieldType,
|
||||
FilterContent,
|
||||
FilterContentProps
|
||||
} from "@saleor/components/Filter";
|
||||
import CardDecorator from "../../CardDecorator";
|
||||
createPriceField,
|
||||
createDateField,
|
||||
createOptionsField
|
||||
} from "@saleor/utils/filters/fields";
|
||||
import useFilter from "@saleor/components/Filter/useFilter";
|
||||
import Decorator from "../../Decorator";
|
||||
|
||||
const props: FilterContentProps = {
|
||||
currencySymbol: "USD",
|
||||
filters: [
|
||||
createPriceField("price", "Price", "USD", {
|
||||
max: "100.00",
|
||||
min: "20.00"
|
||||
}),
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
fieldLabel: "Category Name",
|
||||
type: FieldType.text
|
||||
},
|
||||
label: "Category",
|
||||
value: "category"
|
||||
...createDateField("createdAt", "Created At", {
|
||||
max: "2019-10-23",
|
||||
min: "2019-09-09"
|
||||
}),
|
||||
active: true
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
fieldLabel: "Product Type Name",
|
||||
type: FieldType.text
|
||||
},
|
||||
label: "Product Type",
|
||||
value: "product-type"
|
||||
...createOptionsField("status", "Status", ["val1"], false, [
|
||||
{
|
||||
label: "Value 1",
|
||||
value: "val1"
|
||||
},
|
||||
{
|
||||
label: "Value 2",
|
||||
value: "val2"
|
||||
},
|
||||
{
|
||||
label: "Value 3",
|
||||
value: "val3"
|
||||
}
|
||||
]),
|
||||
active: true
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
fieldLabel: "Status",
|
||||
options: [
|
||||
...createOptionsField(
|
||||
"multiplOptions",
|
||||
"Multiple Options",
|
||||
["val1", "val2"],
|
||||
true,
|
||||
[
|
||||
{
|
||||
label: "Published",
|
||||
value: true
|
||||
label: "Value 1",
|
||||
value: "val1"
|
||||
},
|
||||
{
|
||||
label: "Hidden",
|
||||
value: false
|
||||
label: "Value 2",
|
||||
value: "val2"
|
||||
},
|
||||
{
|
||||
label: "Value 3",
|
||||
value: "val3"
|
||||
}
|
||||
],
|
||||
type: FieldType.select
|
||||
},
|
||||
label: "Published",
|
||||
value: "published"
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
fieldLabel: "Stock",
|
||||
type: FieldType.range
|
||||
},
|
||||
label: "Stock",
|
||||
value: "stock"
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
fieldLabel: "Equal to",
|
||||
type: FieldType.date
|
||||
},
|
||||
label: "Equal to",
|
||||
value: "date-equal"
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
fieldLabel: "Range",
|
||||
type: FieldType.rangeDate
|
||||
},
|
||||
label: "Range",
|
||||
value: "date-range"
|
||||
}
|
||||
],
|
||||
data: {
|
||||
fieldLabel: "Date",
|
||||
type: FieldType.select
|
||||
},
|
||||
label: "Date",
|
||||
value: "date"
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
fieldLabel: "Exactly",
|
||||
type: FieldType.price
|
||||
},
|
||||
label: "Exactly",
|
||||
value: "price-exactly"
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
data: {
|
||||
fieldLabel: "Range",
|
||||
type: FieldType.rangePrice
|
||||
},
|
||||
label: "Range",
|
||||
value: "price-range"
|
||||
}
|
||||
],
|
||||
data: {
|
||||
fieldLabel: "Price",
|
||||
type: FieldType.select
|
||||
},
|
||||
label: "Price",
|
||||
value: "price"
|
||||
]
|
||||
),
|
||||
active: false
|
||||
}
|
||||
],
|
||||
onClear: () => undefined,
|
||||
onFilterPropertyChange: () => undefined,
|
||||
onSubmit: () => undefined
|
||||
};
|
||||
|
||||
const InteractiveStory: React.FC = () => {
|
||||
const [data, dispatchFilterActions, clear] = useFilter(props.filters);
|
||||
|
||||
return (
|
||||
<FilterContent
|
||||
{...props}
|
||||
filters={data}
|
||||
onClear={clear}
|
||||
onFilterPropertyChange={dispatchFilterActions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf("Generics / Filter", module)
|
||||
.addDecorator(CardDecorator)
|
||||
.addDecorator(storyFn => (
|
||||
<div style={{ margin: "auto", width: 400 }}>{storyFn()}</div>
|
||||
))
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <FilterContent {...props} />);
|
||||
.add("default", () => <FilterContent {...props} />)
|
||||
.add("interactive", () => <InteractiveStory />);
|
||||
|
|
|
@ -330,6 +330,7 @@ export default (colors: IThemeColors): Theme =>
|
|||
}
|
||||
}
|
||||
},
|
||||
backgroundColor: colors.background.paper,
|
||||
borderColor: colors.input.border,
|
||||
top: 0
|
||||
}
|
||||
|
|
|
@ -4,9 +4,8 @@ import { IntlShape, useIntl } from "react-intl";
|
|||
|
||||
import AppHeader from "@saleor/components/AppHeader";
|
||||
import Container from "@saleor/components/Container";
|
||||
import FilterSearch from "@saleor/components/Filter/FilterSearch";
|
||||
import SearchInput from "@saleor/components/SearchBar/SearchInput";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
// tslint:disable no-submodule-imports
|
||||
import { ShopInfo_shop_languages } from "@saleor/components/Shop/types/ShopInfo";
|
||||
import FilterTabs, { FilterTab } from "@saleor/components/TableFilter";
|
||||
import { maybe } from "@saleor/misc";
|
||||
|
@ -88,9 +87,13 @@ const tabs: TranslationsEntitiesListFilterTab[] = [
|
|||
"productTypes"
|
||||
];
|
||||
|
||||
const TranslationsEntitiesListPage: React.FC<
|
||||
TranslationsEntitiesListPageProps
|
||||
> = ({ filters, language, onBack, children, ...searchProps }) => {
|
||||
const TranslationsEntitiesListPage: React.FC<TranslationsEntitiesListPageProps> = ({
|
||||
filters,
|
||||
language,
|
||||
onBack,
|
||||
children,
|
||||
...searchProps
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const currentTab = tabs.indexOf(filters.current);
|
||||
|
||||
|
@ -157,7 +160,7 @@ const TranslationsEntitiesListPage: React.FC<
|
|||
onClick={filters.onProductTypesTabClick}
|
||||
/>
|
||||
</FilterTabs>
|
||||
<FilterSearch
|
||||
<SearchInput
|
||||
displaySearchAction={null}
|
||||
searchPlaceholder={getSearchPlaceholder(filters.current, intl)}
|
||||
{...searchProps}
|
||||
|
|
69
src/utils/filters/fields.ts
Normal file
69
src/utils/filters/fields.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { IFilterElement, FieldType } from "@saleor/components/Filter";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
|
||||
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,
|
||||
name,
|
||||
type: FieldType.price,
|
||||
value: [defaultValue.min, defaultValue.max]
|
||||
};
|
||||
}
|
||||
|
||||
export function createDateField<T extends string>(
|
||||
name: T,
|
||||
label: string,
|
||||
defaultValue: MinMax
|
||||
): IFilterElement<T> {
|
||||
return {
|
||||
active: false,
|
||||
label,
|
||||
multiple: true,
|
||||
name,
|
||||
type: FieldType.date,
|
||||
value: [defaultValue.min, defaultValue.max]
|
||||
};
|
||||
}
|
||||
|
||||
export function createNumberField<T extends string>(
|
||||
name: T,
|
||||
label: string,
|
||||
defaultValue: MinMax
|
||||
): IFilterElement<T> {
|
||||
return {
|
||||
active: false,
|
||||
label,
|
||||
multiple: true,
|
||||
name,
|
||||
type: FieldType.number,
|
||||
value: [defaultValue.min, defaultValue.max]
|
||||
};
|
||||
}
|
||||
|
||||
export function createOptionsField<T extends string>(
|
||||
name: T,
|
||||
label: string,
|
||||
defaultValue: string[],
|
||||
multiple: boolean,
|
||||
options: MultiAutocompleteChoiceType[]
|
||||
): IFilterElement<T> {
|
||||
return {
|
||||
active: false,
|
||||
label,
|
||||
multiple,
|
||||
name,
|
||||
options,
|
||||
type: FieldType.options,
|
||||
value: defaultValue
|
||||
};
|
||||
}
|
|
@ -5,13 +5,10 @@ function createFilterUtils<
|
|||
function getActiveFilters(params: TQueryParams): TFilters {
|
||||
return Object.keys(params)
|
||||
.filter(key => Object.keys(filters).includes(key))
|
||||
.reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = params[key];
|
||||
return acc;
|
||||
},
|
||||
{} as any
|
||||
);
|
||||
.reduce((acc, key) => {
|
||||
acc[key] = params[key];
|
||||
return acc;
|
||||
}, {} as any);
|
||||
}
|
||||
|
||||
function areFiltersApplied(params: TQueryParams): boolean {
|
||||
|
@ -24,30 +21,6 @@ function createFilterUtils<
|
|||
};
|
||||
}
|
||||
|
||||
export function valueOrFirst<T>(value: T | T[]): T {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function arrayOrValue<T>(value: T | T[]): T[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return [value];
|
||||
}
|
||||
|
||||
export function arrayOrUndefined<T>(array: T[]): T[] | undefined {
|
||||
if (array.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
export function dedupeFilter<T>(array: T[]): T[] {
|
||||
return Array.from(new Set(array));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue