@servicetitan/table 25.1.0 → 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 (88) 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/overview/product.d.ts +5 -0
  21. package/dist/demo/overview/product.d.ts.map +1 -1
  22. package/dist/demo/overview/products.d.ts.map +1 -1
  23. package/dist/demo/overview/products.js +12 -0
  24. package/dist/demo/overview/products.js.map +1 -1
  25. package/dist/demo/overview/table.store.d.ts.map +1 -1
  26. package/dist/demo/overview/table.store.js +1 -0
  27. package/dist/demo/overview/table.store.js.map +1 -1
  28. package/dist/filters/async-select/async-select-filter.d.ts +7 -3
  29. package/dist/filters/async-select/async-select-filter.d.ts.map +1 -1
  30. package/dist/filters/async-select/async-select-filter.js +21 -6
  31. package/dist/filters/async-select/async-select-filter.js.map +1 -1
  32. package/dist/filters/column-menu-filters.d.ts +10 -1
  33. package/dist/filters/column-menu-filters.d.ts.map +1 -1
  34. package/dist/filters/column-menu-filters.js +11 -3
  35. package/dist/filters/column-menu-filters.js.map +1 -1
  36. package/dist/filters/index.d.ts +1 -0
  37. package/dist/filters/index.d.ts.map +1 -1
  38. package/dist/filters/index.js +1 -0
  39. package/dist/filters/index.js.map +1 -1
  40. package/dist/filters/multiselect-filter/multiselect-filter.d.ts +4 -3
  41. package/dist/filters/multiselect-filter/multiselect-filter.d.ts.map +1 -1
  42. package/dist/filters/multiselect-filter/multiselect-filter.js +7 -7
  43. package/dist/filters/multiselect-filter/multiselect-filter.js.map +1 -1
  44. package/dist/filters/select-filter/object-search.d.ts +2 -0
  45. package/dist/filters/select-filter/object-search.d.ts.map +1 -0
  46. package/dist/filters/select-filter/object-search.js +19 -0
  47. package/dist/filters/select-filter/object-search.js.map +1 -0
  48. package/dist/filters/select-filter/select-filter.d.ts +32 -0
  49. package/dist/filters/select-filter/select-filter.d.ts.map +1 -0
  50. package/dist/filters/select-filter/select-filter.js +214 -0
  51. package/dist/filters/select-filter/select-filter.js.map +1 -0
  52. package/dist/filters/select-filter/select-filter.stories.d.ts +8 -0
  53. package/dist/filters/select-filter/select-filter.stories.d.ts.map +1 -0
  54. package/dist/filters/select-filter/select-filter.stories.js +8 -0
  55. package/dist/filters/select-filter/select-filter.stories.js.map +1 -0
  56. package/dist/filters/select-filter/value-getter.d.ts +3 -0
  57. package/dist/filters/select-filter/value-getter.d.ts.map +1 -0
  58. package/dist/filters/select-filter/value-getter.js +10 -0
  59. package/dist/filters/select-filter/value-getter.js.map +1 -0
  60. package/dist/filters/single-select/single-select-filter.d.ts +3 -2
  61. package/dist/filters/single-select/single-select-filter.d.ts.map +1 -1
  62. package/dist/filters/single-select/single-select-filter.js +17 -7
  63. package/dist/filters/single-select/single-select-filter.js.map +1 -1
  64. package/dist/index.d.ts +1 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -1
  67. package/dist/index.js.map +1 -1
  68. package/package.json +6 -6
  69. package/src/demo/filters/async-select-filter.tsx +1 -1
  70. package/src/demo/filters/categories.tsx +24 -0
  71. package/src/demo/filters/multiselect-filter.tsx +15 -0
  72. package/src/demo/filters/select-filter.tsx +147 -0
  73. package/src/demo/filters/single-select-filter.tsx +2 -1
  74. package/src/demo/filters/table.store.ts +45 -2
  75. package/src/demo/overview/product.ts +6 -0
  76. package/src/demo/overview/products.ts +12 -0
  77. package/src/demo/overview/table.store.ts +1 -0
  78. package/src/filters/async-select/async-select-filter.tsx +26 -15
  79. package/src/filters/column-menu-filters.tsx +44 -21
  80. package/src/filters/index.ts +1 -0
  81. package/src/filters/multiselect-filter/multiselect-filter.tsx +16 -7
  82. package/src/filters/select-filter/__tests__/object-search.test.ts +32 -0
  83. package/src/filters/select-filter/object-search.ts +25 -0
  84. package/src/filters/select-filter/select-filter.stories.tsx +8 -0
  85. package/src/filters/select-filter/select-filter.tsx +320 -0
  86. package/src/filters/select-filter/value-getter.ts +13 -0
  87. package/src/filters/single-select/single-select-filter.tsx +18 -10
  88. package/src/index.ts +1 -0
