Fix products filtering (#2746)
* Fix products filtering * Refactor products filtering functions * Update filter test snapshots * Add tests for attribute filtering
This commit is contained in:
parent
b612e2be69
commit
6725f6fde2
5 changed files with 482 additions and 134 deletions
|
@ -2,6 +2,7 @@ import { IFilter } from "@saleor/components/Filter";
|
||||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||||
import { AttributeInputTypeEnum, StockAvailability } from "@saleor/graphql";
|
import { AttributeInputTypeEnum, StockAvailability } from "@saleor/graphql";
|
||||||
import { commonMessages, sectionNames } from "@saleor/intl";
|
import { commonMessages, sectionNames } from "@saleor/intl";
|
||||||
|
import { ProductListUrlFiltersAsDictWithMultipleValues } from "@saleor/products/urls";
|
||||||
import {
|
import {
|
||||||
AutocompleteFilterOpts,
|
AutocompleteFilterOpts,
|
||||||
FilterOpts,
|
FilterOpts,
|
||||||
|
@ -20,17 +21,18 @@ import {
|
||||||
} from "@saleor/utils/filters/fields";
|
} from "@saleor/utils/filters/fields";
|
||||||
import { defineMessages, IntlShape } from "react-intl";
|
import { defineMessages, IntlShape } from "react-intl";
|
||||||
|
|
||||||
export enum ProductFilterKeys {
|
export const ProductFilterKeys = {
|
||||||
attributes = "attributes",
|
...ProductListUrlFiltersAsDictWithMultipleValues,
|
||||||
categories = "categories",
|
categories: "categories",
|
||||||
collections = "collections",
|
collections: "collections",
|
||||||
metadata = "metadata",
|
metadata: "metadata",
|
||||||
price = "price",
|
price: "price",
|
||||||
productType = "productType",
|
productType: "productType",
|
||||||
stock = "stock",
|
stock: "stock",
|
||||||
channel = "channel",
|
channel: "channel",
|
||||||
productKind = "productKind",
|
productKind: "productKind",
|
||||||
}
|
} as const;
|
||||||
|
export type ProductFilterKeys = typeof ProductFilterKeys[keyof typeof ProductFilterKeys];
|
||||||
|
|
||||||
export type AttributeFilterOpts = FilterOpts<string[]> & {
|
export type AttributeFilterOpts = FilterOpts<string[]> & {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -260,7 +262,7 @@ export function createFilterStructure(
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
active: attr.active,
|
active: attr.active,
|
||||||
group: ProductFilterKeys.attributes,
|
group: ProductFilterKeys.booleanAttributes,
|
||||||
})),
|
})),
|
||||||
...dateAttributes.map(attr => ({
|
...dateAttributes.map(attr => ({
|
||||||
...createDateField(attr.slug, attr.name, {
|
...createDateField(attr.slug, attr.name, {
|
||||||
|
@ -268,7 +270,7 @@ export function createFilterStructure(
|
||||||
max: attr.value[1] ?? attr.value[0],
|
max: attr.value[1] ?? attr.value[0],
|
||||||
}),
|
}),
|
||||||
active: attr.active,
|
active: attr.active,
|
||||||
group: ProductFilterKeys.attributes,
|
group: ProductFilterKeys.dateAttributes,
|
||||||
})),
|
})),
|
||||||
...dateTimeAttributes.map(attr => ({
|
...dateTimeAttributes.map(attr => ({
|
||||||
...createDateTimeField(attr.slug, attr.name, {
|
...createDateTimeField(attr.slug, attr.name, {
|
||||||
|
@ -276,7 +278,7 @@ export function createFilterStructure(
|
||||||
max: attr.value[1] ?? attr.value[0],
|
max: attr.value[1] ?? attr.value[0],
|
||||||
}),
|
}),
|
||||||
active: attr.active,
|
active: attr.active,
|
||||||
group: ProductFilterKeys.attributes,
|
group: ProductFilterKeys.dateTimeAttributes,
|
||||||
})),
|
})),
|
||||||
...numericAttributes.map(attr => ({
|
...numericAttributes.map(attr => ({
|
||||||
...createNumberField(attr.slug, attr.name, {
|
...createNumberField(attr.slug, attr.name, {
|
||||||
|
@ -284,7 +286,7 @@ export function createFilterStructure(
|
||||||
max: attr.value[1] ?? attr.value[0],
|
max: attr.value[1] ?? attr.value[0],
|
||||||
}),
|
}),
|
||||||
active: attr.active,
|
active: attr.active,
|
||||||
group: ProductFilterKeys.attributes,
|
group: ProductFilterKeys.numericAttributes,
|
||||||
})),
|
})),
|
||||||
...defaultAttributes.map(attr => ({
|
...defaultAttributes.map(attr => ({
|
||||||
...createAutocompleteField(
|
...createAutocompleteField(
|
||||||
|
@ -304,7 +306,7 @@ export function createFilterStructure(
|
||||||
attr.id,
|
attr.id,
|
||||||
),
|
),
|
||||||
active: attr.active,
|
active: attr.active,
|
||||||
group: ProductFilterKeys.attributes,
|
group: ProductFilterKeys.stringAttributes,
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,9 +42,14 @@ export enum ProductListUrlFiltersWithMultipleValues {
|
||||||
collections = "collections",
|
collections = "collections",
|
||||||
productTypes = "productTypes",
|
productTypes = "productTypes",
|
||||||
}
|
}
|
||||||
export enum ProductListUrlFiltersAsDictWithMultipleValues {
|
export const ProductListUrlFiltersAsDictWithMultipleValues = {
|
||||||
attributes = "attributes",
|
booleanAttributes: "boolean-attributes",
|
||||||
}
|
dateAttributes: "date-attributes",
|
||||||
|
dateTimeAttributes: "datetime-attributes",
|
||||||
|
numericAttributes: "numeric-attributes",
|
||||||
|
stringAttributes: "string-attributes",
|
||||||
|
} as const;
|
||||||
|
export type ProductListUrlFiltersAsDictWithMultipleValues = typeof ProductListUrlFiltersAsDictWithMultipleValues[keyof typeof ProductListUrlFiltersAsDictWithMultipleValues];
|
||||||
export enum ProductListUrlFiltersWithKeyValueValues {
|
export enum ProductListUrlFiltersWithKeyValueValues {
|
||||||
metadata = "metadata",
|
metadata = "metadata",
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,27 @@
|
||||||
|
|
||||||
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"attributes": Object {
|
"categories": Array [
|
||||||
|
"878752",
|
||||||
|
],
|
||||||
|
"channel": "default-channel",
|
||||||
|
"collections": Array [
|
||||||
|
"Q29sbGVjdGlvbjoc",
|
||||||
|
],
|
||||||
|
"metadata": Array [
|
||||||
|
Object {
|
||||||
|
"key": "metadataKey",
|
||||||
|
"value": "metadataValue",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"priceFrom": "10",
|
||||||
|
"priceTo": "20",
|
||||||
|
"productKind": "NORMAL",
|
||||||
|
"productTypes": Array [
|
||||||
|
"UHJvZHVjdFR5cGU6MQ==",
|
||||||
|
],
|
||||||
|
"stockStatus": "IN_STOCK",
|
||||||
|
"string-attributes": Object {
|
||||||
"author": Array [
|
"author": Array [
|
||||||
"john-doe",
|
"john-doe",
|
||||||
false,
|
false,
|
||||||
|
@ -52,27 +72,7 @@ Object {
|
||||||
"m",
|
"m",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"categories": Array [
|
|
||||||
"878752",
|
|
||||||
],
|
|
||||||
"channel": "default-channel",
|
|
||||||
"collections": Array [
|
|
||||||
"Q29sbGVjdGlvbjoc",
|
|
||||||
],
|
|
||||||
"metadata": Array [
|
|
||||||
Object {
|
|
||||||
"key": "metadataKey",
|
|
||||||
"value": "metadataValue",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"priceFrom": "10",
|
|
||||||
"priceTo": "20",
|
|
||||||
"productKind": "NORMAL",
|
|
||||||
"productTypes": Array [
|
|
||||||
"UHJvZHVjdFR5cGU6MQ==",
|
|
||||||
],
|
|
||||||
"stockStatus": "IN_STOCK",
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"channel=default-channel&metadata%5B0%5D%5Bkey%5D=metadataKey&metadata%5B0%5D%5Bvalue%5D=metadataValue&productKind=NORMAL&stockStatus=IN_STOCK&priceFrom=10&priceTo=20&categories%5B0%5D=878752&collections%5B0%5D=Q29sbGVjdGlvbjoc&productTypes%5B0%5D=UHJvZHVjdFR5cGU6MQ%3D%3D&attributes%5Bauthor%5D%5B0%5D=john-doe&attributes%5Bauthor%5D%5B1%5D=false&attributes%5Bbox-size%5D%5B0%5D=100g&attributes%5Bbox-size%5D%5B1%5D=500g&attributes%5Bbrand%5D%5B0%5D=saleor&attributes%5Bbrand%5D%5B1%5D=false&attributes%5Bcandy-box-size%5D%5B0%5D=100g&attributes%5Bcandy-box-size%5D%5B1%5D=500g&attributes%5Bcoffee-genre%5D%5B0%5D=arabica&attributes%5Bcoffee-genre%5D%5B1%5D=false&attributes%5Bcollar%5D%5B0%5D=round&attributes%5Bcollar%5D%5B1%5D=polo&attributes%5Bcolor%5D%5B0%5D=blue&attributes%5Bcolor%5D%5B1%5D=false&attributes%5Bcover%5D%5B0%5D=soft&attributes%5Bcover%5D%5B1%5D=middle-soft&attributes%5Bflavor%5D%5B0%5D=sour&attributes%5Bflavor%5D%5B1%5D=false&attributes%5Blanguage%5D%5B0%5D=english&attributes%5Blanguage%5D%5B1%5D=false&attributes%5Bpublisher%5D%5B0%5D=mirumee-press&attributes%5Bpublisher%5D%5B1%5D=false&attributes%5Bsize%5D%5B0%5D=xs&attributes%5Bsize%5D%5B1%5D=m"`;
|
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"channel=default-channel&metadata%5B0%5D%5Bkey%5D=metadataKey&metadata%5B0%5D%5Bvalue%5D=metadataValue&productKind=NORMAL&stockStatus=IN_STOCK&priceFrom=10&priceTo=20&categories%5B0%5D=878752&collections%5B0%5D=Q29sbGVjdGlvbjoc&productTypes%5B0%5D=UHJvZHVjdFR5cGU6MQ%3D%3D&string-attributes%5Bauthor%5D%5B0%5D=john-doe&string-attributes%5Bauthor%5D%5B1%5D=false&string-attributes%5Bbox-size%5D%5B0%5D=100g&string-attributes%5Bbox-size%5D%5B1%5D=500g&string-attributes%5Bbrand%5D%5B0%5D=saleor&string-attributes%5Bbrand%5D%5B1%5D=false&string-attributes%5Bcandy-box-size%5D%5B0%5D=100g&string-attributes%5Bcandy-box-size%5D%5B1%5D=500g&string-attributes%5Bcoffee-genre%5D%5B0%5D=arabica&string-attributes%5Bcoffee-genre%5D%5B1%5D=false&string-attributes%5Bcollar%5D%5B0%5D=round&string-attributes%5Bcollar%5D%5B1%5D=polo&string-attributes%5Bcolor%5D%5B0%5D=blue&string-attributes%5Bcolor%5D%5B1%5D=false&string-attributes%5Bcover%5D%5B0%5D=soft&string-attributes%5Bcover%5D%5B1%5D=middle-soft&string-attributes%5Bflavor%5D%5B0%5D=sour&string-attributes%5Bflavor%5D%5B1%5D=false&string-attributes%5Blanguage%5D%5B0%5D=english&string-attributes%5Blanguage%5D%5B1%5D=false&string-attributes%5Bpublisher%5D%5B0%5D=mirumee-press&string-attributes%5Bpublisher%5D%5B1%5D=false&string-attributes%5Bsize%5D%5B0%5D=xs&string-attributes%5Bsize%5D%5B1%5D=m"`;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { StockAvailability } from "@saleor/graphql";
|
import { AttributeInputTypeEnum, StockAvailability } from "@saleor/graphql";
|
||||||
import { createFilterStructure } from "@saleor/products/components/ProductListPage";
|
import { createFilterStructure } from "@saleor/products/components/ProductListPage";
|
||||||
import { ProductListUrlFilters } from "@saleor/products/urls";
|
import { ProductListUrlFilters } from "@saleor/products/urls";
|
||||||
import { getFilterQueryParams } from "@saleor/utils/filters";
|
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||||
|
@ -7,7 +7,15 @@ import { getExistingKeys, setFilterOptsStatus } from "@test/filters";
|
||||||
import { config } from "@test/intl";
|
import { config } from "@test/intl";
|
||||||
import { createIntl } from "react-intl";
|
import { createIntl } from "react-intl";
|
||||||
|
|
||||||
import { getFilterQueryParam, getFilterVariables } from "./filters";
|
import { ProductListUrlFiltersAsDictWithMultipleValues } from "../../urls";
|
||||||
|
import {
|
||||||
|
FilterParam,
|
||||||
|
getAttributeValuesFromParams,
|
||||||
|
getFilterQueryParam,
|
||||||
|
getFilterVariables,
|
||||||
|
mapAttributeParamsToFilterOpts,
|
||||||
|
parseFilterValue,
|
||||||
|
} from "./filters";
|
||||||
import { productListFilterOpts } from "./fixtures";
|
import { productListFilterOpts } from "./fixtures";
|
||||||
|
|
||||||
describe("Filtering query params", () => {
|
describe("Filtering query params", () => {
|
||||||
|
@ -31,6 +39,150 @@ describe("Filtering query params", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Get attribute values from URL params", () => {
|
||||||
|
type GetAttributeValuesFromParams = Parameters<
|
||||||
|
typeof getAttributeValuesFromParams
|
||||||
|
>;
|
||||||
|
|
||||||
|
it("should return empty array when attribute doesn't exist in params", () => {
|
||||||
|
// Arrange
|
||||||
|
const params: GetAttributeValuesFromParams[0] = {};
|
||||||
|
const attribute: GetAttributeValuesFromParams[1] = {
|
||||||
|
slug: "test",
|
||||||
|
inputType: AttributeInputTypeEnum.DROPDOWN,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const attributeValues = getAttributeValuesFromParams(params, attribute);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(attributeValues).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return attribute values when attribute exists in params", () => {
|
||||||
|
// Arrange
|
||||||
|
const params: GetAttributeValuesFromParams[0] = {
|
||||||
|
"string-attributes": {
|
||||||
|
test: ["value-1", "value-2"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const attribute: GetAttributeValuesFromParams[1] = {
|
||||||
|
slug: "test",
|
||||||
|
inputType: AttributeInputTypeEnum.DROPDOWN,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const attributeValues = getAttributeValuesFromParams(params, attribute);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(attributeValues).toEqual(["value-1", "value-2"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Map attribute params to filter opts", () => {
|
||||||
|
type MapAttributeParamsToFilterOpts = Parameters<
|
||||||
|
typeof mapAttributeParamsToFilterOpts
|
||||||
|
>;
|
||||||
|
type MapAttributeParamsToFilterOptsReturn = ReturnType<
|
||||||
|
typeof mapAttributeParamsToFilterOpts
|
||||||
|
>;
|
||||||
|
|
||||||
|
it("should return empty array when no params given", () => {
|
||||||
|
// Arrange
|
||||||
|
const attributes: MapAttributeParamsToFilterOpts[0] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
slug: "test",
|
||||||
|
inputType: AttributeInputTypeEnum.DROPDOWN,
|
||||||
|
name: "Test",
|
||||||
|
__typename: "Attribute",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const params: MapAttributeParamsToFilterOpts[1] = {};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filterOpts = mapAttributeParamsToFilterOpts(attributes, params);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const expectedFilterOpts: MapAttributeParamsToFilterOptsReturn = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
slug: "test",
|
||||||
|
inputType: AttributeInputTypeEnum.DROPDOWN,
|
||||||
|
name: "Test",
|
||||||
|
active: false,
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(filterOpts).toEqual(expectedFilterOpts);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return filter opts with proper values selected according to passed values selection in params", () => {
|
||||||
|
// Arrange
|
||||||
|
const attributes: MapAttributeParamsToFilterOpts[0] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
slug: "test-1",
|
||||||
|
inputType: AttributeInputTypeEnum.MULTISELECT,
|
||||||
|
name: "Test 1",
|
||||||
|
__typename: "Attribute",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
slug: "test-2",
|
||||||
|
inputType: AttributeInputTypeEnum.DROPDOWN,
|
||||||
|
name: "Test 2",
|
||||||
|
__typename: "Attribute",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
slug: "test-3",
|
||||||
|
inputType: AttributeInputTypeEnum.DROPDOWN,
|
||||||
|
name: "Test 3",
|
||||||
|
__typename: "Attribute",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const params: MapAttributeParamsToFilterOpts[1] = {
|
||||||
|
"string-attributes": {
|
||||||
|
"test-1": ["value-1", "value-2"],
|
||||||
|
"test-2": ["value-3"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filterOpts = mapAttributeParamsToFilterOpts(attributes, params);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const expectedFilterOpts: MapAttributeParamsToFilterOptsReturn = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
slug: "test-1",
|
||||||
|
inputType: AttributeInputTypeEnum.MULTISELECT,
|
||||||
|
name: "Test 1",
|
||||||
|
active: true,
|
||||||
|
value: ["value-1", "value-2"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
slug: "test-2",
|
||||||
|
inputType: AttributeInputTypeEnum.DROPDOWN,
|
||||||
|
name: "Test 2",
|
||||||
|
active: true,
|
||||||
|
value: ["value-3"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
slug: "test-3",
|
||||||
|
inputType: AttributeInputTypeEnum.DROPDOWN,
|
||||||
|
name: "Test 3",
|
||||||
|
active: false,
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(filterOpts).toEqual(expectedFilterOpts);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Filtering URL params", () => {
|
describe("Filtering URL params", () => {
|
||||||
const intl = createIntl(config);
|
const intl = createIntl(config);
|
||||||
|
|
||||||
|
@ -55,3 +207,161 @@ describe("Filtering URL params", () => {
|
||||||
expect(stringifyQs(filterQueryParams)).toMatchSnapshot();
|
expect(stringifyQs(filterQueryParams)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Parsing filter value", () => {
|
||||||
|
it("should return boolean values when boolean attributes values passed", () => {
|
||||||
|
// Arrange
|
||||||
|
const params: ProductListUrlFilters = {
|
||||||
|
"boolean-attributes": {
|
||||||
|
"test-1": ["true"],
|
||||||
|
"test-2": ["false"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const type =
|
||||||
|
ProductListUrlFiltersAsDictWithMultipleValues.booleanAttributes;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const parsedValue1 = parseFilterValue(params, "test-1", type);
|
||||||
|
const parsedValue2 = parseFilterValue(params, "test-2", type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const expectedValue1: FilterParam = {
|
||||||
|
slug: "test-1",
|
||||||
|
boolean: true,
|
||||||
|
};
|
||||||
|
const expectedValue2: FilterParam = {
|
||||||
|
slug: "test-2",
|
||||||
|
boolean: false,
|
||||||
|
};
|
||||||
|
expect(parsedValue1).toEqual(expectedValue1);
|
||||||
|
expect(parsedValue2).toEqual(expectedValue2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return numeric values when numeric attributes values passed", () => {
|
||||||
|
// Arrange
|
||||||
|
const params: ProductListUrlFilters = {
|
||||||
|
"numeric-attributes": {
|
||||||
|
"test-1": ["1"],
|
||||||
|
"test-2": ["1", "2"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const type =
|
||||||
|
ProductListUrlFiltersAsDictWithMultipleValues.numericAttributes;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const parsedValue1 = parseFilterValue(params, "test-1", type);
|
||||||
|
const parsedValue2 = parseFilterValue(params, "test-2", type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const expectedValue1: FilterParam = {
|
||||||
|
slug: "test-1",
|
||||||
|
valuesRange: {
|
||||||
|
gte: 1,
|
||||||
|
lte: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expectedValue2: FilterParam = {
|
||||||
|
slug: "test-2",
|
||||||
|
valuesRange: {
|
||||||
|
gte: 1,
|
||||||
|
lte: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(parsedValue1).toEqual(expectedValue1);
|
||||||
|
expect(parsedValue2).toEqual(expectedValue2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return string values when string attributes values passed", () => {
|
||||||
|
// Arrange
|
||||||
|
const params: ProductListUrlFilters = {
|
||||||
|
"string-attributes": {
|
||||||
|
"test-1": ["value-1"],
|
||||||
|
"test-2": ["value-2", "value-3"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const type = ProductListUrlFiltersAsDictWithMultipleValues.stringAttributes;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const parsedValue1 = parseFilterValue(params, "test-1", type);
|
||||||
|
const parsedValue2 = parseFilterValue(params, "test-2", type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const expectedValue1: FilterParam = {
|
||||||
|
slug: "test-1",
|
||||||
|
values: ["value-1"],
|
||||||
|
};
|
||||||
|
const expectedValue2: FilterParam = {
|
||||||
|
slug: "test-2",
|
||||||
|
values: ["value-2", "value-3"],
|
||||||
|
};
|
||||||
|
expect(parsedValue1).toEqual(expectedValue1);
|
||||||
|
expect(parsedValue2).toEqual(expectedValue2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return date values when date attributes values passed", () => {
|
||||||
|
// Arrange
|
||||||
|
const params: ProductListUrlFilters = {
|
||||||
|
"date-attributes": {
|
||||||
|
"test-1": ["2020-01-01"],
|
||||||
|
"test-2": ["2020-01-01", "2020-02-02"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const type = ProductListUrlFiltersAsDictWithMultipleValues.dateAttributes;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const parsedValue1 = parseFilterValue(params, "test-1", type);
|
||||||
|
const parsedValue2 = parseFilterValue(params, "test-2", type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const expectedValue1: FilterParam = {
|
||||||
|
slug: "test-1",
|
||||||
|
date: {
|
||||||
|
gte: "2020-01-01",
|
||||||
|
lte: "2020-01-01",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expectedValue2: FilterParam = {
|
||||||
|
slug: "test-2",
|
||||||
|
date: {
|
||||||
|
gte: "2020-01-01",
|
||||||
|
lte: "2020-02-02",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(parsedValue1).toEqual(expectedValue1);
|
||||||
|
expect(parsedValue2).toEqual(expectedValue2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return datetime values when datetime attributes values passed", () => {
|
||||||
|
// Arrange
|
||||||
|
const params: ProductListUrlFilters = {
|
||||||
|
"datetime-attributes": {
|
||||||
|
"test-1": ["2020-01-01T00:00:00"],
|
||||||
|
"test-2": ["2020-01-01T00:00:00", "2020-02-02T00:00:00"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const type =
|
||||||
|
ProductListUrlFiltersAsDictWithMultipleValues.dateTimeAttributes;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const parsedValue1 = parseFilterValue(params, "test-1", type);
|
||||||
|
const parsedValue2 = parseFilterValue(params, "test-2", type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const expectedValue1: FilterParam = {
|
||||||
|
slug: "test-1",
|
||||||
|
dateTime: {
|
||||||
|
gte: "2020-01-01T00:00:00",
|
||||||
|
lte: "2020-01-01T00:00:00",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expectedValue2: FilterParam = {
|
||||||
|
slug: "test-2",
|
||||||
|
dateTime: {
|
||||||
|
gte: "2020-01-01T00:00:00",
|
||||||
|
lte: "2020-02-02T00:00:00",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(parsedValue1).toEqual(expectedValue1);
|
||||||
|
expect(parsedValue2).toEqual(expectedValue2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||||
import {
|
import {
|
||||||
|
AttributeFragment,
|
||||||
|
AttributeInputTypeEnum,
|
||||||
InitialProductFilterAttributesQuery,
|
InitialProductFilterAttributesQuery,
|
||||||
InitialProductFilterCategoriesQuery,
|
InitialProductFilterCategoriesQuery,
|
||||||
InitialProductFilterCollectionsQuery,
|
InitialProductFilterCollectionsQuery,
|
||||||
|
@ -27,7 +29,6 @@ import {
|
||||||
mapNodeToChoice,
|
mapNodeToChoice,
|
||||||
mapSlugNodeToChoice,
|
mapSlugNodeToChoice,
|
||||||
} from "@saleor/utils/maps";
|
} from "@saleor/utils/maps";
|
||||||
import moment from "moment-timezone";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FilterElement,
|
FilterElement,
|
||||||
|
@ -57,6 +58,52 @@ import {
|
||||||
import { getProductGiftCardFilterParam } from "./utils";
|
import { getProductGiftCardFilterParam } from "./utils";
|
||||||
export const PRODUCT_FILTERS_KEY = "productFilters";
|
export const PRODUCT_FILTERS_KEY = "productFilters";
|
||||||
|
|
||||||
|
function getAttributeFilterParamType(inputType: AttributeInputTypeEnum) {
|
||||||
|
switch (inputType) {
|
||||||
|
case AttributeInputTypeEnum.DATE:
|
||||||
|
return ProductListUrlFiltersAsDictWithMultipleValues.dateAttributes;
|
||||||
|
case AttributeInputTypeEnum.DATE_TIME:
|
||||||
|
return ProductListUrlFiltersAsDictWithMultipleValues.dateTimeAttributes;
|
||||||
|
case AttributeInputTypeEnum.NUMERIC:
|
||||||
|
return ProductListUrlFiltersAsDictWithMultipleValues.numericAttributes;
|
||||||
|
case AttributeInputTypeEnum.BOOLEAN:
|
||||||
|
return ProductListUrlFiltersAsDictWithMultipleValues.booleanAttributes;
|
||||||
|
default:
|
||||||
|
return ProductListUrlFiltersAsDictWithMultipleValues.stringAttributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAttributeValuesFromParams(
|
||||||
|
params: ProductListUrlFilters,
|
||||||
|
attribute: Pick<AttributeFragment, "inputType" | "slug">,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
params[getAttributeFilterParamType(attribute.inputType)]?.[
|
||||||
|
attribute.slug
|
||||||
|
] || []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapAttributeParamsToFilterOpts(
|
||||||
|
attributes: RelayToFlat<InitialProductFilterAttributesQuery["attributes"]>,
|
||||||
|
params: ProductListUrlFilters,
|
||||||
|
) {
|
||||||
|
return attributes
|
||||||
|
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||||
|
.map(attr => {
|
||||||
|
const attrValues = getAttributeValuesFromParams(params, attr);
|
||||||
|
|
||||||
|
return {
|
||||||
|
active: attrValues.length > 0,
|
||||||
|
id: attr.id,
|
||||||
|
name: attr.name,
|
||||||
|
slug: attr.slug,
|
||||||
|
inputType: attr.inputType,
|
||||||
|
value: dedupeFilter(attrValues),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getFilterOpts(
|
export function getFilterOpts(
|
||||||
params: ProductListUrlFilters,
|
params: ProductListUrlFilters,
|
||||||
attributes: RelayToFlat<InitialProductFilterAttributesQuery["attributes"]>,
|
attributes: RelayToFlat<InitialProductFilterAttributesQuery["attributes"]>,
|
||||||
|
@ -89,16 +136,7 @@ export function getFilterOpts(
|
||||||
channels: SingleAutocompleteChoiceType[],
|
channels: SingleAutocompleteChoiceType[],
|
||||||
): ProductListFilterOpts {
|
): ProductListFilterOpts {
|
||||||
return {
|
return {
|
||||||
attributes: attributes
|
attributes: mapAttributeParamsToFilterOpts(attributes, params),
|
||||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
|
||||||
.map(attr => ({
|
|
||||||
active: maybe(() => params.attributes[attr.slug].length > 0, false),
|
|
||||||
id: attr.id,
|
|
||||||
name: attr.name,
|
|
||||||
slug: attr.slug,
|
|
||||||
inputType: attr.inputType,
|
|
||||||
value: dedupeFilter(params.attributes?.[attr.slug] || []),
|
|
||||||
})),
|
|
||||||
attributeChoices: {
|
attributeChoices: {
|
||||||
active: true,
|
active: true,
|
||||||
choices: mapSlugNodeToChoice(
|
choices: mapSlugNodeToChoice(
|
||||||
|
@ -233,40 +271,6 @@ export function getFilterOpts(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseFilterValue = (
|
|
||||||
params: ProductListUrlFilters,
|
|
||||||
key: string,
|
|
||||||
): {
|
|
||||||
type: "boolean" | "date" | "dateTime" | "numeric" | "string";
|
|
||||||
isMulti: boolean;
|
|
||||||
value: string[];
|
|
||||||
} => {
|
|
||||||
const value = params.attributes[key];
|
|
||||||
const isMulti = params.attributes[key].length > 1;
|
|
||||||
|
|
||||||
const isBooleanValue = value.every(val => val === "true" || val === "false");
|
|
||||||
const isDateValue = (isMulti ? value : [value]).some(val =>
|
|
||||||
moment(val, moment.HTML5_FMT.DATE, true).isValid(),
|
|
||||||
);
|
|
||||||
const isDateTimeValue = (isMulti ? value : [value]).some(val =>
|
|
||||||
moment(val, moment.ISO_8601, true).isValid(),
|
|
||||||
);
|
|
||||||
const isNumericValue = value.some(value => !isNaN(parseFloat(value)));
|
|
||||||
|
|
||||||
const data = { isMulti, value };
|
|
||||||
|
|
||||||
if (isBooleanValue) {
|
|
||||||
return { ...data, type: "boolean" };
|
|
||||||
} else if (isDateValue) {
|
|
||||||
return { ...data, type: "date" };
|
|
||||||
} else if (isDateTimeValue) {
|
|
||||||
return { ...data, type: "dateTime" };
|
|
||||||
} else if (isNumericValue) {
|
|
||||||
return { ...data, type: "numeric" };
|
|
||||||
}
|
|
||||||
return { ...data, type: "string" };
|
|
||||||
};
|
|
||||||
|
|
||||||
interface BaseFilterParam {
|
interface BaseFilterParam {
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
@ -282,57 +286,84 @@ interface DateTimeFilterParam extends BaseFilterParam {
|
||||||
interface DefaultFilterParam extends BaseFilterParam {
|
interface DefaultFilterParam extends BaseFilterParam {
|
||||||
values: string[];
|
values: string[];
|
||||||
}
|
}
|
||||||
|
interface NumericFilterParam extends BaseFilterParam {
|
||||||
|
valuesRange: GteLte<number>;
|
||||||
|
}
|
||||||
|
export type FilterParam =
|
||||||
|
| BooleanFilterParam
|
||||||
|
| DateFilterParam
|
||||||
|
| DateTimeFilterParam
|
||||||
|
| DefaultFilterParam
|
||||||
|
| NumericFilterParam;
|
||||||
|
|
||||||
|
export const parseFilterValue = (
|
||||||
|
params: ProductListUrlFilters,
|
||||||
|
key: string,
|
||||||
|
type: ProductListUrlFiltersAsDictWithMultipleValues,
|
||||||
|
): FilterParam => {
|
||||||
|
const value = params[type][key];
|
||||||
|
const isMulti = params[type][key].length > 1;
|
||||||
|
|
||||||
|
const name = { slug: key };
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case ProductListUrlFiltersAsDictWithMultipleValues.booleanAttributes:
|
||||||
|
return { ...name, boolean: JSON.parse(value[0]) };
|
||||||
|
case ProductListUrlFiltersAsDictWithMultipleValues.dateAttributes:
|
||||||
|
return {
|
||||||
|
...name,
|
||||||
|
date: getGteLteVariables({
|
||||||
|
gte: value[0] || null,
|
||||||
|
lte: isMulti ? value[1] || null : value[0],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
case ProductListUrlFiltersAsDictWithMultipleValues.dateTimeAttributes:
|
||||||
|
return {
|
||||||
|
...name,
|
||||||
|
dateTime: getGteLteVariables({
|
||||||
|
gte: value[0] || null,
|
||||||
|
lte: isMulti ? value[1] || null : value[0],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
case ProductListUrlFiltersAsDictWithMultipleValues.numericAttributes:
|
||||||
|
const [gte, lte] = value.map(v => parseFloat(v));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...name,
|
||||||
|
valuesRange: {
|
||||||
|
gte: gte || undefined,
|
||||||
|
lte: isMulti ? lte || undefined : gte || undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return { ...name, values: value };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function getFilteredAttributeValue(
|
function getFilteredAttributeValue(
|
||||||
params: ProductListUrlFilters,
|
params: ProductListUrlFilters,
|
||||||
): Array<
|
): FilterParam[] {
|
||||||
| BooleanFilterParam
|
const attrValues = Object.values(
|
||||||
| BaseFilterParam
|
ProductListUrlFiltersAsDictWithMultipleValues,
|
||||||
| DateTimeFilterParam
|
).reduce<FilterParam[]>((attrValues, attributeType) => {
|
||||||
| DateFilterParam
|
const attributes = params[attributeType];
|
||||||
| DefaultFilterParam
|
|
||||||
> {
|
|
||||||
return !!params.attributes
|
|
||||||
? Object.keys(params.attributes).map(key => {
|
|
||||||
const { isMulti, type, value } = parseFilterValue(params, key);
|
|
||||||
const name = { slug: key };
|
|
||||||
|
|
||||||
switch (type) {
|
if (!attributes) {
|
||||||
case "boolean":
|
return attrValues;
|
||||||
return { ...name, boolean: JSON.parse(value[0]) };
|
}
|
||||||
|
|
||||||
case "date":
|
return [
|
||||||
return {
|
...attrValues,
|
||||||
...name,
|
...Object.keys(attributes).map(key =>
|
||||||
date: getGteLteVariables({
|
parseFilterValue(params, key, attributeType),
|
||||||
gte: value[0] || null,
|
),
|
||||||
lte: isMulti ? value[1] || null : value[0],
|
];
|
||||||
}),
|
}, []);
|
||||||
};
|
|
||||||
|
|
||||||
case "dateTime":
|
if (!attrValues.length) {
|
||||||
return {
|
return null;
|
||||||
...name,
|
}
|
||||||
dateTime: getGteLteVariables({
|
return attrValues;
|
||||||
gte: value[0] || null,
|
|
||||||
lte: isMulti ? value[1] || null : value[0],
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "numeric":
|
|
||||||
return {
|
|
||||||
...name,
|
|
||||||
valuesRange: {
|
|
||||||
gte: value[0] || undefined,
|
|
||||||
lte: isMulti ? value[1] || undefined : value[0] || undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
return { ...name, values: value };
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFilterVariables(
|
export function getFilterVariables(
|
||||||
|
@ -408,7 +439,7 @@ export function getFilterQueryParam(
|
||||||
|
|
||||||
case ProductFilterKeys.stock:
|
case ProductFilterKeys.stock:
|
||||||
return getSingleEnumValueQueryParam(
|
return getSingleEnumValueQueryParam(
|
||||||
filter as FilterElementRegular<ProductFilterKeys.stock>,
|
filter as FilterElementRegular<ProductFilterKeys>,
|
||||||
ProductListUrlFiltersEnum.stockStatus,
|
ProductListUrlFiltersEnum.stockStatus,
|
||||||
StockAvailability,
|
StockAvailability,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue