Refacto filters to handle multiple values

This commit is contained in:
dominik-zeglen 2019-09-06 14:58:44 +02:00
parent 6aadc05322
commit 3917e13613
21 changed files with 581 additions and 144 deletions

View file

@ -19,9 +19,9 @@ import { FilterContent } from ".";
import { FilterContentSubmitData } from "./FilterContent"; import { FilterContentSubmitData } from "./FilterContent";
import { IFilter } from "./types"; import { IFilter } from "./types";
export interface FilterProps { export interface FilterProps<TFilterKeys = string> {
currencySymbol: string; currencySymbol: string;
menu: IFilter; menu: IFilter<TFilterKeys>;
filterLabel: string; filterLabel: string;
onFilterAdd: (filter: FilterContentSubmitData) => void; onFilterAdd: (filter: FilterContentSubmitData) => void;
} }

View file

@ -10,13 +10,13 @@ import SingleSelectField from "../SingleSelectField";
import FilterElement from "./FilterElement"; import FilterElement from "./FilterElement";
import { IFilter } from "./types"; import { IFilter } from "./types";
export interface FilterContentSubmitData { export interface FilterContentSubmitData<TKeys = string> {
name: string; name: TKeys;
value: string | string[]; value: string | string[];
} }
export interface FilterContentProps { export interface FilterContentProps {
currencySymbol: string; currencySymbol: string;
filters: IFilter; filters: IFilter<string>;
onSubmit: (data: FilterContentSubmitData) => void; onSubmit: (data: FilterContentSubmitData) => void;
} }
@ -27,10 +27,10 @@ function checkFilterValue(value: string | string[]): boolean {
return value.some(v => !!v); return value.some(v => !!v);
} }
function getFilterChoices(items: IFilter) { function getFilterChoices(items: IFilter<string>) {
return items.map(filterItem => ({ return items.map(filterItem => ({
label: filterItem.label, label: filterItem.label,
value: filterItem.value value: filterItem.value.toString()
})); }));
} }
@ -46,7 +46,7 @@ const FilterContent: React.FC<FilterContentProps> = ({
onSubmit onSubmit
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const [menuValue, setMenuValue] = React.useState<string>(""); const [menuValue, setMenuValue] = React.useState<string>(null);
const [filterValue, setFilterValue] = React.useState<string | string[]>(""); const [filterValue, setFilterValue] = React.useState<string | string[]>("");
const classes = useStyles({}); const classes = useStyles({});
@ -95,7 +95,7 @@ const FilterContent: React.FC<FilterContentProps> = ({
}} }}
value={ value={
filterItemIndex === menus.length - 1 filterItemIndex === menus.length - 1
? menuValue ? menuValue.toString()
: menus[filterItemIndex - 1].label.toString() : menus[filterItemIndex - 1].label.toString()
} }
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({

View file

@ -10,9 +10,9 @@ import PriceField from "../PriceField";
import SingleSelectField from "../SingleSelectField"; import SingleSelectField from "../SingleSelectField";
import { FieldType, IFilterItem } from "./types"; import { FieldType, IFilterItem } from "./types";
export interface FilterElementProps { export interface FilterElementProps<TFilterKeys = string> {
className?: string; className?: string;
filter: IFilterItem; filter: IFilterItem<TFilterKeys>;
value: string | string[]; value: string | string[];
onChange: (value: string | string[]) => void; onChange: (value: string | string[]) => void;
} }
@ -26,10 +26,10 @@ const useStyles = makeStyles({
} }
}); });
export interface FilterElementProps { export interface FilterElementProps<TFilterKeys = string> {
className?: string; className?: string;
currencySymbol: string; currencySymbol: string;
filter: IFilterItem; filter: IFilterItem<TFilterKeys>;
value: string | string[]; value: string | string[];
onChange: (value: string | string[]) => void; onChange: (value: string | string[]) => void;
} }

View file

@ -25,6 +25,6 @@ export interface FilterData {
value?: string; value?: string;
} }
export type IFilterItem = IMenuItem<FilterData>; export type IFilterItem<TKeys> = IMenuItem<FilterData, TKeys>;
export type IFilter = IMenu<FilterData>; export type IFilter<TKeys> = IMenu<FilterData, TKeys>;

View file

