Add category and collection filter

This commit is contained in:
dominik-zeglen 2020-01-15 16:36:45 +01:00
parent 525ce272e3
commit aeb209744a
17 changed files with 490 additions and 35 deletions

View file

@ -12,11 +12,16 @@ import { fade } from "@material-ui/core/styles/colorManipulator";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { TextField } from "@material-ui/core"; import { TextField } from "@material-ui/core";
import { toggle } from "@saleor/utils/lists"; import { toggle } from "@saleor/utils/lists";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import Hr from "../Hr"; import Hr from "../Hr";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
import SingleSelectField from "../SingleSelectField"; import SingleSelectField from "../SingleSelectField";
import { SingleAutocompleteChoiceType } from "../SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "../SingleAutocompleteSelectField";
import FormSpacer from "../FormSpacer"; import FormSpacer from "../FormSpacer";
import MultiAutocompleteSelectField, {
MultiAutocompleteChoiceType
} from "../MultiAutocompleteSelectField";
import { IFilter, FieldType, FilterType } from "./types"; import { IFilter, FieldType, FilterType } from "./types";
import Arrow from "./Arrow"; import Arrow from "./Arrow";
import { FilterReducerAction } from "./reducer"; import { FilterReducerAction } from "./reducer";
@ -107,6 +112,18 @@ const FilterContent: React.FC<FilterContentProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const classes = useStyles({}); const classes = useStyles({});
const [
autocompleteDisplayValues,
setAutocompleteDisplayValues
] = useStateFromProps<Record<string, MultiAutocompleteChoiceType[]>>(
filters.reduce((acc, filterField) => {
if (filterField.type === FieldType.autocomplete) {
acc[filterField.name] = filterField.displayValues;
}
return acc;
}, {})
);
return ( return (
<Paper> <Paper>
@ -409,6 +426,54 @@ const FilterContent: React.FC<FilterContentProps> = ({
/> />
</div> </div>
))} ))}
{filterField.type === FieldType.autocomplete &&
filterField.multiple && (
<MultiAutocompleteSelectField
displayValues={
autocompleteDisplayValues[filterField.name]
}
label={filterField.label}
choices={filterField.options}
name={filterField.name}
value={filterField.value}
// helperText={intl.formatMessage({
// defaultMessage:
// "*Optional. Adding product to collection helps users find it.",
// description: "field is optional"
// })}
onChange={createMultiAutocompleteSelectHandler(
event =>
onFilterPropertyChange({
payload: {
name: filterField.name,
update: {
value: toggle(
event.target.value,
filterField.value,
(a, b) => a === b
)
}
},
type: "set-property"
}),
value =>
setAutocompleteDisplayValues({
...autocompleteDisplayValues,
[filterField.name]: toggle(
value[0],
autocompleteDisplayValues[filterField.name],
(a, b) => a.value === b.value
)
}),
[],
filterField.options
)}
fetchChoices={filterField.onSearchChange}
loading={filterField.loading}
data-tc={filterField.name}
key={filterField.name}
/>
)}
</div> </div>
)} )}
</React.Fragment> </React.Fragment>

View file

