Add category and collection filter
This commit is contained in:
parent
525ce272e3
commit
aeb209744a
17 changed files with 490 additions and 35 deletions
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
49
src/products/types/InitialProductFilterData.ts
Normal file
49
src/products/types/InitialProductFilterData.ts
Normal 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;
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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"`;
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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[];
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue