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