@servicetitan/table 23.3.2 → 24.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. package/dist/demo/filters/async-select-filter.d.ts +3 -0
  2. package/dist/demo/filters/async-select-filter.d.ts.map +1 -0
  3. package/dist/demo/filters/async-select-filter.js +23 -0
  4. package/dist/demo/filters/async-select-filter.js.map +1 -0
  5. package/dist/demo/filters/categories.d.ts +9 -0
  6. package/dist/demo/filters/categories.d.ts.map +1 -0
  7. package/dist/demo/filters/categories.js +33 -0
  8. package/dist/demo/filters/categories.js.map +1 -0
  9. package/dist/demo/filters/single-select-filter.d.ts +3 -0
  10. package/dist/demo/filters/single-select-filter.d.ts.map +1 -0
  11. package/dist/demo/filters/single-select-filter.js +20 -0
  12. package/dist/demo/filters/single-select-filter.js.map +1 -0
  13. package/dist/demo/filters/table.store.d.ts +9 -0
  14. package/dist/demo/filters/table.store.d.ts.map +1 -0
  15. package/dist/demo/filters/table.store.js +73 -0
  16. package/dist/demo/filters/table.store.js.map +1 -0
  17. package/dist/filters/async-select/async-select-filter.d.ts +19 -0
  18. package/dist/filters/async-select/async-select-filter.d.ts.map +1 -0
  19. package/dist/filters/async-select/async-select-filter.js +168 -0
  20. package/dist/filters/async-select/async-select-filter.js.map +1 -0
  21. package/dist/filters/async-select/async-select-filter.stories.d.ts +7 -0
  22. package/dist/filters/async-select/async-select-filter.stories.d.ts.map +1 -0
  23. package/dist/filters/async-select/async-select-filter.stories.js +7 -0
  24. package/dist/filters/async-select/async-select-filter.stories.js.map +1 -0
  25. package/dist/filters/index.d.ts +2 -0
  26. package/dist/filters/index.d.ts.map +1 -1
  27. package/dist/filters/index.js +2 -0
  28. package/dist/filters/index.js.map +1 -1
  29. package/dist/filters/single-select/single-select-filter.d.ts +9 -0
  30. package/dist/filters/single-select/single-select-filter.d.ts.map +1 -0
  31. package/dist/filters/single-select/single-select-filter.js +30 -0
  32. package/dist/filters/single-select/single-select-filter.js.map +1 -0
  33. package/dist/filters/single-select/single-select-filter.stories.d.ts +7 -0
  34. package/dist/filters/single-select/single-select-filter.stories.d.ts.map +1 -0
  35. package/dist/filters/single-select/single-select-filter.stories.js +7 -0
  36. package/dist/filters/single-select/single-select-filter.stories.js.map +1 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/table-state.d.ts +1 -1
  42. package/dist/table-state.d.ts.map +1 -1
  43. package/dist/use-observing-table-state/demo/components/use-observing-table-state-demo.d.ts +7 -0
  44. package/dist/use-observing-table-state/demo/components/use-observing-table-state-demo.d.ts.map +1 -0
  45. package/dist/use-observing-table-state/demo/components/use-observing-table-state-demo.js +15 -0
  46. package/dist/use-observing-table-state/demo/components/use-observing-table-state-demo.js.map +1 -0
  47. package/dist/use-observing-table-state/demo/stores/use-observing-table-state-demo.store.d.ts +15 -0
  48. package/dist/use-observing-table-state/demo/stores/use-observing-table-state-demo.store.d.ts.map +1 -0
  49. package/dist/use-observing-table-state/demo/stores/use-observing-table-state-demo.store.js +128 -0
  50. package/dist/use-observing-table-state/demo/stores/use-observing-table-state-demo.store.js.map +1 -0
  51. package/dist/use-observing-table-state/index.d.ts +2 -0
  52. package/dist/use-observing-table-state/index.d.ts.map +1 -0
  53. package/dist/use-observing-table-state/index.js +2 -0
  54. package/dist/use-observing-table-state/index.js.map +1 -0
  55. package/dist/use-observing-table-state/use-observing-table-state.d.ts +13 -0
  56. package/dist/use-observing-table-state/use-observing-table-state.d.ts.map +1 -0
  57. package/dist/use-observing-table-state/use-observing-table-state.js +44 -0
  58. package/dist/use-observing-table-state/use-observing-table-state.js.map +1 -0
  59. package/dist/use-observing-table-state/use-observing-table-state.stories.d.ts +10 -0
  60. package/dist/use-observing-table-state/use-observing-table-state.stories.d.ts.map +1 -0
  61. package/dist/use-observing-table-state/use-observing-table-state.stories.js +11 -0
  62. package/dist/use-observing-table-state/use-observing-table-state.stories.js.map +1 -0
  63. package/dist/utils/filters.d.ts +4 -0
  64. package/dist/utils/filters.d.ts.map +1 -0
  65. package/dist/utils/filters.js +22 -0
  66. package/dist/utils/filters.js.map +1 -0
  67. package/package.json +7 -6
  68. package/src/demo/filters/async-select-filter.tsx +53 -0
  69. package/src/demo/filters/categories.tsx +40 -0
  70. package/src/demo/filters/single-select-filter.tsx +51 -0
  71. package/src/demo/filters/table.store.ts +45 -0
  72. package/src/filters/async-select/async-select-filter.stories.tsx +7 -0
  73. package/src/filters/async-select/async-select-filter.tsx +249 -0
  74. package/src/filters/index.ts +2 -0
  75. package/src/filters/single-select/single-select-filter.stories.tsx +7 -0
  76. package/src/filters/single-select/single-select-filter.tsx +66 -0
  77. package/src/index.ts +2 -0
  78. package/src/table-state.ts +1 -1
  79. package/src/use-observing-table-state/demo/components/use-observing-table-state-demo.tsx +82 -0
  80. package/src/use-observing-table-state/demo/stores/use-observing-table-state-demo.store.ts +69 -0
  81. package/src/use-observing-table-state/index.ts +1 -0
  82. package/src/use-observing-table-state/use-observing-table-state.stories.tsx +20 -0
  83. package/src/use-observing-table-state/use-observing-table-state.ts +69 -0
  84. package/src/utils/__tests__/filters.test.ts +31 -0
  85. package/src/utils/filters.ts +33 -0
