@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.
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
+ };