From dc8a72b44e12eb9032ff1392b8a94d391a3a20d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Thu, 13 Feb 2025 16:56:16 +0100 Subject: [PATCH 1/9] Migrate from tabs to filter presets --- locale/defaultMessages.json | 14 +-- .../ProductTypeListPage.tsx | 112 ++++++++++++------ .../views/ProductTypeList/ProductTypeList.tsx | 73 ++++++------ .../views/ProductTypeList/filters.ts | 3 +- 4 files changed, 114 insertions(+), 88 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index a32f6309c4e..4456dcc0adf 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -582,10 +582,6 @@ "context": "expiry date section header", "string": "Expiry date" }, - "1KSqnn": { - "context": "tab name", - "string": "All Product Types" - }, "1LBYpE": { "context": "dialog header", "string": "Delete Menus" @@ -1675,6 +1671,9 @@ "context": "navigation section name", "string": "Navigation" }, + "9CC8JI": { + "string": "Search product types..." + }, "9CEu8k": { "context": "modal button images upload", "string": "Upload Images" @@ -7270,6 +7269,10 @@ "ivJ1qt": { "string": "Manage your permission groups and their permissions" }, + "ivmwpV": { + "context": "tab name", + "string": "All product types" + }, "ixjvkM": { "string": "We’ve created your default token. Make sure to copy your new personal access token now. You won’t be able to see it again." }, @@ -8572,9 +8575,6 @@ "context": "expires in label", "string": "Expires in" }, - "rpFdD1": { - "string": "Search Product Type" - }, "rqiCWU": { "string": "Saved changes" }, diff --git a/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx b/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx index 30327eb944b..407964c07c2 100644 --- a/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx +++ b/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx @@ -1,32 +1,30 @@ // @ts-strict-ignore +import { ListFilters } from "@dashboard/components/AppLayout/ListFilters"; import { TopNav } from "@dashboard/components/AppLayout/TopNav"; -import { Button } from "@dashboard/components/Button"; import { DashboardCard } from "@dashboard/components/Card"; -import FilterBar from "@dashboard/components/FilterBar"; -import { configurationMenuUrl } from "@dashboard/configuration"; +import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect"; +import { ListPageLayout } from "@dashboard/components/Layouts"; import { ProductTypeFragment } from "@dashboard/graphql"; +import useNavigator from "@dashboard/hooks/useNavigator"; import { sectionNames } from "@dashboard/intl"; import ProductTypeList from "@dashboard/productTypes/components/ProductTypeList/ProductTypeList"; import { productTypeAddUrl, ProductTypeListUrlSortField } from "@dashboard/productTypes/urls"; -import React from "react"; +import { Box, Button, ChevronRightIcon } from "@saleor/macaw-ui-next"; +import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { - FilterPageProps, - ListActions, - PageListProps, - SortPage, - TabPageProps, -} from "../../../types"; +import { FilterPageProps, ListActions, PageListProps, SortPage } from "../../../types"; import { createFilterStructure, ProductTypeFilterKeys, ProductTypeListFilterOpts } from "./filters"; export interface ProductTypeListPageProps extends PageListProps, ListActions, - FilterPageProps, - SortPage, - TabPageProps { + Omit, "onTabDelete">, + SortPage { productTypes: ProductTypeFragment[]; + onTabUpdate: (tabName: string) => void; + onTabDelete: (id: number) => void; + hasPresetsChanged: () => boolean; } const ProductTypeListPage: React.FC = ({ @@ -39,44 +37,80 @@ const ProductTypeListPage: React.FC = ({ onTabChange, onTabDelete, onTabSave, + onTabUpdate, tabs, + hasPresetsChanged, + disabled, ...listProps }) => { const intl = useIntl(); - const structure = createFilterStructure(intl, filterOpts); + const navigate = useNavigator(); + const [isFilterPresetOpen, setFilterPresetOpen] = useState(false); + const filterStructure = createFilterStructure(intl, filterOpts); return ( - <> - - + + + + + + + + + + + + + + - - + - + + - + ); }; diff --git a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx index c9f41c7cc9e..6d2418ed7d6 100644 --- a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx +++ b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx @@ -1,10 +1,9 @@ // @ts-strict-ignore import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; -import SaveFilterTabDialog, { - SaveFilterTabDialogFormData, -} from "@dashboard/components/SaveFilterTabDialog"; +import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog"; import { useProductTypeBulkDeleteMutation, useProductTypeListQuery } from "@dashboard/graphql"; import useBulkActions from "@dashboard/hooks/useBulkActions"; +import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; import useNotifier from "@dashboard/hooks/useNotifier"; @@ -33,16 +32,7 @@ import { ProductTypeListUrlDialog, ProductTypeListUrlQueryParams, } from "../../urls"; -import { - deleteFilterTab, - getActiveFilters, - getFilterOpts, - getFilterQueryParam, - getFiltersCurrentTab, - getFilterTabs, - getFilterVariables, - saveFilterTab, -} from "./filters"; +import { getFilterOpts, getFilterQueryParam, getFilterVariables, storageUtils } from "./filters"; import { getSortQueryVariables } from "./sort"; interface ProductTypeListProps { @@ -77,8 +67,6 @@ export const ProductTypeList: React.FC = ({ params }) => { displayLoader: true, variables: queryVariables, }); - const tabs = getFilterTabs(); - const currentTab = getFiltersCurrentTab(params, tabs); const [changeFilters, resetFilters, handleSearchChange] = createFilterHandlers({ cleanupFn: reset, createUrl: productTypeListUrl, @@ -90,24 +78,24 @@ export const ProductTypeList: React.FC = ({ params }) => { ProductTypeListUrlDialog, ProductTypeListUrlQueryParams >(navigate, productTypeListUrl, params); - const handleTabChange = (tab: number) => { - reset(); - navigate( - productTypeListUrl({ - activeTab: tab.toString(), - ...getFilterTabs()[tab - 1].data, - }), - ); - }; - const handleTabDelete = () => { - deleteFilterTab(currentTab); - reset(); - navigate(productTypeListUrl()); - }; - const handleTabSave = (data: SaveFilterTabDialogFormData) => { - saveFilterTab(data.name, getActiveFilters(params)); - handleTabChange(tabs.length + 1); - }; + + const { + selectedPreset, + presets, + hasPresetsChanged, + onPresetChange, + onPresetDelete, + onPresetSave, + onPresetUpdate, + setPresetIdToDelete, + getPresetNameToDelete, + } = useFilterPresets({ + params, + reset, + getUrl: productTypeListUrl, + storageUtils, + }); + const paginationValues = usePaginator({ pageInfo: maybe(() => data.productTypes.pageInfo), paginationState, @@ -150,16 +138,20 @@ export const ProductTypeList: React.FC = ({ params }) => { return ( openModal("delete-search")} + onTabChange={onPresetChange} + onTabDelete={(id: number) => { + setPresetIdToDelete(id); + openModal("delete-search"); + }} onTabSave={() => openModal("save-search")} - tabs={tabs.map(tab => tab.name)} + onTabUpdate={onPresetUpdate} + tabs={presets.map(tab => tab.name)} disabled={loading} productTypes={productTypesData} onSort={handleSort} @@ -182,6 +174,7 @@ export const ProductTypeList: React.FC = ({ params }) => { } + hasPresetsChanged={hasPresetsChanged} /> {productTypesData && ( = ({ params }) => { open={params.action === "save-search"} confirmButtonState="default" onClose={closeModal} - onSubmit={handleTabSave} + onSubmit={onPresetSave} /> tabs[currentTab - 1].name, "...")} + onSubmit={onPresetDelete} + tabName={getPresetNameToDelete()} /> ); diff --git a/src/productTypes/views/ProductTypeList/filters.ts b/src/productTypes/views/ProductTypeList/filters.ts index 8cba86f2edd..e494d2372fe 100644 --- a/src/productTypes/views/ProductTypeList/filters.ts +++ b/src/productTypes/views/ProductTypeList/filters.ts @@ -61,8 +61,7 @@ export function getFilterQueryParam( } } -export const { deleteFilterTab, getFilterTabs, saveFilterTab } = - createFilterTabUtils(PRODUCT_TYPE_FILTERS_KEY); +export const storageUtils = createFilterTabUtils(PRODUCT_TYPE_FILTERS_KEY); export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } = createFilterUtils< ProductTypeListUrlQueryParams, From 318d71d15145c41e5c2bbe05c71703b85d3d87c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Thu, 13 Feb 2025 17:05:56 +0100 Subject: [PATCH 2/9] Add changeset --- .changeset/flat-squids-bow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/flat-squids-bow.md diff --git a/.changeset/flat-squids-bow.md b/.changeset/flat-squids-bow.md new file mode 100644 index 00000000000..a561e0cb714 --- /dev/null +++ b/.changeset/flat-squids-bow.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +You can now save,update and delete filter presets on product types list From 8a25ea24a34ceca1cd18be6230af36e86c4f38cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Fri, 14 Feb 2025 11:03:24 +0100 Subject: [PATCH 3/9] Update test id --- .../components/ProductTypeListPage/ProductTypeListPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx b/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx index 407964c07c2..634e929af87 100644 --- a/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx +++ b/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx @@ -84,7 +84,7 @@ const ProductTypeListPage: React.FC = ({ disabled={disabled} variant="primary" onClick={() => navigate(productTypeAddUrl())} - data-test-id="create-product-type" + data-test-id="add-product-type" > Date: Fri, 14 Feb 2025 13:11:21 +0100 Subject: [PATCH 4/9] Fix import in providers and extract getFilterElement with test --- .../providers/CollectionFilterAPIProvider.tsx | 10 +---- .../providers/DiscountFiltersAPIProvider.tsx | 2 +- .../providers/DraftOrderFilterAPIProvider.tsx | 2 +- .../providers/GiftCardsFilterAPIProvider.tsx | 21 +++------ .../API/providers/OrderFilterAPIProvider.tsx | 13 +----- .../API/providers/PageFilterAPIProvider.tsx | 18 ++------ .../providers/ProductFilterAPIProvider.tsx | 10 +---- .../API/providers/VoucherFilterAPIProvider.ts | 22 ++-------- .../ConditionalFilter/API/utils.test.ts | 43 +++++++++++++++++++ src/components/ConditionalFilter/API/utils.ts | 15 +++++++ 10 files changed, 75 insertions(+), 81 deletions(-) create mode 100644 src/components/ConditionalFilter/API/utils.test.ts create mode 100644 src/components/ConditionalFilter/API/utils.ts 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/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; +}; From c678a864a58df95ed2bdd7a221910aaa86947d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Fri, 14 Feb 2025 15:07:57 +0100 Subject: [PATCH 5/9] Build flags --- .featureFlags/generated.tsx | 46 ++++++++++++------ .../images/product-types-filters.png | Bin 0 -> 31133 bytes .featureFlags/product_types_filters.md | 11 +++++ 3 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 .featureFlags/images/product-types-filters.png create mode 100644 .featureFlags/product_types_filters.md 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:

  • • Choose between automatic calculations based on selected items or enter refund amounts directly for overcharges and custom adjustments.

    @@ -39,11 +40,15 @@ const improved_refunds = () => (<>

    Improved refunds<

) -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 0000000000000000000000000000000000000000..997e65ccf5ad5794486c31789bfb9f05d344a2f2 GIT binary patch literal 31133 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 a5dGY1Qwg1RcTYbWCnh8 Date: Fri, 14 Feb 2025 15:08:13 +0100 Subject: [PATCH 6/9] Add new conditional filters --- locale/defaultMessages.json | 8 +++ .../productTypes/InitialProductTypesState.ts | 39 ++++++++++++ .../useInitialProdutTypesState.ts | 61 +++++++++++++++++++ src/components/ConditionalFilter/API/intl.ts | 15 +++++ .../ConditionalFilter/API/messages.ts | 13 ++++ .../ProductTypesFilterAPIProvider.tsx | 51 ++++++++++++++++ .../TokenArray/fetchingParams.ts | 28 ++++++++- .../ValueProvider/TokenArray/index.ts | 9 +++ .../ValueProvider/UrlToken.ts | 3 + .../ValueProvider/useUrlValueProvider.ts | 7 +++ src/components/ConditionalFilter/constants.ts | 30 +++++++++ .../ConditionalFilter/context/provider.tsx | 29 +++++++++ .../ConditionalFilter/queryVariables.ts | 21 +++++++ src/components/ConditionalFilter/types.ts | 11 +++- .../ProductTypeListPage.tsx | 34 ++++++++--- src/productTypes/index.tsx | 7 ++- .../views/ProductTypeList/ProductTypeList.tsx | 21 ++++++- 17 files changed, 370 insertions(+), 17 deletions(-) create mode 100644 src/components/ConditionalFilter/API/initialState/productTypes/InitialProductTypesState.ts create mode 100644 src/components/ConditionalFilter/API/initialState/productTypes/useInitialProdutTypesState.ts create mode 100644 src/components/ConditionalFilter/API/providers/ProductTypesFilterAPIProvider.tsx diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 4456dcc0adf..38f23a227cd 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -3745,6 +3745,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." @@ -4780,6 +4784,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.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/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/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.ts b/src/components/ConditionalFilter/queryVariables.ts index 06eec2f249c..74e145f07b3 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] = 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 634e929af87..be9de6c5dd7 100644 --- a/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx +++ b/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx @@ -4,6 +4,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"; @@ -47,6 +48,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 ( @@ -97,16 +99,28 @@ 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 6d2418ed7d6..7324762c5e6 100644 --- a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx +++ b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx @@ -1,6 +1,9 @@ // @ts-strict-ignore +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"; @@ -41,6 +44,7 @@ interface ProductTypeListProps { export const ProductTypeList: React.FC = ({ params }) => { const navigate = useNavigator(); + const intl = useIntl(); const notify = useNotifier(); const { isSelected, @@ -50,7 +54,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); @@ -63,9 +69,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, From e490b0f88bad6ac052086dc2fa6eb3ae13ef40ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Fri, 14 Feb 2025 15:20:31 +0100 Subject: [PATCH 7/9] Add tests --- .../InitialProductTypesState.test.ts | 70 +++++++++++++++++++ .../TokenArray/fetchingParams.test.ts | 41 +++++++++++ .../ConditionalFilter/queryVariables.test.ts | 60 ++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 src/components/ConditionalFilter/API/initialState/productTypes/InitialProductTypesState.test.ts 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/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/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 From 11b75a77860f0e8e678384d3c481515ac6ee3629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Fri, 14 Feb 2025 15:22:04 +0100 Subject: [PATCH 8/9] Add changeset --- .changeset/curly-penguins-vanish.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curly-penguins-vanish.md 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. From 27af4dc35714cd254dbe586c1f2302f69de6cbeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Fri, 14 Feb 2025 15:25:01 +0100 Subject: [PATCH 9/9] Fix type --- src/components/ConditionalFilter/queryVariables.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ConditionalFilter/queryVariables.ts b/src/components/ConditionalFilter/queryVariables.ts index 74e145f07b3..458c4c21eea 100644 --- a/src/components/ConditionalFilter/queryVariables.ts +++ b/src/components/ConditionalFilter/queryVariables.ts @@ -381,7 +381,7 @@ export const createProductTypesQueryVariables = ( return p; } - p[c.value.value as keyof ProductTypeFilterInput] = value; + (p[c.value.value as keyof ProductTypeFilterInput] as ProductTypeFilterInput) = value; return p; }, {} as ProductTypeFilterInput);