@@ -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
+ };
@@ -3,7 +3,10 @@ import { Component, SyntheticEvent, ReactNode, FC } from 'react';
3
3
  import { IdType } from '@servicetitan/data-query';
4
4
  import { TableFilterCellProps, Radio } from '@servicetitan/design-system';
5
5
 
6
- import { renderCustomColumnMenuFilter } from '../column-menu-filters';
6
+ import {
7
+ CustomColumnMenuFilterSingleOpts,
8
+ renderCustomColumnMenuFilter,
9
+ } from '../column-menu-filters';
7
10
 
8
11
  interface SelectorProps<TV, TO> {
9
12
  options: TO[];
@@ -33,16 +36,21 @@ class SelectorRadio<TV, TO> extends Component<SelectorProps<TV, TO>> {
33
36
  }
34
37
  }
35
38
 
36
- export interface SingleSelectColumnMenuOptions<TV, TO = TV> {
39
+ export interface SingleSelectColumnMenuOptions<TV, TO = TV>
40
+ extends CustomColumnMenuFilterSingleOpts {
37
41
  options: TO[];
38
42
  valueSelector?: (item: TO) => TV;
39
43
  renderItem?: (item: TO) => ReactNode | string;
40
44
  filterOperator?: (listItem: any, value: TV) => boolean;
41
45
  }
42
46
 
43
- export function singleSelectColumnMenuFilter<TV extends IdType, TO = TV>(
44
- props: SingleSelectColumnMenuOptions<TV, TO>
45
- ) {
47
+ export function singleSelectColumnMenuFilter<TV extends IdType, TO = TV>({
48
+ options,
49
+ valueSelector,
50
+ renderItem,
51
+ filterOperator,
52
+ ...opts
53
+ }: SingleSelectColumnMenuOptions<TV, TO>) {
46
54
  const FilterCell: FC<TableFilterCellProps> = ({ value, onChange }) => {
47
55
  const handleChange = (value: TV | undefined, event: SyntheticEvent<HTMLInputElement>) => {
48
56
  const filter =
@@ -50,7 +58,7 @@ export function singleSelectColumnMenuFilter<TV extends IdType, TO = TV>(
50
58
  ? { value: '', operator: '' }
51
59
  : {
52
60
  value,
53
- operator: props.filterOperator ?? 'equals',
61
+ operator: filterOperator ?? 'equals',
54
62
  };
55
63
 
56
64
  onChange({
@@ -61,14 +69,14 @@ export function singleSelectColumnMenuFilter<TV extends IdType, TO = TV>(
61
69
 
62
70
  return (
63
71
  <SelectorRadio
64
- options={props.options}
72
+ options={options}
65
73
  value={value === '' ? undefined : value}
66
74
  onChange={handleChange}
67
- renderer={props.renderItem}
68
- valueSelector={props.valueSelector ?? (option => option)}
75
+ renderer={renderItem}
76
+ valueSelector={valueSelector ?? (option => option)}
69
77
  />
70
78
  );
71
79
  };
72
80
 
73
- return renderCustomColumnMenuFilter(FilterCell);
81
+ return renderCustomColumnMenuFilter(FilterCell, opts);
74
82
  }
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  TableColumnReorderEvent,
16
16
  TableCell,
17
17
  TableHeaderCellProps,
18
+ TableHeaderCell,
18
19
  TableRowProps,
19
20
  TableRowClickEvent,
20
21
  TableDetailRow,