@servicetitan/table 25.0.1 → 25.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. package/dist/demo/filters/async-select-filter.js +1 -1
  2. package/dist/demo/filters/async-select-filter.js.map +1 -1
  3. package/dist/demo/filters/categories.d.ts.map +1 -1
  4. package/dist/demo/filters/categories.js +24 -0
  5. package/dist/demo/filters/categories.js.map +1 -1
  6. package/dist/demo/filters/multiselect-filter.d.ts.map +1 -1
  7. package/dist/demo/filters/multiselect-filter.js +2 -1
  8. package/dist/demo/filters/multiselect-filter.js.map +1 -1
  9. package/dist/demo/filters/select-filter.d.ts +6 -0
  10. package/dist/demo/filters/select-filter.d.ts.map +1 -0
  11. package/dist/demo/filters/select-filter.js +58 -0
  12. package/dist/demo/filters/select-filter.js.map +1 -0
  13. package/dist/demo/filters/single-select-filter.d.ts.map +1 -1
  14. package/dist/demo/filters/single-select-filter.js +2 -1
  15. package/dist/demo/filters/single-select-filter.js.map +1 -1
  16. package/dist/demo/filters/table.store.d.ts +8 -1
  17. package/dist/demo/filters/table.store.d.ts.map +1 -1
  18. package/dist/demo/filters/table.store.js +35 -1
  19. package/dist/demo/filters/table.store.js.map +1 -1
  20. package/dist/demo/footer-page-size/index.d.ts +2 -0
  21. package/dist/demo/footer-page-size/index.d.ts.map +1 -0
  22. package/dist/demo/footer-page-size/index.js +2 -0
  23. package/dist/demo/footer-page-size/index.js.map +1 -0
  24. package/dist/demo/footer-page-size/table.d.ts +3 -0
  25. package/dist/demo/footer-page-size/table.d.ts.map +1 -0
  26. package/dist/demo/footer-page-size/table.js +18 -0
  27. package/dist/demo/footer-page-size/table.js.map +1 -0
  28. package/dist/demo/index.d.ts +1 -0
  29. package/dist/demo/index.d.ts.map +1 -1
  30. package/dist/demo/index.js +1 -0
  31. package/dist/demo/index.js.map +1 -1
  32. package/dist/demo/overview/product.d.ts +5 -0
  33. package/dist/demo/overview/product.d.ts.map +1 -1
  34. package/dist/demo/overview/products.d.ts.map +1 -1
  35. package/dist/demo/overview/products.js +12 -0
  36. package/dist/demo/overview/products.js.map +1 -1
  37. package/dist/demo/overview/table.store.d.ts.map +1 -1
  38. package/dist/demo/overview/table.store.js +1 -0
  39. package/dist/demo/overview/table.store.js.map +1 -1
  40. package/dist/filters/async-select/async-select-filter.d.ts +7 -3
  41. package/dist/filters/async-select/async-select-filter.d.ts.map +1 -1
  42. package/dist/filters/async-select/async-select-filter.js +21 -6
  43. package/dist/filters/async-select/async-select-filter.js.map +1 -1
  44. package/dist/filters/column-menu-filters.d.ts +10 -1
  45. package/dist/filters/column-menu-filters.d.ts.map +1 -1
  46. package/dist/filters/column-menu-filters.js +11 -3
  47. package/dist/filters/column-menu-filters.js.map +1 -1
  48. package/dist/filters/index.d.ts +1 -0
  49. package/dist/filters/index.d.ts.map +1 -1
  50. package/dist/filters/index.js +1 -0
  51. package/dist/filters/index.js.map +1 -1
  52. package/dist/filters/multiselect-filter/multiselect-filter.d.ts +4 -3
  53. package/dist/filters/multiselect-filter/multiselect-filter.d.ts.map +1 -1
  54. package/dist/filters/multiselect-filter/multiselect-filter.js +7 -7
  55. package/dist/filters/multiselect-filter/multiselect-filter.js.map +1 -1
  56. package/dist/filters/select-filter/object-search.d.ts +2 -0
  57. package/dist/filters/select-filter/object-search.d.ts.map +1 -0
  58. package/dist/filters/select-filter/object-search.js +19 -0
  59. package/dist/filters/select-filter/object-search.js.map +1 -0
  60. package/dist/filters/select-filter/select-filter.d.ts +32 -0
  61. package/dist/filters/select-filter/select-filter.d.ts.map +1 -0
  62. package/dist/filters/select-filter/select-filter.js +214 -0
  63. package/dist/filters/select-filter/select-filter.js.map +1 -0
  64. package/dist/filters/select-filter/select-filter.stories.d.ts +8 -0
  65. package/dist/filters/select-filter/select-filter.stories.d.ts.map +1 -0
  66. package/dist/filters/select-filter/select-filter.stories.js +8 -0
  67. package/dist/filters/select-filter/select-filter.stories.js.map +1 -0
  68. package/dist/filters/select-filter/value-getter.d.ts +3 -0
  69. package/dist/filters/select-filter/value-getter.d.ts.map +1 -0
  70. package/dist/filters/select-filter/value-getter.js +10 -0
  71. package/dist/filters/select-filter/value-getter.js.map +1 -0
  72. package/dist/filters/single-select/single-select-filter.d.ts +3 -2
  73. package/dist/filters/single-select/single-select-filter.d.ts.map +1 -1
  74. package/dist/filters/single-select/single-select-filter.js +17 -7
  75. package/dist/filters/single-select/single-select-filter.js.map +1 -1
  76. package/dist/index.d.ts +1 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +1 -1
  79. package/dist/index.js.map +1 -1
  80. package/package.json +6 -6
  81. package/src/demo/filters/async-select-filter.tsx +1 -1
  82. package/src/demo/filters/categories.tsx +24 -0
  83. package/src/demo/filters/multiselect-filter.tsx +15 -0
  84. package/src/demo/filters/select-filter.tsx +147 -0
  85. package/src/demo/filters/single-select-filter.tsx +2 -1
  86. package/src/demo/filters/table.store.ts +45 -2
  87. package/src/demo/footer-page-size/index.ts +1 -0
  88. package/src/demo/footer-page-size/table.tsx +104 -0
  89. package/src/demo/index.ts +1 -0
  90. package/src/demo/overview/product.ts +6 -0
  91. package/src/demo/overview/products.ts +12 -0
  92. package/src/demo/overview/table.store.ts +1 -0
  93. package/src/filters/async-select/async-select-filter.tsx +26 -15
  94. package/src/filters/column-menu-filters.tsx +44 -21
  95. package/src/filters/index.ts +1 -0
  96. package/src/filters/multiselect-filter/multiselect-filter.tsx +16 -7
  97. package/src/filters/select-filter/__tests__/object-search.test.ts +32 -0
  98. package/src/filters/select-filter/object-search.ts +25 -0
  99. package/src/filters/select-filter/select-filter.stories.tsx +8 -0
  100. package/src/filters/select-filter/select-filter.tsx +320 -0
  101. package/src/filters/select-filter/value-getter.ts +13 -0
  102. package/src/filters/single-select/single-select-filter.tsx +18 -10
  103. package/src/index.ts +1 -0
  104. 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 { renderCustomColumnMenuFilter } from '../column-menu-filters';
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: string;
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: string;
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
- export function asyncSelectColumnMenuFilter<TID extends IdType, TO extends AsyncSelectItem<TID>>(
189
- options: AsyncSelectFilterOptions<TID, TO>
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 = options.multiple
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={options.placeholder}
218
- renderer={options.renderItem}
219
- dataFetcher={options.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={options.placeholder}
242
- renderer={options.renderItem}
243
- dataFetcher={options.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
- double?: boolean
28
+ doubleOrOptions?: boolean | CustomColumnMenuFilterOpts
17
29
  ) {
18
- const filterUI: FC<TableColumnMenuFilterUIProps> = ({
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
- <div>
26
- <FilterCell {...firstFilterProps} />
27
- {double && (
28
- <div>
29
- <DropDownList
30
- data={logicData}
31
- value={logicValue}
32
- onChange={onLogicChange}
33
- className="k-filter-and m-b-1"
34
- textField="text"
35
- />
36
- <FilterCell {...secondFilterProps} />
37
- </div>
38
- )}
39
- </div>
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={filterUI} expanded />
66
+ <TableColumnMenuFilter {...props} filterUI={FilterUI} expanded />
44
67
  );
45
68
  }
@@ -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 { renderCustomColumnMenuFilter } from '../column-menu-filters';
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>(data: T[], renderItem?: (item: T) => ReactNode) {
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>&nbsp;</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
+ };