@visns-studio/visns-components 6.20.1 → 6.23.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/package.json +1 -1
- package/src/components/DataGrid.jsx +15 -2
- package/src/components/Navigation.jsx +10 -1
- package/src/components/columns/SelectFilterEditor.jsx +372 -0
- package/src/components/controls/DataGridSearch.jsx +6 -1
- package/src/components/layout/AnchoredMenu.jsx +131 -0
- package/src/components/layout/BandedList.jsx +158 -0
- package/src/components/layout/FactsBand.jsx +150 -0
- package/src/components/layout/PageHeader.jsx +90 -0
- package/src/components/layout/ScrollRegion.jsx +53 -0
- package/src/components/layout/StatStrip.jsx +170 -0
- package/src/components/layout/useContainedHeight.js +82 -0
- package/src/components/navigation/TabStrip.jsx +156 -0
- package/src/components/phone/ZoomPhoneBadge.jsx +625 -0
- package/src/components/phone/phonePresenceEndpoints.js +34 -0
- package/src/components/phone/phonePresenceHelpers.js +298 -0
- package/src/components/phone/useZoomPhoneLive.js +165 -0
- package/src/components/sms/SmsClientConversations.jsx +8 -0
- package/src/components/sms/SmsInbox.jsx +11 -0
- package/src/components/sms/SmsThreadPanel.jsx +258 -29
- package/src/components/sms/smsHelpers.js +95 -0
- package/src/components/styles/AnchoredMenu.module.scss +43 -0
- package/src/components/styles/BandedList.module.scss +278 -0
- package/src/components/styles/DataGrid.module.scss +10 -0
- package/src/components/styles/FactsBand.module.scss +108 -0
- package/src/components/styles/GenericDetail.module.scss +12 -2
- package/src/components/styles/PageHeader.module.scss +98 -0
- package/src/components/styles/ScrollRegion.module.scss +20 -0
- package/src/components/styles/Sms.module.scss +131 -0
- package/src/components/styles/StatStrip.module.scss +183 -0
- package/src/components/styles/TabStrip.module.scss +96 -0
- package/src/components/styles/Vault.module.scss +46 -0
- package/src/components/styles/ZoomPhone.module.scss +550 -0
- package/src/components/styles/_surface.scss +49 -0
- package/src/components/styles/global-datagrid.css +23 -0
- package/src/components/vault/VaultAccessLog.jsx +24 -0
- package/src/components/vault/VaultShareModal.jsx +126 -5
- package/src/components/vault/vaultShares.js +67 -3
- package/src/index.js +88 -2
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { toast } from 'react-toastify';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AlertTriangle,
|
|
5
|
+
Check,
|
|
6
|
+
Copy,
|
|
7
|
+
Link2,
|
|
8
|
+
Loader2,
|
|
9
|
+
Mail,
|
|
10
|
+
ShieldAlert,
|
|
11
|
+
Trash2,
|
|
12
|
+
} from 'lucide-react';
|
|
4
13
|
|
|
5
14
|
import CustomFetch from '../Fetch';
|
|
6
15
|
import StandardModal from '../generic/StandardModal';
|
|
@@ -14,6 +23,8 @@ import {
|
|
|
14
23
|
availableShareFields,
|
|
15
24
|
initialShareFields,
|
|
16
25
|
isShareRevocable,
|
|
26
|
+
shareEmailEnabled,
|
|
27
|
+
shareEmailIssue,
|
|
17
28
|
shareFieldsLabel,
|
|
18
29
|
shareRequestBody,
|
|
19
30
|
shareStatus,
|
|
@@ -51,6 +62,19 @@ const STATUS_CLASS = {
|
|
|
51
62
|
* the recipient reveals — never the seed. That is said on the checkbox, not
|
|
52
63
|
* just in this comment, because it is the one thing about this dialog a person
|
|
53
64
|
* could reasonably get wrong.
|
|
65
|
+
*
|
|
66
|
+
* EMAILING THE LINK is optional and changes none of the above. The server sends
|
|
67
|
+
* it at creation — the only moment the raw URL exists on that side — and the
|
|
68
|
+
* mail carries the entry's title, the link and its limits, never a value off
|
|
69
|
+
* the credential. Two consequences show up in this file:
|
|
70
|
+
*
|
|
71
|
+
* - The URL is still shown here, always, exactly as before. An email is a
|
|
72
|
+
* delivery, not a substitute for the one copy of the link, and a send that
|
|
73
|
+
* quietly failed must not take the credential with it — which is why the
|
|
74
|
+
* server answers 201 with `email_error` rather than an error status, and why
|
|
75
|
+
* the success panel below reports either outcome next to a copyable URL.
|
|
76
|
+
* - Whether the field appears at all is the server's call, read from
|
|
77
|
+
* `meta.email_enabled` on the share list this modal already fetches.
|
|
54
78
|
*/
|
|
55
79
|
const VaultShareModal = ({ entry, endpoints, isOpen = true, onClose }) => {
|
|
56
80
|
const routes = useMemo(() => resolveVaultEndpoints(endpoints), [endpoints]);
|
|
@@ -60,6 +84,12 @@ const VaultShareModal = ({ entry, endpoints, isOpen = true, onClose }) => {
|
|
|
60
84
|
const [fields, setFields] = useState(() => initialShareFields(entry));
|
|
61
85
|
const [expiresInHours, setExpiresInHours] = useState(DEFAULT_EXPIRY_HOURS);
|
|
62
86
|
const [maxViews, setMaxViews] = useState(null);
|
|
87
|
+
const [recipient, setRecipient] = useState('');
|
|
88
|
+
|
|
89
|
+
// Whether the server will email at all. Optimistic until the list answers:
|
|
90
|
+
// a field that flickers away is better than one that never appears because
|
|
91
|
+
// a request was slow, and the server refuses a recipient it did not offer.
|
|
92
|
+
const [emailEnabled, setEmailEnabled] = useState(true);
|
|
63
93
|
|
|
64
94
|
const [creating, setCreating] = useState(false);
|
|
65
95
|
// The one and only copy of a freshly minted URL. Held in component state
|
|
@@ -97,6 +127,9 @@ const VaultShareModal = ({ entry, endpoints, isOpen = true, onClose }) => {
|
|
|
97
127
|
if (!mountedRef.current) return;
|
|
98
128
|
|
|
99
129
|
setShares(Array.isArray(result?.data) ? result.data : []);
|
|
130
|
+
// The one place this modal can learn its own configuration:
|
|
131
|
+
// the list request it was already making.
|
|
132
|
+
setEmailEnabled(shareEmailEnabled(result));
|
|
100
133
|
setLoading(false);
|
|
101
134
|
},
|
|
102
135
|
null
|
|
@@ -120,6 +153,11 @@ const VaultShareModal = ({ entry, endpoints, isOpen = true, onClose }) => {
|
|
|
120
153
|
|
|
121
154
|
const toggle = (key) => setFields((current) => toggleShareField(current, key));
|
|
122
155
|
|
|
156
|
+
// A typo caught here is a sentence under the field; the same typo caught by
|
|
157
|
+
// the server is a 422 — and the server validates before it mints, so no
|
|
158
|
+
// link is lost either way. This is politeness, not a control.
|
|
159
|
+
const emailIssue = emailEnabled ? shareEmailIssue(recipient) : null;
|
|
160
|
+
|
|
123
161
|
const create = useCallback(() => {
|
|
124
162
|
if (!entry?.id || fields.length === 0) return;
|
|
125
163
|
|
|
@@ -128,7 +166,15 @@ const VaultShareModal = ({ entry, endpoints, isOpen = true, onClose }) => {
|
|
|
128
166
|
CustomFetch(
|
|
129
167
|
routes.createShare(entry.id),
|
|
130
168
|
'POST',
|
|
131
|
-
shareRequestBody({
|
|
169
|
+
shareRequestBody({
|
|
170
|
+
fields,
|
|
171
|
+
expiresInHours,
|
|
172
|
+
maxViews,
|
|
173
|
+
// Never sent when the server said it does not email, so a
|
|
174
|
+
// stale tab cannot post a recipient into an installation that
|
|
175
|
+
// has switched it off.
|
|
176
|
+
recipientEmail: emailEnabled ? recipient : '',
|
|
177
|
+
}),
|
|
132
178
|
(result) => {
|
|
133
179
|
if (!mountedRef.current) return;
|
|
134
180
|
|
|
@@ -150,7 +196,16 @@ const VaultShareModal = ({ entry, endpoints, isOpen = true, onClose }) => {
|
|
|
150
196
|
// server's "this entry has no password to share".
|
|
151
197
|
if (mountedRef.current) setCreating(false);
|
|
152
198
|
});
|
|
153
|
-
}, [
|
|
199
|
+
}, [
|
|
200
|
+
routes,
|
|
201
|
+
entry,
|
|
202
|
+
fields,
|
|
203
|
+
expiresInHours,
|
|
204
|
+
maxViews,
|
|
205
|
+
recipient,
|
|
206
|
+
emailEnabled,
|
|
207
|
+
load,
|
|
208
|
+
]);
|
|
154
209
|
|
|
155
210
|
const copyUrl = useCallback(async () => {
|
|
156
211
|
if (!created?.url) return;
|
|
@@ -240,6 +295,29 @@ const VaultShareModal = ({ entry, endpoints, isOpen = true, onClose }) => {
|
|
|
240
295
|
</button>
|
|
241
296
|
</div>
|
|
242
297
|
|
|
298
|
+
{created.emailed_to && (
|
|
299
|
+
<p className={styles.shareEmailed}>
|
|
300
|
+
<Mail size={14} strokeWidth={2.2} aria-hidden="true" />
|
|
301
|
+
<span>
|
|
302
|
+
Also emailed to{' '}
|
|
303
|
+
<strong>{created.emailed_to}</strong>. The
|
|
304
|
+
email carries this link and the entry's
|
|
305
|
+
title — none of the details themselves.
|
|
306
|
+
</span>
|
|
307
|
+
</p>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{created.email_error && (
|
|
311
|
+
<p className={styles.shareEmailFailed} role="alert">
|
|
312
|
+
<AlertTriangle
|
|
313
|
+
size={14}
|
|
314
|
+
strokeWidth={2.2}
|
|
315
|
+
aria-hidden="true"
|
|
316
|
+
/>
|
|
317
|
+
<span>{created.email_error}</span>
|
|
318
|
+
</p>
|
|
319
|
+
)}
|
|
320
|
+
|
|
243
321
|
<p className={styles.shareWarning}>
|
|
244
322
|
<ShieldAlert size={14} strokeWidth={2.2} aria-hidden="true" />
|
|
245
323
|
<span>
|
|
@@ -259,6 +337,10 @@ const VaultShareModal = ({ entry, endpoints, isOpen = true, onClose }) => {
|
|
|
259
337
|
onClick={() => {
|
|
260
338
|
setCreated(null);
|
|
261
339
|
setCopied(false);
|
|
340
|
+
// A second link almost never goes to the
|
|
341
|
+
// same person, and a left-over address is
|
|
342
|
+
// how one quietly does.
|
|
343
|
+
setRecipient('');
|
|
262
344
|
}}
|
|
263
345
|
>
|
|
264
346
|
Create another
|
|
@@ -361,17 +443,56 @@ const VaultShareModal = ({ entry, endpoints, isOpen = true, onClose }) => {
|
|
|
361
443
|
</p>
|
|
362
444
|
</div>
|
|
363
445
|
|
|
446
|
+
{emailEnabled && (
|
|
447
|
+
<div className={styles.field}>
|
|
448
|
+
<label
|
|
449
|
+
className={styles.label}
|
|
450
|
+
htmlFor="vault-share-recipient"
|
|
451
|
+
>
|
|
452
|
+
Email the link to (optional)
|
|
453
|
+
</label>
|
|
454
|
+
<input
|
|
455
|
+
id="vault-share-recipient"
|
|
456
|
+
type="email"
|
|
457
|
+
className={styles.input}
|
|
458
|
+
value={recipient}
|
|
459
|
+
placeholder="name@example.com"
|
|
460
|
+
autoComplete="off"
|
|
461
|
+
spellCheck="false"
|
|
462
|
+
onChange={(e) => setRecipient(e.target.value)}
|
|
463
|
+
/>
|
|
464
|
+
{emailIssue ? (
|
|
465
|
+
<p className={styles.fieldError}>{emailIssue}</p>
|
|
466
|
+
) : (
|
|
467
|
+
<p className={styles.help}>
|
|
468
|
+
Sent when you create the link. The email
|
|
469
|
+
carries the link and this entry's title —
|
|
470
|
+
never the username, password or code. You
|
|
471
|
+
still get the link here to copy.
|
|
472
|
+
</p>
|
|
473
|
+
)}
|
|
474
|
+
</div>
|
|
475
|
+
)}
|
|
476
|
+
|
|
364
477
|
<div className={styles.formActions}>
|
|
365
478
|
<button
|
|
366
479
|
type="button"
|
|
367
480
|
className={styles.primaryButton}
|
|
368
481
|
onClick={create}
|
|
369
|
-
disabled={
|
|
482
|
+
disabled={
|
|
483
|
+
creating ||
|
|
484
|
+
fields.length === 0 ||
|
|
485
|
+
Boolean(emailIssue)
|
|
486
|
+
}
|
|
370
487
|
>
|
|
371
488
|
{creating && (
|
|
372
489
|
<Loader2 size={15} className={styles.spin} />
|
|
373
490
|
)}
|
|
374
|
-
{creating
|
|
491
|
+
{creating
|
|
492
|
+
? 'Creating…'
|
|
493
|
+
: recipient.trim() !== '' && emailEnabled
|
|
494
|
+
? 'Create and email link'
|
|
495
|
+
: 'Create link'}
|
|
375
496
|
</button>
|
|
376
497
|
<button
|
|
377
498
|
type="button"
|
|
@@ -131,17 +131,69 @@ export const toggleShareField = (fields, key) => {
|
|
|
131
131
|
return known.filter((field) => next.includes(field));
|
|
132
132
|
};
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Whether this installation will email a link for the sender.
|
|
136
|
+
*
|
|
137
|
+
* Read off the share LIST response, which the modal already fetches when it
|
|
138
|
+
* opens — a second request to learn one boolean would be a round trip for
|
|
139
|
+
* nothing. Defaults to true when the key is absent, so a build talking to a
|
|
140
|
+
* server that predates the flag offers the field and lets the server refuse,
|
|
141
|
+
* rather than hiding a feature that is switched on.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} listResponse the payload from `endpoints.shares(id)`
|
|
144
|
+
* @returns {boolean}
|
|
145
|
+
*/
|
|
146
|
+
export const shareEmailEnabled = (listResponse) => {
|
|
147
|
+
const flag = listResponse?.meta?.email_enabled;
|
|
148
|
+
|
|
149
|
+
return flag === undefined || flag === null ? true : Boolean(flag);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* What is wrong with the typed recipient address, or null.
|
|
154
|
+
*
|
|
155
|
+
* Deliberately the loosest check that still catches a typo — something, an @,
|
|
156
|
+
* something with a dot in it. The server validates properly and is the only
|
|
157
|
+
* opinion that counts; this exists so that a mistyped address is a sentence
|
|
158
|
+
* under the field rather than a 422 toast after the link has already been
|
|
159
|
+
* minted.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} value
|
|
162
|
+
* @returns {(string|null)}
|
|
163
|
+
*/
|
|
164
|
+
export const shareEmailIssue = (value) => {
|
|
165
|
+
const address = String(value ?? '').trim();
|
|
166
|
+
|
|
167
|
+
if (address === '') return null;
|
|
168
|
+
|
|
169
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(address)
|
|
170
|
+
? null
|
|
171
|
+
: 'That does not look like an email address.';
|
|
172
|
+
};
|
|
173
|
+
|
|
134
174
|
/**
|
|
135
175
|
* The body the create endpoint wants.
|
|
136
176
|
*
|
|
137
177
|
* `max_views` is omitted rather than sent as null when there is no limit: the
|
|
138
178
|
* server treats an absent key and a null the same way, and a body that carries
|
|
139
|
-
* only what was chosen is easier to read in a network panel.
|
|
179
|
+
* only what was chosen is easier to read in a network panel. `recipient_email`
|
|
180
|
+
* is omitted the same way when nobody is being mailed — a create request with
|
|
181
|
+
* no recipient key in it is a create request that cannot possibly send mail,
|
|
182
|
+
* which is worth being able to read off a network panel at a glance.
|
|
183
|
+
*
|
|
184
|
+
* A recipient name is only sent alongside an address: it is the greeting on the
|
|
185
|
+
* email and means nothing without one.
|
|
140
186
|
*
|
|
141
|
-
* @param {{fields: Array<string>, expiresInHours: number, maxViews: (number|null)}} choice
|
|
187
|
+
* @param {{fields: Array<string>, expiresInHours: number, maxViews: (number|null), recipientEmail?: string, recipientName?: string}} choice
|
|
142
188
|
* @returns {object}
|
|
143
189
|
*/
|
|
144
|
-
export const shareRequestBody = ({
|
|
190
|
+
export const shareRequestBody = ({
|
|
191
|
+
fields,
|
|
192
|
+
expiresInHours,
|
|
193
|
+
maxViews,
|
|
194
|
+
recipientEmail,
|
|
195
|
+
recipientName,
|
|
196
|
+
} = {}) => {
|
|
145
197
|
const known = SHARE_FIELDS.map((field) => field.key);
|
|
146
198
|
const clean = known.filter((field) => (fields || []).includes(field));
|
|
147
199
|
|
|
@@ -160,6 +212,18 @@ export const shareRequestBody = ({ fields, expiresInHours, maxViews } = {}) => {
|
|
|
160
212
|
body.max_views = Math.round(views);
|
|
161
213
|
}
|
|
162
214
|
|
|
215
|
+
const address = String(recipientEmail ?? '').trim();
|
|
216
|
+
|
|
217
|
+
if (address !== '') {
|
|
218
|
+
body.recipient_email = address;
|
|
219
|
+
|
|
220
|
+
const name = String(recipientName ?? '').trim();
|
|
221
|
+
|
|
222
|
+
if (name !== '') {
|
|
223
|
+
body.recipient_name = name;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
163
227
|
return body;
|
|
164
228
|
};
|
|
165
229
|
|
package/src/index.js
CHANGED
|
@@ -178,6 +178,7 @@ import {
|
|
|
178
178
|
clientRecipient,
|
|
179
179
|
conversationCountLabel,
|
|
180
180
|
describeTransport,
|
|
181
|
+
displayNumber,
|
|
181
182
|
fillTemplate,
|
|
182
183
|
groupMessagesByDay,
|
|
183
184
|
initialsFor,
|
|
@@ -189,11 +190,37 @@ import {
|
|
|
189
190
|
pickInitialThread,
|
|
190
191
|
relativeTime,
|
|
191
192
|
segmentCount,
|
|
193
|
+
smsHref,
|
|
192
194
|
splitPersonName,
|
|
195
|
+
telHref,
|
|
193
196
|
threadDisplayName,
|
|
194
197
|
toE164,
|
|
195
198
|
} from './components/sms/smsHelpers';
|
|
196
199
|
|
|
200
|
+
// Zoom Phone presence — the header phone chip. ZoomPhoneBadge mounts through
|
|
201
|
+
// Navigation's `renderers` slot and is the whole feature; the helpers are
|
|
202
|
+
// exported for a consumer that wants the same roster somewhere else (a wall
|
|
203
|
+
// board, say) without re-deriving what "available" means.
|
|
204
|
+
import ZoomPhoneBadge from './components/phone/ZoomPhoneBadge';
|
|
205
|
+
import useZoomPhoneLive from './components/phone/useZoomPhoneLive';
|
|
206
|
+
import {
|
|
207
|
+
DEFAULT_ENDPOINTS as DEFAULT_PHONE_PRESENCE_ENDPOINTS,
|
|
208
|
+
makePhonePresenceEndpoints,
|
|
209
|
+
resolvePhonePresenceEndpoints,
|
|
210
|
+
} from './components/phone/phonePresenceEndpoints';
|
|
211
|
+
import {
|
|
212
|
+
STATUS as PHONE_PRESENCE_STATUS,
|
|
213
|
+
applyPresenceEvent,
|
|
214
|
+
callDuration,
|
|
215
|
+
callSummary,
|
|
216
|
+
freshnessLabel,
|
|
217
|
+
onCallCount,
|
|
218
|
+
orderRoster,
|
|
219
|
+
presenceNote,
|
|
220
|
+
statusLabel as phoneStatusLabel,
|
|
221
|
+
statusTone as phoneStatusTone,
|
|
222
|
+
} from './components/phone/phonePresenceHelpers';
|
|
223
|
+
|
|
197
224
|
// Desktop alerts — the browser Notification API behind three gates (support,
|
|
198
225
|
// permission, the user's own switch). Used by the bell and the SMS badge; the
|
|
199
226
|
// module is exported so a consumer can drive it from anywhere else too.
|
|
@@ -247,6 +274,30 @@ import SectionTypeSelector from './components/proposal/SectionTypeSelector';
|
|
|
247
274
|
import VariableInserter from './components/proposal/VariableInserter';
|
|
248
275
|
import ProposalTemplatePreview from './components/proposal/ProposalTemplatePreview';
|
|
249
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Page primitives.
|
|
279
|
+
*
|
|
280
|
+
* The scaffolding a bespoke screen is built out of — the strip of figures over a
|
|
281
|
+
* list, the underline tabs, the urgency bands, the header row and the band of
|
|
282
|
+
* facts under a record's title. Every one of them was written by hand on three
|
|
283
|
+
* or more CRM screens first, and had already drifted between them; see
|
|
284
|
+
* `styles/_surface.scss` for the two-level token scheme that lets a page with an
|
|
285
|
+
* existing palette adopt them without redrawing itself.
|
|
286
|
+
*/
|
|
287
|
+
import AnchoredMenu from './components/layout/AnchoredMenu';
|
|
288
|
+
import BandedList, { Band } from './components/layout/BandedList';
|
|
289
|
+
import FactsBand, { Fact } from './components/layout/FactsBand';
|
|
290
|
+
import PageHeader from './components/layout/PageHeader';
|
|
291
|
+
import ScrollRegion from './components/layout/ScrollRegion';
|
|
292
|
+
import StatStrip, { Stat } from './components/layout/StatStrip';
|
|
293
|
+
import useContainedHeight from './components/layout/useContainedHeight';
|
|
294
|
+
import TabStrip from './components/navigation/TabStrip';
|
|
295
|
+
|
|
296
|
+
// Copying, and what to do when the browser will not. Named for the vault
|
|
297
|
+
// because that is where it was written; nothing in it is vault-specific, and
|
|
298
|
+
// every screen with a copy button was otherwise restating it.
|
|
299
|
+
import { canCopy, copyText } from './components/vault/vaultClipboard';
|
|
300
|
+
|
|
250
301
|
/** OAuth Components */
|
|
251
302
|
import OAuthManager from './components/oauth/OAuthManager';
|
|
252
303
|
import OAuthProviderCard from './components/oauth/OAuthProviderCard';
|
|
@@ -254,7 +305,6 @@ import OAuthConnectionStatus from './components/oauth/OAuthConnectionStatus';
|
|
|
254
305
|
import OAuthIntegrationsPage from './components/oauth/OAuthIntegrationsPage';
|
|
255
306
|
import DataSyncWizard from './components/oauth/DataSyncWizard';
|
|
256
307
|
|
|
257
|
-
|
|
258
308
|
export {
|
|
259
309
|
applyUsernameDomain,
|
|
260
310
|
AsyncSelect,
|
|
@@ -339,6 +389,42 @@ export {
|
|
|
339
389
|
useSmsLive,
|
|
340
390
|
UniversalSearch,
|
|
341
391
|
useSmsThreads,
|
|
392
|
+
// Phone numbers, for display and for dialling. Beside the SMS module's own
|
|
393
|
+
// normaliser because that is the formatter they are built on.
|
|
394
|
+
displayNumber,
|
|
395
|
+
smsHref,
|
|
396
|
+
telHref,
|
|
397
|
+
// Zoom Phone presence — the header roster.
|
|
398
|
+
ZoomPhoneBadge,
|
|
399
|
+
useZoomPhoneLive,
|
|
400
|
+
DEFAULT_PHONE_PRESENCE_ENDPOINTS,
|
|
401
|
+
makePhonePresenceEndpoints,
|
|
402
|
+
resolvePhonePresenceEndpoints,
|
|
403
|
+
PHONE_PRESENCE_STATUS,
|
|
404
|
+
applyPresenceEvent,
|
|
405
|
+
callDuration,
|
|
406
|
+
callSummary,
|
|
407
|
+
freshnessLabel,
|
|
408
|
+
onCallCount,
|
|
409
|
+
orderRoster,
|
|
410
|
+
presenceNote,
|
|
411
|
+
phoneStatusLabel,
|
|
412
|
+
phoneStatusTone,
|
|
413
|
+
// Page primitives.
|
|
414
|
+
AnchoredMenu,
|
|
415
|
+
Band,
|
|
416
|
+
BandedList,
|
|
417
|
+
Fact,
|
|
418
|
+
FactsBand,
|
|
419
|
+
PageHeader,
|
|
420
|
+
ScrollRegion,
|
|
421
|
+
Stat,
|
|
422
|
+
StatStrip,
|
|
423
|
+
TabStrip,
|
|
424
|
+
useContainedHeight,
|
|
425
|
+
// Clipboard.
|
|
426
|
+
canCopy,
|
|
427
|
+
copyText,
|
|
342
428
|
CameraPlacement,
|
|
343
429
|
CellWithTooltip,
|
|
344
430
|
clampOtp,
|
|
@@ -458,4 +544,4 @@ export {
|
|
|
458
544
|
VariableInserter,
|
|
459
545
|
verifyContactFor,
|
|
460
546
|
Verify,
|
|
461
|
-
};
|
|
547
|
+
};
|