diff --git a/src/components/Filter/Filter.tsx b/src/components/Filter/Filter.tsx index 791a8abac..dbe8cd1e4 100644 --- a/src/components/Filter/Filter.tsx +++ b/src/components/Filter/Filter.tsx @@ -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 { +export interface FilterProps { currencySymbol: string; menu: IFilter; - onFilterAdd: (filter: FilterContentSubmitData) => void; + onFilterAdd: (filter: Array>) => 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 = props => { const anchor = React.useRef(); const [isFilterMenuOpened, setFilterMenuOpened] = React.useState(false); + const [data, dispatch, reset] = useFilter(menu); return (
@@ -122,8 +124,10 @@ const Filter: React.FC = props => { > { + filters={data} + onClear={reset} + onFilterPropertyChange={dispatch} + onSubmit={() => { onFilterAdd(data); setFilterMenuOpened(false); }} diff --git a/src/components/Filter/FilterActions.tsx b/src/components/Filter/FilterActions.tsx deleted file mode 100644 index 211b5ba59..000000000 --- a/src/components/Filter/FilterActions.tsx +++ /dev/null @@ -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 = props => { - const classes = useInputStyles({}); - - return ( - - ); -}; - -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) => void; -} -export interface FilterActionsPropsFilters { - currencySymbol: string; - menu: IFilter; - onFilterAdd: (filter: FilterContentSubmitData) => void; -} - -export const FilterActionsOnlySearch: React.FC = props => { - const { onSearchChange, placeholder, search } = props; - const classes = useStyles(props); - - return ( -
- -
- ); -}; - -export type FilterActionsProps = FilterActionsPropsSearch & - FilterActionsPropsFilters; -const FilterActions: React.FC = props => { - const { - currencySymbol, - menu, - onFilterAdd, - onSearchChange, - placeholder, - search - } = props; - const classes = useStyles(props); - - return ( -
- - -
- ); -}; - -FilterActions.displayName = "FilterActions"; -export default FilterActions; diff --git a/src/components/Filter/FilterContent.tsx b/src/components/Filter/FilterContent.tsx index 79c8e4c9f..8f3c24b21 100644 --- a/src/components/Filter/FilterContent.tsx +++ b/src/components/Filter/FilterContent.tsx @@ -19,10 +19,6 @@ import { IFilter, FieldType, FilterType } from "./types"; import Arrow from "./Arrow"; import { FilterReducerAction } from "./reducer"; -export interface FilterContentSubmitData { - name: TKeys; - value: string[]; -} export interface FilterContentProps { currencySymbol: string; filters: IFilter; @@ -31,17 +27,6 @@ export interface FilterContentProps { onSubmit: () => void; } -function checkFilterValue(value: string[]): boolean { - return value.some(v => !!v); -} - -function getFilterChoices(items: IFilter) { - return items.map(filterItem => ({ - label: filterItem.label, - value: filterItem.value.toString() - })); -} - const useStyles = makeStyles( theme => ({ actionBar: { diff --git a/src/components/Filter/FilterElement.tsx b/src/components/Filter/FilterElement.tsx deleted file mode 100644 index b5cb5c050..000000000 --- a/src/components/Filter/FilterElement.tsx +++ /dev/null @@ -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 { - className?: string; - filter: IFilterItem; - value: string | string[]; - onChange: (value: string | string[]) => void; -} - -const useStyles = makeStyles( - { - calendar: { - margin: 8 - }, - input: { - padding: "20px 12px 17px" - } - }, - { name: "FilterElement" } -); - -export interface FilterElementProps { - className?: string; - currencySymbol: string; - filter: IFilterItem; - value: string | string[]; - onChange: (value: string | string[]) => void; -} - -const FilterElement: React.FC = ({ - currencySymbol, - className, - filter, - onChange, - value -}) => { - const intl = useIntl(); - const classes = useStyles({}); - - if (filter.data.type === FieldType.date) { - return ( - onChange(event.target.value)} - value={value} - InputProps={{ - classes: { - input: classes.input - }, - startAdornment: - }} - /> - ); - } else if (filter.data.type === FieldType.rangeDate) { - return ( - <> - - - - onChange([event.target.value, value[1]])} - InputProps={{ - classes: { - input: classes.input - }, - startAdornment: - }} - /> - - - - - onChange([value[0], event.target.value])} - InputProps={{ - classes: { - input: classes.input - }, - startAdornment: - }} - /> - - ); - } else if (filter.data.type === FieldType.range) { - return ( - <> - - - - onChange([event.target.value, value[1]])} - type="number" - InputProps={{ - classes: { - input: classes.input - } - }} - /> - - - - - onChange([value[0], event.target.value])} - type="number" - InputProps={{ - classes: { - input: classes.input - } - }} - /> - - ); - } else if (filter.data.type === FieldType.rangePrice) { - return ( - <> - - - - onChange([event.target.value, value[1]])} - InputProps={{ - classes: { - input: classes.input - } - }} - /> - - - - - onChange([value[0], event.target.value])} - InputProps={{ - classes: { - input: classes.input - } - }} - /> - - ); - } else if (filter.data.type === FieldType.select) { - return ( - ({ - ...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 ( - 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 ; - } - return ( - onChange(event.target.value)} - value={value as string} - /> - ); -}; -FilterElement.displayName = "FilterElement"; -export default FilterElement; diff --git a/src/components/Filter/reducer.ts b/src/components/Filter/reducer.ts index 0bbf59602..a36a09050 100644 --- a/src/components/Filter/reducer.ts +++ b/src/components/Filter/reducer.ts @@ -30,14 +30,14 @@ function reduceFilter( 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; + + default: + return prevState; } - return prevState; } export default reduceFilter; diff --git a/src/components/Filter/types.ts b/src/components/Filter/types.ts index cfa8fb606..1f8b4d7ea 100644 --- a/src/components/Filter/types.ts +++ b/src/components/Filter/types.ts @@ -19,7 +19,6 @@ export interface IFilterElement extends Partial, IFilterElementMutableData { autocomplete?: boolean; - currencySymbol?: string; label: string; name: T; type: FieldType; diff --git a/src/components/FilterBar/FilterBar.tsx b/src/components/FilterBar/FilterBar.tsx index f4a71b815..54ed13792 100644 --- a/src/components/FilterBar/FilterBar.tsx +++ b/src/components/FilterBar/FilterBar.tsx @@ -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 extends FilterProps { - filterMenu: IFilter; +export interface FilterBarProps + extends FilterProps, + SearchBarProps { + filterStructure: IFilter; } -const FilterBar: React.FC = ({ - 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 = 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 = ({ /> )} - - {debounceSearchChange => { - const handleSearchChange = (event: React.ChangeEvent) => { - const value = event.target.value; - setSearch(value); - debounceSearchChange(value); - }; - - return ( - - ); - }} - +
+ + +
+ {displayTabAction === null ? ( +
+ ) : ( +
+ {displayTabAction === "save" ? ( + + + + ) : ( + displayTabAction === "delete" && ( + + + + ) + )} +
+ )} ); }; diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index 15d8d67d8..9636c5ac0 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -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 = props => { const { allTabLabel, @@ -23,9 +44,16 @@ const SearchBar: React.FC = 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 = props => { /> )} - +
+ +
+ {displayTabAction === null ? ( +
+ ) : ( +
+ {displayTabAction === "save" ? ( + + + + ) : ( + displayTabAction === "delete" && ( + + + + ) + )} +
+ )} ); }; diff --git a/src/components/SearchBar/SearchInput.tsx b/src/components/SearchBar/SearchInput.tsx index 4816a8942..5d8b38e3e 100644 --- a/src/components/SearchBar/SearchInput.tsx +++ b/src/components/SearchBar/SearchInput.tsx @@ -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 = 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 = props => { }; return ( - <> - - {!!displaySearchAction ? ( -
-
- {displaySearchAction === "save" ? ( - - - - ) : ( - - - - )} -
-
- ) : ( -
- )} - + ); }} diff --git a/src/components/TableFilter/FilterChips.tsx b/src/components/TableFilter/FilterChips.tsx deleted file mode 100644 index c71d71984..000000000 --- a/src/components/TableFilter/FilterChips.tsx +++ /dev/null @@ -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 = ({ - currencySymbol, - displayTabAction, - filtersList, - menu, - filterLabel, - placeholder, - onSearchChange, - search, - onFilterAdd, - onFilterSave, - onFilterDelete -}) => { - const theme = useTheme(); - const classes = useStyles({ theme }); - - return ( - <> - - {search || (filtersList && filtersList.length > 0) ? ( -
-
- {filtersList.map(filter => ( -
- - {filter.label} - - - - -
- ))} -
- {displayTabAction === "save" ? ( - - - - ) : ( - displayTabAction === "delete" && ( - - - - ) - )} -
- ) : ( -
- )} - - ); -}; - -export default FilterChips; diff --git a/src/components/TableFilter/index.ts b/src/components/TableFilter/index.ts index 5cb8830ab..c1bffcf95 100644 --- a/src/components/TableFilter/index.ts +++ b/src/components/TableFilter/index.ts @@ -1,4 +1,3 @@ export { default } from "./FilterTabs"; export * from "./FilterTabs"; export * from "./FilterTab"; -export * from "./FilterChips"; diff --git a/src/products/components/ProductListFilter/ProductListFilter.tsx b/src/products/components/ProductListFilter/ProductListFilter.tsx deleted file mode 100644 index 52074f5ed..000000000 --- a/src/products/components/ProductListFilter/ProductListFilter.tsx +++ /dev/null @@ -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 = props => { - const intl = useIntl(); - - const filterMenu: IFilter = [ - { - 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 ( - - ); -}; -ProductListFilter.displayName = "ProductListFilter"; -export default ProductListFilter; diff --git a/src/products/components/ProductListFilter/index.ts b/src/products/components/ProductListFilter/index.ts deleted file mode 100644 index c251bdbef..000000000 --- a/src/products/components/ProductListFilter/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./ProductListFilter"; -export * from "./ProductListFilter"; diff --git a/src/products/components/ProductListPage/ProductListPage.tsx b/src/products/components/ProductListPage/ProductListPage.tsx index 6ebc13096..4c58bfff0 100644 --- a/src/products/components/ProductListPage/ProductListPage.tsx +++ b/src/products/components/ProductListPage/ProductListPage.tsx @@ -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, @@ -55,7 +59,6 @@ export const ProductListPage: React.FC = props => { currencySymbol, currentTab, defaultSettings, - filtersList, gridAttributes, availableInGridAttributes, hasMore, @@ -67,8 +70,8 @@ export const ProductListPage: React.FC = props => { onAdd, onAll, onFetchMore, + onFilterChange, onSearchChange, - onFilterAdd, onTabChange, onTabDelete, onTabSave, @@ -81,6 +84,8 @@ export const ProductListPage: React.FC = 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 = props => { - ; diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index 5b826e5e2..1a0aafb88 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -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 = ({ params }) => { - const { locale } = useLocale(); const navigate = useNavigator(); const notify = useNotifier(); const paginate = usePaginator(); @@ -108,17 +107,28 @@ export const ProductList: React.FC = ({ params }) => { navigate(productListUrl(filters)); }; - const changeFilterField = (filter: ProductListUrlFilters) => { + const changeFilterField = (filter: IFilter) => { 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 = ({ 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 = ({ 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} diff --git a/src/products/views/ProductList/filters.test.ts b/src/products/views/ProductList/filters.test.ts index cdb06af94..83b9a8cbb 100644 --- a/src/products/views/ProductList/filters.test.ts +++ b/src/products/views/ProductList/filters.test.ts @@ -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(); diff --git a/src/products/views/ProductList/filters.ts b/src/products/views/ProductList/filters.ts index 09896b1a7..21cc8ff50 100644 --- a/src/products/views/ProductList/filters.ts +++ b/src/products/views/ProductList/filters.ts @@ -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 { + 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 +export function getFilterQueryParam( + filter: IFilterElement ): 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 +): ProductListUrlFilters { + return filter.reduce( + (acc, filterField) => ({ + ...acc, + ...getFilterQueryParam(filterField) + }), + {} + ); } export const { diff --git a/src/products/views/ProductList/messages.ts b/src/products/views/ProductList/messages.ts new file mode 100644 index 000000000..236534926 --- /dev/null +++ b/src/products/views/ProductList/messages.ts @@ -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; diff --git a/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx b/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx index e726fc962..e9352f64f 100644 --- a/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx +++ b/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx @@ -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 = ({ - filters, - language, - onBack, - children, - ...searchProps -}) => { +const TranslationsEntitiesListPage: React.FC = 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 onClick={filters.onProductTypesTabClick} /> - +
+ +
{children}
diff --git a/src/types.ts b/src/types.ts index 4f091c732..cb4ef1d1b 100644 --- a/src/types.ts +++ b/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 +export interface FilterPageProps extends SearchPageProps, TabPageProps { currencySymbol: string; - filtersList: Filter[]; - onFilterAdd: (filter: FilterContentSubmitData) => void; + onFilterChange: (filter: IFilter) => void; } export interface SearchProps { searchPlaceholder: string; } -export interface FilterProps +export interface FilterProps extends FilterPageProps, SearchProps { - allTabLabel: string; - filterLabel: string; + currencySymbol: string; } export interface TabPageProps { diff --git a/src/utils/filters/fields.ts b/src/utils/filters/fields.ts index cc248a93b..a56dfeb8b 100644 --- a/src/utils/filters/fields.ts +++ b/src/utils/filters/fields.ts @@ -6,14 +6,12 @@ type MinMax = Record<"min" | "max", string>; export function createPriceField( name: T, label: string, - currencySymbol: string, defaultValue: MinMax ): IFilterElement { return { active: false, - currencySymbol, label, - multiple: true, + multiple: defaultValue.min !== defaultValue.max, name, type: FieldType.price, value: [defaultValue.min, defaultValue.max]