Add filters to voucher list

This commit is contained in:
dominik-zeglen 2020-01-03 16:17:51 +01:00
parent f9d0c22531
commit 7820c65050
7 changed files with 375 additions and 19 deletions

View file

@ -5,23 +5,28 @@ 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 FilterBar from "@saleor/components/FilterBar";
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 { VoucherListUrlSortField } from "@saleor/discounts/urls"; import { VoucherListUrlSortField } from "@saleor/discounts/urls";
import {
createFilterStructure,
VoucherFilterKeys
} from "@saleor/discounts/views/VoucherList/filter";
import { VoucherListFilterOpts } from "@saleor/discounts/types";
import { VoucherList_vouchers_edges_node } from "../../types/VoucherList"; import { VoucherList_vouchers_edges_node } from "../../types/VoucherList";
import VoucherList from "../VoucherList"; import VoucherList from "../VoucherList";
export interface VoucherListPageProps export interface VoucherListPageProps
extends PageListProps, extends PageListProps,
ListActions, ListActions,
SearchPageProps, FilterPageProps<VoucherFilterKeys, VoucherListFilterOpts>,
SortPage<VoucherListUrlSortField>, SortPage<VoucherListUrlSortField>,
TabPageProps { TabPageProps {
defaultCurrency: string; defaultCurrency: string;
@ -29,10 +34,13 @@ export interface VoucherListPageProps
} }
const VoucherListPage: React.FC<VoucherListPageProps> = ({ const VoucherListPage: React.FC<VoucherListPageProps> = ({
currencySymbol,
currentTab, currentTab,
filterOpts,
initialSearch, initialSearch,
onAdd, onAdd,
onAll, onAll,
onFilterChange,
onSearchChange, onSearchChange,
onTabChange, onTabChange,
onTabDelete, onTabDelete,
@ -42,6 +50,8 @@ const VoucherListPage: React.FC<VoucherListPageProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const structure = createFilterStructure(intl, filterOpts);
return ( return (
<Container> <Container>
<PageHeader title={intl.formatMessage(sectionNames.vouchers)}> <PageHeader title={intl.formatMessage(sectionNames.vouchers)}>
@ -53,18 +63,21 @@ const VoucherListPage: React.FC<VoucherListPageProps> = ({
</Button> </Button>
</PageHeader> </PageHeader>
<Card> <Card>
<SearchBar <FilterBar
allTabLabel={intl.formatMessage({ allTabLabel={intl.formatMessage({
defaultMessage: "All Vouchers", defaultMessage: "All Vouchers",
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 Voucher" defaultMessage: "Search Voucher"
})} })}
tabs={tabs} tabs={tabs}
onAll={onAll} onAll={onAll}
onFilterChange={onFilterChange}
onSearchChange={onSearchChange} onSearchChange={onSearchChange}
onTabChange={onTabChange} onTabChange={onTabChange}
onTabDelete={onTabDelete} onTabDelete={onTabDelete}

View file

@ -1,7 +1,8 @@
import { FilterOpts, MinMax } from "@saleor/types"; import { FilterOpts, MinMax } from "@saleor/types";
import { import {
DiscountStatusEnum, DiscountStatusEnum,
DiscountValueTypeEnum DiscountValueTypeEnum,
VoucherDiscountType
} from "@saleor/types/globalTypes"; } from "@saleor/types/globalTypes";
export interface SaleListFilterOpts { export interface SaleListFilterOpts {
@ -10,6 +11,13 @@ export interface SaleListFilterOpts {
status: FilterOpts<DiscountStatusEnum[]>; status: FilterOpts<DiscountStatusEnum[]>;
} }
export interface VoucherListFilterOpts {
saleType: FilterOpts<VoucherDiscountType[]>;
started: FilterOpts<MinMax>;
status: FilterOpts<DiscountStatusEnum[]>;
timesUsed: FilterOpts<MinMax>;
}
export enum RequirementsPicker { export enum RequirementsPicker {
ORDER = "ORDER", ORDER = "ORDER",
ITEM = "ITEM", ITEM = "ITEM",

View file

@ -67,9 +67,18 @@ export const saleAddUrl = saleAddPath;
export const voucherSection = urlJoin(discountSection, "vouchers"); export const voucherSection = urlJoin(discountSection, "vouchers");
export const voucherListPath = voucherSection; export const voucherListPath = voucherSection;
export enum VoucherListUrlFiltersEnum { export enum VoucherListUrlFiltersEnum {
startedFrom = "startedFrom",
startedTo = "startedTo",
timesUsedFrom = "timesUsedFrom",
timesUsedTo = "timesUsedTo",
query = "query" query = "query"
} }
export type VoucherListUrlFilters = Filters<VoucherListUrlFiltersEnum>; export enum VoucherListUrlFiltersWithMultipleValues {
status = "status",
type = "type"
}
export type VoucherListUrlFilters = Filters<VoucherListUrlFiltersEnum> &
FiltersWithMultipleValues<VoucherListUrlFiltersWithMultipleValues>;
export type VoucherListUrlDialog = "remove" | TabActionDialog; export type VoucherListUrlDialog = "remove" | TabActionDialog;
export enum VoucherListUrlSortField { export enum VoucherListUrlSortField {
code = "code", code = "code",

View file

@ -24,6 +24,8 @@ 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 { getFilterQueryParams } from "@saleor/utils/filters";
import { IFilter } from "@saleor/components/Filter";
import VoucherListPage from "../../components/VoucherListPage"; import VoucherListPage from "../../components/VoucherListPage";
import { TypedVoucherBulkDelete } from "../../mutations"; import { TypedVoucherBulkDelete } from "../../mutations";
import { useVoucherListQuery } from "../../queries"; import { useVoucherListQuery } from "../../queries";
@ -31,7 +33,6 @@ import { VoucherBulkDelete } from "../../types/VoucherBulkDelete";
import { import {
voucherAddUrl, voucherAddUrl,
voucherListUrl, voucherListUrl,
VoucherListUrlFilters,
VoucherListUrlQueryParams, VoucherListUrlQueryParams,
voucherUrl, voucherUrl,
VoucherListUrlDialog VoucherListUrlDialog
@ -42,7 +43,10 @@ import {
getActiveFilters, getActiveFilters,
getFilterTabs, getFilterTabs,
getFilterVariables, getFilterVariables,
saveFilterTab saveFilterTab,
getFilterQueryParam,
VoucherFilterKeys,
getFilterOpts
} from "./filter"; } from "./filter";
import { getSortQueryVariables } from "./sort"; import { getSortQueryVariables } from "./sort";
@ -86,17 +90,38 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
: 0 : 0
: parseInt(params.activeTab, 0); : parseInt(params.activeTab, 0);
const changeFilterField = (filter: VoucherListUrlFilters) => { const changeFilters = (filter: IFilter<VoucherFilterKeys>) => {
reset(); reset();
navigate( navigate(
voucherListUrl({ voucherListUrl({
...getActiveFilters(params), ...params,
...filter, ...getFilterQueryParams(filter, getFilterQueryParam),
activeTab: undefined activeTab: undefined
}) })
); );
}; };
const resetFilters = () => {
reset();
navigate(
voucherListUrl({
asc: params.asc,
sort: params.sort
})
);
};
const handleSearchChange = (query: string) => {
reset();
navigate(
voucherListUrl({
...params,
activeTab: undefined,
query
})
);
};
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
VoucherListUrlDialog, VoucherListUrlDialog,
VoucherListUrlQueryParams VoucherListUrlQueryParams
@ -143,6 +168,7 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
}; };
const handleSort = createSortHandler(navigate, voucherListUrl, params); const handleSort = createSortHandler(navigate, voucherListUrl, params);
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
return ( return (
<TypedVoucherBulkDelete onCompleted={handleVoucherBulkDelete}> <TypedVoucherBulkDelete onCompleted={handleVoucherBulkDelete}>
@ -158,10 +184,13 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
<> <>
<WindowTitle title={intl.formatMessage(sectionNames.vouchers)} /> <WindowTitle title={intl.formatMessage(sectionNames.vouchers)} />
<VoucherListPage <VoucherListPage
currencySymbol={currencySymbol}
currentTab={currentTab} currentTab={currentTab}
filterOpts={getFilterOpts(params)}
initialSearch={params.query || ""} initialSearch={params.query || ""}
onSearchChange={query => changeFilterField({ query })} onSearchChange={handleSearchChange}
onAll={() => navigate(voucherListUrl())} onFilterChange={filter => changeFilters(filter)}
onAll={resetFilters}
onTabChange={handleTabChange} onTabChange={handleTabChange}
onTabDelete={() => openModal("delete-search")} onTabDelete={() => openModal("delete-search")}
onTabSave={() => openModal("save-search")} onTabSave={() => openModal("save-search")}

View file

@ -1,24 +1,250 @@
import { VoucherFilterInput } from "@saleor/types/globalTypes"; import { IntlShape } from "react-intl";
import {
VoucherFilterInput,
DiscountStatusEnum,
DiscountValueTypeEnum,
VoucherDiscountType
} from "@saleor/types/globalTypes";
import { maybe, findValueInEnum, joinDateTime } from "@saleor/misc";
import { VoucherListFilterOpts } from "@saleor/discounts/types";
import { IFilter, IFilterElement } from "@saleor/components/Filter";
import {
createDateField,
createOptionsField,
createNumberField
} from "@saleor/utils/filters/fields";
import { import {
createFilterTabUtils, createFilterTabUtils,
createFilterUtils createFilterUtils,
dedupeFilter
} from "../../../utils/filters"; } from "../../../utils/filters";
import { import {
VoucherListUrlFilters, VoucherListUrlFilters,
VoucherListUrlFiltersEnum, VoucherListUrlFiltersEnum,
VoucherListUrlQueryParams VoucherListUrlQueryParams
} from "../../urls"; } from "../../urls";
import messages from "./messages";
export const VOUCHER_FILTERS_KEY = "VoucherFilters"; export const VOUCHER_FILTERS_KEY = "voucherFilters";
export enum VoucherFilterKeys {
saleType = "saleType",
started = "started",
status = "status",
timesUsed = "timesUsed"
}
export function getFilterOpts(
params: VoucherListUrlFilters
): VoucherListFilterOpts {
return {
saleType: {
active: !!maybe(() => params.type),
value: maybe(
() =>
dedupeFilter(
params.type.map(type => findValueInEnum(type, VoucherDiscountType))
),
[]
)
},
started: {
active: maybe(
() =>
[params.startedFrom, params.startedTo].some(
field => field !== undefined
),
false
),
value: {
max: maybe(() => params.startedTo, ""),
min: maybe(() => params.startedFrom, "")
}
},
status: {
active: !!maybe(() => params.status),
value: maybe(
() =>
dedupeFilter(
params.status.map(status =>
findValueInEnum(status, DiscountStatusEnum)
)
),
[]
)
},
timesUsed: {
active: maybe(
() =>
[params.timesUsedFrom, params.timesUsedTo].some(
field => field !== undefined
),
false
),
value: {
max: maybe(() => params.timesUsedTo, ""),
min: maybe(() => params.timesUsedFrom, "")
}
}
};
}
export function createFilterStructure(
intl: IntlShape,
opts: VoucherListFilterOpts
): IFilter<VoucherFilterKeys> {
return [
{
...createDateField(
VoucherFilterKeys.started,
intl.formatMessage(messages.started),
opts.started.value
),
active: opts.started.active
},
{
...createNumberField(
VoucherFilterKeys.timesUsed,
intl.formatMessage(messages.timesUsed),
opts.timesUsed.value
),
active: opts.timesUsed.active
},
{
...createOptionsField(
VoucherFilterKeys.status,
intl.formatMessage(messages.status),
opts.status.value,
true,
[
{
label: intl.formatMessage(messages.active),
value: DiscountStatusEnum.ACTIVE
},
{
label: intl.formatMessage(messages.expired),
value: DiscountStatusEnum.EXPIRED
},
{
label: intl.formatMessage(messages.scheduled),
value: DiscountStatusEnum.SCHEDULED
}
]
),
active: opts.status.active
},
{
...createOptionsField(
VoucherFilterKeys.saleType,
intl.formatMessage(messages.type),
opts.saleType.value,
false,
[
{
label: intl.formatMessage(messages.fixed),
value: DiscountValueTypeEnum.FIXED
},
{
label: intl.formatMessage(messages.percentage),
value: DiscountValueTypeEnum.PERCENTAGE
}
]
),
active: opts.saleType.active
}
];
}
export function getFilterVariables( export function getFilterVariables(
params: VoucherListUrlFilters params: VoucherListUrlFilters
): VoucherFilterInput { ): VoucherFilterInput {
return { return {
search: params.query discountType:
params.type &&
params.type.map(type => findValueInEnum(type, VoucherDiscountType)),
search: params.query,
started: {
gte: joinDateTime(params.startedFrom),
lte: joinDateTime(params.startedTo)
},
status:
params.status &&
params.status.map(status => findValueInEnum(status, DiscountStatusEnum)),
timesUsed: {
gte: parseInt(params.timesUsedFrom, 0),
lte: parseInt(params.timesUsedTo, 0)
}
}; };
} }
export function getFilterQueryParam(
filter: IFilterElement<VoucherFilterKeys>
): VoucherListUrlFilters {
const { active, multiple, name, value } = filter;
switch (name) {
case VoucherFilterKeys.saleType:
if (!active) {
return {
type: undefined
};
}
return {
type: value.map(type => findValueInEnum(type, VoucherDiscountType))
};
case VoucherFilterKeys.started:
if (!active) {
return {
startedFrom: undefined,
startedTo: undefined
};
}
if (multiple) {
return {
startedFrom: value[0],
startedTo: value[1]
};
}
return {
startedFrom: value[0],
startedTo: value[0]
};
case VoucherFilterKeys.timesUsed:
if (!active) {
return {
timesUsedFrom: undefined,
timesUsedTo: undefined
};
}
if (multiple) {
return {
timesUsedFrom: value[0],
timesUsedTo: value[1]
};
}
return {
timesUsedFrom: value[0],
timesUsedTo: value[0]
};
case VoucherFilterKeys.status:
if (!active) {
return {
status: undefined
};
}
return {
status: value.map(val => findValueInEnum(val, DiscountStatusEnum))
};
}
}
export const { export const {
deleteFilterTab, deleteFilterTab,
getFilterTabs, getFilterTabs,

View file

@ -0,0 +1,41 @@
import { defineMessages } from "react-intl";
const messages = defineMessages({
active: {
defaultMessage: "Active",
description: "voucher status"
},
expired: {
defaultMessage: "Expired",
description: "voucher status"
},
fixed: {
defaultMessage: "Fixed amount",
description: "discount type"
},
percentage: {
defaultMessage: "Percentage",
description: "discount type"
},
scheduled: {
defaultMessage: "Scheduled",
description: "voucher status"
},
started: {
defaultMessage: "Started",
description: "voucher start date"
},
status: {
defaultMessage: "Status",
description: "voucher status"
},
timesUsed: {
defaultMessage: "Times used",
description: "voucher"
},
type: {
defaultMessage: "Discount Type"
}
});
export default messages;

View file

@ -2,6 +2,10 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import { VoucherListUrlSortField } from "@saleor/discounts/urls"; import { VoucherListUrlSortField } from "@saleor/discounts/urls";
import {
VoucherDiscountType,
DiscountStatusEnum
} from "@saleor/types/globalTypes";
import VoucherListPage, { import VoucherListPage, {
VoucherListPageProps VoucherListPageProps
} from "../../../discounts/components/VoucherListPage"; } from "../../../discounts/components/VoucherListPage";
@ -11,7 +15,8 @@ import {
pageListProps, pageListProps,
searchPageProps, searchPageProps,
tabPageProps, tabPageProps,
sortPageProps sortPageProps,
filterPageProps
} from "../../../fixtures"; } from "../../../fixtures";
import Decorator from "../../Decorator"; import Decorator from "../../Decorator";
@ -21,7 +26,32 @@ const props: VoucherListPageProps = {
...searchPageProps, ...searchPageProps,
...sortPageProps, ...sortPageProps,
...tabPageProps, ...tabPageProps,
...filterPageProps,
defaultCurrency: "USD", defaultCurrency: "USD",
filterOpts: {
saleType: {
active: false,
value: [VoucherDiscountType.FIXED, VoucherDiscountType.PERCENTAGE]
},
started: {
active: false,
value: {
max: undefined,
min: undefined
}
},
status: {
active: false,
value: [DiscountStatusEnum.ACTIVE]
},
timesUsed: {
active: false,
value: {
max: undefined,
min: undefined
}
}
},
sort: { sort: {
...sortPageProps.sort, ...sortPageProps.sort,
sort: VoucherListUrlSortField.code sort: VoucherListUrlSortField.code