From 198341cb4182242602d8aa650975bc4bc19f0309 Mon Sep 17 00:00:00 2001 From: Patryk Andrzejewski Date: Thu, 29 Jun 2023 14:10:19 +0200 Subject: [PATCH] New filters for the product listing page (prototype) (#3811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Expression filters * Filters * Tokenizing * Tokenizing * feat: fetch inital state from API * fix: integrate with code * Loading * feat: add attribute name & label * feat: move input type * Loading * feat: update left operator + condition * feat: fetch inital options on focus * feat: fetch right options on autocomplete * Flags * Refactor * fix: add loading state * fix: after changes * fix: remove debugger * fix: proper selected setting * Refactor * Display properly * Display properly * Display properly * feat: fetch left options * Persist * feat: add loading state * feat: refactor getAPIOptions * feat: add additional checks to filter element * feat: use debounce * FilterArray * FilterArray * Modeling * fix: filters in popover * feat: use new macaw ui version * Types * Feature flag * fix: type errors * Alignment * Fix api * feat: add slug * feat: add slug * feat: add slug for the last time * fix: return slug from left options * Fix combobox * Force slug * Changeset * fix: serialize value --------- Co-authored-by: Krzysztof Żuraw <9116238+krzysztofzuraw@users.noreply.github.com> --- .changeset/olive-bikes-switch.md | 5 + package-lock.json | 14 +- package.json | 6 +- .../AppLayout/ListFilters/ListFilters.tsx | 69 +-- .../components/ExpressionFilters.tsx | 17 + .../API/InitialStateResponse.ts | 56 +++ .../ConditionalFilter/API/getAPIOptions.tsx | 211 +++++++++ .../API/getInitalAPIState.tsx | 177 +++++++ .../ConditionalFilter/API/queries.ts | 156 +++++++ .../FilterElement/Condition.ts | 80 ++++ .../FilterElement/ConditionOptions.ts | 79 ++++ .../FilterElement/ConditionSelected.ts | 68 +++ .../FilterElement/FilterElement.ts | 179 +++++++ .../ConditionalFilter/FilterElement/index.ts | 2 + .../ConditionalFilter/FilterValueProvider.ts | 7 + .../TokenArray/fetchingParams.ts | 40 ++ .../ValueProvider/TokenArray/index.ts | 106 +++++ .../ValueProvider/UrlToken.ts | 47 ++ .../ValueProvider/useUrlValueProvider.ts | 51 ++ src/components/ConditionalFilter/constants.ts | 30 ++ .../ConditionalFilter/controlsType.ts | 14 + src/components/ConditionalFilter/index.tsx | 142 ++++++ .../ConditionalFilter/useFilterContainer.ts | 125 +++++ .../ConditionalFilter/useLeftOperands.ts | 34 ++ src/featureFlags/availableFlags.ts | 14 +- src/graphql/hooks.generated.ts | 439 ++++++++++++++++++ src/graphql/types.generated.ts | 81 ++++ 27 files changed, 2200 insertions(+), 49 deletions(-) create mode 100644 .changeset/olive-bikes-switch.md create mode 100644 src/components/AppLayout/ListFilters/components/ExpressionFilters.tsx create mode 100644 src/components/ConditionalFilter/API/InitialStateResponse.ts create mode 100644 src/components/ConditionalFilter/API/getAPIOptions.tsx create mode 100644 src/components/ConditionalFilter/API/getInitalAPIState.tsx create mode 100644 src/components/ConditionalFilter/API/queries.ts create mode 100644 src/components/ConditionalFilter/FilterElement/Condition.ts create mode 100644 src/components/ConditionalFilter/FilterElement/ConditionOptions.ts create mode 100644 src/components/ConditionalFilter/FilterElement/ConditionSelected.ts create mode 100644 src/components/ConditionalFilter/FilterElement/FilterElement.ts create mode 100644 src/components/ConditionalFilter/FilterElement/index.ts create mode 100644 src/components/ConditionalFilter/FilterValueProvider.ts create mode 100644 src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.ts create mode 100644 src/components/ConditionalFilter/ValueProvider/TokenArray/index.ts create mode 100644 src/components/ConditionalFilter/ValueProvider/UrlToken.ts create mode 100644 src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts create mode 100644 src/components/ConditionalFilter/constants.ts create mode 100644 src/components/ConditionalFilter/controlsType.ts create mode 100644 src/components/ConditionalFilter/index.tsx create mode 100644 src/components/ConditionalFilter/useFilterContainer.ts create mode 100644 src/components/ConditionalFilter/useLeftOperands.ts diff --git a/.changeset/olive-bikes-switch.md b/.changeset/olive-bikes-switch.md new file mode 100644 index 000000000..3f46b4192 --- /dev/null +++ b/.changeset/olive-bikes-switch.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Prototype of the new filters for product listing page diff --git a/package-lock.json b/package-lock.json index c43cc1c2c..e068ec56c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/styles": "^4.11.4", "@reach/auto-id": "^0.16.0", - "@saleor/macaw-ui": "0.8.0-pre.98", + "@saleor/macaw-ui": "0.8.0-pre.101", "@saleor/sdk": "0.6.0", "@sentry/react": "^6.0.0", "@types/faker": "^5.1.6", @@ -7840,9 +7840,9 @@ } }, "node_modules/@saleor/macaw-ui": { - "version": "0.8.0-pre.98", - "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.98.tgz", - "integrity": "sha512-W/KCjRoVVr751JxPET/ebXt9im5lleCGAtydFcuDwmxoU11wTlyxHM25owsJJBpTq1L+jdrMyXO/vZ1FL06Osg==", + "version": "0.8.0-pre.101", + "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.101.tgz", + "integrity": "sha512-q68uQs33L8CEjQkZyfRIaljOiRNV6dyIMR211VYdWtAs60jsozwfwnoifPMz3m1DBBJbbaHjwsC7dD9AdlaR2w==", "dependencies": { "@dessert-box/react": "^0.4.0", "@floating-ui/react-dom-interactions": "^0.5.0", @@ -40651,9 +40651,9 @@ } }, "@saleor/macaw-ui": { - "version": "0.8.0-pre.98", - "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.98.tgz", - "integrity": "sha512-W/KCjRoVVr751JxPET/ebXt9im5lleCGAtydFcuDwmxoU11wTlyxHM25owsJJBpTq1L+jdrMyXO/vZ1FL06Osg==", + "version": "0.8.0-pre.101", + "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.101.tgz", + "integrity": "sha512-q68uQs33L8CEjQkZyfRIaljOiRNV6dyIMR211VYdWtAs60jsozwfwnoifPMz3m1DBBJbbaHjwsC7dD9AdlaR2w==", "requires": { "@dessert-box/react": "^0.4.0", "@floating-ui/react-dom-interactions": "^0.5.0", diff --git a/package.json b/package.json index 1df571a31..b40c4b471 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/styles": "^4.11.4", "@reach/auto-id": "^0.16.0", - "@saleor/macaw-ui": "0.8.0-pre.98", + "@saleor/macaw-ui": "0.8.0-pre.101", "@saleor/sdk": "0.6.0", "@sentry/react": "^6.0.0", "@types/faker": "^5.1.6", @@ -96,8 +96,6 @@ "use-react-router": "^1.0.7" }, "devDependencies": { - "@changesets/changelog-github": "^0.4.8", - "@changesets/cli": "^2.26.1", "@babel/cli": "^7.5.5", "@babel/core": "^7.7.7", "@babel/plugin-proposal-class-properties": "^7.5.0", @@ -110,6 +108,8 @@ "@babel/preset-react": "^7.7.4", "@babel/preset-typescript": "^7.13.0", "@babel/runtime": "^7.7.6", + "@changesets/changelog-github": "^0.4.8", + "@changesets/cli": "^2.26.1", "@editorjs/embed": "^2.5.3", "@esbuild-plugins/node-globals-polyfill": "^0.1.1", "@formatjs/cli": "^4.5.0", diff --git a/src/components/AppLayout/ListFilters/ListFilters.tsx b/src/components/AppLayout/ListFilters/ListFilters.tsx index ffcfc1901..759f27cf7 100644 --- a/src/components/AppLayout/ListFilters/ListFilters.tsx +++ b/src/components/AppLayout/ListFilters/ListFilters.tsx @@ -1,8 +1,10 @@ import { FilterErrorMessages, IFilter } from "@dashboard/components/Filter"; +import { useFlag } from "@dashboard/featureFlags"; import { FilterProps, SearchPageProps } from "@dashboard/types"; import { Box } from "@saleor/macaw-ui/next"; import React, { ReactNode } from "react"; +import { ExpressionFilters } from "./components/ExpressionFilters"; import { FiltersSelect } from "./components/FiltersSelect"; import SearchInput from "./components/SearchInput"; @@ -25,36 +27,45 @@ export const ListFilters = ({ onFilterAttributeFocus, errorMessages, actions, -}: ListFiltersProps) => ( - <> - - - +}: ListFiltersProps) => { + const isProductPage = window.location.pathname.includes("/products"); + const productListingPageFiltersFlag = useFlag("product_filters"); + const filtersEnabled = isProductPage && productListingPageFiltersFlag.enabled; - - + return ( + <> + + + {filtersEnabled ? ( + + ) : ( + + )} + + + + + + {actions} - - {actions} - - - -); + + ); +}; ListFilters.displayName = "FilterBar"; diff --git a/src/components/AppLayout/ListFilters/components/ExpressionFilters.tsx b/src/components/AppLayout/ListFilters/components/ExpressionFilters.tsx new file mode 100644 index 000000000..0ae82fd2d --- /dev/null +++ b/src/components/AppLayout/ListFilters/components/ExpressionFilters.tsx @@ -0,0 +1,17 @@ +import { ConditionalFilters } from "@dashboard/components/ConditionalFilter"; +import { Box, Button, Popover } from "@saleor/macaw-ui/next"; +import React from "react"; + +export const ExpressionFilters = () => ( + + + + + + + + + + + +); diff --git a/src/components/ConditionalFilter/API/InitialStateResponse.ts b/src/components/ConditionalFilter/API/InitialStateResponse.ts new file mode 100644 index 000000000..1f24cb4f4 --- /dev/null +++ b/src/components/ConditionalFilter/API/InitialStateResponse.ts @@ -0,0 +1,56 @@ +import { AttributeInputType } from "../FilterElement/ConditionOptions"; +import { ItemOption } from "../FilterElement/ConditionSelected"; +import { UrlToken } from "../ValueProvider/UrlToken"; + +interface AttributeDTO { + choices: Array<{ label: string; value: string; slug: string }>; + inputType: AttributeInputType; + label: string; + slug: string; + value: string; +} + +export class InitialStateResponse { + constructor( + public category: ItemOption[], + public attribute: Record, + public channel: ItemOption[], + public collection: ItemOption[], + public producttype: ItemOption[], + ) {} + + public attributeByName(name: string) { + return this.attribute[name]; + } + + public filterByUrlToken(token: UrlToken) { + if (token.isAttribute()) { + return this.attribute[token.name].choices.filter(({ value }) => + token.value.includes(value), + ); + } + + if (!token.isLoadable()) { + return [token.value] as string[]; + } + + return this.getEntryByname(token.name).filter( + ({ slug }) => slug && token.value.includes(slug), + ); + } + + private getEntryByname(name: string) { + switch (name) { + case "category": + return this.category; + case "collection": + return this.collection; + case "producttype": + return this.producttype; + case "channel": + return this.channel; + default: + return []; + } + } +} diff --git a/src/components/ConditionalFilter/API/getAPIOptions.tsx b/src/components/ConditionalFilter/API/getAPIOptions.tsx new file mode 100644 index 000000000..142de11f1 --- /dev/null +++ b/src/components/ConditionalFilter/API/getAPIOptions.tsx @@ -0,0 +1,211 @@ +// @ts-strict-ignore +import { ApolloClient } from "@apollo/client"; +import { + _GetAttributeChoicesDocument, + _GetCategoriesChoicesDocument, + _GetChannelOperandsDocument, + _GetCollectionsChoicesDocument, + _GetDynamicLeftOperandsDocument, + _GetProductTypesChoicesDocument, +} from "@dashboard/graphql"; + +import { FilterElement } from "../FilterElement"; +import { FilterContainer } from "../useFilterContainer"; + +const getFilterElement = (value: any, index: number): FilterElement => { + const possibleFilterElement = value[index]; + return typeof possibleFilterElement != "string" + ? possibleFilterElement + : null; +}; + +export const getInitialRightOperatorOptions = async ( + client: ApolloClient, + position: string, + value: FilterContainer, +) => { + const index = parseInt(position, 10); + const filterElement = getFilterElement(value, index); + + if (filterElement.isAttribute()) { + const { data } = await client.query({ + query: _GetAttributeChoicesDocument, + variables: { + slug: filterElement.value.value, + first: 5, + query: "", + }, + }); + return data.attribute.choices.edges.map(({ node }) => ({ + label: node.name, + value: node.id, + slug: node.slug, + })); + } + + if (filterElement.isCollection()) { + const { data } = await client.query({ + query: _GetCollectionsChoicesDocument, + variables: { + first: 5, + query: "", + }, + }); + + return data.collections.edges.map(({ node }) => ({ + label: node.name, + value: node.id, + slug: node.slug, + })); + } + + if (filterElement.isCategory()) { + const { data } = await client.query({ + query: _GetCategoriesChoicesDocument, + variables: { + first: 5, + query: "", + }, + }); + + return data.categories.edges.map(({ node }) => ({ + label: node.name, + value: node.id, + slug: node.slug, + })); + } + + if (filterElement.isProductType()) { + const { data } = await client.query({ + query: _GetProductTypesChoicesDocument, + variables: { + first: 5, + query: "", + }, + }); + + return data.productTypes.edges.map(({ node }) => ({ + label: node.name, + value: node.id, + slug: node.slug, + })); + } + + if (filterElement.isChannel()) { + const { data } = await client.query({ + query: _GetChannelOperandsDocument, + }); + + return data.channels.map(({ id, name, slug }) => ({ + label: name, + value: id, + slug, + })); + } +}; + +export const getRightOperatorOptionsByQuery = async ( + client: ApolloClient, + position: string, + value: FilterContainer, + inputValue: string, +) => { + const index = parseInt(position, 10); + const filterElement = getFilterElement(value, index); + + if (filterElement.isAttribute()) { + const { data } = await client.query({ + query: _GetAttributeChoicesDocument, + variables: { + slug: filterElement.value.value, + first: 5, + query: inputValue, + }, + }); + return data.attribute.choices.edges.map(({ node }) => ({ + label: node.name, + value: node.id, + })); + } + + if (filterElement.isCollection()) { + const { data } = await client.query({ + query: _GetCollectionsChoicesDocument, + variables: { + first: 5, + query: inputValue, + }, + }); + + return data.collections.edges.map(({ node }) => ({ + label: node.name, + value: node.id, + slug: node.slug, + })); + } + + if (filterElement.isCategory()) { + const { data } = await client.query({ + query: _GetCategoriesChoicesDocument, + variables: { + first: 5, + query: inputValue, + }, + }); + + return data.categories.edges.map(({ node }) => ({ + label: node.name, + value: node.id, + slug: node.slug, + })); + } + + if (filterElement.isProductType()) { + const { data } = await client.query({ + query: _GetProductTypesChoicesDocument, + variables: { + first: 5, + query: inputValue, + }, + }); + + return data.productTypes.edges.map(({ node }) => ({ + label: node.name, + value: node.id, + })); + } + + if (filterElement.isChannel()) { + const { data } = await client.query({ + query: _GetChannelOperandsDocument, + }); + const options = data.channels.map(({ id, name, slug }) => ({ + label: name, + value: id, + slug, + })); + + return options.filter(({ label }) => + label.toLowerCase().includes(inputValue.toLowerCase()), + ); + } +}; + +export const getLeftOperatorOptions = async ( + client: any, + inputValue: string, +) => { + const { data } = await client.query({ + query: _GetDynamicLeftOperandsDocument, + variables: { + first: 5, + query: inputValue, + }, + }); + return data.attributes.edges.map(({ node }) => ({ + label: node.name, + value: node.id, + type: node.inputType, + slug: node.slug, + })); +}; diff --git a/src/components/ConditionalFilter/API/getInitalAPIState.tsx b/src/components/ConditionalFilter/API/getInitalAPIState.tsx new file mode 100644 index 000000000..9bbf4e0ee --- /dev/null +++ b/src/components/ConditionalFilter/API/getInitalAPIState.tsx @@ -0,0 +1,177 @@ +// @ts-strict-ignore +import { useApolloClient } from "@apollo/client"; +import { + _GetChannelOperandsDocument, + _SearchAttributeOperandsDocument, + _SearchCategoriesOperandsDocument, + _SearchCollectionsOperandsDocument, + _SearchProductTypesOperandsDocument, +} from "@dashboard/graphql"; +import { useEffect, useState } from "react"; + +import { InitialStateResponse } from "./InitialStateResponse"; + +interface Props { + category?: string[]; + collection?: string[]; + channel?: string[]; + producttype?: string[]; + attribute?: { + [attribute: string]: string[]; + }; +} + +export const useInitialAPIState = ({ + category = [], + collection = [], + producttype = [], + channel = [], + attribute = {}, +}: Props) => { + const client = useApolloClient(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const queriesToRun = []; + + const fetchQueries = async () => { + const data = await Promise.all(queriesToRun); + setData(data); + setLoading(false); + }; + + if (channel.length > 0) { + queriesToRun.push( + client.query({ + query: _GetChannelOperandsDocument, + }), + ); + } else { + queriesToRun.push({}); + } + + if (collection.length > 0) { + queriesToRun.push( + client.query({ + query: _SearchCollectionsOperandsDocument, + variables: { + collectionsSlugs: collection, + first: collection.length, + }, + }), + ); + } else { + queriesToRun.push({}); + } + + if (category.length > 0) { + queriesToRun.push( + client.query({ + query: _SearchCategoriesOperandsDocument, + variables: { + categoriesSlugs: category, + first: category.length, + }, + }), + ); + } else { + queriesToRun.push({}); + } + + if (producttype.length > 0) { + queriesToRun.push( + client.query({ + query: _SearchProductTypesOperandsDocument, + variables: { + productTypesSlugs: producttype, + first: producttype.length, + }, + }), + ); + } else { + queriesToRun.push({}); + } + + if (Object.keys(attribute).length > 0) { + queriesToRun.push( + client.query({ + query: _SearchAttributeOperandsDocument, + variables: { + attributesSlugs: Object.keys(attribute), + choicesIds: Object.values(attribute).flat(), + first: Object.keys(attribute).length, + }, + }), + ); + } else { + queriesToRun.push({}); + } + + fetchQueries(); + }, []); + + const [ + channelData, + collectionData, + categoryData, + productTypesData, + attributesData, + ] = data; + + const channelPicks = + channelData?.data?.channels + ?.filter(({ slug }) => channel.includes(slug)) + .map(({ id, name }) => ({ label: name, value: id })) ?? []; + + const collectionPicks = + collectionData?.data?.search?.edges.map(({ node }) => ({ + label: node?.name, + value: node?.id, + slug: node?.slug, + })) ?? []; + + const categoryPicks = + categoryData?.data?.search?.edges.map(({ node }) => ({ + label: node?.name, + value: node?.id, + slug: node?.slug, + })) ?? []; + + const productTypePicks = + productTypesData?.data?.search?.edges.map(({ node }) => ({ + label: node?.name, + value: node?.id, + slug: node?.slug, + })) ?? []; + + const attributePicks = + attributesData?.data?.search?.edges.reduce( + (acc, { node }) => ({ + ...acc, + [node?.slug]: { + choices: node?.choices.edges.map(({ node }) => ({ + label: node?.name, + value: node?.id, + slug: node?.slug, + })), + slug: node?.slug, + value: node?.id, + label: node?.name, + inputType: node?.inputType, + }, + }), + {}, + ) ?? {}; + + return { + data: new InitialStateResponse( + categoryPicks, + attributePicks, + channelPicks, + collectionPicks, + productTypePicks, + ), + loading, + }; +}; diff --git a/src/components/ConditionalFilter/API/queries.ts b/src/components/ConditionalFilter/API/queries.ts new file mode 100644 index 000000000..e21b7e0c5 --- /dev/null +++ b/src/components/ConditionalFilter/API/queries.ts @@ -0,0 +1,156 @@ +import { gql } from "@apollo/client"; + +export const initialDynamicLeftOperands = gql` + query _GetDynamicLeftOperands($first: Int!, $query: String!) { + attributes(first: $first, filter: { type: PRODUCT_TYPE, search: $query }) { + edges { + node { + id + name + slug + inputType + } + } + } + } +`; + +export const initialDynamicOperands = gql` + query _GetChannelOperands { + channels { + id + name + slug + } + } + + query _SearchCollectionsOperands($first: Int!, $collectionsSlugs: [String!]) { + search: collections(first: $first, filter: { slugs: $collectionsSlugs }) { + edges { + node { + id + name + slug + } + } + } + } + + query _SearchCategoriesOperands( + $after: String + $first: Int! + $categoriesSlugs: [String!] + ) { + search: categories( + after: $after + first: $first + filter: { slugs: $categoriesSlugs } + ) { + edges { + node { + id + name + slug + } + } + } + } + + query _SearchProductTypesOperands( + $after: String + $first: Int! + $productTypesSlugs: [String!] + ) { + search: productTypes( + after: $after + first: $first + filter: { slugs: $productTypesSlugs } + ) { + edges { + node { + id + name + slug + } + } + } + } + + query _SearchAttributeOperands( + $attributesSlugs: [String!] + $choicesIds: [ID!] + $first: Int! + ) { + search: attributes(first: $first, filter: { slugs: $attributesSlugs }) { + edges { + node { + id + name + slug + inputType + choices(first: 5, filter: { ids: $choicesIds }) { + edges { + node { + slug: id + id + name + } + } + } + } + } + } + } +`; + +export const dynamicOperandsQueries = gql` + query _GetAttributeChoices($slug: String!, $first: Int!, $query: String!) { + attribute(slug: $slug) { + choices(first: $first, filter: { search: $query }) { + edges { + node { + slug: id + id + name + } + } + } + } + } + + query _GetCollectionsChoices($first: Int!, $query: String!) { + collections(first: $first, filter: { search: $query }) { + edges { + node { + id + name + slug + } + } + } + } + + query _GetCategoriesChoices($first: Int!, $query: String!) { + categories(first: $first, filter: { search: $query }) { + edges { + node { + id + name + slug + } + } + } + } + + query _GetProductTypesChoices($first: Int!, $query: String!) { + productTypes(first: $first, filter: { search: $query }) { + edges { + node { + id + name + slug + } + } + } + } +`; diff --git a/src/components/ConditionalFilter/FilterElement/Condition.ts b/src/components/ConditionalFilter/FilterElement/Condition.ts new file mode 100644 index 000000000..d4bacf619 --- /dev/null +++ b/src/components/ConditionalFilter/FilterElement/Condition.ts @@ -0,0 +1,80 @@ +/* eslint-disable @typescript-eslint/member-ordering */ +import { InitialStateResponse } from "../API/InitialStateResponse"; +import { LeftOperand } from "./../useLeftOperands"; +import { UrlToken } from "./../ValueProvider/UrlToken"; +import { ConditionOptions } from "./ConditionOptions"; +import { ConditionSelected } from "./ConditionSelected"; + +export class Condition { + private constructor( + public options: ConditionOptions, + public selected: ConditionSelected, + public loading: boolean, + ) {} + + public enableLoading() { + this.loading = true; + } + + public disableLoading() { + this.loading = false; + } + + public isLoading() { + return this.loading; + } + + public static createEmpty() { + return new Condition( + ConditionOptions.empty(), + ConditionSelected.empty(), + false, + ); + } + + public static emptyFromLeftOperand(operand: LeftOperand) { + const options = ConditionOptions.fromName(operand.type); + + return new Condition( + options, + ConditionSelected.fromConditionItem(options.first()), + false, + ); + } + + public static fromUrlToken(token: UrlToken, response: InitialStateResponse) { + if (ConditionOptions.isStaticName(token.name)) { + const staticOptions = ConditionOptions.fromStaticElementName(token.name); + const selectedOption = staticOptions.findByLabel(token.conditionKind); + const valueItems = response.filterByUrlToken(token); + const value = + selectedOption?.type === "multiselect" && valueItems.length > 0 + ? valueItems + : valueItems[0]; + + if (!selectedOption) { + return Condition.createEmpty(); + } + + return new Condition( + staticOptions, + ConditionSelected.fromConditionItemAndValue(selectedOption, value), + false, + ); + } + + if (token.isAttribute()) { + const attribute = response.attributeByName(token.name); + const options = ConditionOptions.fromAtributeType(attribute.inputType); + const value = response.filterByUrlToken(token); + + return new Condition( + options, + ConditionSelected.fromConditionItemAndValue(options.first(), value), + false, + ); + } + + return Condition.createEmpty(); + } +} diff --git a/src/components/ConditionalFilter/FilterElement/ConditionOptions.ts b/src/components/ConditionalFilter/FilterElement/ConditionOptions.ts new file mode 100644 index 000000000..144d64e04 --- /dev/null +++ b/src/components/ConditionalFilter/FilterElement/ConditionOptions.ts @@ -0,0 +1,79 @@ +import { + ATTRIBUTE_INPUT_TYPE_CONDITIONS, + STATIC_CONDITIONS, +} from "../constants"; + +export type StaticElementName = keyof typeof STATIC_CONDITIONS; +export type AttributeInputType = keyof typeof ATTRIBUTE_INPUT_TYPE_CONDITIONS; + +export interface ConditionItem { + type: string; + label: string; + value: string; +} + +export class ConditionOptions extends Array { + private constructor(options: ConditionItem[] | number) { + if (Array.isArray(options)) { + super(...options); + return; + } + + super(options); + } + + public static isStaticName(name: string): name is StaticElementName { + return name in STATIC_CONDITIONS; + } + + public static isAttributeInputType(name: string): name is AttributeInputType { + return name in ATTRIBUTE_INPUT_TYPE_CONDITIONS; + } + + public static fromAtributeType(inputType: AttributeInputType) { + const options = ATTRIBUTE_INPUT_TYPE_CONDITIONS[inputType]; + + if (!options) { + throw new Error(`Unsupported attribute input type "${inputType}"`); + } + + return new ConditionOptions(options); + } + public static fromStaticElementName(name: StaticElementName) { + const options = STATIC_CONDITIONS[name]; + + if (!options) { + throw new Error(`Unsupported static element "${name}"`); + } + + return new ConditionOptions(options); + } + + public static fromName(name: AttributeInputType | StaticElementName) { + const optionsStatic = this.isStaticName(name) && STATIC_CONDITIONS[name]; + const optionsAttribute = + this.isAttributeInputType(name) && ATTRIBUTE_INPUT_TYPE_CONDITIONS[name]; + + if (optionsStatic) { + return new ConditionOptions(optionsStatic); + } + + if (optionsAttribute) { + return new ConditionOptions(optionsAttribute); + } + + throw new Error(`Unsupported condition element "${name}"`); + } + + public static empty() { + return new ConditionOptions([]); + } + + public findByLabel(label: string) { + return this.find(f => f.label === label); + } + + public first() { + return this[0]; + } +} diff --git a/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts b/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts new file mode 100644 index 000000000..1f67b5c53 --- /dev/null +++ b/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts @@ -0,0 +1,68 @@ +import { getDefaultByControlName } from "../controlsType"; +import { ConditionItem } from "./ConditionOptions"; + +export interface ItemOption { + label: string; + value: string; + slug?: string; +} + +export type ConditionOption = + | ItemOption + | ItemOption[] + | string + | string[] + | [string, string]; + +export class ConditionSelected { + private constructor( + public value: ConditionOption, + public conditionValue: ConditionItem | null, + public options: ConditionOption[], + public loading: boolean, + ) {} + + public static empty() { + return new ConditionSelected("", null, [], false); + } + + public static fromConditionItem(conditionItem: ConditionItem) { + return new ConditionSelected( + getDefaultByControlName(conditionItem.type), + conditionItem, + [], + false, + ); + } + + public static fromConditionItemAndValue( + conditionItem: ConditionItem, + value: ConditionOption, + ) { + return new ConditionSelected(value, conditionItem, [], false); + } + + public enableLoading() { + this.loading = true; + } + + public disableLoading() { + this.loading = false; + } + + public isLoading() { + return this.loading; + } + + public setValue(value: ConditionOption) { + this.value = value; + } + + public setOptions(options: ConditionOption[]) { + this.options = options; + + if (this.conditionValue) { + this.value = getDefaultByControlName(this.conditionValue.type); + } + } +} diff --git a/src/components/ConditionalFilter/FilterElement/FilterElement.ts b/src/components/ConditionalFilter/FilterElement/FilterElement.ts new file mode 100644 index 000000000..8087c95c9 --- /dev/null +++ b/src/components/ConditionalFilter/FilterElement/FilterElement.ts @@ -0,0 +1,179 @@ +/* eslint-disable @typescript-eslint/member-ordering */ +import { InitialStateResponse } from "../API/InitialStateResponse"; +import { LeftOperand } from "./../useLeftOperands"; +import { CONDITIONS, UrlEntry, UrlToken } from "./../ValueProvider/UrlToken"; +import { Condition } from "./Condition"; +import { ConditionItem, ConditionOptions } from "./ConditionOptions"; +import { ConditionOption, ConditionSelected } from "./ConditionSelected"; + +interface ExpressionValue { + value: string; + label: string; + type: string; +} + +const createStaticEntry = (rawEntry: ConditionOption) => { + if (typeof rawEntry === "string") { + return rawEntry; + } + + if (Array.isArray(rawEntry)) { + return rawEntry.map(el => (typeof el === "string" ? el : el.slug)); + } + + return rawEntry.slug; +}; + +const createAttributeEntry = (rawEntry: ConditionOption) => { + if (typeof rawEntry === "string") { + return rawEntry; + } + + if (Array.isArray(rawEntry)) { + return rawEntry.map(el => (typeof el === "string" ? el : el.slug)); + } + + return rawEntry.slug; +}; + +export class FilterElement { + private constructor( + public value: ExpressionValue, + public condition: Condition, + public loading: boolean, + ) {} + + public enableLoading() { + this.loading = true; + } + + public disableLoading() { + this.loading = false; + } + + public isLoading() { + return this.loading; + } + + public updateLeftOperator(leftOperand: LeftOperand) { + this.value = { + value: leftOperand.slug, + label: leftOperand.label, + type: leftOperand.type, + }; + this.condition = Condition.emptyFromLeftOperand(leftOperand); + } + + public updateLeftLoadingState(loading: boolean) { + this.loading = loading; + } + + public updateCondition(conditionValue: ConditionItem) { + this.condition.selected = + ConditionSelected.fromConditionItem(conditionValue); + } + + public updateRightOperator(value: ConditionOption) { + this.condition.selected.setValue(value); + } + + public updateRightOptions(options: ConditionOption[]) { + this.condition.selected.setOptions(options); + } + + public updateRightLoadingState(loading: boolean) { + if (loading) { + this.condition.selected.enableLoading(); + return; + } + + this.condition.selected.disableLoading(); + } + + public isEmpty() { + return this.value.type === "e"; + } + + public isStatic() { + return ConditionOptions.isStaticName(this.value.type); + } + + public isAttribute() { + return ConditionOptions.isAttributeInputType(this.value.type); + } + + public isCollection() { + return this.value.value === "collection"; + } + + public isCategory() { + return this.value.value === "category"; + } + + public isProductType() { + return this.value.value === "producttype"; + } + + public isChannel() { + return this.value.value === "channel"; + } + + public asUrlEntry(): UrlEntry { + const { conditionValue } = this.condition.selected; + const conditionIndex = CONDITIONS.findIndex( + el => conditionValue && el === conditionValue.label, + ); + + if (this.isAttribute()) { + return { + [`a${conditionIndex}.${this.value.value}`]: createAttributeEntry( + this.condition.selected.value, + ), + }; + } + + return { + [`s${conditionIndex}.${this.value.value}`]: createStaticEntry( + this.condition.selected.value, + ), + }; + } + + public static fromValueEntry(valueEntry: any) { + return new FilterElement(valueEntry.value, valueEntry.condition, false); + } + + public static createEmpty() { + return new FilterElement( + { value: "", label: "", type: "s" }, + Condition.createEmpty(), + false, + ); + } + + public static fromUrlToken(token: UrlToken, response: InitialStateResponse) { + if (token.isStatic()) { + return new FilterElement( + { value: token.name, label: token.name, type: token.name }, + Condition.fromUrlToken(token, response), + false, + ); + } + + if (token.isAttribute()) { + const attribute = response.attributeByName(token.name); + + return new FilterElement( + { + value: token.name, + label: attribute.label, + type: attribute.inputType, + }, + Condition.fromUrlToken(token, response), + false, + ); + } + + return null; + } +} diff --git a/src/components/ConditionalFilter/FilterElement/index.ts b/src/components/ConditionalFilter/FilterElement/index.ts new file mode 100644 index 000000000..58aadb6d7 --- /dev/null +++ b/src/components/ConditionalFilter/FilterElement/index.ts @@ -0,0 +1,2 @@ +export { FilterElement } from "./FilterElement" +export { Condition } from "./Condition" \ No newline at end of file diff --git a/src/components/ConditionalFilter/FilterValueProvider.ts b/src/components/ConditionalFilter/FilterValueProvider.ts new file mode 100644 index 000000000..023819539 --- /dev/null +++ b/src/components/ConditionalFilter/FilterValueProvider.ts @@ -0,0 +1,7 @@ +import { FilterContainer } from "./useFilterContainer"; + +export interface FilterValueProvider { + value: FilterContainer; + loading: boolean; + persist: (newValue: FilterContainer) => void; +} diff --git a/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.ts b/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.ts new file mode 100644 index 000000000..a075334af --- /dev/null +++ b/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.ts @@ -0,0 +1,40 @@ +// @ts-strict-ignore +import { UrlToken } from "../UrlToken"; + +export interface FetchingParams { + category: string[]; + collection: string[]; + channel: string[]; + producttype: []; + attribute: Record; +} + +export const emptyFetchingParams: FetchingParams = { + category: [], + collection: [], + channel: [], + producttype: [], + attribute: {}, +}; + +const unique = (array: Iterable) => Array.from(new Set(array)); + +export const toFetchingParams = (p: FetchingParams, c: UrlToken) => { + if (!c.isAttribute() && !p[c.name]) { + p[c.name] = []; + } + + if (c.isAttribute() && !p.attribute[c.name]) { + p.attribute[c.name] = []; + } + + if (c.isAttribute()) { + p.attribute[c.name] = unique(p.attribute[c.name].concat(c.value)); + + return p; + } + + p[c.name] = unique(p[c.name].concat(c.value)); + + return p; +}; diff --git a/src/components/ConditionalFilter/ValueProvider/TokenArray/index.ts b/src/components/ConditionalFilter/ValueProvider/TokenArray/index.ts new file mode 100644 index 000000000..00b83243d --- /dev/null +++ b/src/components/ConditionalFilter/ValueProvider/TokenArray/index.ts @@ -0,0 +1,106 @@ +// @ts-strict-ignore + +import { parse, ParsedQs } from "qs"; +import { useRef } from "react"; + +import { InitialStateResponse } from "../../API/InitialStateResponse"; +import { FilterElement } from "../../FilterElement"; +import { FilterContainer } from "../../useFilterContainer"; +import { UrlToken } from "../UrlToken"; +import { + emptyFetchingParams, + FetchingParams, + toFetchingParams, +} from "./fetchingParams"; + +const toFlatUrlTokens = (p: UrlToken[], c: TokenArray[number]) => { + if (typeof c == "string") { + return p; + } + + if (Array.isArray(c)) { + return p.concat(flatenate(c)); + } + + return p.concat(c); +}; + +const flatenate = (tokens: TokenArray): UrlToken[] => + tokens.reduce(toFlatUrlTokens, []); + +const mapToTokens = (urlEntries: Array): TokenArray => + urlEntries.map(entry => { + if (typeof entry === "string") { + return entry; + } + + if (Array.isArray(entry)) { + return mapToTokens(entry); + } + + return UrlToken.fromUrlEntry(entry); + }) as TokenArray; + +const tokenizeUrl = (urlParams: string) => { + const parsedUrl = Object.values(parse(urlParams)) as Array; + + return mapToTokens(parsedUrl); +}; + +const mapUrlTokensToFilterValues = ( + urlTokens: TokenArray, + response: InitialStateResponse, +) => + urlTokens.map(el => { + if (typeof el === "string") { + return el; + } + + if (Array.isArray(el)) { + return mapUrlTokensToFilterValues(el, response); + } + + return FilterElement.fromUrlToken(el, response); + }); + +export class TokenArray extends Array { + constructor(url: string) { + super(...tokenizeUrl(url)); + } + + public getFetchingParams() { + return this.asFlatArray() + .filter(token => token.isLoadable()) + .reduce(toFetchingParams, emptyFetchingParams); + } + + public asFlatArray() { + return flatenate(this); + } + + public asFilterValuesFromResponse( + response: InitialStateResponse, + ): FilterContainer { + return this.map(el => { + if (typeof el === "string") { + return el; + } + + if (Array.isArray(el)) { + return mapUrlTokensToFilterValues(el, response); + } + + return FilterElement.fromUrlToken(el, response); + }); + } +} + +export const useTokenArray = (url: string) => { + const instance = useRef(null); + + if (!instance.current) { + instance.current = new TokenArray(url); + } + + return instance.current; +}; diff --git a/src/components/ConditionalFilter/ValueProvider/UrlToken.ts b/src/components/ConditionalFilter/ValueProvider/UrlToken.ts new file mode 100644 index 000000000..0b9a95054 --- /dev/null +++ b/src/components/ConditionalFilter/ValueProvider/UrlToken.ts @@ -0,0 +1,47 @@ +// @ts-strict-ignore + +export const CONDITIONS = ["is", "equals", "in", "between", "lower", "greater"]; + +const STATIC_TO_LOAD = ["category", "collection", "channel", "producttype"]; + +type TokenType = "a" | "s"; + +// export type UrlEntry = Record + +export class UrlEntry { + constructor(key: string, value: string | string[]) { + this[key] = value; + } +} + +export class UrlToken { + private constructor( + public name: string, + public value: string | string[], + public type: TokenType, + public conditionKind: string, + ) {} + + public static fromUrlEntry(entry: UrlEntry) { + const [key, value] = Object.entries(entry)[0] as [ + string, + string | string[], + ]; + const [identifier, entryName] = key.split("."); + const [type, control] = identifier.split("") as [TokenType, string]; + + return new UrlToken(entryName, value, type, CONDITIONS[control]); + } + + public isStatic() { + return this.type === "s"; + } + + public isAttribute() { + return this.type === "a"; + } + + public isLoadable() { + return STATIC_TO_LOAD.includes(this.name) || this.isAttribute(); + } +} diff --git a/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts b/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts new file mode 100644 index 000000000..ee5cbde99 --- /dev/null +++ b/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts @@ -0,0 +1,51 @@ +// @ts-strict-ignore + +import { stringify } from "qs"; +import useRouter from "use-react-router"; + +import { useInitialAPIState } from "../API/getInitalAPIState"; +import { FilterValueProvider } from "../FilterValueProvider"; +import { FilterContainer } from "../useFilterContainer"; +import { useTokenArray } from "./TokenArray"; + +const prepareStructure = filterValue => + filterValue.map(f => { + if (typeof f === "string") { + return f; + } + + if (Array.isArray(f)) { + return prepareStructure(f); + } + + return f.asUrlEntry(); + }); + +/* + exampple url: http://localhost:9000/dashboard/products/?0%5Bs2.category%5D%5B0%5D=accessories&0%5Bs2.category%5D%5B1%5D=groceries&1=o&2%5Ba2.abv%5D%5B0%5D=QXR0cmlidXRlVmFsdWU6Njg%3D&3=a&4%5Bs2.collection%5D%5B0%5D=featured-products&5=a&6%5Bs2.producttype%5D%5B0%5D=beer&7=a&8%5B0%5D%5Bs2.category%5D%5B0%5D=apparel&8%5B1%5D=o&8%5B2%5D%5Ba2.bottle-size%5D%5B0%5D=QXR0cmlidXRlVmFsdWU6NDY%3D&8%5B2%5D%5Ba2.bottle-size%5D%5B1%5D=QXR0cmlidXRlVmFsdWU6NDc%3D&asc=true&sort=name +*/ +export const useUrlValueProvider = (): FilterValueProvider => { + const router = useRouter(); + const params = new URLSearchParams(router.location.search); + params.delete("asc"); + params.delete("sort"); + + const tokenizedUrl = useTokenArray(params.toString()); + const fetchingParams = tokenizedUrl.getFetchingParams(); + const { data, loading } = useInitialAPIState(fetchingParams); + const value = loading ? [] : tokenizedUrl.asFilterValuesFromResponse(data); + + + const persist = (filterValue: FilterContainer) => { + router.history.replace({ + pathname: router.location.pathname, + search: stringify(prepareStructure(filterValue)), + }); + }; + + return { + value, + loading, + persist, + }; +}; diff --git a/src/components/ConditionalFilter/constants.ts b/src/components/ConditionalFilter/constants.ts new file mode 100644 index 000000000..32fd8f07c --- /dev/null +++ b/src/components/ConditionalFilter/constants.ts @@ -0,0 +1,30 @@ +export const STATIC_CONDITIONS = { + category: [ + { type: "combobox", label: "is", value: "input-1" }, + { type: "multiselect", label: "in", value: "input-2" }, + ], + price: [ + { type: "number", label: "is", value: "input-1" }, + { type: "number", label: "lower", value: "input-2" }, + { type: "number", label: "greater", value: "input-3" }, + { type: "number.range", label: "between", value: "input-4" }, + ], + collection: [{ type: "multiselect", label: "in", value: "input-4" }], + producttype: [{ type: "multiselect", label: "in", value: "input-4" }], + channel: [{ type: "select", label: "is", value: "input-5" }], +}; + +export const ATTRIBUTE_INPUT_TYPE_CONDITIONS = { + DROPDOWN: [{ type: "multiselect", label: "in", value: "input-2" }], + MULTISELECT: [{ type: "multiselect", label: "in", value: "input-2" }], + BOOLEAN: [{ type: "select", label: "is", value: "input-5" }], + NUMERIC: [ + { type: "number", label: "is", value: "input-1" }, + { type: "number", label: "lower", value: "input-2" }, + { type: "number", label: "greater", value: "input-3" }, + { type: "number.range", label: "between", value: "input-4" }, + ], + DATE_TIME: [{ type: "date", label: "is", value: "input-1" }], + DATE: [{ type: "date", label: "is", value: "input-1" }], + SWATCH: [{ type: "multiselect", label: "in", value: "input-2" }], +}; diff --git a/src/components/ConditionalFilter/controlsType.ts b/src/components/ConditionalFilter/controlsType.ts new file mode 100644 index 000000000..647eb2a46 --- /dev/null +++ b/src/components/ConditionalFilter/controlsType.ts @@ -0,0 +1,14 @@ +// @ts-strict-ignore +import { ConditionOption } from "./FilterElement/ConditionSelected"; + +export const CONTROL_DEFAULTS = { + text: "", + number: "", + "number.range": [] as unknown as [string, string], + multiselect: [] as ConditionOption[], + select: "", + combobox: "", +}; + +export const getDefaultByControlName = (name: string): ConditionOption => + CONTROL_DEFAULTS[name]; diff --git a/src/components/ConditionalFilter/index.tsx b/src/components/ConditionalFilter/index.tsx new file mode 100644 index 000000000..21914aa89 --- /dev/null +++ b/src/components/ConditionalFilter/index.tsx @@ -0,0 +1,142 @@ +// @ts-strict-ignore +import { useApolloClient } from "@apollo/client"; +import useDebounce from "@dashboard/hooks/useDebounce"; +import { _ExperimentalFilters, Box, Text } from "@saleor/macaw-ui/next"; +import React from "react"; + +import { + getInitialRightOperatorOptions, + getLeftOperatorOptions, + getRightOperatorOptionsByQuery, +} from "./API/getAPIOptions"; +import { useFilterContainer } from "./useFilterContainer"; +import { useLeftOperands } from "./useLeftOperands"; +import { useUrlValueProvider } from "./ValueProvider/useUrlValueProvider"; + +const FiltersArea = ({ provider, onConfirm }) => { + const client = useApolloClient(); + + const { + value, + addEmpty, + removeAt, + updateLeftOperator, + updateRightOperator, + updateCondition, + updateRightOptions, + updateRightLoadingState, + updateLeftLoadingState, + } = useFilterContainer(provider); + + const { operands, setOperands } = useLeftOperands(); + + const handleLeftOperatorInputValueChange = (event: any) => { + const fetchAPI = async () => { + const options = await getLeftOperatorOptions(client, event.value); + setOperands(options); + }; + updateLeftLoadingState(event.path, true); + fetchAPI(); + updateLeftLoadingState(event.path, false); + }; + + const handleLeftOperatorInputValueChangeDebounced = useDebounce( + handleLeftOperatorInputValueChange, + 500, + ); + + const handleRightOperatorInputValueChange = (event: any) => { + const fetchAPI = async () => { + const options = await getRightOperatorOptionsByQuery( + client, + event.path.split(".")[0], + value, + event.value, + ); + updateRightOptions(event.path.split(".")[0], options); + }; + updateRightLoadingState(event.path.split(".")[0], true); + fetchAPI(); + updateRightLoadingState(event.path.split(".")[0], false); + }; + + const handleRightOperatorInputValueChangeDebounced = useDebounce( + handleRightOperatorInputValueChange, + 500, + ); + + const handleStateChange = async event => { + if (event.type === "row.add") { + addEmpty(); + } + + if (event.type === "row.remove") { + removeAt(event.path); + } + + if (event.type === "leftOperator.onChange") { + updateLeftOperator(event.path, event.value); + } + + if (event.type === "condition.onChange") { + updateCondition(event.path.split(".")[0], event.value); + } + + if (event.type === "rightOperator.onChange") { + updateRightOperator(event.path.split(".")[0], event.value); + } + + if (event.type === "rightOperator.onFocus") { + const path = event.path.split(".")[0]; + updateRightLoadingState(path, true); + const options = await getInitialRightOperatorOptions(client, path, value); + updateRightOptions(path, options); + updateRightLoadingState(path, false); + } + + if (event.type === "rightOperator.onInputValueChange") { + handleRightOperatorInputValueChangeDebounced(event); + } + + if (event.type === "leftOperator.onInputValueChange") { + handleLeftOperatorInputValueChangeDebounced(event); + } + }; + + const handleConfirm = () => onConfirm(value); + + return ( + + <_ExperimentalFilters + leftOptions={operands} + // @ts-ignore + value={value} + onChange={handleStateChange} + > + <_ExperimentalFilters.Footer> + <_ExperimentalFilters.AddRowButton> + Add new row + + <_ExperimentalFilters.ConfirmButton onClick={handleConfirm}> + Confirm + + + + + ); +}; + +export const ConditionalFilters = () => { + const provider = useUrlValueProvider(); + + return ( + + {provider.loading ? ( + Loading... + ) : ( + // @ts-ignore + + )} + + ); +}; diff --git a/src/components/ConditionalFilter/useFilterContainer.ts b/src/components/ConditionalFilter/useFilterContainer.ts new file mode 100644 index 000000000..82416c090 --- /dev/null +++ b/src/components/ConditionalFilter/useFilterContainer.ts @@ -0,0 +1,125 @@ +// @ts-strict-ignore +import { useState } from "react"; + +import { FilterElement } from "./FilterElement"; + +export type FilterContainer = Array; + +export const useFilterContainer = (initialValue: FilterContainer) => { + const [value, setValue] = useState(initialValue); + + const addEmpty = () => { + const newValue = []; + if (value.length > 0) { + newValue.push("OR"); + } + + newValue.push(FilterElement.createEmpty()); + + setValue(v => v.concat(newValue)); + }; + + const removeAt = (position: string) => { + const index = parseInt(position, 10); + + if (value.length > 0) { + setValue(v => + v.filter((_, elIndex) => ![index - 1, index].includes(elIndex)), + ); + return; + } + + setValue(v => v.filter((_, elIndex) => ![index].includes(elIndex))); + }; + + const updateLeftOperator = (position: string, leftOperator: any) => { + const index = parseInt(position, 10); + setValue(v => + v.map((el, elIndex) => { + if (elIndex === index && typeof el != "string" && !Array.isArray(el)) { + el.updateLeftOperator(leftOperator); + } + + return el; + }), + ); + }; + + const updateLeftLoadingState = (position: string, loading: boolean) => { + const index = parseInt(position, 10); + setValue(v => + v.map((el, elIndex) => { + if (elIndex === index && typeof el != "string" && !Array.isArray(el)) { + el.updateLeftLoadingState(loading); + } + + return el; + }), + ); + }; + + const updateRightOperator = (position: string, leftOperator: any) => { + const index = parseInt(position, 10); + setValue(v => + v.map((el, elIndex) => { + if (elIndex === index && typeof el != "string" && !Array.isArray(el)) { + el.updateRightOperator(leftOperator); + } + + return el; + }), + ); + }; + + const updateRightOptions = (position: string, options: any) => { + const index = parseInt(position, 10); + setValue(v => + v.map((el, elIndex) => { + if (elIndex === index && typeof el != "string" && !Array.isArray(el)) { + el.updateRightOptions(options); + } + + return el; + }), + ); + }; + + const updateRightLoadingState = (position: string, loading: boolean) => { + const index = parseInt(position, 10); + setValue(v => + v.map((el, elIndex) => { + if (elIndex === index && typeof el != "string" && !Array.isArray(el)) { + el.updateRightLoadingState(loading); + } + + return el; + }), + ); + }; + + const updateCondition = (position: string, conditionValue: any) => { + const index = parseInt(position, 10); + + setValue(v => + v.map((el, elIndex) => { + if (elIndex === index && typeof el != "string" && !Array.isArray(el)) { + el.updateCondition(conditionValue); + } + + return el; + }), + ); + }; + + return { + value, + addEmpty, + removeAt, + updateLeftOperator, + updateRightOperator, + updateCondition, + updateRightOptions, + updateRightLoadingState, + updateLeftLoadingState, + }; +}; diff --git a/src/components/ConditionalFilter/useLeftOperands.ts b/src/components/ConditionalFilter/useLeftOperands.ts new file mode 100644 index 000000000..235b26ed4 --- /dev/null +++ b/src/components/ConditionalFilter/useLeftOperands.ts @@ -0,0 +1,34 @@ +import { useState } from "react"; + +import { + AttributeInputType, + StaticElementName, +} from "./FilterElement/ConditionOptions"; + +export interface LeftOperand { + type: AttributeInputType | StaticElementName; + label: string; + value: string; + slug: string; +} + +const STATIC_OPTIONS: LeftOperand[] = [ + { value: "price", label: "Price", type: "price", slug: "price" }, + { value: "category", label: "Category", type: "category", slug: "category" }, + { + value: "collection", + label: "Collection", + type: "collection", + slug: "collection", + }, + { value: "channel", label: "Channel", type: "channel", slug: "channel" }, +]; + +export const useLeftOperands = () => { + const [operands, setOperands] = useState(STATIC_OPTIONS); + + return { + operands, + setOperands, + }; +}; diff --git a/src/featureFlags/availableFlags.ts b/src/featureFlags/availableFlags.ts index 4462db2bf..892916dd2 100644 --- a/src/featureFlags/availableFlags.ts +++ b/src/featureFlags/availableFlags.ts @@ -24,16 +24,10 @@ const AVAILABLE_FLAGS = [ */ { - name: "flag1", - displayName: "Flag 1", - description: "some description", - content: { enabled: false, payload: "default" }, - } as const, - { - name: "flag2", - displayName: "Flag 2", - description: "some description 2", - content: { enabled: false, payload: "default2" }, + name: "product_filters", + displayName: "Product filters", + description: "New filters on product listing page", + content: { enabled: false, payload: "" }, } as const, ] satisfies FlagDefinition[]; diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 5700381c8..741adef37 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -5417,6 +5417,445 @@ export function useAddressValidationRulesLazyQuery(baseOptions?: ApolloReactHook export type AddressValidationRulesQueryHookResult = ReturnType; export type AddressValidationRulesLazyQueryHookResult = ReturnType; export type AddressValidationRulesQueryResult = Apollo.QueryResult; +export const _GetDynamicLeftOperandsDocument = gql` + query _GetDynamicLeftOperands($first: Int!, $query: String!) { + attributes(first: $first, filter: {type: PRODUCT_TYPE, search: $query}) { + edges { + node { + id + name + slug + inputType + } + } + } +} + `; + +/** + * __use_GetDynamicLeftOperandsQuery__ + * + * To run a query within a React component, call `use_GetDynamicLeftOperandsQuery` and pass it any options that fit your needs. + * When your component renders, `use_GetDynamicLeftOperandsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = use_GetDynamicLeftOperandsQuery({ + * variables: { + * first: // value for 'first' + * query: // value for 'query' + * }, + * }); + */ +export function use_GetDynamicLeftOperandsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(_GetDynamicLeftOperandsDocument, options); + } +export function use_GetDynamicLeftOperandsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(_GetDynamicLeftOperandsDocument, options); + } +export type _GetDynamicLeftOperandsQueryHookResult = ReturnType; +export type _GetDynamicLeftOperandsLazyQueryHookResult = ReturnType; +export type _GetDynamicLeftOperandsQueryResult = Apollo.QueryResult; +export const _GetChannelOperandsDocument = gql` + query _GetChannelOperands { + channels { + id + name + slug + } +} + `; + +/** + * __use_GetChannelOperandsQuery__ + * + * To run a query within a React component, call `use_GetChannelOperandsQuery` and pass it any options that fit your needs. + * When your component renders, `use_GetChannelOperandsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = use_GetChannelOperandsQuery({ + * variables: { + * }, + * }); + */ +export function use_GetChannelOperandsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(_GetChannelOperandsDocument, options); + } +export function use_GetChannelOperandsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(_GetChannelOperandsDocument, options); + } +export type _GetChannelOperandsQueryHookResult = ReturnType; +export type _GetChannelOperandsLazyQueryHookResult = ReturnType; +export type _GetChannelOperandsQueryResult = Apollo.QueryResult; +export const _SearchCollectionsOperandsDocument = gql` + query _SearchCollectionsOperands($first: Int!, $collectionsSlugs: [String!]) { + search: collections(first: $first, filter: {slugs: $collectionsSlugs}) { + edges { + node { + id + name + slug + } + } + } +} + `; + +/** + * __use_SearchCollectionsOperandsQuery__ + * + * To run a query within a React component, call `use_SearchCollectionsOperandsQuery` and pass it any options that fit your needs. + * When your component renders, `use_SearchCollectionsOperandsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = use_SearchCollectionsOperandsQuery({ + * variables: { + * first: // value for 'first' + * collectionsSlugs: // value for 'collectionsSlugs' + * }, + * }); + */ +export function use_SearchCollectionsOperandsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(_SearchCollectionsOperandsDocument, options); + } +export function use_SearchCollectionsOperandsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(_SearchCollectionsOperandsDocument, options); + } +export type _SearchCollectionsOperandsQueryHookResult = ReturnType; +export type _SearchCollectionsOperandsLazyQueryHookResult = ReturnType; +export type _SearchCollectionsOperandsQueryResult = Apollo.QueryResult; +export const _SearchCategoriesOperandsDocument = gql` + query _SearchCategoriesOperands($after: String, $first: Int!, $categoriesSlugs: [String!]) { + search: categories( + after: $after + first: $first + filter: {slugs: $categoriesSlugs} + ) { + edges { + node { + id + name + slug + } + } + } +} + `; + +/** + * __use_SearchCategoriesOperandsQuery__ + * + * To run a query within a React component, call `use_SearchCategoriesOperandsQuery` and pass it any options that fit your needs. + * When your component renders, `use_SearchCategoriesOperandsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = use_SearchCategoriesOperandsQuery({ + * variables: { + * after: // value for 'after' + * first: // value for 'first' + * categoriesSlugs: // value for 'categoriesSlugs' + * }, + * }); + */ +export function use_SearchCategoriesOperandsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(_SearchCategoriesOperandsDocument, options); + } +export function use_SearchCategoriesOperandsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(_SearchCategoriesOperandsDocument, options); + } +export type _SearchCategoriesOperandsQueryHookResult = ReturnType; +export type _SearchCategoriesOperandsLazyQueryHookResult = ReturnType; +export type _SearchCategoriesOperandsQueryResult = Apollo.QueryResult; +export const _SearchProductTypesOperandsDocument = gql` + query _SearchProductTypesOperands($after: String, $first: Int!, $productTypesSlugs: [String!]) { + search: productTypes( + after: $after + first: $first + filter: {slugs: $productTypesSlugs} + ) { + edges { + node { + id + name + slug + } + } + } +} + `; + +/** + * __use_SearchProductTypesOperandsQuery__ + * + * To run a query within a React component, call `use_SearchProductTypesOperandsQuery` and pass it any options that fit your needs. + * When your component renders, `use_SearchProductTypesOperandsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = use_SearchProductTypesOperandsQuery({ + * variables: { + * after: // value for 'after' + * first: // value for 'first' + * productTypesSlugs: // value for 'productTypesSlugs' + * }, + * }); + */ +export function use_SearchProductTypesOperandsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(_SearchProductTypesOperandsDocument, options); + } +export function use_SearchProductTypesOperandsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(_SearchProductTypesOperandsDocument, options); + } +export type _SearchProductTypesOperandsQueryHookResult = ReturnType; +export type _SearchProductTypesOperandsLazyQueryHookResult = ReturnType; +export type _SearchProductTypesOperandsQueryResult = Apollo.QueryResult; +export const _SearchAttributeOperandsDocument = gql` + query _SearchAttributeOperands($attributesSlugs: [String!], $choicesIds: [ID!], $first: Int!) { + search: attributes(first: $first, filter: {slugs: $attributesSlugs}) { + edges { + node { + id + name + slug + inputType + choices(first: 5, filter: {ids: $choicesIds}) { + edges { + node { + slug: id + id + name + } + } + } + } + } + } +} + `; + +/** + * __use_SearchAttributeOperandsQuery__ + * + * To run a query within a React component, call `use_SearchAttributeOperandsQuery` and pass it any options that fit your needs. + * When your component renders, `use_SearchAttributeOperandsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = use_SearchAttributeOperandsQuery({ + * variables: { + * attributesSlugs: // value for 'attributesSlugs' + * choicesIds: // value for 'choicesIds' + * first: // value for 'first' + * }, + * }); + */ +export function use_SearchAttributeOperandsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(_SearchAttributeOperandsDocument, options); + } +export function use_SearchAttributeOperandsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(_SearchAttributeOperandsDocument, options); + } +export type _SearchAttributeOperandsQueryHookResult = ReturnType; +export type _SearchAttributeOperandsLazyQueryHookResult = ReturnType; +export type _SearchAttributeOperandsQueryResult = Apollo.QueryResult; +export const _GetAttributeChoicesDocument = gql` + query _GetAttributeChoices($slug: String!, $first: Int!, $query: String!) { + attribute(slug: $slug) { + choices(first: $first, filter: {search: $query}) { + edges { + node { + slug: id + id + name + } + } + } + } +} + `; + +/** + * __use_GetAttributeChoicesQuery__ + * + * To run a query within a React component, call `use_GetAttributeChoicesQuery` and pass it any options that fit your needs. + * When your component renders, `use_GetAttributeChoicesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = use_GetAttributeChoicesQuery({ + * variables: { + * slug: // value for 'slug' + * first: // value for 'first' + * query: // value for 'query' + * }, + * }); + */ +export function use_GetAttributeChoicesQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(_GetAttributeChoicesDocument, options); + } +export function use_GetAttributeChoicesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(_GetAttributeChoicesDocument, options); + } +export type _GetAttributeChoicesQueryHookResult = ReturnType; +export type _GetAttributeChoicesLazyQueryHookResult = ReturnType; +export type _GetAttributeChoicesQueryResult = Apollo.QueryResult; +export const _GetCollectionsChoicesDocument = gql` + query _GetCollectionsChoices($first: Int!, $query: String!) { + collections(first: $first, filter: {search: $query}) { + edges { + node { + id + name + slug + } + } + } +} + `; + +/** + * __use_GetCollectionsChoicesQuery__ + * + * To run a query within a React component, call `use_GetCollectionsChoicesQuery` and pass it any options that fit your needs. + * When your component renders, `use_GetCollectionsChoicesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = use_GetCollectionsChoicesQuery({ + * variables: { + * first: // value for 'first' + * query: // value for 'query' + * }, + * }); + */ +export function use_GetCollectionsChoicesQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(_GetCollectionsChoicesDocument, options); + } +export function use_GetCollectionsChoicesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(_GetCollectionsChoicesDocument, options); + } +export type _GetCollectionsChoicesQueryHookResult = ReturnType; +export type _GetCollectionsChoicesLazyQueryHookResult = ReturnType; +export type _GetCollectionsChoicesQueryResult = Apollo.QueryResult; +export const _GetCategoriesChoicesDocument = gql` + query _GetCategoriesChoices($first: Int!, $query: String!) { + categories(first: $first, filter: {search: $query}) { + edges { + node { + id + name + slug + } + } + } +} + `; + +/** + * __use_GetCategoriesChoicesQuery__ + * + * To run a query within a React component, call `use_GetCategoriesChoicesQuery` and pass it any options that fit your needs. + * When your component renders, `use_GetCategoriesChoicesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = use_GetCategoriesChoicesQuery({ + * variables: { + * first: // value for 'first' + * query: // value for 'query' + * }, + * }); + */ +export function use_GetCategoriesChoicesQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(_GetCategoriesChoicesDocument, options); + } +export function use_GetCategoriesChoicesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(_GetCategoriesChoicesDocument, options); + } +export type _GetCategoriesChoicesQueryHookResult = ReturnType; +export type _GetCategoriesChoicesLazyQueryHookResult = ReturnType; +export type _GetCategoriesChoicesQueryResult = Apollo.QueryResult; +export const _GetProductTypesChoicesDocument = gql` + query _GetProductTypesChoices($first: Int!, $query: String!) { + productTypes(first: $first, filter: {search: $query}) { + edges { + node { + id + name + slug + } + } + } +} + `; + +/** + * __use_GetProductTypesChoicesQuery__ + * + * To run a query within a React component, call `use_GetProductTypesChoicesQuery` and pass it any options that fit your needs. + * When your component renders, `use_GetProductTypesChoicesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = use_GetProductTypesChoicesQuery({ + * variables: { + * first: // value for 'first' + * query: // value for 'query' + * }, + * }); + */ +export function use_GetProductTypesChoicesQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(_GetProductTypesChoicesDocument, options); + } +export function use_GetProductTypesChoicesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(_GetProductTypesChoicesDocument, options); + } +export type _GetProductTypesChoicesQueryHookResult = ReturnType; +export type _GetProductTypesChoicesLazyQueryHookResult = ReturnType; +export type _GetProductTypesChoicesQueryResult = Apollo.QueryResult; export const TriggerWebhookDryRunDocument = gql` mutation TriggerWebhookDryRun($objectId: ID!, $query: String!) { webhookDryRun(objectId: $objectId, query: $query) { diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index 67b775266..423412ccf 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -8323,6 +8323,87 @@ export type AddressValidationRulesQueryVariables = Exact<{ export type AddressValidationRulesQuery = { __typename: 'Query', addressValidationRules: { __typename: 'AddressValidationData', allowedFields: Array, countryAreaChoices: Array<{ __typename: 'ChoiceValue', raw: string | null, verbose: string | null }> } | null }; +export type _GetDynamicLeftOperandsQueryVariables = Exact<{ + first: Scalars['Int']; + query: Scalars['String']; +}>; + + +export type _GetDynamicLeftOperandsQuery = { __typename: 'Query', attributes: { __typename: 'AttributeCountableConnection', edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null } }> } | null }; + +export type _GetChannelOperandsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type _GetChannelOperandsQuery = { __typename: 'Query', channels: Array<{ __typename: 'Channel', id: string, name: string, slug: string }> | null }; + +export type _SearchCollectionsOperandsQueryVariables = Exact<{ + first: Scalars['Int']; + collectionsSlugs?: InputMaybe | Scalars['String']>; +}>; + + +export type _SearchCollectionsOperandsQuery = { __typename: 'Query', search: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, slug: string } }> } | null }; + +export type _SearchCategoriesOperandsQueryVariables = Exact<{ + after?: InputMaybe; + first: Scalars['Int']; + categoriesSlugs?: InputMaybe | Scalars['String']>; +}>; + + +export type _SearchCategoriesOperandsQuery = { __typename: 'Query', search: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, slug: string } }> } | null }; + +export type _SearchProductTypesOperandsQueryVariables = Exact<{ + after?: InputMaybe; + first: Scalars['Int']; + productTypesSlugs?: InputMaybe | Scalars['String']>; +}>; + + +export type _SearchProductTypesOperandsQuery = { __typename: 'Query', search: { __typename: 'ProductTypeCountableConnection', edges: Array<{ __typename: 'ProductTypeCountableEdge', node: { __typename: 'ProductType', id: string, name: string, slug: string } }> } | null }; + +export type _SearchAttributeOperandsQueryVariables = Exact<{ + attributesSlugs?: InputMaybe | Scalars['String']>; + choicesIds?: InputMaybe | Scalars['ID']>; + first: Scalars['Int']; +}>; + + +export type _SearchAttributeOperandsQuery = { __typename: 'Query', search: { __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 _GetAttributeChoicesQueryVariables = Exact<{ + slug: Scalars['String']; + first: Scalars['Int']; + query: Scalars['String']; +}>; + + +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 _GetCollectionsChoicesQueryVariables = Exact<{ + first: Scalars['Int']; + query: Scalars['String']; +}>; + + +export type _GetCollectionsChoicesQuery = { __typename: 'Query', collections: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, slug: string } }> } | null }; + +export type _GetCategoriesChoicesQueryVariables = Exact<{ + first: Scalars['Int']; + query: Scalars['String']; +}>; + + +export type _GetCategoriesChoicesQuery = { __typename: 'Query', categories: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, slug: string } }> } | null }; + +export type _GetProductTypesChoicesQueryVariables = Exact<{ + first: Scalars['Int']; + query: Scalars['String']; +}>; + + +export type _GetProductTypesChoicesQuery = { __typename: 'Query', productTypes: { __typename: 'ProductTypeCountableConnection', edges: Array<{ __typename: 'ProductTypeCountableEdge', node: { __typename: 'ProductType', id: string, name: string, slug: string } }> } | null }; + export type TriggerWebhookDryRunMutationVariables = Exact<{ objectId: Scalars['ID']; query: Scalars['String'];