@servicetitan/form 22.2.0 → 22.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +24 -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 +15 -16
  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
- }