Add attribute filter

This commit is contained in:
dominik-zeglen 2020-01-17 15:25:50 +01:00
parent 0e247c7b95
commit 8ddf66f134
11 changed files with 143 additions and 18 deletions

View file

@ -24,6 +24,7 @@ export interface IFilterElement<T extends string = string>
Partial<FetchMoreProps & SearchPageProps> { Partial<FetchMoreProps & SearchPageProps> {
autocomplete?: boolean; autocomplete?: boolean;
displayValues?: MultiAutocompleteChoiceType[]; displayValues?: MultiAutocompleteChoiceType[];
group?: T;
label: string; label: string;
name: T; name: T;
type: FieldType; type: FieldType;

View file

@ -9,8 +9,10 @@ import {
} 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"; import { sectionNames } from "@saleor/intl";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
export enum ProductFilterKeys { export enum ProductFilterKeys {
attributes = "attributes",
categories = "categories", categories = "categories",
collections = "collections", collections = "collections",
status = "status", status = "status",
@ -20,6 +22,13 @@ export enum ProductFilterKeys {
} }
export interface ProductListFilterOpts { export interface ProductListFilterOpts {
attributes: Array<
FilterOpts<string[]> & {
choices: MultiAutocompleteChoiceType[];
name: string;
slug: string;
}
>;
categories: FilterOpts<string[]> & AutocompleteFilterOpts; categories: FilterOpts<string[]> & AutocompleteFilterOpts;
collections: FilterOpts<string[]> & AutocompleteFilterOpts; collections: FilterOpts<string[]> & AutocompleteFilterOpts;
price: FilterOpts<MinMax>; price: FilterOpts<MinMax>;
@ -167,6 +176,17 @@ export function createFilterStructure(
} }
), ),
active: opts.productType.active 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
}))
]; ];
} }

View file

