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