@servicetitan/table 23.3.2 → 24.0.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 (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
+ };