diff --git a/.changeset/curly-penguins-vanish.md b/.changeset/curly-penguins-vanish.md new file mode 100644 index 00000000000..51d05c1a8ea --- /dev/null +++ b/.changeset/curly-penguins-vanish.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Product types list page uses now new filters. New filters are under feature flag and are enabled by default. diff --git a/.featureFlags/generated.tsx b/.featureFlags/generated.tsx index ffa25c70ac1..c0f43cb8b60 100644 --- a/.featureFlags/generated.tsx +++ b/.featureFlags/generated.tsx @@ -1,35 +1,36 @@ // @ts-nocheck -import W24585 from "./images/collection-filters.jpg" -import L16463 from "./images/customers-filters.png" -import L94878 from "./images/discounts-list.png" -import E09437 from "./images/draft-orders-filters.png" -import E78766 from "./images/gift-cards-filters.png" -import X68306 from "./images/improved_refunds.png" -import E85468 from "./images/page-filters.png" -import B37301 from "./images/vouchers-filters.png" +import B52941 from "./images/collection-filters.jpg" +import N65883 from "./images/customers-filters.png" +import M49541 from "./images/discounts-list.png" +import J34031 from "./images/draft-orders-filters.png" +import A69414 from "./images/gift-cards-filters.png" +import Y18806 from "./images/improved_refunds.png" +import V51684 from "./images/page-filters.png" +import D27626 from "./images/product-types-filters.png" +import M01387 from "./images/vouchers-filters.png" -const collection_filters = () => (<>

