From 8ddf66f1349f79fedef8be9275f0e312e76b098f Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Fri, 17 Jan 2020 15:25:50 +0100 Subject: [PATCH] Add attribute filter --- src/components/Filter/types.ts | 1 + .../components/ProductListPage/filters.ts | 22 +++++++- src/products/queries.ts | 14 +++++ .../types/InitialProductFilterData.ts | 26 +++++++++ src/products/urls.ts | 11 +++- .../views/ProductList/ProductList.tsx | 6 +-- .../views/ProductList/filters.test.ts | 11 ++++ src/products/views/ProductList/filters.ts | 53 +++++++++++++++++-- src/types.ts | 3 ++ src/utils/filters/filters.ts | 6 ++- src/utils/handlers/filterHandlers.ts | 8 +-- 11 files changed, 143 insertions(+), 18 deletions(-) diff --git a/src/components/Filter/types.ts b/src/components/Filter/types.ts index 86930048e..a7b2f28a4 100644 --- a/src/components/Filter/types.ts +++ b/src/components/Filter/types.ts @@ -24,6 +24,7 @@ export interface IFilterElement Partial { autocomplete?: boolean; displayValues?: MultiAutocompleteChoiceType[]; + group?: T; label: string; name: T; type: FieldType; diff --git a/src/products/components/ProductListPage/filters.ts b/src/products/components/ProductListPage/filters.ts index 1ac4bd5f7..32225b967 100644 --- a/src/products/components/ProductListPage/filters.ts +++ b/src/products/components/ProductListPage/filters.ts @@ -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 & { + choices: MultiAutocompleteChoiceType[]; + name: string; + slug: string; + } + >; categories: FilterOpts & AutocompleteFilterOpts; collections: FilterOpts & AutocompleteFilterOpts; price: FilterOpts; @@ -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 + })) ]; } diff --git a/src/products/queries.ts b/src/products/queries.ts index ca5c1a273..99236c111 100644 --- a/src/products/queries.ts +++ b/src/products/queries.ts @@ -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 { diff --git a/src/products/types/InitialProductFilterData.ts b/src/products/types/InitialProductFilterData.ts index c4c323c0d..0c982e04b 100644 --- a/src/products/types/InitialProductFilterData.ts +++ b/src/products/types/InitialProductFilterData.ts @@ -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; diff --git a/src/products/urls.ts b/src/products/urls.ts index 0f56302ea..9441c04a5 100644 --- a/src/products/urls.ts +++ b/src/products/urls.ts @@ -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 & - FiltersWithMultipleValues; + FiltersWithMultipleValues & + FiltersAsDictWithMultipleValues< + ProductListUrlFiltersAsDictWithMultipleValues + >; export enum ProductListUrlSortField { attribute = "attribute", name = "name", diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index 5d735a38c..818a47395 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -82,11 +82,6 @@ export const ProductList: React.FC = ({ 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 = ({ params }) => { const filterOpts = getFilterOpts( params, + maybe(() => initialFilterData.attributes.edges.map(edge => edge.node), []), { initial: maybe( () => initialFilterData.categories.edges.map(edge => edge.node), diff --git a/src/products/views/ProductList/filters.test.ts b/src/products/views/ProductList/filters.test.ts index a6addc42b..6c5460dac 100644 --- a/src/products/views/ProductList/filters.test.ts +++ b/src/products/views/ProductList/filters.test.ts @@ -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, diff --git a/src/products/views/ProductList/filters.ts b/src/products/views/ProductList/filters.ts index 539f18b4f..69ba4463b 100644 --- a/src/products/views/ProductList/filters.ts +++ b/src/products/views/ProductList/filters.ts @@ -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; @@ -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 + filter: IFilterElement, + 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: diff --git a/src/types.ts b/src/types.ts index ec663ad21..9aea9641d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -135,6 +135,9 @@ export type Filters = Partial< export type FiltersWithMultipleValues = Partial< Record >; +export type FiltersAsDictWithMultipleValues = Partial< + Record> +>; export type Search = Partial<{ query: string; }>; diff --git a/src/utils/filters/filters.ts b/src/utils/filters/filters.ts index c80edc459..8ce699521 100644 --- a/src/utils/filters/filters.ts +++ b/src/utils/filters/filters.ts @@ -34,12 +34,16 @@ export function dedupeFilter(array: T[]): T[] { return Array.from(new Set(array)); } +export type GetFilterQueryParam< + TFilterKeys extends string, + TFilters extends object +> = (filter: IFilterElement, params?: object) => TFilters; export function getFilterQueryParams< TFilterKeys extends string, TUrlFilters extends object >( filter: IFilter, - getFilterQueryParam: (filter: IFilterElement) => TUrlFilters + getFilterQueryParam: GetFilterQueryParam ): TUrlFilters { return filter.reduce( (acc, filterField) => ({ diff --git a/src/utils/handlers/filterHandlers.ts b/src/utils/handlers/filterHandlers.ts index 3c70db069..60c4dbc3c 100644 --- a/src/utils/handlers/filterHandlers.ts +++ b/src/utils/handlers/filterHandlers.ts @@ -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) => TFilters; type CreateFilterHandlers = [ (filter: IFilter) => void, () => void,