Add filtering to customer list
This commit is contained in:
parent
d3bd6a6c22
commit
0ad80813fe
7 changed files with 285 additions and 14 deletions
|
@ -5,33 +5,41 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import Container from "@saleor/components/Container";
|
import Container from "@saleor/components/Container";
|
||||||
import PageHeader from "@saleor/components/PageHeader";
|
import PageHeader from "@saleor/components/PageHeader";
|
||||||
import SearchBar from "@saleor/components/SearchBar";
|
|
||||||
import { sectionNames } from "@saleor/intl";
|
import { sectionNames } from "@saleor/intl";
|
||||||
import {
|
import {
|
||||||
ListActions,
|
ListActions,
|
||||||
PageListProps,
|
PageListProps,
|
||||||
SearchPageProps,
|
|
||||||
TabPageProps,
|
TabPageProps,
|
||||||
SortPage
|
SortPage,
|
||||||
|
FilterPageProps
|
||||||
} from "@saleor/types";
|
} from "@saleor/types";
|
||||||
import { CustomerListUrlSortField } from "@saleor/customers/urls";
|
import { CustomerListUrlSortField } from "@saleor/customers/urls";
|
||||||
import { ListCustomers_customers_edges_node } from "../../types/ListCustomers";
|
import {
|
||||||
|
CustomerFilterKeys,
|
||||||
|
createFilterStructure
|
||||||
|
} from "@saleor/customers/views/CustomerList/filter";
|
||||||
|
import { CustomerListFilterOpts } from "@saleor/customers/types";
|
||||||
|
import FilterBar from "@saleor/components/FilterBar";
|
||||||
import CustomerList from "../CustomerList/CustomerList";
|
import CustomerList from "../CustomerList/CustomerList";
|
||||||
|
import { ListCustomers_customers_edges_node } from "../../types/ListCustomers";
|
||||||
|
|
||||||
export interface CustomerListPageProps
|
export interface CustomerListPageProps
|
||||||
extends PageListProps,
|
extends PageListProps,
|
||||||
ListActions,
|
ListActions,
|
||||||
SearchPageProps,
|
FilterPageProps<CustomerFilterKeys, CustomerListFilterOpts>,
|
||||||
SortPage<CustomerListUrlSortField>,
|
SortPage<CustomerListUrlSortField>,
|
||||||
TabPageProps {
|
TabPageProps {
|
||||||
customers: ListCustomers_customers_edges_node[];
|
customers: ListCustomers_customers_edges_node[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
||||||
|
currencySymbol,
|
||||||
currentTab,
|
currentTab,
|
||||||
|
filterOpts,
|
||||||
initialSearch,
|
initialSearch,
|
||||||
onAdd,
|
onAdd,
|
||||||
onAll,
|
onAll,
|
||||||
|
onFilterChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
|
@ -41,6 +49,8 @@ const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const structure = createFilterStructure(intl, filterOpts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<PageHeader title={intl.formatMessage(sectionNames.customers)}>
|
<PageHeader title={intl.formatMessage(sectionNames.customers)}>
|
||||||
|
@ -52,18 +62,21 @@ const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Card>
|
<Card>
|
||||||
<SearchBar
|
<FilterBar
|
||||||
allTabLabel={intl.formatMessage({
|
allTabLabel={intl.formatMessage({
|
||||||
defaultMessage: "All Customers",
|
defaultMessage: "All Customers",
|
||||||
description: "tab name"
|
description: "tab name"
|
||||||
})}
|
})}
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterStructure={structure}
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
searchPlaceholder={intl.formatMessage({
|
searchPlaceholder={intl.formatMessage({
|
||||||
defaultMessage: "Search Customer"
|
defaultMessage: "Search Customer"
|
||||||
})}
|
})}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
onAll={onAll}
|
onAll={onAll}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
onTabChange={onTabChange}
|
onTabChange={onTabChange}
|
||||||
onTabDelete={onTabDelete}
|
onTabDelete={onTabDelete}
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
import { FilterOpts, MinMax } from "@saleor/types";
|
||||||
|
|
||||||
|
export interface CustomerListFilterOpts {
|
||||||
|
joined: FilterOpts<MinMax>;
|
||||||
|
moneySpent: FilterOpts<MinMax>;
|
||||||
|
numberOfOrders: FilterOpts<MinMax>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AddressTypeInput {
|
export interface AddressTypeInput {
|
||||||
city: string;
|
city: string;
|
||||||
cityArea?: string;
|
cityArea?: string;
|
||||||
|
|
|
@ -16,6 +16,12 @@ export const customerSection = "/customers/";
|
||||||
|
|
||||||
export const customerListPath = customerSection;
|
export const customerListPath = customerSection;
|
||||||
export enum CustomerListUrlFiltersEnum {
|
export enum CustomerListUrlFiltersEnum {
|
||||||
|
joinedFrom = "joinedFrom",
|
||||||
|
joinedTo = "joinedTo",
|
||||||
|
moneySpentFrom = "moneySpentFrom",
|
||||||
|
moneySpentTo = "moneySpentTo",
|
||||||
|
numberOfOrdersFrom = "numberOfOrdersFrom",
|
||||||
|
numberOfOrdersTo = "numberOfOrdersTo",
|
||||||
query = "query"
|
query = "query"
|
||||||
}
|
}
|
||||||
export type CustomerListUrlFilters = Filters<CustomerListUrlFiltersEnum>;
|
export type CustomerListUrlFilters = Filters<CustomerListUrlFiltersEnum>;
|
||||||
|
|
|
@ -22,6 +22,9 @@ import { ListViews } from "@saleor/types";
|
||||||
import { getSortParams } from "@saleor/utils/sort";
|
import { getSortParams } from "@saleor/utils/sort";
|
||||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||||
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
|
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||||
|
import useShop from "@saleor/hooks/useShop";
|
||||||
import CustomerListPage from "../../components/CustomerListPage";
|
import CustomerListPage from "../../components/CustomerListPage";
|
||||||
import { TypedBulkRemoveCustomers } from "../../mutations";
|
import { TypedBulkRemoveCustomers } from "../../mutations";
|
||||||
import { useCustomerListQuery } from "../../queries";
|
import { useCustomerListQuery } from "../../queries";
|
||||||
|
@ -29,7 +32,6 @@ import { BulkRemoveCustomers } from "../../types/BulkRemoveCustomers";
|
||||||
import {
|
import {
|
||||||
customerAddUrl,
|
customerAddUrl,
|
||||||
customerListUrl,
|
customerListUrl,
|
||||||
CustomerListUrlFilters,
|
|
||||||
CustomerListUrlQueryParams,
|
CustomerListUrlQueryParams,
|
||||||
customerUrl,
|
customerUrl,
|
||||||
CustomerListUrlDialog
|
CustomerListUrlDialog
|
||||||
|
@ -40,7 +42,10 @@ import {
|
||||||
getActiveFilters,
|
getActiveFilters,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
getFilterVariables,
|
getFilterVariables,
|
||||||
saveFilterTab
|
saveFilterTab,
|
||||||
|
CustomerFilterKeys,
|
||||||
|
getFilterQueryParam,
|
||||||
|
getFilterOpts
|
||||||
} from "./filter";
|
} from "./filter";
|
||||||
import { getSortQueryVariables } from "./sort";
|
import { getSortQueryVariables } from "./sort";
|
||||||
|
|
||||||
|
@ -59,6 +64,7 @@ export const CustomerList: React.FC<CustomerListProps> = ({ params }) => {
|
||||||
ListViews.CUSTOMER_LIST
|
ListViews.CUSTOMER_LIST
|
||||||
);
|
);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const shop = useShop();
|
||||||
|
|
||||||
const paginationState = createPaginationState(settings.rowNumber, params);
|
const paginationState = createPaginationState(settings.rowNumber, params);
|
||||||
const queryVariables = React.useMemo(
|
const queryVariables = React.useMemo(
|
||||||
|
@ -83,17 +89,38 @@ export const CustomerList: React.FC<CustomerListProps> = ({ params }) => {
|
||||||
: 0
|
: 0
|
||||||
: parseInt(params.activeTab, 0);
|
: parseInt(params.activeTab, 0);
|
||||||
|
|
||||||
const changeFilterField = (filter: CustomerListUrlFilters) => {
|
const changeFilters = (filter: IFilter<CustomerFilterKeys>) => {
|
||||||
reset();
|
reset();
|
||||||
navigate(
|
navigate(
|
||||||
customerListUrl({
|
customerListUrl({
|
||||||
...getActiveFilters(params),
|
...params,
|
||||||
...filter,
|
...getFilterQueryParams(filter, getFilterQueryParam),
|
||||||
activeTab: undefined
|
activeTab: undefined
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
reset();
|
||||||
|
navigate(
|
||||||
|
customerListUrl({
|
||||||
|
asc: params.asc,
|
||||||
|
sort: params.sort
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (query: string) => {
|
||||||
|
reset();
|
||||||
|
navigate(
|
||||||
|
customerListUrl({
|
||||||
|
...params,
|
||||||
|
activeTab: undefined,
|
||||||
|
query
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const [openModal, closeModal] = createDialogActionHandlers<
|
const [openModal, closeModal] = createDialogActionHandlers<
|
||||||
CustomerListUrlDialog,
|
CustomerListUrlDialog,
|
||||||
CustomerListUrlQueryParams
|
CustomerListUrlQueryParams
|
||||||
|
@ -138,16 +165,20 @@ export const CustomerList: React.FC<CustomerListProps> = ({ params }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSort = createSortHandler(navigate, customerListUrl, params);
|
const handleSort = createSortHandler(navigate, customerListUrl, params);
|
||||||
|
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TypedBulkRemoveCustomers onCompleted={handleBulkCustomerDelete}>
|
<TypedBulkRemoveCustomers onCompleted={handleBulkCustomerDelete}>
|
||||||
{(bulkRemoveCustomers, bulkRemoveCustomersOpts) => (
|
{(bulkRemoveCustomers, bulkRemoveCustomersOpts) => (
|
||||||
<>
|
<>
|
||||||
<CustomerListPage
|
<CustomerListPage
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterOpts={getFilterOpts(params)}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
onSearchChange={query => changeFilterField({ query })}
|
onSearchChange={handleSearchChange}
|
||||||
onAll={() => navigate(customerListUrl())}
|
onFilterChange={changeFilters}
|
||||||
|
onAll={resetFilters}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabDelete={() => openModal("delete-search")}
|
||||||
onTabSave={() => openModal("save-search")}
|
onTabSave={() => openModal("save-search")}
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
|
import { IntlShape } from "react-intl";
|
||||||
|
|
||||||
import { CustomerFilterInput } from "@saleor/types/globalTypes";
|
import { CustomerFilterInput } from "@saleor/types/globalTypes";
|
||||||
|
import { maybe } from "@saleor/misc";
|
||||||
|
import {
|
||||||
|
createDateField,
|
||||||
|
createNumberField
|
||||||
|
} from "@saleor/utils/filters/fields";
|
||||||
|
import { IFilter, IFilterElement } from "@saleor/components/Filter";
|
||||||
import {
|
import {
|
||||||
createFilterTabUtils,
|
createFilterTabUtils,
|
||||||
createFilterUtils
|
createFilterUtils
|
||||||
|
@ -8,17 +16,180 @@ import {
|
||||||
CustomerListUrlFiltersEnum,
|
CustomerListUrlFiltersEnum,
|
||||||
CustomerListUrlQueryParams
|
CustomerListUrlQueryParams
|
||||||
} from "../../urls";
|
} from "../../urls";
|
||||||
|
import { CustomerListFilterOpts } from "../../types";
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export const CUSTOMER_FILTERS_KEY = "customerFilters";
|
export const CUSTOMER_FILTERS_KEY = "customerFilters";
|
||||||
|
|
||||||
|
export enum CustomerFilterKeys {
|
||||||
|
joined = "joined",
|
||||||
|
moneySpent = "spent",
|
||||||
|
numberOfOrders = "orders"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterOpts(
|
||||||
|
params: CustomerListUrlFilters
|
||||||
|
): CustomerListFilterOpts {
|
||||||
|
return {
|
||||||
|
joined: {
|
||||||
|
active: maybe(
|
||||||
|
() =>
|
||||||
|
[params.joinedFrom, params.joinedTo].some(
|
||||||
|
field => field !== undefined
|
||||||
|
),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
max: maybe(() => params.joinedTo, ""),
|
||||||
|
min: maybe(() => params.joinedFrom, "")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moneySpent: {
|
||||||
|
active: maybe(
|
||||||
|
() =>
|
||||||
|
[params.moneySpentFrom, params.moneySpentTo].some(
|
||||||
|
field => field !== undefined
|
||||||
|
),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
max: maybe(() => params.moneySpentTo, ""),
|
||||||
|
min: maybe(() => params.moneySpentFrom, "")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
numberOfOrders: {
|
||||||
|
active: maybe(
|
||||||
|
() =>
|
||||||
|
[params.numberOfOrdersFrom, params.numberOfOrdersTo].some(
|
||||||
|
field => field !== undefined
|
||||||
|
),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
max: maybe(() => params.numberOfOrdersTo, ""),
|
||||||
|
min: maybe(() => params.numberOfOrdersFrom, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFilterStructure(
|
||||||
|
intl: IntlShape,
|
||||||
|
opts: CustomerListFilterOpts
|
||||||
|
): IFilter<CustomerFilterKeys> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...createDateField(
|
||||||
|
CustomerFilterKeys.joined,
|
||||||
|
intl.formatMessage(messages.joinDate),
|
||||||
|
opts.joined.value
|
||||||
|
),
|
||||||
|
active: opts.joined.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createNumberField(
|
||||||
|
CustomerFilterKeys.moneySpent,
|
||||||
|
intl.formatMessage(messages.moneySpent),
|
||||||
|
opts.moneySpent.value
|
||||||
|
),
|
||||||
|
active: opts.moneySpent.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createNumberField(
|
||||||
|
CustomerFilterKeys.numberOfOrders,
|
||||||
|
intl.formatMessage(messages.numberOfOrders),
|
||||||
|
opts.numberOfOrders.value
|
||||||
|
),
|
||||||
|
active: opts.numberOfOrders.active
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export function getFilterVariables(
|
export function getFilterVariables(
|
||||||
params: CustomerListUrlFilters
|
params: CustomerListUrlFilters
|
||||||
): CustomerFilterInput {
|
): CustomerFilterInput {
|
||||||
return {
|
return {
|
||||||
|
dateJoined: {
|
||||||
|
gte: params.joinedFrom,
|
||||||
|
lte: params.joinedTo
|
||||||
|
},
|
||||||
|
moneySpent: {
|
||||||
|
gte: parseInt(params.moneySpentFrom, 0),
|
||||||
|
lte: parseInt(params.moneySpentTo, 0)
|
||||||
|
},
|
||||||
|
numberOfOrders: {
|
||||||
|
gte: parseInt(params.numberOfOrdersFrom, 0),
|
||||||
|
lte: parseInt(params.numberOfOrdersTo, 0)
|
||||||
|
},
|
||||||
search: params.query
|
search: params.query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFilterQueryParam(
|
||||||
|
filter: IFilterElement<CustomerFilterKeys>
|
||||||
|
): CustomerListUrlFilters {
|
||||||
|
const { active, multiple, name, value } = filter;
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case CustomerFilterKeys.joined:
|
||||||
|
if (!active) {
|
||||||
|
return {
|
||||||
|
joinedFrom: undefined,
|
||||||
|
joinedTo: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (multiple) {
|
||||||
|
return {
|
||||||
|
joinedFrom: value[0],
|
||||||
|
joinedTo: value[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
joinedFrom: value[0],
|
||||||
|
joinedTo: value[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
case CustomerFilterKeys.moneySpent:
|
||||||
|
if (!active) {
|
||||||
|
return {
|
||||||
|
moneySpentFrom: undefined,
|
||||||
|
moneySpentTo: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (multiple) {
|
||||||
|
return {
|
||||||
|
moneySpentFrom: value[0],
|
||||||
|
moneySpentTo: value[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
moneySpentFrom: value[0],
|
||||||
|
moneySpentTo: value[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
case CustomerFilterKeys.numberOfOrders:
|
||||||
|
if (!active) {
|
||||||
|
return {
|
||||||
|
numberOfOrdersFrom: undefined,
|
||||||
|
numberOfOrdersTo: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (multiple) {
|
||||||
|
return {
|
||||||
|
numberOfOrdersFrom: value[0],
|
||||||
|
numberOfOrdersTo: value[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
numberOfOrdersFrom: value[0],
|
||||||
|
numberOfOrdersTo: value[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
deleteFilterTab,
|
deleteFilterTab,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
|
|
17
src/customers/views/CustomerList/messages.ts
Normal file
17
src/customers/views/CustomerList/messages.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
joinDate: {
|
||||||
|
defaultMessage: "Join Date",
|
||||||
|
description: "customer"
|
||||||
|
},
|
||||||
|
moneySpent: {
|
||||||
|
defaultMessage: "Money Spent",
|
||||||
|
description: "customer"
|
||||||
|
},
|
||||||
|
numberOfOrders: {
|
||||||
|
defaultMessage: "Number of Orders"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
|
@ -11,17 +11,42 @@ import {
|
||||||
pageListProps,
|
pageListProps,
|
||||||
searchPageProps,
|
searchPageProps,
|
||||||
tabPageProps,
|
tabPageProps,
|
||||||
sortPageProps
|
sortPageProps,
|
||||||
|
filterPageProps
|
||||||
} from "../../../fixtures";
|
} from "../../../fixtures";
|
||||||
import Decorator from "../../Decorator";
|
import Decorator from "../../Decorator";
|
||||||
|
|
||||||
const props: CustomerListPageProps = {
|
const props: CustomerListPageProps = {
|
||||||
|
...filterPageProps,
|
||||||
...listActionsProps,
|
...listActionsProps,
|
||||||
...pageListProps.default,
|
...pageListProps.default,
|
||||||
...searchPageProps,
|
...searchPageProps,
|
||||||
...sortPageProps,
|
...sortPageProps,
|
||||||
...tabPageProps,
|
...tabPageProps,
|
||||||
customers: customerList,
|
customers: customerList,
|
||||||
|
filterOpts: {
|
||||||
|
joined: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: undefined,
|
||||||
|
min: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moneySpent: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: undefined,
|
||||||
|
min: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
numberOfOrders: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: undefined,
|
||||||
|
min: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
sort: {
|
sort: {
|
||||||
...sortPageProps.sort,
|
...sortPageProps.sort,
|
||||||
sort: CustomerListUrlSortField.name
|
sort: CustomerListUrlSortField.name
|
||||||
|
|
Loading…
Reference in a new issue