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 { IFilter } from "./types";
export interface FilterProps {
export interface FilterProps<TFilterKeys = string> {
currencySymbol: string;
menu: IFilter;
menu: IFilter<TFilterKeys>;
filterLabel: string;
onFilterAdd: (filter: FilterContentSubmitData) => void;
}

View file

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

View file

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

View file

@ -25,6 +25,6 @@ export interface FilterData {
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 FilterTabs, { FilterChips, FilterTab } from "../TableFilter";
export interface FilterBarProps<TUrlFilters = object>
extends FilterProps<TUrlFilters> {
filterMenu: IFilter;
export interface FilterBarProps<TUrlFilters = object, TFilterKeys = any>
extends FilterProps<TUrlFilters, TFilterKeys> {
filterMenu: IFilter<TFilterKeys>;
}
const FilterBar: React.FC<FilterBarProps> = ({

View file

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

View file

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

View file

@ -10,17 +10,16 @@ import { FilterProps } from "../../../types";
import { OrderStatusFilter } from "../../../types/globalTypes";
import { OrderListUrlFilters } from "../../urls";
type OrderListFilterProps = FilterProps<OrderListUrlFilters>;
type OrderListFilterProps = FilterProps<OrderListUrlFilters, OrderFilterKeys>;
export enum OrderFilterKeys {
date,
dateEqual,
dateRange,
dateLastWeek,
dateLastMonth,
dateLastYear,
email,
fulfillment
date = "date",
dateEqual = "dateEqual",
dateRange = "dateRange",
dateLastWeek = "dateLastWeek",
dateLastMonth = "dateLastMonth",
dateLastYear = "dateLastYear",
fulfillment = "fulfillment"
}
const OrderListFilter: React.FC<OrderListFilterProps> = props => {
@ -28,7 +27,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
const tz = React.useContext(TimezoneContext);
const intl = useIntl();
const filterMenu: IFilter = [
const filterMenu: IFilter<OrderFilterKeys> = [
{
children: [
{
@ -44,7 +43,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({
defaultMessage: "Last 7 Days"
}),
value: OrderFilterKeys.dateLastWeek.toString()
value: OrderFilterKeys.dateLastWeek
},
{
children: [],
@ -59,7 +58,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({
defaultMessage: "Last 30 Days"
}),
value: OrderFilterKeys.dateLastMonth.toString()
value: OrderFilterKeys.dateLastMonth
},
{
children: [],
@ -74,7 +73,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({
defaultMessage: "Last Year"
}),
value: OrderFilterKeys.dateLastYear.toString()
value: OrderFilterKeys.dateLastYear
},
{
children: [],
@ -88,7 +87,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({
defaultMessage: "Specific Date"
}),
value: OrderFilterKeys.dateEqual.toString()
value: OrderFilterKeys.dateEqual
},
{
children: [],
@ -101,7 +100,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({
defaultMessage: "Range"
}),
value: OrderFilterKeys.dateRange.toString()
value: OrderFilterKeys.dateRange
}
],
data: {
@ -113,7 +112,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
label: intl.formatMessage({
defaultMessage: "Date"
}),
value: OrderFilterKeys.date.toString()
value: OrderFilterKeys.date
},
{
children: [],
@ -155,7 +154,7 @@ const OrderListFilter: React.FC<OrderListFilterProps> = props => {
defaultMessage: "Fulfillment Status",
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 PageHeader from "@saleor/components/PageHeader";
import { sectionNames } from "@saleor/intl";
import { OrderFilterKeys } from "@saleor/orders/components/OrderListFilter";
import { FilterPageProps, ListActions, PageListProps } from "@saleor/types";
import { OrderList_orders_edges_node } from "../../types/OrderList";
import { OrderListUrlFilters } from "../../urls";
@ -16,7 +17,7 @@ import OrderListFilter from "../OrderListFilter";
export interface OrderListPageProps
extends PageListProps,
ListActions,
FilterPageProps<OrderListUrlFilters> {
FilterPageProps<OrderListUrlFilters, OrderFilterKeys> {
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(
filter: OrderListUrlFilters,
data: FilterContentSubmitData
data: FilterContentSubmitData<OrderFilterKeys>
): OrderListUrlFilters {
const { name: filterName, value } = data;
if (filterName === OrderFilterKeys.dateEqual.toString()) {
if (filterName === OrderFilterKeys.dateEqual) {
return {
dateFrom: valueOrFirst(value),
dateTo: valueOrFirst(value)
};
} else if (filterName === OrderFilterKeys.dateRange.toString()) {
} else if (filterName === OrderFilterKeys.dateRange) {
return {
dateFrom: value[0],
dateTo: value[1]
@ -104,15 +104,13 @@ export function createFilter(
OrderFilterKeys.dateLastWeek,
OrderFilterKeys.dateLastMonth,
OrderFilterKeys.dateLastYear
]
.map(value => value.toString())
.includes(filterName)
].includes(filterName)
) {
return {
dateFrom: valueOrFirst(value),
dateTo: undefined
};
} else if (filterName === OrderFilterKeys.fulfillment.toString()) {
} else if (filterName === OrderFilterKeys.fulfillment) {
return {
status: dedupeFilter(
filter.status

View file

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

View file

@ -26,12 +26,12 @@ import {
} from "@saleor/types";
import { ProductListUrlFilters } from "../../urls";
import ProductList from "../ProductList";
import ProductListFilter from "../ProductListFilter";
import ProductListFilter, { ProductFilterKeys } from "../ProductListFilter";
export interface ProductListPageProps
extends PageListProps<ProductListColumns>,
ListActions,
FilterPageProps<ProductListUrlFilters>,
FilterPageProps<ProductListUrlFilters, ProductFilterKeys>,
FetchMoreProps {
availableInGridAttributes: AvailableInGridAttributes_availableInGrid_edges_node[];
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(
filter: FilterContentSubmitData
filter: FilterContentSubmitData<ProductFilterKeys>
): ProductListUrlFilters {
const filterName = filter.name;
if (filterName === ProductFilterKeys.priceEqual.toString()) {
if (filterName === ProductFilterKeys.priceEqual) {
const value = filter.value as string;
return {
priceFrom: value,
priceTo: value
};
} else if (filterName === ProductFilterKeys.priceRange.toString()) {
} else if (filterName === ProductFilterKeys.priceRange) {
const { value } = filter;
return {
priceFrom: value[0],
priceTo: value[1]
};
} else if (filterName === ProductFilterKeys.published.toString()) {
} else if (filterName === ProductFilterKeys.published) {
return {
isPublished: filter.value as string
};
} else if (filterName === ProductFilterKeys.stock.toString()) {
} else if (filterName === ProductFilterKeys.stock) {
const value = filter.value as string;
return {
status: StockAvailability[value]

View file

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

View file

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

View file

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

View file

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