@ -6,9 +6,9 @@ import Debounce from "../Debounce";
import { IFilter } from "../Filter/types"; import { IFilter } from "../Filter/types";
import FilterTabs, { FilterChips, FilterTab } from "../TableFilter"; import FilterTabs, { FilterChips, FilterTab } from "../TableFilter";
export interface FilterBarProps<TUrlFilters = object> export interface FilterBarProps<TUrlFilters = object, TFilterKeys = any>
extends FilterProps<TUrlFilters> { extends FilterProps<TUrlFilters, TFilterKeys> {
filterMenu: IFilter; filterMenu: IFilter<TFilterKeys>;
} }
const FilterBar: React.FC<FilterBarProps> = ({ const FilterBar: React.FC<FilterBarProps> = ({

View file

@ -99,16 +99,16 @@ const useStyles = makeStyles(
} }
); );
interface FilterChipProps { interface FilterChipProps<TFilterKeys = string> {
currencySymbol: string; currencySymbol: string;
menu: IFilter; menu: IFilter<TFilterKeys>;
filtersList: Filter[]; filtersList: Filter[];
filterLabel: string; filterLabel: string;
placeholder: string; placeholder: string;
search: string; search: string;
isCustomSearch: boolean; isCustomSearch: boolean;
onSearchChange: (event: React.ChangeEvent<any>) => void; onSearchChange: (event: React.ChangeEvent<any>) => void;
onFilterAdd: (filter: FilterContentSubmitData) => void; onFilterAdd: (filter: FilterContentSubmitData<TFilterKeys>) => void;
onFilterDelete: () => void; onFilterDelete: () => void;
onFilterSave: () => void; onFilterSave: () => void;
} }

View file

@ -46,7 +46,7 @@ export const countries = [
{ code: "AS", label: "American Samoa" } { code: "AS", label: "American Samoa" }
]; ];
export const filterPageProps: FilterPageProps<{}> = { export const filterPageProps: FilterPageProps<{}, unknown> = {
currencySymbol: "USD", currencySymbol: "USD",
currentTab: 0, currentTab: 0,
filterTabs: [ filterTabs: [

View file

@ -10,17 +10,16 @@ import { FilterProps } from "../../../types";
import { OrderStatusFilter } from "../../../types/globalTypes"; import { OrderStatusFilter } from "../../../types/globalTypes";
import { OrderListUrlFilters } from "../../urls"; import { OrderListUrlFilters } from "../../urls";
type OrderListFilterProps = FilterProps<OrderListUrlFilters>; type OrderListFilterProps = FilterProps<OrderListUrlFilters, OrderFilterKeys>;
export enum OrderFilterKeys { export enum OrderFilterKeys {
date, date = "date",
dateEqual, dateEqual = "dateEqual",
dateRange, dateRange = "dateRange",
dateLastWeek, dateLastWeek = "dateLastWeek",
dateLastMonth, dateLastMonth = "dateLastMonth",
dateLastYear, dateLastYear = "dateLastYear",
email, fulfillment = "fulfillment"
fulfillment
} }
const OrderListFilter: React.FC<OrderListFilterProps> = props => { const OrderListFilter: React.FC<OrderListFilterProps> = props => {
@ -28,7 +27,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
const tz = React.useContext(TimezoneContext); const tz = React.useContext(TimezoneContext);
const intl = useIntl(); const intl = useIntl();
const filterMenu: IFilter = [ const filterMenu: IFilter<OrderFilterKeys> = [
{ {
children: [ children: [
{ {
@ -44,7 +43,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({ label: intl.formatMessage({
defaultMessage: "Last 7 Days" defaultMessage: "Last 7 Days"
}), }),
value: OrderFilterKeys.dateLastWeek.toString() value: OrderFilterKeys.dateLastWeek
}, },
{ {
children: [], children: [],
@ -59,7 +58,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({ label: intl.formatMessage({
defaultMessage: "Last 30 Days" defaultMessage: "Last 30 Days"
}), }),
value: OrderFilterKeys.dateLastMonth.toString() value: OrderFilterKeys.dateLastMonth
}, },
{ {
children: [], children: [],
@ -74,7 +73,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({ label: intl.formatMessage({
defaultMessage: "Last Year" defaultMessage: "Last Year"
}), }),
value: OrderFilterKeys.dateLastYear.toString() value: OrderFilterKeys.dateLastYear
}, },
{ {
children: [], children: [],
@ -88,7 +87,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({ label: intl.formatMessage({
defaultMessage: "Specific Date" defaultMessage: "Specific Date"
}), }),
value: OrderFilterKeys.dateEqual.toString() value: OrderFilterKeys.dateEqual
}, },
{ {
children: [], children: [],
@ -101,7 +100,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({ label: intl.formatMessage({
defaultMessage: "Range" defaultMessage: "Range"
}), }),
value: OrderFilterKeys.dateRange.toString() value: OrderFilterKeys.dateRange
} }
], ],
data: { data: {
@ -113,7 +112,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({ label: intl.formatMessage({
defaultMessage: "Date" defaultMessage: "Date"
}), }),
value: OrderFilterKeys.date.toString() value: OrderFilterKeys.date
}, },
{ {
children: [], children: [],
@ -155,7 +154,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
defaultMessage: "Fulfillment Status", defaultMessage: "Fulfillment Status",
description: "order" description: "order"
}), }),
value: OrderFilterKeys.fulfillment.toString() value: OrderFilterKeys.fulfillment
} }
]; ];

View file

@ -7,6 +7,7 @@ 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 { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { OrderFilterKeys } from "@saleor/orders/components/OrderListFilter";
import { FilterPageProps, ListActions, PageListProps } from "@saleor/types"; import { FilterPageProps, ListActions, PageListProps } from "@saleor/types";
import { OrderList_orders_edges_node } from "../../types/OrderList"; import { OrderList_orders_edges_node } from "../../types/OrderList";
import { OrderListUrlFilters } from "../../urls"; import { OrderListUrlFilters } from "../../urls";
@ -16,7 +17,7 @@ import OrderListFilter from "../OrderListFilter";
export interface OrderListPageProps export interface OrderListPageProps
extends PageListProps, extends PageListProps,
ListActions, ListActions,
FilterPageProps<OrderListUrlFilters> { FilterPageProps<OrderListUrlFilters, OrderFilterKeys> {
orders: OrderList_orders_edges_node[]; orders: OrderList_orders_edges_node[];
} }

View file

@ -0,0 +1,109 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Crate filter chips 1`] = `
Array [
Object {
"label": "Date from 2019-09-01",
"onClick": [Function],
},
Object {
"label": "Date to 2019-09-10",
"onClick": [Function],
},
Object {
"label": "Fulfilled",
"onClick": [Function],
},
Object {
"label": "Partially Fulfilled",
"onClick": [Function],
},
]
`;
exports[`Create filter object with date 1`] = `
Object {
"dateFrom": "2019-09-01",
"dateTo": "2019-09-01",
}
`;
exports[`Create filter object with date last month 1`] = `
Object {
"dateFrom": "2019-09-01",
"dateTo": undefined,
}
`;
exports[`Create filter object with date last week 1`] = `
Object {
"dateFrom": "2019-09-01",
"dateTo": undefined,
}
`;
exports[`Create filter object with date last year 1`] = `
Object {
"dateFrom": "2019-09-01",
"dateTo": undefined,
}
`;
exports[`Create filter object with date range 1`] = `
Object {
"dateFrom": "2019-09-01",
"dateTo": "2019-09-10",
}
`;
exports[`Create filter object with fulfillment status 1`] = `
Object {
"status": Array [
"PARTIALLY_FULFILLED",
],
}
`;
exports[`Create filter object with multiple deduped values 1`] = `
Object {
"status": Array [
"FULFILLED",
],
}
`;
exports[`Create filter object with multiple values 1`] = `
Object {
"status": Array [
"FULFILLED",
"PARTIALLY_FULFILLED",
],
}
`;
exports[`Get filter variables from multiple status value 1`] = `
Object {
"created": Object {
"gte": "2019-09-01",
"lte": "2019-09-10",
},
"customer": "email@example.com",
"status": Array [
"FULFILLED",
"PARTIALLY_FULFILLED",
],
}
`;
exports[`Get filter variables from single status value 1`] = `
Object {
"created": Object {
"gte": "2019-09-01",
"lte": "2019-09-10",
},
"customer": "email@example.com",
"status": Array [
"FULFILLED",
],
}
`;

View file

@ -0,0 +1,155 @@
import { createIntl } from "react-intl";
import { OrderFilterKeys } from "@saleor/orders/components/OrderListFilter";
import { OrderStatus, OrderStatusFilter } from "@saleor/types/globalTypes";
import { createFilter, createFilterChips, getFilterVariables } from "./filters";
const mockIntl = createIntl({
locale: "en"
});
describe("Create filter object", () => {
it("with date", () => {
const filter = createFilter(
{},
{
name: OrderFilterKeys.dateEqual,
value: "2019-09-01"
}
);
expect(filter).toMatchSnapshot();
});
it("with date range", () => {
const filter = createFilter(
{},
{
name: OrderFilterKeys.dateRange,
value: ["2019-09-01", "2019-09-10"]
}
);
expect(filter).toMatchSnapshot();
});
it("with date last week", () => {
const filter = createFilter(
{},
{
name: OrderFilterKeys.dateLastWeek,
value: "2019-09-01"
}
);
expect(filter).toMatchSnapshot();
});
it("with date last month", () => {
const filter = createFilter(
{},
{
name: OrderFilterKeys.dateLastMonth,
value: "2019-09-01"
}
);
expect(filter).toMatchSnapshot();
});
it("with date last year", () => {
const filter = createFilter(
{},
{
name: OrderFilterKeys.dateLastYear,
value: "2019-09-01"
}
);
expect(filter).toMatchSnapshot();
});
it("with fulfillment status", () => {
const filter = createFilter(
{},
{
name: OrderFilterKeys.fulfillment,
value: OrderStatusFilter.PARTIALLY_FULFILLED
}
);
expect(filter).toMatchSnapshot();
});
it("with multiple values", () => {
const filter = createFilter(
{
status: [OrderStatusFilter.FULFILLED]
},
{
name: OrderFilterKeys.fulfillment,
value: OrderStatusFilter.PARTIALLY_FULFILLED
}
);
expect(filter).toMatchSnapshot();
});
it("with multiple deduped values", () => {
const filter = createFilter(
{
status: [OrderStatusFilter.FULFILLED]
},
{
name: OrderFilterKeys.fulfillment,
value: OrderStatusFilter.FULFILLED
}
);
expect(filter).toMatchSnapshot();
});
});
test("Crate filter chips", () => {
const chips = createFilterChips(
{
dateFrom: "2019-09-01",
dateTo: "2019-09-10",
status: [OrderStatus.FULFILLED, OrderStatus.PARTIALLY_FULFILLED]
},
{
formatDate: date => date
},
jest.fn(),
mockIntl as any
);
expect(chips).toMatchSnapshot();
});
describe("Get filter variables", () => {
it("from single status value", () => {
const filter = getFilterVariables({
dateFrom: "2019-09-01",
dateTo: "2019-09-10",
email: "email@example.com",
status: OrderStatus.FULFILLED.toString()
});
expect(filter).toMatchSnapshot();
});
it("from multiple status value", () => {
const filter = getFilterVariables({
dateFrom: "2019-09-01",
dateTo: "2019-09-10",
email: "email@example.com",
status: [
OrderStatus.FULFILLED.toString(),
OrderStatus.PARTIALLY_FULFILLED.toString()
]
});
expect(filter).toMatchSnapshot();
});
});

View file

@ -86,15 +86,15 @@ export function getFilterVariables(
export function createFilter( export function createFilter(
filter: OrderListUrlFilters, filter: OrderListUrlFilters,
data: FilterContentSubmitData data: FilterContentSubmitData<OrderFilterKeys>
): OrderListUrlFilters { ): OrderListUrlFilters {
const { name: filterName, value } = data; const { name: filterName, value } = data;
if (filterName === OrderFilterKeys.dateEqual.toString()) { if (filterName === OrderFilterKeys.dateEqual) {
return { return {
dateFrom: valueOrFirst(value), dateFrom: valueOrFirst(value),
dateTo: valueOrFirst(value) dateTo: valueOrFirst(value)
}; };
} else if (filterName === OrderFilterKeys.dateRange.toString()) { } else if (filterName === OrderFilterKeys.dateRange) {
return { return {
dateFrom: value[0], dateFrom: value[0],
dateTo: value[1] dateTo: value[1]
@ -104,15 +104,13 @@ export function createFilter(
OrderFilterKeys.dateLastWeek, OrderFilterKeys.dateLastWeek,
OrderFilterKeys.dateLastMonth, OrderFilterKeys.dateLastMonth,
OrderFilterKeys.dateLastYear OrderFilterKeys.dateLastYear
] ].includes(filterName)
.map(value => value.toString())
.includes(filterName)
) { ) {
return { return {
dateFrom: valueOrFirst(value), dateFrom: valueOrFirst(value),
dateTo: undefined dateTo: undefined
}; };
} else if (filterName === OrderFilterKeys.fulfillment.toString()) { } else if (filterName === OrderFilterKeys.fulfillment) {
return { return {
status: dedupeFilter( status: dedupeFilter(
filter.status filter.status

View file

@ -7,21 +7,23 @@ import { FilterProps } from "@saleor/types";
import { StockAvailability } from "@saleor/types/globalTypes"; import { StockAvailability } from "@saleor/types/globalTypes";
import { ProductListUrlFilters } from "../../urls"; import { ProductListUrlFilters } from "../../urls";
type ProductListFilterProps = FilterProps<ProductListUrlFilters>; type ProductListFilterProps = FilterProps<
ProductListUrlFilters,
ProductFilterKeys
>;
export enum ProductFilterKeys { export enum ProductFilterKeys {
published, published = "published",
price, price = "price",
priceEqual, priceEqual = "priceEqual",
priceRange, priceRange = "priceRange",
stock, stock = "stock"
query
} }
const ProductListFilter: React.FC<ProductListFilterProps> = props => { const ProductListFilter: React.FC<ProductListFilterProps> = props => {
const intl = useIntl(); const intl = useIntl();
const filterMenu: IFilter = [ const filterMenu: IFilter<ProductFilterKeys> = [
{ {
children: [], children: [],
data: { data: {
@ -55,7 +57,7 @@ const ProductListFilter: React.FC<ProductListFilterProps> = props => {
defaultMessage: "Visibility", defaultMessage: "Visibility",
description: "product visibility" description: "product visibility"
}), }),
value: ProductFilterKeys.published.toString() value: ProductFilterKeys.published
}, },
{ {
children: [], children: [],
@ -85,7 +87,7 @@ const ProductListFilter: React.FC<ProductListFilterProps> = props => {
defaultMessage: "Stock", defaultMessage: "Stock",
description: "product stock" description: "product stock"
}), }),
value: ProductFilterKeys.stock.toString() value: ProductFilterKeys.stock
}, },
{ {
children: [ children: [
@ -102,7 +104,7 @@ const ProductListFilter: React.FC<ProductListFilterProps> = props => {
label: intl.formatMessage({ label: intl.formatMessage({
defaultMessage: "Specific Price" defaultMessage: "Specific Price"
}), }),
value: ProductFilterKeys.priceEqual.toString() value: ProductFilterKeys.priceEqual
}, },
{ {
children: [], children: [],
@ -115,7 +117,7 @@ const ProductListFilter: React.FC<ProductListFilterProps> = props => {
label: intl.formatMessage({ label: intl.formatMessage({
defaultMessage: "Range" defaultMessage: "Range"
}), }),
value: ProductFilterKeys.priceRange.toString() value: ProductFilterKeys.priceRange
} }
], ],
data: { data: {
@ -127,7 +129,7 @@ const ProductListFilter: React.FC<ProductListFilterProps> = props => {
label: intl.formatMessage({ label: intl.formatMessage({
defaultMessage: "Price" defaultMessage: "Price"
}), }),
value: ProductFilterKeys.price.toString() value: ProductFilterKeys.price
} }
]; ];

View file

@ -26,12 +26,12 @@ import {
} from "@saleor/types"; } from "@saleor/types";
import { ProductListUrlFilters } from "../../urls"; import { ProductListUrlFilters } from "../../urls";
import ProductList from "../ProductList"; import ProductList from "../ProductList";
import ProductListFilter from "../ProductListFilter"; import ProductListFilter, { ProductFilterKeys } from "../ProductListFilter";
export interface ProductListPageProps export interface ProductListPageProps
extends PageListProps<ProductListColumns>, extends PageListProps<ProductListColumns>,
ListActions, ListActions,
FilterPageProps<ProductListUrlFilters>, FilterPageProps<ProductListUrlFilters, ProductFilterKeys>,
FetchMoreProps { FetchMoreProps {
availableInGridAttributes: AvailableInGridAttributes_availableInGrid_edges_node[]; availableInGridAttributes: AvailableInGridAttributes_availableInGrid_edges_node[];
currencySymbol: string; currencySymbol: string;

View file

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Crate filter chips 1`] = `
Array [
Object {
"label": "Price from $10.00",
"onClick": [Function],
},
Object {
"label": "Price to $20.00",
"onClick": [Function],
},
Object {
"label": "Available",
"onClick": [Function],
},
Object {
"label": "Published",
"onClick": [Function],
},
]
`;
exports[`Create filter object with price 1`] = `
Object {
"priceFrom": "10",
"priceTo": "10",
}
`;
exports[`Create filter object with price range 1`] = `
Object {
"priceFrom": Array [
"10",
"20",
],
"priceTo": Array [
"10",
"20",
],
}
`;
exports[`Create filter object with publication status 1`] = `
Object {
"isPublished": "false",
}
`;
exports[`Create filter object with stock status 1`] = `
Object {
"status": "OUT_OF_STOCK",
}
`;
exports[`Get filter variables 1`] = `
Object {
"isPublished": true,
"price": Object {
"gte": 10,
"lte": 20,
},
"search": undefined,
"stockAvailability": "IN_STOCK",
}
`;

View file

@ -0,0 +1,77 @@
import { createIntl } from "react-intl";
import { ProductFilterKeys } from "@saleor/products/components/ProductListFilter";
import { StockAvailability } from "@saleor/types/globalTypes";
import { createFilter, createFilterChips, getFilterVariables } from "./filters";
const mockIntl = createIntl({
locale: "en"
});
describe("Create filter object", () => {
it("with price", () => {
const filter = createFilter({
name: ProductFilterKeys.priceEqual,
value: "10"
});
expect(filter).toMatchSnapshot();
});
it("with price range", () => {
const filter = createFilter({
name: ProductFilterKeys.priceEqual,
value: ["10", "20"]
});
expect(filter).toMatchSnapshot();
});
it("with publication status", () => {
const filter = createFilter({
name: ProductFilterKeys.published,
value: "false"
});
expect(filter).toMatchSnapshot();
});
it("with stock status", () => {
const filter = createFilter({
name: ProductFilterKeys.stock,
value: StockAvailability.OUT_OF_STOCK
});
expect(filter).toMatchSnapshot();
});
});
test("Crate filter chips", () => {
const chips = createFilterChips(
{
isPublished: "true",
priceFrom: "10",
priceTo: "20",
status: StockAvailability.IN_STOCK
},
{
currencySymbol: "USD",
locale: "en"
},
jest.fn(),
mockIntl as any
);
expect(chips).toMatchSnapshot();
});
test("Get filter variables", () => {
const filter = getFilterVariables({
isPublished: "true",
priceFrom: "10",
priceTo: "20",
status: StockAvailability.IN_STOCK
});
expect(filter).toMatchSnapshot();
});

View file

@ -34,26 +34,26 @@ export function getFilterVariables(
} }
export function createFilter( export function createFilter(
filter: FilterContentSubmitData filter: FilterContentSubmitData<ProductFilterKeys>
): ProductListUrlFilters { ): ProductListUrlFilters {
const filterName = filter.name; const filterName = filter.name;
if (filterName === ProductFilterKeys.priceEqual.toString()) { if (filterName === ProductFilterKeys.priceEqual) {
const value = filter.value as string; const value = filter.value as string;
return { return {
priceFrom: value, priceFrom: value,
priceTo: value priceTo: value
}; };
} else if (filterName === ProductFilterKeys.priceRange.toString()) { } else if (filterName === ProductFilterKeys.priceRange) {
const { value } = filter; const { value } = filter;
return { return {
priceFrom: value[0], priceFrom: value[0],
priceTo: value[1] priceTo: value[1]
}; };
} else if (filterName === ProductFilterKeys.published.toString()) { } else if (filterName === ProductFilterKeys.published) {
return { return {
isPublished: filter.value as string isPublished: filter.value as string
}; };
} else if (filterName === ProductFilterKeys.stock.toString()) { } else if (filterName === ProductFilterKeys.stock) {
const value = filter.value as string; const value = filter.value as string;
return { return {
status: StockAvailability[value] status: StockAvailability[value]

View file

@ -65,7 +65,7 @@ export interface PageListProps<TColumns extends string = string>
defaultSettings?: ListSettings<TColumns>; defaultSettings?: ListSettings<TColumns>;
onAdd: () => void; onAdd: () => void;
} }
export interface FilterPageProps<TUrlFilters> { export interface FilterPageProps<TUrlFilters, TFilterKeys> {
currencySymbol: string; currencySymbol: string;
currentTab: number; currentTab: number;
filterTabs: GetFilterTabsOutput<TUrlFilters>; filterTabs: GetFilterTabsOutput<TUrlFilters>;
@ -73,12 +73,13 @@ export interface FilterPageProps<TUrlFilters> {
initialSearch: string; initialSearch: string;
onAll: () => void; onAll: () => void;
onSearchChange: (value: string) => void; onSearchChange: (value: string) => void;
onFilterAdd: (filter: FilterContentSubmitData) => void; onFilterAdd: (filter: FilterContentSubmitData<TFilterKeys>) => void;
onFilterDelete: () => void; onFilterDelete: () => void;
onFilterSave: () => void; onFilterSave: () => void;
onTabChange: (tab: number) => void; onTabChange: (tab: number) => void;
} }
export interface FilterProps<TUrlFilters> extends FilterPageProps<TUrlFilters> { export interface FilterProps<TUrlFilters, TFilterKeys>
extends FilterPageProps<TUrlFilters, TFilterKeys> {
allTabLabel: string; allTabLabel: string;
filterLabel: string; filterLabel: string;
searchPlaceholder: string; searchPlaceholder: string;

View file

@ -8,7 +8,7 @@ Array [
"label": "1", "label": "1",
"parent": null, "parent": null,
"sort": 0, "sort": 0,
"value": "1", "value": 0,
}, },
Object { Object {
"data": null, "data": null,
@ -16,7 +16,7 @@ Array [
"label": "2", "label": "2",
"parent": null, "parent": null,
"sort": 1, "sort": 1,
"value": "2", "value": 1,
}, },
Object { Object {
"data": null, "data": null,
@ -24,7 +24,7 @@ Array [
"label": "3", "label": "3",
"parent": null, "parent": null,
"sort": 2, "sort": 2,
"value": "3", "value": 2,
}, },
Object { Object {
"data": null, "data": null,
@ -32,7 +32,7 @@ Array [
"label": "4", "label": "4",
"parent": null, "parent": null,
"sort": 3, "sort": 3,
"value": "4", "value": 3,
}, },
Object { Object {
"data": null, "data": null,
@ -40,7 +40,7 @@ Array [
"label": "4.1", "label": "4.1",
"parent": "3", "parent": "3",
"sort": 0, "sort": 0,
"value": "4.1", "value": 4,
}, },
Object { Object {
"data": null, "data": null,
@ -48,7 +48,7 @@ Array [
"label": "4.2", "label": "4.2",
"parent": "3", "parent": "3",
"sort": 1, "sort": 1,
"value": "4.2", "value": 5,
}, },
] ]
`; `;
@ -61,24 +61,24 @@ Array [
"children": Array [], "children": Array [],
"data": null, "data": null,
"label": "4.1", "label": "4.1",
"value": "4.1", "value": 4,
}, },
Object { Object {
"children": Array [], "children": Array [],
"data": null, "data": null,
"label": "4.2", "label": "4.2",
"value": "4.2", "value": 5,
}, },
], ],
"data": null, "data": null,
"label": "4", "label": "4",
"value": "4", "value": 3,
}, },
Object { Object {
"children": Array [], "children": Array [],
"data": null, "data": null,
"label": "4.1", "label": "4.1",
"value": "4.1", "value": 4,
}, },
] ]
`; `;
@ -89,7 +89,7 @@ Array [
"children": Array [], "children": Array [],
"data": null, "data": null,
"label": "4.1", "label": "4.1",
"value": "4.1", "value": 4,
}, },
Object { Object {
"children": Array [ "children": Array [
@ -97,18 +97,18 @@ Array [
"children": Array [], "children": Array [],
"data": null, "data": null,
"label": "4.1", "label": "4.1",
"value": "4.1", "value": 4,
}, },
Object { Object {
"children": Array [], "children": Array [],
"data": null, "data": null,
"label": "4.2", "label": "4.2",
"value": "4.2", "value": 5,
}, },
], ],
"data": null, "data": null,
"label": "4", "label": "4",
"value": "4", "value": 3,
}, },
] ]
`; `;

View file

@ -9,24 +9,33 @@ import {
walkToRoot walkToRoot
} from "./menu"; } from "./menu";
const validMenu: IMenu = [ enum MenuKey {
one,
two,
three,
four,
fourOne,
foutTwo
}
const validMenu: IMenu<null, MenuKey> = [
{ {
children: [], children: [],
data: null, data: null,
label: "1", label: "1",
value: "1" value: MenuKey.one
}, },
{ {
children: [], children: [],
data: null, data: null,
label: "2", label: "2",
value: "2" value: MenuKey.two
}, },
{ {
children: [], children: [],
data: null, data: null,
label: "3", label: "3",
value: "3" value: MenuKey.three
}, },
{ {
children: [ children: [
@ -34,18 +43,18 @@ const validMenu: IMenu = [
children: [], children: [],
data: null, data: null,
label: "4.1", label: "4.1",
value: "4.1" value: MenuKey.fourOne
}, },
{ {
children: [], children: [],
data: null, data: null,
label: "4.2", label: "4.2",
value: "4.2" value: MenuKey.foutTwo
} }
], ],
data: null, data: null,
label: "4", label: "4",
value: "4" value: MenuKey.four
} }
]; ];

