@visns-studio/visns-components 6.17.1 → 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 +45 -25
- 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/navActive.js +25 -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,183 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Building2, Loader2, X } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
import CustomFetch from '../Fetch';
|
|
5
|
+
import styles from '../styles/Vault.module.scss';
|
|
6
|
+
import useDebouncedValue from './useDebouncedValue';
|
|
7
|
+
import { resolveVaultEndpoints } from './vaultEndpoints';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Attach a vault entry to a client.
|
|
11
|
+
*
|
|
12
|
+
* A typeahead rather than a select, because the practice has thousands of
|
|
13
|
+
* clients and a select of that size is unusable — and rather than a free-text
|
|
14
|
+
* box, because the entry stores a foreign key and a typed name that matches
|
|
15
|
+
* nothing would silently save as "no client".
|
|
16
|
+
*
|
|
17
|
+
* The chosen client is held as `{ id, label }` so the field can render the
|
|
18
|
+
* current selection on an existing entry without a lookup: the list payload
|
|
19
|
+
* already carries `client_id` and `client_label`.
|
|
20
|
+
*
|
|
21
|
+
* The endpoint is optional. When the host app has not configured a client
|
|
22
|
+
* model the route answers with an empty list, so this degrades to a field that
|
|
23
|
+
* finds nothing rather than to an error.
|
|
24
|
+
*/
|
|
25
|
+
const VaultClientPicker = ({
|
|
26
|
+
value = null,
|
|
27
|
+
onChange,
|
|
28
|
+
endpoints,
|
|
29
|
+
disabled = false,
|
|
30
|
+
placeholder = 'Search clients…',
|
|
31
|
+
}) => {
|
|
32
|
+
const routes = useMemo(() => resolveVaultEndpoints(endpoints), [endpoints]);
|
|
33
|
+
|
|
34
|
+
const [term, setTerm] = useState('');
|
|
35
|
+
const [results, setResults] = useState([]);
|
|
36
|
+
const [open, setOpen] = useState(false);
|
|
37
|
+
const [loading, setLoading] = useState(false);
|
|
38
|
+
const [active, setActive] = useState(0);
|
|
39
|
+
|
|
40
|
+
const debounced = useDebouncedValue(term, 220);
|
|
41
|
+
const mountedRef = useRef(true);
|
|
42
|
+
const boxRef = useRef(null);
|
|
43
|
+
|
|
44
|
+
// The last term actually requested, so a response that arrives out of
|
|
45
|
+
// order cannot overwrite the results for what the user is typing now.
|
|
46
|
+
const inFlightRef = useRef('');
|
|
47
|
+
|
|
48
|
+
useEffect(() => () => {
|
|
49
|
+
mountedRef.current = false;
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!open) return undefined;
|
|
54
|
+
|
|
55
|
+
const onDocClick = (e) => {
|
|
56
|
+
if (boxRef.current && !boxRef.current.contains(e.target)) setOpen(false);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
document.addEventListener('mousedown', onDocClick);
|
|
60
|
+
|
|
61
|
+
return () => document.removeEventListener('mousedown', onDocClick);
|
|
62
|
+
}, [open]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!open) return;
|
|
66
|
+
|
|
67
|
+
inFlightRef.current = debounced;
|
|
68
|
+
setLoading(true);
|
|
69
|
+
|
|
70
|
+
CustomFetch(
|
|
71
|
+
routes.clients,
|
|
72
|
+
'GET',
|
|
73
|
+
{ q: debounced },
|
|
74
|
+
(result) => {
|
|
75
|
+
if (!mountedRef.current || inFlightRef.current !== debounced) return;
|
|
76
|
+
|
|
77
|
+
setResults(result?.data || []);
|
|
78
|
+
setActive(0);
|
|
79
|
+
setLoading(false);
|
|
80
|
+
},
|
|
81
|
+
null
|
|
82
|
+
).catch(() => {
|
|
83
|
+
if (!mountedRef.current || inFlightRef.current !== debounced) return;
|
|
84
|
+
|
|
85
|
+
// A picker that cannot reach the server shows nothing rather than
|
|
86
|
+
// a toast — it is one optional field on a form.
|
|
87
|
+
setResults([]);
|
|
88
|
+
setLoading(false);
|
|
89
|
+
});
|
|
90
|
+
}, [debounced, open, routes.clients]);
|
|
91
|
+
|
|
92
|
+
const choose = (client) => {
|
|
93
|
+
onChange?.(client);
|
|
94
|
+
setTerm('');
|
|
95
|
+
setOpen(false);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onKeyDown = (e) => {
|
|
99
|
+
if (!open) return;
|
|
100
|
+
|
|
101
|
+
if (e.key === 'ArrowDown') {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
setActive((i) => Math.min(i + 1, results.length - 1));
|
|
104
|
+
} else if (e.key === 'ArrowUp') {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
setActive((i) => Math.max(i - 1, 0));
|
|
107
|
+
} else if (e.key === 'Enter' && results[active]) {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
choose(results[active]);
|
|
110
|
+
} else if (e.key === 'Escape') {
|
|
111
|
+
setOpen(false);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (value?.id) {
|
|
116
|
+
return (
|
|
117
|
+
<div className={styles.clientChosen}>
|
|
118
|
+
<Building2 size={15} strokeWidth={2} />
|
|
119
|
+
<span className={styles.clientChosenLabel}>{value.label}</span>
|
|
120
|
+
{!disabled && (
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
className={styles.clientClear}
|
|
124
|
+
onClick={() => onChange?.(null)}
|
|
125
|
+
aria-label={`Detach this entry from ${value.label}`}
|
|
126
|
+
>
|
|
127
|
+
<X size={13} strokeWidth={2.5} />
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className={styles.clientPicker} ref={boxRef}>
|
|
136
|
+
<input
|
|
137
|
+
id="vault-client"
|
|
138
|
+
className={styles.input}
|
|
139
|
+
value={term}
|
|
140
|
+
disabled={disabled}
|
|
141
|
+
placeholder={placeholder}
|
|
142
|
+
autoComplete="off"
|
|
143
|
+
role="combobox"
|
|
144
|
+
aria-expanded={open}
|
|
145
|
+
aria-controls="vault-client-results"
|
|
146
|
+
onFocus={() => setOpen(true)}
|
|
147
|
+
onChange={(e) => {
|
|
148
|
+
setTerm(e.target.value);
|
|
149
|
+
setOpen(true);
|
|
150
|
+
}}
|
|
151
|
+
onKeyDown={onKeyDown}
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
{loading && <Loader2 size={15} className={styles.clientSpinner} />}
|
|
155
|
+
|
|
156
|
+
{open && (
|
|
157
|
+
<ul className={styles.clientResults} id="vault-client-results" role="listbox">
|
|
158
|
+
{results.map((client, i) => (
|
|
159
|
+
<li key={client.id} role="option" aria-selected={i === active}>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
className={i === active ? styles.clientResultActive : styles.clientResult}
|
|
163
|
+
onMouseEnter={() => setActive(i)}
|
|
164
|
+
onClick={() => choose(client)}
|
|
165
|
+
>
|
|
166
|
+
<Building2 size={14} strokeWidth={2} />
|
|
167
|
+
{client.label}
|
|
168
|
+
</button>
|
|
169
|
+
</li>
|
|
170
|
+
))}
|
|
171
|
+
|
|
172
|
+
{!loading && results.length === 0 && (
|
|
173
|
+
<li className={styles.clientEmpty}>
|
|
174
|
+
{term ? `No client matches “${term}”` : 'Start typing to find a client'}
|
|
175
|
+
</li>
|
|
176
|
+
)}
|
|
177
|
+
</ul>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export default VaultClientPicker;
|
|
@@ -16,6 +16,7 @@ import CustomFetch from '../Fetch';
|
|
|
16
16
|
import StandardModal from '../generic/StandardModal';
|
|
17
17
|
import styles from '../styles/Vault.module.scss';
|
|
18
18
|
import PasswordGenerator from './PasswordGenerator';
|
|
19
|
+
import VaultClientPicker from './VaultClientPicker';
|
|
19
20
|
import QrScanner from './QrScanner';
|
|
20
21
|
import { decodeFromBlob, imageFromTransfer } from './qrDecode';
|
|
21
22
|
import { resolveVaultEndpoints } from './vaultEndpoints';
|
|
@@ -76,6 +77,14 @@ const VaultEntryForm = ({
|
|
|
76
77
|
const [tags, setTags] = useState(() => parseTags(entry?.tags || prefill.tags || []));
|
|
77
78
|
const [tagDraft, setTagDraft] = useState('');
|
|
78
79
|
|
|
80
|
+
// Held as {id, label} so an existing entry renders its client without a
|
|
81
|
+
// lookup — the list payload already carries both.
|
|
82
|
+
const [client, setClient] = useState(() => {
|
|
83
|
+
const id = entry?.client_id ?? prefill.client_id ?? null;
|
|
84
|
+
|
|
85
|
+
return id ? { id, label: entry?.client_label || prefill.client_label || `#${id}` } : null;
|
|
86
|
+
});
|
|
87
|
+
|
|
79
88
|
const [password, setPassword] = useState('');
|
|
80
89
|
const [clearPassword, setClearPassword] = useState(false);
|
|
81
90
|
const [showPassword, setShowPassword] = useState(false);
|
|
@@ -260,6 +269,9 @@ const VaultEntryForm = ({
|
|
|
260
269
|
notes: values.notes || '',
|
|
261
270
|
tags,
|
|
262
271
|
visibility: values.visibility,
|
|
272
|
+
// Always sent, including as null — that is how an entry gets
|
|
273
|
+
// detached from a client it was attached to by mistake.
|
|
274
|
+
client_id: client?.id ?? null,
|
|
263
275
|
};
|
|
264
276
|
|
|
265
277
|
// Key presence is the contract — see the note at the top of the file.
|
|
@@ -370,6 +382,17 @@ const VaultEntryForm = ({
|
|
|
370
382
|
/>
|
|
371
383
|
)}
|
|
372
384
|
|
|
385
|
+
{field(
|
|
386
|
+
'client_id',
|
|
387
|
+
'Client',
|
|
388
|
+
<VaultClientPicker
|
|
389
|
+
value={client}
|
|
390
|
+
onChange={setClient}
|
|
391
|
+
endpoints={endpoints}
|
|
392
|
+
/>,
|
|
393
|
+
'Attach this credential to the client it belongs to, so it turns up on their record.'
|
|
394
|
+
)}
|
|
395
|
+
|
|
373
396
|
{field(
|
|
374
397
|
'username',
|
|
375
398
|
'Username',
|