@servicetitan/table 23.4.0 → 24.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/demo/filters/async-select-filter.d.ts +3 -0
- package/dist/demo/filters/async-select-filter.d.ts.map +1 -0
- package/dist/demo/filters/async-select-filter.js +23 -0
- package/dist/demo/filters/async-select-filter.js.map +1 -0
- package/dist/demo/filters/categories.d.ts +9 -0
- package/dist/demo/filters/categories.d.ts.map +1 -0
- package/dist/demo/filters/categories.js +33 -0
- package/dist/demo/filters/categories.js.map +1 -0
- package/dist/demo/filters/single-select-filter.d.ts +3 -0
- package/dist/demo/filters/single-select-filter.d.ts.map +1 -0
- package/dist/demo/filters/single-select-filter.js +20 -0
- package/dist/demo/filters/single-select-filter.js.map +1 -0
- package/dist/demo/filters/table.store.d.ts +9 -0
- package/dist/demo/filters/table.store.d.ts.map +1 -0
- package/dist/demo/filters/table.store.js +73 -0
- package/dist/demo/filters/table.store.js.map +1 -0
- package/dist/filters/async-select/async-select-filter.d.ts +19 -0
- package/dist/filters/async-select/async-select-filter.d.ts.map +1 -0
- package/dist/filters/async-select/async-select-filter.js +168 -0
- package/dist/filters/async-select/async-select-filter.js.map +1 -0
- package/dist/filters/async-select/async-select-filter.stories.d.ts +7 -0
- package/dist/filters/async-select/async-select-filter.stories.d.ts.map +1 -0
- package/dist/filters/async-select/async-select-filter.stories.js +7 -0
- package/dist/filters/async-select/async-select-filter.stories.js.map +1 -0
- package/dist/filters/index.d.ts +2 -0
- package/dist/filters/index.d.ts.map +1 -1
- package/dist/filters/index.js +2 -0
- package/dist/filters/index.js.map +1 -1
- package/dist/filters/single-select/single-select-filter.d.ts +9 -0
- package/dist/filters/single-select/single-select-filter.d.ts.map +1 -0
- package/dist/filters/single-select/single-select-filter.js +30 -0
- package/dist/filters/single-select/single-select-filter.js.map +1 -0
- package/dist/filters/single-select/single-select-filter.stories.d.ts +7 -0
- package/dist/filters/single-select/single-select-filter.stories.d.ts.map +1 -0
- package/dist/filters/single-select/single-select-filter.stories.js +7 -0
- package/dist/filters/single-select/single-select-filter.stories.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/filters.d.ts +4 -0
- package/dist/utils/filters.d.ts.map +1 -0
- package/dist/utils/filters.js +22 -0
- package/dist/utils/filters.js.map +1 -0
- package/package.json +6 -6
- package/src/demo/filters/async-select-filter.tsx +53 -0
- package/src/demo/filters/categories.tsx +40 -0
- package/src/demo/filters/single-select-filter.tsx +51 -0
- package/src/demo/filters/table.store.ts +45 -0
- package/src/filters/async-select/async-select-filter.stories.tsx +7 -0
- package/src/filters/async-select/async-select-filter.tsx +249 -0
- package/src/filters/index.ts +2 -0
- package/src/filters/single-select/single-select-filter.stories.tsx +7 -0
- package/src/filters/single-select/single-select-filter.tsx +66 -0
- package/src/index.ts +1 -0
- package/src/utils/__tests__/filters.test.ts +31 -0
- package/src/utils/filters.ts +33 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
import { isCompositeFilterDescriptor, } from '@servicetitan/data-query';
|
2
|
+
const getFiltersFlatInner = (filter) => {
|
3
|
+
if (!filter) {
|
4
|
+
return [];
|
5
|
+
}
|
6
|
+
else if (isCompositeFilterDescriptor(filter)) {
|
7
|
+
return filter.filters.reduce((out, item) => [...out, ...getFiltersFlatInner(item)], []);
|
8
|
+
}
|
9
|
+
return [filter];
|
10
|
+
};
|
11
|
+
export const getFiltersFlat = (filter) => {
|
12
|
+
return getFiltersFlatInner(filter);
|
13
|
+
};
|
14
|
+
export const getFiltersMap = (filter) => {
|
15
|
+
return getFiltersFlatInner(filter).reduce((out, filter) => {
|
16
|
+
if (typeof filter.field === 'string') {
|
17
|
+
out[filter.field] = filter;
|
18
|
+
}
|
19
|
+
return out;
|
20
|
+
}, {});
|
21
|
+
};
|
22
|
+
//# sourceMappingURL=filters.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"filters.js","sourceRoot":"","sources":["../../src/utils/filters.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,2BAA2B,GAE9B,MAAM,0BAA0B,CAAC;AAElC,MAAM,mBAAmB,GAAG,CACxB,MAAqD,EACnC,EAAE;IACpB,IAAI,CAAC,MAAM,EAAE;QACT,OAAO,EAAE,CAAC;KACb;SAAM,IAAI,2BAA2B,CAAC,MAAM,CAAC,EAAE;QAC5C,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CACxB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC,EACrD,EAAwB,CAC3B,CAAC;KACL;IACD,OAAO,CAAC,MAAM,CAAC,CAAC;AACpB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAuB,EAAsB,EAAE;IAC1E,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAC;AACvC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAuB,EAAoC,EAAE;IACvF,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;QACtD,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,EAAE;YAClC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC;SAC9B;QACD,OAAO,GAAG,CAAC;IACf,CAAC,EAAE,EAAsC,CAAC,CAAC;AAC/C,CAAC,CAAC"}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@servicetitan/table",
|
3
|
-
"version": "
|
3
|
+
"version": "24.0.0",
|
4
4
|
"description": "",
|
5
5
|
"homepage": "https://docs.st.dev/docs/frontend/table",
|
6
6
|
"repository": {
|
@@ -37,9 +37,9 @@
|
|
37
37
|
"memoize-one": "~6.0.0"
|
38
38
|
},
|
39
39
|
"devDependencies": {
|
40
|
-
"@servicetitan/data-query": "^
|
40
|
+
"@servicetitan/data-query": "^24.0.0",
|
41
41
|
"@servicetitan/design-system": ">=12.4.1",
|
42
|
-
"@servicetitan/form": "^
|
42
|
+
"@servicetitan/form": "^24.0.0",
|
43
43
|
"@servicetitan/react-ioc": "^21.6.0",
|
44
44
|
"@servicetitan/suppress-warnings": "^21.6.0",
|
45
45
|
"@types/accounting": "~0.4.2",
|
@@ -53,9 +53,9 @@
|
|
53
53
|
"react": "~17.0.2"
|
54
54
|
},
|
55
55
|
"peerDependencies": {
|
56
|
-
"@servicetitan/data-query": "^
|
56
|
+
"@servicetitan/data-query": "^24.0.0",
|
57
57
|
"@servicetitan/design-system": ">=12.4.1",
|
58
|
-
"@servicetitan/form": "^
|
58
|
+
"@servicetitan/form": "^24.0.0",
|
59
59
|
"@servicetitan/react-ioc": ">21.0.0",
|
60
60
|
"@servicetitan/suppress-warnings": ">21.0.0",
|
61
61
|
"accounting": "~0.4.1",
|
@@ -72,5 +72,5 @@
|
|
72
72
|
"cli": {
|
73
73
|
"webpack": false
|
74
74
|
},
|
75
|
-
"gitHead": "
|
75
|
+
"gitHead": "136cde3c32f5798d1c73dd456b16bd691d0cf257"
|
76
76
|
}
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import { useMemo, FC } from 'react';
|
2
|
+
import { observer } from 'mobx-react';
|
3
|
+
import { provide, useDependencies } from '@servicetitan/react-ioc';
|
4
|
+
|
5
|
+
import { Table, TableColumn, asyncSelectColumnMenuFilter } from '../..';
|
6
|
+
|
7
|
+
import { TableStore } from './table.store';
|
8
|
+
import { CategoryCell } from './categories';
|
9
|
+
|
10
|
+
export const TableExample: FC = provide({
|
11
|
+
singletons: [TableStore],
|
12
|
+
})(
|
13
|
+
observer(() => {
|
14
|
+
const [{ categoryFetcher, madeInFetcher, tableState }] = useDependencies(TableStore);
|
15
|
+
|
16
|
+
const madeInColumnMenu = useMemo(
|
17
|
+
() =>
|
18
|
+
asyncSelectColumnMenuFilter({
|
19
|
+
dataFetcher: madeInFetcher,
|
20
|
+
placeholder: 'Search for made-ins',
|
21
|
+
}),
|
22
|
+
[madeInFetcher]
|
23
|
+
);
|
24
|
+
|
25
|
+
const categoryColumnMenu = useMemo(
|
26
|
+
() =>
|
27
|
+
asyncSelectColumnMenuFilter({
|
28
|
+
dataFetcher: categoryFetcher,
|
29
|
+
placeholder: 'Search for Categories',
|
30
|
+
multiple: true,
|
31
|
+
}),
|
32
|
+
[categoryFetcher]
|
33
|
+
);
|
34
|
+
|
35
|
+
return (
|
36
|
+
<Table tableState={tableState} striped={false}>
|
37
|
+
<TableColumn field="ProductID" title="ID" editable={false} width="100px" />
|
38
|
+
<TableColumn field="ProductName" title="Product Name" width="240px" />
|
39
|
+
<TableColumn
|
40
|
+
field="MadeIn"
|
41
|
+
title="Made In (async single-select filter)"
|
42
|
+
columnMenu={madeInColumnMenu}
|
43
|
+
/>
|
44
|
+
<TableColumn
|
45
|
+
field="CategoryID"
|
46
|
+
title="Category (async multi-select filter)"
|
47
|
+
columnMenu={categoryColumnMenu}
|
48
|
+
cell={CategoryCell}
|
49
|
+
/>
|
50
|
+
</Table>
|
51
|
+
);
|
52
|
+
})
|
53
|
+
);
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import { FC } from 'react';
|
2
|
+
import { TableCellProps } from '../../table';
|
3
|
+
|
4
|
+
export interface Category {
|
5
|
+
CategoryID: number;
|
6
|
+
CategoryName: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
export const categories: Category[] = [
|
10
|
+
{
|
11
|
+
CategoryID: 1,
|
12
|
+
CategoryName: 'Wok',
|
13
|
+
},
|
14
|
+
{
|
15
|
+
CategoryID: 2,
|
16
|
+
CategoryName: 'Sushi',
|
17
|
+
},
|
18
|
+
{
|
19
|
+
CategoryID: 6,
|
20
|
+
CategoryName: 'Gunkan',
|
21
|
+
},
|
22
|
+
{
|
23
|
+
CategoryID: 7,
|
24
|
+
CategoryName: 'Miso',
|
25
|
+
},
|
26
|
+
{
|
27
|
+
CategoryID: 8,
|
28
|
+
CategoryName: 'Roll',
|
29
|
+
},
|
30
|
+
{
|
31
|
+
CategoryID: 10,
|
32
|
+
CategoryName: 'Sashimi',
|
33
|
+
},
|
34
|
+
];
|
35
|
+
|
36
|
+
export const CategoryCell: FC<TableCellProps> = props => {
|
37
|
+
const { field, dataItem } = props;
|
38
|
+
|
39
|
+
return <td>{categories.find(c => c.CategoryID === dataItem[field!])?.CategoryName}</td>;
|
40
|
+
};
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import { useMemo, FC } from 'react';
|
2
|
+
|
3
|
+
import { provide, useDependencies } from '@servicetitan/react-ioc';
|
4
|
+
|
5
|
+
import { observer } from 'mobx-react';
|
6
|
+
|
7
|
+
import { Table, TableColumn, singleSelectColumnMenuFilter } from '../..';
|
8
|
+
|
9
|
+
import { TableStore } from './table.store';
|
10
|
+
import { categories, CategoryCell } from './categories';
|
11
|
+
|
12
|
+
export const TableExample: FC = provide({
|
13
|
+
singletons: [TableStore],
|
14
|
+
})(
|
15
|
+
observer(() => {
|
16
|
+
const [{ tableState, madeInOptions }] = useDependencies(TableStore);
|
17
|
+
|
18
|
+
const madeInColumnMenu = useMemo(
|
19
|
+
() => singleSelectColumnMenuFilter({ options: madeInOptions }),
|
20
|
+
[madeInOptions]
|
21
|
+
);
|
22
|
+
|
23
|
+
const categoryColumnMenu = useMemo(
|
24
|
+
() =>
|
25
|
+
singleSelectColumnMenuFilter({
|
26
|
+
options: categories,
|
27
|
+
renderItem: cat => cat.CategoryName,
|
28
|
+
valueSelector: cat => cat.CategoryID,
|
29
|
+
}),
|
30
|
+
[]
|
31
|
+
);
|
32
|
+
|
33
|
+
return (
|
34
|
+
<Table tableState={tableState} striped={false}>
|
35
|
+
<TableColumn field="ProductID" title="ID" editable={false} width="100px" />
|
36
|
+
<TableColumn field="ProductName" title="Product Name" width="240px" />
|
37
|
+
<TableColumn
|
38
|
+
field="MadeIn"
|
39
|
+
title="Made In (simple single-select filter)"
|
40
|
+
columnMenu={madeInColumnMenu}
|
41
|
+
/>
|
42
|
+
<TableColumn
|
43
|
+
field="CategoryID"
|
44
|
+
title="Category (simple single-select filter)"
|
45
|
+
columnMenu={categoryColumnMenu}
|
46
|
+
cell={CategoryCell}
|
47
|
+
/>
|
48
|
+
</Table>
|
49
|
+
);
|
50
|
+
})
|
51
|
+
);
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import { injectable } from '@servicetitan/react-ioc';
|
2
|
+
|
3
|
+
import { InMemoryDataSource, TableState, AsyncSelectFilterDataFetcher } from '../..';
|
4
|
+
import { products } from '../overview/products';
|
5
|
+
import { categories } from './categories';
|
6
|
+
|
7
|
+
@injectable()
|
8
|
+
export class TableStore {
|
9
|
+
tableState = new TableState({
|
10
|
+
dataSource: this.getDataSource(),
|
11
|
+
pageSize: 5,
|
12
|
+
});
|
13
|
+
|
14
|
+
get madeInOptions() {
|
15
|
+
return Array.from(new Set(products.map(p => p.MadeIn)));
|
16
|
+
}
|
17
|
+
|
18
|
+
categoryFetcher: AsyncSelectFilterDataFetcher<number> = async opts => {
|
19
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
20
|
+
|
21
|
+
const sv = opts.search?.trim().toLowerCase();
|
22
|
+
|
23
|
+
return {
|
24
|
+
data: categories
|
25
|
+
.filter(cat => (sv ? cat.CategoryName.toLowerCase().includes(sv) : true))
|
26
|
+
.map(cat => ({ value: cat.CategoryID, text: cat.CategoryName })),
|
27
|
+
};
|
28
|
+
};
|
29
|
+
|
30
|
+
madeInFetcher: AsyncSelectFilterDataFetcher<string> = async opts => {
|
31
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
32
|
+
|
33
|
+
const sv = opts.search?.trim().toLowerCase();
|
34
|
+
|
35
|
+
return {
|
36
|
+
data: this.madeInOptions
|
37
|
+
.filter(opt => (sv ? opt.toLowerCase().includes(sv) : true))
|
38
|
+
.map(opt => ({ value: opt, text: opt })),
|
39
|
+
};
|
40
|
+
};
|
41
|
+
|
42
|
+
private getDataSource() {
|
43
|
+
return new InMemoryDataSource(products, row => row.ProductID);
|
44
|
+
}
|
45
|
+
}
|
@@ -0,0 +1,249 @@
|
|
1
|
+
import { Component, SyntheticEvent, ReactNode, Fragment, KeyboardEvent, FC } from 'react';
|
2
|
+
|
3
|
+
import {
|
4
|
+
Checkbox,
|
5
|
+
TableFilterCellProps,
|
6
|
+
Radio,
|
7
|
+
Input,
|
8
|
+
Spinner,
|
9
|
+
BodyText,
|
10
|
+
} from '@servicetitan/design-system';
|
11
|
+
import { IdType } from '@servicetitan/data-query';
|
12
|
+
|
13
|
+
import { renderCustomColumnMenuFilter } from '../column-menu-filters';
|
14
|
+
import { makeObservable, observable, runInAction } from 'mobx';
|
15
|
+
import { observer } from 'mobx-react';
|
16
|
+
|
17
|
+
export type AsyncSelectFilterDataFetcher<
|
18
|
+
TV extends IdType = IdType,
|
19
|
+
TO extends AsyncSelectItem<TV> = AsyncSelectItem<TV>
|
20
|
+
> = (opts: { search?: string }) => Promise<{ data: TO[] }>;
|
21
|
+
|
22
|
+
export interface AsyncSelectItem<TV extends IdType = IdType> {
|
23
|
+
value: TV;
|
24
|
+
text: string;
|
25
|
+
}
|
26
|
+
|
27
|
+
interface SelectorProps<TID extends IdType, TO extends AsyncSelectItem<TID>> {
|
28
|
+
placeholder: string;
|
29
|
+
selected: TO[];
|
30
|
+
dataFetcher: AsyncSelectFilterDataFetcher<TID, TO>;
|
31
|
+
itemComponent: FC<SelectorItemProps>;
|
32
|
+
onChange(option: TO, checked: boolean, event: SyntheticEvent<HTMLInputElement>): void;
|
33
|
+
renderer?(item: TO): ReactNode;
|
34
|
+
}
|
35
|
+
|
36
|
+
interface SelectorItemProps {
|
37
|
+
option: AsyncSelectItem;
|
38
|
+
checked: boolean;
|
39
|
+
onChange(
|
40
|
+
option: AsyncSelectItem,
|
41
|
+
checked: boolean,
|
42
|
+
event: SyntheticEvent<HTMLInputElement>
|
43
|
+
): void;
|
44
|
+
renderer?(item: AsyncSelectItem): ReactNode;
|
45
|
+
}
|
46
|
+
|
47
|
+
@observer
|
48
|
+
class SelectorAsync<TID extends IdType, TO extends AsyncSelectItem<TID>> extends Component<
|
49
|
+
SelectorProps<TID, TO>
|
50
|
+
> {
|
51
|
+
@observable shownOptions: AsyncSelectItem<TID>[] = [];
|
52
|
+
@observable search = '';
|
53
|
+
@observable error = false;
|
54
|
+
@observable loading = false;
|
55
|
+
|
56
|
+
constructor(props: SelectorProps<TID, TO>) {
|
57
|
+
super(props);
|
58
|
+
makeObservable(this);
|
59
|
+
}
|
60
|
+
|
61
|
+
componentDidMount() {
|
62
|
+
this.searchOptions().catch();
|
63
|
+
}
|
64
|
+
|
65
|
+
handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
66
|
+
if (event.key === 'Enter') {
|
67
|
+
event.stopPropagation();
|
68
|
+
}
|
69
|
+
};
|
70
|
+
|
71
|
+
handleSearch = (_0: SyntheticEvent<HTMLInputElement>, data: { value: string }) => {
|
72
|
+
runInAction(() => (this.search = data.value));
|
73
|
+
this.searchOptions().catch();
|
74
|
+
};
|
75
|
+
|
76
|
+
searchOptions = async () => {
|
77
|
+
runInAction(() => {
|
78
|
+
this.loading = true;
|
79
|
+
});
|
80
|
+
|
81
|
+
try {
|
82
|
+
const { data } = await this.props.dataFetcher({
|
83
|
+
search: this.search,
|
84
|
+
});
|
85
|
+
|
86
|
+
runInAction(() => {
|
87
|
+
this.shownOptions = data;
|
88
|
+
this.error = false;
|
89
|
+
this.loading = false;
|
90
|
+
});
|
91
|
+
} catch {
|
92
|
+
runInAction(() => {
|
93
|
+
this.error = true;
|
94
|
+
this.loading = false;
|
95
|
+
});
|
96
|
+
}
|
97
|
+
};
|
98
|
+
|
99
|
+
render() {
|
100
|
+
const selectedOptions = this.props.selected;
|
101
|
+
const selected = new Set(selectedOptions.map(opt => opt.value));
|
102
|
+
const ItemComponent = this.props.itemComponent;
|
103
|
+
|
104
|
+
return (
|
105
|
+
<Fragment>
|
106
|
+
<Input
|
107
|
+
className="m-x-half m-t-half m-b-2"
|
108
|
+
placeholder={this.props.placeholder}
|
109
|
+
onChange={this.handleSearch}
|
110
|
+
onKeyDown={this.handleKeyDown}
|
111
|
+
size="xsmall"
|
112
|
+
value={this.search}
|
113
|
+
/>
|
114
|
+
<div className="p-x-half position-relative" onKeyDown={this.handleKeyDown}>
|
115
|
+
{!!selectedOptions.length && (
|
116
|
+
<div className="border-bottom m-y-half">
|
117
|
+
{selectedOptions.map(option => (
|
118
|
+
<ItemComponent
|
119
|
+
key={option.value}
|
120
|
+
option={option}
|
121
|
+
checked
|
122
|
+
renderer={this.props.renderer}
|
123
|
+
onChange={this.props.onChange}
|
124
|
+
/>
|
125
|
+
))}
|
126
|
+
</div>
|
127
|
+
)}
|
128
|
+
{this.error ? (
|
129
|
+
<BodyText className="c-red-500">Unable to load options</BodyText>
|
130
|
+
) : this.shownOptions.length ? (
|
131
|
+
this.shownOptions
|
132
|
+
.filter(opt => !selected.has(opt.value))
|
133
|
+
.map(option => (
|
134
|
+
<ItemComponent
|
135
|
+
key={option.value}
|
136
|
+
option={option}
|
137
|
+
checked={false}
|
138
|
+
renderer={this.props.renderer}
|
139
|
+
onChange={this.props.onChange}
|
140
|
+
/>
|
141
|
+
))
|
142
|
+
) : this.loading ? (
|
143
|
+
<BodyText> </BodyText>
|
144
|
+
) : (
|
145
|
+
<BodyText>No options found</BodyText>
|
146
|
+
)}
|
147
|
+
|
148
|
+
{this.loading && (
|
149
|
+
<div className="position-absolute top-0 bottom-0 left-0 right-0 opacity-disabled bg-white d-f justify-content-center">
|
150
|
+
<Spinner size="tiny" />
|
151
|
+
</div>
|
152
|
+
)}
|
153
|
+
</div>
|
154
|
+
</Fragment>
|
155
|
+
);
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
const SelectorItemSingle: FC<SelectorItemProps> = ({ option, renderer, checked, onChange }) => (
|
160
|
+
<Radio
|
161
|
+
label={renderer?.(option) ?? option.text}
|
162
|
+
checked={checked}
|
163
|
+
onChange={(_, event) => onChange(option, true, event)}
|
164
|
+
className="m-b-1"
|
165
|
+
/>
|
166
|
+
);
|
167
|
+
|
168
|
+
const SelectorItemMultiple: FC<SelectorItemProps> = ({ option, renderer, checked, onChange }) => (
|
169
|
+
<Checkbox
|
170
|
+
label={renderer?.(option) ?? option.text}
|
171
|
+
checked={checked}
|
172
|
+
onChange={(_, checked, event) => onChange(option, checked, event)}
|
173
|
+
className="m-b-1"
|
174
|
+
/>
|
175
|
+
);
|
176
|
+
|
177
|
+
export interface AsyncSelectFilterOptions<TID extends IdType, TO extends AsyncSelectItem<TID>> {
|
178
|
+
dataFetcher: AsyncSelectFilterDataFetcher<TID, TO>;
|
179
|
+
placeholder: string;
|
180
|
+
multiple?: boolean;
|
181
|
+
renderItem?: (item: TO) => ReactNode;
|
182
|
+
}
|
183
|
+
|
184
|
+
interface TableFilterCellPropsTyped<T = any> extends Omit<TableFilterCellProps, 'value'> {
|
185
|
+
value?: T;
|
186
|
+
}
|
187
|
+
|
188
|
+
export function asyncSelectColumnMenuFilter<TID extends IdType, TO extends AsyncSelectItem<TID>>(
|
189
|
+
options: AsyncSelectFilterOptions<TID, TO>
|
190
|
+
) {
|
191
|
+
const contains = (value: TID, options?: TO[]) => options?.some(opt => opt.value === value);
|
192
|
+
const equals = (value: TID, option?: TO) => option?.value === value;
|
193
|
+
|
194
|
+
const FilterCell = options.multiple
|
195
|
+
? ({ value, onChange }: TableFilterCellPropsTyped<TO[]>) => {
|
196
|
+
const handleChange = (
|
197
|
+
option: TO,
|
198
|
+
checked: boolean,
|
199
|
+
event: SyntheticEvent<HTMLInputElement>
|
200
|
+
) => {
|
201
|
+
const val = checked
|
202
|
+
? (value ?? []).concat(option)
|
203
|
+
: (value ?? []).filter(opt => opt.value !== option.value);
|
204
|
+
|
205
|
+
onChange({
|
206
|
+
value: val.length ? val : undefined,
|
207
|
+
operator: val.length ? contains : '',
|
208
|
+
syntheticEvent: event,
|
209
|
+
});
|
210
|
+
};
|
211
|
+
|
212
|
+
return (
|
213
|
+
<SelectorAsync
|
214
|
+
selected={value ?? []}
|
215
|
+
itemComponent={SelectorItemMultiple}
|
216
|
+
onChange={handleChange}
|
217
|
+
placeholder={options.placeholder}
|
218
|
+
renderer={options.renderItem}
|
219
|
+
dataFetcher={options.dataFetcher}
|
220
|
+
/>
|
221
|
+
);
|
222
|
+
}
|
223
|
+
: ({ value, onChange }: TableFilterCellPropsTyped<TO>) => {
|
224
|
+
const handleChange = (
|
225
|
+
option: TO,
|
226
|
+
checked: boolean,
|
227
|
+
event: SyntheticEvent<HTMLInputElement>
|
228
|
+
) => {
|
229
|
+
onChange({
|
230
|
+
value: option,
|
231
|
+
operator: option ? equals : '',
|
232
|
+
syntheticEvent: event,
|
233
|
+
});
|
234
|
+
};
|
235
|
+
|
236
|
+
return (
|
237
|
+
<SelectorAsync
|
238
|
+
selected={value ? [value] : []}
|
239
|
+
onChange={handleChange}
|
240
|
+
itemComponent={SelectorItemSingle}
|
241
|
+
placeholder={options.placeholder}
|
242
|
+
renderer={options.renderItem}
|
243
|
+
dataFetcher={options.dataFetcher}
|
244
|
+
/>
|
245
|
+
);
|
246
|
+
};
|
247
|
+
|
248
|
+
return renderCustomColumnMenuFilter(FilterCell);
|
249
|
+
}
|
package/src/filters/index.ts
CHANGED
@@ -6,3 +6,5 @@ export * from './column-menu-filters';
|
|
6
6
|
export * from './field-values-filter';
|
7
7
|
export * from './multiselect-filter';
|
8
8
|
export * from './numeric-filter-extended/numeric-filter-extended';
|
9
|
+
export * from './single-select/single-select-filter';
|
10
|
+
export * from './async-select/async-select-filter';
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import { Component, SyntheticEvent, ReactNode, FC } from 'react';
|
2
|
+
|
3
|
+
import { TableFilterCellProps, Radio } from '@servicetitan/design-system';
|
4
|
+
|
5
|
+
import { renderCustomColumnMenuFilter } from '../column-menu-filters';
|
6
|
+
import { IdType } from '@servicetitan/data-query';
|
7
|
+
|
8
|
+
interface SelectorProps<TV, TO> {
|
9
|
+
options: TO[];
|
10
|
+
value?: TV;
|
11
|
+
valueSelector(item: TO): TV;
|
12
|
+
onChange(value: TV | undefined, event: SyntheticEvent<HTMLInputElement>): void;
|
13
|
+
renderer?(item: TO): ReactNode;
|
14
|
+
}
|
15
|
+
|
16
|
+
class SelectorRadio<TV, TO> extends Component<SelectorProps<TV, TO>> {
|
17
|
+
render() {
|
18
|
+
const { options, value, renderer } = this.props;
|
19
|
+
|
20
|
+
return options.map((option, index) => {
|
21
|
+
const optionValue = this.props.valueSelector(option);
|
22
|
+
return (
|
23
|
+
// eslint-disable-next-line react/no-array-index-key
|
24
|
+
<span key={index} className="k-widget m-b-1-i d-b">
|
25
|
+
<Radio
|
26
|
+
label={renderer ? renderer(option) : `${option}`}
|
27
|
+
checked={value === optionValue}
|
28
|
+
onChange={(_, event) => this.props.onChange(optionValue, event)}
|
29
|
+
/>
|
30
|
+
</span>
|
31
|
+
);
|
32
|
+
});
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
export interface SingleSelectColumnMenuOptions<TV, TO = TV> {
|
37
|
+
options: TO[];
|
38
|
+
valueSelector?: (item: TO) => TV;
|
39
|
+
renderItem?: (item: TO) => ReactNode | string;
|
40
|
+
}
|
41
|
+
|
42
|
+
export function singleSelectColumnMenuFilter<TV extends IdType, TO = TV>(
|
43
|
+
props: SingleSelectColumnMenuOptions<TV, TO>
|
44
|
+
) {
|
45
|
+
const FilterCell: FC<TableFilterCellProps> = ({ value, onChange }) => {
|
46
|
+
const handleChange = (value: TV | undefined, event: SyntheticEvent<HTMLInputElement>) => {
|
47
|
+
onChange({
|
48
|
+
value: value ?? '',
|
49
|
+
operator: value ? 'equals' : '',
|
50
|
+
syntheticEvent: event,
|
51
|
+
});
|
52
|
+
};
|
53
|
+
|
54
|
+
return (
|
55
|
+
<SelectorRadio
|
56
|
+
options={props.options}
|
57
|
+
value={value === '' ? undefined : value}
|
58
|
+
onChange={handleChange}
|
59
|
+
renderer={props.renderItem}
|
60
|
+
valueSelector={props.valueSelector ?? (option => option)}
|
61
|
+
/>
|
62
|
+
);
|
63
|
+
};
|
64
|
+
|
65
|
+
return renderCustomColumnMenuFilter(FilterCell);
|
66
|
+
}
|
package/src/index.ts
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
import { getFiltersFlat, getFiltersMap } from '../filters';
|
2
|
+
import { CompositeFilterDescriptor, FilterDescriptor } from '@servicetitan/data-query';
|
3
|
+
|
4
|
+
const filter = (field: string) => ({ field, operator: 'qq', value: 'ee' });
|
5
|
+
const composite = (
|
6
|
+
filters: (FilterDescriptor | CompositeFilterDescriptor)[]
|
7
|
+
): CompositeFilterDescriptor => ({ filters, logic: 'and' });
|
8
|
+
|
9
|
+
const filters1 = () =>
|
10
|
+
composite([composite([filter('f1'), filter('f2')]), filter('f3'), composite([filter('f1')])]);
|
11
|
+
|
12
|
+
describe('getFiltersFlat', () => {
|
13
|
+
test('getFiltersFlat', () => {
|
14
|
+
expect(getFiltersFlat(filters1())).toEqual([
|
15
|
+
filter('f1'),
|
16
|
+
filter('f2'),
|
17
|
+
filter('f3'),
|
18
|
+
filter('f1'),
|
19
|
+
]);
|
20
|
+
});
|
21
|
+
});
|
22
|
+
|
23
|
+
describe('getFiltersMap', () => {
|
24
|
+
test('getFiltersMap', () => {
|
25
|
+
expect(getFiltersMap(filters1())).toEqual({
|
26
|
+
f1: filter('f1'),
|
27
|
+
f2: filter('f2'),
|
28
|
+
f3: filter('f3'),
|
29
|
+
});
|
30
|
+
});
|
31
|
+
});
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import {
|
2
|
+
CompositeFilterDescriptor,
|
3
|
+
FilterDescriptor,
|
4
|
+
isCompositeFilterDescriptor,
|
5
|
+
State,
|
6
|
+
} from '@servicetitan/data-query';
|
7
|
+
|
8
|
+
const getFiltersFlatInner = (
|
9
|
+
filter?: FilterDescriptor | CompositeFilterDescriptor
|
10
|
+
): FilterDescriptor[] => {
|
11
|
+
if (!filter) {
|
12
|
+
return [];
|
13
|
+
} else if (isCompositeFilterDescriptor(filter)) {
|
14
|
+
return filter.filters.reduce(
|
15
|
+
(out, item) => [...out, ...getFiltersFlatInner(item)],
|
16
|
+
[] as FilterDescriptor[]
|
17
|
+
);
|
18
|
+
}
|
19
|
+
return [filter];
|
20
|
+
};
|
21
|
+
|
22
|
+
export const getFiltersFlat = (filter: State['filter']): FilterDescriptor[] => {
|
23
|
+
return getFiltersFlatInner(filter);
|
24
|
+
};
|
25
|
+
|
26
|
+
export const getFiltersMap = (filter: State['filter']): Record<string, FilterDescriptor> => {
|
27
|
+
return getFiltersFlatInner(filter).reduce((out, filter) => {
|
28
|
+
if (typeof filter.field === 'string') {
|
29
|
+
out[filter.field] = filter;
|
30
|
+
}
|
31
|
+
return out;
|
32
|
+
}, {} as Record<string, FilterDescriptor>);
|
33
|
+
};
|