@servicetitan/docs-anvil-uikit-contrib 25.0.1-canary.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/docs/confirm-navigation.mdx +11 -0
- package/docs/confirm.mdx +54 -0
- package/docs/culture.mdx +84 -0
- package/docs/form-state.mdx +245 -0
- package/docs/form.mdx +151 -0
- package/docs/link-item.mdx +65 -0
- package/docs/notifications-center.mdx +307 -0
- package/docs/skeleton.mdx +38 -0
- package/docs/table.mdx +224 -0
- package/package.json +19 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Confirm Navigation
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
import { BasicExample } from '@servicetitan/confirm-navigation/dist/demo';
|
|
6
|
+
|
|
7
|
+
import { CodeDemo } from '@site/src/components/code-demo';
|
|
8
|
+
|
|
9
|
+
Sometimes you might want user to confirm leaving from page e.g. if he has some unsaved changes. `@servicetitan/confirm-navigation` is a component that receives title, message, and condition to determinate when confirmation should be shown.
|
|
10
|
+
|
|
11
|
+
<CodeDemo example={BasicExample} srcPath="confirm-navigation/src/demo/basic.tsx" />
|
package/docs/confirm.mdx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Confirm
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BasicExample,
|
|
7
|
+
HookExample,
|
|
8
|
+
ConfigurableExample,
|
|
9
|
+
ToggleableExample,
|
|
10
|
+
YesNoExample,
|
|
11
|
+
CustomExample,
|
|
12
|
+
} from '@servicetitan/confirm/dist/demo';
|
|
13
|
+
|
|
14
|
+
import { CodeDemo } from '@site/src/components/code-demo';
|
|
15
|
+
|
|
16
|
+
Sometimes you might want user to confirm actions like destructive button clicks. `@servicetitan/confirm` is a high order component that helps managing confirmation dialog state without creating one. `Confirm` patches passed `onConfirm` handler that you can later pass to trigger component, calling patched `onConfirm` would cause confirmation dialog to appear.
|
|
17
|
+
|
|
18
|
+
## Examples
|
|
19
|
+
|
|
20
|
+
### Basic
|
|
21
|
+
|
|
22
|
+
Zero configuration approach. Good fit for most use-cases.
|
|
23
|
+
|
|
24
|
+
<CodeDemo example={BasicExample} srcPath="confirm/src/demo/basic.tsx" />
|
|
25
|
+
|
|
26
|
+
### React Hook
|
|
27
|
+
|
|
28
|
+
Alternatively you can use `useConfirm` hook to create patched handler and confirmation component. This might be useful to debulk syntax in case of having several actions that need confirmation in a same component.
|
|
29
|
+
|
|
30
|
+
<CodeDemo example={HookExample} srcPath="confirm/src/demo/hook.tsx" />
|
|
31
|
+
|
|
32
|
+
### Configurable default dialog
|
|
33
|
+
|
|
34
|
+
Confirmation dialog title and message can be customized via prop.
|
|
35
|
+
|
|
36
|
+
<CodeDemo example={ConfigurableExample} srcPath="confirm/src/demo/configurable.tsx" />
|
|
37
|
+
|
|
38
|
+
### Toggleable confirmation
|
|
39
|
+
|
|
40
|
+
Toggle confirmation state conditionally.
|
|
41
|
+
|
|
42
|
+
<CodeDemo example={ToggleableExample} srcPath="confirm/src/demo/toggleable.tsx" />
|
|
43
|
+
|
|
44
|
+
### (Yes / No / Cancel) confrimation
|
|
45
|
+
|
|
46
|
+
`YesNoConfirmation` component is another option of a confirmation dialog.
|
|
47
|
+
|
|
48
|
+
<CodeDemo example={YesNoExample} srcPath="confirm/src/demo/yes-no.tsx" />
|
|
49
|
+
|
|
50
|
+
### Custom confirmation component
|
|
51
|
+
|
|
52
|
+
Finally you can have totally custom confirmation.
|
|
53
|
+
|
|
54
|
+
<CodeDemo example={CustomExample} srcPath="confirm/src/demo/custom.tsx" />
|
package/docs/culture.mdx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Culture
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
`@servicetitan/culture` holds UI culture (internationalization) settings.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { useDependencies } from '@servicetitan/react-ioc';
|
|
12
|
+
import { CultureProvider, CULTURE_TOKEN } from '@servicetitan/culture';
|
|
13
|
+
|
|
14
|
+
const App: React.FC = () => {
|
|
15
|
+
return (
|
|
16
|
+
<CultureProvider
|
|
17
|
+
value={{
|
|
18
|
+
Name: 'en-US',
|
|
19
|
+
NumberFormat: {
|
|
20
|
+
CurrencyPositivePattern: '%s%v',
|
|
21
|
+
CurrencyNegativePattern: '%s-%v',
|
|
22
|
+
CurrencySymbol: '$',
|
|
23
|
+
CurrencyDecimalSeparator: '.',
|
|
24
|
+
CurrencyGroupSeparator: ',',
|
|
25
|
+
CurrencyDecimalDigits: 2,
|
|
26
|
+
NumberDecimalSeparator: '.',
|
|
27
|
+
NumberGroupSeparator: ',',
|
|
28
|
+
NumberDecimalDigits: 2,
|
|
29
|
+
},
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<DisplayCulture />
|
|
33
|
+
</CultureProvider>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Somewhere later in the component tree you can consume culture settings via CULTURE_TOKEN
|
|
38
|
+
const DisplayCulture: React.FC = () => {
|
|
39
|
+
const [culture] = useDependencies(CULTURE_TOKEN);
|
|
40
|
+
return <span>culture.Name</span>;
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Package contents
|
|
45
|
+
|
|
46
|
+
### CultureProvider
|
|
47
|
+
|
|
48
|
+
React Component that passes culture setttings to `@servicetitan/react-ioc` container.
|
|
49
|
+
|
|
50
|
+
#### Props
|
|
51
|
+
|
|
52
|
+
| Name | Type | Description | Required |
|
|
53
|
+
| :-----: | :-------: | :---------------------: | :------: |
|
|
54
|
+
| `value` | `Culture` | Culture settings object | Yes |
|
|
55
|
+
|
|
56
|
+
### Culture
|
|
57
|
+
|
|
58
|
+
TS inteface of culture setttings.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
interface Culture {
|
|
62
|
+
Name: string;
|
|
63
|
+
DateTimeFormat: {
|
|
64
|
+
MomentShortDatePattern: string;
|
|
65
|
+
MomentShortYearDatePattern: string;
|
|
66
|
+
MomentShortYearDateTimePattern: string;
|
|
67
|
+
};
|
|
68
|
+
NumberFormat: {
|
|
69
|
+
NumberDecimalSeparator: string;
|
|
70
|
+
NumberGroupSeparator: string;
|
|
71
|
+
};
|
|
72
|
+
PhoneFormat: {
|
|
73
|
+
PhoneFormatInfo: {
|
|
74
|
+
Pattern: RegExp;
|
|
75
|
+
};
|
|
76
|
+
PhoneMask: string;
|
|
77
|
+
SimplePhoneMask: string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### CULTURE_TOKEN
|
|
83
|
+
|
|
84
|
+
InversifyJS token to resolve culture settings from `@servicetitan/react-ioc` container.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: FormState
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
import { CodeDemo } from '@site/src/components/code-demo';
|
|
6
|
+
|
|
7
|
+
`@servicetitan/form-state` is a collection of utils to simplify daily routine related to form state management.
|
|
8
|
+
|
|
9
|
+
## Utilities
|
|
10
|
+
|
|
11
|
+
### Anvil/Kendo/Semantic helpers
|
|
12
|
+
|
|
13
|
+
In order to avoid verbose `onChange` syntax we created a collection of `FieldState` extensions, each of them has custom `onChangeHandler` method that can be passed directly to `onChange` prop in JSX.
|
|
14
|
+
|
|
15
|
+
`FieldState` extensions vary by type of form element they represent e.g. for inputs there is `InputFieldState`, for dropdowns it is `DropdownFieldState` and so on.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const searchForm = new FormState({
|
|
19
|
+
jobStatus: new DropdownFieldState(JobStatus.Scheduled),
|
|
20
|
+
customerName: new InputFieldState(''),
|
|
21
|
+
details: new TextAreaFieldState(''),
|
|
22
|
+
isActive: new CheckboxFieldState(false),
|
|
23
|
+
from: new DatetimeFieldState(new Date()),
|
|
24
|
+
to: new DatetimeFieldState(new Date()),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const SearchForm: React.FC = () => (
|
|
28
|
+
<Form>
|
|
29
|
+
<Form.Dropdown
|
|
30
|
+
value={searchForm.$.jobStatus.value}
|
|
31
|
+
onChange={searchForm.$.jobStatus.onChangeHandler}
|
|
32
|
+
options={enumToOptions(JobStatus)}
|
|
33
|
+
placeholder="Job Status"
|
|
34
|
+
/>
|
|
35
|
+
<Form.Input
|
|
36
|
+
value={searchForm.$.customerName.value}
|
|
37
|
+
onChange={searchForm.$.customerName.onChangeHandler}
|
|
38
|
+
placeholder="Customer Name"
|
|
39
|
+
/>
|
|
40
|
+
<Form.TextArea
|
|
41
|
+
value={searchForm.$.details.value}
|
|
42
|
+
onChange={searchForm.$.details.onChangeHandler}
|
|
43
|
+
placeholder="Details"
|
|
44
|
+
/>
|
|
45
|
+
<Form.Checkbox
|
|
46
|
+
label="Is Active"
|
|
47
|
+
checked={searchForm.$.isActive.value}
|
|
48
|
+
onChange={searchForm.$.isActive.onChangeHandler}
|
|
49
|
+
/>
|
|
50
|
+
<DatePicker value={searchForm.$.from.value} onChange={searchForm.$.from.onChangeHandler} />
|
|
51
|
+
<DatePicker value={searchForm.$.to.value} onChange={searchForm.$.to.onChangeHandler} />
|
|
52
|
+
</Form>
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### FormValidators
|
|
57
|
+
|
|
58
|
+
- `required` - checks if value is defined
|
|
59
|
+
- `hasLowerCase` - checks if string has lower case letter
|
|
60
|
+
- `hasUpperCase` - checks if string has upper case letter
|
|
61
|
+
- `hasNumber` - checks if string has digit symbol
|
|
62
|
+
- `passwordIsValidFormat` - checks if string have more than 7 symbols length and contains at least one lower letter, upper letter, and digit
|
|
63
|
+
- `emailFormatIsValid` - checks if string contains a valid email address
|
|
64
|
+
- `isDateValid` - checks if date between `new Date(1900, 1, 1)` and `new Date(2099, 12, 31)`
|
|
65
|
+
- `isDateRangeValid` - checks if start date isn't greater than end date
|
|
66
|
+
- `isDateRangeLessThanMaxLength` - checks if date range isn't greater than maximum duration
|
|
67
|
+
- `isAlphaNumeric` - checks if string contains only letters, numbers or underscores
|
|
68
|
+
- `isMatchingRegex` - checks if string match a regular expression
|
|
69
|
+
- `maxLength` - checks if string length isn't greater than maximum
|
|
70
|
+
|
|
71
|
+
### PersistentFormState
|
|
72
|
+
|
|
73
|
+
We have extended the `FormState` library to enable saving the state of form and re-applying them to their previous condition. It encrypts the data before saving and decrypts after fetching for security concerns.
|
|
74
|
+
|
|
75
|
+
Available 3 different ways to save the current state of form:
|
|
76
|
+
|
|
77
|
+
- `Domain` - form data saved in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), it will persists through out multiple sessions
|
|
78
|
+
- `Session` - form data saved in [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage), it persists in the one session you currently are in
|
|
79
|
+
- `InMemory` - form data saved in memory, it will not persist if there is any reload/refresh of browser (no encryption is done with this method)
|
|
80
|
+
|
|
81
|
+
_Currently implemented only `InMemory` storage, `Session` and `Domain` will throw an error._
|
|
82
|
+
|
|
83
|
+
#### Constructor parameters
|
|
84
|
+
|
|
85
|
+
1. `$` - javascript object where values can be either `FieldState`s or [wrapper of FieldStates](#anvilkendosemantic-helpers) or `FormState` itself
|
|
86
|
+
2. `cacheKey` - unique string that represents the form in the storage system
|
|
87
|
+
3. `persistenceMode` - type of storage system that will be used, there is a enum of possible values called `PersistenceMode`
|
|
88
|
+
4. `autoSave` - flag indicates if the form data will be saved automatically on change
|
|
89
|
+
|
|
90
|
+
#### Methods
|
|
91
|
+
|
|
92
|
+
- `save` - saves form in the preferred storage system, must be used if autoSave is disabled.
|
|
93
|
+
- `reset` - resets all the form fields and clear the data saved in storage.
|
|
94
|
+
|
|
95
|
+
#### Example
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
const form = new PersistentFormState(
|
|
99
|
+
{
|
|
100
|
+
field1: new FieldState(''),
|
|
101
|
+
field2: new InputFieldState(''),
|
|
102
|
+
},
|
|
103
|
+
'uniqueNameOfForm',
|
|
104
|
+
PersistenceMode.InMemory,
|
|
105
|
+
true
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### formStateToJS
|
|
110
|
+
|
|
111
|
+
Creates `JSON` object that has the same structure and values as `FormState`.
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
const form = new FormState({
|
|
115
|
+
a: new FieldState(7),
|
|
116
|
+
b: new FormState({
|
|
117
|
+
c: new FieldState('Lorem ipsum'),
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
formStateToJS(form); // { a: 7, b: { c: 'Lorem ipsum' } }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### setFormStateValues
|
|
125
|
+
|
|
126
|
+
Applies new values to `FormState` based on `JSON` object with the same structure, fields aren't mentioned in the object will be skipped.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
const form = new FormState({
|
|
130
|
+
a: new FieldState(7),
|
|
131
|
+
b: new FormState({
|
|
132
|
+
c: new FieldState('Lorem ipsum'),
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
setFormStateValues(form, { b: { c: 'New value' } });
|
|
137
|
+
|
|
138
|
+
formStateToJS(form); // { a: 7, b: { c: 'New value' } }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### traverseFormState
|
|
142
|
+
|
|
143
|
+
Goes through `FormState` tree and calls callbacks for visited `FieldState`s and `FormState`s.
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
const form = new FormState({
|
|
147
|
+
a: new FieldState(7),
|
|
148
|
+
b: new FormState({
|
|
149
|
+
c: new FieldState('Lorem ipsum'),
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
traverseFormState(
|
|
154
|
+
true,
|
|
155
|
+
form,
|
|
156
|
+
() => console.log("I'm in FormState"),
|
|
157
|
+
() => console.log("I'm in FieldState")
|
|
158
|
+
);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### isFormStateChanged
|
|
162
|
+
|
|
163
|
+
Checks if any of `FieldState`s were changed.
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
const form = new FormState({
|
|
167
|
+
a: new FieldState(7),
|
|
168
|
+
});
|
|
169
|
+
const isFormChanged = isFormStateChanged(form);
|
|
170
|
+
|
|
171
|
+
isFormChanged.get(); // false
|
|
172
|
+
|
|
173
|
+
form.$.a.onChange(777);
|
|
174
|
+
|
|
175
|
+
isFormChanged.get(); // true
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### commitFormState
|
|
179
|
+
|
|
180
|
+
Makes `FieldState`s current values as initials and marks them as untouched.
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
const form = new FormState({
|
|
184
|
+
a: new FieldState(7),
|
|
185
|
+
});
|
|
186
|
+
const isFormChanged = isFormStateChanged(form);
|
|
187
|
+
|
|
188
|
+
form.$.a.onChange(777);
|
|
189
|
+
|
|
190
|
+
commitFormState(form);
|
|
191
|
+
|
|
192
|
+
isFormChanged.get(); // false
|
|
193
|
+
|
|
194
|
+
form.reset();
|
|
195
|
+
|
|
196
|
+
form.$.a.value; // 777
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### camelCaseToTitleCase
|
|
200
|
+
|
|
201
|
+
Adds spaces between words in `UpperCamelCase` notation strings, useful for enum keys transformation.
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
const key = CampaignType[CampaignType.ExpiringMemberships]; // ExpiringMemberships
|
|
205
|
+
camelCaseToTitleCase(key); // Expiring Memberships
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### enumToOptions
|
|
209
|
+
|
|
210
|
+
Creates `Option`s collection compatible with Anvil dropdowns based on enum.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
enum CampaignType {
|
|
214
|
+
Default,
|
|
215
|
+
UnsoldEstimates,
|
|
216
|
+
ExpiringMemberships,
|
|
217
|
+
IdleAccount,
|
|
218
|
+
AgingEquipment,
|
|
219
|
+
Other,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// enum keys will be used as options texts
|
|
223
|
+
enumToOptions(CampaignType);
|
|
224
|
+
|
|
225
|
+
// enums texts will be calculated using values
|
|
226
|
+
const campaignTypeOptions = enumToOptions(CampaignType, value =>
|
|
227
|
+
camelCaseToTitleCase(CampaignType[value])
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### getEnumKeys
|
|
232
|
+
|
|
233
|
+
Returns collection with all enum keys.
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
getEnumKeys(CampaignType); // ['Default', 'UnsoldEstimates', 'ExpiringMemberships', 'IdleAccount', 'AgingEquipment', 'Other']
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### getEnumValues
|
|
240
|
+
|
|
241
|
+
Returns collection with all enum values.
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
getEnumValues(CampaignType); // [0, 1, 2, 3, 4, 5]
|
|
245
|
+
```
|
package/docs/form.mdx
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Form
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
import { NumberInputExample, FileUploaderExample } from '@servicetitan/form/dist/demo';
|
|
6
|
+
|
|
7
|
+
import { CodeDemo } from '@site/src/components/code-demo';
|
|
8
|
+
|
|
9
|
+
`@servicetitan/form` is a collection of utils and components to simplify daily routine related to forms.
|
|
10
|
+
|
|
11
|
+
Form state helpers was moved to [`@servicetitan/form-state`](./form-state) package. You can import helpers from `@servicetitan/form-state` or `@servicetitan/form`.
|
|
12
|
+
|
|
13
|
+
## Components
|
|
14
|
+
|
|
15
|
+
### Label
|
|
16
|
+
|
|
17
|
+
More powerful label version, it supports error message and tooltips. Designed to be used with Anvil form controls.
|
|
18
|
+
|
|
19
|
+
#### Props
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
interface LabelProps {
|
|
23
|
+
label: string;
|
|
24
|
+
tooltip?: string;
|
|
25
|
+
tooltipDirection?: TooltipProps['direction'];
|
|
26
|
+
hasError?: boolean;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
#### Example
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
const form = new FormState({
|
|
35
|
+
name: new InputFieldState(''),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const NameInput: React.FC = () => {
|
|
39
|
+
const { name } = form.$;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Input
|
|
43
|
+
label={
|
|
44
|
+
<Label
|
|
45
|
+
label="Name"
|
|
46
|
+
tooltip="Unique display user name"
|
|
47
|
+
hasError={name.hasError}
|
|
48
|
+
error={name.error}
|
|
49
|
+
/>
|
|
50
|
+
}
|
|
51
|
+
value={name.value}
|
|
52
|
+
onChange={name.onChangeHandler}
|
|
53
|
+
error={name.hasError}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### FileUploader
|
|
60
|
+
|
|
61
|
+
Wrapper around `Resumable.js` and Anvil's `FilePicker` have to be used in most of file uploading cases.
|
|
62
|
+
|
|
63
|
+
#### Props
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
interface FileUploaderConfig {
|
|
67
|
+
target?: string;
|
|
68
|
+
downloadLink?: string;
|
|
69
|
+
chunkSize?: number;
|
|
70
|
+
testChunks?: boolean;
|
|
71
|
+
forceChunkSize?: boolean;
|
|
72
|
+
simultaneousUploads?: number;
|
|
73
|
+
headers?: Record<string, string>;
|
|
74
|
+
permanentErrors?: number[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface FileUploaderProps
|
|
78
|
+
extends Pick<FilePickerProps, 'limitReached' | 'typesNote' | 'multiple' | 'accept'> {
|
|
79
|
+
config?: FileUploaderConfig;
|
|
80
|
+
value: FileDescriptor[];
|
|
81
|
+
onChange?(value: FileDescriptor[]): void;
|
|
82
|
+
folderName: string;
|
|
83
|
+
hideReplace?: boolean;
|
|
84
|
+
hideDownload?: boolean;
|
|
85
|
+
centerAligned?: boolean;
|
|
86
|
+
layout?: 'grid' | 'list';
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Example
|
|
91
|
+
|
|
92
|
+
<CodeDemo example={FileUploaderExample} srcPath="form/src/demo/file-uploader.tsx" />
|
|
93
|
+
|
|
94
|
+
### NumberInput
|
|
95
|
+
|
|
96
|
+
Visually looks the same as regular input but only numbers can be typed. Supports min, max, and precision configuration.
|
|
97
|
+
|
|
98
|
+
#### Props
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
type EmptyValue = 0 | undefined;
|
|
102
|
+
type NumberValue<TEmpty extends EmptyValue> = TEmpty extends 0 ? number : number | undefined;
|
|
103
|
+
|
|
104
|
+
interface NumberInputProps<TEmpty extends EmptyValue>
|
|
105
|
+
extends Omit<InputProps, 'value' | 'onChange'> {
|
|
106
|
+
value: NumberValue<TEmpty>;
|
|
107
|
+
onChange(value: NumberValue<TEmpty>): void;
|
|
108
|
+
emptyValue: TEmpty;
|
|
109
|
+
decimalPlaces?: number;
|
|
110
|
+
useEmptyThousandsSeparator?: boolean;
|
|
111
|
+
min?: number;
|
|
112
|
+
max?: number;
|
|
113
|
+
useKeyboardNavigation?: boolean;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### Example
|
|
118
|
+
|
|
119
|
+
<CodeDemo example={NumberInputExample} srcPath="form/src/demo/number-input.tsx" />
|
|
120
|
+
|
|
121
|
+
### FormStateErrorBanner
|
|
122
|
+
|
|
123
|
+
Accumulates all `FieldState`s errors in a single banner.
|
|
124
|
+
|
|
125
|
+
#### Props
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
interface FormStateErrorBannerProps<T extends ValidatableMapOrArray> {
|
|
129
|
+
form: FormState<T>;
|
|
130
|
+
errorTitle?: string;
|
|
131
|
+
successTitle?: string;
|
|
132
|
+
className?: string;
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Example
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
const form = new FormState({
|
|
140
|
+
login: new InputFieldState(''),
|
|
141
|
+
password: new InputFieldState(''),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const ValidationBanner: React.FC = () => (
|
|
145
|
+
<FormStateErrorBanner
|
|
146
|
+
form={form}
|
|
147
|
+
errorTitle="Make sure if all fields valid"
|
|
148
|
+
successTitle="The form is ready to submit"
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Link Item
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
`@servicetitan/link-item` is a collection of wrappers around various navigation components adapted to work with React router.
|
|
6
|
+
|
|
7
|
+
## SideNavLinkItem
|
|
8
|
+
|
|
9
|
+
For use inside of the Anvil's [SideNav](https://anvil.servicetitan.com/#/navigation/sidenav).
|
|
10
|
+
|
|
11
|
+
### Props
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
interface SideNavLinkItemProps {
|
|
15
|
+
/** When true, will only match if the path matches the location.pathname exactly */
|
|
16
|
+
exact?: boolean;
|
|
17
|
+
/** A string representing the path to link to */
|
|
18
|
+
pathname: string;
|
|
19
|
+
/** Adds one or more classnames for an element */
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Example
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
<SideNav title="Navigation">
|
|
28
|
+
<SideNavLinkItem pathname="/" exact>
|
|
29
|
+
Home
|
|
30
|
+
</SideNavLinkItem>
|
|
31
|
+
<SideNavLinkItem pathname="/section1">Section 1</SideNavLinkItem>
|
|
32
|
+
<SideNavLinkItem pathname="/section2">Section 2</SideNavLinkItem>
|
|
33
|
+
<SideNavLinkItem pathname="/section3">Section 3</SideNavLinkItem>
|
|
34
|
+
</SideNav>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## TabLinkItem
|
|
38
|
+
|
|
39
|
+
For use inside of the Anvil's [TabGroup](https://anvil.servicetitan.com/#/navigation/tab).
|
|
40
|
+
|
|
41
|
+
### Props
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
interface TabLinkItemProps {
|
|
45
|
+
/** When true, will only match if the path matches the location.pathname exactly */
|
|
46
|
+
exact?: boolean;
|
|
47
|
+
/** A string representing the path to link to */
|
|
48
|
+
pathname: string;
|
|
49
|
+
/** Adds one or more classnames for an element */
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Example
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<TabGroup title="Navigation">
|
|
58
|
+
<TabLinkItem pathname="/" exact>
|
|
59
|
+
Home
|
|
60
|
+
</TabLinkItem>
|
|
61
|
+
<TabLinkItem pathname="/section1">Section 1</TabLinkItem>
|
|
62
|
+
<TabLinkItem pathname="/section2">Section 2</TabLinkItem>
|
|
63
|
+
<TabLinkItem pathname="/section3">Section 3</TabLinkItem>
|
|
64
|
+
</TabGroup>
|
|
65
|
+
```
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Notifications Center
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
import Tabs from '@theme/Tabs';
|
|
6
|
+
import TabItem from '@theme/TabItem';
|
|
7
|
+
import CodeBlock from '@theme/CodeBlock';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
BasicExample,
|
|
11
|
+
StatusVariationsExample,
|
|
12
|
+
DurationExample,
|
|
13
|
+
ActionButtonExample,
|
|
14
|
+
ProgressExample,
|
|
15
|
+
PreventDuplicatesExample,
|
|
16
|
+
MultilineMessageExample,
|
|
17
|
+
ServerDefaultExample,
|
|
18
|
+
ServerCustomExample,
|
|
19
|
+
} from '@servicetitan/notifications/dist/demo';
|
|
20
|
+
|
|
21
|
+
import { CodeDemo } from '@site/src/components/code-demo';
|
|
22
|
+
|
|
23
|
+
There're a lot of cases when a user should be notified in the result of various events. It could be from a simple one-time notification about the result of the network request to informing about the state of a complex long-running process launched on the worker.
|
|
24
|
+
|
|
25
|
+
Notification Center was created to simplify and generalize everything related to user notifications. It provides few touchpoints: `NotificationCenter` to send various notification types from the backend, `NotificationsService` to show messages on the frontend, and `register` function to configure rendering of notifications arrived from the backend.
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
### Client-side notifications
|
|
30
|
+
|
|
31
|
+
To initiate notifications from the client side it's enough to call one of the `NotificationsService` methods. They accept `options` - a configuration of the visible part of notification which should cover most of the use cases and `preventDuplicates` flag used to avoid rendering of notifications that are already visible to a user.
|
|
32
|
+
|
|
33
|
+
#### Basic
|
|
34
|
+
|
|
35
|
+
To show simple notification it's just enough to pass title and maybe in various cases, you'll also need a message.
|
|
36
|
+
|
|
37
|
+
<CodeDemo example={BasicExample} srcPath="notifications/src/demo/basic-preview.tsx" />
|
|
38
|
+
|
|
39
|
+
#### Status Variations
|
|
40
|
+
|
|
41
|
+
Depends on situations various color schemes could be used.
|
|
42
|
+
|
|
43
|
+
<CodeDemo
|
|
44
|
+
example={StatusVariationsExample}
|
|
45
|
+
srcPath="notifications/src/demo/status-variations-preview.tsx"
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
#### Duration
|
|
49
|
+
|
|
50
|
+
By default, notification automatically disappears in 8 seconds. However, it can be configured. Zero value could be used to avoid automatically disappearing.
|
|
51
|
+
|
|
52
|
+
<CodeDemo example={DurationExample} srcPath="notifications/src/demo/duration-preview.tsx" />
|
|
53
|
+
|
|
54
|
+
#### Action Button
|
|
55
|
+
|
|
56
|
+
Sometimes it could be useful to add a button to suggest some actions to the user, it can be from a simple URL redirection to complex calculations encapsulated in a handler function.
|
|
57
|
+
|
|
58
|
+
<CodeDemo
|
|
59
|
+
example={ActionButtonExample}
|
|
60
|
+
srcPath="notifications/src/demo/action-button-preview.tsx"
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
#### Progress
|
|
64
|
+
|
|
65
|
+
Also, it's possible to show a progress bar in a notification.
|
|
66
|
+
|
|
67
|
+
<CodeDemo example={ProgressExample} srcPath="notifications/src/demo/progress-preview.tsx" />
|
|
68
|
+
|
|
69
|
+
#### Prevent Duplicates
|
|
70
|
+
|
|
71
|
+
Sometimes it can be redundant to show equal notifications to the user, in that case, you can easily achieve this with `preventDuplicates` option.
|
|
72
|
+
|
|
73
|
+
<CodeDemo
|
|
74
|
+
example={PreventDuplicatesExample}
|
|
75
|
+
srcPath="notifications/src/demo/prevent-duplicates-preview.tsx"
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
#### Multiline Message
|
|
79
|
+
|
|
80
|
+
Notification message supports the newline control character, so if you need to show something list like just add `\r\n` between rows.
|
|
81
|
+
|
|
82
|
+
<CodeDemo
|
|
83
|
+
example={MultilineMessageExample}
|
|
84
|
+
srcPath="notifications/src/demo/multiline-message-preview.tsx"
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
### Server-side notifications
|
|
88
|
+
|
|
89
|
+
Notification Center will automatically show notifications to the user sent from the backend and also guarantee delivery in cases when the user is offline. Right now it only covers the scenario of notifying a single user, it couldn't be used for something like broadcast.
|
|
90
|
+
|
|
91
|
+
#### Basic
|
|
92
|
+
|
|
93
|
+
Most of the cases could be easily covered with default notification type and `DefaultPayload` container, it's also the preferable way.
|
|
94
|
+
|
|
95
|
+
export const defaultServerCode =
|
|
96
|
+
'' +
|
|
97
|
+
'IUserNotifier notification;\r\n' +
|
|
98
|
+
'\r\n' +
|
|
99
|
+
'var payload = new DefaultPayload {\r\n' +
|
|
100
|
+
' Title = "Server Notification",\r\n' +
|
|
101
|
+
' Message = "Task was pushed into queue."\r\n' +
|
|
102
|
+
'};\r\n' +
|
|
103
|
+
'notification = await notificationCenter.CreateNotifierAsync(\r\n' +
|
|
104
|
+
' GetUserId(),\r\n' +
|
|
105
|
+
' initialPayload: payload\r\n' +
|
|
106
|
+
');\r\n' +
|
|
107
|
+
'\r\n' +
|
|
108
|
+
'await Task.Delay(2000);\r\n' +
|
|
109
|
+
'\r\n' +
|
|
110
|
+
'payload.Message = "Task is running.";\r\n' +
|
|
111
|
+
'await notification.OnChangePayload(\r\n' +
|
|
112
|
+
' payload,\r\n' +
|
|
113
|
+
' UserNotificationStatus.InProgress\r\n' +
|
|
114
|
+
');\r\n' +
|
|
115
|
+
'\r\n' +
|
|
116
|
+
'for (int progress = 0; progress < 100; progress += 5) {\r\n' +
|
|
117
|
+
' payload.Progress = progress;\r\n' +
|
|
118
|
+
' await notification.OnChangePayload(payload);\r\n' +
|
|
119
|
+
'\r\n' +
|
|
120
|
+
' await Task.Delay(250);\r\n' +
|
|
121
|
+
'}\r\n' +
|
|
122
|
+
'\r\n' +
|
|
123
|
+
'payload.Message = "Task was successfully completed.";\r\n' +
|
|
124
|
+
'payload.Progress = null;\r\n' +
|
|
125
|
+
'await notification.OnChangePayload(\r\n' +
|
|
126
|
+
' payload,\r\n' +
|
|
127
|
+
' UserNotificationStatus.Success\r\n' +
|
|
128
|
+
');';
|
|
129
|
+
|
|
130
|
+
<Tabs
|
|
131
|
+
defaultValue="example"
|
|
132
|
+
values={[
|
|
133
|
+
{ label: 'Example', value: 'example' },
|
|
134
|
+
{ label: 'Server', value: 'server' },
|
|
135
|
+
]}
|
|
136
|
+
>
|
|
137
|
+
<TabItem value="example">
|
|
138
|
+
<ServerDefaultExample />
|
|
139
|
+
</TabItem>
|
|
140
|
+
<TabItem value="server">
|
|
141
|
+
<CodeBlock className="csharp">{defaultServerCode}</CodeBlock>
|
|
142
|
+
</TabItem>
|
|
143
|
+
</Tabs>
|
|
144
|
+
|
|
145
|
+
#### Custom notification component
|
|
146
|
+
|
|
147
|
+
But if you need to show something more specific, then you can create a custom notification type and register renderer for it, which should determine what would be shown to a user depending on a notification status.
|
|
148
|
+
|
|
149
|
+
**_It's required to register all your custom renderers in the `custom-notifications.ts` file on the module level, which should be imported in your module file._**
|
|
150
|
+
|
|
151
|
+
export const customServerCode =
|
|
152
|
+
'' +
|
|
153
|
+
'public class CustomPayload : INotificationPayload\r\n' +
|
|
154
|
+
'{\r\n' +
|
|
155
|
+
' public string Username { get; set; }\r\n' +
|
|
156
|
+
' public string Message { get; set; }\r\n' +
|
|
157
|
+
'}\r\n' +
|
|
158
|
+
'\r\n' +
|
|
159
|
+
'await notificationCenter.CreateNotifierAsync(\r\n' +
|
|
160
|
+
' GetUserId(),\r\n' +
|
|
161
|
+
' "CustomServerNotification",\r\n' +
|
|
162
|
+
' initialPayload: new CustomPayload {\r\n' +
|
|
163
|
+
' Username = "Jane",\r\n' +
|
|
164
|
+
' Message = "Hello, your order completed."\r\n' +
|
|
165
|
+
' }\r\n' +
|
|
166
|
+
');';
|
|
167
|
+
|
|
168
|
+
<Tabs
|
|
169
|
+
defaultValue="example"
|
|
170
|
+
values={[
|
|
171
|
+
{ label: 'Example', value: 'example' },
|
|
172
|
+
{ label: 'Client', value: 'client' },
|
|
173
|
+
{ label: 'Server', value: 'server' },
|
|
174
|
+
]}
|
|
175
|
+
>
|
|
176
|
+
<TabItem value="example">
|
|
177
|
+
<ServerCustomExample />
|
|
178
|
+
</TabItem>
|
|
179
|
+
<TabItem value="client">
|
|
180
|
+
<CodeBlock className="ts">
|
|
181
|
+
{require('@servicetitan/notifications/src/demo/server-custom-preview.tsx?raw')}
|
|
182
|
+
</CodeBlock>
|
|
183
|
+
</TabItem>
|
|
184
|
+
<TabItem value="server">
|
|
185
|
+
<CodeBlock className="csharp">{customServerCode}</CodeBlock>
|
|
186
|
+
</TabItem>
|
|
187
|
+
</Tabs>
|
|
188
|
+
|
|
189
|
+
## API Reference
|
|
190
|
+
|
|
191
|
+
### NotificationCenter
|
|
192
|
+
|
|
193
|
+
```csharp
|
|
194
|
+
public interface INotificationCenter {
|
|
195
|
+
Task<IUserNotifier> CreateNotifierAsync(
|
|
196
|
+
long userId,
|
|
197
|
+
string type = "DefaultServerNotification",
|
|
198
|
+
UserNotificationStatus initialStatus = UserNotificationStatus.Info,
|
|
199
|
+
INotificationPayload initialPayload = null
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
IUserNotifier GetNotifier(long userId, long id);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public enum UserNotificationStatus {
|
|
206
|
+
Info = 0,
|
|
207
|
+
InProgress = 1,
|
|
208
|
+
Success = 2,
|
|
209
|
+
Failure = 3
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public interface INotificationPayload {}
|
|
213
|
+
|
|
214
|
+
public interface IUserNotifier {
|
|
215
|
+
Task<NotificationUpdateResult> OnChangeStatus(
|
|
216
|
+
UserNotificationStatus newStatus
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
Task<NotificationUpdateResult> OnChangePayload(
|
|
220
|
+
INotificationPayload notificationPayload,
|
|
221
|
+
UserNotificationStatus? newStatus = null
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### DefaultPayload
|
|
227
|
+
|
|
228
|
+
```csharp
|
|
229
|
+
public class DefaultPayload : INotificationPayload {
|
|
230
|
+
public string Title { get; set; }
|
|
231
|
+
public string Message { get; set; }
|
|
232
|
+
public int? Progress { get; set; }
|
|
233
|
+
public LinkAction Action { get; set; }
|
|
234
|
+
|
|
235
|
+
public class LinkAction {
|
|
236
|
+
public string Label { get; set; }
|
|
237
|
+
public string Link { get; set; }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### NotificationsService
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
interface NotificationsService {
|
|
246
|
+
show(options: DefaultNotificationOptions, preventDuplicates?: boolean): void;
|
|
247
|
+
|
|
248
|
+
info(options: Omit<DefaultNotificationOptions, 'status'>, preventDuplicates?: boolean): void;
|
|
249
|
+
|
|
250
|
+
success(options: Omit<DefaultNotificationOptions, 'status'>, preventDuplicates?: boolean): void;
|
|
251
|
+
|
|
252
|
+
warning(options: Omit<DefaultNotificationOptions, 'status'>, preventDuplicates?: boolean): void;
|
|
253
|
+
|
|
254
|
+
error(options: Omit<DefaultNotificationOptions, 'status'>, preventDuplicates?: boolean): void;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface DefaultNotificationOptions {
|
|
258
|
+
status?: Status;
|
|
259
|
+
title: string;
|
|
260
|
+
message?: string;
|
|
261
|
+
duration?: number;
|
|
262
|
+
progress?: number;
|
|
263
|
+
action?: LinkAction | FunctionAction;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
enum Status {
|
|
267
|
+
Info = 'info',
|
|
268
|
+
Success = 'success',
|
|
269
|
+
Warning = 'warning',
|
|
270
|
+
Error = 'critical',
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface LinkAction {
|
|
274
|
+
label: string;
|
|
275
|
+
link: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface FunctionAction {
|
|
279
|
+
label: string;
|
|
280
|
+
onClick(): void;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### register
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
register(type: string, mapper: NotificationMapper): void;
|
|
288
|
+
|
|
289
|
+
type NotificationMapper = React.ComponentType<{
|
|
290
|
+
notification: Notification;
|
|
291
|
+
onClose(): void;
|
|
292
|
+
}>;
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## FAQ
|
|
296
|
+
|
|
297
|
+
### What is the delivery logic, what happens if user has several windows open?
|
|
298
|
+
|
|
299
|
+
Notifications will be delivered to each window.
|
|
300
|
+
|
|
301
|
+
### What happens if user closes window without closing notification?
|
|
302
|
+
|
|
303
|
+
Unclosed notifications will be shown again on application reopen.
|
|
304
|
+
|
|
305
|
+
### How will be displayed a large number of notifications?
|
|
306
|
+
|
|
307
|
+
Only 5 notifications will be shown at a time, the rest will appear after displayed ones closing.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Skeleton
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
import { AutoExample } from '@servicetitan/skeleton/dist/demo/auto';
|
|
6
|
+
import { ManualExample } from '@servicetitan/skeleton/dist/demo/manual';
|
|
7
|
+
|
|
8
|
+
import { CodeDemo } from '@site/src/components/code-demo';
|
|
9
|
+
|
|
10
|
+
`@servicetitan/skeleton` allows you to create fancy skeletons for the loading state. It provides a set of global CSS-styles that can be used to add skeletons over any component.
|
|
11
|
+
|
|
12
|
+
This package doesn't contain any components. But you can use the same components that you use in your project - make it dumb, provide some fake data, and add skeleton classes while data is loading. It will make your skeletons look the same as real components, which is a very fancy UI.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
To add global styles, install `@servicetitan/skeleton` and add the following code to your global CSS.
|
|
17
|
+
|
|
18
|
+
```css
|
|
19
|
+
@import '~@servicetitan/skeleton/dist/styles.css';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Auto-styles
|
|
23
|
+
|
|
24
|
+
`skeleton-auto` is a global CSS-class that automatically applies skeleton background for basic anvil components. You just need to add this class to your component when it is in loading state.
|
|
25
|
+
|
|
26
|
+
<CodeDemo example={AutoExample} srcPath="skeleton/src/demo/auto.tsx" />
|
|
27
|
+
|
|
28
|
+
### Manual-styles
|
|
29
|
+
|
|
30
|
+
`skeleton`, `skeleton-item`, and `skeleton-text` is a global CSS-classes that allows you to add skeleton-functionality to any component you want. You just need to add the `skeleton-item` or `skeleton-text` class to all child components you need and then add the `skeleton` class for the parent component when it should be in a loading state.
|
|
31
|
+
|
|
32
|
+
The difference between `skeleton-item` and `skeleton-text` is that `skeleton-text` adds small border-radius. Use it for text components.
|
|
33
|
+
|
|
34
|
+
<CodeDemo example={ManualExample} srcPath="skeleton/src/demo/manual.tsx" />
|
|
35
|
+
|
|
36
|
+
### Fixed width texts
|
|
37
|
+
|
|
38
|
+
If your component contains several text lines close to each other, they usually have different widths in a loading state. You can use `skeleton-text-75`, `skeleton-text-50` or `skeleton-text-33` CSS-classes to achieve this. You can use it both with `skeleton` and `skeleton-auto` parent classes.
|
package/docs/table.mdx
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Table
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
TableExample,
|
|
7
|
+
TableMasterDetailExample,
|
|
8
|
+
TableStateCachingExample,
|
|
9
|
+
} from '@servicetitan/table/dist/demo';
|
|
10
|
+
|
|
11
|
+
import { CodeDemo } from '@site/src/components/code-demo';
|
|
12
|
+
|
|
13
|
+
`@servicetitan/table` is a table implementation we use to display large data sets that require filtering, sorting, pagination and grouping. Use-cases of Table include, but not limited to Reporting Engine, Pricebook Pro, Phones Pro and Feature Management.
|
|
14
|
+
|
|
15
|
+
Package encapsulates and re-exports table components from `@servicetitan/design-system` with additional set of extensions and MobX state management.
|
|
16
|
+
|
|
17
|
+
For more info about available options and settings visit [Design System website](https://anvil.servicetitan.com/components/visualization/table/).
|
|
18
|
+
|
|
19
|
+
## Data Sources
|
|
20
|
+
|
|
21
|
+
`DataSource` is an abstraction used to provide items to the `TableState`.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
interface DataSource<T, TId extends IdType = never> {
|
|
25
|
+
/** Gets the items, applying the specified operation descriptors */
|
|
26
|
+
getData(params: State): Promise<ProcessedData<T>>;
|
|
27
|
+
/** Adds an item to the data source to `index` position (to the end if omitted) */
|
|
28
|
+
addData?(row: T, index?: number): Promise<T>;
|
|
29
|
+
/** Applies changes for an item by id */
|
|
30
|
+
updateData?(id: TId, changes: Partial<T>): Promise<void>;
|
|
31
|
+
/** Remove an item by id */
|
|
32
|
+
removeData?(id: TId): Promise<T | undefined>;
|
|
33
|
+
/** Returns unique identifier by item, must be persistent */
|
|
34
|
+
idSelector?(row: T): TId;
|
|
35
|
+
/** Gets all items excluded by the filter (only for persistent data sources) */
|
|
36
|
+
getFilteredPersistentItems?: (filter: CompositeFilterDescriptor) => T[];
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### InMemoryDataSource
|
|
41
|
+
|
|
42
|
+
It's a persistent data source contains the whole collection of items, operations return a deep copy of items.
|
|
43
|
+
|
|
44
|
+
#### Constructor parameters
|
|
45
|
+
|
|
46
|
+
1. `data` - collection of items
|
|
47
|
+
2. `idSelector` - method returns unique identifier by item
|
|
48
|
+
3. `preprocessors` - object with item fields transformations, using for sorting, filtering, and other operations, transformations don't affect on item objects
|
|
49
|
+
|
|
50
|
+
### AsyncDataSource
|
|
51
|
+
|
|
52
|
+
It doesn't store any items inside and used to handle server-side table operations.
|
|
53
|
+
|
|
54
|
+
#### Constructor parameters
|
|
55
|
+
|
|
56
|
+
1. `operations` - object with get/add/update/remove methods
|
|
57
|
+
2. `idSelector` - method returns unique identifier by item
|
|
58
|
+
|
|
59
|
+
## TableState
|
|
60
|
+
|
|
61
|
+
`TableState` makes all dirty `Table`'s job: it stores state, applies operations, paging and etc.
|
|
62
|
+
|
|
63
|
+
### Constructor parameters
|
|
64
|
+
|
|
65
|
+
- `dataSource` - an instance of `DataSource`, source of items for table
|
|
66
|
+
- `getDetailTableState` - method returns detail table state for an item, used to `Master/Detail` tables
|
|
67
|
+
- `getFormState` - method returns form state for an item, used to editable tables
|
|
68
|
+
- `isRowUnselectable` - method used to disable some items selection
|
|
69
|
+
- `pageSize` - number of items per page, paging is disabled if omitted
|
|
70
|
+
- `parent` - object with parent item and table state, used to `Master/Detail` tables
|
|
71
|
+
- `selectionLimit` - number of items can be selected, unlimited if omitted
|
|
72
|
+
- `initialState` - `TableStateModel` object with initial state values
|
|
73
|
+
|
|
74
|
+
### Properties
|
|
75
|
+
|
|
76
|
+
- `aggregates` - applied aggregate descriptors
|
|
77
|
+
- `data` - items shown at current page
|
|
78
|
+
- `dataSource` - currently used data source instance
|
|
79
|
+
- `filter` - applied filter descriptors
|
|
80
|
+
- `filteredCount` - number of items after application current filters
|
|
81
|
+
- `group` - applied group descriptors
|
|
82
|
+
- `isAllPageRowsSelected` - indicates if all current page items selected
|
|
83
|
+
- `isAllRowsSelected` - indicates if all items selected
|
|
84
|
+
- `isSomePageRowsSelected` - indicates if some current page items selected but not all
|
|
85
|
+
- `isSomeRowsSelected` - indicates if some items selected but not all
|
|
86
|
+
- `selectableCount` - number of items can be selected
|
|
87
|
+
- `selectedCount` - number of selected items
|
|
88
|
+
- `skip` - number of items on previous pages
|
|
89
|
+
- `sort` - applied sort descriptors
|
|
90
|
+
- `totalCount` - number of items in data source
|
|
91
|
+
- `totalFilteredSelectableCount` - number of items can be selected after application current filters
|
|
92
|
+
|
|
93
|
+
### Methods
|
|
94
|
+
|
|
95
|
+
- `addToDataSource` - adds an item to data source and refresh current page
|
|
96
|
+
- `cancelEdit` - discards item modifications
|
|
97
|
+
- `cancelEditAll` - discards all items modifications
|
|
98
|
+
- `deselectAll` - deselects all items
|
|
99
|
+
- `deselectPage` - deselects all current page items
|
|
100
|
+
- `edit` - runs edit mode for item
|
|
101
|
+
- `editAll` - runs edit mode for all items
|
|
102
|
+
- `exportExcel` - initiates excel export process
|
|
103
|
+
- `exportPdf` - initiates pdf export process
|
|
104
|
+
- `fetchData` - applies new descriptors values and refresh current page (only mentioned descriptors will be changed)
|
|
105
|
+
- `removeFromDataSource` - removes an item from data source and refresh current page
|
|
106
|
+
- `reset` - resets table to initial state
|
|
107
|
+
- `saveEdit` - saves item modifications
|
|
108
|
+
- `saveEditAll` - saves all items modifications
|
|
109
|
+
- `selectAll` - selects all items
|
|
110
|
+
- `selectPage` - selects all current page items
|
|
111
|
+
- `setDataSource` - replaces active data source and goes to the first page
|
|
112
|
+
- `setRowsSelection` - changes selection of items collection
|
|
113
|
+
- `toggleRowSelection` - toggles item selection
|
|
114
|
+
- `exportState` - gets `TableStateModel` object
|
|
115
|
+
- `importState` - sets state values from `TableStateModel`
|
|
116
|
+
|
|
117
|
+
## Table
|
|
118
|
+
|
|
119
|
+
### Props
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
type ExcludedTableProps =
|
|
123
|
+
| 'data'
|
|
124
|
+
| 'editField'
|
|
125
|
+
| 'filter'
|
|
126
|
+
| 'onFilterChange'
|
|
127
|
+
| 'group'
|
|
128
|
+
| 'onGroupChange'
|
|
129
|
+
| 'onExpandChange'
|
|
130
|
+
| 'expandField'
|
|
131
|
+
| 'sort'
|
|
132
|
+
| 'onSortChange'
|
|
133
|
+
| 'onHeaderSelectionChange'
|
|
134
|
+
| 'onSelectionChange'
|
|
135
|
+
| 'selectedField'
|
|
136
|
+
| 'pageable'
|
|
137
|
+
| 'pageSize'
|
|
138
|
+
| 'skip'
|
|
139
|
+
| 'onPageChange'
|
|
140
|
+
| 'total';
|
|
141
|
+
|
|
142
|
+
export interface TableProps<T, TId extends IdType = any, P = never, PId extends IdType = never>
|
|
143
|
+
extends Omit<AnvilTableProps, ExcludedTableProps> {
|
|
144
|
+
/** Enables items selection, data source should support `idSelector` */
|
|
145
|
+
selectable?: boolean;
|
|
146
|
+
/** Enables table export */
|
|
147
|
+
exportable?: boolean;
|
|
148
|
+
/** Hides checkbox in the header row, works with `selectable` property */
|
|
149
|
+
hideSelectAll?: boolean;
|
|
150
|
+
/** Name of export result file, works with `exportable` property */
|
|
151
|
+
exportFileName?: string;
|
|
152
|
+
/** Enables loading spinner */
|
|
153
|
+
loading?: boolean;
|
|
154
|
+
/** An instance of `TableState` which must be shown */
|
|
155
|
+
tableState: TableState<T, TId, P, PId>;
|
|
156
|
+
/** Enables zebra-striping for table rows, `true` by default */
|
|
157
|
+
striped?: boolean;
|
|
158
|
+
/** Enables cell borders, `horizontal` is `true` by default if `striped` is `false` */
|
|
159
|
+
borders?: { vertical?: boolean; horizontal?: boolean };
|
|
160
|
+
/** Adds one or more classnames for an element */
|
|
161
|
+
className?: string;
|
|
162
|
+
/** Custom selection component, works with `selectable` property */
|
|
163
|
+
selectionControl?: SelectionControlType;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Examples
|
|
168
|
+
|
|
169
|
+
#### Basic
|
|
170
|
+
|
|
171
|
+
Simple example with some out of the box functionality.
|
|
172
|
+
|
|
173
|
+
<CodeDemo example={TableExample} srcPath="table/src/demo/overview/table.tsx" />
|
|
174
|
+
|
|
175
|
+
_This example consists of several files, the full code can be found [here](https://github.com/servicetitan/uikit/tree/master/packages/table/src/demo/overview)._
|
|
176
|
+
|
|
177
|
+
#### Detail Row
|
|
178
|
+
|
|
179
|
+
Table rows can expand and show additional content related to the open row. Another useful supported feature is a row selection in detail row that is connected with a global selection state.
|
|
180
|
+
|
|
181
|
+
<CodeDemo
|
|
182
|
+
example={TableMasterDetailExample}
|
|
183
|
+
srcPath="table/src/demo/master-detail/table-master-detail.tsx"
|
|
184
|
+
/>
|
|
185
|
+
|
|
186
|
+
_This example consists of several files, the full code can be found [here](https://github.com/servicetitan/uikit/tree/master/packages/table/src/demo/master-detail)._
|
|
187
|
+
|
|
188
|
+
#### State Caching (`TableStateModel`)
|
|
189
|
+
|
|
190
|
+
Table State can be exported to restore it later. In the example below, we can switch between two states without losing the previous state.
|
|
191
|
+
|
|
192
|
+
<CodeDemo
|
|
193
|
+
example={TableStateCachingExample}
|
|
194
|
+
srcPath="table/src/demo/state-caching/state-caching-table.tsx"
|
|
195
|
+
/>
|
|
196
|
+
|
|
197
|
+
_This example consists of several files, the full code can be found [here](https://github.com/servicetitan/uikit/tree/master/packages/table/src/demo/state-caching)._
|
|
198
|
+
|
|
199
|
+
## Utility
|
|
200
|
+
|
|
201
|
+
### useObservingTableState
|
|
202
|
+
|
|
203
|
+
`useObservingTableState` is a utility hook to pass observable data and update table contents automatically right inside your React component.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
const tableState = useTableState(
|
|
207
|
+
data, // usual observable data array
|
|
208
|
+
{ pageSize: 5 }, // TableState constructor parameters
|
|
209
|
+
{ idSelector: p => p.ProductID }, // InMemoryDataSource constructor parameters
|
|
210
|
+
ResetPaginationMode.Never // when to reset pagination (see below)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
<Table tableState={tableState} sortable groupable>
|
|
214
|
+
// some columns
|
|
215
|
+
</Table>;
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`ResetPaginationMode` is a parameter indicating whether you would like to reset the current table page on data update or not. Possible modes are:
|
|
219
|
+
|
|
220
|
+
- `Never` - never reset the current page (except in cases when new data does not contain enough pages)
|
|
221
|
+
- `Always` - the current page will be reset on any data update
|
|
222
|
+
- `OnUpdateDataRoot` (default) - the current page will be reset when the reference to the `data` variable changes. When only the inner contents of the array are changed - the table page is kept.
|
|
223
|
+
|
|
224
|
+
You can check the demonstration of different pagination reset modes at [anvil-uikit-contrib's Storybook page](https://anvil-uikit-contrib.st.dev/?path=/story/table-useobservingtablestate)
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@servicetitan/docs-anvil-uikit-contrib",
|
|
3
|
+
"version": "25.0.1-canary.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/servicetitan/anvil-uikit-contrib.git",
|
|
8
|
+
"directory": "packages/docs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"docs"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"cli": {
|
|
17
|
+
"webpack": false
|
|
18
|
+
}
|
|
19
|
+
}
|