@@ -0,0 +1,249 @@
1
+ import { Component, SyntheticEvent, ReactNode, Fragment, KeyboardEvent, FC } from 'react';
2
+
3
+ import {
4
+ Checkbox,
5
+ TableFilterCellProps,
6
+ Radio,
7
+ Input,
8
+ Spinner,
9
+ BodyText,
10
+ } from '@servicetitan/design-system';
11
+ import { IdType } from '@servicetitan/data-query';
12
+
13
+ import { renderCustomColumnMenuFilter } from '../column-menu-filters';
14
+ import { makeObservable, observable, runInAction } from 'mobx';
15
+ import { observer } from 'mobx-react';
16
+
17
+ export type AsyncSelectFilterDataFetcher<
18
+ TV extends IdType = IdType,
19
+ TO extends AsyncSelectItem<TV> = AsyncSelectItem<TV>
20
+ > = (opts: { search?: string }) => Promise<{ data: TO[] }>;
21
+
22
+ export interface AsyncSelectItem<TV extends IdType = IdType> {
23
+ value: TV;
24
+ text: string;
25
+ }
26
+
27
+ interface SelectorProps<TID extends IdType, TO extends AsyncSelectItem<TID>> {
28
+ placeholder: string;
29
+ selected: TO[];
30
+ dataFetcher: AsyncSelectFilterDataFetcher<TID, TO>;
31
+ itemComponent: FC<SelectorItemProps>;
32
+ onChange(option: TO, checked: boolean, event: SyntheticEvent<HTMLInputElement>): void;
33
+ renderer?(item: TO): ReactNode;
34
+ }
35
+
36
+ interface SelectorItemProps {
37
+ option: AsyncSelectItem;
38
+ checked: boolean;
39
+ onChange(
40
+ option: AsyncSelectItem,
41
+ checked: boolean,
42
+ event: SyntheticEvent<HTMLInputElement>
43
+ ): void;
44
+ renderer?(item: AsyncSelectItem): ReactNode;
45
+ }
46
+
47
+ @observer
48
+ class SelectorAsync<TID extends IdType, TO extends AsyncSelectItem<TID>> extends Component<
49
+ SelectorProps<TID, TO>
50
+ > {
51
+ @observable shownOptions: AsyncSelectItem<TID>[] = [];
52
+ @observable search = '';
53
+ @observable error = false;
54
+ @observable loading = false;
55
+
56
+ constructor(props: SelectorProps<TID, TO>) {
57
+ super(props);
58
+ makeObservable(this);
59
+ }
60
+
61
+ componentDidMount() {
62
+ this.searchOptions().catch();
63
+ }
64
+
65
+ handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
66
+ if (event.key === 'Enter') {
67
+ event.stopPropagation();
68
+ }
69
+ };
70
+
71
+ handleSearch = (_0: SyntheticEvent<HTMLInputElement>, data: { value: string }) => {
72
+ runInAction(() => (this.search = data.value));
73
+ this.searchOptions().catch();
74
+ };
75
+
76
+ searchOptions = async () => {
77
+ runInAction(() => {
78
+ this.loading = true;
79
+ });
80
+
81
+ try {
82
+ const { data } = await this.props.dataFetcher({
83
+ search: this.search,
84
+ });
85
+
86
+ runInAction(() => {
87
+ this.shownOptions = data;
88
+ this.error = false;
89
+ this.loading = false;
90
+ });
91
+ } catch {
92
+ runInAction(() => {
93
+ this.error = true;
94
+ this.loading = false;
95
+ });
96
+ }
97
+ };
98
+
99
+ render() {
100
+ const selectedOptions = this.props.selected;
101
+ const selected = new Set(selectedOptions.map(opt => opt.value));
102
+ const ItemComponent = this.props.itemComponent;
103
+
104
+ return (
105
+ <Fragment>
106
+ <Input
107
+ className="m-x-half m-t-half m-b-2"
108
+ placeholder={this.props.placeholder}
109
+ onChange={this.handleSearch}
110
+ onKeyDown={this.handleKeyDown}
111
+ size="xsmall"
112
+ value={this.search}
113
+ />
114
+ <div className="p-x-half position-relative" onKeyDown={this.handleKeyDown}>
115
+ {!!selectedOptions.length && (
116
+ <div className="border-bottom m-y-half">
117
+ {selectedOptions.map(option => (
118
+ <ItemComponent
119
+ key={option.value}
120
+ option={option}
121
+ checked
122
+ renderer={this.props.renderer}
123
+ onChange={this.props.onChange}
124
+ />
125
+ ))}
126
+ </div>
127
+ )}
128
+ {this.error ? (
129
+ <BodyText className="c-red-500">Unable to load options</BodyText>
130
+ ) : this.shownOptions.length ? (
131
+ this.shownOptions
132
+ .filter(opt => !selected.has(opt.value))
133
+ .map(option => (
134
+ <ItemComponent
135
+ key={option.value}
136
+ option={option}
137
+ checked={false}
138
+ renderer={this.props.renderer}
139
+ onChange={this.props.onChange}
140
+ />
141
+ ))
142
+ ) : this.loading ? (
143
+ <BodyText>&nbsp;</BodyText>
144
+ ) : (
145
+ <BodyText>No options found</BodyText>
146
+ )}
147
+
148
+ {this.loading && (
149
+ <div className="position-absolute top-0 bottom-0 left-0 right-0 opacity-disabled bg-white d-f justify-content-center">
150
+ <Spinner size="tiny" />
151
+ </div>
152
+ )}
153
+ </div>
154
+ </Fragment>
155
+ );
156
+ }
157
+ }
158
+
159
+ const SelectorItemSingle: FC<SelectorItemProps> = ({ option, renderer, checked, onChange }) => (
160
+ <Radio
161
+ label={renderer?.(option) ?? option.text}
162
+ checked={checked}
163
+ onChange={(_, event) => onChange(option, true, event)}
164
+ className="m-b-1"
165
+ />
166
+ );
167
+
168
+ const SelectorItemMultiple: FC<SelectorItemProps> = ({ option, renderer, checked, onChange }) => (
169
+ <Checkbox
170
+ label={renderer?.(option) ?? option.text}
171
+ checked={checked}
172
+ onChange={(_, checked, event) => onChange(option, checked, event)}
173
+ className="m-b-1"
174
+ />
175
+ );
176
+
177
+ export interface AsyncSelectFilterOptions<TID extends IdType, TO extends AsyncSelectItem<TID>> {
178
+ dataFetcher: AsyncSelectFilterDataFetcher<TID, TO>;
179
+ placeholder: string;
180
+ multiple?: boolean;
181
+ renderItem?: (item: TO) => ReactNode;
182
+ }
183
+
184
+ interface TableFilterCellPropsTyped<T = any> extends Omit<TableFilterCellProps, 'value'> {
185
+ value?: T;
186
+ }
187
+
188
+ export function asyncSelectColumnMenuFilter<TID extends IdType, TO extends AsyncSelectItem<TID>>(
189
+ options: AsyncSelectFilterOptions<TID, TO>
190
+ ) {
191
+ const contains = (value: TID, options?: TO[]) => options?.some(opt => opt.value === value);
192
+ const equals = (value: TID, option?: TO) => option?.value === value;
193
+
194
+ const FilterCell = options.multiple
195
+ ? ({ value, onChange }: TableFilterCellPropsTyped<TO[]>) => {
196
+ const handleChange = (
197
+ option: TO,
198
+ checked: boolean,
199
+ event: SyntheticEvent<HTMLInputElement>
200
+ ) => {
201
+ const val = checked
202
+ ? (value ?? []).concat(option)
203
+ : (value ?? []).filter(opt => opt.value !== option.value);
204
+
205
+ onChange({
206
+ value: val.length ? val : undefined,
207
+ operator: val.length ? contains : '',
208
+ syntheticEvent: event,
209
+ });
210
+ };
211
+
212
+ return (
213
+ <SelectorAsync
214
+ selected={value ?? []}
215
+ itemComponent={SelectorItemMultiple}
216
+ onChange={handleChange}
217
+ placeholder={options.placeholder}
218
+ renderer={options.renderItem}
219
+ dataFetcher={options.dataFetcher}
220
+ />
221
+ );
222
+ }
223
+ : ({ value, onChange }: TableFilterCellPropsTyped<TO>) => {
224
+ const handleChange = (
225
+ option: TO,
226
+ checked: boolean,
227
+ event: SyntheticEvent<HTMLInputElement>
228
+ ) => {
229
+ onChange({
230
+ value: option,
231
+ operator: option ? equals : '',
232
+ syntheticEvent: event,
233
+ });
234
+ };
235
+
236
+ return (
237
+ <SelectorAsync
238
+ selected={value ? [value] : []}
239
+ onChange={handleChange}
240
+ itemComponent={SelectorItemSingle}
241
+ placeholder={options.placeholder}
242
+ renderer={options.renderItem}
243
+ dataFetcher={options.dataFetcher}
244
+ />
245
+ );
246
+ };
247
+
248
+ return renderCustomColumnMenuFilter(FilterCell);
249
+ }
@@ -6,3 +6,5 @@ export * from './column-menu-filters';
6
6
  export * from './field-values-filter';