View file

@ -1,62 +1,76 @@
interface IBaseMenuItem<TMenuData = {}> { interface IBaseMenuItem<TMenuData = {}, TValue = string> {
label: React.ReactNode; label: React.ReactNode;
value?: string; value?: TValue;
data: TMenuData | null; data: TMenuData | null;
} }
export type IFlatMenuItem<TMenuData = {}> = IBaseMenuItem<TMenuData> & { export type IFlatMenuItem<TMenuData = {}, TValue = string> = IBaseMenuItem<
TMenuData,
TValue
> & {
id: string; id: string;
parent: string | null; parent: string | null;
sort: number; sort: number;
}; };
export type IMenuItem<TMenuData = {}> = IBaseMenuItem<TMenuData> & { export type IMenuItem<TMenuData = {}, TValue = string> = IBaseMenuItem<
children: Array<IMenuItem<TMenuData>>; TMenuData,
TValue
> & {
children: Array<IMenuItem<TMenuData, TValue>>;
}; };
export type IMenu<TMenuData = {}> = Array<IMenuItem<TMenuData>>; export type IMenu<TMenuData = {}, TValue = string> = Array<
export type IFlatMenu<TMenuData = {}> = Array<IFlatMenuItem<TMenuData>>; IMenuItem<TMenuData, TValue>
>;
export type IFlatMenu<TMenuData = {}, TValue = string> = Array<
IFlatMenuItem<TMenuData, TValue>
>;
export function validateMenuOptions<TMenuData = {}>( export function validateMenuOptions<TMenuData = {}, TValue = string>(
menu: IMenu<TMenuData> menu: IMenu<TMenuData, TValue>
): boolean { ): boolean {
const values: string[] = toFlat(menu) const values: TValue[] = toFlat(menu)
.map(menuItem => menuItem.value) .map(menuItem => menuItem.value)
.filter(value => value !== undefined); .filter(value => value !== undefined);
const uniqueValues = Array.from(new Set(values)); const uniqueValues = Array.from(new Set(values));
return uniqueValues.length === values.length; return uniqueValues.length === values.length;
} }
function _getMenuItemByPath<TMenuData = {}>( function _getMenuItemByPath<TMenuData = {}, TValue = string>(
menuItem: IMenuItem<TMenuData>, menuItem: IMenuItem<TMenuData, TValue>,
path: number[] path: number[]
): IMenuItem<TMenuData> { ): IMenuItem<TMenuData, TValue> {
if (path.length === 0) { if (path.length === 0) {
return menuItem; return menuItem;
} }
return _getMenuItemByPath(menuItem.children[path[0]], path.slice(1)); return _getMenuItemByPath(menuItem.children[path[0]], path.slice(1));
} }
export function getMenuItemByPath<TMenuData = {}>( export function getMenuItemByPath<TMenuData = {}, TValue = string>(
menu: IMenu<TMenuData>, menu: IMenu<TMenuData, TValue>,
path: number[] path: number[]
): IMenuItem<TMenuData> { ): IMenuItem<TMenuData, TValue> {
return _getMenuItemByPath(menu[path[0]], path.slice(1)); return _getMenuItemByPath(menu[path[0]], path.slice(1));
} }
export function getMenuItemByValue<TMenuData = {}>( export function getMenuItemByValue<TMenuData = {}, TValue = string>(
menu: IMenu<TMenuData>, menu: IMenu<TMenuData, TValue>,
value: string value: TValue
): IMenuItem<TMenuData> { ): IMenuItem<TMenuData, TValue> {
const flatMenu = toFlat(menu); const flatMenu = toFlat(menu);
const flatMenuItem: IFlatMenuItem<TMenuData> = flatMenu.find( const flatMenuItem: IFlatMenuItem<TMenuData, TValue> = flatMenu.find(
menuItem => menuItem.value === value menuItem => menuItem.value === value
); );
if (flatMenuItem === undefined) {
throw new Error(`Value ${value} does not exist in menu`);
}
return _fromFlat(flatMenu, flatMenuItem); return _fromFlat(flatMenu, flatMenuItem);
} }
function _walkToMenuItem<TMenuData = {}>( function _walkToMenuItem<TMenuData = {}, TValue = string>(
menuItem: IMenuItem<TMenuData>, menuItem: IMenuItem<TMenuData, TValue>,
path: number[] path: number[]
): IMenu<TMenuData> { ): IMenu<TMenuData, TValue> {
const node = menuItem.children[path[0]]; const node = menuItem.children[path[0]];
if (path.length === 1) { if (path.length === 1) {
@ -66,18 +80,18 @@ function _walkToMenuItem<TMenuData = {}>(
return [node, ..._walkToMenuItem(node, path.slice(1))]; return [node, ..._walkToMenuItem(node, path.slice(1))];
} }
export function walkToMenuItem<TMenuData = {}>( export function walkToMenuItem<TMenuData = {}, TValue = string>(
menu: IMenu<TMenuData>, menu: IMenu<TMenuData, TValue>,
path: number[] path: number[]
): IMenu<TMenuData> { ): IMenu<TMenuData, TValue> {
const walkByNode = menu[path[0]]; const walkByNode = menu[path[0]];
return [walkByNode, ..._walkToMenuItem(walkByNode, path.slice(1))]; return [walkByNode, ..._walkToMenuItem(walkByNode, path.slice(1))];
} }
function _walkToRoot<TMenuData = {}>( function _walkToRoot<TMenuData = {}, TValue = string>(
flatMenu: IFlatMenu<TMenuData>, flatMenu: IFlatMenu<TMenuData, TValue>,
parent: string parent: string
): IFlatMenu<TMenuData> { ): IFlatMenu<TMenuData, TValue> {
const menuItem = flatMenu.find(menuItem => menuItem.id === parent); const menuItem = flatMenu.find(menuItem => menuItem.id === parent);
if (menuItem.parent === null) { if (menuItem.parent === null) {
@ -86,10 +100,10 @@ function _walkToRoot<TMenuData = {}>(
return [menuItem, ..._walkToRoot(flatMenu, menuItem.parent)]; return [menuItem, ..._walkToRoot(flatMenu, menuItem.parent)];
} }
export function walkToRoot<TMenuData = {}>( export function walkToRoot<TMenuData = {}, TValue = string>(
menu: IMenu<TMenuData>, menu: IMenu<TMenuData, TValue>,
value: string value: TValue
): IMenu<TMenuData> { ): IMenu<TMenuData, TValue> {
const flatMenu = toFlat(menu); const flatMenu = toFlat(menu);
const menuItem = flatMenu.find(menuItem => menuItem.value === value); const menuItem = flatMenu.find(menuItem => menuItem.value === value);
@ -99,13 +113,13 @@ export function walkToRoot<TMenuData = {}>(
).map(flatMenuItem => _fromFlat(flatMenu, flatMenuItem)); ).map(flatMenuItem => _fromFlat(flatMenu, flatMenuItem));
} }
function _toFlat<TMenuData = {}>( function _toFlat<TMenuData = {}, TValue = string>(
menuItem: IMenuItem<TMenuData>, menuItem: IMenuItem<TMenuData, TValue>,
sort: number, sort: number,
parent: string parent: string
): IFlatMenu<TMenuData> { ): IFlatMenu<TMenuData, TValue> {
const id = parent ? [parent, sort].join(":") : sort.toString(); const id = parent ? [parent, sort].join(":") : sort.toString();
const flatMenuItem: IFlatMenuItem<TMenuData> = { const flatMenuItem: IFlatMenuItem<TMenuData, TValue> = {
data: menuItem.data, data: menuItem.data,
id, id,
label: menuItem.label, label: menuItem.label,
@ -117,22 +131,28 @@ function _toFlat<TMenuData = {}>(
flatMenuItem, flatMenuItem,
...menuItem.children ...menuItem.children
.map((child, childIndex) => _toFlat(child, childIndex, id)) .map((child, childIndex) => _toFlat(child, childIndex, id))
.reduce((acc, curr) => [...acc, ...curr], [] as IFlatMenu<TMenuData>) .reduce((acc, curr) => [...acc, ...curr], [] as IFlatMenu<
TMenuData,
TValue
>)
]; ];
} }
export function toFlat<TMenuData = {}>( export function toFlat<TMenuData = {}, TValue = string>(
menu: IMenu<TMenuData> menu: IMenu<TMenuData, TValue>
): IFlatMenu<TMenuData> { ): IFlatMenu<TMenuData, TValue> {
return menu return menu
.map((menuItem, menuItemIndex) => _toFlat(menuItem, menuItemIndex, null)) .map((menuItem, menuItemIndex) => _toFlat(menuItem, menuItemIndex, null))
.reduce((acc, curr) => [...acc, ...curr], [] as IFlatMenu<TMenuData>); .reduce((acc, curr) => [...acc, ...curr], [] as IFlatMenu<
TMenuData,
TValue
>);
} }
function _fromFlat<TMenuData = {}>( function _fromFlat<TMenuData = {}, TValue = string>(
menu: IFlatMenu<TMenuData>, menu: IFlatMenu<TMenuData, TValue>,
flatMenuItem: IFlatMenuItem<TMenuData> flatMenuItem: IFlatMenuItem<TMenuData, TValue>
): IMenuItem<TMenuData> { ): IMenuItem<TMenuData, TValue> {
const children: Array<IMenuItem<TMenuData>> = menu const children: Array<IMenuItem<TMenuData, TValue>> = menu
.filter(menuItem => menuItem.parent === flatMenuItem.id) .filter(menuItem => menuItem.parent === flatMenuItem.id)
.map(menuItem => _fromFlat(menu, menuItem)); .map(menuItem => _fromFlat(menu, menuItem));
@ -143,16 +163,16 @@ function _fromFlat<TMenuData = {}>(
value: flatMenuItem.value value: flatMenuItem.value
}; };
} }
export function fromFlat<TMenuData = {}>( export function fromFlat<TMenuData = {}, TValue = string>(
menu: IFlatMenu<TMenuData> menu: IFlatMenu<TMenuData, TValue>
): IMenu<TMenuData> { ): IMenu<TMenuData, TValue> {
return menu return menu
.filter(menuItem => menuItem.parent === null) .filter(menuItem => menuItem.parent === null)
.map(menuItem => _fromFlat(menu, menuItem)); .map(menuItem => _fromFlat(menu, menuItem));
} }
export function isLeaf<TMenuData = {}>( export function isLeaf<TMenuData = {}, TValue = string>(
menuItem: IMenuItem<TMenuData> menuItem: IMenuItem<TMenuData, TValue>
): boolean { ): boolean {
return menuItem.children.length === 0; return menuItem.children.length === 0;
} }