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> {
|
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;
|
||||||
|
|
|
@ -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
|
||||||
|
}))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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;
|
||||||
}>;
|
}>;
|
||||||
|
|
|
@ -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) => ({
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue