diff --git a/.changeset/seven-moons-talk.md b/.changeset/seven-moons-talk.md new file mode 100644 index 00000000000..95be7198199 --- /dev/null +++ b/.changeset/seven-moons-talk.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +List of customers now uses conditional filters diff --git a/.featureFlags/customers_filters.md b/.featureFlags/customers_filters.md new file mode 100644 index 00000000000..c44d00958c0 --- /dev/null +++ b/.featureFlags/customers_filters.md @@ -0,0 +1,11 @@ +--- +name: customers_filters +displayName: Customers filtering +enabled: true +payload: "default" +visible: true +--- + +![new filters](./images/customers-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/.featureFlags/generated.tsx b/.featureFlags/generated.tsx index 65f93244cce..979585bde3b 100644 --- a/.featureFlags/generated.tsx +++ b/.featureFlags/generated.tsx @@ -1,25 +1,30 @@ // @ts-nocheck -import K01586 from "./images/discounts-list.png" -import J60656 from "./images/draft-orders-filters.png" -import W15309 from "./images/gift-cards-filters.png" -import P14884 from "./images/improved_refunds.png" -import J91775 from "./images/page-filters.png" -import F54595 from "./images/vouchers-filters.png" +import J53093 from "./images/customers-filters.png" +import Q09991 from "./images/discounts-list.png" +import Z15989 from "./images/draft-orders-filters.png" +import C33827 from "./images/gift-cards-filters.png" +import H90846 from "./images/improved_refunds.png" +import A12919 from "./images/page-filters.png" +import X74531 from "./images/vouchers-filters.png" -const discounts_rules = () => (<>

Discount rules

+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

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 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.

) export const AVAILABLE_FLAGS = [{ + name: "customers_filters", + displayName: "Customers filtering", + component: customers_filters, + visible: true, + content: { + enabled: true, + payload: "default", + } +},{ name: "discounts_rules", displayName: "Discounts rules", component: discounts_rules, diff --git a/.featureFlags/images/customers-filters.png b/.featureFlags/images/customers-filters.png new file mode 100644 index 00000000000..70ccc661e23 Binary files /dev/null and b/.featureFlags/images/customers-filters.png differ diff --git a/src/components/ConditionalFilter/API/CustomerFilterAPIProvider.tsx b/src/components/ConditionalFilter/API/CustomerFilterAPIProvider.tsx new file mode 100644 index 00000000000..5bc80d3c68a --- /dev/null +++ b/src/components/ConditionalFilter/API/CustomerFilterAPIProvider.tsx @@ -0,0 +1,16 @@ +import { FilterAPIProvider } from "./FilterAPIProvider"; + +export const useCustomerAPIProvider = (): FilterAPIProvider => { + const fetchRightOptions = async () => { + 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 29c6122a2fc..a68e87f4cd4 100644 --- a/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.ts +++ b/src/components/ConditionalFilter/ValueProvider/TokenArray/fetchingParams.ts @@ -163,7 +163,15 @@ export const toGiftCardsFetchingParams = (p: GiftCardsFetchingParams, c: UrlToke }; export const getFetchingPrams = ( - type: "product" | "order" | "discount" | "voucher" | "page" | "draft-order" | "gift-cards", + type: + | "product" + | "order" + | "discount" + | "voucher" + | "page" + | "draft-order" + | "gift-cards" + | "customer", ) => { switch (type) { case "product": diff --git a/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts b/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts index b5e2fdd8e6d..65a4ba966da 100644 --- a/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts +++ b/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts @@ -23,7 +23,15 @@ import { prepareStructure } from "./utils"; export const useUrlValueProvider = ( locationSearch: string, - type: "product" | "order" | "discount" | "voucher" | "page" | "draft-order" | "gift-cards", + type: + | "product" + | "order" + | "discount" + | "voucher" + | "page" + | "draft-order" + | "gift-cards" + | "customer", initialState?: | InitialAPIState | InitialOrderAPIState diff --git a/src/components/ConditionalFilter/constants.ts b/src/components/ConditionalFilter/constants.ts index ba3989ca1fe..5518f689761 100644 --- a/src/components/ConditionalFilter/constants.ts +++ b/src/components/ConditionalFilter/constants.ts @@ -99,6 +99,17 @@ export const STATIC_CONDITIONS = { { type: "date", label: "greater", value: "input-2" }, { type: "date.range", label: "between", value: "input-3" }, ], + dateJoined: [ + { type: "date", label: "lower", value: "input-1" }, + { type: "date", label: "greater", value: "input-2" }, + { type: "date.range", label: "between", value: "input-3" }, + ], + numberOfOrders: [ + { type: "number", label: "is", value: "input-1" }, + { type: "number", label: "lower", value: "input-1" }, + { type: "number", label: "greater", value: "input-2" }, + { type: "number.range", label: "between", value: "input-2" }, + ], started: [ { type: "datetime", label: "lower", value: "input-1" }, { type: "datetime", label: "greater", value: "input-2" }, @@ -433,6 +444,21 @@ export const STATIC_GIFT_CARDS_OPTIONS: LeftOperand[] = [ }, ]; +export const STATIC_CUSTOMER_OPTIONS: LeftOperand[] = [ + { + value: "dateJoined", + label: "Join date", + type: "dateJoined", + slug: "dateJoined", + }, + { + value: "numberOfOrders", + label: "Number of orders", + type: "numberOfOrders", + slug: "numberOfOrders", + }, +]; + export const STATIC_OPTIONS = [ ...STATIC_PRODUCT_OPTIONS, ...STATIC_DISCOUNT_OPTIONS, @@ -441,6 +467,7 @@ export const STATIC_OPTIONS = [ ...STATIC_PAGE_OPTIONS, ...STATIC_DRAFT_ORDER_OPTIONS, ...STATIC_GIFT_CARDS_OPTIONS, + ...STATIC_CUSTOMER_OPTIONS, ]; export const ATTRIBUTE_INPUT_TYPE_CONDITIONS = { diff --git a/src/components/ConditionalFilter/context/provider.tsx b/src/components/ConditionalFilter/context/provider.tsx index 4a82eaee71c..7d6bfcd0c7d 100644 --- a/src/components/ConditionalFilter/context/provider.tsx +++ b/src/components/ConditionalFilter/context/provider.tsx @@ -1,5 +1,6 @@ import React, { FC } from "react"; +import { useCustomerAPIProvider } from "../API/CustomerFilterAPIProvider"; import { useDiscountFilterAPIProvider } from "../API/DiscountFiltersAPIProvider"; import { useDraftOrderFilterAPIProvider } from "../API/DraftOrderFilterAPIProvider"; import { useGiftCardsFiltersAPIProvider } from "../API/GiftCardsFilterAPIProvider"; @@ -13,6 +14,7 @@ import { usePageAPIProvider } from "../API/PageFilterAPIProvider"; import { useProductFilterAPIProvider } from "../API/ProductFilterAPIProvider"; import { useVoucherAPIProvider } from "../API/VoucherFilterAPIProvider"; import { + STATIC_CUSTOMER_OPTIONS, STATIC_DISCOUNT_OPTIONS, STATIC_DRAFT_ORDER_OPTIONS, STATIC_GIFT_CARDS_OPTIONS, @@ -205,3 +207,28 @@ export const ConditionalGiftCardsFilterProver: FC<{ locationSearch: string }> = ); }; + +export const ConditionalCustomerFilterProvider: FC<{ + locationSearch: string; +}> = ({ children, locationSearch }) => { + const apiProvider = useCustomerAPIProvider(); + + const valueProvider = useUrlValueProvider(locationSearch, "customer"); + const leftOperandsProvider = useFilterLeftOperandsProvider(STATIC_CUSTOMER_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 afc4096fb2c..614a2609e74 100644 --- a/src/components/ConditionalFilter/queryVariables.test.ts +++ b/src/components/ConditionalFilter/queryVariables.test.ts @@ -4,6 +4,7 @@ import { ConditionSelected } from "./FilterElement/ConditionSelected"; import { ExpressionValue } from "./FilterElement/FilterElement"; import { creatDraftOrderQueryVariables, + createCustomerQueryVariables, createGiftCardQueryVariables, createPageQueryVariables, createProductQueryVariables, @@ -302,54 +303,6 @@ describe("ConditionalFilter / queryVariables / creatDraftOrderQueryVariables", ( }); }); -describe("ConditionalFilter / queryVariables / mapStaticQueryPartToLegacyVariables", () => { - it("should return queryPart if it is not an object", () => { - // Arrange - const queryPart = "queryPart"; - const expectedOutput = "queryPart"; - - // Act - const result = mapStaticQueryPartToLegacyVariables(queryPart); - - // Assert - expect(result).toEqual(expectedOutput); - }); - - it("should transform range input to legacy format", () => { - // Arrange - const queryPart = { range: { lte: "value" } }; - const expectedOutput = { lte: "value" }; - - // Act - const result = mapStaticQueryPartToLegacyVariables(queryPart); - - // Assert - expect(result).toEqual(expectedOutput); - }); - - it("should transform eq input to legacy format", () => { - // Arrange - const queryPart = { eq: "value" }; - const expectedOutput = "value"; - // Act - const result = mapStaticQueryPartToLegacyVariables(queryPart); - - // Assert - expect(result).toEqual(expectedOutput); - }); - - it("should transform oneOf input to legacy format", () => { - // Arrange - const queryPart = { oneOf: ["value1", "value2"] }; - const expectedOutput = ["value1", "value2"]; - // Act - const result = mapStaticQueryPartToLegacyVariables(queryPart); - - // Assert - expect(result).toEqual(expectedOutput); - }); -}); - describe("ConditionalFilter / queryVariables / createGiftCardQueryVariables", () => { it("should return empty variables for empty filters", () => { // Arrange @@ -516,6 +469,63 @@ describe("ConditionalFilter / queryVariables / createGiftCardQueryVariables", () }); }); +describe("ConditionalFilter / queryVariables / createCustomerQueryVariables", () => { + it("should return empty variables for empty filters", () => { + // Arrange + const filters: FilterContainer = []; + const expectedOutput = {}; + // Act + const result = createCustomerQueryVariables(filters); + + // Assert + expect(result).toEqual(expectedOutput); + }); + + it("should create variables with selected filters", () => { + // Arrange + const filters: FilterContainer = [ + new FilterElement( + new ExpressionValue("dateJoined", "Date joined", "dateJoined"), + new Condition( + ConditionOptions.fromStaticElementName("dateJoined"), + new ConditionSelected( + ["2025-02-01", "2025-02-08"], + { type: "number.range", label: "between", value: "input-2" }, + [], + false, + ), + false, + ), + false, + ), + "AND", + new FilterElement( + new ExpressionValue("numberOfOrders", "Number of orders", "numberOfOrders"), + new Condition( + ConditionOptions.fromStaticElementName("numberOfOrders"), + new ConditionSelected( + ["1", "100"], + { type: "number.range", label: "between", value: "input-2" }, + [], + false, + ), + false, + ), + false, + ), + ]; + const expectedOutput = { + dateJoined: { gte: "2025-02-01", lte: "2025-02-08" }, + numberOfOrders: { gte: "1", lte: "100" }, + }; + // Act + const result = createCustomerQueryVariables(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 69f99732b5f..01a72346437 100644 --- a/src/components/ConditionalFilter/queryVariables.ts +++ b/src/components/ConditionalFilter/queryVariables.ts @@ -1,5 +1,6 @@ import { AttributeInput, + CustomerFilterInput, DateRangeInput, DateTimeFilterInput, DateTimeRangeInput, @@ -318,3 +319,24 @@ export const createGiftCardQueryVariables = (value: FilterContainer) => { return p; }, {} as GiftCardFilterInput); }; + +export const createCustomerQueryVariables = (value: FilterContainer): CustomerFilterInput => { + return value.reduce((p, c) => { + if (typeof c === "string" || Array.isArray(c)) return p; + + if (c.value.type === "numberOfOrders" && c.condition.selected.conditionValue?.label === "is") { + p["numberOfOrders"] = { + gte: Number(c.condition.selected.value), + lte: Number(c.condition.selected.value), + }; + + return p; + } + + p[c.value.value as keyof CustomerFilterInput] = mapStaticQueryPartToLegacyVariables( + createStaticQueryPart(c.condition.selected), + ); + + return p; + }, {} as CustomerFilterInput); +}; diff --git a/src/customers/components/CustomerListPage/CustomerListPage.tsx b/src/customers/components/CustomerListPage/CustomerListPage.tsx index dca7d85656e..fe1b305875b 100644 --- a/src/customers/components/CustomerListPage/CustomerListPage.tsx +++ b/src/customers/components/CustomerListPage/CustomerListPage.tsx @@ -13,6 +13,7 @@ import { ButtonWithDropdown } from "@dashboard/components/ButtonWithDropdown"; import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect"; import { Customers } from "@dashboard/customers/types"; import { customerAddUrl, CustomerListUrlSortField, customerUrl } from "@dashboard/customers/urls"; +import { useFlag } from "@dashboard/featureFlags"; import useNavigator from "@dashboard/hooks/useNavigator"; import { sectionNames } from "@dashboard/intl"; import { FilterPagePropsWithPresets, PageListProps, SortPage } from "@dashboard/types"; @@ -56,6 +57,7 @@ const CustomerListPage: React.FC = ({ const userPermissions = useUserPermissions(); const structure = createFilterStructure(intl, filterOpts, userPermissions); const [isFilterPresetOpen, setFilterPresetOpen] = useState(false); + const { enabled: isCustomersFiltersEnabled } = useFlag("customers_filters"); const { CUSTOMER_OVERVIEW_CREATE, CUSTOMER_OVERVIEW_MORE_ACTIONS } = useExtensions( extensionMountPoints.CUSTOMER_LIST, ); @@ -122,25 +124,46 @@ const CustomerListPage: React.FC = ({ - - {selectedCustomerIds.length > 0 && ( - - - - )} - - } - /> + {isCustomersFiltersEnabled ? ( + + {selectedCustomerIds.length > 0 && ( + + + + )} + + } + /> + ) : ( + + {selectedCustomerIds.length > 0 && ( + + + + )} + + } + /> + )} > = ({ location }) => { const qs = parseQs(location.search.substr(1)) as any; const params: CustomerListUrlQueryParams = asSortParams(qs, CustomerListUrlSortField); - return ; + return ( + + + + ); }; interface CustomerDetailsRouteParams { diff --git a/src/customers/views/CustomerList/CustomerList.tsx b/src/customers/views/CustomerList/CustomerList.tsx index 2d8dea79e43..a3b67515f80 100644 --- a/src/customers/views/CustomerList/CustomerList.tsx +++ b/src/customers/views/CustomerList/CustomerList.tsx @@ -1,7 +1,10 @@ import ActionDialog from "@dashboard/components/ActionDialog"; +import { useConditionalFilterContext } from "@dashboard/components/ConditionalFilter"; +import { createCustomerQueryVariables } from "@dashboard/components/ConditionalFilter/queryVariables"; import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog"; import { WindowTitle } from "@dashboard/components/WindowTitle"; +import { useFlag } from "@dashboard/featureFlags"; import { useBulkRemoveCustomersMutation, useListCustomersQuery } from "@dashboard/graphql"; import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; @@ -38,6 +41,9 @@ export const CustomerList: React.FC = ({ params }) => { const notify = useNotifier(); const intl = useIntl(); const { updateListSettings, settings } = useListSettings(ListViews.CUSTOMER_LIST); + const { enabled: isCustomersFiltersEnabled } = useFlag("customers_filters"); + const { valueProvider } = useConditionalFilterContext(); + const filter = createCustomerQueryVariables(valueProvider.value); usePaginationReset(customerListUrl, params, settings.rowNumber); @@ -72,9 +78,21 @@ export const CustomerList: React.FC = ({ params }) => { }), [params, settings.rowNumber], ); + const newQueryVariables = React.useMemo( + () => ({ + ...paginationState, + filter: { + ...filter, + search: params.query, + }, + sort: getSortQueryVariables(params), + }), + [params, settings.rowNumber, valueProvider.value], + ); + const { data, refetch } = useListCustomersQuery({ displayLoader: true, - variables: queryVariables, + variables: isCustomersFiltersEnabled ? newQueryVariables : queryVariables, }); const customers = mapEdgesToItems(data?.customers); const [changeFilters, resetFilters, handleSearchChange] = createFilterHandlers({