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> {
autocomplete?: boolean;
displayValues?: MultiAutocompleteChoiceType[];
group?: T;
label: string;
name: T;
type: FieldType;

View file

@ -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
}))
];
}

View file

@ -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 {

View file

@ -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;

View file

@ -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",

View file

@ -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),

View file

@ -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,

View file

@ -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:

View file

@ -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;
}>;

View file

@ -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) => ({

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 { 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,