diff --git a/src/components/Filter/Arrow.tsx b/src/components/Filter/Arrow.tsx new file mode 100644 index 000000000..ef4cbe5d8 --- /dev/null +++ b/src/components/Filter/Arrow.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +const Arrow: React.FC> = props => ( + + + +); + +Arrow.displayName = "Arrow"; +export default Arrow; diff --git a/src/components/Filter/Filter.tsx b/src/components/Filter/Filter.tsx index 7b0774307..791a8abac 100644 --- a/src/components/Filter/Filter.tsx +++ b/src/components/Filter/Filter.tsx @@ -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 { currencySymbol: string; menu: IFilter; - filterLabel: string; onFilterAdd: (filter: FilterContentSubmitData) => void; } @@ -82,7 +80,7 @@ const useStyles = makeStyles( { name: "Filter" } ); const Filter: React.FC = props => { - const { currencySymbol, filterLabel, menu, onFilterAdd } = props; + const { currencySymbol, menu, onFilterAdd } = props; const classes = useStyles(props); const anchor = React.useRef(); @@ -122,17 +120,14 @@ const Filter: React.FC = props => { placement === "bottom" ? "right top" : "right bottom" }} > - - {filterLabel} - { - onFilterAdd(data); - setFilterMenuOpened(false); - }} - /> - + { + onFilterAdd(data); + setFilterMenuOpened(false); + }} + /> )} diff --git a/src/components/Filter/FilterActions.tsx b/src/components/Filter/FilterActions.tsx index 60eee1d97..211b5ba59 100644 --- a/src/components/Filter/FilterActions.tsx +++ b/src/components/Filter/FilterActions.tsx @@ -56,13 +56,10 @@ export interface FilterActionsPropsSearch { export interface FilterActionsPropsFilters { currencySymbol: string; menu: IFilter; - filterLabel: string; onFilterAdd: (filter: FilterContentSubmitData) => void; } -export const FilterActionsOnlySearch: React.FC< - FilterActionsPropsSearch -> = props => { +export const FilterActionsOnlySearch: React.FC = props => { const { onSearchChange, placeholder, search } = props; const classes = useStyles(props); @@ -83,7 +80,6 @@ export type FilterActionsProps = FilterActionsPropsSearch & const FilterActions: React.FC = props => { const { currencySymbol, - filterLabel, menu, onFilterAdd, onSearchChange, @@ -97,7 +93,6 @@ const FilterActions: React.FC = props => { { name: TKeys; - value: string | string[]; + value: string[]; } -export interface FilterContentProps { +export interface FilterContentProps { currencySymbol: string; - filters: IFilter; - onSubmit: (data: FilterContentSubmitData) => void; + filters: IFilter; + onFilterPropertyChange: React.Dispatch>; + 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) { } 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 = ({ currencySymbol, filters, + onClear, + onFilterPropertyChange, onSubmit }) => { const intl = useIntl(); - const [menuValue, setMenuValue] = React.useState(null); - const [filterValue, setFilterValue] = React.useState(""); const classes = useStyles({}); - const activeMenu = menuValue - ? getMenuItemByValue(filters, menuValue) - : undefined; - const menus = menuValue - ? walkToRoot(filters, menuValue).slice(-1) - : undefined; - - const onMenuChange = (event: React.ChangeEvent) => { - setMenuValue(event.target.value); - setFilterValue(""); - }; - return ( - <> - +
{ + event.preventDefault(); + onSubmit(); }} - value={menus ? menus[0].value : menuValue} - placeholder={intl.formatMessage({ - defaultMessage: "Select Filter..." - })} - /> - {menus && - menus.map( - (filterItem, filterItemIndex) => - !isLeaf(filterItem) && ( - - - +
+ + + +
+ + +
+
+
+ {filters + .sort((a, b) => (a.name > b.name ? 1 : -1)) + .map(filterField => ( + +
+ } + label={filterField.label} + onChange={() => + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + active: !filterField.active + } + }, + type: "set-property" + }) } - placeholder={intl.formatMessage({ - defaultMessage: "Select Filter..." - })} /> - - ) - )} - {activeMenu && isLeaf(activeMenu) && ( - <> - - {activeMenu.data.additionalText && ( - {activeMenu.data.additionalText} - )} - setFilterValue(value)} - /> - {checkFilterValue(filterValue) && ( - <> - - - - )} - - )} - +
+ {filterField.active && ( +
+ {[FieldType.date, FieldType.price, FieldType.number].includes( + filterField.type + ) && ( + <> + + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + multiple: + event.target.value === FilterType.MULTIPLE + } + }, + type: "set-property" + }) + } + /> + +
+
+ +
+ {filterField.multiple ? ( + <> + + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: [ + event.target.value, + filterField.value[1] + ] + } + }, + type: "set-property" + }) + } + /> + + + + + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: [ + filterField.value[0], + event.target.value + ] + } + }, + type: "set-property" + }) + } + /> + + ) : ( + + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: [ + event.target.value, + filterField.value[1] + ] + } + }, + type: "set-property" + }) + } + /> + )} +
+ + )} + {filterField.type === FieldType.options && + (filterField.multiple ? ( + filterField.options.map(option => ( +
+ + } + 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" + }) + } + /> +
+ )) + ) : ( + + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: [event.target.value] + } + }, + type: "set-property" + }) + } + /> + ))} +
+ )} +
+ ))} + + ); }; FilterContent.displayName = "FilterContent"; diff --git a/src/components/Filter/reducer.test.ts b/src/components/Filter/reducer.test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/Filter/reducer.ts b/src/components/Filter/reducer.ts new file mode 100644 index 000000000..0bbf59602 --- /dev/null +++ b/src/components/Filter/reducer.ts @@ -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 { + type: FilterReducerActionType; + payload: Partial<{ + name: T; + update: Partial; + reset: IFilter; + }>; +} + +function setProperty( + prevState: IFilter, + filter: T, + updateData: Partial +): IFilter { + const field = prevState.find(f => f.name === filter); + const updatedField = { + ...field, + ...updateData + }; + + return update(updatedField, prevState, (a, b) => a.name === b.name); +} + +function reduceFilter( + prevState: IFilter, + action: FilterReducerAction +): IFilter { + 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; diff --git a/src/components/Filter/types.ts b/src/components/Filter/types.ts index c991801b8..cfa8fb606 100644 --- a/src/components/Filter/types.ts +++ b/src/components/Filter/types.ts @@ -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 { + active: boolean; + multiple: boolean; + options?: MultiAutocompleteChoiceType[]; + value: T[]; +} +export interface IFilterElement + extends Partial, + 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 = IMenuItem; +export type IFilter = Array>; -export type IFilter = IMenu; +export enum FilterType { + MULTIPLE = "MULTIPLE", + SINGULAR = "SINGULAR" +} diff --git a/src/components/Filter/useFilter.ts b/src/components/Filter/useFilter.ts new file mode 100644 index 000000000..dff73fdb6 --- /dev/null +++ b/src/components/Filter/useFilter.ts @@ -0,0 +1,37 @@ +import { useReducer, useEffect, Dispatch } from "react"; + +import reduceFilter, { FilterReducerAction } from "./reducer"; +import { IFilter, IFilterElement } from "./types"; + +function createInitialFilter( + initialFilter: IFilter +): IFilter { + return initialFilter; +} + +export type UseFilter = [ + Array>, + Dispatch>, + () => void +]; + +function useFilter(initialFilter: IFilter): UseFilter { + 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; diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index 9e34a024b..15d8d67d8 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -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 = props => { /> )} - void; @@ -29,11 +29,11 @@ const useStyles = makeStyles( } }), { - name: "FilterSearch" + name: "SearchInput" } ); -const FilterSearch: React.FC = props => { +const SearchInput: React.FC = props => { const { displaySearchAction, initialSearch, @@ -93,5 +93,5 @@ const FilterSearch: React.FC = props => { ); }; -FilterSearch.displayName = "FilterSearch"; -export default FilterSearch; +SearchInput.displayName = "SearchInput"; +export default SearchInput; diff --git a/src/components/SingleSelectField/SingleSelectField.tsx b/src/components/SingleSelectField/SingleSelectField.tsx index 39810f834..87bd69de3 100644 --- a/src/components/SingleSelectField/SingleSelectField.tsx +++ b/src/components/SingleSelectField/SingleSelectField.tsx @@ -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 = props => { const { className, - disabled, error, label, @@ -54,7 +55,8 @@ export const SingleSelectField: React.FC = props => { name, hint, selectProps, - placeholder + placeholder, + InputProps } = props; const classes = useStyles(props); @@ -90,6 +92,7 @@ export const SingleSelectField: React.FC = props => { }} name={name} labelWidth={180} + {...InputProps} /> } {...selectProps} diff --git a/src/storybook/stories/components/Filter.tsx b/src/storybook/stories/components/Filter.tsx index 96cbf23de..2d4bf3b62 100644 --- a/src/storybook/stories/components/Filter.tsx +++ b/src/storybook/stories/components/Filter.tsx @@ -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 ( + + ); +}; + storiesOf("Generics / Filter", module) - .addDecorator(CardDecorator) + .addDecorator(storyFn => ( +
{storyFn()}
+ )) .addDecorator(Decorator) - .add("default", () => ); + .add("default", () => ) + .add("interactive", () => ); diff --git a/src/theme.ts b/src/theme.ts index 24530be60..164e9f620 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -330,6 +330,7 @@ export default (colors: IThemeColors): Theme => } } }, + backgroundColor: colors.background.paper, borderColor: colors.input.border, top: 0 } diff --git a/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx b/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx index f5c40b3c2..e726fc962 100644 --- a/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx +++ b/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx @@ -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 = ({ + 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} /> - ; + +export function createPriceField( + name: T, + label: string, + currencySymbol: string, + defaultValue: MinMax +): IFilterElement { + return { + active: false, + currencySymbol, + label, + multiple: true, + name, + type: FieldType.price, + value: [defaultValue.min, defaultValue.max] + }; +} + +export function createDateField( + name: T, + label: string, + defaultValue: MinMax +): IFilterElement { + return { + active: false, + label, + multiple: true, + name, + type: FieldType.date, + value: [defaultValue.min, defaultValue.max] + }; +} + +export function createNumberField( + name: T, + label: string, + defaultValue: MinMax +): IFilterElement { + return { + active: false, + label, + multiple: true, + name, + type: FieldType.number, + value: [defaultValue.min, defaultValue.max] + }; +} + +export function createOptionsField( + name: T, + label: string, + defaultValue: string[], + multiple: boolean, + options: MultiAutocompleteChoiceType[] +): IFilterElement { + return { + active: false, + label, + multiple, + name, + options, + type: FieldType.options, + value: defaultValue + }; +} diff --git a/src/utils/filters/filters.ts b/src/utils/filters/filters.ts index c620d9fd7..09601718b 100644 --- a/src/utils/filters/filters.ts +++ b/src/utils/filters/filters.ts @@ -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(value: T | T[]): T { - if (Array.isArray(value)) { - return value[0]; - } - - return value; -} - -export function arrayOrValue(value: T | T[]): T[] { - if (Array.isArray(value)) { - return value; - } - - return [value]; -} - -export function arrayOrUndefined(array: T[]): T[] | undefined { - if (array.length === 0) { - return undefined; - } - - return array; -} - export function dedupeFilter(array: T[]): T[] { return Array.from(new Set(array)); }