@servicetitan/form 22.3.0 → 22.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/date-range-picker/date-range-picker.d.ts +1 -1
  3. package/dist/date-range-picker/date-range-picker.d.ts.map +1 -1
  4. package/dist/date-range-picker/date-range-picker.js.map +1 -1
  5. package/dist/demo/date-range-picker.js.map +1 -1
  6. package/dist/demo/index.d.ts +0 -1
  7. package/dist/demo/index.d.ts.map +1 -1
  8. package/dist/demo/index.js +0 -1
  9. package/dist/demo/index.js.map +1 -1
  10. package/dist/demo/input-date-mask.d.ts.map +1 -1
  11. package/dist/demo/input-date-mask.js.map +1 -1
  12. package/dist/form-state-error-banner/form-state-error-banner.js +1 -1
  13. package/dist/form-state-error-banner/form-state-error-banner.js.map +1 -1
  14. package/dist/index.d.ts +1 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -4
  17. package/dist/index.js.map +1 -1
  18. package/package.json +14 -15
  19. package/src/date-range-picker/date-range-picker.tsx +1 -1
  20. package/src/demo/date-range-picker.tsx +1 -1
  21. package/src/demo/index.ts +0 -1
  22. package/src/demo/input-date-mask.tsx +0 -1
  23. package/src/form-state-error-banner/form-state-error-banner.tsx +1 -1
  24. package/src/index.ts +2 -4
  25. package/dist/date-range.d.ts +0 -5
  26. package/dist/date-range.d.ts.map +0 -1
  27. package/dist/date-range.js +0 -2
  28. package/dist/date-range.js.map +0 -1
  29. package/dist/demo/dropdown-state.d.ts +0 -3
  30. package/dist/demo/dropdown-state.d.ts.map +0 -1
  31. package/dist/demo/dropdown-state.js +0 -133
  32. package/dist/demo/dropdown-state.js.map +0 -1
  33. package/dist/dropdown-state.d.ts +0 -42
  34. package/dist/dropdown-state.d.ts.map +0 -1
  35. package/dist/dropdown-state.js +0 -314
  36. package/dist/dropdown-state.js.map +0 -1
  37. package/dist/form-helpers.d.ts +0 -70
  38. package/dist/form-helpers.d.ts.map +0 -1
  39. package/dist/form-helpers.js +0 -232
  40. package/dist/form-helpers.js.map +0 -1
  41. package/dist/form-validators.d.ts +0 -30
  42. package/dist/form-validators.d.ts.map +0 -1
  43. package/dist/form-validators.js +0 -56
  44. package/dist/form-validators.js.map +0 -1
  45. package/dist/persistent-form-state/domain-storage.d.ts +0 -14
  46. package/dist/persistent-form-state/domain-storage.d.ts.map +0 -1
  47. package/dist/persistent-form-state/domain-storage.js +0 -42
  48. package/dist/persistent-form-state/domain-storage.js.map +0 -1
  49. package/dist/persistent-form-state/in-memory-storage.d.ts +0 -13
  50. package/dist/persistent-form-state/in-memory-storage.d.ts.map +0 -1
  51. package/dist/persistent-form-state/in-memory-storage.js +0 -30
  52. package/dist/persistent-form-state/in-memory-storage.js.map +0 -1
  53. package/dist/persistent-form-state/index.d.ts +0 -2
  54. package/dist/persistent-form-state/index.d.ts.map +0 -1
  55. package/dist/persistent-form-state/index.js +0 -2
  56. package/dist/persistent-form-state/index.js.map +0 -1
  57. package/dist/persistent-form-state/persistent-form-state.d.ts +0 -18
  58. package/dist/persistent-form-state/persistent-form-state.d.ts.map +0 -1
  59. package/dist/persistent-form-state/persistent-form-state.js +0 -93
  60. package/dist/persistent-form-state/persistent-form-state.js.map +0 -1
  61. package/src/__tests__/__snapshots__/form-helpers.test.ts.snap +0 -37
  62. package/src/__tests__/form-helpers.test.ts +0 -229
  63. package/src/__tests__/form-validators.test.ts +0 -55
  64. package/src/date-range.ts +0 -4
  65. package/src/demo/dropdown-state.tsx +0 -233
  66. package/src/dropdown-state.ts +0 -205
  67. package/src/form-helpers.ts +0 -259
  68. package/src/form-validators.ts +0 -106
  69. package/src/persistent-form-state/__tests__/domain-storage.test.ts +0 -81
  70. package/src/persistent-form-state/domain-storage.ts +0 -43
  71. package/src/persistent-form-state/in-memory-storage.ts +0 -32
  72. package/src/persistent-form-state/index.ts +0 -1
  73. package/src/persistent-form-state/persistent-form-state.ts +0 -68
@@ -1,55 +0,0 @@
1
- import { FormValidators } from '../form-validators';
2
-
3
- describe('FormValidators', () => {
4
- describe('website', () => {
5
- const exec = FormValidators.website();
6
-
7
- it('should return false if website url is valid', () => {
8
- for (const url of [
9
- 'servicetitan.com',
10
- 'www.servicetitan.com',
11
- 'http://servicetitan.com',
12
- 'http://wwww.servicetitan.com',
13
- 'https://servicetitan.com',
14
- 'https://wwww.servicetitan.com',
15
- 'something.servicetitan.com',
16
- ]) {
17
- expect(exec(url)).toBeFalsy();
18
- }
19
- });
20
-
21
- it('should return error message if website url is not valid', () => {
22
- for (const url of [
23
- 'servicetitan.c',
24
- 'servicetitan',
25
- 'htp://servicetitan.com',
26
- 'http://wwww.servicetitan.com!',
27
- 'https://servicetitan.com:',
28
- 'https://wwww.servic:etitan.com',
29
- ]) {
30
- expect(exec(url)).toBeTruthy();
31
- }
32
- });
33
- });
34
-
35
- describe('exactLength', () => {
36
- const exactLength = 10;
37
- const exec = FormValidators.exactLength(exactLength);
38
-
39
- it('should return error message if value length is greater than defined', () => {
40
- expect(exec('a'.repeat(exactLength + 1))).toBeTruthy();
41
- });
42
-
43
- it('should return error message if value length is less than defined', () => {
44
- expect(exec('a'.repeat(exactLength - 1))).toBeTruthy();
45
- });
46
-
47
- it('should return false if value length is equal to defined', () => {
48
- expect(exec('a'.repeat(exactLength))).toBeFalsy();
49
- });
50
-
51
- it('should return false if value is undefined', () => {
52
- expect(exec(undefined)).toBeFalsy();
53
- });
54
- });
55
- });
package/src/date-range.ts DELETED
@@ -1,4 +0,0 @@
1
- export interface DateRange {
2
- from?: Date;
3
- to?: Date;
4
- }
@@ -1,233 +0,0 @@
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
- };
@@ -1,205 +0,0 @@
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
- }