@ -1,16 +1,39 @@
import { update } from "@saleor/utils/lists"; import { update } from "@saleor/utils/lists";
import { IFilter, IFilterElementMutableData } from "./types"; import { IFilter, IFilterElementMutableData } from "./types";
export type FilterReducerActionType = "clear" | "reset" | "set-property"; export type FilterReducerActionType =
| "clear"
| "merge"
| "reset"
| "set-property";
export interface FilterReducerAction<T extends string> { export interface FilterReducerAction<T extends string> {
type: FilterReducerActionType; type: FilterReducerActionType;
payload: Partial<{ payload: Partial<{
name: T; name: T;
update: Partial<IFilterElementMutableData>; update: Partial<IFilterElementMutableData>;
reset: IFilter<T>; new: IFilter<T>;
}>; }>;
} }
function merge<T extends string>(
prevState: IFilter<T>,
newState: IFilter<T>
): IFilter<T> {
return newState.map(newFilter => {
const prevFilter = prevState.find(
prevFilter => prevFilter.name === newFilter.name
);
if (!!prevFilter) {
return {
...newFilter,
active: prevFilter.active
};
}
return newFilter;
});
}
function setProperty<T extends string>( function setProperty<T extends string>(
prevState: IFilter<T>, prevState: IFilter<T>,
filter: T, filter: T,
@ -32,8 +55,10 @@ function reduceFilter<T extends string>(
switch (action.type) { switch (action.type) {
case "set-property": case "set-property":
return setProperty(prevState, action.payload.name, action.payload.update); return setProperty(prevState, action.payload.name, action.payload.update);
case "merge":
return merge(prevState, action.payload.new);
case "reset": case "reset":
return action.payload.reset; return action.payload.new;
default: default:
return prevState; return prevState;

View file

@ -1,7 +1,8 @@
import { FetchMoreProps } from "@saleor/types"; import { FetchMoreProps, SearchPageProps } from "@saleor/types";
import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField"; import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
export enum FieldType { export enum FieldType {
autocomplete,
boolean, boolean,
date, date,
dateTime, dateTime,
@ -18,9 +19,10 @@ export interface IFilterElementMutableData<T extends string = string> {
value: T[]; value: T[];
} }
export interface IFilterElement<T extends string = string> export interface IFilterElement<T extends string = string>
extends Partial<FetchMoreProps>, extends IFilterElementMutableData,
IFilterElementMutableData { Partial<FetchMoreProps & SearchPageProps> {
autocomplete?: boolean; autocomplete?: boolean;
displayValues?: MultiAutocompleteChoiceType[];
label: string; label: string;
name: T; name: T;
type: FieldType; type: FieldType;

View file

@ -17,12 +17,20 @@ function useFilter<T extends string>(initialFilter: IFilter<T>): UseFilter<T> {
const reset = () => const reset = () =>
dispatchFilterAction({ dispatchFilterAction({
payload: { payload: {
reset: initialFilter new: initialFilter
}, },
type: "reset" type: "reset"
}); });
useEffect(reset, [initialFilter]); const refresh = () =>
dispatchFilterAction({
payload: {
new: initialFilter
},
type: "merge"
});
useEffect(refresh, [initialFilter]);
return [data, dispatchFilterAction, reset]; return [data, dispatchFilterAction, reset];
} }

View file

@ -41,17 +41,14 @@ export interface SingleAutocompleteSelectFieldProps
onChange: (event: React.ChangeEvent<any>) => void; onChange: (event: React.ChangeEvent<any>) => void;
} }
const DebounceAutocomplete: React.ComponentType< const DebounceAutocomplete: React.ComponentType<DebounceProps<
DebounceProps<string> string
> = Debounce; >> = Debounce;
const SingleAutocompleteSelectFieldComponent: React.FC< const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectFieldProps> = props => {
SingleAutocompleteSelectFieldProps
> = props => {
const { const {
choices,
allowCustomValues, allowCustomValues,
choices,
disabled, disabled,
displayValue, displayValue,
emptyOption, emptyOption,
@ -169,9 +166,11 @@ const SingleAutocompleteSelectFieldComponent: React.FC<
); );
}; };
const SingleAutocompleteSelectField: React.FC< const SingleAutocompleteSelectField: React.FC<SingleAutocompleteSelectFieldProps> = ({
SingleAutocompleteSelectFieldProps choices,
> = ({ choices, fetchChoices, ...rest }) => { fetchChoices,
...rest
}) => {
const [query, setQuery] = React.useState(""); const [query, setQuery] = React.useState("");
if (fetchChoices) { if (fetchChoices) {
return ( return (

View file

@ -7,8 +7,7 @@ import {
} from "@saleor/utils/filters/fields"; } from "@saleor/utils/filters/fields";
import { import {
VoucherDiscountType, VoucherDiscountType,
DiscountStatusEnum, DiscountStatusEnum
DiscountValueTypeEnum
} from "@saleor/types/globalTypes"; } from "@saleor/types/globalTypes";
import { MinMax, FilterOpts } from "@saleor/types"; import { MinMax, FilterOpts } from "@saleor/types";
import { IFilter } from "@saleor/components/Filter"; import { IFilter } from "@saleor/components/Filter";
@ -118,11 +117,15 @@ export function createFilterStructure(
[ [
{ {
label: intl.formatMessage(messages.fixed), label: intl.formatMessage(messages.fixed),
value: DiscountValueTypeEnum.FIXED value: VoucherDiscountType.FIXED
}, },
{ {
label: intl.formatMessage(messages.percentage), label: intl.formatMessage(messages.percentage),
value: DiscountValueTypeEnum.PERCENTAGE value: VoucherDiscountType.PERCENTAGE
},
{
label: intl.formatMessage(messages.percentage),
value: VoucherDiscountType.SHIPPING
} }
] ]
), ),

View file

@ -1,20 +1,26 @@
import { defineMessages, IntlShape } from "react-intl"; import { defineMessages, IntlShape } from "react-intl";
import { FilterOpts, MinMax } from "@saleor/types"; import { FilterOpts, MinMax, AutocompleteFilterOpts } from "@saleor/types";
import { StockAvailability } from "@saleor/types/globalTypes"; import { StockAvailability } from "@saleor/types/globalTypes";
import { import {
createOptionsField, createOptionsField,
createPriceField createPriceField,
createAutocompleteField
} from "@saleor/utils/filters/fields"; } from "@saleor/utils/filters/fields";
import { IFilter } from "@saleor/components/Filter"; import { IFilter } from "@saleor/components/Filter";
import { sectionNames } from "@saleor/intl";
export enum ProductFilterKeys { export enum ProductFilterKeys {
categories = "categories",
collections = "collections",
status = "status", status = "status",
price = "price", price = "price",
stock = "stock" stock = "stock"
} }
export interface ProductListFilterOpts { export interface ProductListFilterOpts {
categories: FilterOpts<string[]> & AutocompleteFilterOpts;
collections: FilterOpts<string[]> & AutocompleteFilterOpts;
price: FilterOpts<MinMax>; price: FilterOpts<MinMax>;
status: FilterOpts<ProductStatus>; status: FilterOpts<ProductStatus>;
stockStatus: FilterOpts<StockAvailability>; stockStatus: FilterOpts<StockAvailability>;
@ -105,6 +111,42 @@ export function createFilterStructure(
opts.price.value opts.price.value
), ),
active: opts.price.active active: opts.price.active
},
{
...createAutocompleteField(
ProductFilterKeys.categories,
intl.formatMessage(sectionNames.categories),
opts.categories.value,
opts.categories.displayValues,
true,
opts.categories.choices,
{
hasMore: opts.categories.hasMore,
initialSearch: "",
loading: opts.categories.loading,
onFetchMore: opts.categories.onFetchMore,
onSearchChange: opts.categories.onSearchChange
}
),
active: opts.categories.active
},
{
...createAutocompleteField(
ProductFilterKeys.collections,
intl.formatMessage(sectionNames.collections),
opts.collections.value,
opts.collections.displayValues,
true,
opts.collections.choices,
{
hasMore: opts.collections.hasMore,
initialSearch: "",
loading: opts.collections.loading,
onFetchMore: opts.collections.onFetchMore,
onSearchChange: opts.collections.onSearchChange
}
),
active: opts.collections.active
} }
]; ];
} }

View file

@ -1,5 +1,6 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import makeQuery from "@saleor/hooks/makeQuery";
import { pageInfoFragment, TypedQuery } from "../queries"; import { pageInfoFragment, TypedQuery } from "../queries";
import { import {
AvailableInGridAttributes, AvailableInGridAttributes,
@ -22,6 +23,10 @@ import {
ProductVariantDetails, ProductVariantDetails,
ProductVariantDetailsVariables ProductVariantDetailsVariables
} from "./types/ProductVariantDetails"; } from "./types/ProductVariantDetails";
import {
InitialProductFilterData,
InitialProductFilterDataVariables
} from "./types/InitialProductFilterData";
export const fragmentMoney = gql` export const fragmentMoney = gql`
fragment Money on Money { fragment Money on Money {
@ -209,6 +214,31 @@ export const fragmentVariant = gql`
} }
`; `;
const initialProductFilterDataQuery = gql`
query InitialProductFilterData($categories: [ID!], $collections: [ID!]) {
categories(first: 20, filter: { ids: $categories }) {
edges {
node {
id
name
}
}
}
collections(first: 20, filter: { ids: $collections }) {
edges {
node {
id
name
}
}
}
}
`;
export const useInitialProductFilterDataQuery = makeQuery<
InitialProductFilterData,
InitialProductFilterDataVariables
>(initialProductFilterDataQuery);
const productListQuery = gql` const productListQuery = gql`
${productFragment} ${productFragment}
query ProductList( query ProductList(

View file

@ -0,0 +1,49 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: InitialProductFilterData
// ====================================================
export interface InitialProductFilterData_categories_edges_node {
__typename: "Category";
id: string;
name: string;
}
export interface InitialProductFilterData_categories_edges {
__typename: "CategoryCountableEdge";
node: InitialProductFilterData_categories_edges_node;
}
export interface InitialProductFilterData_categories {
__typename: "CategoryCountableConnection";
edges: InitialProductFilterData_categories_edges[];
}
export interface InitialProductFilterData_collections_edges_node {
__typename: "Collection";
id: string;
name: string;
}
export interface InitialProductFilterData_collections_edges {
__typename: "CollectionCountableEdge";
node: InitialProductFilterData_collections_edges_node;
}
export interface InitialProductFilterData_collections {
__typename: "CollectionCountableConnection";
edges: InitialProductFilterData_collections_edges[];
}
export interface InitialProductFilterData {
categories: InitialProductFilterData_categories | null;
collections: InitialProductFilterData_collections | null;
}
export interface InitialProductFilterDataVariables {
categories?: string[] | null;
collections?: string[] | null;
}

View file

@ -9,7 +9,8 @@ import {
Filters, Filters,
Pagination, Pagination,
Sort, Sort,
TabActionDialog TabActionDialog,
FiltersWithMultipleValues
} from "../types"; } from "../types";
const productSection = "/products/"; const productSection = "/products/";
@ -30,7 +31,12 @@ export enum ProductListUrlFiltersEnum {
stockStatus = "stockStatus", stockStatus = "stockStatus",
query = "query" query = "query"
} }
export type ProductListUrlFilters = Filters<ProductListUrlFiltersEnum>; export enum ProductListUrlFiltersWithMultipleValues {
categories = "categories",
collections = "collections"
}
export type ProductListUrlFilters = Filters<ProductListUrlFiltersEnum> &
FiltersWithMultipleValues<ProductListUrlFiltersWithMultipleValues>;
export enum ProductListUrlSortField { export enum ProductListUrlSortField {
attribute = "attribute", attribute = "attribute",
name = "name", name = "name",

View file

@ -10,7 +10,11 @@ import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
import SaveFilterTabDialog, { import SaveFilterTabDialog, {
SaveFilterTabDialogFormData SaveFilterTabDialogFormData
} from "@saleor/components/SaveFilterTabDialog"; } from "@saleor/components/SaveFilterTabDialog";
import { defaultListSettings, ProductListColumns } from "@saleor/config"; import {
defaultListSettings,
ProductListColumns,
DEFAULT_INITIAL_SEARCH_DATA
} from "@saleor/config";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useListSettings from "@saleor/hooks/useListSettings"; import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
@ -26,6 +30,8 @@ import { ListViews } from "@saleor/types";
import { getSortUrlVariables } from "@saleor/utils/sort"; import { getSortUrlVariables } from "@saleor/utils/sort";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import ProductListPage from "../../components/ProductListPage"; import ProductListPage from "../../components/ProductListPage";
import { import {
TypedProductBulkDeleteMutation, TypedProductBulkDeleteMutation,
@ -33,7 +39,8 @@ import {
} from "../../mutations"; } from "../../mutations";
import { import {
AvailableInGridAttributesQuery, AvailableInGridAttributesQuery,
TypedProductListQuery TypedProductListQuery,
useInitialProductFilterDataQuery
} from "../../queries"; } from "../../queries";
import { productBulkDelete } from "../../types/productBulkDelete"; import { productBulkDelete } from "../../types/productBulkDelete";
import { productBulkPublish } from "../../types/productBulkPublish"; import { productBulkPublish } from "../../types/productBulkPublish";
@ -73,6 +80,19 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
ListViews.PRODUCT_LIST ListViews.PRODUCT_LIST
); );
const intl = useIntl(); const intl = useIntl();
const { data: initialFilterData } = useInitialProductFilterDataQuery({
skip: !(!!params.categories || !!params.collections),
variables: {
categories: params.categories,
collections: params.collections
}
});
const searchCategories = useCategorySearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const searchCollections = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
React.useEffect( React.useEffect(
() => () =>
@ -156,6 +176,24 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
[params, settings.rowNumber] [params, settings.rowNumber]
); );
const filterOpts = getFilterOpts(
params,
{
initial: maybe(
() => initialFilterData.categories.edges.map(edge => edge.node),
[]
),
search: searchCategories
},
{
initial: maybe(
() => initialFilterData.collections.edges.map(edge => edge.node),
[]
),
search: searchCollections
}
);
return ( return (
<AvailableInGridAttributesQuery <AvailableInGridAttributesQuery
variables={{ first: 6, ids: settings.columns }} variables={{ first: 6, ids: settings.columns }}
@ -218,7 +256,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
defaultSettings={ defaultSettings={
defaultListSettings[ListViews.PRODUCT_LIST] defaultListSettings[ListViews.PRODUCT_LIST]
} }
filterOpts={getFilterOpts(params)} filterOpts={filterOpts}
gridAttributes={maybe( gridAttributes={maybe(
() => () =>
attributes.data.grid.edges.map(edge => edge.node), attributes.data.grid.edges.map(edge => edge.node),

View file

@ -2,6 +2,12 @@
exports[`Filtering URL params should not be empty if active filters are present 1`] = ` exports[`Filtering URL params should not be empty if active filters are present 1`] = `
Object { Object {
"categories": Array [
"878752",
],
"collections": Array [
"Q29sbGVjdGlvbjoc",
],
"priceFrom": "10", "priceFrom": "10",
"priceTo": "20", "priceTo": "20",
"status": "published", "status": "published",
@ -9,4 +15,4 @@ Object {
} }
`; `;
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"status=published&stockStatus=IN_STOCK&priceFrom=10&priceTo=20"`; exports[`Filtering URL params should not be empty if active filters are present 2`] = `"status=published&stockStatus=IN_STOCK&priceFrom=10&priceTo=20&categories%5B0%5D=878752&collections%5B0%5D=Q29sbGVjdGlvbjoc"`;

View file

@ -10,6 +10,9 @@ import { getFilterQueryParams } from "@saleor/utils/filters";
import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; import { getExistingKeys, setFilterOptsStatus } from "@test/filters";
import { config } from "@test/intl"; import { config } from "@test/intl";
import { StockAvailability } from "@saleor/types/globalTypes"; import { StockAvailability } from "@saleor/types/globalTypes";
import { categories } from "@saleor/categories/fixtures";
import { fetchMoreProps, searchPageProps } from "@saleor/fixtures";
import { collections } from "@saleor/collections/fixtures";
import { getFilterVariables, getFilterQueryParam } from "./filters"; import { getFilterVariables, getFilterQueryParam } from "./filters";
describe("Filtering query params", () => { describe("Filtering query params", () => {
@ -37,6 +40,38 @@ describe("Filtering URL params", () => {
const intl = createIntl(config); const intl = createIntl(config);
const filters = createFilterStructure(intl, { const filters = createFilterStructure(intl, {
categories: {
...fetchMoreProps,
...searchPageProps,
active: false,
choices: categories.slice(5).map(category => ({
label: category.name,
value: category.id
})),
displayValues: [
{
label: categories[5].name,
value: categories[5].id
}
],
value: [categories[5].id]
},
collections: {
...fetchMoreProps,
...searchPageProps,
active: false,
choices: collections.slice(5).map(category => ({
label: category.name,
value: category.id
})),
displayValues: [
{
label: collections[5].name,
value: collections[5].id
}
],
value: [collections[5].id]
},
price: { price: {
active: false, active: false,
value: { value: {

View file

@ -4,6 +4,19 @@ import {
ProductListFilterOpts, ProductListFilterOpts,
ProductStatus ProductStatus
} from "@saleor/products/components/ProductListPage"; } from "@saleor/products/components/ProductListPage";
import { UseSearchResult } from "@saleor/hooks/makeSearch";
import {
SearchCategories,
SearchCategoriesVariables
} from "@saleor/searches/types/SearchCategories";
import {
InitialProductFilterData_categories_edges_node,
InitialProductFilterData_collections_edges_node
} from "@saleor/products/types/InitialProductFilterData";
import {
SearchCollections,
SearchCollectionsVariables
} from "@saleor/searches/types/SearchCollections";
import { IFilterElement } from "../../../components/Filter"; import { IFilterElement } from "../../../components/Filter";
import { import {
ProductFilterInput, ProductFilterInput,
@ -14,20 +27,87 @@ import {
createFilterUtils, createFilterUtils,
getGteLteVariables, getGteLteVariables,
getMinMaxQueryParam, getMinMaxQueryParam,
getSingleEnumValueQueryParam getSingleEnumValueQueryParam,
dedupeFilter,
getMultipleValueQueryParam
} from "../../../utils/filters"; } from "../../../utils/filters";
import { import {
ProductListUrlFilters, ProductListUrlFilters,
ProductListUrlFiltersEnum, ProductListUrlFiltersEnum,
ProductListUrlQueryParams ProductListUrlQueryParams,
ProductListUrlFiltersWithMultipleValues
} from "../../urls"; } from "../../urls";
export const PRODUCT_FILTERS_KEY = "productFilters"; export const PRODUCT_FILTERS_KEY = "productFilters";
export function getFilterOpts( export function getFilterOpts(
params: ProductListUrlFilters params: ProductListUrlFilters,
categories: {
initial: InitialProductFilterData_categories_edges_node[];
search: UseSearchResult<SearchCategories, SearchCategoriesVariables>;
},
collections: {
initial: InitialProductFilterData_collections_edges_node[];
search: UseSearchResult<SearchCollections, SearchCollectionsVariables>;
}
): ProductListFilterOpts { ): ProductListFilterOpts {
return { return {
categories: {
active: !!params.categories,
choices: maybe(
() =>
categories.search.result.data.search.edges.map(edge => ({
label: edge.node.name,
value: edge.node.id
})),
[]
),
displayValues: maybe(
() =>
categories.initial.map(category => ({
label: category.name,
value: category.id
})),
[]
),
hasMore: maybe(
() => categories.search.result.data.search.pageInfo.hasNextPage,
false
),
initialSearch: "",
loading: categories.search.result.loading,
onFetchMore: categories.search.loadMore,
onSearchChange: categories.search.search,
value: maybe(() => dedupeFilter(params.categories), [])
},
collections: {
active: !!params.collections,
choices: maybe(
() =>
collections.search.result.data.search.edges.map(edge => ({
label: edge.node.name,
value: edge.node.id
})),
[]
),
displayValues: maybe(
() =>
collections.initial.map(category => ({
label: category.name,
value: category.id
})),
[]
),
hasMore: maybe(
() => collections.search.result.data.search.pageInfo.hasNextPage,
false
),
initialSearch: "",
loading: collections.search.result.loading,
onFetchMore: collections.search.loadMore,
onSearchChange: collections.search.search,
value: maybe(() => dedupeFilter(params.collections), [])
},
price: { price: {
active: maybe( active: maybe(
() => () =>
@ -54,6 +134,8 @@ export function getFilterVariables(
params: ProductListUrlFilters params: ProductListUrlFilters
): ProductFilterInput { ): ProductFilterInput {
return { return {
categories: params.categories !== undefined ? params.categories : null,
collections: params.collections !== undefined ? params.collections : null,
isPublished: isPublished:
params.status !== undefined params.status !== undefined
? params.status === ProductStatus.PUBLISHED ? params.status === ProductStatus.PUBLISHED
@ -76,6 +158,18 @@ export function getFilterQueryParam(
const { name } = filter; const { name } = filter;
switch (name) { switch (name) {
case ProductFilterKeys.categories:
return getMultipleValueQueryParam(
filter,
ProductListUrlFiltersWithMultipleValues.categories
);
case ProductFilterKeys.collections:
return getMultipleValueQueryParam(
filter,
ProductListUrlFiltersWithMultipleValues.collections
);
case ProductFilterKeys.price: case ProductFilterKeys.price:
return getMinMaxQueryParam( return getMinMaxQueryParam(
filter, filter,

View file

@ -3,6 +3,7 @@ import { MutationResult } from "react-apollo";
import { User_permissions } from "./auth/types/User"; import { User_permissions } from "./auth/types/User";
import { ConfirmButtonTransitionState } from "./components/ConfirmButton"; import { ConfirmButtonTransitionState } from "./components/ConfirmButton";
import { IFilter } from "./components/Filter"; import { IFilter } from "./components/Filter";
import { MultiAutocompleteChoiceType } from "./components/MultiAutocompleteSelectField";
export interface UserError { export interface UserError {
field: string; field: string;
@ -176,3 +177,10 @@ export interface FilterOpts<T> {
active: boolean; active: boolean;
value: T; value: T;
} }
export interface AutocompleteFilterOpts
extends FetchMoreProps,
SearchPageProps {
choices: MultiAutocompleteChoiceType[];
displayValues: MultiAutocompleteChoiceType[];
}

View file

@ -1,6 +1,6 @@
import { IFilterElement, FieldType } from "@saleor/components/Filter"; import { IFilterElement, FieldType } from "@saleor/components/Filter";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { MinMax } from "@saleor/types"; import { MinMax, FetchMoreProps, SearchPageProps } from "@saleor/types";
export function createPriceField<T extends string>( export function createPriceField<T extends string>(
name: T, name: T,
@ -65,6 +65,28 @@ export function createOptionsField<T extends string>(
}; };
} }
export function createAutocompleteField<T extends string>(
name: T,
label: string,
defaultValue: string[],
displayValues: MultiAutocompleteChoiceType[],
multiple: boolean,
options: MultiAutocompleteChoiceType[],
opts: FetchMoreProps & SearchPageProps
): IFilterElement<T> {
return {
...opts,
active: false,
displayValues,
label,
multiple,
name,
options,
type: FieldType.autocomplete,
value: defaultValue
};
}
export function createTextField<T extends string>( export function createTextField<T extends string>(
name: T, name: T,
label: string, label: string,

View file

@ -1,3 +1,5 @@
import isArray from "lodash-es/isArray";
import { IFilterElement, IFilter } from "@saleor/components/Filter"; import { IFilterElement, IFilter } from "@saleor/components/Filter";
import { findValueInEnum } from "@saleor/misc"; import { findValueInEnum } from "@saleor/misc";
@ -25,6 +27,10 @@ function createFilterUtils<
} }
export function dedupeFilter<T>(array: T[]): T[] { export function dedupeFilter<T>(array: T[]): T[] {
if (!isArray(array)) {
return [array];
}
return Array.from(new Set(array)); return Array.from(new Set(array));
} }
@ -110,6 +116,23 @@ export function getMultipleEnumValueQueryParam<
}; };
} }
export function getMultipleValueQueryParam<
TKey extends string,
TUrlKey extends string
>(param: IFilterElement<TKey>, key: TUrlKey) {
const { active, value } = param;
if (!active) {
return {
[key]: undefined
};
}
return {
[key]: value
};
}
export function getMinMaxQueryParam< export function getMinMaxQueryParam<
TKey extends string, TKey extends string,
TUrlKey extends string TUrlKey extends string