7
7
  export * from './multiselect-filter';
8
8
  export * from './numeric-filter-extended/numeric-filter-extended';
9
+ export * from './single-select/single-select-filter';
10
+ export * from './async-select/async-select-filter';
@@ -0,0 +1,7 @@
1
+ import { TableExample } from '../../demo/filters/single-select-filter';
2
+
3
+ export default {
4
+ title: 'Table/Filters',
5
+ };
6
+
7
+ export const SingleSelect = () => <TableExample />;
@@ -0,0 +1,66 @@
1
+ import { Component, SyntheticEvent, ReactNode, FC } from 'react';
2
+
3
+ import { TableFilterCellProps, Radio } from '@servicetitan/design-system';
4
+
5
+ import { renderCustomColumnMenuFilter } from '../column-menu-filters';
6
+ import { IdType } from '@servicetitan/data-query';
7
+
8
+ interface SelectorProps<TV, TO> {
9
+ options: TO[];
10
+ value?: TV;
11
+ valueSelector(item: TO): TV;
12
+ onChange(value: TV | undefined, event: SyntheticEvent<HTMLInputElement>): void;
13
+ renderer?(item: TO): ReactNode;
14
+ }
15
+
16
+ class SelectorRadio<TV, TO> extends Component<SelectorProps<TV, TO>> {
17
+ render() {
18
+ const { options, value, renderer } = this.props;
19
+
20
+ return options.map((option, index) => {
21
+ const optionValue = this.props.valueSelector(option);
22
+ return (
23
+ // eslint-disable-next-line react/no-array-index-key
24
+ <span key={index} className="k-widget m-b-1-i d-b">
25
+ <Radio
26
+ label={renderer ? renderer(option) : `${option}`}
27
+ checked={value === optionValue}
28
+ onChange={(_, event) => this.props.onChange(optionValue, event)}
29
+ />
30
+ </span>
31
+ );
32
+ });
33
+ }
34
+ }
35
+
36
+ export interface SingleSelectColumnMenuOptions<TV, TO = TV> {
37
+ options: TO[];
38
+ valueSelector?: (item: TO) => TV;
39
+ renderItem?: (item: TO) => ReactNode | string;
40
+ }
41
+
42
+ export function singleSelectColumnMenuFilter<TV extends IdType, TO = TV>(
43
+ props: SingleSelectColumnMenuOptions<TV, TO>
44
+ ) {
45
+ const FilterCell: FC<TableFilterCellProps> = ({ value, onChange }) => {
46
+ const handleChange = (value: TV | undefined, event: SyntheticEvent<HTMLInputElement>) => {
47
+ onChange({
48
+ value: value ?? '',
49
+ operator: value ? 'equals' : '',
50
+ syntheticEvent: event,
51
+ });
52
+ };
53
+
54
+ return (
55
+ <SelectorRadio
56
+ options={props.options}
57
+ value={value === '' ? undefined : value}
58
+ onChange={handleChange}
59
+ renderer={props.renderItem}
60
+ valueSelector={props.valueSelector ?? (option => option)}
61
+ />
62
+ );
63
+ };
64
+
65
+ return renderCustomColumnMenuFilter(FilterCell);
66
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,8 @@ export * from './editable-cell';
4
4
  export * from './filters';
5
5
  export * from './export';
6
6
  export * from './select-cell';
7
+ export * from './use-observing-table-state';
8
+ export * from './utils/filters';
7
9
 
8
10
  export {
9
11
  TableColumn,
@@ -67,7 +67,7 @@ export interface TableStateModel<TId extends IdType = never> {
67
67
  group: GroupDescriptor[];
68
68
  }
69
69
 
70
- interface TableStateConstructorParams<
70
+ export interface TableStateConstructorParams<
71
71
  T,
72
72
  TId extends IdType = never,
73
73
  P = never,
@@ -0,0 +1,82 @@
1
+ import { FC, Fragment } from 'react';
2
+ import { observer } from 'mobx-react';
3
+ import { provide, useDependencies } from '@servicetitan/react-ioc';
4
+ import { BodyText, Button, TableColumn } from '@servicetitan/design-system';
5
+
6
+ import { Table, ResetPaginationMode, useObservingTableState } from '../../../';
7
+ import { UseObservingTableStateDemoStore } from '../stores/use-observing-table-state-demo.store';
8
+ //
9
+
10
+ export interface TableUseTableStateExampleProps {
11
+ resetPaginationMode: ResetPaginationMode;
12
+ }
13
+ export const TableUseTableStateExample: FC<TableUseTableStateExampleProps> = provide({
14
+ singletons: [UseObservingTableStateDemoStore],
15
+ })(
16
+ observer(({ resetPaginationMode }) => {
17
+ const [
18
+ {
19
+ data,
20
+ updateDataKeepRoot,
21
+ updateDataChangeRoot,
22
+ add5RowsWithKeepDataRoot,
23
+ remove5RowsWithKeepDataRoot,
24
+ add5RowsWithUpdateDataRoot,
25
+ remove5RowsWithUpdateDataRoot,
26
+ },
27
+ ] = useDependencies(UseObservingTableStateDemoStore);
28
+ const tableState = useObservingTableState(
29
+ data,
30
+ { pageSize: 5 },
31
+ { idSelector: p => p.ProductID },
32
+ resetPaginationMode
33
+ );
34
+
35
+ return (
36
+ <Fragment>
37
+ <table className="m-b-2" cellPadding={4}>
38
+ <tr>
39
+ <td>
40
+ <BodyText>Keep data root:</BodyText>
41
+ </td>
42
+ <td>
43
+ <Button onClick={updateDataKeepRoot}>Update prices</Button>
44
+ </td>
45
+ <td>
46
+ <Button onClick={add5RowsWithKeepDataRoot}>Add 5 rows</Button>
47
+ </td>
48
+ <td>
49
+ <Button onClick={remove5RowsWithKeepDataRoot}>Remove 5 rows</Button>
50
+ </td>
51
+ </tr>
52
+ <tr>
53
+ <td>
54
+ <BodyText>Update data root:</BodyText>
55
+ </td>
56
+ <td>
57
+ <Button onClick={updateDataChangeRoot}>Update prices</Button>
58
+ </td>
59
+ <td>
60
+ <Button onClick={add5RowsWithUpdateDataRoot}>Add 5 rows</Button>
61
+ </td>
62
+ <td>
63
+ <Button onClick={remove5RowsWithUpdateDataRoot}>Remove 5 rows</Button>
64
+ </td>
65
+ </tr>
66
+ </table>
67
+ <Table tableState={tableState} sortable groupable>
68
+ <TableColumn field="ProductID" title="ID" width="100px" />
69
+
70
+ <TableColumn field="ProductName" title="Product Name" />
71
+
72
+ <TableColumn
73
+ field="UnitPrice"
74
+ title="Unit Price"
75
+ format="{0:c}"
76
+ width="125px"
77
+ />
78
+ </Table>
79
+ </Fragment>
80
+ );
81
+ })
82
+ );
@@ -0,0 +1,69 @@
1
+ import { injectable } from '@servicetitan/react-ioc';
2
+ import { action, makeObservable, observable } from 'mobx';
3
+
4
+ import { products } from '../../../demo/overview/products';
5
+
6
+ const getRandomPrice = () => Math.round(Math.random() * 10000) / 100;
7
+
8
+ @injectable()
9
+ export class UseObservingTableStateDemoStore {
10
+ @observable data = products.map(p => ({
11
+ ProductID: p.ProductID,
12
+ ProductName: p.ProductName,
13
+ UnitPrice: p.UnitPrice,
14
+ }));
15
+
16
+ constructor() {
17
+ makeObservable(this);
18
+ }
19
+
20
+ @action
21
+ updateDataKeepRoot = () => {
22
+ this.data.forEach(p => (p.UnitPrice = getRandomPrice()));
23
+ };
24
+
25
+ @action
26
+ updateDataChangeRoot = () => {
27
+ this.data = this.data.map(p => ({
28
+ ...p,
29
+ UnitPrice: getRandomPrice(),
30
+ }));
31
+ };
32
+
33
+ @action
34
+ add5RowsWithUpdateDataRoot = () => {
35
+ this.data = [
36
+ ...this.data,
37
+ ...Array.from({ length: 5 }).map((v, i) => ({
38
+ ProductID: this.data.length + i + 1,
39
+ ProductName: `Product ${this.data.length + i + 1}`,
40
+ UnitPrice: getRandomPrice(),
41
+ })),
42
+ ];
43
+ };
44
+ @action
45
+ remove5RowsWithUpdateDataRoot = () => {
46
+ if (this.data.length >= 5) {
47
+ this.data = this.data.slice(0, this.data.length - 5);
48
+ }
49
+ };
50
+
51
+ @action
52
+ add5RowsWithKeepDataRoot = () => {
53
+ const length = this.data.length;
54
+ for (let i = this.data.length; i < length + 5; ++i) {
55
+ this.data.push({
56
+ ProductID: i,
57
+ ProductName: `Product ${i}`,
58
+ UnitPrice: getRandomPrice(),
59
+ });
60
+ }
61
+ };
62
+
63
+ @action
64
+ remove5RowsWithKeepDataRoot = () => {
65
+ if (this.data.length >= 5) {
66
+ this.data.splice(this.data.length - 5, 5);
67
+ }
68
+ };
69
+ }
@@ -0,0 +1 @@
1
+ export * from './use-observing-table-state';
@@ -0,0 +1,20 @@
1
+ import { FC } from 'react';
2
+ import { TableUseTableStateExample } from './demo/components/use-observing-table-state-demo';
3
+ import { ResetPaginationMode } from './';
4
+
5
+ export default {
6
+ title: 'Table/UseObservingTableState',
7
+ parameters: {},
8
+ };
9
+
10
+ export const ResetPaginationOnUpdateDataRoot: FC = () => (
11
+ <TableUseTableStateExample resetPaginationMode={ResetPaginationMode.OnUpdateDataRoot} />
12
+ );
13
+
14
+ export const ResetPaginationNever: FC = () => (
15
+ <TableUseTableStateExample resetPaginationMode={ResetPaginationMode.Never} />
16
+ );
17
+
18
+ export const ResetPaginationAlways: FC = () => (
19
+ <TableUseTableStateExample resetPaginationMode={ResetPaginationMode.Always} />
20
+ );
@@ -0,0 +1,69 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { autorun } from 'mobx';
3
+
4
+ import { IdType, InMemoryDataSource, Preprocessors } from '@servicetitan/data-query';
5
+ import { useInitializedRef } from '@servicetitan/react-hooks';
6
+ import { TableState, TableStateConstructorParams } from '../table-state';
7
+ //
8
+
9
+ export enum ResetPaginationMode {
10
+ Never,
11
+ OnUpdateDataRoot,
12
+ Always,
13
+ }
14
+
15
+ export interface DatasourceOptions<T, TID extends IdType = any> {
16
+ idSelector: (row: T) => TID;
17
+ datasourcePreprocessors?: Preprocessors<T>;
18
+ }
19
+
20
+ export const useObservingTableState = <
21
+ T,
22
+ TID extends IdType = any,
23
+ P = never,
24
+ PID extends IdType = never
25
+ >(
26
+ data: T[],
27
+ tableArgs: TableStateConstructorParams<T, TID, P, PID>,
28
+ datasourceOptions: DatasourceOptions<T, TID>,
29
+ resetPagination: ResetPaginationMode = ResetPaginationMode.OnUpdateDataRoot
30
+ ): TableState<T, TID, P, PID> => {
31
+ const tableStateRef = useInitializedRef(() => new TableState<T, TID, P, PID>(tableArgs));
32
+ const datasourceDataRef = useRef<T[]>();
33
+ const tableStateDataRef = useRef<T[]>();
34
+ const [dataSource, setDataSource] = useState<InMemoryDataSource<T, TID>>();
35
+
36
+ useEffect(() => {
37
+ const disposer = autorun(() => {
38
+ setDataSource(
39
+ new InMemoryDataSource(
40
+ data,
41
+ datasourceOptions.idSelector,
42
+ datasourceOptions.datasourcePreprocessors
43
+ )
44
+ );
45
+ datasourceDataRef.current = data;
46
+ });
47
+ return () => disposer();
48
+ // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ }, [data]);
50
+
51
+ useEffect(() => {
52
+ if (dataSource) {
53
+ if (
54
+ tableStateRef.current.skip >= (datasourceDataRef.current?.length ?? 0) ||
55
+ resetPagination === ResetPaginationMode.Always ||
56
+ (resetPagination === ResetPaginationMode.OnUpdateDataRoot &&
57
+ tableStateDataRef.current !== datasourceDataRef.current)
58
+ ) {
59
+ tableStateRef.current.setDataSource(dataSource);
60
+ } else {
61
+ const state = tableStateRef.current.exportState();
62
+ tableStateRef.current.setDataSource(dataSource, { initialState: state });
63
+ }
64
+ tableStateDataRef.current = datasourceDataRef.current;
65
+ }
66
+ }, [dataSource, resetPagination, tableStateRef]);
67
+
68
+ return tableStateRef.current;
69
+ };
@@ -0,0 +1,31 @@
1
+ import { getFiltersFlat, getFiltersMap } from '../filters';
2
+ import { CompositeFilterDescriptor, FilterDescriptor } from '@servicetitan/data-query';
3
+
4
+ const filter = (field: string) => ({ field, operator: 'qq', value: 'ee' });
5
+ const composite = (
6
+ filters: (FilterDescriptor | CompositeFilterDescriptor)[]
7
+ ): CompositeFilterDescriptor => ({ filters, logic: 'and' });
8
+
9
+ const filters1 = () =>
10
+ composite([composite([filter('f1'), filter('f2')]), filter('f3'), composite([filter('f1')])]);
11
+
12
+ describe('getFiltersFlat', () => {
13
+ test('getFiltersFlat', () => {
14
+ expect(getFiltersFlat(filters1())).toEqual([
15
+ filter('f1'),
16
+ filter('f2'),
17
+ filter('f3'),
18
+ filter('f1'),
19
+ ]);
20
+ });
21
+ });
22
+
23
+ describe('getFiltersMap', () => {
24
+ test('getFiltersMap', () => {
25
+ expect(getFiltersMap(filters1())).toEqual({
26
+ f1: filter('f1'),
27
+ f2: filter('f2'),
28
+ f3: filter('f3'),
29
+ });
30
+ });
31
+ });
@@ -0,0 +1,33 @@
1
+ import {
2
+ CompositeFilterDescriptor,
3
+ FilterDescriptor,
4
+ isCompositeFilterDescriptor,
5
+ State,
6
+ } from '@servicetitan/data-query';
7
+
8
+ const getFiltersFlatInner = (
9
+ filter?: FilterDescriptor | CompositeFilterDescriptor
10
+ ): FilterDescriptor[] => {
11
+ if (!filter) {
12
+ return [];
13
+ } else if (isCompositeFilterDescriptor(filter)) {
14
+ return filter.filters.reduce(
15
+ (out, item) => [...out, ...getFiltersFlatInner(item)],
16
+ [] as FilterDescriptor[]
17
+ );
18
+ }
19
+ return [filter];
20
+ };
21
+
22
+ export const getFiltersFlat = (filter: State['filter']): FilterDescriptor[] => {
23
+ return getFiltersFlatInner(filter);
24
+ };
25
+
26
+ export const getFiltersMap = (filter: State['filter']): Record<string, FilterDescriptor> => {
27
+ return getFiltersFlatInner(filter).reduce((out, filter) => {
28
+ if (typeof filter.field === 'string') {
29
+ out[filter.field] = filter;
30
+ }
31
+ return out;
32
+ }, {} as Record<string, FilterDescriptor>);
33
+ };