@servicetitan/form-state 22.4.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/CHANGELOG.md +12 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.d.ts +12 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.d.ts.map +1 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.js +89 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.js.map +1 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.stories.d.ts +11 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.stories.d.ts.map +1 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.stories.js +55 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.stories.js.map +1 -0
- package/dist/async-lazy-dropdown-state/index.d.ts +3 -0
- package/dist/async-lazy-dropdown-state/index.d.ts.map +1 -0
- package/dist/async-lazy-dropdown-state/index.js +3 -0
- package/dist/async-lazy-dropdown-state/index.js.map +1 -0
- package/dist/async-lazy-dropdown-state/use-async-lazy-dropdown-state.d.ts +5 -0
- package/dist/async-lazy-dropdown-state/use-async-lazy-dropdown-state.d.ts.map +1 -0
- package/dist/async-lazy-dropdown-state/use-async-lazy-dropdown-state.js +26 -0
- package/dist/async-lazy-dropdown-state/use-async-lazy-dropdown-state.js.map +1 -0
- package/dist/date-range.d.ts +5 -0
- package/dist/date-range.d.ts.map +1 -0
- package/dist/date-range.js +2 -0
- package/dist/date-range.js.map +1 -0
- package/dist/demo/dropdown-state.d.ts +3 -0
- package/dist/demo/dropdown-state.d.ts.map +1 -0
- package/dist/demo/dropdown-state.js +133 -0
- package/dist/demo/dropdown-state.js.map +1 -0
- package/dist/demo/index.d.ts +2 -0
- package/dist/demo/index.d.ts.map +1 -0
- package/dist/demo/index.js +2 -0
- package/dist/demo/index.js.map +1 -0
- package/dist/dropdown-state.d.ts +42 -0
- package/dist/dropdown-state.d.ts.map +1 -0
- package/dist/dropdown-state.js +314 -0
- package/dist/dropdown-state.js.map +1 -0
- package/dist/form-helpers.d.ts +70 -0
- package/dist/form-helpers.d.ts.map +1 -0
- package/dist/form-helpers.js +232 -0
- package/dist/form-helpers.js.map +1 -0
- package/dist/form-validators.d.ts +30 -0
- package/dist/form-validators.d.ts.map +1 -0
- package/dist/form-validators.js +56 -0
- package/dist/form-validators.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/persistent-form-state/domain-storage.d.ts +14 -0
- package/dist/persistent-form-state/domain-storage.d.ts.map +1 -0
- package/dist/persistent-form-state/domain-storage.js +42 -0
- package/dist/persistent-form-state/domain-storage.js.map +1 -0
- package/dist/persistent-form-state/in-memory-storage.d.ts +13 -0
- package/dist/persistent-form-state/in-memory-storage.d.ts.map +1 -0
- package/dist/persistent-form-state/in-memory-storage.js +30 -0
- package/dist/persistent-form-state/in-memory-storage.js.map +1 -0
- package/dist/persistent-form-state/index.d.ts +2 -0
- package/dist/persistent-form-state/index.d.ts.map +1 -0
- package/dist/persistent-form-state/index.js +2 -0
- package/dist/persistent-form-state/index.js.map +1 -0
- package/dist/persistent-form-state/persistent-form-state.d.ts +18 -0
- package/dist/persistent-form-state/persistent-form-state.d.ts.map +1 -0
- package/dist/persistent-form-state/persistent-form-state.js +93 -0
- package/dist/persistent-form-state/persistent-form-state.js.map +1 -0
- package/package.json +45 -0
- package/src/__tests__/__snapshots__/form-helpers.test.ts.snap +37 -0
- package/src/__tests__/form-helpers.test.ts +229 -0
- package/src/__tests__/form-validators.test.ts +55 -0
- package/src/async-lazy-dropdown-state/async-lazy-dropdown-state.stories.tsx +66 -0
- package/src/async-lazy-dropdown-state/async-lazy-dropdown-state.ts +77 -0
- package/src/async-lazy-dropdown-state/index.ts +2 -0
- package/src/async-lazy-dropdown-state/use-async-lazy-dropdown-state.ts +38 -0
- package/src/date-range.ts +4 -0
- package/src/demo/dropdown-state.tsx +233 -0
- package/src/demo/index.ts +1 -0
- package/src/dropdown-state.ts +205 -0
- package/src/form-helpers.ts +259 -0
- package/src/form-validators.ts +106 -0
- package/src/index.ts +5 -0
- package/src/persistent-form-state/__tests__/domain-storage.test.ts +81 -0
- package/src/persistent-form-state/domain-storage.ts +43 -0
- package/src/persistent-form-state/in-memory-storage.ts +32 -0
- package/src/persistent-form-state/index.ts +1 -0
- package/src/persistent-form-state/persistent-form-state.ts +68 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { useRef, Fragment, FC } from 'react';
|
|
2
|
+
|
|
3
|
+
import { InMemoryDataSource, AsyncDataSource, FilterDescriptor } from '@servicetitan/data-query';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AnvilSelect,
|
|
7
|
+
AnvilSelectOptionsProps,
|
|
8
|
+
Text,
|
|
9
|
+
ButtonGroup,
|
|
10
|
+
Button,
|
|
11
|
+
} from '@servicetitan/design-system';
|
|
12
|
+
|
|
13
|
+
import { FieldState } from 'formstate';
|
|
14
|
+
import { observer } from 'mobx-react';
|
|
15
|
+
|
|
16
|
+
import { DropdownState, DropdownOption } from '..';
|
|
17
|
+
|
|
18
|
+
function useDropdownField<T>(initial: T) {
|
|
19
|
+
return useRef(new FieldState(initial)).current;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function useDropdownState<T extends DropdownOption<any>>(state: DropdownState<T>) {
|
|
23
|
+
return useRef(state).current;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Planet extends DropdownOption<number> {
|
|
27
|
+
diameter: number;
|
|
28
|
+
moons: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const options: Planet[] = [
|
|
32
|
+
{ value: 1, text: 'Alderaan', diameter: 12500, moons: 0 },
|
|
33
|
+
{ value: 2, text: 'Bespin', diameter: 118000, moons: 2 },
|
|
34
|
+
{ value: 3, text: 'Coruscant', diameter: 12240, moons: 4 },
|
|
35
|
+
{ value: 4, text: 'Dagobah', diameter: 14410, moons: 0 },
|
|
36
|
+
{ value: 5, text: 'Hoth', diameter: 7200, moons: 3 },
|
|
37
|
+
{ value: 6, text: 'Kashyyyk', diameter: 12765, moons: 3 },
|
|
38
|
+
{ value: 7, text: 'Naboo', diameter: 12120, moons: 3 },
|
|
39
|
+
{ value: 8, text: 'Tatooine', diameter: 10465, moons: 3 },
|
|
40
|
+
{ value: 9, text: 'Yavin', diameter: 200000, moons: 26 },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
interface Character extends DropdownOption<number> {
|
|
44
|
+
group: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const optionsWithGroups: Character[] = [
|
|
48
|
+
{ value: 1, text: 'Hem Dazon', group: 'Arcona' },
|
|
49
|
+
{ value: 2, text: 'El-Les', group: 'Arcona' },
|
|
50
|
+
{ value: 3, text: 'Jheeg', group: 'Arcona' },
|
|
51
|
+
{ value: 4, text: 'Paploo', group: 'Ewok' },
|
|
52
|
+
{ value: 5, text: 'Gallo', group: 'Gungan' },
|
|
53
|
+
{ value: 6, text: 'Lyonie', group: 'Gungan' },
|
|
54
|
+
{ value: 7, text: 'Tobler Ceel', group: 'Gungan' },
|
|
55
|
+
{ value: 8, text: 'Ganne', group: 'Gungan' },
|
|
56
|
+
{ value: 9, text: 'Augara Jowil', group: 'Gungan' },
|
|
57
|
+
{ value: 10, text: 'Reegesk', group: 'Ranat' },
|
|
58
|
+
{ value: 11, text: 'Rik-tak', group: 'Ranat' },
|
|
59
|
+
{ value: 12, text: 'Bahb', group: 'Zeltron' },
|
|
60
|
+
{ value: 13, text: 'Chantique', group: 'Zeltron' },
|
|
61
|
+
{ value: 14, text: 'Luxa', group: 'Zeltron' },
|
|
62
|
+
{ value: 15, text: 'Rahuhl', group: 'Zeltron' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const InMemoryDropdownStateExample: FC = observer(() => {
|
|
66
|
+
const field = useDropdownField<AnvilSelectOptionsProps[]>([]);
|
|
67
|
+
const state = useDropdownState(
|
|
68
|
+
new DropdownState<Planet | Character>({
|
|
69
|
+
dataSource: new InMemoryDataSource(options),
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const handleChange = (data: AnvilSelectOptionsProps | AnvilSelectOptionsProps[]) => {
|
|
74
|
+
field.onChange(Array.isArray(data) ? data : [data]);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const withReset = (handler: () => void) => {
|
|
78
|
+
return async () => {
|
|
79
|
+
handleChange([]);
|
|
80
|
+
state.setSearch('');
|
|
81
|
+
state.setDataSource(null);
|
|
82
|
+
state.setSearchByGroup(false);
|
|
83
|
+
state.setSort([{ field: 'text' }]);
|
|
84
|
+
state.setFilter(null);
|
|
85
|
+
state.setGroup([{ field: 'group' }]);
|
|
86
|
+
|
|
87
|
+
await Promise.resolve();
|
|
88
|
+
|
|
89
|
+
handler();
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const withoutGroups = withReset(() => {
|
|
94
|
+
state.setDataSource(new InMemoryDataSource(options));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const withGroups = withReset(() => {
|
|
98
|
+
state.setDataSource(new InMemoryDataSource(optionsWithGroups));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const withGroupsAndSearch = withReset(() => {
|
|
102
|
+
state.setDataSource(new InMemoryDataSource(optionsWithGroups));
|
|
103
|
+
state.setSearchByGroup(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const customSort = withReset(() => {
|
|
107
|
+
state.setDataSource(new InMemoryDataSource(options));
|
|
108
|
+
state.setSort([{ field: 'diameter', dir: 'desc' }]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const customFilter = withReset(() => {
|
|
112
|
+
state.setDataSource(new InMemoryDataSource(options));
|
|
113
|
+
state.setFilter({
|
|
114
|
+
logic: 'and',
|
|
115
|
+
filters: [{ field: 'diameter', value: 12250, operator: 'gte' }],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const customGroup = withReset(() => {
|
|
120
|
+
state.setDataSource(new InMemoryDataSource(options));
|
|
121
|
+
state.setGroup([{ field: 'moons', dir: 'desc' }]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Fragment>
|
|
126
|
+
<AnvilSelect
|
|
127
|
+
value={field.value}
|
|
128
|
+
onChange={handleChange}
|
|
129
|
+
options={state.options}
|
|
130
|
+
search={{ value: state.search, onChange: state.onSearchChange }}
|
|
131
|
+
multiple
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<Text size={4} className="m-t-4 m-b-half">
|
|
135
|
+
Without groups
|
|
136
|
+
</Text>
|
|
137
|
+
<Button onClick={withoutGroups}>Apply</Button>
|
|
138
|
+
|
|
139
|
+
<Text size={4} className="m-t-4 m-b-half">
|
|
140
|
+
With groups
|
|
141
|
+
</Text>
|
|
142
|
+
<ButtonGroup>
|
|
143
|
+
<Button onClick={withGroups}>Search in options</Button>
|
|
144
|
+
<Button onClick={withGroupsAndSearch}>Search in options & groups</Button>
|
|
145
|
+
</ButtonGroup>
|
|
146
|
+
|
|
147
|
+
<Text size={4} className="m-t-4 m-b-half">
|
|
148
|
+
Custom sort
|
|
149
|
+
</Text>
|
|
150
|
+
<Button onClick={customSort}>Apply</Button>
|
|
151
|
+
|
|
152
|
+
<Text size={4} className="m-t-4 m-b-half">
|
|
153
|
+
Custom filter
|
|
154
|
+
</Text>
|
|
155
|
+
<Button onClick={customFilter}>Apply</Button>
|
|
156
|
+
|
|
157
|
+
<Text size={4} className="m-t-4 m-b-half">
|
|
158
|
+
Custom group
|
|
159
|
+
</Text>
|
|
160
|
+
<Button onClick={customGroup}>Apply</Button>
|
|
161
|
+
</Fragment>
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const AsyncDropdownStateExample: FC = observer(() => {
|
|
166
|
+
const field = useDropdownField<AnvilSelectOptionsProps[]>([]);
|
|
167
|
+
const state = useDropdownState(
|
|
168
|
+
new DropdownState<Planet>({
|
|
169
|
+
dataSource: new AsyncDataSource(
|
|
170
|
+
{
|
|
171
|
+
async get({ filter }) {
|
|
172
|
+
const search = (filter?.filters?.[0] as FilterDescriptor | undefined)
|
|
173
|
+
?.value as string | undefined;
|
|
174
|
+
|
|
175
|
+
const result = search
|
|
176
|
+
? options.filter(({ text }) =>
|
|
177
|
+
text.toLowerCase().includes(search.toLowerCase())
|
|
178
|
+
)
|
|
179
|
+
: options;
|
|
180
|
+
|
|
181
|
+
await new Promise(resolver => setTimeout(resolver, 1000));
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
data: result,
|
|
185
|
+
total: result.length,
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
undefined,
|
|
190
|
+
true
|
|
191
|
+
),
|
|
192
|
+
lazy: true,
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const handleChange = (data: AnvilSelectOptionsProps | AnvilSelectOptionsProps[]) => {
|
|
197
|
+
field.onChange(Array.isArray(data) ? data : [data]);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const handleOpenChange = (open: boolean) => {
|
|
201
|
+
if (open) {
|
|
202
|
+
state.fetch();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<AnvilSelect
|
|
208
|
+
value={field.value}
|
|
209
|
+
onChange={handleChange}
|
|
210
|
+
options={state.options}
|
|
211
|
+
search={{ value: state.search, onChange: state.onSearchChange }}
|
|
212
|
+
onOpenChange={handleOpenChange}
|
|
213
|
+
loading={state.loading}
|
|
214
|
+
multiple
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
export const DropdownStateExample: FC = () => {
|
|
220
|
+
return (
|
|
221
|
+
<Fragment>
|
|
222
|
+
<Text size={5} className="m-b-half">
|
|
223
|
+
InMemoryDataSource
|
|
224
|
+
</Text>
|
|
225
|
+
<InMemoryDropdownStateExample />
|
|
226
|
+
|
|
227
|
+
<Text size={5} className="m-t-5 m-b-half">
|
|
228
|
+
AsyncDataSource
|
|
229
|
+
</Text>
|
|
230
|
+
<AsyncDropdownStateExample />
|
|
231
|
+
</Fragment>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dropdown-state';
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { ChangeEvent } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
State,
|
|
4
|
+
SortDescriptor,
|
|
5
|
+
CompositeFilterDescriptor,
|
|
6
|
+
GroupDescriptor,
|
|
7
|
+
GroupResult,
|
|
8
|
+
DataSource,
|
|
9
|
+
} from '@servicetitan/data-query';
|
|
10
|
+
|
|
11
|
+
import { InputOnChangeData, AnvilSelectOptionsProps } from '@servicetitan/design-system';
|
|
12
|
+
|
|
13
|
+
import { observable, computed, action, runInAction, makeObservable } from 'mobx';
|
|
14
|
+
|
|
15
|
+
import debounce from 'debounce';
|
|
16
|
+
|
|
17
|
+
function isGroupItem<T>(item: T | GroupResult<T>, groupedBy: string): item is GroupResult<T> {
|
|
18
|
+
const { items, field } = item as GroupResult<T>;
|
|
19
|
+
return !!items && field === groupedBy;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DropdownOption<T> {
|
|
23
|
+
value: T;
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface DropdownStateConstructorParams<T> {
|
|
28
|
+
dataSource?: DataSource<T>;
|
|
29
|
+
searchByGroup?: boolean;
|
|
30
|
+
state?: {
|
|
31
|
+
sort?: SortDescriptor[];
|
|
32
|
+
filter?: CompositeFilterDescriptor;
|
|
33
|
+
group?: [GroupDescriptor];
|
|
34
|
+
};
|
|
35
|
+
lazy?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class DropdownState<T extends DropdownOption<any>> {
|
|
39
|
+
@observable loading = false;
|
|
40
|
+
@observable search = '';
|
|
41
|
+
|
|
42
|
+
@computed get options() {
|
|
43
|
+
const result = new Map<T[keyof T], AnvilSelectOptionsProps>();
|
|
44
|
+
const flat: AnvilSelectOptionsProps[] = [];
|
|
45
|
+
|
|
46
|
+
const groupField = this.group[0].field;
|
|
47
|
+
|
|
48
|
+
this.traverse(
|
|
49
|
+
this.data,
|
|
50
|
+
item => {
|
|
51
|
+
const group = item[groupField as keyof T];
|
|
52
|
+
|
|
53
|
+
if (group) {
|
|
54
|
+
if (result.has(group)) {
|
|
55
|
+
result.get(group)?.options?.push(item);
|
|
56
|
+
} else {
|
|
57
|
+
result.set(group, {
|
|
58
|
+
value: group,
|
|
59
|
+
text: String(group),
|
|
60
|
+
options: [item],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
flat.push(item);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
groupField
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return [...flat, ...Array.from(result.values())];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@computed get state() {
|
|
74
|
+
return {
|
|
75
|
+
sort: this.sort,
|
|
76
|
+
filter:
|
|
77
|
+
this.searchFilter || this.filter
|
|
78
|
+
? this.searchFilter && this.filter
|
|
79
|
+
? { logic: 'and', filters: [this.searchFilter, this.filter] }
|
|
80
|
+
: this.searchFilter ?? this.filter
|
|
81
|
+
: undefined,
|
|
82
|
+
group: this.group,
|
|
83
|
+
} as State;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@computed private get searchFilter() {
|
|
87
|
+
if (!this.search) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const filter = { operator: 'contains', value: this.search, ignoreCase: true };
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
logic: 'or',
|
|
95
|
+
filters: [
|
|
96
|
+
{ ...filter, field: 'text' },
|
|
97
|
+
...(this.searchByGroup ? [{ ...filter, field: 'group' }] : []),
|
|
98
|
+
],
|
|
99
|
+
} as CompositeFilterDescriptor;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@observable private dataSource: DataSource<T> | null;
|
|
103
|
+
@observable private searchByGroup: boolean;
|
|
104
|
+
@observable private sort: SortDescriptor[];
|
|
105
|
+
@observable private filter: CompositeFilterDescriptor | null;
|
|
106
|
+
@observable private group: [GroupDescriptor];
|
|
107
|
+
|
|
108
|
+
@observable private data: T[] | GroupResult<T>[] = [];
|
|
109
|
+
|
|
110
|
+
constructor({
|
|
111
|
+
dataSource,
|
|
112
|
+
searchByGroup,
|
|
113
|
+
state,
|
|
114
|
+
lazy,
|
|
115
|
+
}: DropdownStateConstructorParams<T> = {}) {
|
|
116
|
+
makeObservable(this);
|
|
117
|
+
|
|
118
|
+
this.dataSource = dataSource ?? null;
|
|
119
|
+
this.searchByGroup = searchByGroup ?? false;
|
|
120
|
+
this.sort = state?.sort ?? [{ field: 'text' }];
|
|
121
|
+
this.filter = state?.filter ?? null;
|
|
122
|
+
this.group = state?.group ?? [{ field: 'group' }];
|
|
123
|
+
|
|
124
|
+
if (!lazy) {
|
|
125
|
+
this.fetch();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
130
|
+
onSearchChange = (() => {
|
|
131
|
+
const fetch = debounce(() => this.fetch(), 100);
|
|
132
|
+
return (_0: ChangeEvent<HTMLInputElement>, data: InputOnChangeData) => {
|
|
133
|
+
runInAction(() => {
|
|
134
|
+
this.search = data.value;
|
|
135
|
+
});
|
|
136
|
+
fetch();
|
|
137
|
+
};
|
|
138
|
+
})();
|
|
139
|
+
|
|
140
|
+
@action
|
|
141
|
+
setSearch = async (search: string) => {
|
|
142
|
+
this.search = search;
|
|
143
|
+
await this.fetch();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
@action
|
|
147
|
+
setDataSource = async (dataSource: DataSource<T> | null) => {
|
|
148
|
+
this.dataSource = dataSource;
|
|
149
|
+
await this.fetch();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
@action
|
|
153
|
+
setSearchByGroup = async (searchByGroup: boolean) => {
|
|
154
|
+
this.searchByGroup = searchByGroup;
|
|
155
|
+
await this.fetch();
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
@action
|
|
159
|
+
setSort = async (sort: SortDescriptor[]) => {
|
|
160
|
+
this.sort = sort;
|
|
161
|
+
await this.fetch();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
@action
|
|
165
|
+
setFilter = async (filter: CompositeFilterDescriptor | null) => {
|
|
166
|
+
this.filter = filter;
|
|
167
|
+
await this.fetch();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
@action
|
|
171
|
+
setGroup = async (group: [GroupDescriptor]) => {
|
|
172
|
+
this.group = group;
|
|
173
|
+
await this.fetch();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
@action
|
|
177
|
+
fetch = async () => {
|
|
178
|
+
this.loading = true;
|
|
179
|
+
|
|
180
|
+
const response = await this.dataSource?.getData(this.state);
|
|
181
|
+
|
|
182
|
+
runInAction(() => {
|
|
183
|
+
this.data = response?.data ?? [];
|
|
184
|
+
this.loading = false;
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
private traverse = (
|
|
189
|
+
items: T[] | GroupResult<T>[],
|
|
190
|
+
itemCallback: (item: T) => void,
|
|
191
|
+
groupedBy: string
|
|
192
|
+
) => {
|
|
193
|
+
if (!items) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const item of items) {
|
|
198
|
+
if (isGroupItem(item, groupedBy)) {
|
|
199
|
+
this.traverse(item.items, itemCallback, groupedBy);
|
|
200
|
+
} else {
|
|
201
|
+
itemCallback(item);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { SyntheticEvent, ChangeEvent, Key, ReactNode } from 'react';
|
|
2
|
+
import { FieldState, FormState, ValidatableMapOrArray, ComposibleValidatable } from 'formstate';
|
|
3
|
+
import { toJS, isObservableMap, runInAction, transaction, computed, comparer } from 'mobx';
|
|
4
|
+
|
|
5
|
+
export class CheckboxFieldState extends FieldState<boolean> {
|
|
6
|
+
onChangeHandler = (_0: any, checked: boolean) => {
|
|
7
|
+
this.onChange(checked);
|
|
8
|
+
};
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
10
|
+
DEPRECATED_onChangeHandler = (
|
|
11
|
+
_0: SyntheticEvent<HTMLInputElement>,
|
|
12
|
+
data: { checked?: boolean }
|
|
13
|
+
) => {
|
|
14
|
+
this.onChange(!!data.checked);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class InputFieldState<T> extends FieldState<T> {
|
|
19
|
+
onChangeHandler = (_0: SyntheticEvent<HTMLInputElement>, data: { value: T }) => {
|
|
20
|
+
this.onChange(data.value);
|
|
21
|
+
};
|
|
22
|
+
onChangeNativeHandler = (event: ChangeEvent<HTMLInputElement>) => {
|
|
23
|
+
this.onChange(event.currentTarget.value as unknown as T);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class TextAreaFieldState<T> extends FieldState<T> {
|
|
28
|
+
/**
|
|
29
|
+
* react-semantic type for onChange event of TextArea component seems to have a bug with data object type
|
|
30
|
+
* so declaring data as any
|
|
31
|
+
*/
|
|
32
|
+
onChangeHandler = (_0: SyntheticEvent<HTMLTextAreaElement>, data: any) => {
|
|
33
|
+
this.onChange(data.value);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class DropdownFieldState<T> extends FieldState<T> {
|
|
38
|
+
/**
|
|
39
|
+
* react-semantic type for onChange event of Dropdown component seems to have a bug with data object type
|
|
40
|
+
* so declaring data as any
|
|
41
|
+
*/
|
|
42
|
+
onChangeHandler = (_0: SyntheticEvent<HTMLElement>, data: any) => {
|
|
43
|
+
this.onChange(data.value);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class DropdownSearchFieldState<T> extends FieldState<T> {
|
|
48
|
+
onChangeHandler = (_0: SyntheticEvent<HTMLElement>, data: { searchQuery: T }) => {
|
|
49
|
+
this.onChange(data.searchQuery);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class DatetimeFieldState extends FieldState<Date | null> {
|
|
54
|
+
onChangeHandler = (event: { target: { value: Date | null } }) => {
|
|
55
|
+
this.onChange(event.target.value);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Option<T> {
|
|
60
|
+
key: Key;
|
|
61
|
+
text: ReactNode;
|
|
62
|
+
value: T;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function enumToOptions<T>(
|
|
66
|
+
enumObject: T,
|
|
67
|
+
nameProvider?: (value: T[keyof T]) => Option<T[keyof T]>['text']
|
|
68
|
+
): Option<T[keyof T]>[] {
|
|
69
|
+
return getEnumKeys(enumObject).map(k => ({
|
|
70
|
+
key: k as string,
|
|
71
|
+
text: nameProvider ? nameProvider(enumObject[k]) : k,
|
|
72
|
+
value: enumObject[k],
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getEnumKeys<T>(enumObject: T) {
|
|
77
|
+
let keys = Object.keys(enumObject) as (keyof T)[];
|
|
78
|
+
|
|
79
|
+
if (keys.some(k => typeof enumObject[k] === 'number')) {
|
|
80
|
+
keys = keys.filter(k => typeof enumObject[k] === 'number');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return keys;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getEnumValues<T>(enumObject: T) {
|
|
87
|
+
return getEnumKeys(enumObject).map(k => enumObject[k]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type FormValues = string | number | boolean | (string | number | boolean)[];
|
|
91
|
+
export interface FormStateAsJS {
|
|
92
|
+
[index: string]: FormValues | Map<any, any> | Set<any> | FormStateAsJS;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function traverseFormState<T extends ValidatableMapOrArray>(
|
|
96
|
+
recursive: boolean,
|
|
97
|
+
formState: FormState<T>,
|
|
98
|
+
onFormVisit?: (key: string, form: FormState<any>) => void,
|
|
99
|
+
onFieldVisit?: (key: string, field: FieldState<any>) => void
|
|
100
|
+
) {
|
|
101
|
+
const visitChild = (key: string, child: ComposibleValidatable<any>) => {
|
|
102
|
+
if (child instanceof FormState) {
|
|
103
|
+
if (recursive) {
|
|
104
|
+
traverseFormState(recursive, child, onFormVisit, onFieldVisit);
|
|
105
|
+
}
|
|
106
|
+
if (onFormVisit) {
|
|
107
|
+
onFormVisit(key, child);
|
|
108
|
+
}
|
|
109
|
+
} else if (onFieldVisit) {
|
|
110
|
+
onFieldVisit(key, child as FieldState<any>);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (Array.isArray(formState.$)) {
|
|
115
|
+
formState.$.forEach((child, index) => visitChild(index.toString(), child));
|
|
116
|
+
} else {
|
|
117
|
+
(isObservableMap(formState.$)
|
|
118
|
+
? Array.from(formState.$ as Map<any, ComposibleValidatable<any>>)
|
|
119
|
+
: Object.entries(formState.$ as Record<string, ComposibleValidatable<any>>)
|
|
120
|
+
).forEach(([key, child]) => visitChild(key, child));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
125
|
+
export function BAD_formStateToJS<T extends ValidatableMapOrArray>(formState: FormState<T>) {
|
|
126
|
+
const formValues = {} as any;
|
|
127
|
+
traverseFormState(
|
|
128
|
+
false,
|
|
129
|
+
formState,
|
|
130
|
+
(key: any, form: FormState<any>) => (formValues[key] = BAD_formStateToJS(form)),
|
|
131
|
+
(key: any, field: FieldState<any>) => (formValues[key] = toJS(field.value))
|
|
132
|
+
);
|
|
133
|
+
return formValues as unknown as FormStateAsJS;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type FormStateShape<T> = T extends FormState<infer U>[]
|
|
137
|
+
? FormStateMapOrObject<U>[]
|
|
138
|
+
: FormStateMapOrObject<T>;
|
|
139
|
+
|
|
140
|
+
type FormStateMapOrObject<T> = T extends Map<infer K, infer V>
|
|
141
|
+
? FormStateObject<Record<K & (string | number | symbol), V>>
|
|
142
|
+
: FormStateObject<T>;
|
|
143
|
+
|
|
144
|
+
type FormStateObject<T> = {
|
|
145
|
+
[P in keyof T]: T[P] extends FieldState<infer U>
|
|
146
|
+
? U
|
|
147
|
+
: T[P] extends FormState<infer U>
|
|
148
|
+
? FormStateShape<U>
|
|
149
|
+
: never;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export function formStateToJS<T extends ValidatableMapOrArray>(
|
|
153
|
+
formState: FormState<T>
|
|
154
|
+
): FormStateShape<T> {
|
|
155
|
+
const formValues = Array.isArray(formState.$) ? [] : ({} as any);
|
|
156
|
+
traverseFormState(
|
|
157
|
+
false,
|
|
158
|
+
formState,
|
|
159
|
+
(key: any, form: FormState<any>) => (formValues[key] = formStateToJS(form)),
|
|
160
|
+
(key: any, field: FieldState<any>) => (formValues[key] = toJS(field.$))
|
|
161
|
+
);
|
|
162
|
+
return formValues as unknown as FormStateShape<T>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type RecursivePartial<T> = T extends (infer U)[]
|
|
166
|
+
? RecursivePartialObject<U>[]
|
|
167
|
+
: RecursivePartialObject<T>;
|
|
168
|
+
|
|
169
|
+
type RecursivePartialObject<T> = {
|
|
170
|
+
[P in keyof T]?: T[P] extends Record<any, any> ? RecursivePartial<T[P]> : T[P];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export function setFormStateValues<T extends ValidatableMapOrArray>(
|
|
174
|
+
formState: FormState<T>,
|
|
175
|
+
data: RecursivePartial<FormStateShape<T>>,
|
|
176
|
+
triggerValidation = false
|
|
177
|
+
): FormState<T> {
|
|
178
|
+
if (Array.isArray(formState.$) && (data as any[]).length !== formState.$.length) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
'Number of elements in data object node is different from number of element in form.' +
|
|
181
|
+
' All array nodes should match in size before values can be applied to form.'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
transaction(() => {
|
|
185
|
+
traverseFormState(
|
|
186
|
+
false,
|
|
187
|
+
formState,
|
|
188
|
+
(key: string, form: FormState<any>) => {
|
|
189
|
+
if (data instanceof Map || isObservableMap(data)) {
|
|
190
|
+
if (data.has(key)) {
|
|
191
|
+
setFormStateValues(form, data.get(key));
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
195
|
+
setFormStateValues(form, data[key as keyof typeof data]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
(key: string, field: FieldState<any>) => {
|
|
200
|
+
if (data instanceof Map || isObservableMap(data)) {
|
|
201
|
+
if (data.has(key)) {
|
|
202
|
+
runInAction(() => {
|
|
203
|
+
field.value = data.get(key);
|
|
204
|
+
field.$ = data.get(key);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
209
|
+
runInAction(() => {
|
|
210
|
+
field.value = data[key as keyof typeof data];
|
|
211
|
+
field.$ = data[key as keyof typeof data];
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
if (triggerValidation) {
|
|
218
|
+
const setValidatedSubFields = (form: FormState<any>) => {
|
|
219
|
+
form.validatedSubFields = (form as any).getValues();
|
|
220
|
+
};
|
|
221
|
+
traverseFormState(true, formState, (_0: string, form: FormState<any>) => {
|
|
222
|
+
setValidatedSubFields(form);
|
|
223
|
+
});
|
|
224
|
+
setValidatedSubFields(formState);
|
|
225
|
+
formState.validate();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return formState;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function commitFormState<T extends ValidatableMapOrArray>(formState: FormState<T>) {
|
|
232
|
+
traverseFormState(true, formState, undefined, (_0: string, field: FieldState<any>) => {
|
|
233
|
+
field.dirty = false;
|
|
234
|
+
(field as any)._initValue = field.value;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function camelCaseToTitleCase(value: string) {
|
|
239
|
+
const regexp = /[A-Z](?=[A-Z][a-z])|[^A-Z](?=[A-Z])|[A-Za-z](?=[^A-Za-z])/g;
|
|
240
|
+
return value.replace(regexp, '$& ');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function isFormStateChanged<T extends ValidatableMapOrArray>(formState: FormState<T>) {
|
|
244
|
+
return computed(() => {
|
|
245
|
+
let isChanged = false;
|
|
246
|
+
traverseFormState(true, formState, undefined, (_0: string, field: FieldState<any>) => {
|
|
247
|
+
const isValueChanged = () => {
|
|
248
|
+
if (field instanceof InputFieldState) {
|
|
249
|
+
if ((field as any)._initValue === undefined && field.value === '') {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return !comparer.structural((field as any)._initValue, field.value);
|
|
254
|
+
};
|
|
255
|
+
isChanged = isChanged || (field.dirty === true && isValueChanged());
|
|
256
|
+
});
|
|
257
|
+
return isChanged;
|
|
258
|
+
});
|
|
259
|
+
}
|