From 7820c65050839c80b906e03431f07f1a65368489 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Fri, 3 Jan 2020 16:17:51 +0100 Subject: [PATCH] Add filters to voucher list --- .../VoucherListPage/VoucherListPage.tsx | 23 +- src/discounts/types.ts | 10 +- src/discounts/urls.ts | 11 +- .../views/VoucherList/VoucherList.tsx | 43 +++- src/discounts/views/VoucherList/filter.ts | 234 +++++++++++++++++- src/discounts/views/VoucherList/messages.ts | 41 +++ .../stories/discounts/VoucherListPage.tsx | 32 ++- 7 files changed, 375 insertions(+), 19 deletions(-) create mode 100644 src/discounts/views/VoucherList/messages.ts diff --git a/src/discounts/components/VoucherListPage/VoucherListPage.tsx b/src/discounts/components/VoucherListPage/VoucherListPage.tsx index bf7cc6079..947e11ffa 100644 --- a/src/discounts/components/VoucherListPage/VoucherListPage.tsx +++ b/src/discounts/components/VoucherListPage/VoucherListPage.tsx @@ -5,23 +5,28 @@ import { FormattedMessage, useIntl } from "react-intl"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import SearchBar from "@saleor/components/SearchBar"; +import FilterBar from "@saleor/components/FilterBar"; import { sectionNames } from "@saleor/intl"; import { ListActions, PageListProps, - SearchPageProps, TabPageProps, - SortPage + SortPage, + FilterPageProps } from "@saleor/types"; 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 from "../VoucherList"; export interface VoucherListPageProps extends PageListProps, ListActions, - SearchPageProps, + FilterPageProps, SortPage, TabPageProps { defaultCurrency: string; @@ -29,10 +34,13 @@ export interface VoucherListPageProps } const VoucherListPage: React.FC = ({ + currencySymbol, currentTab, + filterOpts, initialSearch, onAdd, onAll, + onFilterChange, onSearchChange, onTabChange, onTabDelete, @@ -42,6 +50,8 @@ const VoucherListPage: React.FC = ({ }) => { const intl = useIntl(); + const structure = createFilterStructure(intl, filterOpts); + return ( @@ -53,18 +63,21 @@ const VoucherListPage: React.FC = ({ - ; } +export interface VoucherListFilterOpts { + saleType: FilterOpts; + started: FilterOpts; + status: FilterOpts; + timesUsed: FilterOpts; +} + export enum RequirementsPicker { ORDER = "ORDER", ITEM = "ITEM", diff --git a/src/discounts/urls.ts b/src/discounts/urls.ts index 7fe025069..186e9a722 100644 --- a/src/discounts/urls.ts +++ b/src/discounts/urls.ts @@ -67,9 +67,18 @@ export const saleAddUrl = saleAddPath; export const voucherSection = urlJoin(discountSection, "vouchers"); export const voucherListPath = voucherSection; export enum VoucherListUrlFiltersEnum { + startedFrom = "startedFrom", + startedTo = "startedTo", + timesUsedFrom = "timesUsedFrom", + timesUsedTo = "timesUsedTo", query = "query" } -export type VoucherListUrlFilters = Filters; +export enum VoucherListUrlFiltersWithMultipleValues { + status = "status", + type = "type" +} +export type VoucherListUrlFilters = Filters & + FiltersWithMultipleValues; export type VoucherListUrlDialog = "remove" | TabActionDialog; export enum VoucherListUrlSortField { code = "code", diff --git a/src/discounts/views/VoucherList/VoucherList.tsx b/src/discounts/views/VoucherList/VoucherList.tsx index 522a3ac74..f9931f941 100644 --- a/src/discounts/views/VoucherList/VoucherList.tsx +++ b/src/discounts/views/VoucherList/VoucherList.tsx @@ -24,6 +24,8 @@ import { ListViews } from "@saleor/types"; import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; 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 { TypedVoucherBulkDelete } from "../../mutations"; import { useVoucherListQuery } from "../../queries"; @@ -31,7 +33,6 @@ import { VoucherBulkDelete } from "../../types/VoucherBulkDelete"; import { voucherAddUrl, voucherListUrl, - VoucherListUrlFilters, VoucherListUrlQueryParams, voucherUrl, VoucherListUrlDialog @@ -42,7 +43,10 @@ import { getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab + saveFilterTab, + getFilterQueryParam, + VoucherFilterKeys, + getFilterOpts } from "./filter"; import { getSortQueryVariables } from "./sort"; @@ -86,17 +90,38 @@ export const VoucherList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilterField = (filter: VoucherListUrlFilters) => { + const changeFilters = (filter: IFilter) => { reset(); navigate( voucherListUrl({ - ...getActiveFilters(params), - ...filter, + ...params, + ...getFilterQueryParams(filter, getFilterQueryParam), 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< VoucherListUrlDialog, VoucherListUrlQueryParams @@ -143,6 +168,7 @@ export const VoucherList: React.FC = ({ params }) => { }; const handleSort = createSortHandler(navigate, voucherListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( @@ -158,10 +184,13 @@ export const VoucherList: React.FC = ({ params }) => { <> changeFilterField({ query })} - onAll={() => navigate(voucherListUrl())} + onSearchChange={handleSearchChange} + onFilterChange={filter => changeFilters(filter)} + onAll={resetFilters} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} onTabSave={() => openModal("save-search")} diff --git a/src/discounts/views/VoucherList/filter.ts b/src/discounts/views/VoucherList/filter.ts index 6f5d7ce57..acf149b80 100644 --- a/src/discounts/views/VoucherList/filter.ts +++ b/src/discounts/views/VoucherList/filter.ts @@ -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 { createFilterTabUtils, - createFilterUtils + createFilterUtils, + dedupeFilter } from "../../../utils/filters"; import { VoucherListUrlFilters, VoucherListUrlFiltersEnum, VoucherListUrlQueryParams } 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 { + 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( params: VoucherListUrlFilters ): VoucherFilterInput { 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 +): 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 { deleteFilterTab, getFilterTabs, diff --git a/src/discounts/views/VoucherList/messages.ts b/src/discounts/views/VoucherList/messages.ts new file mode 100644 index 000000000..df51e9142 --- /dev/null +++ b/src/discounts/views/VoucherList/messages.ts @@ -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; diff --git a/src/storybook/stories/discounts/VoucherListPage.tsx b/src/storybook/stories/discounts/VoucherListPage.tsx index ffa4dff49..89b232735 100644 --- a/src/storybook/stories/discounts/VoucherListPage.tsx +++ b/src/storybook/stories/discounts/VoucherListPage.tsx @@ -2,6 +2,10 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import { VoucherListUrlSortField } from "@saleor/discounts/urls"; +import { + VoucherDiscountType, + DiscountStatusEnum +} from "@saleor/types/globalTypes"; import VoucherListPage, { VoucherListPageProps } from "../../../discounts/components/VoucherListPage"; @@ -11,7 +15,8 @@ import { pageListProps, searchPageProps, tabPageProps, - sortPageProps + sortPageProps, + filterPageProps } from "../../../fixtures"; import Decorator from "../../Decorator"; @@ -21,7 +26,32 @@ const props: VoucherListPageProps = { ...searchPageProps, ...sortPageProps, ...tabPageProps, + ...filterPageProps, 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: { ...sortPageProps.sort, sort: VoucherListUrlSortField.code