@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.
- package/dist/demo/filters/async-select-filter.d.ts +3 -0
- package/dist/demo/filters/async-select-filter.d.ts.map +1 -0
- package/dist/demo/filters/async-select-filter.js +23 -0
- package/dist/demo/filters/async-select-filter.js.map +1 -0
- package/dist/demo/filters/categories.d.ts +9 -0
- package/dist/demo/filters/categories.d.ts.map +1 -0
- package/dist/demo/filters/categories.js +33 -0
- package/dist/demo/filters/categories.js.map +1 -0
- package/dist/demo/filters/single-select-filter.d.ts +3 -0
- package/dist/demo/filters/single-select-filter.d.ts.map +1 -0
- package/dist/demo/filters/single-select-filter.js +20 -0
- package/dist/demo/filters/single-select-filter.js.map +1 -0
- package/dist/demo/filters/table.store.d.ts +9 -0
- package/dist/demo/filters/table.store.d.ts.map +1 -0
- package/dist/demo/filters/table.store.js +73 -0
- package/dist/demo/filters/table.store.js.map +1 -0
- package/dist/filters/async-select/async-select-filter.d.ts +19 -0
- package/dist/filters/async-select/async-select-filter.d.ts.map +1 -0
- package/dist/filters/async-select/async-select-filter.js +168 -0
- package/dist/filters/async-select/async-select-filter.js.map +1 -0
- package/dist/filters/async-select/async-select-filter.stories.d.ts +7 -0
- package/dist/filters/async-select/async-select-filter.stories.d.ts.map +1 -0
- package/dist/filters/async-select/async-select-filter.stories.js +7 -0
- package/dist/filters/async-select/async-select-filter.stories.js.map +1 -0
- package/dist/filters/index.d.ts +2 -0
- package/dist/filters/index.d.ts.map +1 -1
- package/dist/filters/index.js +2 -0
- package/dist/filters/index.js.map +1 -1
- package/dist/filters/single-select/single-select-filter.d.ts +9 -0
- package/dist/filters/single-select/single-select-filter.d.ts.map +1 -0
- package/dist/filters/single-select/single-select-filter.js +30 -0
- package/dist/filters/single-select/single-select-filter.js.map +1 -0
- package/dist/filters/single-select/single-select-filter.stories.d.ts +7 -0
- package/dist/filters/single-select/single-select-filter.stories.d.ts.map +1 -0
- package/dist/filters/single-select/single-select-filter.stories.js +7 -0
- package/dist/filters/single-select/single-select-filter.stories.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/table-state.d.ts +1 -1
- package/dist/table-state.d.ts.map +1 -1
- package/dist/use-observing-table-state/demo/components/use-observing-table-state-demo.d.ts +7 -0
- package/dist/use-observing-table-state/demo/components/use-observing-table-state-demo.d.ts.map +1 -0
- package/dist/use-observing-table-state/demo/components/use-observing-table-state-demo.js +15 -0
- package/dist/use-observing-table-state/demo/components/use-observing-table-state-demo.js.map +1 -0
- package/dist/use-observing-table-state/demo/stores/use-observing-table-state-demo.store.d.ts +15 -0
- package/dist/use-observing-table-state/demo/stores/use-observing-table-state-demo.store.d.ts.map +1 -0
- package/dist/use-observing-table-state/demo/stores/use-observing-table-state-demo.store.js +128 -0
- package/dist/use-observing-table-state/demo/stores/use-observing-table-state-demo.store.js.map +1 -0
- package/dist/use-observing-table-state/index.d.ts +2 -0
- package/dist/use-observing-table-state/index.d.ts.map +1 -0
- package/dist/use-observing-table-state/index.js +2 -0
- package/dist/use-observing-table-state/index.js.map +1 -0
- package/dist/use-observing-table-state/use-observing-table-state.d.ts +13 -0
- package/dist/use-observing-table-state/use-observing-table-state.d.ts.map +1 -0
- package/dist/use-observing-table-state/use-observing-table-state.js +44 -0
- package/dist/use-observing-table-state/use-observing-table-state.js.map +1 -0
- package/dist/use-observing-table-state/use-observing-table-state.stories.d.ts +10 -0
- package/dist/use-observing-table-state/use-observing-table-state.stories.d.ts.map +1 -0
- package/dist/use-observing-table-state/use-observing-table-state.stories.js +11 -0
- package/dist/use-observing-table-state/use-observing-table-state.stories.js.map +1 -0
- package/dist/utils/filters.d.ts +4 -0
- package/dist/utils/filters.d.ts.map +1 -0
- package/dist/utils/filters.js +22 -0
- package/dist/utils/filters.js.map +1 -0
- package/package.json +7 -6
- package/src/demo/filters/async-select-filter.tsx +53 -0
- package/src/demo/filters/categories.tsx +40 -0
- package/src/demo/filters/single-select-filter.tsx +51 -0
- package/src/demo/filters/table.store.ts +45 -0
- package/src/filters/async-select/async-select-filter.stories.tsx +7 -0
- package/src/filters/async-select/async-select-filter.tsx +249 -0
- package/src/filters/index.ts +2 -0
- package/src/filters/single-select/single-select-filter.stories.tsx +7 -0
- package/src/filters/single-select/single-select-filter.tsx +66 -0
- package/src/index.ts +2 -0
- package/src/table-state.ts +1 -1
- package/src/use-observing-table-state/demo/components/use-observing-table-state-demo.tsx +82 -0
- package/src/use-observing-table-state/demo/stores/use-observing-table-state-demo.store.ts +69 -0
- package/src/use-observing-table-state/index.ts +1 -0
- package/src/use-observing-table-state/use-observing-table-state.stories.tsx +20 -0
- package/src/use-observing-table-state/use-observing-table-state.ts +69 -0
- package/src/utils/__tests__/filters.test.ts +31 -0
- 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> </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
|
+
}
|
package/src/filters/index.ts
CHANGED
|
@@ -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,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
package/src/table-state.ts
CHANGED
|
@@ -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
|
+
};
|