@ -220,6 +220,20 @@ const initialProductFilterDataQuery = gql`
$collections: [ID!] $collections: [ID!]
$productTypes: [ID!] $productTypes: [ID!]
) { ) {
attributes(first: 100, filter: { filterableInDashboard: true }) {
edges {
node {
id
name
slug
values {
id
name
slug
}
}
}
}
categories(first: 100, filter: { ids: $categories }) { categories(first: 100, filter: { ids: $categories }) {
edges { edges {
node { node {

View file

@ -6,6 +6,31 @@
// GraphQL query operation: InitialProductFilterData // 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 { export interface InitialProductFilterData_categories_edges_node {
__typename: "Category"; __typename: "Category";
id: string; id: string;
@ -55,6 +80,7 @@ export interface InitialProductFilterData_productTypes {
} }
export interface InitialProductFilterData { export interface InitialProductFilterData {
attributes: InitialProductFilterData_attributes | null;
categories: InitialProductFilterData_categories | null; categories: InitialProductFilterData_categories | null;
collections: InitialProductFilterData_collections | null; collections: InitialProductFilterData_collections | null;
productTypes: InitialProductFilterData_productTypes | null; productTypes: InitialProductFilterData_productTypes | null;

View file

@ -10,7 +10,8 @@ import {
Pagination, Pagination,
Sort, Sort,
TabActionDialog, TabActionDialog,
FiltersWithMultipleValues FiltersWithMultipleValues,
FiltersAsDictWithMultipleValues
} from "../types"; } from "../types";
const productSection = "/products/"; const productSection = "/products/";
@ -36,8 +37,14 @@ export enum ProductListUrlFiltersWithMultipleValues {
collections = "collections", collections = "collections",
productTypes = "productTypes" productTypes = "productTypes"
} }
export enum ProductListUrlFiltersAsDictWithMultipleValues {
attributes = "attributes"
}
export type ProductListUrlFilters = Filters<ProductListUrlFiltersEnum> & export type ProductListUrlFilters = Filters<ProductListUrlFiltersEnum> &
FiltersWithMultipleValues<ProductListUrlFiltersWithMultipleValues>; FiltersWithMultipleValues<ProductListUrlFiltersWithMultipleValues> &
FiltersAsDictWithMultipleValues<
ProductListUrlFiltersAsDictWithMultipleValues
>;
export enum ProductListUrlSortField { export enum ProductListUrlSortField {
attribute = "attribute", attribute = "attribute",
name = "name", name = "name",

View file

@ -82,11 +82,6 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
); );
const intl = useIntl(); const intl = useIntl();
const { data: initialFilterData } = useInitialProductFilterDataQuery({ const { data: initialFilterData } = useInitialProductFilterDataQuery({
skip: !(
!!params.categories ||
!!params.collections ||
!!params.productTypes
),
variables: { variables: {
categories: params.categories, categories: params.categories,
collections: params.collections, collections: params.collections,
@ -196,6 +191,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
const filterOpts = getFilterOpts( const filterOpts = getFilterOpts(
params, params,
maybe(() => initialFilterData.attributes.edges.map(edge => edge.node), []),
{ {
initial: maybe( initial: maybe(
() => initialFilterData.categories.edges.map(edge => edge.node), () => initialFilterData.categories.edges.map(edge => edge.node),

View file

@ -14,6 +14,7 @@ import { categories } from "@saleor/categories/fixtures";
import { fetchMoreProps, searchPageProps } from "@saleor/fixtures"; import { fetchMoreProps, searchPageProps } from "@saleor/fixtures";
import { collections } from "@saleor/collections/fixtures"; import { collections } from "@saleor/collections/fixtures";
import { productTypes } from "@saleor/productTypes/fixtures"; import { productTypes } from "@saleor/productTypes/fixtures";
import { attributes } from "@saleor/attributes/fixtures";
import { getFilterVariables, getFilterQueryParam } from "./filters"; import { getFilterVariables, getFilterQueryParam } from "./filters";
describe("Filtering query params", () => { describe("Filtering query params", () => {
@ -41,6 +42,16 @@ describe("Filtering URL params", () => {
const intl = createIntl(config); const intl = createIntl(config);
const filters = createFilterStructure(intl, { 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: { categories: {
...fetchMoreProps, ...fetchMoreProps,
...searchPageProps, ...searchPageProps,

View file

@ -1,3 +1,5 @@
import isArray from "lodash-es/isArray";
import { maybe, findValueInEnum } from "@saleor/misc"; import { maybe, findValueInEnum } from "@saleor/misc";
import { import {
ProductFilterKeys, ProductFilterKeys,
@ -12,7 +14,8 @@ import {
import { import {
InitialProductFilterData_categories_edges_node, InitialProductFilterData_categories_edges_node,
InitialProductFilterData_collections_edges_node, InitialProductFilterData_collections_edges_node,
InitialProductFilterData_productTypes_edges_node InitialProductFilterData_productTypes_edges_node,
InitialProductFilterData_attributes_edges_node
} from "@saleor/products/types/InitialProductFilterData"; } from "@saleor/products/types/InitialProductFilterData";
import { import {
SearchCollections, SearchCollections,
@ -47,6 +50,7 @@ export const PRODUCT_FILTERS_KEY = "productFilters";
export function getFilterOpts( export function getFilterOpts(
params: ProductListUrlFilters, params: ProductListUrlFilters,
attributes: InitialProductFilterData_attributes_edges_node[],
categories: { categories: {
initial: InitialProductFilterData_categories_edges_node[]; initial: InitialProductFilterData_categories_edges_node[];
search: UseSearchResult<SearchCategories, SearchCategoriesVariables>; search: UseSearchResult<SearchCategories, SearchCategoriesVariables>;
@ -61,6 +65,21 @@ export function getFilterOpts(
} }
): ProductListFilterOpts { ): ProductListFilterOpts {
return { 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: { categories: {
active: !!params.categories, active: !!params.categories,
choices: maybe( choices: maybe(
@ -177,6 +196,15 @@ export function getFilterVariables(
params: ProductListUrlFilters params: ProductListUrlFilters
): ProductFilterInput { ): ProductFilterInput {
return { 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, categories: params.categories !== undefined ? params.categories : null,
collections: params.collections !== undefined ? params.collections : null, collections: params.collections !== undefined ? params.collections : null,
isPublished: isPublished:
@ -198,9 +226,28 @@ export function getFilterVariables(
} }
export function getFilterQueryParam( export function getFilterQueryParam(
filter: IFilterElement<ProductFilterKeys> filter: IFilterElement<ProductFilterKeys>,
params: ProductListUrlFilters
): 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) { switch (name) {
case ProductFilterKeys.categories: case ProductFilterKeys.categories:

View file

@ -135,6 +135,9 @@ export type Filters<TFilters extends string> = Partial<
export type FiltersWithMultipleValues<TFilters extends string> = Partial< export type FiltersWithMultipleValues<TFilters extends string> = Partial<
Record<TFilters, string[]> Record<TFilters, string[]>
>; >;
export type FiltersAsDictWithMultipleValues<TFilters extends string> = Partial<
Record<TFilters, Record<string, string[]>>
>;
export type Search = Partial<{ export type Search = Partial<{
query: string; query: string;
}>; }>;

View file

@ -34,12 +34,16 @@ export function dedupeFilter<T>(array: T[]): T[] {
return Array.from(new Set(array)); return Array.from(new Set(array));
} }
export type GetFilterQueryParam<
TFilterKeys extends string,
TFilters extends object
> = (filter: IFilterElement<TFilterKeys>, params?: object) => TFilters;
export function getFilterQueryParams< export function getFilterQueryParams<
TFilterKeys extends string, TFilterKeys extends string,
TUrlFilters extends object TUrlFilters extends object
>( >(
filter: IFilter<TFilterKeys>, filter: IFilter<TFilterKeys>,
getFilterQueryParam: (filter: IFilterElement<TFilterKeys>) => TUrlFilters getFilterQueryParam: GetFilterQueryParam<TFilterKeys, TUrlFilters>
): TUrlFilters { ): TUrlFilters {
return filter.reduce( return filter.reduce(
(acc, filterField) => ({ (acc, filterField) => ({

View file

@ -1,14 +1,10 @@
import { IFilter, IFilterElement } from "@saleor/components/Filter"; import { IFilter } from "@saleor/components/Filter";
import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; import { UseNavigatorResult } from "@saleor/hooks/useNavigator";
import { Sort, Pagination, ActiveTab, Search } from "@saleor/types"; import { Sort, Pagination, ActiveTab, Search } from "@saleor/types";
import { getFilterQueryParams } from "../filters"; import { getFilterQueryParams, GetFilterQueryParam } from "../filters";
type RequiredParams = ActiveTab & Search & Sort & Pagination; type RequiredParams = ActiveTab & Search & Sort & Pagination;
type CreateUrl = (params: RequiredParams) => string; type CreateUrl = (params: RequiredParams) => string;
type GetFilterQueryParam<
TFilterKeys extends string,
TFilters extends object
> = (filter: IFilterElement<TFilterKeys>) => TFilters;
type CreateFilterHandlers<TFilterKeys extends string> = [ type CreateFilterHandlers<TFilterKeys extends string> = [
(filter: IFilter<TFilterKeys>) => void, (filter: IFilter<TFilterKeys>) => void,
() => void, () => void,