new filters +const collection_filters = () => (<>

new filters Experience the new look and enhanced abilities of new filtering mechanism. Easily combine any criteria you want, and quickly browse their values.

) -const customers_filters = () => (<>

new filters +const customers_filters = () => (<>

new filters Experience the new look and enhanced abilities of new fitering mechanism. Easily combine any criteria you want, and quickly browse their values.

) -const discounts_rules = () => (<>

Discount rules

+const discounts_rules = () => (<>

Discount rules

Apply the new discounts rules to narrow your promotions audience. Set up conditions and channels that must be fulfilled to apply defined reward.

) -const draft_orders_filters = () => (<>

new filters +const draft_orders_filters = () => (<>

new filters Experience the new look and enhanced abilities of new fitering mechanism. Easily combine any criteria you want, and quickly browse their values.

) -const gift_cards_filters = () => (<>

new filters +const gift_cards_filters = () => (<>

new filters Experience the new look and enhanced abilities of new fitering mechanism. Easily combine any criteria you want, and quickly browse their values.

) -const improved_refunds = () => (<>

Improved refunds

+const improved_refunds = () => (<>

Improved refunds

Enable the enhanced refund feature to streamline your refund process:

) -const pages_filters = () => (<>

new filters +const pages_filters = () => (<>

new filters Experience the new look and enhanced abilities of new fitering mechanism. Easily combine any criteria you want, and quickly browse their values.

) -const vouchers_filters = () => (<>

new filters +const product_types_filters = () => (<>

new filters +Experience the new look and enhanced abilities of new fitering mechanism. +Easily combine any criteria you want, and quickly browse their values.

+) +const vouchers_filters = () => (<>

new filters Experience the new look and enhanced abilities of new fitering mechanism. Easily combine any criteria you want, and quickly browse their values.

) @@ -111,6 +116,15 @@ export const AVAILABLE_FLAGS = [{ enabled: true, payload: "default", } +},{ + name: "product_types_filters", + displayName: "Product types filtering", + component: product_types_filters, + visible: true, + content: { + enabled: true, + payload: "default", + } },{ name: "vouchers_filters", displayName: "Vouchers filtering", diff --git a/.featureFlags/images/product-types-filters.png b/.featureFlags/images/product-types-filters.png new file mode 100644 index 00000000000..997e65ccf5a Binary files /dev/null and b/.featureFlags/images/product-types-filters.png differ diff --git a/.featureFlags/product_types_filters.md b/.featureFlags/product_types_filters.md new file mode 100644 index 00000000000..30b194debb6 --- /dev/null +++ b/.featureFlags/product_types_filters.md @@ -0,0 +1,11 @@ +--- +name: product_types_filters +displayName: Product types filtering +enabled: true +payload: "default" +visible: true +--- + +![new filters](./images/product-types-filters.png) +Experience the new look and enhanced abilities of new fitering mechanism. +Easily combine any criteria you want, and quickly browse their values. \ No newline at end of file diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 86bf8507a34..1e4e2aecf92 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -3742,6 +3742,10 @@ "M59JhX": { "string": "Manual" }, + "M61TN/": { + "context": "product type shippable", + "string": "Shippable" + }, "M6mWHL": { "context": "tooltip helper text", "string": "You do not have permissions to create a manual refund." @@ -4781,6 +4785,10 @@ "SceSNp": { "string": "Remove following permissions:" }, + "SgFE10": { + "context": "product type digital", + "string": "Digital" + }, "Sjd7wm": { "context": "product filter label", "string": "Product" diff --git a/src/components/ConditionalFilter/API/initialState/productTypes/InitialProductTypesState.test.ts b/src/components/ConditionalFilter/API/initialState/productTypes/InitialProductTypesState.test.ts new file mode 100644 index 00000000000..275628e414e --- /dev/null +++ b/src/components/ConditionalFilter/API/initialState/productTypes/InitialProductTypesState.test.ts @@ -0,0 +1,70 @@ +import { UrlEntry, UrlToken } from "../../../ValueProvider/UrlToken"; +import { InitialProductTypesStateResponse } from "./InitialProductTypesState"; + +describe("ConditionalFilter / API / Page / InitialProductTypesState", () => { + it("should filter by product type", () => { + // Arrange + const initialPageState = InitialProductTypesStateResponse.empty(); + + initialPageState.typeOfProduct = [ + { + label: "Type 1", + value: "type-1", + slug: "type-1", + }, + { + label: "Type 2", + value: "type-2", + slug: "type-2", + }, + ]; + + const token = UrlToken.fromUrlEntry(new UrlEntry("s0.typeOfProduct", "type-2")); + const expectedOutput = [ + { + label: "Type 2", + value: "type-2", + slug: "type-2", + }, + ]; + + // Act + const result = initialPageState.filterByUrlToken(token); + + // Assert + expect(result).toEqual(expectedOutput); + }); + + it("should filter by configurable", () => { + // Arrange + const initialPageState = InitialProductTypesStateResponse.empty(); + + initialPageState.configurable = [ + { + label: "Yes", + value: "CONFIGURABLE", + slug: "yes", + }, + { + label: "No", + value: "SIMPLE", + slug: "no", + }, + ]; + + const token = UrlToken.fromUrlEntry(new UrlEntry("s0.configurable", "no")); + const expectedOutput = [ + { + label: "No", + value: "SIMPLE", + slug: "no", + }, + ]; + + // Act + const result = initialPageState.filterByUrlToken(token); + + // Assert + expect(result).toEqual(expectedOutput); + }); +}); diff --git a/src/components/ConditionalFilter/API/initialState/productTypes/InitialProductTypesState.ts b/src/components/ConditionalFilter/API/initialState/productTypes/InitialProductTypesState.ts new file mode 100644 index 00000000000..9ee7ee05eef --- /dev/null +++ b/src/components/ConditionalFilter/API/initialState/productTypes/InitialProductTypesState.ts @@ -0,0 +1,39 @@ +import { ItemOption } from "@dashboard/components/ConditionalFilter/FilterElement/ConditionValue"; +import { UrlToken } from "@dashboard/components/ConditionalFilter/ValueProvider/UrlToken"; + +export interface InitialProductTypesState { + typeOfProduct: ItemOption[]; + configurable: ItemOption[]; +} + +export class InitialProductTypesStateResponse implements InitialProductTypesState { + constructor( + public typeOfProduct: ItemOption[] = [], + public configurable: ItemOption[] = [], + ) {} + + public static empty() { + return new InitialProductTypesStateResponse(); + } + + public filterByUrlToken(token: UrlToken) { + const entry = this.getEntryByName(token.name); + + if (!token.isLoadable()) { + return [token.value] as string[]; + } + + return (entry as ItemOption[]).filter(({ slug }) => slug && token.value.includes(slug)); + } + + private getEntryByName(name: string): ItemOption[] { + switch (name) { + case "typeOfProduct": + return this.typeOfProduct; + case "configurable": + return this.configurable; + default: + return []; + } + } +} diff --git a/src/components/ConditionalFilter/API/initialState/productTypes/useInitialProdutTypesState.ts b/src/components/ConditionalFilter/API/initialState/productTypes/useInitialProdutTypesState.ts new file mode 100644 index 00000000000..a954173dbeb --- /dev/null +++ b/src/components/ConditionalFilter/API/initialState/productTypes/useInitialProdutTypesState.ts @@ -0,0 +1,61 @@ +import { InitialProductTypesStateResponse } from "@dashboard/components/ConditionalFilter/API/initialState/productTypes/InitialProductTypesState"; +import { ProductTypeConfigurable, ProductTypeEnum } from "@dashboard/graphql"; +import { useState } from "react"; +import { useIntl } from "react-intl"; + +import { ProductTypesFetchingParams } from "../../../ValueProvider/TokenArray/fetchingParams"; +import { BooleanValuesHandler, EnumValuesHandler } from "../../Handler"; + +export interface InitialProductTypesAPIState { + data: InitialProductTypesStateResponse; + loading: boolean; + fetchQueries: (params: ProductTypesFetchingParams) => Promise; +} + +export const useInitialProductTypesState = (): InitialProductTypesAPIState => { + const intl = useIntl(); + const [data, setData] = useState( + InitialProductTypesStateResponse.empty(), + ); + const [loading, setLoading] = useState(true); + + const fetchQueries = async ({ typeOfProduct }: ProductTypesFetchingParams) => { + const typeOfProductInit = new EnumValuesHandler( + ProductTypeEnum, + "typeOfProduct", + intl, + typeOfProduct, + ); + + const configurableInit = new BooleanValuesHandler([ + { + label: "Yes", + value: ProductTypeConfigurable.CONFIGURABLE, + type: "configurable", + slug: "true", + }, + { + label: "No", + value: ProductTypeConfigurable.SIMPLE, + type: "configurable", + slug: "false", + }, + ]); + + const initialState = { + typeOfProduct: await typeOfProductInit.fetch(), + configurable: await configurableInit.fetch(), + }; + + setData( + new InitialProductTypesStateResponse(initialState.typeOfProduct, initialState.configurable), + ); + setLoading(false); + }; + + return { + data, + loading, + fetchQueries, + }; +}; diff --git a/src/components/ConditionalFilter/API/intl.ts b/src/components/ConditionalFilter/API/intl.ts index 4d25ab101d2..9914ba23ef8 100644 --- a/src/components/ConditionalFilter/API/intl.ts +++ b/src/components/ConditionalFilter/API/intl.ts @@ -6,6 +6,7 @@ import { OrderChargeStatusEnum, OrderStatusFilter, PaymentChargeStatusEnum, + ProductTypeEnum, VoucherDiscountType, } from "@dashboard/graphql"; import { transformOrderStatus, transformPaymentStatus } from "@dashboard/misc"; @@ -16,6 +17,7 @@ import { chargeStatusMessages, collectionFilterMessages, discountTypeMessages, + productTypeMessages, voucherStatusMessages, } from "./messages"; @@ -94,6 +96,17 @@ const getPublishedLabel = (status: CollectionPublished, intl: IntlShape) => { } }; +export const getProductTypeLabel = (type: ProductTypeEnum, intl: IntlShape) => { + switch (type) { + case ProductTypeEnum.DIGITAL: + return intl.formatMessage(productTypeMessages.digital); + case ProductTypeEnum.SHIPPABLE: + return intl.formatMessage(productTypeMessages.shippable); + default: + return type; + } +}; + export const getLocalizedLabel = (rowType: LeftOperand["type"], value: string, intl: IntlShape) => { switch (rowType) { case "paymentStatus": @@ -110,6 +123,8 @@ export const getLocalizedLabel = (rowType: LeftOperand["type"], value: string, i return getDiscountTypeLabel(value as VoucherDiscountType, intl); case "voucherStatus": return getVoucherStatusLabel(value as DiscountStatusEnum, intl); + case "typeOfProduct": + return getProductTypeLabel(value as ProductTypeEnum, intl); default: return value; } diff --git a/src/components/ConditionalFilter/API/messages.ts b/src/components/ConditionalFilter/API/messages.ts index b70e6216a24..6efc4025580 100644 --- a/src/components/ConditionalFilter/API/messages.ts +++ b/src/components/ConditionalFilter/API/messages.ts @@ -87,3 +87,16 @@ export const collectionFilterMessages = defineMessages({ id: "ThUvIL", }, }); + +export const productTypeMessages = defineMessages({ + digital: { + defaultMessage: "Digital", + description: "product type digital", + id: "SgFE10", + }, + shippable: { + defaultMessage: "Shippable", + description: "product type shippable", + id: "M61TN/", + }, +}); diff --git a/src/components/ConditionalFilter/API/providers/CollectionFilterAPIProvider.tsx b/src/components/ConditionalFilter/API/providers/CollectionFilterAPIProvider.tsx index db7ed0afcad..78b1620cb85 100644 --- a/src/components/ConditionalFilter/API/providers/CollectionFilterAPIProvider.tsx +++ b/src/components/ConditionalFilter/API/providers/CollectionFilterAPIProvider.tsx @@ -5,16 +5,8 @@ import { IntlShape, useIntl } from "react-intl"; import { FilterContainer, FilterElement } from "../../FilterElement"; import { FilterAPIProvider } from "../FilterAPIProvider"; import { ChannelHandler, EnumValuesHandler, Handler, NoopValuesHandler } from "../Handler"; +import { getFilterElement } from "../utils"; -const getFilterElement = (value: FilterContainer, index: number): FilterElement => { - const possibleFilterElement = value[index]; - - if (typeof possibleFilterElement !== "string" && !Array.isArray(possibleFilterElement)) { - return possibleFilterElement; - } - - throw new Error("Unknown filter element used to create API handler"); -}; const createAPIHandler = ( selectedRow: FilterElement, client: ApolloClient, diff --git a/src/components/ConditionalFilter/API/providers/DiscountFiltersAPIProvider.tsx b/src/components/ConditionalFilter/API/providers/DiscountFiltersAPIProvider.tsx index 8a3975dd0e1..5e4db129890 100644 --- a/src/components/ConditionalFilter/API/providers/DiscountFiltersAPIProvider.tsx +++ b/src/components/ConditionalFilter/API/providers/DiscountFiltersAPIProvider.tsx @@ -1,4 +1,4 @@ -import { FilterAPIProvider } from "@dashboard/components/ConditionalFilter/API/FilterAPIProvider"; +import { FilterAPIProvider } from "../FilterAPIProvider"; export const useDiscountFilterAPIProvider = (): FilterAPIProvider => { const fetchRightOptions = async () => { diff --git a/src/components/ConditionalFilter/API/providers/DraftOrderFilterAPIProvider.tsx b/src/components/ConditionalFilter/API/providers/DraftOrderFilterAPIProvider.tsx index 70d14c9d653..0d474e290dd 100644 --- a/src/components/ConditionalFilter/API/providers/DraftOrderFilterAPIProvider.tsx +++ b/src/components/ConditionalFilter/API/providers/DraftOrderFilterAPIProvider.tsx @@ -1,4 +1,4 @@ -import { FilterAPIProvider } from "@dashboard/components/ConditionalFilter/API/FilterAPIProvider"; +import { FilterAPIProvider } from "../FilterAPIProvider"; export const useDraftOrderFilterAPIProvider = (): FilterAPIProvider => { const fetchRightOptions = async () => { diff --git a/src/components/ConditionalFilter/API/providers/GiftCardsFilterAPIProvider.tsx b/src/components/ConditionalFilter/API/providers/GiftCardsFilterAPIProvider.tsx index c30cb6930b7..aa2cc096d4f 100644 --- a/src/components/ConditionalFilter/API/providers/GiftCardsFilterAPIProvider.tsx +++ b/src/components/ConditionalFilter/API/providers/GiftCardsFilterAPIProvider.tsx @@ -1,5 +1,7 @@ import { ApolloClient, useApolloClient } from "@apollo/client"; -import { FilterAPIProvider } from "@dashboard/components/ConditionalFilter/API/FilterAPIProvider"; + +import { FilterContainer, FilterElement } from "../../FilterElement"; +import { FilterAPIProvider } from "../FilterAPIProvider"; import { BooleanValuesHandler, CurrencyHandler, @@ -7,21 +9,8 @@ import { GiftCardTagsHandler, Handler, ProductsHandler, -} from "@dashboard/components/ConditionalFilter/API/Handler"; -import { - FilterContainer, - FilterElement, -} from "@dashboard/components/ConditionalFilter/FilterElement"; - -const getFilterElement = (value: FilterContainer, index: number): FilterElement => { - const possibleFilterElement = value[index]; - - if (typeof possibleFilterElement !== "string" && !Array.isArray(possibleFilterElement)) { - return possibleFilterElement; - } - - throw new Error("Unknown filter element used to create API handler"); -}; +} from "../Handler"; +import { getFilterElement } from "../utils"; const createAPIHandler = ( selectedRow: FilterElement, diff --git a/src/components/ConditionalFilter/API/providers/OrderFilterAPIProvider.tsx b/src/components/ConditionalFilter/API/providers/OrderFilterAPIProvider.tsx index 102b960bbd6..e77a3fbf0c3 100644 --- a/src/components/ConditionalFilter/API/providers/OrderFilterAPIProvider.tsx +++ b/src/components/ConditionalFilter/API/providers/OrderFilterAPIProvider.tsx @@ -1,5 +1,4 @@ import { ApolloClient, useApolloClient } from "@apollo/client"; -import { FilterAPIProvider } from "@dashboard/components/ConditionalFilter/API/FilterAPIProvider"; import { OrderAuthorizeStatusEnum, OrderChargeStatusEnum, @@ -10,6 +9,7 @@ import { IntlShape, useIntl } from "react-intl"; import { RowType } from "../../constants"; import { FilterContainer, FilterElement } from "../../FilterElement"; +import { FilterAPIProvider } from "../FilterAPIProvider"; import { BooleanValuesHandler, EnumValuesHandler, @@ -18,16 +18,7 @@ import { NoopValuesHandler, TextInputValuesHandler, } from "../Handler"; - -const getFilterElement = (value: FilterContainer, index: number): FilterElement => { - const possibleFilterElement = value[index]; - - if (typeof possibleFilterElement !== "string" && !Array.isArray(possibleFilterElement)) { - return possibleFilterElement; - } - - throw new Error("Unknown filter element used to create API handler"); -}; +import { getFilterElement } from "../utils"; const isStaticBoolean = (rowType: RowType) => { return ["isClickAndCollect", "isPreorder", "giftCardUsed", "giftCardBought"].includes(rowType); diff --git a/src/components/ConditionalFilter/API/providers/PageFilterAPIProvider.tsx b/src/components/ConditionalFilter/API/providers/PageFilterAPIProvider.tsx index 25f4432c695..e884bdca729 100644 --- a/src/components/ConditionalFilter/API/providers/PageFilterAPIProvider.tsx +++ b/src/components/ConditionalFilter/API/providers/PageFilterAPIProvider.tsx @@ -1,21 +1,9 @@ import { useApolloClient } from "@apollo/client"; -import { PageTypesHandler } from "@dashboard/components/ConditionalFilter/API/Handler"; -import { - FilterContainer, - FilterElement, -} from "@dashboard/components/ConditionalFilter/FilterElement"; +import { FilterContainer } from "../../FilterElement"; import { FilterAPIProvider } from "../FilterAPIProvider"; - -const getFilterElement = (value: FilterContainer, index: number): FilterElement => { - const possibleFilterElement = value[index]; - - if (typeof possibleFilterElement !== "string" && !Array.isArray(possibleFilterElement)) { - return possibleFilterElement; - } - - throw new Error("Unknown filter element used to create API handler"); -}; +import { PageTypesHandler } from "../Handler"; +import { getFilterElement } from "../utils"; export const usePageAPIProvider = (): FilterAPIProvider => { const client = useApolloClient(); diff --git a/src/components/ConditionalFilter/API/providers/ProductFilterAPIProvider.tsx b/src/components/ConditionalFilter/API/providers/ProductFilterAPIProvider.tsx index 0d68087751b..22467c4bdf8 100644 --- a/src/components/ConditionalFilter/API/providers/ProductFilterAPIProvider.tsx +++ b/src/components/ConditionalFilter/API/providers/ProductFilterAPIProvider.tsx @@ -14,16 +14,8 @@ import { Handler, ProductTypeHandler, } from "../Handler"; +import { getFilterElement } from "../utils"; -const getFilterElement = (value: FilterContainer, index: number): FilterElement => { - const possibleFilterElement = value[index]; - - if (typeof possibleFilterElement !== "string" && !Array.isArray(possibleFilterElement)) { - return possibleFilterElement; - } - - throw new Error("Unknown filter element used to create API handler"); -}; const isStaticBoolean = (rowType: RowType) => { return [ "isAvailable", diff --git a/src/components/ConditionalFilter/API/providers/ProductTypesFilterAPIProvider.tsx b/src/components/ConditionalFilter/API/providers/ProductTypesFilterAPIProvider.tsx new file mode 100644 index 00000000000..092b3e7064e --- /dev/null +++ b/src/components/ConditionalFilter/API/providers/ProductTypesFilterAPIProvider.tsx @@ -0,0 +1,51 @@ +import { + BooleanValuesHandler, + EnumValuesHandler, +} from "@dashboard/components/ConditionalFilter/API/Handler"; +import { ProductTypeConfigurable, ProductTypeEnum } from "@dashboard/graphql"; +import { useIntl } from "react-intl"; + +import { FilterContainer } from "../../FilterElement"; +import { FilterAPIProvider } from "../FilterAPIProvider"; +import { getFilterElement } from "../utils"; + +export const useProductTypesFilterAPIProvider = (): FilterAPIProvider => { + const intl = useIntl(); + + const fetchRightOptions = async (position: string, value: FilterContainer) => { + const index = parseInt(position, 10); + const filterElement = getFilterElement(value, index); + const rowType = filterElement.rowType(); + + if (rowType === "configurable") { + return await new BooleanValuesHandler([ + { + label: "Yes", + value: ProductTypeConfigurable.CONFIGURABLE, + type: rowType, + slug: "true", + }, + { + label: "No", + value: ProductTypeConfigurable.SIMPLE, + type: rowType, + slug: "false", + }, + ]).fetch(); + } + + if (rowType === "typeOfProduct") { + return await new EnumValuesHandler(ProductTypeEnum, "typeOfProduct", intl).fetch(); + } + + return []; + }; + const fetchLeftOptions = async () => { + return []; + }; + + return { + fetchRightOptions, + fetchLeftOptions, + }; +}; diff --git a/src/components/ConditionalFilter/API/providers/VoucherFilterAPIProvider.ts b/src/components/ConditionalFilter/API/providers/VoucherFilterAPIProvider.ts index 9d9167720c5..a2fd37c5704 100644 --- a/src/components/ConditionalFilter/API/providers/VoucherFilterAPIProvider.ts +++ b/src/components/ConditionalFilter/API/providers/VoucherFilterAPIProvider.ts @@ -1,27 +1,11 @@ import { ApolloClient, useApolloClient } from "@apollo/client"; -import { - ChannelHandler, - EnumValuesHandler, - Handler, -} from "@dashboard/components/ConditionalFilter/API/Handler"; -import { - FilterContainer, - FilterElement, -} from "@dashboard/components/ConditionalFilter/FilterElement"; import { DiscountStatusEnum, VoucherDiscountType } from "@dashboard/graphql"; import { IntlShape, useIntl } from "react-intl"; +import { FilterContainer, FilterElement } from "../../FilterElement"; import { FilterAPIProvider } from "../FilterAPIProvider"; - -const getFilterElement = (value: FilterContainer, index: number): FilterElement => { - const possibleFilterElement = value[index]; - - if (typeof possibleFilterElement !== "string" && !Array.isArray(possibleFilterElement)) { - return possibleFilterElement; - } - - throw new Error("Unknown filter element used to create API handler"); -}; +import { ChannelHandler, EnumValuesHandler, Handler } from "../Handler"; +import { getFilterElement } from "../utils"; const createAPIHandler = ( selectedRow: FilterElement, diff --git a/src/components/ConditionalFilter/API/utils.test.ts b/src/components/ConditionalFilter/API/utils.test.ts new file mode 100644 index 00000000000..f21f0b02aad --- /dev/null +++ b/src/components/ConditionalFilter/API/utils.test.ts @@ -0,0 +1,43 @@ +import { ConditionOptions } from "@dashboard/components/ConditionalFilter/FilterElement/ConditionOptions"; +import { ConditionSelected } from "@dashboard/components/ConditionalFilter/FilterElement/ConditionSelected"; +import { ExpressionValue } from "@dashboard/components/ConditionalFilter/FilterElement/FilterElement"; + +import { Condition, FilterContainer, FilterElement } from "../FilterElement"; +import { getFilterElement } from "./utils"; + +describe("ConditionalFilter / API / utils / getFilterElement", () => { + it("should return filter element at index", () => { + // Arrange + const firstFilterElement: FilterElement = new FilterElement( + new ExpressionValue("price", "Price", "price"), + new Condition( + ConditionOptions.fromStaticElementName("price"), + new ConditionSelected( + { label: "price", slug: "price", value: "123" }, + { type: "price", value: "123", label: "Price" }, + [], + false, + ), + false, + ), + false, + ); + const filterContainer: FilterContainer = [firstFilterElement]; + + // Act + const result = getFilterElement(filterContainer, 0); + + // Assert + expect(result).toEqual(firstFilterElement); + }); + + it("throws error when unknown filter element is used", () => { + // Arrange + const filterContainer: FilterContainer = []; + + // Act & Assert + expect(() => getFilterElement(filterContainer, 2)).toThrowError( + "Unknown filter element used to create API handler", + ); + }); +}); diff --git a/src/components/ConditionalFilter/API/utils.ts b/src/components/ConditionalFilter/API/utils.ts new file mode 100644 index 00000000000..6e3f64e0af0 --- /dev/null +++ b/src/components/ConditionalFilter/API/utils.ts @@ -0,0 +1,15 @@ +import { FilterContainer, FilterElement } from "../FilterElement"; + +export const getFilterElement = (value: FilterContainer, index: number): FilterElement => { + const possibleFilterElement = value[index]; + + if ( + !possibleFilterElement || + typeof possibleFilterElement === "string" || + Array.isArray(possibleFilterElement) + ) { + throw new Error("Unknown filter element used to create API handler"); + } + + return possibleFilterElement; +}; diff --git a/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.test.ts b/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.test.ts index e83e784d74a..7e272998f11 100644 --- a/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.test.ts +++ b/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.test.ts @@ -4,6 +4,7 @@ import { toCollectionFetchingParams, toGiftCardsFetchingParams, toPageFetchingParams, + toProductTypesFetchingParams, toVouchersFetchingParams, } from "./fetchingParams"; @@ -87,6 +88,20 @@ describe("TokenArray / fetchingParams / getEmptyFetchingPrams", () => { usedBy: [], }); }); + + it("should return gift product tyes fetching params", () => { + // Arrange + const type = "product-types"; + + // Act + const fetchingParams = getEmptyFetchingPrams(type); + + // Assert + expect(fetchingParams).toEqual({ + typeOfProduct: [], + configurable: [], + }); + }); }); describe("TokenArray / fetchingParams / toVouchersFetchingParams", () => { @@ -198,3 +213,29 @@ describe("TokenArray / fetchingParams / toCollectionFetchingParams", () => { }); }); }); + +describe("TokenArray / fetchingParams / toProductTypesFetchingParams", () => { + it("should return fetching params", () => { + // Arrange + const params = { + typeOfProduct: [], + configurable: [], + }; + + const token = { + conditionKind: "is", + name: "typeOfProduct", + type: "s", + value: "SHIPPABLE", + } as UrlToken; + + // Act + const fetchingParams = toProductTypesFetchingParams(params, token); + + // Assert + expect(fetchingParams).toEqual({ + typeOfProduct: ["SHIPPABLE"], + configurable: [], + }); + }); +}); diff --git a/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.ts b/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.ts index b07d1f42ea8..25a9942a341 100644 --- a/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.ts +++ b/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.ts @@ -42,11 +42,17 @@ export interface CollectionFetchingParams { published: string[]; } +export interface ProductTypesFetchingParams { + typeOfProduct: string[]; + configurable: string[]; +} + type FetchingParamsKeys = keyof Omit; type OrderParamsKeys = keyof OrderFetchingParams; type VoucherParamsKeys = keyof VoucherFetchingParams; type PageParamsKeys = keyof PageFetchingParams; type GiftCardsParamKeys = keyof GiftCardsFetchingParams; +type ProductTypesParamsKeys = keyof ProductTypesFetchingParams; export const emptyFetchingParams: FetchingParams = { category: [], @@ -89,6 +95,11 @@ export const emptyCollectionFetchingParams: CollectionFetchingParams = { published: [], }; +export const emptyProductTypesFetchingParams: ProductTypesFetchingParams = { + typeOfProduct: [], + configurable: [], +}; + const unique = (array: Iterable) => Array.from(new Set(array)); const includedInParams = (c: UrlToken) => TokenType.ATTRIBUTE_DROPDOWN === c.type || TokenType.ATTRIBUTE_MULTISELECT === c.type; @@ -187,13 +198,26 @@ export const toCollectionFetchingParams = (p: CollectionFetchingParams, c: UrlTo return p; }; +export const toProductTypesFetchingParams = (p: ProductTypesFetchingParams, c: UrlToken) => { + const key = c.name as ProductTypesParamsKeys; + + if (!p[key]) { + p[key] = []; + } + + p[key] = unique(p[key].concat(c.value)); + + return p; +}; + export type FetchingParamsType = | OrderFetchingParams | FetchingParams | CollectionFetchingParams | GiftCardsFetchingParams | PageFetchingParams - | VoucherFetchingParams; + | VoucherFetchingParams + | ProductTypesFetchingParams; export const getEmptyFetchingPrams = (type: FilterProviderType) => { switch (type) { @@ -209,5 +233,7 @@ export const getEmptyFetchingPrams = (type: FilterProviderType) => { return emptyGiftCardsFetchingParams; case "collection": return emptyCollectionFetchingParams; + case "product-types": + return emptyProductTypesFetchingParams; } }; diff --git a/src/components/ConditionalFilter/ValueProvider/TokenArray/index.ts b/src/components/ConditionalFilter/ValueProvider/TokenArray/index.ts index 0363230b796..5c1e90e8b8e 100644 --- a/src/components/ConditionalFilter/ValueProvider/TokenArray/index.ts +++ b/src/components/ConditionalFilter/ValueProvider/TokenArray/index.ts @@ -11,11 +11,13 @@ import { GiftCardsFetchingParams, OrderFetchingParams, PageFetchingParams, + ProductTypesFetchingParams, toCollectionFetchingParams, toFetchingParams, toGiftCardsFetchingParams, toOrderFetchingParams, toPageFetchingParams, + toProductTypesFetchingParams, toVouchersFetchingParams, VoucherFetchingParams, } from "./fetchingParams"; @@ -99,6 +101,13 @@ export class TokenArray extends Array { toGiftCardsFetchingParams, params as GiftCardsFetchingParams, ); + case "product-types": + return this.asFlatArray() + .filter(token => token.isLoadable()) + .reduce( + toProductTypesFetchingParams, + params as ProductTypesFetchingParams, + ); default: return this.asFlatArray() .filter(token => token.isLoadable()) diff --git a/src/components/ConditionalFilter/ValueProvider/UrlToken.ts b/src/components/ConditionalFilter/ValueProvider/UrlToken.ts index 676fc814bb0..53f3180e2e7 100644 --- a/src/components/ConditionalFilter/ValueProvider/UrlToken.ts +++ b/src/components/ConditionalFilter/ValueProvider/UrlToken.ts @@ -39,6 +39,8 @@ const GIFT_CARDS_STATICS = ["currency", "products", "isActive", "tags", "usedBy" const COLLECTION_STATICS = ["channel", "published"]; +const PRODUCT_TYPES_STATICS = ["typeOfProduct", "configurable"]; + const STATIC_TO_LOAD = [ ...PRODUCT_STATICS, ...ORDER_STATICS, @@ -46,6 +48,7 @@ const STATIC_TO_LOAD = [ ...PAGE_STATIC, ...GIFT_CARDS_STATICS, ...COLLECTION_STATICS, + ...PRODUCT_TYPES_STATICS, ]; export const TokenType = { diff --git a/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts b/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts index fe48f3227d2..b6d5f51ca11 100644 --- a/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts +++ b/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts @@ -1,4 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ +import { InitialProductTypesAPIState } from "@dashboard/components/ConditionalFilter/API/initialState/productTypes/useInitialProdutTypesState"; import { stringify } from "qs"; import { useEffect, useState } from "react"; import useRouter from "use-react-router"; @@ -20,6 +21,7 @@ import { GiftCardsFetchingParams, OrderFetchingParams, PageFetchingParams, + ProductTypesFetchingParams, VoucherFetchingParams, } from "./TokenArray/fetchingParams"; import { prepareStructure } from "./utils"; @@ -79,6 +81,11 @@ export const useUrlValueProvider = ( fetchingParams as CollectionFetchingParams, ); break; + case "product-types": + (initialState as InitialProductTypesAPIState).fetchQueries( + fetchingParams as ProductTypesFetchingParams, + ); + break; } } }, [locationSearch]); diff --git a/src/components/ConditionalFilter/constants.ts b/src/components/ConditionalFilter/constants.ts index acea0513748..f42ae58a0a2 100644 --- a/src/components/ConditionalFilter/constants.ts +++ b/src/components/ConditionalFilter/constants.ts @@ -196,6 +196,20 @@ export const STATIC_CONDITIONS = { value: "input-1", }, ], + typeOfProduct: [ + { + type: "select", + label: "is", + value: "input-1", + }, + ], + configurable: [ + { + type: "select", + label: "is", + value: "input-1", + }, + ], }; export const CONSTRAINTS = { @@ -494,6 +508,21 @@ export const STATIC_CUSTOMER_OPTIONS: LeftOperand[] = [ }, ]; +export const STATIC_PRODUCT_TYPES_OPTIONS: LeftOperand[] = [ + { + value: "configurable", + label: "Configurable", + type: "configurable", + slug: "configurable", + }, + { + value: "typeOfProduct", + label: "Type", + type: "typeOfProduct", + slug: "typeOfProduct", + }, +]; + export const STATIC_OPTIONS = [ ...STATIC_PRODUCT_OPTIONS, ...STATIC_DISCOUNT_OPTIONS, @@ -504,6 +533,7 @@ export const STATIC_OPTIONS = [ ...STATIC_GIFT_CARDS_OPTIONS, ...STATIC_CUSTOMER_OPTIONS, ...STATIC_COLLECTION_OPTIONS, + ...STATIC_PRODUCT_TYPES_OPTIONS, ]; export const ATTRIBUTE_INPUT_TYPE_CONDITIONS = { diff --git a/src/components/ConditionalFilter/context/provider.tsx b/src/components/ConditionalFilter/context/provider.tsx index 71664358aaf..3f8e1194c5d 100644 --- a/src/components/ConditionalFilter/context/provider.tsx +++ b/src/components/ConditionalFilter/context/provider.tsx @@ -1,3 +1,5 @@ +import { useInitialProductTypesState } from "@dashboard/components/ConditionalFilter/API/initialState/productTypes/useInitialProdutTypesState"; +import { useProductTypesFilterAPIProvider } from "@dashboard/components/ConditionalFilter/API/providers/ProductTypesFilterAPIProvider"; import React, { FC } from "react"; import { useInitialCollectionState } from "../API/initialState/collections/useInitialCollectionsState"; @@ -24,6 +26,7 @@ import { STATIC_ORDER_OPTIONS, STATIC_PAGE_OPTIONS, STATIC_PRODUCT_OPTIONS, + STATIC_PRODUCT_TYPES_OPTIONS, STATIC_VOUCHER_OPTIONS, } from "../constants"; import { useContainerState } from "../useContainerState"; @@ -262,3 +265,29 @@ export const ConditionalCollectionFilterProvider: FC<{ ); }; + +export const ConditionalProductTypesFilterProvider: FC<{ + locationSearch: string; +}> = ({ children, locationSearch }) => { + const apiProvider = useProductTypesFilterAPIProvider(); + + const initialState = useInitialProductTypesState(); + const valueProvider = useUrlValueProvider(locationSearch, "product-types", initialState); + const leftOperandsProvider = useFilterLeftOperandsProvider(STATIC_PRODUCT_TYPES_OPTIONS); + const containerState = useContainerState(valueProvider); + const filterWindow = useFilterWindow(); + + return ( + + {children} + + ); +}; diff --git a/src/components/ConditionalFilter/queryVariables.test.ts b/src/components/ConditionalFilter/queryVariables.test.ts index 614a2609e74..db9f319780d 100644 --- a/src/components/ConditionalFilter/queryVariables.test.ts +++ b/src/components/ConditionalFilter/queryVariables.test.ts @@ -1,3 +1,5 @@ +import { ProductTypeConfigurable, ProductTypeEnum } from "@dashboard/graphql"; + import { Condition, FilterContainer, FilterElement } from "./FilterElement"; import { ConditionOptions } from "./FilterElement/ConditionOptions"; import { ConditionSelected } from "./FilterElement/ConditionSelected"; @@ -8,6 +10,7 @@ import { createGiftCardQueryVariables, createPageQueryVariables, createProductQueryVariables, + createProductTypesQueryVariables, creatVoucherQueryVariables, mapStaticQueryPartToLegacyVariables, } from "./queryVariables"; @@ -526,6 +529,63 @@ describe("ConditionalFilter / queryVariables / createCustomerQueryVariables", () }); }); +describe("ConditionalFilter / queryVariables / createProductTypesQueryVariables", () => { + it("should return empty variables for empty filters", () => { + // Arrange + const filters: FilterContainer = []; + const expectedOutput = {}; + // Act + const result = createProductTypesQueryVariables(filters); + + // Assert + expect(result).toEqual(expectedOutput); + }); + + it("should create variables with selected filters", () => { + // Arrange + const filters: FilterContainer = [ + new FilterElement( + new ExpressionValue("typeOfProduct", "Product type", "typeOfProduct"), + new Condition( + ConditionOptions.fromStaticElementName("typeOfProduct"), + new ConditionSelected( + ProductTypeEnum.DIGITAL, + { type: "select", label: "is", value: "input-1" }, + [], + false, + ), + false, + ), + false, + ), + "AND", + new FilterElement( + new ExpressionValue("configurable", "Configurable", "configurable"), + new Condition( + ConditionOptions.fromStaticElementName("configurable"), + new ConditionSelected( + ProductTypeConfigurable.SIMPLE, + { type: "select", label: "is", value: "input-1" }, + [], + false, + ), + false, + ), + false, + ), + ]; + const expectedOutput = { + productType: "DIGITAL", + configurable: "SIMPLE", + }; + // Act + const result = createProductTypesQueryVariables(filters); + + // Assert + expect(result).toEqual(expectedOutput); + }); +}); + describe("ConditionalFilter / queryVariables / mapStaticQueryPartToLegacyVariables", () => { it("should return queryPart if it is not an object", () => { // Arrange diff --git a/src/components/ConditionalFilter/queryVariables.ts b/src/components/ConditionalFilter/queryVariables.ts index 06eec2f249c..458c4c21eea 100644 --- a/src/components/ConditionalFilter/queryVariables.ts +++ b/src/components/ConditionalFilter/queryVariables.ts @@ -10,6 +10,7 @@ import { GlobalIdFilterInput, OrderDraftFilterInput, PageFilterInput, + ProductTypeFilterInput, ProductWhereInput, PromotionWhereInput, VoucherFilterInput, @@ -365,3 +366,23 @@ export const createCollectionsQueryVariables = (value: FilterContainer): Collect return p; }, {} as CollectionQueryVars); }; + +export const createProductTypesQueryVariables = ( + value: FilterContainer, +): ProductTypeFilterInput => { + return value.reduce((p, c) => { + if (typeof c === "string" || Array.isArray(c)) return p; + + const value = mapStaticQueryPartToLegacyVariables(createStaticQueryPart(c.condition.selected)); + + if (c.value.type === "typeOfProduct") { + p["productType"] = value; + + return p; + } + + (p[c.value.value as keyof ProductTypeFilterInput] as ProductTypeFilterInput) = value; + + return p; + }, {} as ProductTypeFilterInput); +}; diff --git a/src/components/ConditionalFilter/types.ts b/src/components/ConditionalFilter/types.ts index 762b6cab042..112bfedac58 100644 --- a/src/components/ConditionalFilter/types.ts +++ b/src/components/ConditionalFilter/types.ts @@ -8,6 +8,8 @@ import { InitialPageStateResponse } from "./API/initialState/page/InitialPageSta import { InitialPageAPIState } from "./API/initialState/page/useInitialPageState"; import { InitialProductStateResponse } from "./API/initialState/product/InitialProductStateResponse"; import { InitialProductAPIState } from "./API/initialState/product/useProductInitialAPIState"; +import { InitialProductTypesStateResponse } from "./API/initialState/productTypes/InitialProductTypesState"; +import { InitialProductTypesAPIState } from "./API/initialState/productTypes/useInitialProdutTypesState"; import { InitialVouchersStateResponse } from "./API/initialState/vouchers/InitialVouchersState"; import { InitialVoucherAPIState } from "./API/initialState/vouchers/useInitialVouchersState"; @@ -17,7 +19,8 @@ export type InitialResponseType = | InitialCollectionStateResponse | InitialVouchersStateResponse | InitialPageStateResponse - | InitialGiftCardsStateResponse; + | InitialGiftCardsStateResponse + | InitialProductTypesStateResponse; export type InitialAPIState = | InitialProductAPIState @@ -25,7 +28,8 @@ export type InitialAPIState = | InitialVoucherAPIState | InitialPageAPIState | InitialGiftCardsAPIState - | InitialCollectionAPIState; + | InitialCollectionAPIState + | InitialProductTypesAPIState; export type FilterProviderType = | "product" @@ -36,4 +40,5 @@ export type FilterProviderType = | "page" | "draft-order" | "gift-cards" - | "collection"; + | "collection" + | "product-types"; diff --git a/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx b/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx index 7501f5742f5..df6a92aee90 100644 --- a/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx +++ b/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx @@ -3,6 +3,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav"; import { DashboardCard } from "@dashboard/components/Card"; import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect"; import { ListPageLayout } from "@dashboard/components/Layouts"; +import { useFlag } from "@dashboard/featureFlags"; import { ProductTypeFragment } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; import { sectionNames } from "@dashboard/intl"; @@ -46,6 +47,7 @@ const ProductTypeListPage: React.FC = ({ const navigate = useNavigator(); const [isFilterPresetOpen, setFilterPresetOpen] = useState(false); const filterStructure = createFilterStructure(intl, filterOpts); + const { enabled: isProductTypesFilterEnabled } = useFlag("product_types_filters"); return ( @@ -96,17 +98,30 @@ const ProductTypeListPage: React.FC = ({ - + {isProductTypesFilterEnabled ? ( + + ) : ( + + )} diff --git a/src/productTypes/index.tsx b/src/productTypes/index.tsx index fb09c1869d4..c2e3190d20a 100644 --- a/src/productTypes/index.tsx +++ b/src/productTypes/index.tsx @@ -1,3 +1,4 @@ +import { ConditionalProductTypesFilterProvider } from "@dashboard/components/ConditionalFilter"; import { Route } from "@dashboard/components/Router"; import { sectionNames } from "@dashboard/intl"; import { asSortParams } from "@dashboard/utils/sort"; @@ -30,7 +31,11 @@ const ProductTypeList: React.FC> = ({ location }) => { }) as any; const params: ProductTypeListUrlQueryParams = asSortParams(qs, ProductTypeListUrlSortField); - return ; + return ( + + + + ); }; interface ProductTypeCreateRouteParams { diff --git a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx index 396414030df..6b7545609db 100644 --- a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx +++ b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx @@ -1,5 +1,8 @@ +import { useConditionalFilterContext } from "@dashboard/components/ConditionalFilter"; +import { createProductTypesQueryVariables } from "@dashboard/components/ConditionalFilter/queryVariables"; import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog"; +import { useFlag } from "@dashboard/featureFlags"; import { useProductTypeBulkDeleteMutation, useProductTypeListQuery } from "@dashboard/graphql"; import useBulkActions from "@dashboard/hooks/useBulkActions"; import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; @@ -40,6 +43,7 @@ interface ProductTypeListProps { export const ProductTypeList: React.FC = ({ params }) => { const navigate = useNavigator(); + const intl = useIntl(); const notify = useNotifier(); const { isSelected, @@ -49,7 +53,9 @@ export const ProductTypeList: React.FC = ({ params }) => { toggleAll, } = useBulkActions(params.ids); const { settings } = useListSettings(ListViews.PRODUCT_LIST); - const intl = useIntl(); + const { enabled: isProductTypesFilterEnabled } = useFlag("product_types_filters"); + const { valueProvider } = useConditionalFilterContext(); + const filters = createProductTypesQueryVariables(valueProvider.value); usePaginationReset(productTypeListUrl, params, settings.rowNumber); @@ -62,9 +68,20 @@ export const ProductTypeList: React.FC = ({ params }) => { }), [params, settings.rowNumber], ); + const newQueryVariables = React.useMemo( + () => ({ + ...paginationState, + filter: { + ...filters, + search: params.query, + }, + sort: getSortQueryVariables(params), + }), + [params, settings.rowNumber, valueProvider.value], + ); const { data, loading, refetch } = useProductTypeListQuery({ displayLoader: true, - variables: queryVariables, + variables: isProductTypesFilterEnabled ? newQueryVariables : queryVariables, }); const [changeFilters, resetFilters, handleSearchChange] = createFilterHandlers({ cleanupFn: reset,