@visns-studio/visns-components 6.17.2 → 6.20.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 +10 -7
- package/src/components/AssociationManager.jsx +344 -177
- package/src/components/AsyncSelect.jsx +2 -2
- package/src/components/Autocomplete.jsx +57 -12
- package/src/components/Breadcrumb.jsx +23 -4
- package/src/components/ClientAssociationManager.jsx +321 -145
- package/src/components/DataGrid.jsx +625 -20
- package/src/components/DropZone.jsx +4 -1
- package/src/components/Form.jsx +233 -6
- package/src/components/MultiSelect.jsx +2 -2
- package/src/components/Navigation.jsx +39 -23
- package/src/components/TableFilter.jsx +172 -12
- package/src/components/UniversalSearch.jsx +390 -0
- package/src/components/auth/Login.jsx +164 -8
- package/src/components/auth/Profile.jsx +209 -22
- package/src/components/auth/authEndpoints.js +4 -0
- package/src/components/auth/webauthn.js +194 -0
- package/src/components/columns/ColumnRenderers.jsx +6 -1
- package/src/components/controls/DataGridSearch.jsx +11 -5
- package/src/components/generic/GenericAuth.jsx +6 -0
- package/src/components/generic/GenericDashboard.jsx +93 -36
- package/src/components/generic/GenericDetail.jsx +146 -61
- package/src/components/generic/GenericIndex.jsx +134 -38
- package/src/components/generic/GenericSettings.jsx +220 -0
- package/src/components/generic/StandardModal.jsx +165 -8
- package/src/components/generic/dashboardChartDefaults.js +230 -0
- package/src/components/generic/shared/indexLayout.js +38 -0
- package/src/components/settings/IntegrationsManager.jsx +474 -0
- package/src/components/sms/SmsLineSettings.jsx +16 -12
- package/src/components/styles/AssociationManager.module.scss +4 -363
- package/src/components/styles/AuditLog.module.scss +6 -6
- package/src/components/styles/AuditLogs.module.scss +3 -3
- package/src/components/styles/Autocomplete.module.scss +7 -7
- package/src/components/styles/BusinessCardOcr.module.scss +6 -1
- package/src/components/styles/CallQueueSettings.module.scss +4 -1
- package/src/components/styles/CameraPlacement.module.scss +19 -19
- package/src/components/styles/CategorizedDropZone.module.scss +6 -1
- package/src/components/styles/ClientAssociationManager.module.scss +5 -289
- package/src/components/styles/ClientAuth.module.scss +1 -1
- package/src/components/styles/DataGrid.module.scss +455 -8
- package/src/components/styles/Field.module.scss +32 -24
- package/src/components/styles/Form.module.scss +123 -3
- package/src/components/styles/GenericDashboard.module.scss +612 -825
- package/src/components/styles/GenericDetail.module.scss +406 -61
- package/src/components/styles/GenericDynamic.module.scss +25 -25
- package/src/components/styles/GenericEditableTable.module.scss +33 -33
- package/src/components/styles/GenericFormBuilder.module.scss +119 -119
- package/src/components/styles/GenericIndex.module.scss +483 -38
- package/src/components/styles/GenericMain.module.scss +6 -3
- package/src/components/styles/GenericQuote.module.scss +399 -81
- package/src/components/styles/GenericReport.module.scss +8 -8
- package/src/components/styles/GenericSettings.module.scss +209 -0
- package/src/components/styles/GenericSort.module.scss +9 -9
- package/src/components/styles/Integrations.module.scss +321 -0
- package/src/components/styles/List.module.scss +4 -4
- package/src/components/styles/Login.module.scss +57 -0
- package/src/components/styles/MultiSelect.module.scss +4 -4
- package/src/components/styles/Navigation.module.scss +12 -12
- package/src/components/styles/Notification.module.scss +12 -8
- package/src/components/styles/NotificationList.module.scss +5 -5
- package/src/components/styles/Profile.module.scss +5 -5
- package/src/components/styles/QrCode.module.scss +4 -4
- package/src/components/styles/QuickAction.module.scss +20 -20
- package/src/components/styles/SortableQuoteItems.module.scss +4 -4
- package/src/components/styles/TableFilter.module.scss +128 -17
- package/src/components/styles/TicketConversation.module.scss +497 -0
- package/src/components/styles/UniversalSearch.module.scss +229 -0
- package/src/components/styles/Vault.module.scss +413 -0
- package/src/components/styles/_associationManager.scss +467 -0
- package/src/components/styles/_authBrandPanel.scss +15 -9
- package/src/components/styles/_controls.scss +43 -39
- package/src/components/styles/global-datagrid.css +196 -0
- package/src/components/styles/global.css +168 -17
- package/src/components/tickets/TicketConversation.jsx +455 -0
- package/src/components/utils/rowActionSettings.js +117 -0
- package/src/components/vault/VaultClientPicker.jsx +183 -0
- package/src/components/vault/VaultEntryForm.jsx +23 -0
- package/src/components/vault/VaultManager.jsx +382 -34
- package/src/components/vault/VaultShareModal.jsx +454 -0
- package/src/components/vault/vaultEndpoints.js +16 -2
- package/src/components/vault/vaultShares.js +247 -0
- package/src/index.js +35 -0
- package/src/utils/fetchUtil.js +38 -14
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { toast } from 'react-toastify';
|
|
3
|
+
import { Check, Copy, Link2, Loader2, ShieldAlert, Trash2 } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import CustomFetch from '../Fetch';
|
|
6
|
+
import StandardModal from '../generic/StandardModal';
|
|
7
|
+
import styles from '../styles/Vault.module.scss';
|
|
8
|
+
import { copyText } from './vaultClipboard';
|
|
9
|
+
import { resolveVaultEndpoints } from './vaultEndpoints';
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_EXPIRY_HOURS,
|
|
12
|
+
EXPIRY_OPTIONS,
|
|
13
|
+
VIEW_LIMIT_OPTIONS,
|
|
14
|
+
availableShareFields,
|
|
15
|
+
initialShareFields,
|
|
16
|
+
isShareRevocable,
|
|
17
|
+
shareFieldsLabel,
|
|
18
|
+
shareRequestBody,
|
|
19
|
+
shareStatus,
|
|
20
|
+
shareStatusLabel,
|
|
21
|
+
shareViewsLabel,
|
|
22
|
+
toggleShareField,
|
|
23
|
+
} from './vaultShares';
|
|
24
|
+
|
|
25
|
+
/** The pill colour per state. Anything unknown falls through to the neutral. */
|
|
26
|
+
const STATUS_CLASS = {
|
|
27
|
+
active: 'shareStatusActive',
|
|
28
|
+
expired: 'shareStatusClosed',
|
|
29
|
+
spent: 'shareStatusClosed',
|
|
30
|
+
revoked: 'shareStatusRevoked',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Send one credential to somebody who does not have a CRM account.
|
|
35
|
+
*
|
|
36
|
+
* THE LINK IS THE CREDENTIAL. There is no password on the far side; anyone
|
|
37
|
+
* holding the URL inside its expiry and its view budget can read what it
|
|
38
|
+
* carries. Everything in this modal is shaped by that one fact:
|
|
39
|
+
*
|
|
40
|
+
* - Expiry is not optional and there is no "never" in the list, because the
|
|
41
|
+
* server has no such option either.
|
|
42
|
+
* - The default field selection is username and password, not everything.
|
|
43
|
+
* - The URL is shown exactly once. The server stores only its SHA-256, so
|
|
44
|
+
* "you will not see this link again" is a statement of fact rather than a
|
|
45
|
+
* warning — nothing, here or on the server, can reproduce it.
|
|
46
|
+
* - The existing links are listed under the form rather than behind a tab,
|
|
47
|
+
* because "have we already sent this out" is the question somebody opening
|
|
48
|
+
* this dialog most often actually has.
|
|
49
|
+
*
|
|
50
|
+
* The 2FA option sends the CURRENT CODE, computed by the server at the moment
|
|
51
|
+
* the recipient reveals — never the seed. That is said on the checkbox, not
|
|
52
|
+
* just in this comment, because it is the one thing about this dialog a person
|
|
53
|
+
* could reasonably get wrong.
|
|
54
|
+
*/
|
|
55
|
+
const VaultShareModal = ({ entry, endpoints, isOpen = true, onClose }) => {
|
|
56
|
+
const routes = useMemo(() => resolveVaultEndpoints(endpoints), [endpoints]);
|
|
57
|
+
|
|
58
|
+
const fieldOptions = useMemo(() => availableShareFields(entry), [entry]);
|
|
59
|
+
|
|
60
|
+
const [fields, setFields] = useState(() => initialShareFields(entry));
|
|
61
|
+
const [expiresInHours, setExpiresInHours] = useState(DEFAULT_EXPIRY_HOURS);
|
|
62
|
+
const [maxViews, setMaxViews] = useState(null);
|
|
63
|
+
|
|
64
|
+
const [creating, setCreating] = useState(false);
|
|
65
|
+
// The one and only copy of a freshly minted URL. Held in component state
|
|
66
|
+
// and nowhere else — closing the modal is what destroys it.
|
|
67
|
+
const [created, setCreated] = useState(null);
|
|
68
|
+
const [copied, setCopied] = useState(false);
|
|
69
|
+
|
|
70
|
+
const [shares, setShares] = useState([]);
|
|
71
|
+
const [loading, setLoading] = useState(false);
|
|
72
|
+
const [revokingId, setRevokingId] = useState(null);
|
|
73
|
+
|
|
74
|
+
const mountedRef = useRef(true);
|
|
75
|
+
const copiedTimer = useRef(null);
|
|
76
|
+
const urlRef = useRef(null);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
mountedRef.current = true;
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
mountedRef.current = false;
|
|
83
|
+
if (copiedTimer.current) clearTimeout(copiedTimer.current);
|
|
84
|
+
};
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const load = useCallback(() => {
|
|
88
|
+
if (!entry?.id) return;
|
|
89
|
+
|
|
90
|
+
setLoading(true);
|
|
91
|
+
|
|
92
|
+
CustomFetch(
|
|
93
|
+
routes.shares(entry.id),
|
|
94
|
+
'GET',
|
|
95
|
+
{},
|
|
96
|
+
(result) => {
|
|
97
|
+
if (!mountedRef.current) return;
|
|
98
|
+
|
|
99
|
+
setShares(Array.isArray(result?.data) ? result.data : []);
|
|
100
|
+
setLoading(false);
|
|
101
|
+
},
|
|
102
|
+
null
|
|
103
|
+
).catch(() => {
|
|
104
|
+
if (mountedRef.current) setLoading(false);
|
|
105
|
+
});
|
|
106
|
+
}, [routes, entry]);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
load();
|
|
110
|
+
}, [load]);
|
|
111
|
+
|
|
112
|
+
// A brand new link is worth selecting for the reader, so the keyboard path
|
|
113
|
+
// (focus, ⌘A, ⌘C) works for anyone whose browser refuses the clipboard API.
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (created && urlRef.current) {
|
|
116
|
+
urlRef.current.focus();
|
|
117
|
+
urlRef.current.select();
|
|
118
|
+
}
|
|
119
|
+
}, [created]);
|
|
120
|
+
|
|
121
|
+
const toggle = (key) => setFields((current) => toggleShareField(current, key));
|
|
122
|
+
|
|
123
|
+
const create = useCallback(() => {
|
|
124
|
+
if (!entry?.id || fields.length === 0) return;
|
|
125
|
+
|
|
126
|
+
setCreating(true);
|
|
127
|
+
|
|
128
|
+
CustomFetch(
|
|
129
|
+
routes.createShare(entry.id),
|
|
130
|
+
'POST',
|
|
131
|
+
shareRequestBody({ fields, expiresInHours, maxViews }),
|
|
132
|
+
(result) => {
|
|
133
|
+
if (!mountedRef.current) return;
|
|
134
|
+
|
|
135
|
+
setCreating(false);
|
|
136
|
+
|
|
137
|
+
if (!result?.url) {
|
|
138
|
+
toast.error('The link could not be created.');
|
|
139
|
+
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setCreated(result);
|
|
144
|
+
setCopied(false);
|
|
145
|
+
load();
|
|
146
|
+
},
|
|
147
|
+
null
|
|
148
|
+
).catch(() => {
|
|
149
|
+
// CustomFetch has already said what went wrong — including the
|
|
150
|
+
// server's "this entry has no password to share".
|
|
151
|
+
if (mountedRef.current) setCreating(false);
|
|
152
|
+
});
|
|
153
|
+
}, [routes, entry, fields, expiresInHours, maxViews, load]);
|
|
154
|
+
|
|
155
|
+
const copyUrl = useCallback(async () => {
|
|
156
|
+
if (!created?.url) return;
|
|
157
|
+
|
|
158
|
+
const ok = await copyText(created.url);
|
|
159
|
+
|
|
160
|
+
if (!mountedRef.current) return;
|
|
161
|
+
|
|
162
|
+
if (!ok) {
|
|
163
|
+
// The input below is already on screen and already selected, so
|
|
164
|
+
// there is somewhere to fall back TO — say so rather than failing.
|
|
165
|
+
toast.info('Copy it from the box above — your browser blocked the clipboard.');
|
|
166
|
+
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
toast.success('Link copied');
|
|
171
|
+
setCopied(true);
|
|
172
|
+
|
|
173
|
+
if (copiedTimer.current) clearTimeout(copiedTimer.current);
|
|
174
|
+
|
|
175
|
+
copiedTimer.current = setTimeout(() => {
|
|
176
|
+
copiedTimer.current = null;
|
|
177
|
+
if (mountedRef.current) setCopied(false);
|
|
178
|
+
}, 1500);
|
|
179
|
+
}, [created]);
|
|
180
|
+
|
|
181
|
+
const revoke = useCallback(
|
|
182
|
+
(share) => {
|
|
183
|
+
setRevokingId(share.id);
|
|
184
|
+
|
|
185
|
+
CustomFetch(
|
|
186
|
+
routes.revokeShare(entry.id, share.id),
|
|
187
|
+
'DELETE',
|
|
188
|
+
{},
|
|
189
|
+
() => {
|
|
190
|
+
if (!mountedRef.current) return;
|
|
191
|
+
|
|
192
|
+
setRevokingId(null);
|
|
193
|
+
toast.success('Link revoked');
|
|
194
|
+
load();
|
|
195
|
+
},
|
|
196
|
+
null
|
|
197
|
+
).catch(() => {
|
|
198
|
+
if (mountedRef.current) setRevokingId(null);
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
[routes, entry, load]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<StandardModal
|
|
206
|
+
isOpen={isOpen}
|
|
207
|
+
onClose={onClose}
|
|
208
|
+
size="medium"
|
|
209
|
+
showHeader
|
|
210
|
+
title={`Share ${entry?.title || 'credential'}`}
|
|
211
|
+
>
|
|
212
|
+
<div className={styles.form}>
|
|
213
|
+
{created ? (
|
|
214
|
+
<div className={styles.shareResult}>
|
|
215
|
+
<p className={styles.shareResultHead}>
|
|
216
|
+
<Link2 size={15} strokeWidth={2.2} aria-hidden="true" />
|
|
217
|
+
Your link is ready
|
|
218
|
+
</p>
|
|
219
|
+
|
|
220
|
+
<div className={styles.inputRow}>
|
|
221
|
+
<input
|
|
222
|
+
ref={urlRef}
|
|
223
|
+
className={styles.input}
|
|
224
|
+
readOnly
|
|
225
|
+
value={created.url}
|
|
226
|
+
onFocus={(e) => e.target.select()}
|
|
227
|
+
aria-label="The share link"
|
|
228
|
+
/>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
className={styles.primaryButton}
|
|
232
|
+
onClick={copyUrl}
|
|
233
|
+
>
|
|
234
|
+
{copied ? (
|
|
235
|
+
<Check size={15} strokeWidth={2.6} aria-hidden="true" />
|
|
236
|
+
) : (
|
|
237
|
+
<Copy size={15} strokeWidth={2} aria-hidden="true" />
|
|
238
|
+
)}
|
|
239
|
+
{copied ? 'Copied' : 'Copy'}
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<p className={styles.shareWarning}>
|
|
244
|
+
<ShieldAlert size={14} strokeWidth={2.2} aria-hidden="true" />
|
|
245
|
+
<span>
|
|
246
|
+
<strong>You will not see this link again.</strong>{' '}
|
|
247
|
+
Only its fingerprint is stored, so it cannot be
|
|
248
|
+
looked up later. Treat the link itself as the
|
|
249
|
+
password — anyone who has it can read{' '}
|
|
250
|
+
{shareFieldsLabel(created).toLowerCase()} until it
|
|
251
|
+
expires or you revoke it.
|
|
252
|
+
</span>
|
|
253
|
+
</p>
|
|
254
|
+
|
|
255
|
+
<div className={styles.formActions}>
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
className={styles.ghostButton}
|
|
259
|
+
onClick={() => {
|
|
260
|
+
setCreated(null);
|
|
261
|
+
setCopied(false);
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
Create another
|
|
265
|
+
</button>
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
className={styles.linkButton}
|
|
269
|
+
onClick={onClose}
|
|
270
|
+
>
|
|
271
|
+
Done
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
) : (
|
|
276
|
+
<>
|
|
277
|
+
<div className={styles.field}>
|
|
278
|
+
<span className={styles.label}>What to send</span>
|
|
279
|
+
<div className={styles.shareFields}>
|
|
280
|
+
{fieldOptions.map((field) => (
|
|
281
|
+
<label
|
|
282
|
+
key={field.key}
|
|
283
|
+
className={`${styles.inlineCheck} ${
|
|
284
|
+
field.disabled ? styles.radioDisabled : ''
|
|
285
|
+
}`}
|
|
286
|
+
title={field.reason || undefined}
|
|
287
|
+
>
|
|
288
|
+
<input
|
|
289
|
+
type="checkbox"
|
|
290
|
+
className={styles.checkControl}
|
|
291
|
+
checked={fields.includes(field.key)}
|
|
292
|
+
disabled={field.disabled}
|
|
293
|
+
onChange={() => toggle(field.key)}
|
|
294
|
+
/>
|
|
295
|
+
<span>
|
|
296
|
+
{field.label}
|
|
297
|
+
{field.hint && (
|
|
298
|
+
<em className={styles.shareHint}>
|
|
299
|
+
{field.hint}
|
|
300
|
+
</em>
|
|
301
|
+
)}
|
|
302
|
+
{field.disabled && field.reason && (
|
|
303
|
+
<em className={styles.shareHint}>
|
|
304
|
+
{field.reason}
|
|
305
|
+
</em>
|
|
306
|
+
)}
|
|
307
|
+
</span>
|
|
308
|
+
</label>
|
|
309
|
+
))}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<div className={styles.field}>
|
|
314
|
+
<label className={styles.label} htmlFor="vault-share-expiry">
|
|
315
|
+
Expires after
|
|
316
|
+
</label>
|
|
317
|
+
<select
|
|
318
|
+
id="vault-share-expiry"
|
|
319
|
+
className={styles.select}
|
|
320
|
+
value={expiresInHours}
|
|
321
|
+
onChange={(e) =>
|
|
322
|
+
setExpiresInHours(Number(e.target.value))
|
|
323
|
+
}
|
|
324
|
+
>
|
|
325
|
+
{EXPIRY_OPTIONS.map((option) => (
|
|
326
|
+
<option key={option.value} value={option.value}>
|
|
327
|
+
{option.label}
|
|
328
|
+
</option>
|
|
329
|
+
))}
|
|
330
|
+
</select>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<div className={styles.field}>
|
|
334
|
+
<label className={styles.label} htmlFor="vault-share-views">
|
|
335
|
+
View limit
|
|
336
|
+
</label>
|
|
337
|
+
<select
|
|
338
|
+
id="vault-share-views"
|
|
339
|
+
className={styles.select}
|
|
340
|
+
value={maxViews === null ? '' : maxViews}
|
|
341
|
+
onChange={(e) =>
|
|
342
|
+
setMaxViews(
|
|
343
|
+
e.target.value === ''
|
|
344
|
+
? null
|
|
345
|
+
: Number(e.target.value)
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
>
|
|
349
|
+
{VIEW_LIMIT_OPTIONS.map((option) => (
|
|
350
|
+
<option
|
|
351
|
+
key={String(option.value)}
|
|
352
|
+
value={option.value === null ? '' : option.value}
|
|
353
|
+
>
|
|
354
|
+
{option.label}
|
|
355
|
+
</option>
|
|
356
|
+
))}
|
|
357
|
+
</select>
|
|
358
|
+
<p className={styles.help}>
|
|
359
|
+
A one-view link closes itself the moment it is
|
|
360
|
+
opened — the safest choice for a single recipient.
|
|
361
|
+
</p>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<div className={styles.formActions}>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
className={styles.primaryButton}
|
|
368
|
+
onClick={create}
|
|
369
|
+
disabled={creating || fields.length === 0}
|
|
370
|
+
>
|
|
371
|
+
{creating && (
|
|
372
|
+
<Loader2 size={15} className={styles.spin} />
|
|
373
|
+
)}
|
|
374
|
+
{creating ? 'Creating…' : 'Create link'}
|
|
375
|
+
</button>
|
|
376
|
+
<button
|
|
377
|
+
type="button"
|
|
378
|
+
className={styles.ghostButton}
|
|
379
|
+
onClick={onClose}
|
|
380
|
+
disabled={creating}
|
|
381
|
+
>
|
|
382
|
+
Cancel
|
|
383
|
+
</button>
|
|
384
|
+
</div>
|
|
385
|
+
</>
|
|
386
|
+
)}
|
|
387
|
+
|
|
388
|
+
<div className={styles.shareList}>
|
|
389
|
+
<p className={styles.label}>
|
|
390
|
+
Links for this entry
|
|
391
|
+
{loading && (
|
|
392
|
+
<Loader2 size={13} className={styles.spin} aria-hidden="true" />
|
|
393
|
+
)}
|
|
394
|
+
</p>
|
|
395
|
+
|
|
396
|
+
{shares.length === 0 && !loading && (
|
|
397
|
+
<p className={styles.muted}>
|
|
398
|
+
No links have been created for this entry.
|
|
399
|
+
</p>
|
|
400
|
+
)}
|
|
401
|
+
|
|
402
|
+
{shares.map((share) => (
|
|
403
|
+
<div key={share.id} className={styles.shareRow}>
|
|
404
|
+
<div className={styles.shareRowMain}>
|
|
405
|
+
<span
|
|
406
|
+
className={`${styles.pill} ${
|
|
407
|
+
styles[
|
|
408
|
+
STATUS_CLASS[shareStatus(share)] ||
|
|
409
|
+
'shareStatusClosed'
|
|
410
|
+
] || ''
|
|
411
|
+
}`}
|
|
412
|
+
>
|
|
413
|
+
{shareStatusLabel(share)}
|
|
414
|
+
</span>
|
|
415
|
+
<span className={styles.shareRowFields}>
|
|
416
|
+
{shareFieldsLabel(share)}
|
|
417
|
+
</span>
|
|
418
|
+
</div>
|
|
419
|
+
<div className={styles.shareRowMeta}>
|
|
420
|
+
<span>{shareViewsLabel(share)}</span>
|
|
421
|
+
{share.created_by && <span>by {share.created_by}</span>}
|
|
422
|
+
{share.expires_at && (
|
|
423
|
+
<span>
|
|
424
|
+
{shareStatus(share) === 'expired'
|
|
425
|
+
? 'expired'
|
|
426
|
+
: 'expires'}{' '}
|
|
427
|
+
{new Date(share.expires_at).toLocaleString()}
|
|
428
|
+
</span>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
{isShareRevocable(share) && (
|
|
432
|
+
<button
|
|
433
|
+
type="button"
|
|
434
|
+
className={styles.linkButton}
|
|
435
|
+
onClick={() => revoke(share)}
|
|
436
|
+
disabled={revokingId === share.id}
|
|
437
|
+
>
|
|
438
|
+
{revokingId === share.id ? (
|
|
439
|
+
<Loader2 size={13} className={styles.spin} />
|
|
440
|
+
) : (
|
|
441
|
+
<Trash2 size={13} strokeWidth={2} aria-hidden="true" />
|
|
442
|
+
)}
|
|
443
|
+
Revoke
|
|
444
|
+
</button>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
))}
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</StandardModal>
|
|
451
|
+
);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
export default VaultShareModal;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The URLs the vault talks to.
|
|
3
3
|
*
|
|
4
4
|
* Every vault component takes an `endpoints` object rather than a base string,
|
|
5
|
-
* so a consumer can move one route without reproducing the
|
|
5
|
+
* so a consumer can move one route without reproducing the rest. The
|
|
6
6
|
* defaults are the CRM's routes under `/ajax/vault`; `makeVaultEndpoints` is
|
|
7
7
|
* there for the common case of moving all of them at once.
|
|
8
8
|
*
|
|
@@ -22,6 +22,20 @@ export const makeVaultEndpoints = (base = '/ajax/vault') => ({
|
|
|
22
22
|
logCopy: (id) => `${base}/${id}/log`,
|
|
23
23
|
entryLog: (id) => `${base}/${id}/log`,
|
|
24
24
|
globalLog: `${base}/log`,
|
|
25
|
+
// The client typeahead behind the "Client" field on an entry: every client
|
|
26
|
+
// in the host application, whether or not it has a credential.
|
|
27
|
+
clients: `${base}/clients`,
|
|
28
|
+
// The clients that actually HAVE entries, for the list's client filter.
|
|
29
|
+
// A different question from `clients` above, and it has to come from the
|
|
30
|
+
// server: the list is paginated, so the clients on the visible page are
|
|
31
|
+
// not the clients in the vault.
|
|
32
|
+
entryClients: `${base}/entry-clients`,
|
|
33
|
+
// External share links. There is no endpoint that reads a link back:
|
|
34
|
+
// `createShare` returns the URL once and the server keeps only its hash,
|
|
35
|
+
// so there is nothing to read.
|
|
36
|
+
shares: (id) => `${base}/${id}/shares`,
|
|
37
|
+
createShare: (id) => `${base}/${id}/shares`,
|
|
38
|
+
revokeShare: (id, shareId) => `${base}/${id}/shares/${shareId}`,
|
|
25
39
|
});
|
|
26
40
|
|
|
27
41
|
export const DEFAULT_ENDPOINTS = makeVaultEndpoints();
|
|
@@ -29,7 +43,7 @@ export const DEFAULT_ENDPOINTS = makeVaultEndpoints();
|
|
|
29
43
|
/**
|
|
30
44
|
* Merge a partial `endpoints` prop over the defaults.
|
|
31
45
|
*
|
|
32
|
-
* A consumer overriding one route must not lose the
|
|
46
|
+
* A consumer overriding one route must not lose the rest, and passing
|
|
33
47
|
* nothing at all must land exactly on DEFAULT_ENDPOINTS.
|
|
34
48
|
*/
|
|
35
49
|
export const resolveVaultEndpoints = (endpoints) => ({
|