Add filtering to sale list
This commit is contained in:
parent
890a27ae68
commit
f9d0c22531
8 changed files with 311 additions and 20 deletions
|
@ -3,6 +3,7 @@ import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
|
|||
|
||||
export enum FieldType {
|
||||
date,
|
||||
dateTime,
|
||||
number,
|
||||
price,
|
||||
options,
|
||||
|
|
|
@ -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 { SaleListUrlSortField } from "@saleor/discounts/urls";
|
||||
import {
|
||||
SaleFilterKeys,
|
||||
createFilterStructure
|
||||
} from "@saleor/discounts/views/SaleList/filter";
|
||||
import { SaleListFilterOpts } from "@saleor/discounts/types";
|
||||
import { SaleList_sales_edges_node } from "../../types/SaleList";
|
||||
import SaleList from "../SaleList";
|
||||
|
||||
export interface SaleListPageProps
|
||||
extends PageListProps,
|
||||
ListActions,
|
||||
SearchPageProps,
|
||||
FilterPageProps<SaleFilterKeys, SaleListFilterOpts>,
|
||||
SortPage<SaleListUrlSortField>,
|
||||
TabPageProps {
|
||||
defaultCurrency: string;
|
||||
|
@ -29,10 +34,13 @@ export interface SaleListPageProps
|
|||
}
|
||||
|
||||
const SaleListPage: React.FC<SaleListPageProps> = ({
|
||||
currencySymbol,
|
||||
currentTab,
|
||||
filterOpts,
|
||||
initialSearch,
|
||||
onAdd,
|
||||
onAll,
|
||||
onFilterChange,
|
||||
onSearchChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
|
@ -42,6 +50,8 @@ const SaleListPage: React.FC<SaleListPageProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const structure = createFilterStructure(intl, filterOpts);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.sales)}>
|
||||
|
@ -50,18 +60,21 @@ const SaleListPage: React.FC<SaleListPageProps> = ({
|
|||
</Button>
|
||||
</PageHeader>
|
||||
<Card>
|
||||
<SearchBar
|
||||
<FilterBar
|
||||
allTabLabel={intl.formatMessage({
|
||||
defaultMessage: "All Sales",
|
||||
description: "tab name"
|
||||
})}
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterStructure={structure}
|
||||
initialSearch={initialSearch}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
defaultMessage: "Search Sale"
|
||||
})}
|
||||
tabs={tabs}
|
||||
onAll={onAll}
|
||||
onFilterChange={onFilterChange}
|
||||
onSearchChange={onSearchChange}
|
||||
onTabChange={onTabChange}
|
||||
onTabDelete={onTabDelete}
|
||||
|
|
|
@ -1,3 +1,15 @@
|
|||
import { FilterOpts, MinMax } from "@saleor/types";
|
||||
import {
|
||||
DiscountStatusEnum,
|
||||
DiscountValueTypeEnum
|
||||
} from "@saleor/types/globalTypes";
|
||||
|
||||
export interface SaleListFilterOpts {
|
||||
saleType: FilterOpts<DiscountValueTypeEnum>;
|
||||
started: FilterOpts<MinMax>;
|
||||
status: FilterOpts<DiscountStatusEnum[]>;
|
||||
}
|
||||
|
||||
export enum RequirementsPicker {
|
||||
ORDER = "ORDER",
|
||||
ITEM = "ITEM",
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
Filters,
|
||||
Pagination,
|
||||
TabActionDialog,
|
||||
Sort
|
||||
Sort,
|
||||
FiltersWithMultipleValues
|
||||
} from "../types";
|
||||
import { SaleDetailsPageTab } from "./components/SaleDetailsPage";
|
||||
import { VoucherDetailsPageTab } from "./components/VoucherDetailsPage";
|
||||
|
@ -18,9 +19,16 @@ export const discountSection = "/discounts/";
|
|||
export const saleSection = urlJoin(discountSection, "sales");
|
||||
export const saleListPath = saleSection;
|
||||
export enum SaleListUrlFiltersEnum {
|
||||
type = "type",
|
||||
startedFrom = "startedFrom",
|
||||
startedTo = "startedTo",
|
||||
query = "query"
|
||||
}
|
||||
export type SaleListUrlFilters = Filters<SaleListUrlFiltersEnum>;
|
||||
export enum SaleListUrlFiltersWithMultipleValues {
|
||||
status = "status"
|
||||
}
|
||||
export type SaleListUrlFilters = Filters<SaleListUrlFiltersEnum> &
|
||||
FiltersWithMultipleValues<SaleListUrlFiltersWithMultipleValues>;
|
||||
export type SaleListUrlDialog = "remove" | TabActionDialog;
|
||||
export enum SaleListUrlSortField {
|
||||
name = "name",
|
||||
|
|
|
@ -24,13 +24,14 @@ 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 { IFilter } from "@saleor/components/Filter";
|
||||
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||
import SaleListPage from "../../components/SaleListPage";
|
||||
import { TypedSaleBulkDelete } from "../../mutations";
|
||||
import { useSaleListQuery } from "../../queries";
|
||||
import { SaleBulkDelete } from "../../types/SaleBulkDelete";
|
||||
import {
|
||||
saleAddUrl,
|
||||
SaleListUrlFilters,
|
||||
SaleListUrlQueryParams,
|
||||
saleUrl,
|
||||
saleListUrl,
|
||||
|
@ -42,7 +43,10 @@ import {
|
|||
getActiveFilters,
|
||||
getFilterTabs,
|
||||
getFilterVariables,
|
||||
saveFilterTab
|
||||
saveFilterTab,
|
||||
SaleFilterKeys,
|
||||
getFilterQueryParam,
|
||||
getFilterOpts
|
||||
} from "./filter";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
|
||||
|
@ -86,17 +90,38 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
: 0
|
||||
: parseInt(params.activeTab, 0);
|
||||
|
||||
const changeFilterField = (filter: SaleListUrlFilters) => {
|
||||
const changeFilters = (filter: IFilter<SaleFilterKeys>) => {
|
||||
reset();
|
||||
navigate(
|
||||
saleListUrl({
|
||||
...getActiveFilters(params),
|
||||
...filter,
|
||||
...params,
|
||||
...getFilterQueryParams(filter, getFilterQueryParam),
|
||||
activeTab: undefined
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
reset();
|
||||
navigate(
|
||||
saleListUrl({
|
||||
asc: params.asc,
|
||||
sort: params.sort
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSearchChange = (query: string) => {
|
||||
reset();
|
||||
navigate(
|
||||
saleListUrl({
|
||||
...params,
|
||||
activeTab: undefined,
|
||||
query
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
SaleListUrlDialog,
|
||||
SaleListUrlQueryParams
|
||||
|
@ -143,6 +168,7 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
};
|
||||
|
||||
const handleSort = createSortHandler(navigate, saleListUrl, params);
|
||||
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||
|
||||
return (
|
||||
<TypedSaleBulkDelete onCompleted={handleSaleBulkDelete}>
|
||||
|
@ -158,10 +184,13 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
<>
|
||||
<WindowTitle title={intl.formatMessage(sectionNames.sales)} />
|
||||
<SaleListPage
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={query => changeFilterField({ query })}
|
||||
onAll={() => navigate(saleListUrl())}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={filter => changeFilters(filter)}
|
||||
onAll={resetFilters}
|
||||
onTabChange={handleTabChange}
|
||||
onTabDelete={() => openModal("delete-search")}
|
||||
onTabSave={() => openModal("save-search")}
|
||||
|
|
|
@ -1,24 +1,194 @@
|
|||
import { SaleFilterInput } from "@saleor/types/globalTypes";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
import {
|
||||
SaleFilterInput,
|
||||
DiscountStatusEnum,
|
||||
DiscountValueTypeEnum
|
||||
} from "@saleor/types/globalTypes";
|
||||
import { maybe, findValueInEnum, joinDateTime } from "@saleor/misc";
|
||||
import { SaleListFilterOpts } from "@saleor/discounts/types";
|
||||
import { IFilter, IFilterElement } from "@saleor/components/Filter";
|
||||
import {
|
||||
createDateField,
|
||||
createOptionsField
|
||||
} from "@saleor/utils/filters/fields";
|
||||
import {
|
||||
createFilterTabUtils,
|
||||
createFilterUtils
|
||||
createFilterUtils,
|
||||
dedupeFilter
|
||||
} from "../../../utils/filters";
|
||||
import {
|
||||
SaleListUrlFilters,
|
||||
SaleListUrlFiltersEnum,
|
||||
SaleListUrlQueryParams
|
||||
} from "../../urls";
|
||||
import messages from "./messages";
|
||||
|
||||
export const SALE_FILTERS_KEY = "saleFilters";
|
||||
|
||||
export enum SaleFilterKeys {
|
||||
saleType = "saleType",
|
||||
started = "started",
|
||||
status = "status"
|
||||
}
|
||||
|
||||
export function getFilterOpts(params: SaleListUrlFilters): SaleListFilterOpts {
|
||||
return {
|
||||
saleType: {
|
||||
active: !!maybe(() => params.type),
|
||||
value: maybe(() => findValueInEnum(params.type, DiscountValueTypeEnum))
|
||||
},
|
||||
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)
|
||||
)
|
||||
),
|
||||
[]
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createFilterStructure(
|
||||
intl: IntlShape,
|
||||
opts: SaleListFilterOpts
|
||||
): IFilter<SaleFilterKeys> {
|
||||
return [
|
||||
{
|
||||
...createDateField(
|
||||
SaleFilterKeys.started,
|
||||
intl.formatMessage(messages.started),
|
||||
opts.started.value
|
||||
),
|
||||
active: opts.started.active
|
||||
},
|
||||
{
|
||||
...createOptionsField(
|
||||
SaleFilterKeys.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(
|
||||
SaleFilterKeys.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: SaleListUrlFilters
|
||||
): SaleFilterInput {
|
||||
return {
|
||||
search: params.query
|
||||
saleType:
|
||||
params.type && findValueInEnum(params.type, DiscountValueTypeEnum),
|
||||
search: params.query,
|
||||
started: {
|
||||
gte: joinDateTime(params.startedFrom),
|
||||
lte: joinDateTime(params.startedTo)
|
||||
},
|
||||
status:
|
||||
params.status &&
|
||||
params.status.map(status => findValueInEnum(status, DiscountStatusEnum))
|
||||
};
|
||||
}
|
||||
|
||||
export function getFilterQueryParam(
|
||||
filter: IFilterElement<SaleFilterKeys>
|
||||
): SaleListUrlFilters {
|
||||
const { active, multiple, name, value } = filter;
|
||||
|
||||
switch (name) {
|
||||
case SaleFilterKeys.saleType:
|
||||
if (!active) {
|
||||
return {
|
||||
type: undefined
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: findValueInEnum(value[0], DiscountValueTypeEnum)
|
||||
};
|
||||
|
||||
case SaleFilterKeys.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 SaleFilterKeys.status:
|
||||
if (!active) {
|
||||
return {
|
||||
status: undefined
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: value.map(val => findValueInEnum(val, DiscountStatusEnum))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const {
|
||||
deleteFilterTab,
|
||||
getFilterTabs,
|
||||
|
|
37
src/discounts/views/SaleList/messages.ts
Normal file
37
src/discounts/views/SaleList/messages.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
const messages = defineMessages({
|
||||
active: {
|
||||
defaultMessage: "Active",
|
||||
description: "sale status"
|
||||
},
|
||||
expired: {
|
||||
defaultMessage: "Expired",
|
||||
description: "sale status"
|
||||
},
|
||||
fixed: {
|
||||
defaultMessage: "Fixed amount",
|
||||
description: "discount type"
|
||||
},
|
||||
percentage: {
|
||||
defaultMessage: "Percentage",
|
||||
description: "discount type"
|
||||
},
|
||||
scheduled: {
|
||||
defaultMessage: "Scheduled",
|
||||
description: "sale status"
|
||||
},
|
||||
started: {
|
||||
defaultMessage: "Started",
|
||||
description: "sale start date"
|
||||
},
|
||||
status: {
|
||||
defaultMessage: "Status",
|
||||
description: "sale status"
|
||||
},
|
||||
type: {
|
||||
defaultMessage: "Discount Type"
|
||||
}
|
||||
});
|
||||
|
||||
export default messages;
|
|
@ -2,6 +2,10 @@ import { storiesOf } from "@storybook/react";
|
|||
import React from "react";
|
||||
|
||||
import { SaleListUrlSortField } from "@saleor/discounts/urls";
|
||||
import {
|
||||
DiscountValueTypeEnum,
|
||||
DiscountStatusEnum
|
||||
} from "@saleor/types/globalTypes";
|
||||
import SaleListPage, {
|
||||
SaleListPageProps
|
||||
} from "../../../discounts/components/SaleListPage";
|
||||
|
@ -9,19 +13,36 @@ import { saleList } from "../../../discounts/fixtures";
|
|||
import {
|
||||
listActionsProps,
|
||||
pageListProps,
|
||||
searchPageProps,
|
||||
tabPageProps,
|
||||
sortPageProps
|
||||
sortPageProps,
|
||||
filterPageProps
|
||||
} from "../../../fixtures";
|
||||
import Decorator from "../../Decorator";
|
||||
|
||||
const props: SaleListPageProps = {
|
||||
...listActionsProps,
|
||||
...pageListProps.default,
|
||||
...searchPageProps,
|
||||
...filterPageProps,
|
||||
...sortPageProps,
|
||||
...tabPageProps,
|
||||
defaultCurrency: "USD",
|
||||
filterOpts: {
|
||||
saleType: {
|
||||
active: false,
|
||||
value: DiscountValueTypeEnum.FIXED
|
||||
},
|
||||
started: {
|
||||
active: false,
|
||||
value: {
|
||||
max: undefined,
|
||||
min: undefined
|
||||
}
|
||||
},
|
||||
status: {
|
||||
active: false,
|
||||
value: [DiscountStatusEnum.ACTIVE]
|
||||
}
|
||||
},
|
||||
sales: saleList,
|
||||
sort: {
|
||||
...sortPageProps.sort,
|
||||
|
|
Loading…
Reference in a new issue