@visns-studio/visns-components 6.17.2 → 6.20.1

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.
Files changed (83) hide show
  1. package/package.json +10 -7
  2. package/src/components/AssociationManager.jsx +344 -177
  3. package/src/components/AsyncSelect.jsx +2 -2
  4. package/src/components/Autocomplete.jsx +57 -12
  5. package/src/components/Breadcrumb.jsx +23 -4
  6. package/src/components/ClientAssociationManager.jsx +321 -145
  7. package/src/components/DataGrid.jsx +625 -20
  8. package/src/components/DropZone.jsx +4 -1
  9. package/src/components/Form.jsx +233 -6
  10. package/src/components/MultiSelect.jsx +2 -2
  11. package/src/components/Navigation.jsx +39 -23
  12. package/src/components/TableFilter.jsx +172 -12
  13. package/src/components/UniversalSearch.jsx +390 -0
  14. package/src/components/auth/Login.jsx +164 -8
  15. package/src/components/auth/Profile.jsx +209 -22
  16. package/src/components/auth/authEndpoints.js +4 -0
  17. package/src/components/auth/webauthn.js +194 -0
  18. package/src/components/columns/ColumnRenderers.jsx +6 -1
  19. package/src/components/controls/DataGridSearch.jsx +11 -5
  20. package/src/components/generic/GenericAuth.jsx +6 -0
  21. package/src/components/generic/GenericDashboard.jsx +93 -36
  22. package/src/components/generic/GenericDetail.jsx +146 -61
  23. package/src/components/generic/GenericIndex.jsx +142 -38
  24. package/src/components/generic/GenericSettings.jsx +220 -0
  25. package/src/components/generic/StandardModal.jsx +165 -8
  26. package/src/components/generic/dashboardChartDefaults.js +230 -0
  27. package/src/components/generic/shared/indexLayout.js +38 -0
  28. package/src/components/settings/IntegrationsManager.jsx +474 -0
  29. package/src/components/sms/SmsLineSettings.jsx +16 -12
  30. package/src/components/styles/AssociationManager.module.scss +4 -363
  31. package/src/components/styles/AuditLog.module.scss +6 -6
  32. package/src/components/styles/AuditLogs.module.scss +3 -3
  33. package/src/components/styles/Autocomplete.module.scss +7 -7
  34. package/src/components/styles/BusinessCardOcr.module.scss +6 -1
  35. package/src/components/styles/CallQueueSettings.module.scss +4 -1
  36. package/src/components/styles/CameraPlacement.module.scss +19 -19
  37. package/src/components/styles/CategorizedDropZone.module.scss +6 -1
  38. package/src/components/styles/ClientAssociationManager.module.scss +5 -289
  39. package/src/components/styles/ClientAuth.module.scss +1 -1
  40. package/src/components/styles/DataGrid.module.scss +455 -8
  41. package/src/components/styles/Field.module.scss +32 -24
  42. package/src/components/styles/Form.module.scss +123 -3
  43. package/src/components/styles/GenericDashboard.module.scss +612 -825
  44. package/src/components/styles/GenericDetail.module.scss +406 -61
  45. package/src/components/styles/GenericDynamic.module.scss +25 -25
  46. package/src/components/styles/GenericEditableTable.module.scss +33 -33
  47. package/src/components/styles/GenericFormBuilder.module.scss +119 -119
  48. package/src/components/styles/GenericIndex.module.scss +483 -38
  49. package/src/components/styles/GenericMain.module.scss +6 -3
  50. package/src/components/styles/GenericQuote.module.scss +399 -81
  51. package/src/components/styles/GenericReport.module.scss +8 -8
  52. package/src/components/styles/GenericSettings.module.scss +209 -0
  53. package/src/components/styles/GenericSort.module.scss +9 -9
  54. package/src/components/styles/Integrations.module.scss +321 -0
  55. package/src/components/styles/List.module.scss +4 -4
  56. package/src/components/styles/Login.module.scss +57 -0
  57. package/src/components/styles/MultiSelect.module.scss +4 -4
  58. package/src/components/styles/Navigation.module.scss +12 -12
  59. package/src/components/styles/Notification.module.scss +12 -8
  60. package/src/components/styles/NotificationList.module.scss +5 -5
  61. package/src/components/styles/Profile.module.scss +5 -5
  62. package/src/components/styles/QrCode.module.scss +4 -4
  63. package/src/components/styles/QuickAction.module.scss +20 -20
  64. package/src/components/styles/SortableQuoteItems.module.scss +4 -4
  65. package/src/components/styles/TableFilter.module.scss +128 -17
  66. package/src/components/styles/TicketConversation.module.scss +497 -0
  67. package/src/components/styles/UniversalSearch.module.scss +229 -0
  68. package/src/components/styles/Vault.module.scss +413 -0
  69. package/src/components/styles/_associationManager.scss +467 -0
  70. package/src/components/styles/_authBrandPanel.scss +15 -9
  71. package/src/components/styles/_controls.scss +43 -39
  72. package/src/components/styles/global-datagrid.css +196 -0
  73. package/src/components/styles/global.css +168 -17
  74. package/src/components/tickets/TicketConversation.jsx +455 -0
  75. package/src/components/utils/rowActionSettings.js +117 -0
  76. package/src/components/vault/VaultClientPicker.jsx +183 -0
  77. package/src/components/vault/VaultEntryForm.jsx +23 -0
  78. package/src/components/vault/VaultManager.jsx +382 -34
  79. package/src/components/vault/VaultShareModal.jsx +454 -0
  80. package/src/components/vault/vaultEndpoints.js +16 -2
  81. package/src/components/vault/vaultShares.js +247 -0
  82. package/src/index.js +35 -0
  83. 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',