@servicetitan/table 25.0.1 → 25.2.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.js +1 -1
- package/dist/demo/filters/async-select-filter.js.map +1 -1
- package/dist/demo/filters/categories.d.ts.map +1 -1
- package/dist/demo/filters/categories.js +24 -0
- package/dist/demo/filters/categories.js.map +1 -1
- package/dist/demo/filters/multiselect-filter.d.ts.map +1 -1
- package/dist/demo/filters/multiselect-filter.js +2 -1
- package/dist/demo/filters/multiselect-filter.js.map +1 -1
- package/dist/demo/filters/select-filter.d.ts +6 -0
- package/dist/demo/filters/select-filter.d.ts.map +1 -0
- package/dist/demo/filters/select-filter.js +58 -0
- package/dist/demo/filters/select-filter.js.map +1 -0
- package/dist/demo/filters/single-select-filter.d.ts.map +1 -1
- package/dist/demo/filters/single-select-filter.js +2 -1
- package/dist/demo/filters/single-select-filter.js.map +1 -1
- package/dist/demo/filters/table.store.d.ts +8 -1
- package/dist/demo/filters/table.store.d.ts.map +1 -1
- package/dist/demo/filters/table.store.js +35 -1
- package/dist/demo/filters/table.store.js.map +1 -1
- package/dist/demo/footer-page-size/index.d.ts +2 -0
- package/dist/demo/footer-page-size/index.d.ts.map +1 -0
- package/dist/demo/footer-page-size/index.js +2 -0
- package/dist/demo/footer-page-size/index.js.map +1 -0
- package/dist/demo/footer-page-size/table.d.ts +3 -0
- package/dist/demo/footer-page-size/table.d.ts.map +1 -0
- package/dist/demo/footer-page-size/table.js +18 -0
- package/dist/demo/footer-page-size/table.js.map +1 -0
- package/dist/demo/index.d.ts +1 -0
- package/dist/demo/index.d.ts.map +1 -1
- package/dist/demo/index.js +1 -0
- package/dist/demo/index.js.map +1 -1
- package/dist/demo/overview/product.d.ts +5 -0
- package/dist/demo/overview/product.d.ts.map +1 -1
- package/dist/demo/overview/products.d.ts.map +1 -1
- package/dist/demo/overview/products.js +12 -0
- package/dist/demo/overview/products.js.map +1 -1
- package/dist/demo/overview/table.store.d.ts.map +1 -1
- package/dist/demo/overview/table.store.js +1 -0
- package/dist/demo/overview/table.store.js.map +1 -1
- package/dist/filters/async-select/async-select-filter.d.ts +7 -3
- package/dist/filters/async-select/async-select-filter.d.ts.map +1 -1
- package/dist/filters/async-select/async-select-filter.js +21 -6
- package/dist/filters/async-select/async-select-filter.js.map +1 -1
- package/dist/filters/column-menu-filters.d.ts +10 -1
- package/dist/filters/column-menu-filters.d.ts.map +1 -1
- package/dist/filters/column-menu-filters.js +11 -3
- package/dist/filters/column-menu-filters.js.map +1 -1
- package/dist/filters/index.d.ts +1 -0
- package/dist/filters/index.d.ts.map +1 -1
- package/dist/filters/index.js +1 -0
- package/dist/filters/index.js.map +1 -1
- package/dist/filters/multiselect-filter/multiselect-filter.d.ts +4 -3
- package/dist/filters/multiselect-filter/multiselect-filter.d.ts.map +1 -1
- package/dist/filters/multiselect-filter/multiselect-filter.js +7 -7
- package/dist/filters/multiselect-filter/multiselect-filter.js.map +1 -1
- package/dist/filters/select-filter/object-search.d.ts +2 -0
- package/dist/filters/select-filter/object-search.d.ts.map +1 -0
- package/dist/filters/select-filter/object-search.js +19 -0
- package/dist/filters/select-filter/object-search.js.map +1 -0
- package/dist/filters/select-filter/select-filter.d.ts +32 -0
- package/dist/filters/select-filter/select-filter.d.ts.map +1 -0
- package/dist/filters/select-filter/select-filter.js +214 -0
- package/dist/filters/select-filter/select-filter.js.map +1 -0
- package/dist/filters/select-filter/select-filter.stories.d.ts +8 -0
- package/dist/filters/select-filter/select-filter.stories.d.ts.map +1 -0
- package/dist/filters/select-filter/select-filter.stories.js +8 -0
- package/dist/filters/select-filter/select-filter.stories.js.map +1 -0
- package/dist/filters/select-filter/value-getter.d.ts +3 -0
- package/dist/filters/select-filter/value-getter.d.ts.map +1 -0
- package/dist/filters/select-filter/value-getter.js +10 -0
- package/dist/filters/select-filter/value-getter.js.map +1 -0
- package/dist/filters/single-select/single-select-filter.d.ts +3 -2
- package/dist/filters/single-select/single-select-filter.d.ts.map +1 -1
- package/dist/filters/single-select/single-select-filter.js +17 -7
- package/dist/filters/single-select/single-select-filter.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/demo/filters/async-select-filter.tsx +1 -1
- package/src/demo/filters/categories.tsx +24 -0
- package/src/demo/filters/multiselect-filter.tsx +15 -0
- package/src/demo/filters/select-filter.tsx +147 -0
- package/src/demo/filters/single-select-filter.tsx +2 -1
- package/src/demo/filters/table.store.ts +45 -2
- package/src/demo/footer-page-size/index.ts +1 -0
- package/src/demo/footer-page-size/table.tsx +104 -0
- package/src/demo/index.ts +1 -0
- package/src/demo/overview/product.ts +6 -0
- package/src/demo/overview/products.ts +12 -0
- package/src/demo/overview/table.store.ts +1 -0
- package/src/filters/async-select/async-select-filter.tsx +26 -15
- package/src/filters/column-menu-filters.tsx +44 -21
- package/src/filters/index.ts +1 -0
- package/src/filters/multiselect-filter/multiselect-filter.tsx +16 -7
- package/src/filters/select-filter/__tests__/object-search.test.ts +32 -0
- package/src/filters/select-filter/object-search.ts +25 -0
- package/src/filters/select-filter/select-filter.stories.tsx +8 -0
- package/src/filters/select-filter/select-filter.tsx +320 -0
- package/src/filters/select-filter/value-getter.ts +13 -0
- package/src/filters/single-select/single-select-filter.tsx +18 -10
- package/src/index.ts +1 -0
- package/src/table.stories.tsx +4 -2
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
} from '@servicetitan/design-system';
|
|
11
11
|
import { IdType } from '@servicetitan/data-query';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
CustomColumnMenuFilterSingleOpts,
|
|
15
|
+
renderCustomColumnMenuFilter,
|
|
16
|
+
} from '../column-menu-filters';
|
|
14
17
|
import { makeObservable, observable, runInAction } from 'mobx';
|
|
15
18
|
import { observer } from 'mobx-react';
|
|
16
19
|
|
|
@@ -25,7 +28,7 @@ export interface AsyncSelectItem<TV extends IdType = IdType> {
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
interface SelectorProps<TID extends IdType, TO extends AsyncSelectItem<TID>> {
|
|
28
|
-
placeholder
|
|
31
|
+
placeholder?: string;
|
|
29
32
|
selected: TO[];
|
|
30
33
|
dataFetcher: AsyncSelectFilterDataFetcher<TID, TO>;
|
|
31
34
|
itemComponent: FC<SelectorItemProps>;
|
|
@@ -174,9 +177,10 @@ const SelectorItemMultiple: FC<SelectorItemProps> = ({ option, renderer, checked
|
|
|
174
177
|
/>
|
|
175
178
|
);
|
|
176
179
|
|
|
177
|
-
export interface AsyncSelectFilterOptions<TID extends IdType, TO extends AsyncSelectItem<TID>>
|
|
180
|
+
export interface AsyncSelectFilterOptions<TID extends IdType, TO extends AsyncSelectItem<TID>>
|
|
181
|
+
extends CustomColumnMenuFilterSingleOpts {
|
|
178
182
|
dataFetcher: AsyncSelectFilterDataFetcher<TID, TO>;
|
|
179
|
-
placeholder
|
|
183
|
+
placeholder?: string;
|
|
180
184
|
multiple?: boolean;
|
|
181
185
|
renderItem?: (item: TO) => ReactNode;
|
|
182
186
|
}
|
|
@@ -185,13 +189,20 @@ interface TableFilterCellPropsTyped<T = any> extends Omit<TableFilterCellProps,
|
|
|
185
189
|
value?: T;
|
|
186
190
|
}
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
/**
|
|
193
|
+
* @deprecated use selectColumnMenuFilter instead
|
|
194
|
+
*/
|
|
195
|
+
export function asyncSelectColumnMenuFilter<TID extends IdType, TO extends AsyncSelectItem<TID>>({
|
|
196
|
+
dataFetcher,
|
|
197
|
+
placeholder,
|
|
198
|
+
multiple,
|
|
199
|
+
renderItem,
|
|
200
|
+
...opts
|
|
201
|
+
}: AsyncSelectFilterOptions<TID, TO>) {
|
|
191
202
|
const contains = (value: TID, options?: TO[]) => options?.some(opt => opt.value === value);
|
|
192
203
|
const equals = (value: TID, option?: TO) => option?.value === value;
|
|
193
204
|
|
|
194
|
-
const FilterCell =
|
|
205
|
+
const FilterCell = multiple
|
|
195
206
|
? ({ value, onChange }: TableFilterCellPropsTyped<TO[]>) => {
|
|
196
207
|
const handleChange = (
|
|
197
208
|
option: TO,
|
|
@@ -214,9 +225,9 @@ export function asyncSelectColumnMenuFilter<TID extends IdType, TO extends Async
|
|
|
214
225
|
selected={value ?? []}
|
|
215
226
|
itemComponent={SelectorItemMultiple}
|
|
216
227
|
onChange={handleChange}
|
|
217
|
-
placeholder={
|
|
218
|
-
renderer={
|
|
219
|
-
dataFetcher={
|
|
228
|
+
placeholder={placeholder}
|
|
229
|
+
renderer={renderItem}
|
|
230
|
+
dataFetcher={dataFetcher}
|
|
220
231
|
/>
|
|
221
232
|
);
|
|
222
233
|
}
|
|
@@ -238,12 +249,12 @@ export function asyncSelectColumnMenuFilter<TID extends IdType, TO extends Async
|
|
|
238
249
|
selected={value ? [value] : []}
|
|
239
250
|
onChange={handleChange}
|
|
240
251
|
itemComponent={SelectorItemSingle}
|
|
241
|
-
placeholder={
|
|
242
|
-
renderer={
|
|
243
|
-
dataFetcher={
|
|
252
|
+
placeholder={placeholder}
|
|
253
|
+
renderer={renderItem}
|
|
254
|
+
dataFetcher={dataFetcher}
|
|
244
255
|
/>
|
|
245
256
|
);
|
|
246
257
|
};
|
|
247
258
|
|
|
248
|
-
return renderCustomColumnMenuFilter(FilterCell);
|
|
259
|
+
return renderCustomColumnMenuFilter(FilterCell, opts);
|
|
249
260
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ComponentType, FC } from 'react';
|
|
1
|
+
import { ComponentType, CSSProperties, FC } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
TableColumnMenuFilter,
|
|
4
4
|
TableColumnMenuProps,
|
|
@@ -11,35 +11,58 @@ export const StandardColumnMenuFilter = (props: TableColumnMenuProps) => (
|
|
|
11
11
|
<TableColumnMenuFilter {...props} expanded />
|
|
12
12
|
);
|
|
13
13
|
|
|
14
|
+
export interface CustomColumnMenuFilterSingleOpts {
|
|
15
|
+
contentMaxHeight?: string;
|
|
16
|
+
contentClassName?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface CustomColumnMenuFilterDoubleOpts {
|
|
19
|
+
double?: boolean;
|
|
20
|
+
doubleContentClassName?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CustomColumnMenuFilterOpts = CustomColumnMenuFilterSingleOpts &
|
|
24
|
+
CustomColumnMenuFilterDoubleOpts;
|
|
25
|
+
|
|
14
26
|
export function renderCustomColumnMenuFilter(
|
|
15
27
|
FilterCell: ComponentType<TableFilterCellProps>,
|
|
16
|
-
|
|
28
|
+
doubleOrOptions?: boolean | CustomColumnMenuFilterOpts
|
|
17
29
|
) {
|
|
18
|
-
const
|
|
30
|
+
const opts: CustomColumnMenuFilterOpts =
|
|
31
|
+
typeof doubleOrOptions === 'boolean' ? { double: doubleOrOptions } : doubleOrOptions ?? {};
|
|
32
|
+
const FilterUI: FC<TableColumnMenuFilterUIProps> = ({
|
|
19
33
|
firstFilterProps,
|
|
20
34
|
secondFilterProps,
|
|
21
35
|
logicData,
|
|
22
36
|
logicValue,
|
|
23
37
|
onLogicChange,
|
|
24
|
-
}) =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
}) => {
|
|
39
|
+
const contentStyles: CSSProperties = {};
|
|
40
|
+
|
|
41
|
+
if (opts.contentMaxHeight) {
|
|
42
|
+
contentStyles.maxHeight = opts.contentMaxHeight;
|
|
43
|
+
contentStyles.overflowY = 'auto';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className={opts.contentClassName} style={contentStyles}>
|
|
48
|
+
<FilterCell {...firstFilterProps} />
|
|
49
|
+
{!!opts.double && (
|
|
50
|
+
<div className={opts.doubleContentClassName}>
|
|
51
|
+
<DropDownList
|
|
52
|
+
data={logicData}
|
|
53
|
+
value={logicValue}
|
|
54
|
+
onChange={onLogicChange}
|
|
55
|
+
className="k-filter-and m-b-1"
|
|
56
|
+
textField="text"
|
|
57
|
+
/>
|
|
58
|
+
<FilterCell {...secondFilterProps} />
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
41
64
|
|
|
42
65
|
return (props: TableColumnMenuProps) => (
|
|
43
|
-
<TableColumnMenuFilter {...props} filterUI={
|
|
66
|
+
<TableColumnMenuFilter {...props} filterUI={FilterUI} expanded />
|
|
44
67
|
);
|
|
45
68
|
}
|
package/src/filters/index.ts
CHANGED
|
@@ -8,3 +8,4 @@ export * from './multiselect-filter/multiselect-filter';
|
|
|
8
8
|
export * from './numeric-filter-extended/numeric-filter-extended';
|
|
9
9
|
export * from './single-select/single-select-filter';
|
|
10
10
|
export * from './async-select/async-select-filter';
|
|
11
|
+
export * from './select-filter/select-filter';
|
|
@@ -2,7 +2,10 @@ import { Component, SyntheticEvent, ReactNode, FC } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { TableFilterCellProps, Checkbox } from '@servicetitan/design-system';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
CustomColumnMenuFilterSingleOpts,
|
|
7
|
+
renderCustomColumnMenuFilter,
|
|
8
|
+
} from '../column-menu-filters';
|
|
6
9
|
|
|
7
10
|
interface SelectorProps<T> {
|
|
8
11
|
options: T[];
|
|
@@ -37,7 +40,11 @@ class Selector<T> extends Component<SelectorProps<T>> {
|
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
export function multiSelectColumnMenuFilter<T>(
|
|
43
|
+
export function multiSelectColumnMenuFilter<T>(
|
|
44
|
+
data: T[],
|
|
45
|
+
renderItem?: (item: T) => ReactNode,
|
|
46
|
+
opts?: CustomColumnMenuFilterSingleOpts
|
|
47
|
+
) {
|
|
41
48
|
const FilterCell: FC<TableFilterCellProps> = ({ value, onChange }) => {
|
|
42
49
|
const handleChange = (value: T[] | undefined, event: SyntheticEvent<HTMLInputElement>) => {
|
|
43
50
|
onChange({
|
|
@@ -57,12 +64,13 @@ export function multiSelectColumnMenuFilter<T>(data: T[], renderItem?: (item: T)
|
|
|
57
64
|
);
|
|
58
65
|
};
|
|
59
66
|
|
|
60
|
-
return renderCustomColumnMenuFilter(FilterCell);
|
|
67
|
+
return renderCustomColumnMenuFilter(FilterCell, opts);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export function complexItemMultiSelectColumnMenuFilter<T>(
|
|
64
71
|
data: T[],
|
|
65
|
-
renderItem?: (item: T) => ReactNode
|
|
72
|
+
renderItem?: (item: T) => ReactNode,
|
|
73
|
+
opts?: CustomColumnMenuFilterSingleOpts
|
|
66
74
|
) {
|
|
67
75
|
class FilterCell extends Component<TableFilterCellProps> {
|
|
68
76
|
contains = (item: any) => {
|
|
@@ -93,12 +101,13 @@ export function complexItemMultiSelectColumnMenuFilter<T>(
|
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
return renderCustomColumnMenuFilter(FilterCell);
|
|
104
|
+
return renderCustomColumnMenuFilter(FilterCell, opts);
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
export function multiItemMultiSelectColumnMenuFilter<T>(
|
|
100
108
|
data: T[],
|
|
101
|
-
renderItem?: (item: T) => ReactNode
|
|
109
|
+
renderItem?: (item: T) => ReactNode,
|
|
110
|
+
opts?: CustomColumnMenuFilterSingleOpts
|
|
102
111
|
) {
|
|
103
112
|
class FilterCell extends Component<TableFilterCellProps> {
|
|
104
113
|
contains = (item: any) => {
|
|
@@ -129,5 +138,5 @@ export function multiItemMultiSelectColumnMenuFilter<T>(
|
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
return renderCustomColumnMenuFilter(FilterCell);
|
|
141
|
+
return renderCustomColumnMenuFilter(FilterCell, opts);
|
|
133
142
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { objectSearch } from '../object-search';
|
|
2
|
+
|
|
3
|
+
describe('objectSearch', () => {
|
|
4
|
+
const obj = () => ({
|
|
5
|
+
id: 12345,
|
|
6
|
+
name: 'Some Name',
|
|
7
|
+
active: true,
|
|
8
|
+
category: null,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('search and find in simple string', () => {
|
|
12
|
+
expect(objectSearch('iMPl')('sImplE String')).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
test('search and not find in simple string', () => {
|
|
15
|
+
expect(objectSearch('iMoPl')('sImplE String')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('search simple number', () => {
|
|
19
|
+
expect(objectSearch('23')(12345)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('search null', () => {
|
|
23
|
+
expect(objectSearch('23')(null)).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('search and find in object', () => {
|
|
27
|
+
expect(objectSearch('Me ')(obj())).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
test('search and not find in object', () => {
|
|
30
|
+
expect(objectSearch('Mee')(obj)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const objectSearch = (search: string) => {
|
|
2
|
+
const s = search.toLowerCase().trim();
|
|
3
|
+
|
|
4
|
+
return (item: any): boolean => {
|
|
5
|
+
if (!s) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (item === null) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof item === 'string') {
|
|
14
|
+
return item.toLowerCase().includes(s);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof item === 'object') {
|
|
18
|
+
return Object.values(item).some(value =>
|
|
19
|
+
typeof value === 'string' ? value.toLowerCase().includes(s) : false
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return false;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SelectAsyncExample, SelectSyncExample } from '../../demo/filters/select-filter';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Table/Filters/Select',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const AsyncSelect = () => <SelectAsyncExample />;
|
|
8
|
+
export const SyncSelect = () => <SelectSyncExample />;
|
|
@@ -0,0 +1,320 @@
|
|
|
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 {
|
|
14
|
+
CustomColumnMenuFilterSingleOpts,
|
|
15
|
+
renderCustomColumnMenuFilter,
|
|
16
|
+
} from '../column-menu-filters';
|
|
17
|
+
import { makeObservable, observable, runInAction, toJS } from 'mobx';
|
|
18
|
+
import { observer } from 'mobx-react';
|
|
19
|
+
import { getSimpleValue } from './value-getter';
|
|
20
|
+
import { objectSearch } from './object-search';
|
|
21
|
+
|
|
22
|
+
export type SelectFilterDataFetcher<TO> = (opts: { search?: string }) => Promise<{ data: TO[] }>;
|
|
23
|
+
|
|
24
|
+
export interface SelectFilterSearchOptions<TO = any> {
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
filter(search: string): (item: TO) => boolean | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SelectorProps<TO> {
|
|
30
|
+
search?: SelectFilterSearchOptions<TO>;
|
|
31
|
+
selected: TO[];
|
|
32
|
+
data?: TO[];
|
|
33
|
+
dataFetcher?: SelectFilterDataFetcher<TO>;
|
|
34
|
+
itemComponent: FC<SelectorItemProps<TO>>;
|
|
35
|
+
onChange(option: TO, checked: boolean, event: SyntheticEvent<HTMLInputElement>): void;
|
|
36
|
+
valueSelector(item: TO): IdType;
|
|
37
|
+
renderItem(item: TO): ReactNode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SelectorItemProps<TO> {
|
|
41
|
+
option: TO;
|
|
42
|
+
checked: boolean;
|
|
43
|
+
onChange(option: TO, checked: boolean, event: SyntheticEvent<HTMLInputElement>): void;
|
|
44
|
+
renderer?(item: TO): ReactNode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@observer
|
|
48
|
+
class SelectorAsync<TO> extends Component<SelectorProps<TO>> {
|
|
49
|
+
@observable shownOptions: TO[] = [];
|
|
50
|
+
@observable search = '';
|
|
51
|
+
@observable error = false;
|
|
52
|
+
@observable loading = false;
|
|
53
|
+
|
|
54
|
+
constructor(props: SelectorProps<TO>) {
|
|
55
|
+
super(props);
|
|
56
|
+
makeObservable(this);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
componentDidMount() {
|
|
60
|
+
this.searchOptions().catch();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
64
|
+
if (event.key === 'Enter') {
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
handleSearch = (_0: SyntheticEvent<HTMLInputElement>, data: { value: string }) => {
|
|
70
|
+
runInAction(() => (this.search = data.value));
|
|
71
|
+
this.searchOptions().catch();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
searchOptions = async () => {
|
|
75
|
+
if (this.props.dataFetcher) {
|
|
76
|
+
runInAction(() => {
|
|
77
|
+
this.loading = true;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const data = await this.getData();
|
|
83
|
+
|
|
84
|
+
runInAction(() => {
|
|
85
|
+
this.shownOptions = data;
|
|
86
|
+
this.error = false;
|
|
87
|
+
this.loading = false;
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
runInAction(() => {
|
|
91
|
+
this.error = true;
|
|
92
|
+
this.loading = false;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
render() {
|
|
98
|
+
const selectedOptions = this.props.selected;
|
|
99
|
+
const selected = new Set(selectedOptions.map(opt => this.props.valueSelector(opt)));
|
|
100
|
+
const ItemComponent = this.props.itemComponent;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Fragment>
|
|
104
|
+
{!!this.props.search && (
|
|
105
|
+
<Input
|
|
106
|
+
className="m-x-half m-t-half m-b-2"
|
|
107
|
+
placeholder={this.props.search?.placeholder}
|
|
108
|
+
onChange={this.handleSearch}
|
|
109
|
+
onKeyDown={this.handleKeyDown}
|
|
110
|
+
size="xsmall"
|
|
111
|
+
value={this.search}
|
|
112
|
+
/>
|
|
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, index) => (
|
|
118
|
+
<ItemComponent
|
|
119
|
+
key={this.getItemKey(option, index)}
|
|
120
|
+
option={option}
|
|
121
|
+
checked
|
|
122
|
+
renderer={this.props.renderItem}
|
|
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(this.props.valueSelector(opt)))
|
|
133
|
+
.map((option, index) => (
|
|
134
|
+
<ItemComponent
|
|
135
|
+
key={this.getItemKey(option, index)}
|
|
136
|
+
option={option}
|
|
137
|
+
checked={false}
|
|
138
|
+
renderer={this.props.renderItem}
|
|
139
|
+
onChange={this.props.onChange}
|
|
140
|
+
/>
|
|
141
|
+
))
|
|
142
|
+
) : this.loading ? (
|
|
143
|
+
<BodyText> </BodyText>
|
|
144
|
+
) : (
|
|
145
|
+
<BodyText subdued>No options match search criteria</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
|
+
private async getData(): Promise<TO[]> {
|
|
159
|
+
if (this.props.dataFetcher) {
|
|
160
|
+
const { data } = await this.props.dataFetcher({
|
|
161
|
+
search: this.search,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return data;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let data = this.props.data ?? [];
|
|
168
|
+
|
|
169
|
+
if (this.props.search?.filter) {
|
|
170
|
+
data = data.filter(this.props.search.filter(this.search));
|
|
171
|
+
}
|
|
172
|
+
return data;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private getItemKey = (item: TO, index: number) => {
|
|
176
|
+
return `${index}__${this.props.valueSelector(item)}`;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const SelectorItemSingle: FC<SelectorItemProps<any>> = ({
|
|
181
|
+
option,
|
|
182
|
+
renderer,
|
|
183
|
+
checked,
|
|
184
|
+
onChange,
|
|
185
|
+
}) => (
|
|
186
|
+
<Radio
|
|
187
|
+
label={renderer?.(option) ?? `${option}`}
|
|
188
|
+
checked={checked}
|
|
189
|
+
onChange={(_, event) => onChange(option, true, event)}
|
|
190
|
+
className="m-b-1"
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const SelectorItemMultiple: FC<SelectorItemProps<any>> = ({
|
|
195
|
+
option,
|
|
196
|
+
renderer,
|
|
197
|
+
checked,
|
|
198
|
+
onChange,
|
|
199
|
+
}) => (
|
|
200
|
+
<Checkbox
|
|
201
|
+
label={renderer?.(option) ?? `${option}`}
|
|
202
|
+
checked={checked}
|
|
203
|
+
onChange={(_, checked, event) => onChange(option, checked, event)}
|
|
204
|
+
className="m-b-1"
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
export interface SelectFilterOptions<TO> extends CustomColumnMenuFilterSingleOpts {
|
|
209
|
+
/** Can select multiple options in filter */
|
|
210
|
+
multiple?: boolean;
|
|
211
|
+
/** Ability to search options in filter */
|
|
212
|
+
search?: boolean | Partial<SelectFilterSearchOptions>;
|
|
213
|
+
/** Static options to show in filter */
|
|
214
|
+
data?: TO[];
|
|
215
|
+
/** Method to fetch filter options asynchronously */
|
|
216
|
+
dataFetcher?: SelectFilterDataFetcher<TO>;
|
|
217
|
+
/** Search operator passed to table state */
|
|
218
|
+
operator?: ((value: any, options?: TO[]) => boolean) | ((value: any, options?: TO) => boolean);
|
|
219
|
+
/** Select item value (ex id) for complex items */
|
|
220
|
+
valueSelector?(item: TO): IdType;
|
|
221
|
+
/** Select row item value (from table source row field) for complex items */
|
|
222
|
+
rowValueSelector?(item: any): IdType | undefined;
|
|
223
|
+
/** Render option label */
|
|
224
|
+
renderItem?(item: TO): ReactNode;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface TableFilterCellPropsTyped<T = any> extends Omit<TableFilterCellProps, 'value'> {
|
|
228
|
+
value?: T;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function selectColumnMenuFilter<TO>({
|
|
232
|
+
dataFetcher,
|
|
233
|
+
data,
|
|
234
|
+
search,
|
|
235
|
+
multiple,
|
|
236
|
+
valueSelector = getSimpleValue,
|
|
237
|
+
rowValueSelector = getSimpleValue,
|
|
238
|
+
renderItem,
|
|
239
|
+
operator,
|
|
240
|
+
...opts
|
|
241
|
+
}: SelectFilterOptions<TO>) {
|
|
242
|
+
const renderer = renderItem ?? (item => valueSelector(item));
|
|
243
|
+
|
|
244
|
+
const contains = (value: any, options?: TO[]) =>
|
|
245
|
+
options?.some(opt => valueSelector(opt) === rowValueSelector(value));
|
|
246
|
+
|
|
247
|
+
const equals = (value: any, option?: TO) =>
|
|
248
|
+
option === undefined ? false : valueSelector(option) === rowValueSelector(value);
|
|
249
|
+
|
|
250
|
+
const searchOptions = search
|
|
251
|
+
? {
|
|
252
|
+
filter: objectSearch,
|
|
253
|
+
...(typeof search === 'boolean' ? {} : search),
|
|
254
|
+
}
|
|
255
|
+
: undefined;
|
|
256
|
+
|
|
257
|
+
if (multiple) {
|
|
258
|
+
const FilterCell = ({ value, onChange }: TableFilterCellPropsTyped<TO[]>) => {
|
|
259
|
+
const handleChange = (
|
|
260
|
+
option: TO,
|
|
261
|
+
checked: boolean,
|
|
262
|
+
event: SyntheticEvent<HTMLInputElement>
|
|
263
|
+
) => {
|
|
264
|
+
const val = checked
|
|
265
|
+
? (value ?? []).concat(option)
|
|
266
|
+
: (value ?? []).filter(opt =>
|
|
267
|
+
valueSelector
|
|
268
|
+
? valueSelector(opt) !== valueSelector(option)
|
|
269
|
+
: option !== opt
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
onChange({
|
|
273
|
+
value: val.length ? toJS(val) : undefined,
|
|
274
|
+
operator: val.length ? operator ?? contains : '',
|
|
275
|
+
syntheticEvent: event,
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<SelectorAsync
|
|
281
|
+
selected={value ?? []}
|
|
282
|
+
itemComponent={SelectorItemMultiple}
|
|
283
|
+
onChange={handleChange}
|
|
284
|
+
search={searchOptions}
|
|
285
|
+
renderItem={renderer}
|
|
286
|
+
dataFetcher={dataFetcher}
|
|
287
|
+
data={data}
|
|
288
|
+
valueSelector={valueSelector}
|
|
289
|
+
/>
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
return renderCustomColumnMenuFilter(FilterCell, opts);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const FilterCell = ({ value, onChange }: TableFilterCellPropsTyped<TO>) => {
|
|
297
|
+
const handleChange = (option: TO, _: boolean, event: SyntheticEvent<HTMLInputElement>) => {
|
|
298
|
+
onChange({
|
|
299
|
+
value: toJS(option),
|
|
300
|
+
operator: operator ?? equals,
|
|
301
|
+
syntheticEvent: event,
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<SelectorAsync
|
|
307
|
+
selected={value ? [value] : []}
|
|
308
|
+
onChange={handleChange}
|
|
309
|
+
itemComponent={SelectorItemSingle}
|
|
310
|
+
search={searchOptions}
|
|
311
|
+
renderItem={renderer}
|
|
312
|
+
dataFetcher={dataFetcher}
|
|
313
|
+
data={data}
|
|
314
|
+
valueSelector={valueSelector}
|
|
315
|
+
/>
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return renderCustomColumnMenuFilter(FilterCell, opts);
|
|
320
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { IdType } from '@servicetitan/data-query';
|
|
2
|
+
|
|
3
|
+
export const getSimpleValue = (item: any): IdType => {
|
|
4
|
+
if (item === null) {
|
|
5
|
+
return item;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof item === 'object' || typeof item === 'function') {
|
|
9
|
+
return `${item}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return item;
|
|
13
|
+
};
|