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 { TextField } from "@material-ui/core";
|
||||
import { toggle } from "@saleor/utils/lists";
|
||||
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
|
||||
import useStateFromProps from "@saleor/hooks/useStateFromProps";
|
||||
import Hr from "../Hr";
|
||||
import Checkbox from "../Checkbox";
|
||||
import SingleSelectField from "../SingleSelectField";
|
||||
import { SingleAutocompleteChoiceType } from "../SingleAutocompleteSelectField";
|
||||
import FormSpacer from "../FormSpacer";
|
||||
import MultiAutocompleteSelectField, {
|
||||
MultiAutocompleteChoiceType
|
||||
} from "../MultiAutocompleteSelectField";
|
||||
import { IFilter, FieldType, FilterType } from "./types";
|
||||
import Arrow from "./Arrow";
|
||||
import { FilterReducerAction } from "./reducer";
|
||||
|
@ -107,6 +112,18 @@ const FilterContent: React.FC<FilterContentProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
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 (
|
||||
<Paper>
|
||||
|
@ -409,6 +426,54 @@ const FilterContent: React.FC<FilterContentProps> = ({
|
|||
/>
|
||||
</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>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -1,16 +1,39 @@
|
|||
import { update } from "@saleor/utils/lists";
|
||||
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> {
|
||||
type: FilterReducerActionType;
|
||||
payload: Partial<{
|
||||
name: T;
|
||||
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>(
|
||||
prevState: IFilter<T>,
|
||||
filter: T,
|
||||
|
@ -32,8 +55,10 @@ function reduceFilter<T extends string>(
|
|||
switch (action.type) {
|
||||
case "set-property":
|
||||
return setProperty(prevState, action.payload.name, action.payload.update);
|
||||
case "merge":
|
||||
return merge(prevState, action.payload.new);
|
||||
case "reset":
|
||||
return action.payload.reset;
|
||||
return action.payload.new;
|
||||
|
||||
default:
|
||||
return prevState;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { FetchMoreProps } from "@saleor/types";
|
||||
import { FetchMoreProps, SearchPageProps } from "@saleor/types";
|
||||
import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
|
||||
|
||||
export enum FieldType {
|
||||
autocomplete,
|
||||
boolean,
|
||||
date,
|
||||
dateTime,
|
||||
|
@ -18,9 +19,10 @@ export interface IFilterElementMutableData<T extends string = string> {
|
|||
value: T[];
|
||||
}
|
||||
export interface IFilterElement<T extends string = string>
|
||||
extends Partial<FetchMoreProps>,
|
||||
IFilterElementMutableData {
|
||||
extends IFilterElementMutableData,
|
||||
Partial<FetchMoreProps & SearchPageProps> {
|
||||
autocomplete?: boolean;
|
||||
displayValues?: MultiAutocompleteChoiceType[];
|
||||
label: string;
|
||||
name: T;
|
||||
type: FieldType;
|
||||
|
|
|
@ -17,12 +17,20 @@ function useFilter<T extends string>(initialFilter: IFilter<T>): UseFilter<T> {
|
|||
const reset = () =>
|
||||
dispatchFilterAction({
|
||||
payload: {
|
||||
reset: initialFilter
|
||||
new: initialFilter
|
||||
},
|
||||
type: "reset"
|
||||
});
|
||||
|
||||
useEffect(reset, [initialFilter]);
|
||||
const refresh = () =>
|
||||
dispatchFilterAction({
|
||||
payload: {
|
||||
new: initialFilter
|
||||
},
|
||||
type: "merge"
|
||||
});
|
||||
|
||||
useEffect(refresh, [initialFilter]);
|
||||
|
||||
return [data, dispatchFilterAction, reset];
|
||||
}
|
||||
|
|
|
@ -41,17 +41,14 @@ export interface SingleAutocompleteSelectFieldProps
|
|||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
}
|
||||
|
||||
const DebounceAutocomplete: React.ComponentType<
|
||||
DebounceProps<string>
|
||||
> = Debounce;
|
||||
const DebounceAutocomplete: React.ComponentType<DebounceProps<
|
||||
string
|
||||
>> = Debounce;
|
||||
|
||||
const SingleAutocompleteSelectFieldComponent: React.FC<
|
||||
SingleAutocompleteSelectFieldProps
|
||||
> = props => {
|
||||
const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectFieldProps> = props => {
|
||||
const {
|
||||
choices,
|
||||
|
||||
allowCustomValues,
|
||||
choices,
|
||||
disabled,
|
||||
displayValue,
|
||||
emptyOption,
|
||||
|
@ -169,9 +166,11 @@ const SingleAutocompleteSelectFieldComponent: React.FC<
|
|||
);
|
||||
};
|
||||
|
||||
const SingleAutocompleteSelectField: React.FC<
|
||||
SingleAutocompleteSelectFieldProps
|
||||
> = ({ choices, fetchChoices, ...rest }) => {
|
||||
const SingleAutocompleteSelectField: React.FC<SingleAutocompleteSelectFieldProps> = ({
|
||||
choices,
|
||||
fetchChoices,
|
||||
...rest
|
||||
}) => {
|
||||
const [query, setQuery] = React.useState("");
|
||||
if (fetchChoices) {
|
||||
return (
|
||||
|
|
|
@ -7,8 +7,7 @@ import {
|
|||
} from "@saleor/utils/filters/fields";
|
||||
import {
|
||||
VoucherDiscountType,
|
||||
DiscountStatusEnum,
|
||||
DiscountValueTypeEnum
|
||||
DiscountStatusEnum
|
||||
} from "@saleor/types/globalTypes";
|
||||
import { MinMax, FilterOpts } from "@saleor/types";
|
||||
import { IFilter } from "@saleor/components/Filter";
|
||||
|
@ -118,11 +117,15 @@ export function createFilterStructure(
|
|||
[
|
||||
{
|
||||
label: intl.formatMessage(messages.fixed),
|
||||
value: DiscountValueTypeEnum.FIXED
|
||||
value: VoucherDiscountType.FIXED
|
||||
},
|
||||
{
|
||||
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 { FilterOpts, MinMax } from "@saleor/types";
|
||||
import { FilterOpts, MinMax, AutocompleteFilterOpts } from "@saleor/types";
|
||||
import { StockAvailability } from "@saleor/types/globalTypes";
|
||||
import {
|
||||
createOptionsField,
|
||||
createPriceField
|
||||
createPriceField,
|
||||
createAutocompleteField
|
||||
} from "@saleor/utils/filters/fields";
|
||||
import { IFilter } from "@saleor/components/Filter";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
|
||||
export enum ProductFilterKeys {
|
||||
categories = "categories",
|
||||
collections = "collections",
|
||||
status = "status",
|
||||
price = "price",
|
||||
stock = "stock"
|
||||
}
|
||||
|
||||
export interface ProductListFilterOpts {
|
||||
categories: FilterOpts<string[]> & AutocompleteFilterOpts;
|
||||
collections: FilterOpts<string[]> & AutocompleteFilterOpts;
|
||||
price: FilterOpts<MinMax>;
|
||||
status: FilterOpts<ProductStatus>;
|
||||
stockStatus: FilterOpts<StockAvailability>;
|
||||
|
@ -105,6 +111,42 @@ export function createFilterStructure(
|
|||
opts.price.value
|
||||
),
|
||||
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 makeQuery from "@saleor/hooks/makeQuery";
|
||||
import { pageInfoFragment, TypedQuery } from "../queries";
|
||||
import {
|
||||
AvailableInGridAttributes,
|
||||
|
@ -22,6 +23,10 @@ import {
|
|||
ProductVariantDetails,
|
||||
ProductVariantDetailsVariables
|
||||
} from "./types/ProductVariantDetails";
|
||||
import {
|
||||
InitialProductFilterData,
|
||||
InitialProductFilterDataVariables
|
||||
} from "./types/InitialProductFilterData";
|
||||
|
||||
export const fragmentMoney = gql`
|
||||
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`
|
||||
${productFragment}
|
||||
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,
|
||||
Pagination,
|
||||
Sort,
|
||||
TabActionDialog
|
||||
TabActionDialog,
|
||||
FiltersWithMultipleValues
|
||||
} from "../types";
|
||||
|
||||
const productSection = "/products/";
|
||||
|
@ -30,7 +31,12 @@ export enum ProductListUrlFiltersEnum {
|
|||
stockStatus = "stockStatus",
|
||||
query = "query"
|
||||
}
|
||||
export type ProductListUrlFilters = Filters<ProductListUrlFiltersEnum>;
|
||||
export enum ProductListUrlFiltersWithMultipleValues {
|
||||
categories = "categories",
|
||||
collections = "collections"
|
||||
}
|
||||
export type ProductListUrlFilters = Filters<ProductListUrlFiltersEnum> &
|
||||
FiltersWithMultipleValues<ProductListUrlFiltersWithMultipleValues>;
|
||||
export enum ProductListUrlSortField {
|
||||
attribute = "attribute",
|
||||
name = "name",
|
||||
|
|
|
@ -10,7 +10,11 @@ import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
|
|||
import SaveFilterTabDialog, {
|
||||
SaveFilterTabDialogFormData
|
||||
} 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 useListSettings from "@saleor/hooks/useListSettings";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
|
@ -26,6 +30,8 @@ import { ListViews } from "@saleor/types";
|
|||
import { getSortUrlVariables } from "@saleor/utils/sort";
|
||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
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 {
|
||||
TypedProductBulkDeleteMutation,
|
||||
|
@ -33,7 +39,8 @@ import {
|
|||
} from "../../mutations";
|
||||
import {
|
||||
AvailableInGridAttributesQuery,
|
||||
TypedProductListQuery
|
||||
TypedProductListQuery,
|
||||
useInitialProductFilterDataQuery
|
||||
} from "../../queries";
|
||||
import { productBulkDelete } from "../../types/productBulkDelete";
|
||||
import { productBulkPublish } from "../../types/productBulkPublish";
|
||||
|
@ -73,6 +80,19 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
ListViews.PRODUCT_LIST
|
||||
);
|
||||
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(
|
||||
() =>
|
||||
|
@ -156,6 +176,24 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
[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 (
|
||||
<AvailableInGridAttributesQuery
|
||||
variables={{ first: 6, ids: settings.columns }}
|
||||
|
@ -218,7 +256,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
defaultSettings={
|
||||
defaultListSettings[ListViews.PRODUCT_LIST]
|
||||
}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
filterOpts={filterOpts}
|
||||
gridAttributes={maybe(
|
||||
() =>
|
||||
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`] = `
|
||||
Object {
|
||||
"categories": Array [
|
||||
"878752",
|
||||
],
|
||||
"collections": Array [
|
||||
"Q29sbGVjdGlvbjoc",
|
||||
],
|
||||
"priceFrom": "10",
|
||||
"priceTo": "20",
|
||||
"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 { config } from "@test/intl";
|
||||
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";
|
||||
|
||||
describe("Filtering query params", () => {
|
||||
|
@ -37,6 +40,38 @@ describe("Filtering URL params", () => {
|
|||
const intl = createIntl(config);
|
||||
|
||||
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: {
|
||||
active: false,
|
||||
value: {
|
||||
|
|
|
@ -4,6 +4,19 @@ import {
|
|||
ProductListFilterOpts,
|
||||
ProductStatus
|
||||
} 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 {
|
||||
ProductFilterInput,
|
||||
|
@ -14,20 +27,87 @@ import {
|
|||
createFilterUtils,
|
||||
getGteLteVariables,
|
||||
getMinMaxQueryParam,
|
||||
getSingleEnumValueQueryParam
|
||||
getSingleEnumValueQueryParam,
|
||||
dedupeFilter,
|
||||
getMultipleValueQueryParam
|
||||
} from "../../../utils/filters";
|
||||
import {
|
||||
ProductListUrlFilters,
|
||||
ProductListUrlFiltersEnum,
|
||||
ProductListUrlQueryParams
|
||||
ProductListUrlQueryParams,
|
||||
ProductListUrlFiltersWithMultipleValues
|
||||
} from "../../urls";
|
||||
|
||||
export const PRODUCT_FILTERS_KEY = "productFilters";
|
||||
|
||||
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 {
|
||||
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: {
|
||||
active: maybe(
|
||||
() =>
|
||||
|
@ -54,6 +134,8 @@ export function getFilterVariables(
|
|||
params: ProductListUrlFilters
|
||||
): ProductFilterInput {
|
||||
return {
|
||||
categories: params.categories !== undefined ? params.categories : null,
|
||||
collections: params.collections !== undefined ? params.collections : null,
|
||||
isPublished:
|
||||
params.status !== undefined
|
||||
? params.status === ProductStatus.PUBLISHED
|
||||
|
@ -76,6 +158,18 @@ export function getFilterQueryParam(
|
|||
const { name } = filter;
|
||||
|
||||
switch (name) {
|
||||
case ProductFilterKeys.categories:
|
||||
return getMultipleValueQueryParam(
|
||||
filter,
|
||||
ProductListUrlFiltersWithMultipleValues.categories
|
||||
);
|
||||
|
||||
case ProductFilterKeys.collections:
|
||||
return getMultipleValueQueryParam(
|
||||
filter,
|
||||
ProductListUrlFiltersWithMultipleValues.collections
|
||||
);
|
||||
|
||||
case ProductFilterKeys.price:
|
||||
return getMinMaxQueryParam(
|
||||
filter,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { MutationResult } from "react-apollo";
|
|||
import { User_permissions } from "./auth/types/User";
|
||||
import { ConfirmButtonTransitionState } from "./components/ConfirmButton";
|
||||
import { IFilter } from "./components/Filter";
|
||||
import { MultiAutocompleteChoiceType } from "./components/MultiAutocompleteSelectField";
|
||||
|
||||
export interface UserError {
|
||||
field: string;
|
||||
|
@ -176,3 +177,10 @@ export interface FilterOpts<T> {
|
|||
active: boolean;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export interface AutocompleteFilterOpts
|
||||
extends FetchMoreProps,
|
||||
SearchPageProps {
|
||||
choices: MultiAutocompleteChoiceType[];
|
||||
displayValues: MultiAutocompleteChoiceType[];
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IFilterElement, FieldType } from "@saleor/components/Filter";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
import { MinMax } from "@saleor/types";
|
||||
import { MinMax, FetchMoreProps, SearchPageProps } from "@saleor/types";
|
||||
|
||||
export function createPriceField<T extends string>(
|
||||
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>(
|
||||
name: T,
|
||||
label: string,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import isArray from "lodash-es/isArray";
|
||||
|
||||
import { IFilterElement, IFilter } from "@saleor/components/Filter";
|
||||
import { findValueInEnum } from "@saleor/misc";
|
||||
|
||||
|
@ -25,6 +27,10 @@ function createFilterUtils<
|
|||
}
|
||||
|
||||
export function dedupeFilter<T>(array: T[]): T[] {
|
||||
if (!isArray(array)) {
|
||||
return [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<
|
||||
TKey extends string,
|
||||
TUrlKey extends string
|
||||
|
|
Loading…
Reference in a new issue