@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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { toast } from 'react-toastify';
|
|
3
|
+
import { Star, Trash2, Plus, Calendar } from 'lucide-react';
|
|
3
4
|
import CustomFetch from './Fetch';
|
|
4
5
|
import MultiSelect from './MultiSelect';
|
|
6
|
+
import { confirmDialog } from './utils/ConfirmDialog';
|
|
5
7
|
import styles from './styles/ClientAssociationManager.module.scss';
|
|
6
8
|
|
|
7
9
|
const ClientAssociationManager = ({
|
|
@@ -13,7 +15,11 @@ const ClientAssociationManager = ({
|
|
|
13
15
|
const [associations, setAssociations] = useState([]);
|
|
14
16
|
const [availableOptions, setAvailableOptions] = useState([]);
|
|
15
17
|
const [loading, setLoading] = useState(true);
|
|
16
|
-
|
|
18
|
+
/* The editor is the uncommon case — the common one is reading the list —
|
|
19
|
+
so the form sits behind an "Add" affordance rather than owning the foot
|
|
20
|
+
of the panel. */
|
|
21
|
+
const [showForm, setShowForm] = useState(false);
|
|
22
|
+
|
|
17
23
|
// Dynamic state based on config
|
|
18
24
|
const [newAssociation, setNewAssociation] = useState(() => {
|
|
19
25
|
const initialState = {};
|
|
@@ -93,7 +99,7 @@ const ClientAssociationManager = ({
|
|
|
93
99
|
// Generic action handler
|
|
94
100
|
const handleAction = async (actionKey, data = {}) => {
|
|
95
101
|
const endpoint = endpoints[actionKey];
|
|
96
|
-
if (!endpoint) return;
|
|
102
|
+
if (!endpoint) return false;
|
|
97
103
|
|
|
98
104
|
try {
|
|
99
105
|
const url = processTemplate(endpoint.url || endpoint, data);
|
|
@@ -122,12 +128,16 @@ const ClientAssociationManager = ({
|
|
|
122
128
|
|
|
123
129
|
loadAssociations();
|
|
124
130
|
onUpdate();
|
|
131
|
+
|
|
132
|
+
return true;
|
|
125
133
|
} catch (error) {
|
|
126
|
-
const errorMessage = config.messages?.[`${actionKey}Error`] ||
|
|
127
|
-
endpoint.errorMessage ||
|
|
134
|
+
const errorMessage = config.messages?.[`${actionKey}Error`] ||
|
|
135
|
+
endpoint.errorMessage ||
|
|
128
136
|
'Action failed';
|
|
129
137
|
toast.error(errorMessage);
|
|
130
138
|
console.error(error);
|
|
139
|
+
|
|
140
|
+
return false;
|
|
131
141
|
}
|
|
132
142
|
};
|
|
133
143
|
|
|
@@ -153,7 +163,13 @@ const ClientAssociationManager = ({
|
|
|
153
163
|
}
|
|
154
164
|
});
|
|
155
165
|
|
|
156
|
-
await handleAction('associate', submitData);
|
|
166
|
+
const ok = await handleAction('associate', submitData);
|
|
167
|
+
|
|
168
|
+
// Collapse only on success — a failed save keeps what was typed on
|
|
169
|
+
// screen so it can be corrected rather than retyped.
|
|
170
|
+
if (ok) {
|
|
171
|
+
setShowForm(false);
|
|
172
|
+
}
|
|
157
173
|
};
|
|
158
174
|
|
|
159
175
|
// Remove association handler
|
|
@@ -162,6 +178,24 @@ const ClientAssociationManager = ({
|
|
|
162
178
|
await handleAction('dissociate', { [deleteField]: itemId });
|
|
163
179
|
};
|
|
164
180
|
|
|
181
|
+
/* Removal is the one irreversible action on the panel, so it asks first —
|
|
182
|
+
the same `confirmDialog` every destructive control in the app uses. */
|
|
183
|
+
const confirmDissociate = (association) => {
|
|
184
|
+
const nameKey = config.displayFields?.name || 'name';
|
|
185
|
+
|
|
186
|
+
confirmDialog({
|
|
187
|
+
title: config.labels?.dissociate || config.labels?.remove || 'Remove',
|
|
188
|
+
message:
|
|
189
|
+
config.messages?.confirmDissociate ||
|
|
190
|
+
'Are you sure you want to remove this association?',
|
|
191
|
+
buttons: [
|
|
192
|
+
{ label: 'Yes', onClick: () => handleDissociate(association.id) },
|
|
193
|
+
{ label: 'No', onClick: () => {} },
|
|
194
|
+
],
|
|
195
|
+
data: { name: association[nameKey] },
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
165
199
|
// Set primary handler
|
|
166
200
|
const handleSetPrimary = async (itemId) => {
|
|
167
201
|
if (!endpoints.setPrimary) return;
|
|
@@ -179,75 +213,286 @@ const ClientAssociationManager = ({
|
|
|
179
213
|
}
|
|
180
214
|
}, [dataId]);
|
|
181
215
|
|
|
216
|
+
|
|
217
|
+
/* Field names are resolved once, not per row. */
|
|
218
|
+
const nameField = config.displayFields?.name || 'name';
|
|
219
|
+
const primaryField = config.displayFields?.primary || 'is_primary';
|
|
220
|
+
const relationshipField = config.displayFields?.relationship || 'relationship_type';
|
|
221
|
+
const roleField = config.displayFields?.role || 'role';
|
|
222
|
+
const notesField = config.displayFields?.notes || 'notes';
|
|
223
|
+
const dateField = config.displayFields?.date || 'associated_at';
|
|
224
|
+
|
|
225
|
+
const requiredFieldId = config.fields?.find((f) => f.required)?.id || 'id';
|
|
226
|
+
|
|
227
|
+
/* The host page's tab already names this panel, so the title + subtitle
|
|
228
|
+
block that used to sit here is gone. A standalone mount can ask for a
|
|
229
|
+
compact one back with `config.showTitle`. */
|
|
230
|
+
const showTitle = Boolean(config.showTitle);
|
|
231
|
+
|
|
182
232
|
if (loading) {
|
|
183
|
-
return
|
|
233
|
+
return (
|
|
234
|
+
<div className={styles.container}>
|
|
235
|
+
<p className={styles.loading}>
|
|
236
|
+
{config.messages?.loading || 'Loading associations...'}
|
|
237
|
+
</p>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
184
240
|
}
|
|
185
241
|
|
|
242
|
+
/* One quiet line of metadata under the name, joined by middots. Built as a
|
|
243
|
+
list so a missing field closes the gap rather than leaving a separator
|
|
244
|
+
stranded — the old markup printed a bare " - " before an absent note. */
|
|
245
|
+
const renderMeta = (association) => {
|
|
246
|
+
const parts = [];
|
|
247
|
+
|
|
248
|
+
if (association[relationshipField]) {
|
|
249
|
+
parts.push(
|
|
250
|
+
<span key="relationship" className={styles.relationshipType}>
|
|
251
|
+
{association[relationshipField]}
|
|
252
|
+
</span>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (association[roleField]) {
|
|
257
|
+
parts.push(
|
|
258
|
+
<span key="role" className={styles.relationshipType}>
|
|
259
|
+
{association[roleField]}
|
|
260
|
+
</span>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (association[notesField]) {
|
|
265
|
+
parts.push(
|
|
266
|
+
<span key="notes" className={styles.notes}>
|
|
267
|
+
{association[notesField]}
|
|
268
|
+
</span>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (dateField && association[dateField]) {
|
|
273
|
+
parts.push(
|
|
274
|
+
<span key="date" className={styles.date}>
|
|
275
|
+
<Calendar size={11} aria-hidden="true" />
|
|
276
|
+
{config.labels?.associatedAt || 'Associated'}{' '}
|
|
277
|
+
{new Date(association[dateField]).toLocaleDateString()}
|
|
278
|
+
</span>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (parts.length === 0) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div className={styles.meta}>
|
|
288
|
+
{parts.map((part, index) => (
|
|
289
|
+
<React.Fragment key={part.key}>
|
|
290
|
+
{index > 0 && (
|
|
291
|
+
<span className={styles.sep} aria-hidden="true">
|
|
292
|
+
·
|
|
293
|
+
</span>
|
|
294
|
+
)}
|
|
295
|
+
{part}
|
|
296
|
+
</React.Fragment>
|
|
297
|
+
))}
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const renderField = (field) => {
|
|
303
|
+
const fieldValue = newAssociation[field.id] || '';
|
|
304
|
+
const wide = field.type === 'textarea' || field.type === 'boolean';
|
|
305
|
+
|
|
306
|
+
if (field.type === 'boolean') {
|
|
307
|
+
return (
|
|
308
|
+
<div
|
|
309
|
+
key={field.id}
|
|
310
|
+
className={`${styles.field} ${styles.fieldWide}`}
|
|
311
|
+
>
|
|
312
|
+
<label className={styles.checkboxLabel}>
|
|
313
|
+
<input
|
|
314
|
+
type="checkbox"
|
|
315
|
+
checked={Boolean(fieldValue)}
|
|
316
|
+
onChange={(e) =>
|
|
317
|
+
setNewAssociation((prev) => ({
|
|
318
|
+
...prev,
|
|
319
|
+
[field.id]: e.target.checked,
|
|
320
|
+
}))
|
|
321
|
+
}
|
|
322
|
+
/>
|
|
323
|
+
{field.checkboxLabel || field.label}
|
|
324
|
+
</label>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<div
|
|
331
|
+
key={field.id}
|
|
332
|
+
className={`${styles.field} ${wide ? styles.fieldWide : ''}`}
|
|
333
|
+
>
|
|
334
|
+
<label className={styles.label} htmlFor={`client-assoc-${field.id}`}>
|
|
335
|
+
{field.label}
|
|
336
|
+
{field.required && <span className={styles.required}> *</span>}
|
|
337
|
+
</label>
|
|
338
|
+
|
|
339
|
+
{field.type === 'multi-select' && (
|
|
340
|
+
<MultiSelect
|
|
341
|
+
settings={{ id: field.id }}
|
|
342
|
+
multi={field.multi || false}
|
|
343
|
+
onChange={(value) =>
|
|
344
|
+
setNewAssociation((prev) => ({ ...prev, [field.id]: value }))
|
|
345
|
+
}
|
|
346
|
+
inputValue={fieldValue}
|
|
347
|
+
options={availableOptions}
|
|
348
|
+
placeholder={field.placeholder || `Select ${field.label}...`}
|
|
349
|
+
/>
|
|
350
|
+
)}
|
|
351
|
+
|
|
352
|
+
{field.type === 'select' && (
|
|
353
|
+
<select
|
|
354
|
+
id={`client-assoc-${field.id}`}
|
|
355
|
+
value={fieldValue}
|
|
356
|
+
onChange={(e) =>
|
|
357
|
+
setNewAssociation((prev) => ({
|
|
358
|
+
...prev,
|
|
359
|
+
[field.id]: e.target.value,
|
|
360
|
+
}))
|
|
361
|
+
}
|
|
362
|
+
className={styles.selectField}
|
|
363
|
+
>
|
|
364
|
+
{field.options &&
|
|
365
|
+
field.options.map((option) => (
|
|
366
|
+
<option
|
|
367
|
+
key={option.value || option}
|
|
368
|
+
value={option.value || option}
|
|
369
|
+
>
|
|
370
|
+
{option.label || option}
|
|
371
|
+
</option>
|
|
372
|
+
))}
|
|
373
|
+
</select>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{field.type === 'textarea' && (
|
|
377
|
+
<textarea
|
|
378
|
+
id={`client-assoc-${field.id}`}
|
|
379
|
+
value={fieldValue}
|
|
380
|
+
onChange={(e) =>
|
|
381
|
+
setNewAssociation((prev) => ({
|
|
382
|
+
...prev,
|
|
383
|
+
[field.id]: e.target.value,
|
|
384
|
+
}))
|
|
385
|
+
}
|
|
386
|
+
placeholder={field.placeholder || ''}
|
|
387
|
+
className={styles.textareaField}
|
|
388
|
+
rows={field.rows || 3}
|
|
389
|
+
/>
|
|
390
|
+
)}
|
|
391
|
+
|
|
392
|
+
{field.type === 'text' && (
|
|
393
|
+
<input
|
|
394
|
+
id={`client-assoc-${field.id}`}
|
|
395
|
+
type="text"
|
|
396
|
+
value={fieldValue}
|
|
397
|
+
onChange={(e) =>
|
|
398
|
+
setNewAssociation((prev) => ({
|
|
399
|
+
...prev,
|
|
400
|
+
[field.id]: e.target.value,
|
|
401
|
+
}))
|
|
402
|
+
}
|
|
403
|
+
placeholder={field.placeholder || ''}
|
|
404
|
+
className={styles.textField}
|
|
405
|
+
/>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
};
|
|
410
|
+
|
|
186
411
|
return (
|
|
187
412
|
<div className={styles.container}>
|
|
188
|
-
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
413
|
+
{showTitle && (
|
|
414
|
+
<div>
|
|
415
|
+
<h3 className={styles.heading}>{config.title || 'Associations'}</h3>
|
|
416
|
+
{config.description && (
|
|
417
|
+
<p className={styles.headingNote}>{config.description}</p>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
|
|
422
|
+
<div className={styles.section}>
|
|
423
|
+
<div className={styles.sectionHead}>
|
|
424
|
+
<h4 className={styles.sectionTitle}>
|
|
425
|
+
{config.labels?.currentAssociations || 'Current Associations'}
|
|
426
|
+
</h4>
|
|
427
|
+
{!showForm && (
|
|
428
|
+
<button
|
|
429
|
+
type="button"
|
|
430
|
+
className={styles.addToggle}
|
|
431
|
+
onClick={() => setShowForm(true)}
|
|
432
|
+
>
|
|
433
|
+
<Plus size={14} aria-hidden="true" />
|
|
434
|
+
{config.labels?.addNew || 'Add Association'}
|
|
435
|
+
</button>
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
192
438
|
|
|
193
|
-
{/* Existing Associations */}
|
|
194
|
-
<div className={styles.existingAssociations}>
|
|
195
|
-
<h4>{config.labels?.currentAssociations || 'Current Associations'}</h4>
|
|
196
439
|
{associations.length === 0 ? (
|
|
197
|
-
<
|
|
440
|
+
<p className={styles.empty}>
|
|
198
441
|
{config.messages?.noAssociations || 'No associations found.'}
|
|
199
|
-
</
|
|
442
|
+
</p>
|
|
200
443
|
) : (
|
|
201
|
-
<div className={styles.
|
|
444
|
+
<div className={styles.list}>
|
|
202
445
|
{associations.map((association) => {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
const relationshipField = config.displayFields?.relationship || 'relationship_type';
|
|
206
|
-
const notesField = config.displayFields?.notes || 'notes';
|
|
207
|
-
const dateField = config.displayFields?.date || 'associated_at';
|
|
208
|
-
|
|
446
|
+
const isPrimary = Boolean(association[primaryField]);
|
|
447
|
+
|
|
209
448
|
return (
|
|
210
|
-
<div
|
|
211
|
-
key={association.id}
|
|
212
|
-
className={`${styles.
|
|
449
|
+
<div
|
|
450
|
+
key={association.id}
|
|
451
|
+
className={`${styles.row} ${isPrimary ? styles.rowPrimary : ''}`}
|
|
213
452
|
>
|
|
214
|
-
<div className={styles.
|
|
215
|
-
<div className={styles.
|
|
216
|
-
{association[nameField]}
|
|
217
|
-
{
|
|
218
|
-
<span
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
- {association[notesField]}
|
|
453
|
+
<div className={styles.main}>
|
|
454
|
+
<div className={styles.entityName}>
|
|
455
|
+
<span>{association[nameField]}</span>
|
|
456
|
+
{isPrimary && (
|
|
457
|
+
<span
|
|
458
|
+
className={`${styles.chip} ${styles.chipPrimary}`}
|
|
459
|
+
>
|
|
460
|
+
<Star
|
|
461
|
+
size={10}
|
|
462
|
+
fill="currentColor"
|
|
463
|
+
aria-hidden="true"
|
|
464
|
+
/>
|
|
465
|
+
{config.labels?.primaryBadge || 'Primary'}
|
|
228
466
|
</span>
|
|
229
467
|
)}
|
|
230
468
|
</div>
|
|
231
|
-
|
|
232
|
-
{config.labels?.associatedAt || 'Associated'}: {new Date(association[dateField]).toLocaleDateString()}
|
|
233
|
-
</div>
|
|
469
|
+
{renderMeta(association)}
|
|
234
470
|
</div>
|
|
235
|
-
|
|
236
|
-
|
|
471
|
+
|
|
472
|
+
<div className={styles.actions}>
|
|
473
|
+
{!isPrimary && endpoints.setPrimary && (
|
|
237
474
|
<button
|
|
238
475
|
type="button"
|
|
239
|
-
className={styles.
|
|
476
|
+
className={styles.iconButton}
|
|
240
477
|
onClick={() => handleSetPrimary(association.id)}
|
|
478
|
+
title={
|
|
479
|
+
config.labels?.setPrimary || 'Set Primary'
|
|
480
|
+
}
|
|
481
|
+
aria-label={
|
|
482
|
+
config.labels?.setPrimary || 'Set Primary'
|
|
483
|
+
}
|
|
241
484
|
>
|
|
242
|
-
{
|
|
485
|
+
<Star size={15} aria-hidden="true" />
|
|
243
486
|
</button>
|
|
244
487
|
)}
|
|
245
488
|
<button
|
|
246
489
|
type="button"
|
|
247
|
-
className={styles.
|
|
248
|
-
onClick={() =>
|
|
490
|
+
className={`${styles.iconButton} ${styles.iconButtonDanger}`}
|
|
491
|
+
onClick={() => confirmDissociate(association)}
|
|
492
|
+
title={config.labels?.remove || 'Remove'}
|
|
493
|
+
aria-label={config.labels?.remove || 'Remove'}
|
|
249
494
|
>
|
|
250
|
-
{
|
|
495
|
+
<Trash2 size={15} aria-hidden="true" />
|
|
251
496
|
</button>
|
|
252
497
|
</div>
|
|
253
498
|
</div>
|
|
@@ -257,106 +502,37 @@ const ClientAssociationManager = ({
|
|
|
257
502
|
)}
|
|
258
503
|
</div>
|
|
259
504
|
|
|
260
|
-
{
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return (
|
|
268
|
-
<div key={field.id} className={styles.formRow}>
|
|
269
|
-
<div className={styles.formField}>
|
|
270
|
-
<label>
|
|
271
|
-
{field.label}{field.required && ' *'}
|
|
272
|
-
</label>
|
|
273
|
-
|
|
274
|
-
{field.type === 'multi-select' && (
|
|
275
|
-
<MultiSelect
|
|
276
|
-
settings={{ id: field.id }}
|
|
277
|
-
multi={field.multi || false}
|
|
278
|
-
onChange={(value) => setNewAssociation(prev => ({ ...prev, [field.id]: value }))}
|
|
279
|
-
inputValue={fieldValue}
|
|
280
|
-
options={availableOptions}
|
|
281
|
-
placeholder={field.placeholder || `Select ${field.label}...`}
|
|
282
|
-
/>
|
|
283
|
-
)}
|
|
284
|
-
|
|
285
|
-
{field.type === 'select' && (
|
|
286
|
-
<select
|
|
287
|
-
value={fieldValue}
|
|
288
|
-
onChange={(e) => setNewAssociation(prev => ({
|
|
289
|
-
...prev,
|
|
290
|
-
[field.id]: e.target.value
|
|
291
|
-
}))}
|
|
292
|
-
className={styles.selectField}
|
|
293
|
-
>
|
|
294
|
-
{field.options && field.options.map(option => (
|
|
295
|
-
<option key={option.value || option} value={option.value || option}>
|
|
296
|
-
{option.label || option}
|
|
297
|
-
</option>
|
|
298
|
-
))}
|
|
299
|
-
</select>
|
|
300
|
-
)}
|
|
301
|
-
|
|
302
|
-
{field.type === 'boolean' && (
|
|
303
|
-
<label>
|
|
304
|
-
<input
|
|
305
|
-
type="checkbox"
|
|
306
|
-
checked={fieldValue}
|
|
307
|
-
onChange={(e) => setNewAssociation(prev => ({
|
|
308
|
-
...prev,
|
|
309
|
-
[field.id]: e.target.checked
|
|
310
|
-
}))}
|
|
311
|
-
/>
|
|
312
|
-
{field.checkboxLabel || field.label}
|
|
313
|
-
</label>
|
|
314
|
-
)}
|
|
315
|
-
|
|
316
|
-
{field.type === 'textarea' && (
|
|
317
|
-
<textarea
|
|
318
|
-
value={fieldValue}
|
|
319
|
-
onChange={(e) => setNewAssociation(prev => ({
|
|
320
|
-
...prev,
|
|
321
|
-
[field.id]: e.target.value
|
|
322
|
-
}))}
|
|
323
|
-
placeholder={field.placeholder || ''}
|
|
324
|
-
className={styles.textareaField}
|
|
325
|
-
rows={field.rows || 3}
|
|
326
|
-
/>
|
|
327
|
-
)}
|
|
328
|
-
|
|
329
|
-
{field.type === 'text' && (
|
|
330
|
-
<input
|
|
331
|
-
type="text"
|
|
332
|
-
value={fieldValue}
|
|
333
|
-
onChange={(e) => setNewAssociation(prev => ({
|
|
334
|
-
...prev,
|
|
335
|
-
[field.id]: e.target.value
|
|
336
|
-
}))}
|
|
337
|
-
placeholder={field.placeholder || ''}
|
|
338
|
-
className={styles.textField}
|
|
339
|
-
/>
|
|
340
|
-
)}
|
|
341
|
-
</div>
|
|
342
|
-
</div>
|
|
343
|
-
);
|
|
344
|
-
})}
|
|
505
|
+
{showForm && (
|
|
506
|
+
<div className={styles.section}>
|
|
507
|
+
<h4 className={styles.sectionTitle}>
|
|
508
|
+
{config.labels?.addNew || 'Add New Association'}
|
|
509
|
+
</h4>
|
|
510
|
+
<div className={styles.form}>
|
|
511
|
+
{config.fields && config.fields.map(renderField)}
|
|
345
512
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
513
|
+
<div className={styles.formActions}>
|
|
514
|
+
<button
|
|
515
|
+
type="button"
|
|
516
|
+
className={styles.secondaryButton}
|
|
517
|
+
onClick={() => setShowForm(false)}
|
|
518
|
+
>
|
|
519
|
+
{config.labels?.cancel || 'Cancel'}
|
|
520
|
+
</button>
|
|
521
|
+
<button
|
|
522
|
+
type="button"
|
|
523
|
+
className={styles.primaryButton}
|
|
524
|
+
onClick={handleAssociate}
|
|
525
|
+
disabled={!newAssociation[requiredFieldId]}
|
|
526
|
+
>
|
|
527
|
+
<Plus size={14} aria-hidden="true" />
|
|
528
|
+
{config.labels?.addButton || 'Add Association'}
|
|
529
|
+
</button>
|
|
530
|
+
</div>
|
|
355
531
|
</div>
|
|
356
532
|
</div>
|
|
357
|
-
|
|
533
|
+
)}
|
|
358
534
|
</div>
|
|
359
535
|
);
|
|
360
536
|
};
|
|
361
537
|
|
|
362
|
-
export default ClientAssociationManager;
|
|
538
|
+
export default ClientAssociationManager;
|