From 32d1a5b8c5fe3ab9a5a18ce939498da6148e84b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Tue, 1 Aug 2023 11:37:06 +0200 Subject: [PATCH] Experimental filters: add tests and fix wrong data send to core (#4039) --- .changeset/smart-gifts-applaud.md | 5 + .../ConditionalFilter/API/Handler.ts | 13 +- .../API/InitalStateResponse.test.ts | 83 +++++++++ .../API/InitialStateResponse.ts | 11 +- .../__snapshots__/helpers.test.ts.snap | 167 ++++++++++++++++++ .../API/initialState/helpers.test.ts | 111 ++++++++++++ .../ConditionalFilter/API/queries.ts | 2 + .../ConditionalFilter/ConditionalFilters.tsx | 21 ++- .../FilterElement/ConditionValue.ts | 12 +- .../ConditionalFilter/queryVariables.ts | 7 +- src/graphql/hooks.generated.ts | 2 + src/graphql/types.generated.ts | 4 +- src/products/views/ProductList/sort.ts | 2 +- 13 files changed, 413 insertions(+), 27 deletions(-) create mode 100644 .changeset/smart-gifts-applaud.md create mode 100644 src/components/ConditionalFilter/API/InitalStateResponse.test.ts create mode 100644 src/components/ConditionalFilter/API/initialState/__snapshots__/helpers.test.ts.snap create mode 100644 src/components/ConditionalFilter/API/initialState/helpers.test.ts diff --git a/.changeset/smart-gifts-applaud.md b/.changeset/smart-gifts-applaud.md new file mode 100644 index 000000000..083a5cc3a --- /dev/null +++ b/.changeset/smart-gifts-applaud.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Experimental filters: add unit tests and fix wrong data send to core diff --git a/src/components/ConditionalFilter/API/Handler.ts b/src/components/ConditionalFilter/API/Handler.ts index fe34a548b..998f73f97 100644 --- a/src/components/ConditionalFilter/API/Handler.ts +++ b/src/components/ConditionalFilter/API/Handler.ts @@ -28,13 +28,20 @@ export interface Handler { } export const createOptionsFromAPI = ( - // TODO: try to use type from graphql - data: Array<{ node: { name: string | null; id: string; slug: string } }>, + data: Array<{ + node: { + name: string | null; + id: string; + slug: string; + originalSlug?: string | null; + }; + }>, ): ItemOption[] => data.map(({ node }) => ({ label: node.name ?? "", value: node.id, slug: node.slug, + originalSlug: node.originalSlug, })); export class AttributeChoicesHandler implements Handler { @@ -170,6 +177,6 @@ export class BooleanValuesHandler implements Handler { constructor(public options: LeftOperand[]) {} fetch = async (): Promise => { - return this.options + return this.options; }; } diff --git a/src/components/ConditionalFilter/API/InitalStateResponse.test.ts b/src/components/ConditionalFilter/API/InitalStateResponse.test.ts new file mode 100644 index 000000000..7de3f603f --- /dev/null +++ b/src/components/ConditionalFilter/API/InitalStateResponse.test.ts @@ -0,0 +1,83 @@ +import { UrlEntry, UrlToken } from "../ValueProvider/UrlToken"; +import { InitialStateResponse } from "./InitialStateResponse"; + +describe("ConditionalFilter / API / InitialStateResponse", () => { + it("should filter by dynamic attribute token", () => { + const initialState = InitialStateResponse.empty(); + initialState.attribute = { + "attribute-1": { + choices: [ + { label: "Choice 1", value: "value-1", slug: "choice-1" }, + { label: "Choice 2", value: "value-2", slug: "choice-2" }, + ], + slug: "attribute-1", + value: "1", + label: "Attribute 1", + inputType: "DROPDOWN", + }, + }; + const token = UrlToken.fromUrlEntry( + new UrlEntry("o2.attribute-1", ["value-1"]), + ); + const expectedOutput = [ + { label: "Choice 1", slug: "choice-1", value: "value-1" }, + ]; + const result = initialState.filterByUrlToken(token); + expect(result).toEqual(expectedOutput); + }); + + it("should filter by static token type", () => { + const initialState = InitialStateResponse.empty(); + initialState.category = [ + { label: "Category 1", value: "1", slug: "category-1" }, + ]; + const token = UrlToken.fromUrlEntry( + new UrlEntry("s0.category-1", "category-1"), + ); + const expectedOutput = ["category-1"]; + const result = initialState.filterByUrlToken(token); + expect(result).toEqual(expectedOutput); + }); + + it("should filter by boolean attribute token", () => { + const initialState = InitialStateResponse.empty(); + initialState.attribute = { + "attribute-2": { + choices: [ + { label: "True", value: "true", slug: "true" }, + { label: "False", value: "false", slug: "false" }, + ], + slug: "attribute-2", + value: "2", + label: "Attribute 2", + inputType: "BOOLEAN", + }, + }; + const token = UrlToken.fromUrlEntry(new UrlEntry("b0.attribute-2", "true")); + const expectedOutput = { + label: "Yes", + slug: "true", + type: "BOOLEAN", + value: "true", + }; + const result = initialState.filterByUrlToken(token); + expect(result).toEqual(expectedOutput); + }); + + it("should filter by static attribute token", () => { + const initialState = InitialStateResponse.empty(); + initialState.attribute = { + size: { + value: "", + label: "Size", + slug: "size", + inputType: "NUMERIC", + choices: [], + }, + }; + + const token = UrlToken.fromUrlEntry(new UrlEntry("n0.size", "123")); + const result = initialState.filterByUrlToken(token); + expect(result).toEqual("123"); + }); +}); diff --git a/src/components/ConditionalFilter/API/InitialStateResponse.ts b/src/components/ConditionalFilter/API/InitialStateResponse.ts index 8e3a387c6..8de5d0a7f 100644 --- a/src/components/ConditionalFilter/API/InitialStateResponse.ts +++ b/src/components/ConditionalFilter/API/InitialStateResponse.ts @@ -6,7 +6,12 @@ import { ItemOption } from "../FilterElement/ConditionValue"; import { UrlToken } from "../ValueProvider/UrlToken"; export interface AttributeDTO { - choices: Array<{ label: string; value: string; slug: string }>; + choices: Array<{ + label: string; + value: string; + slug: string; + originalSlug?: string; + }>; inputType: AttributeInputType; label: string; slug: string; @@ -69,12 +74,12 @@ export class InitialStateResponse implements InitialState { return [token.value] as string[]; } - return this.getEntryByname(token.name).filter( + return this.getEntryByName(token.name).filter( ({ slug }) => slug && token.value.includes(slug), ); } - private getEntryByname(name: string) { + private getEntryByName(name: string) { switch (name) { case "category": return this.category; diff --git a/src/components/ConditionalFilter/API/initialState/__snapshots__/helpers.test.ts.snap b/src/components/ConditionalFilter/API/initialState/__snapshots__/helpers.test.ts.snap new file mode 100644 index 000000000..ef5fbf354 --- /dev/null +++ b/src/components/ConditionalFilter/API/initialState/__snapshots__/helpers.test.ts.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConditionalFilter / API / createInitialStateFromData should create initial state from queries 1`] = ` +Object { + "attribute": Object { + "attribute-1": Object { + "choices": Array [ + Object { + "label": "Choice 1", + "originalSlug": undefined, + "slug": "choice-1", + "value": "1", + }, + Object { + "label": "Choice 2", + "originalSlug": undefined, + "slug": "choice-2", + "value": "2", + }, + ], + "inputType": "MULTISELECT", + "label": "Attribute 1", + "slug": "attribute-1", + "value": "1", + }, + "attribute-2": Object { + "choices": Array [ + Object { + "label": "Yes", + "slug": "true", + "type": undefined, + "value": "true", + }, + Object { + "label": "No", + "slug": "false", + "type": undefined, + "value": "false", + }, + ], + "inputType": "BOOLEAN", + "label": "Attribute 2", + "slug": "attribute-2", + "value": "2", + }, + }, + "category": Array [ + Object { + "label": "Category 1", + "originalSlug": undefined, + "slug": "category-1", + "value": "1", + }, + Object { + "label": "Category 2", + "originalSlug": undefined, + "slug": "category-2", + "value": "2", + }, + ], + "channel": Array [ + Object { + "label": "Channel 1", + "slug": "channel-1", + "value": "1", + }, + ], + "collection": Array [ + Object { + "label": "Collection 1", + "originalSlug": undefined, + "slug": "collection-1", + "value": "1", + }, + Object { + "label": "Collection 2", + "originalSlug": undefined, + "slug": "collection-2", + "value": "2", + }, + ], + "giftCard": Array [ + Object { + "label": "Yes", + "slug": "true", + "type": undefined, + "value": "true", + }, + Object { + "label": "No", + "slug": "false", + "type": undefined, + "value": "false", + }, + ], + "hasCategory": Array [ + Object { + "label": "Yes", + "slug": "true", + "type": undefined, + "value": "true", + }, + Object { + "label": "No", + "slug": "false", + "type": undefined, + "value": "false", + }, + ], + "isAvailable": Array [ + Object { + "label": "Yes", + "slug": "true", + "type": undefined, + "value": "true", + }, + Object { + "label": "No", + "slug": "false", + "type": undefined, + "value": "false", + }, + ], + "isPublished": Array [ + Object { + "label": "Yes", + "slug": "true", + "type": undefined, + "value": "true", + }, + Object { + "label": "No", + "slug": "false", + "type": undefined, + "value": "false", + }, + ], + "isVisibleInListing": Array [ + Object { + "label": "Yes", + "slug": "true", + "type": undefined, + "value": "true", + }, + Object { + "label": "No", + "slug": "false", + "type": undefined, + "value": "false", + }, + ], + "productType": Array [ + Object { + "label": "Product Type 1", + "originalSlug": undefined, + "slug": "product-type-1", + "value": "1", + }, + Object { + "label": "Product Type 2", + "originalSlug": undefined, + "slug": "product-type-2", + "value": "2", + }, + ], +} +`; diff --git a/src/components/ConditionalFilter/API/initialState/helpers.test.ts b/src/components/ConditionalFilter/API/initialState/helpers.test.ts new file mode 100644 index 000000000..e6a11f7e0 --- /dev/null +++ b/src/components/ConditionalFilter/API/initialState/helpers.test.ts @@ -0,0 +1,111 @@ +import { ApolloQueryResult } from "@apollo/client"; +import { + _GetChannelOperandsQuery, + _SearchAttributeOperandsQuery, + _SearchCategoriesOperandsQuery, + _SearchCollectionsOperandsQuery, + _SearchProductTypesOperandsQuery, +} from "@dashboard/graphql"; + +import { createInitialStateFromData } from "./helpers"; + +describe("ConditionalFilter / API / createInitialStateFromData", () => { + const channelQuery = { + data: { + channels: [ + { id: "1", name: "Channel 1", slug: "channel-1" }, + { id: "2", name: "Channel 2", slug: "channel-2" }, + ], + }, + } as ApolloQueryResult<_GetChannelOperandsQuery>; + + const collectionQuery = { + data: { + collections: { + edges: [ + { node: { id: "1", name: "Collection 1", slug: "collection-1" } }, + { node: { id: "2", name: "Collection 2", slug: "collection-2" } }, + ], + }, + }, + } as ApolloQueryResult<_SearchCollectionsOperandsQuery>; + + const categoryQuery = { + data: { + categories: { + edges: [ + { node: { id: "1", name: "Category 1", slug: "category-1" } }, + { node: { id: "2", name: "Category 2", slug: "category-2" } }, + ], + }, + }, + } as ApolloQueryResult<_SearchCategoriesOperandsQuery>; + + const productTypeQuery = { + data: { + productTypes: { + edges: [ + { node: { id: "1", name: "Product Type 1", slug: "product-type-1" } }, + { node: { id: "2", name: "Product Type 2", slug: "product-type-2" } }, + ], + }, + }, + } as ApolloQueryResult<_SearchProductTypesOperandsQuery>; + + const attributeQuery = { + data: { + attributes: { + edges: [ + { + node: { + id: "1", + name: "Attribute 1", + slug: "attribute-1", + inputType: "MULTISELECT", + choices: { + edges: [ + { + node: { + id: "1", + name: "Choice 1", + slug: "choice-1", + }, + }, + { + node: { + id: "2", + name: "Choice 2", + slug: "choice-2", + }, + }, + ], + }, + }, + }, + { + node: { + id: "2", + name: "Attribute 2", + slug: "attribute-2", + inputType: "BOOLEAN", + }, + }, + ], + }, + }, + } as ApolloQueryResult<_SearchAttributeOperandsQuery>; + + it("should create initial state from queries", () => { + const data = [ + channelQuery, + collectionQuery, + categoryQuery, + productTypeQuery, + attributeQuery, + ]; + const channel = ["channel-1"]; + + const result = createInitialStateFromData(data, channel); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/src/components/ConditionalFilter/API/queries.ts b/src/components/ConditionalFilter/API/queries.ts index 98b483a2f..a45fbaf9c 100644 --- a/src/components/ConditionalFilter/API/queries.ts +++ b/src/components/ConditionalFilter/API/queries.ts @@ -114,6 +114,7 @@ export const initialDynamicOperands = gql` slug: id id name + originalSlug: slug } } } @@ -132,6 +133,7 @@ export const dynamicOperandsQueries = gql` slug: id id name + originalSlug: slug } } } diff --git a/src/components/ConditionalFilter/ConditionalFilters.tsx b/src/components/ConditionalFilter/ConditionalFilters.tsx index b670d9e1c..19c44739e 100644 --- a/src/components/ConditionalFilter/ConditionalFilters.tsx +++ b/src/components/ConditionalFilter/ConditionalFilters.tsx @@ -11,18 +11,18 @@ export const ConditionalFilters: FC<{ onClose: () => void }> = ({ onClose, }) => { const { valueProvider, containerState } = useConditionalFilterContext(); - const [errors, setErrors] = useState([]) + const [errors, setErrors] = useState([]); const handleConfirm = (value: FilterContainer) => { - const validator = new Validator(value) + const validator = new Validator(value); if (validator.isValid()) { valueProvider.persist(value); onClose(); - return + return; } - setErrors(validator.getErrors()) + setErrors(validator.getErrors()); }; const handleCancel = () => { @@ -34,13 +34,12 @@ export const ConditionalFilters: FC<{ onClose: () => void }> = ({ return valueProvider.loading ? ( ) : ( - - + + ); }; diff --git a/src/components/ConditionalFilter/FilterElement/ConditionValue.ts b/src/components/ConditionalFilter/FilterElement/ConditionValue.ts index 5a303e3a0..42b677c40 100644 --- a/src/components/ConditionalFilter/FilterElement/ConditionValue.ts +++ b/src/components/ConditionalFilter/FilterElement/ConditionValue.ts @@ -2,6 +2,8 @@ export interface ItemOption { label: string; value: string; slug: string; + // TODO: remove this when https://github.com/saleor/saleor/issues/13076 is ready + originalSlug?: string | null; } export type ConditionValue = @@ -11,16 +13,16 @@ export type ConditionValue = | string[] | [string, string]; - export const isItemOption = (x: ConditionValue): x is ItemOption => - typeof x === "object" && "value" in x + typeof x === "object" && "value" in x; export const isItemOptionArray = (x: ConditionValue): x is ItemOption[] => - Array.isArray(x) && (x as ItemOption[]).every(isItemOption) + Array.isArray(x) && (x as ItemOption[]).every(isItemOption); export const isTuple = (x: ConditionValue): x is [string, string] => - Array.isArray(x) && x.length === 2 && (x as string[]).every(y => typeof y === "string") - + Array.isArray(x) && + x.length === 2 && + (x as string[]).every(y => typeof y === "string"); export const slugFromConditionValue = ( rawEntry: ConditionValue, diff --git a/src/components/ConditionalFilter/queryVariables.ts b/src/components/ConditionalFilter/queryVariables.ts index e74c0d7b1..c5f8a5968 100644 --- a/src/components/ConditionalFilter/queryVariables.ts +++ b/src/components/ConditionalFilter/queryVariables.ts @@ -112,11 +112,14 @@ const createAttributeQueryPart = ( } if (isItemOption(value)) { - return { slug: attributeSlug, values: [value.value] }; + return { slug: attributeSlug, values: [value.originalSlug || value.value] }; } if (isItemOptionArray(value)) { - return { slug: attributeSlug, values: value.map(x => x.value) }; + return { + slug: attributeSlug, + values: value.map(x => x.originalSlug || x.value), + }; } if (typeof value === "string") { diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 7a147480d..84e49e605 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -5687,6 +5687,7 @@ export const _SearchAttributeOperandsDocument = gql` slug: id id name + originalSlug: slug } } } @@ -5734,6 +5735,7 @@ export const _GetAttributeChoicesDocument = gql` slug: id id name + originalSlug: slug } } } diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index ef86e4f97..79939a3e1 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -8409,7 +8409,7 @@ export type _SearchAttributeOperandsQueryVariables = Exact<{ }>; -export type _SearchAttributeOperandsQuery = { __typename: 'Query', attributes: { __typename: 'AttributeCountableConnection', edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, choices: { __typename: 'AttributeValueCountableConnection', edges: Array<{ __typename: 'AttributeValueCountableEdge', node: { __typename: 'AttributeValue', id: string, name: string | null, slug: string } }> } | null } }> } | null }; +export type _SearchAttributeOperandsQuery = { __typename: 'Query', attributes: { __typename: 'AttributeCountableConnection', edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, choices: { __typename: 'AttributeValueCountableConnection', edges: Array<{ __typename: 'AttributeValueCountableEdge', node: { __typename: 'AttributeValue', id: string, name: string | null, slug: string, originalSlug: string | null } }> } | null } }> } | null }; export type _GetAttributeChoicesQueryVariables = Exact<{ slug: Scalars['String']; @@ -8418,7 +8418,7 @@ export type _GetAttributeChoicesQueryVariables = Exact<{ }>; -export type _GetAttributeChoicesQuery = { __typename: 'Query', attribute: { __typename: 'Attribute', choices: { __typename: 'AttributeValueCountableConnection', edges: Array<{ __typename: 'AttributeValueCountableEdge', node: { __typename: 'AttributeValue', id: string, name: string | null, slug: string } }> } | null } | null }; +export type _GetAttributeChoicesQuery = { __typename: 'Query', attribute: { __typename: 'Attribute', choices: { __typename: 'AttributeValueCountableConnection', edges: Array<{ __typename: 'AttributeValueCountableEdge', node: { __typename: 'AttributeValue', id: string, name: string | null, slug: string, originalSlug: string | null } }> } | null } | null }; export type _GetCollectionsChoicesQueryVariables = Exact<{ first: Scalars['Int']; diff --git a/src/products/views/ProductList/sort.ts b/src/products/views/ProductList/sort.ts index dbe528ca0..bb30b9d13 100644 --- a/src/products/views/ProductList/sort.ts +++ b/src/products/views/ProductList/sort.ts @@ -66,7 +66,7 @@ export function getSortQueryVariables( } const field = getSortQueryField(params.sort); - // TODO: how to handle search & sort + // TODO: apply fix after https://github.com/saleor/saleor/issues/13557 is done return { direction, field,