Add attribute filter
This commit is contained in:
parent
0e247c7b95
commit
8ddf66f134
11 changed files with 143 additions and 18 deletions
|
@ -24,6 +24,7 @@ export interface IFilterElement<T extends string = string>
|
|||
Partial<FetchMoreProps & SearchPageProps> {
|
||||
autocomplete?: boolean;
|
||||
displayValues?: MultiAutocompleteChoiceType[];
|
||||
group?: T;
|
||||
label: string;
|
||||
name: T;
|
||||
type: FieldType;
|
||||
|
|
|
@ -9,8 +9,10 @@ import {
|
|||
} from "@saleor/utils/filters/fields";
|
||||
import { IFilter } from "@saleor/components/Filter";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
|
||||
export enum ProductFilterKeys {
|
||||
attributes = "attributes",
|
||||
categories = "categories",
|
||||
collections = "collections",
|
||||
status = "status",
|
||||
|
@ -20,6 +22,13 @@ export enum ProductFilterKeys {
|
|||
}
|
||||
|
||||
export interface ProductListFilterOpts {
|
||||
attributes: Array<
|
||||
FilterOpts<string[]> & {
|
||||
choices: MultiAutocompleteChoiceType[];
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
>;
|
||||
categories: FilterOpts<string[]> & AutocompleteFilterOpts;
|
||||
collections: FilterOpts<string[]> & AutocompleteFilterOpts;
|
||||
price: FilterOpts<MinMax>;
|
||||
|
@ -167,6 +176,17 @@ export function createFilterStructure(
|
|||
}
|
||||
),
|
||||
active: opts.productType.active
|
||||
}
|
||||
},
|
||||
...opts.attributes.map(attr => ({
|
||||
...createOptionsField(
|
||||
attr.slug as any,
|
||||
attr.name,
|
||||
attr.value,
|
||||
true,
|
||||
attr.choices
|
||||
),
|
||||
active: attr.active,
|
||||
group: ProductFilterKeys.attributes
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
|
|
@ -220,6 +220,20 @@ const initialProductFilterDataQuery = gql`
|
|||
$collections: [ID!]
|
||||
$productTypes: [ID!]
|
||||
) {
|
||||
attributes(first: 100, filter: { filterableInDashboard: true }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
slug
|
||||
values {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
categories(first: 100, filter: { ids: $categories }) {
|
||||
edges {
|
||||
node {
|
||||
|
|
|
@ -6,6 +6,31 @@
|
|||
// GraphQL query operation: InitialProductFilterData
|
||||
// ====================================================
|
||||
|
||||
export interface InitialProductFilterData_attributes_edges_node_values {
|
||||
__typename: "AttributeValue";
|
||||
id: string;
|
||||
name: string | null;
|
||||
slug: string | null;
|
||||
}
|
||||
|
||||
export interface InitialProductFilterData_attributes_edges_node {
|
||||
__typename: "Attribute";
|
||||
id: string;
|
||||
name: string | null;
|
||||
slug: string | null;
|
||||
values: (InitialProductFilterData_attributes_edges_node_values | null)[] | null;
|
||||
}
|
||||
|
||||
export interface InitialProductFilterData_attributes_edges {
|
||||
__typename: "AttributeCountableEdge";
|
||||
node: InitialProductFilterData_attributes_edges_node;
|
||||
}
|
||||
|
||||
export interface InitialProductFilterData_attributes {
|
||||
__typename: "AttributeCountableConnection";
|
||||
edges: InitialProductFilterData_attributes_edges[];
|
||||
}
|
||||
|
||||
export interface InitialProductFilterData_categories_edges_node {
|
||||
__typename: "Category";
|
||||
id: string;
|
||||
|
@ -55,6 +80,7 @@ export interface InitialProductFilterData_productTypes {
|
|||
}
|
||||
|
||||
export interface InitialProductFilterData {
|
||||
attributes: InitialProductFilterData_attributes | null;
|
||||
categories: InitialProductFilterData_categories | null;
|
||||
collections: InitialProductFilterData_collections | null;
|
||||
productTypes: InitialProductFilterData_productTypes | null;
|
||||
|
|
|
@ -10,7 +10,8 @@ import {
|
|||
Pagination,
|
||||
Sort,
|
||||
TabActionDialog,
|
||||
FiltersWithMultipleValues
|
||||
FiltersWithMultipleValues,
|
||||
FiltersAsDictWithMultipleValues
|
||||
} from "../types";
|
||||
|
||||
const productSection = "/products/";
|
||||
|
@ -36,8 +37,14 @@ export enum ProductListUrlFiltersWithMultipleValues {
|
|||
collections = "collections",
|
||||
productTypes = "productTypes"
|
||||
}
|
||||
export enum ProductListUrlFiltersAsDictWithMultipleValues {
|
||||
attributes = "attributes"
|
||||
}
|
||||
export type ProductListUrlFilters = Filters<ProductListUrlFiltersEnum> &
|
||||
FiltersWithMultipleValues<ProductListUrlFiltersWithMultipleValues>;
|
||||
FiltersWithMultipleValues<ProductListUrlFiltersWithMultipleValues> &
|
||||
FiltersAsDictWithMultipleValues<
|
||||
ProductListUrlFiltersAsDictWithMultipleValues
|
||||
>;
|
||||
export enum ProductListUrlSortField {
|
||||
attribute = "attribute",
|
||||
name = "name",
|
||||
|
|
|
@ -82,11 +82,6 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
);
|
||||
const intl = useIntl();
|
||||
const { data: initialFilterData } = useInitialProductFilterDataQuery({
|
||||
skip: !(
|
||||
!!params.categories ||
|
||||
!!params.collections ||
|
||||
!!params.productTypes
|
||||
),
|
||||
variables: {
|
||||
categories: params.categories,
|
||||
collections: params.collections,
|
||||
|
@ -196,6 +191,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
|
||||
const filterOpts = getFilterOpts(
|
||||
params,
|
||||
maybe(() => initialFilterData.attributes.edges.map(edge => edge.node), []),
|
||||
{
|
||||
initial: maybe(
|
||||
() => initialFilterData.categories.edges.map(edge => edge.node),
|
||||
|
|
|
@ -14,6 +14,7 @@ import { categories } from "@saleor/categories/fixtures";
|
|||
import { fetchMoreProps, searchPageProps } from "@saleor/fixtures";
|
||||
import { collections } from "@saleor/collections/fixtures";
|
||||
import { productTypes } from "@saleor/productTypes/fixtures";
|
||||
import { attributes } from "@saleor/attributes/fixtures";
|
||||
import { getFilterVariables, getFilterQueryParam } from "./filters";
|
||||
|
||||
describe("Filtering query params", () => {
|
||||
|
@ -41,6 +42,16 @@ describe("Filtering URL params", () => {
|
|||
const intl = createIntl(config);
|
||||
|
||||
const filters = createFilterStructure(intl, {
|
||||
attributes: attributes.map(attr => ({
|
||||
active: false,
|
||||
choices: attr.values.map(val => ({
|
||||
label: val.name,
|
||||
value: val.slug
|
||||
})),
|
||||
name: attr.name,
|
||||
slug: attr.slug,
|
||||
value: [attr.values[0].slug, attr.values[2].slug]
|
||||
})),
|
||||
categories: {
|
||||
...fetchMoreProps,
|
||||
...searchPageProps,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import isArray from "lodash-es/isArray";
|
||||
|
||||
import { maybe, findValueInEnum } from "@saleor/misc";
|
||||
import {
|
||||
ProductFilterKeys,
|
||||
|
@ -12,7 +14,8 @@ import {
|
|||
import {
|
||||
InitialProductFilterData_categories_edges_node,
|
||||
InitialProductFilterData_collections_edges_node,
|
||||
InitialProductFilterData_productTypes_edges_node
|
||||
InitialProductFilterData_productTypes_edges_node,
|
||||
InitialProductFilterData_attributes_edges_node
|
||||
} from "@saleor/products/types/InitialProductFilterData";
|
||||
import {
|
||||
SearchCollections,
|
||||
|
@ -47,6 +50,7 @@ export const PRODUCT_FILTERS_KEY = "productFilters";
|
|||
|
||||
export function getFilterOpts(
|
||||
params: ProductListUrlFilters,
|
||||
attributes: InitialProductFilterData_attributes_edges_node[],
|
||||
categories: {
|
||||
initial: InitialProductFilterData_categories_edges_node[];
|
||||
search: UseSearchResult<SearchCategories, SearchCategoriesVariables>;
|
||||
|
@ -61,6 +65,21 @@ export function getFilterOpts(
|
|||
}
|
||||
): ProductListFilterOpts {
|
||||
return {
|
||||
attributes: attributes
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
.map(attr => ({
|
||||
active: maybe(() => params.attributes[attr.slug].length > 0, false),
|
||||
choices: attr.values.map(val => ({
|
||||
label: val.name,
|
||||
value: val.slug
|
||||
})),
|
||||
name: attr.name,
|
||||
slug: attr.slug,
|
||||
value:
|
||||
!!params.attributes && params.attributes[attr.slug]
|
||||
? params.attributes[attr.slug]
|
||||
: []
|
||||
})),
|
||||
categories: {
|
||||
active: !!params.categories,
|
||||
choices: maybe(
|
||||
|
@ -177,6 +196,15 @@ export function getFilterVariables(
|
|||
params: ProductListUrlFilters
|
||||
): ProductFilterInput {
|
||||
return {
|
||||
attributes: !!params.attributes
|
||||
? Object.keys(params.attributes).map(key => ({
|
||||
slug: key,
|
||||
// It is possible for qs to parse values not as string[] but string
|
||||
values: isArray(params.attributes[key])
|
||||
? params.attributes[key]
|
||||
: (([params.attributes[key]] as unknown) as string[])
|
||||
}))
|
||||
: null,
|
||||
categories: params.categories !== undefined ? params.categories : null,
|
||||
collections: params.collections !== undefined ? params.collections : null,
|
||||
isPublished:
|
||||
|
@ -198,9 +226,28 @@ export function getFilterVariables(
|
|||
}
|
||||
|
||||
export function getFilterQueryParam(
|
||||
filter: IFilterElement<ProductFilterKeys>
|
||||
filter: IFilterElement<ProductFilterKeys>,
|
||||
params: ProductListUrlFilters
|
||||
): ProductListUrlFilters {
|
||||
const { name } = filter;
|
||||
const { active, group, name, value } = filter;
|
||||
|
||||
if (!!group) {
|
||||
if (active) {
|
||||
return {
|
||||
[group]:
|
||||
params && params[group]
|
||||
? {
|
||||
...params[group],
|
||||
[name]: [...params[group], value]
|
||||
}
|
||||
: {
|
||||
[name]: [value]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case ProductFilterKeys.categories:
|
||||
|
|
|
@ -135,6 +135,9 @@ export type Filters<TFilters extends string> = Partial<
|
|||
export type FiltersWithMultipleValues<TFilters extends string> = Partial<
|
||||
Record<TFilters, string[]>
|
||||
>;
|
||||
export type FiltersAsDictWithMultipleValues<TFilters extends string> = Partial<
|
||||
Record<TFilters, Record<string, string[]>>
|
||||
>;
|
||||
export type Search = Partial<{
|
||||
query: string;
|
||||
}>;
|
||||
|
|
|
@ -34,12 +34,16 @@ export function dedupeFilter<T>(array: T[]): T[] {
|
|||
return Array.from(new Set(array));
|
||||
}
|
||||
|
||||
export type GetFilterQueryParam<
|
||||
TFilterKeys extends string,
|
||||
TFilters extends object
|
||||
> = (filter: IFilterElement<TFilterKeys>, params?: object) => TFilters;
|
||||
export function getFilterQueryParams<
|
||||
TFilterKeys extends string,
|
||||
TUrlFilters extends object
|
||||
>(
|
||||
filter: IFilter<TFilterKeys>,
|
||||
getFilterQueryParam: (filter: IFilterElement<TFilterKeys>) => TUrlFilters
|
||||
getFilterQueryParam: GetFilterQueryParam<TFilterKeys, TUrlFilters>
|
||||
): TUrlFilters {
|
||||
return filter.reduce(
|
||||
(acc, filterField) => ({
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import { IFilter, IFilterElement } from "@saleor/components/Filter";
|
||||
import { IFilter } from "@saleor/components/Filter";
|
||||
import { UseNavigatorResult } from "@saleor/hooks/useNavigator";
|
||||
import { Sort, Pagination, ActiveTab, Search } from "@saleor/types";
|
||||
import { getFilterQueryParams } from "../filters";
|
||||
import { getFilterQueryParams, GetFilterQueryParam } from "../filters";
|
||||
|
||||
type RequiredParams = ActiveTab & Search & Sort & Pagination;
|
||||
type CreateUrl = (params: RequiredParams) => string;
|
||||
type GetFilterQueryParam<
|
||||
TFilterKeys extends string,
|
||||
TFilters extends object
|
||||
> = (filter: IFilterElement<TFilterKeys>) => TFilters;
|
||||
type CreateFilterHandlers<TFilterKeys extends string> = [
|
||||
(filter: IFilter<TFilterKeys>) => void,
|
||||
() => void,
|
||||
|
|
Loading…
Reference in a new issue