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
+---
+
+
+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 = () => (<>

+const customers_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 = () => (<>
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 = () => (<>
+const draft_orders_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 = () => (<>
+const gift_cards_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 = () => (<>
+const improved_refunds = () => (<>
Enable the enhanced refund feature to streamline your refund process:
>)
-const pages_filters = () => (<>
+const pages_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 = () => (<>
+const vouchers_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({