@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,21 +1,26 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { toast } from 'react-toastify';
|
|
3
|
-
import { Star, Trash2, Plus,
|
|
3
|
+
import { Star, StarOff, Trash2, Plus, Calendar, AlertTriangle } from 'lucide-react';
|
|
4
4
|
import CustomFetch from './Fetch';
|
|
5
5
|
import MultiSelect from './MultiSelect';
|
|
6
|
+
import { confirmDialog } from './utils/ConfirmDialog';
|
|
6
7
|
import styles from './styles/AssociationManager.module.scss';
|
|
7
8
|
|
|
8
|
-
const AssociationManager = ({
|
|
9
|
-
endpoints,
|
|
10
|
-
config,
|
|
9
|
+
const AssociationManager = ({
|
|
10
|
+
endpoints,
|
|
11
|
+
config,
|
|
11
12
|
dataId,
|
|
12
|
-
onUpdate = () => {}
|
|
13
|
+
onUpdate = () => {}
|
|
13
14
|
}) => {
|
|
14
15
|
const [associations, setAssociations] = useState([]);
|
|
15
16
|
const [availableOptions, setAvailableOptions] = useState([]);
|
|
16
17
|
const [childOptions, setChildOptions] = useState({});
|
|
17
18
|
const [loading, setLoading] = useState(true);
|
|
18
|
-
|
|
19
|
+
/* The editor is the uncommon case — the common one is reading the list —
|
|
20
|
+
so the form sits behind an "Add" affordance rather than owning 700px at
|
|
21
|
+
the foot of the panel. */
|
|
22
|
+
const [showForm, setShowForm] = useState(false);
|
|
23
|
+
|
|
19
24
|
// Dynamic state based on config
|
|
20
25
|
const [newAssociation, setNewAssociation] = useState(() => {
|
|
21
26
|
const initialState = {};
|
|
@@ -196,7 +201,7 @@ const AssociationManager = ({
|
|
|
196
201
|
// Generic action handler
|
|
197
202
|
const handleAction = async (actionKey, data = {}) => {
|
|
198
203
|
const endpoint = endpoints[actionKey];
|
|
199
|
-
if (!endpoint) return;
|
|
204
|
+
if (!endpoint) return false;
|
|
200
205
|
|
|
201
206
|
try {
|
|
202
207
|
const url = processTemplate(endpoint.url || endpoint, data);
|
|
@@ -225,12 +230,16 @@ const AssociationManager = ({
|
|
|
225
230
|
|
|
226
231
|
loadAssociations();
|
|
227
232
|
onUpdate();
|
|
233
|
+
|
|
234
|
+
return true;
|
|
228
235
|
} catch (error) {
|
|
229
|
-
const errorMessage = config.messages?.[`${actionKey}Error`] ||
|
|
230
|
-
endpoint.errorMessage ||
|
|
236
|
+
const errorMessage = config.messages?.[`${actionKey}Error`] ||
|
|
237
|
+
endpoint.errorMessage ||
|
|
231
238
|
'Action failed';
|
|
232
239
|
toast.error(errorMessage);
|
|
233
240
|
console.error(error);
|
|
241
|
+
|
|
242
|
+
return false;
|
|
234
243
|
}
|
|
235
244
|
};
|
|
236
245
|
|
|
@@ -256,7 +265,13 @@ const AssociationManager = ({
|
|
|
256
265
|
}
|
|
257
266
|
});
|
|
258
267
|
|
|
259
|
-
await handleAction('associate', submitData);
|
|
268
|
+
const ok = await handleAction('associate', submitData);
|
|
269
|
+
|
|
270
|
+
// Collapse only on success — a failed save keeps what was typed on
|
|
271
|
+
// screen so it can be corrected rather than retyped.
|
|
272
|
+
if (ok) {
|
|
273
|
+
setShowForm(false);
|
|
274
|
+
}
|
|
260
275
|
};
|
|
261
276
|
|
|
262
277
|
// Remove association handler
|
|
@@ -265,6 +280,26 @@ const AssociationManager = ({
|
|
|
265
280
|
await handleAction('dissociate', { [deleteField]: itemId });
|
|
266
281
|
};
|
|
267
282
|
|
|
283
|
+
/* Removal is the one irreversible action on the panel, so it asks first —
|
|
284
|
+
the same `confirmDialog` every destructive control in the app uses. The
|
|
285
|
+
wording is already in the tab config (`messages.confirmDissociate`),
|
|
286
|
+
which nothing read until now. */
|
|
287
|
+
const confirmDissociate = (association) => {
|
|
288
|
+
const nameField = config.displayFields?.name || 'name';
|
|
289
|
+
|
|
290
|
+
confirmDialog({
|
|
291
|
+
title: config.labels?.dissociate || config.labels?.remove || 'Remove',
|
|
292
|
+
message:
|
|
293
|
+
config.messages?.confirmDissociate ||
|
|
294
|
+
'Are you sure you want to remove this association?',
|
|
295
|
+
buttons: [
|
|
296
|
+
{ label: 'Yes', onClick: () => handleDissociate(association.id) },
|
|
297
|
+
{ label: 'No', onClick: () => {} },
|
|
298
|
+
],
|
|
299
|
+
data: { name: association[nameField] },
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
|
|
268
303
|
// Set primary handler
|
|
269
304
|
const handleSetPrimary = async (association) => {
|
|
270
305
|
if (!endpoints.setPrimary) return;
|
|
@@ -319,132 +354,323 @@ const AssociationManager = ({
|
|
|
319
354
|
}
|
|
320
355
|
}, [dataId]);
|
|
321
356
|
|
|
357
|
+
/* Field names are resolved once, not per row. */
|
|
358
|
+
const nameField = config.displayFields?.name || 'name';
|
|
359
|
+
const primaryField = config.displayFields?.primary || 'is_primary';
|
|
360
|
+
const relationshipField = config.displayFields?.relationship || 'relationship_type';
|
|
361
|
+
const roleField = config.displayFields?.role || 'role';
|
|
362
|
+
const notesField = config.displayFields?.notes || 'notes';
|
|
363
|
+
const dateField = config.displayFields?.date || 'associated_at';
|
|
364
|
+
|
|
365
|
+
const requiredFieldId = config.fields?.find((f) => f.required)?.id || 'id';
|
|
366
|
+
|
|
367
|
+
/* The host page's tab already names this panel, so the icon + title +
|
|
368
|
+
subtitle block that used to sit here is gone. A standalone mount (the
|
|
369
|
+
component lab, a modal) can ask for a compact one back. */
|
|
370
|
+
const showTitle = Boolean(config.showTitle);
|
|
371
|
+
|
|
322
372
|
if (loading) {
|
|
323
|
-
return
|
|
373
|
+
return (
|
|
374
|
+
<div className={styles.container}>
|
|
375
|
+
<p className={styles.loading}>
|
|
376
|
+
{config.messages?.loading || 'Loading associations...'}
|
|
377
|
+
</p>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
324
380
|
}
|
|
325
381
|
|
|
382
|
+
/* One quiet line of metadata under the name — relationship, role, notes,
|
|
383
|
+
date — joined by middots. Built as a list so a missing field closes the
|
|
384
|
+
gap instead of leaving a stray separator. */
|
|
385
|
+
const renderMeta = (association) => {
|
|
386
|
+
const parts = [];
|
|
387
|
+
|
|
388
|
+
if (association[relationshipField]) {
|
|
389
|
+
parts.push(
|
|
390
|
+
<span key="relationship" className={styles.relationshipType}>
|
|
391
|
+
{association[relationshipField]}
|
|
392
|
+
</span>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (association[roleField]) {
|
|
397
|
+
parts.push(
|
|
398
|
+
<span key="role" className={styles.relationshipType}>
|
|
399
|
+
{association[roleField]}
|
|
400
|
+
</span>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (association[notesField]) {
|
|
405
|
+
parts.push(
|
|
406
|
+
<span key="notes" className={styles.notes}>
|
|
407
|
+
{association[notesField]}
|
|
408
|
+
</span>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (dateField && association[dateField]) {
|
|
413
|
+
parts.push(
|
|
414
|
+
<span key="date" className={styles.date}>
|
|
415
|
+
<Calendar size={11} aria-hidden="true" />
|
|
416
|
+
{config.labels?.associatedAt || 'Associated'}{' '}
|
|
417
|
+
{new Date(association[dateField]).toLocaleDateString()}
|
|
418
|
+
</span>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (parts.length === 0) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<div className={styles.meta}>
|
|
428
|
+
{parts.map((part, index) => (
|
|
429
|
+
<React.Fragment key={part.key}>
|
|
430
|
+
{index > 0 && (
|
|
431
|
+
<span className={styles.sep} aria-hidden="true">
|
|
432
|
+
·
|
|
433
|
+
</span>
|
|
434
|
+
)}
|
|
435
|
+
{part}
|
|
436
|
+
</React.Fragment>
|
|
437
|
+
))}
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const renderField = (field) => {
|
|
443
|
+
const fieldValue = newAssociation[field.id] || '';
|
|
444
|
+
const wide = field.type === 'textarea' || field.type === 'boolean';
|
|
445
|
+
|
|
446
|
+
if (field.type === 'boolean') {
|
|
447
|
+
return (
|
|
448
|
+
<div
|
|
449
|
+
key={field.id}
|
|
450
|
+
className={`${styles.field} ${styles.fieldWide}`}
|
|
451
|
+
>
|
|
452
|
+
<label className={styles.checkboxLabel}>
|
|
453
|
+
<input
|
|
454
|
+
type="checkbox"
|
|
455
|
+
checked={Boolean(fieldValue)}
|
|
456
|
+
onChange={(e) =>
|
|
457
|
+
handleFieldChange(field.id, e.target.checked, field)
|
|
458
|
+
}
|
|
459
|
+
/>
|
|
460
|
+
{field.checkboxLabel || field.label}
|
|
461
|
+
</label>
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<div
|
|
468
|
+
key={field.id}
|
|
469
|
+
className={`${styles.field} ${wide ? styles.fieldWide : ''}`}
|
|
470
|
+
>
|
|
471
|
+
<label className={styles.label} htmlFor={`assoc-${field.id}`}>
|
|
472
|
+
{field.label}
|
|
473
|
+
{field.required && <span className={styles.required}> *</span>}
|
|
474
|
+
</label>
|
|
475
|
+
|
|
476
|
+
{field.type === 'multi-select' && (
|
|
477
|
+
<MultiSelect
|
|
478
|
+
settings={{ id: field.id }}
|
|
479
|
+
multi={field.multi || false}
|
|
480
|
+
onChange={(value) => handleFieldChange(field.id, value, field)}
|
|
481
|
+
inputValue={fieldValue}
|
|
482
|
+
options={
|
|
483
|
+
field.id === 'customer_id'
|
|
484
|
+
? availableOptions
|
|
485
|
+
: childOptions[field.id] || []
|
|
486
|
+
}
|
|
487
|
+
placeholder={field.placeholder || `Select ${field.label}...`}
|
|
488
|
+
/>
|
|
489
|
+
)}
|
|
490
|
+
|
|
491
|
+
{field.type === 'select' && (
|
|
492
|
+
<select
|
|
493
|
+
id={`assoc-${field.id}`}
|
|
494
|
+
value={fieldValue}
|
|
495
|
+
onChange={(e) =>
|
|
496
|
+
handleFieldChange(field.id, e.target.value, field)
|
|
497
|
+
}
|
|
498
|
+
className={styles.selectField}
|
|
499
|
+
>
|
|
500
|
+
<option value="">Select {field.label}...</option>
|
|
501
|
+
{(field.options || childOptions[field.id] || []).map((option) => (
|
|
502
|
+
<option
|
|
503
|
+
key={option.value || option}
|
|
504
|
+
value={option.value || option}
|
|
505
|
+
>
|
|
506
|
+
{option.label || option}
|
|
507
|
+
</option>
|
|
508
|
+
))}
|
|
509
|
+
</select>
|
|
510
|
+
)}
|
|
511
|
+
|
|
512
|
+
{field.type === 'textarea' && (
|
|
513
|
+
<textarea
|
|
514
|
+
id={`assoc-${field.id}`}
|
|
515
|
+
value={fieldValue}
|
|
516
|
+
onChange={(e) =>
|
|
517
|
+
handleFieldChange(field.id, e.target.value, field)
|
|
518
|
+
}
|
|
519
|
+
placeholder={field.placeholder || ''}
|
|
520
|
+
className={styles.textareaField}
|
|
521
|
+
rows={field.rows || 3}
|
|
522
|
+
/>
|
|
523
|
+
)}
|
|
524
|
+
|
|
525
|
+
{field.type === 'text' && (
|
|
526
|
+
<input
|
|
527
|
+
id={`assoc-${field.id}`}
|
|
528
|
+
type="text"
|
|
529
|
+
value={fieldValue}
|
|
530
|
+
onChange={(e) =>
|
|
531
|
+
handleFieldChange(field.id, e.target.value, field)
|
|
532
|
+
}
|
|
533
|
+
placeholder={field.placeholder || ''}
|
|
534
|
+
className={styles.textField}
|
|
535
|
+
/>
|
|
536
|
+
)}
|
|
537
|
+
</div>
|
|
538
|
+
);
|
|
539
|
+
};
|
|
540
|
+
|
|
326
541
|
return (
|
|
327
542
|
<div className={styles.container}>
|
|
328
|
-
|
|
329
|
-
<
|
|
330
|
-
<
|
|
331
|
-
{config.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
543
|
+
{showTitle && (
|
|
544
|
+
<div>
|
|
545
|
+
<h3 className={styles.heading}>{config.title || 'Associations'}</h3>
|
|
546
|
+
{config.description && (
|
|
547
|
+
<p className={styles.headingNote}>{config.description}</p>
|
|
548
|
+
)}
|
|
549
|
+
</div>
|
|
550
|
+
)}
|
|
335
551
|
|
|
336
|
-
{/* Multiple Primary Warning */}
|
|
337
552
|
{hasMultiplePrimary() && (
|
|
338
|
-
<div className={styles.
|
|
339
|
-
<AlertTriangle
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
553
|
+
<div className={styles.notice} role="status">
|
|
554
|
+
<AlertTriangle
|
|
555
|
+
size={15}
|
|
556
|
+
className={styles.noticeIcon}
|
|
557
|
+
aria-hidden="true"
|
|
558
|
+
/>
|
|
559
|
+
<div className={styles.noticeBody}>
|
|
560
|
+
<strong>
|
|
561
|
+
{config.messages?.multiplePrimaryWarning ||
|
|
562
|
+
'Multiple Primary Associations Detected'}
|
|
563
|
+
</strong>
|
|
564
|
+
<p>
|
|
565
|
+
{config.messages?.multiplePrimaryDescription ||
|
|
566
|
+
'You have multiple associations marked as primary. Please resolve this by keeping only one primary association.'}
|
|
567
|
+
</p>
|
|
343
568
|
{endpoints.clearAllPrimary && (
|
|
344
569
|
<button
|
|
345
570
|
type="button"
|
|
346
|
-
className={styles.
|
|
571
|
+
className={styles.noticeAction}
|
|
347
572
|
onClick={handleClearAllPrimary}
|
|
348
573
|
>
|
|
349
|
-
{config.labels?.clearAllPrimary ||
|
|
574
|
+
{config.labels?.clearAllPrimary ||
|
|
575
|
+
'Clear All Primary Status'}
|
|
350
576
|
</button>
|
|
351
577
|
)}
|
|
352
578
|
</div>
|
|
353
579
|
</div>
|
|
354
580
|
)}
|
|
355
581
|
|
|
356
|
-
{
|
|
357
|
-
|
|
358
|
-
|
|
582
|
+
<div className={styles.section}>
|
|
583
|
+
<div className={styles.sectionHead}>
|
|
584
|
+
<h4 className={styles.sectionTitle}>
|
|
585
|
+
{config.labels?.currentAssociations || 'Current Associations'}
|
|
586
|
+
</h4>
|
|
587
|
+
{!showForm && (
|
|
588
|
+
<button
|
|
589
|
+
type="button"
|
|
590
|
+
className={styles.addToggle}
|
|
591
|
+
onClick={() => setShowForm(true)}
|
|
592
|
+
>
|
|
593
|
+
<Plus size={14} aria-hidden="true" />
|
|
594
|
+
{config.labels?.addNew || 'Add Association'}
|
|
595
|
+
</button>
|
|
596
|
+
)}
|
|
597
|
+
</div>
|
|
598
|
+
|
|
359
599
|
{associations.length === 0 ? (
|
|
360
|
-
<
|
|
600
|
+
<p className={styles.empty}>
|
|
361
601
|
{config.messages?.noAssociations || 'No associations found.'}
|
|
362
|
-
</
|
|
602
|
+
</p>
|
|
363
603
|
) : (
|
|
364
|
-
<div className={styles.
|
|
604
|
+
<div className={styles.list}>
|
|
365
605
|
{associations.map((association) => {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const nameField = config.displayFields?.name || 'name';
|
|
369
|
-
const primaryField = config.displayFields?.primary || 'is_primary';
|
|
370
|
-
const relationshipField = config.displayFields?.relationship || 'relationship_type';
|
|
371
|
-
const notesField = config.displayFields?.notes || 'notes';
|
|
372
|
-
const dateField = config.displayFields?.date || 'associated_at';
|
|
373
|
-
|
|
374
|
-
console.log('📊 Field values:', {
|
|
375
|
-
name: association[nameField],
|
|
376
|
-
primary: association[primaryField],
|
|
377
|
-
relationship: association[relationshipField],
|
|
378
|
-
notes: association[notesField],
|
|
379
|
-
date: association[dateField]
|
|
380
|
-
});
|
|
381
|
-
|
|
606
|
+
const isPrimary = Boolean(association[primaryField]);
|
|
607
|
+
|
|
382
608
|
return (
|
|
383
|
-
<div
|
|
384
|
-
key={association.id}
|
|
385
|
-
className={`${styles.
|
|
609
|
+
<div
|
|
610
|
+
key={association.id}
|
|
611
|
+
className={`${styles.row} ${isPrimary ? styles.rowPrimary : ''}`}
|
|
386
612
|
>
|
|
387
|
-
<div className={styles.
|
|
613
|
+
<div className={styles.main}>
|
|
388
614
|
<div className={styles.entityName}>
|
|
389
|
-
{association[nameField]}
|
|
390
|
-
{
|
|
391
|
-
<span
|
|
392
|
-
|
|
615
|
+
<span>{association[nameField]}</span>
|
|
616
|
+
{isPrimary && (
|
|
617
|
+
<span
|
|
618
|
+
className={`${styles.chip} ${styles.chipPrimary}`}
|
|
619
|
+
>
|
|
620
|
+
<Star
|
|
621
|
+
size={10}
|
|
622
|
+
fill="currentColor"
|
|
623
|
+
aria-hidden="true"
|
|
624
|
+
/>
|
|
393
625
|
{config.labels?.primaryBadge || 'Primary'}
|
|
394
626
|
</span>
|
|
395
627
|
)}
|
|
396
628
|
</div>
|
|
397
|
-
{(association
|
|
398
|
-
<div className={styles.relationshipInfo}>
|
|
399
|
-
{association[relationshipField] && (
|
|
400
|
-
<span className={styles.relationshipType}>
|
|
401
|
-
{association[relationshipField]}
|
|
402
|
-
</span>
|
|
403
|
-
)}
|
|
404
|
-
{association[notesField] && (
|
|
405
|
-
<span className={styles.notes}>
|
|
406
|
-
{association[relationshipField] ? ' - ' : ''}{association[notesField]}
|
|
407
|
-
</span>
|
|
408
|
-
)}
|
|
409
|
-
</div>
|
|
410
|
-
)}
|
|
411
|
-
{dateField && association[dateField] && (
|
|
412
|
-
<div className={styles.associatedAt}>
|
|
413
|
-
<Calendar size={12} style={{ marginRight: '4px' }} />
|
|
414
|
-
{config.labels?.associatedAt || 'Associated'}: {new Date(association[dateField]).toLocaleDateString()}
|
|
415
|
-
</div>
|
|
416
|
-
)}
|
|
629
|
+
{renderMeta(association)}
|
|
417
630
|
</div>
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
{!
|
|
631
|
+
|
|
632
|
+
<div className={styles.actions}>
|
|
633
|
+
{!isPrimary && endpoints.setPrimary && (
|
|
421
634
|
<button
|
|
422
635
|
type="button"
|
|
423
|
-
className={styles.
|
|
636
|
+
className={styles.iconButton}
|
|
424
637
|
onClick={() => handleSetPrimary(association)}
|
|
638
|
+
title={
|
|
639
|
+
config.labels?.setPrimary || 'Set Primary'
|
|
640
|
+
}
|
|
641
|
+
aria-label={
|
|
642
|
+
config.labels?.setPrimary || 'Set Primary'
|
|
643
|
+
}
|
|
425
644
|
>
|
|
426
|
-
<Star size={
|
|
427
|
-
{config.labels?.setPrimary || 'Set Primary'}
|
|
645
|
+
<Star size={15} aria-hidden="true" />
|
|
428
646
|
</button>
|
|
429
647
|
)}
|
|
430
|
-
{
|
|
648
|
+
{isPrimary && endpoints.removePrimary && (
|
|
431
649
|
<button
|
|
432
650
|
type="button"
|
|
433
|
-
className={styles.
|
|
651
|
+
className={`${styles.iconButton} ${styles.iconButtonOn}`}
|
|
434
652
|
onClick={() => handleRemovePrimary(association)}
|
|
435
|
-
title={
|
|
653
|
+
title={
|
|
654
|
+
config.labels?.removePrimaryTooltip ||
|
|
655
|
+
config.labels?.removePrimary ||
|
|
656
|
+
'Remove primary status from this association'
|
|
657
|
+
}
|
|
658
|
+
aria-label={
|
|
659
|
+
config.labels?.removePrimary ||
|
|
660
|
+
'Remove Primary'
|
|
661
|
+
}
|
|
436
662
|
>
|
|
437
|
-
<
|
|
438
|
-
{config.labels?.removePrimary || 'Remove Primary'}
|
|
663
|
+
<StarOff size={15} aria-hidden="true" />
|
|
439
664
|
</button>
|
|
440
665
|
)}
|
|
441
666
|
<button
|
|
442
667
|
type="button"
|
|
443
|
-
className={styles.
|
|
444
|
-
onClick={() =>
|
|
668
|
+
className={`${styles.iconButton} ${styles.iconButtonDanger}`}
|
|
669
|
+
onClick={() => confirmDissociate(association)}
|
|
670
|
+
title={config.labels?.remove || 'Remove'}
|
|
671
|
+
aria-label={config.labels?.remove || 'Remove'}
|
|
445
672
|
>
|
|
446
|
-
<Trash2 size={
|
|
447
|
-
{config.labels?.remove || 'Remove'}
|
|
673
|
+
<Trash2 size={15} aria-hidden="true" />
|
|
448
674
|
</button>
|
|
449
675
|
</div>
|
|
450
676
|
</div>
|
|
@@ -454,96 +680,37 @@ const AssociationManager = ({
|
|
|
454
680
|
)}
|
|
455
681
|
</div>
|
|
456
682
|
|
|
457
|
-
{
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return (
|
|
465
|
-
<div key={field.id} className={styles.formRow}>
|
|
466
|
-
<div className={styles.formField}>
|
|
467
|
-
<label>
|
|
468
|
-
{field.label}{field.required && ' *'}
|
|
469
|
-
</label>
|
|
470
|
-
|
|
471
|
-
{field.type === 'multi-select' && (
|
|
472
|
-
<MultiSelect
|
|
473
|
-
settings={{ id: field.id }}
|
|
474
|
-
multi={field.multi || false}
|
|
475
|
-
onChange={(value) => handleFieldChange(field.id, value, field)}
|
|
476
|
-
inputValue={fieldValue}
|
|
477
|
-
options={field.id === 'customer_id' ? availableOptions : (childOptions[field.id] || [])}
|
|
478
|
-
placeholder={field.placeholder || `Select ${field.label}...`}
|
|
479
|
-
/>
|
|
480
|
-
)}
|
|
481
|
-
|
|
482
|
-
{field.type === 'select' && (
|
|
483
|
-
<select
|
|
484
|
-
value={fieldValue}
|
|
485
|
-
onChange={(e) => handleFieldChange(field.id, e.target.value, field)}
|
|
486
|
-
className={styles.selectField}
|
|
487
|
-
>
|
|
488
|
-
<option value="">Select {field.label}...</option>
|
|
489
|
-
{(field.options || childOptions[field.id] || []).map(option => (
|
|
490
|
-
<option key={option.value || option} value={option.value || option}>
|
|
491
|
-
{option.label || option}
|
|
492
|
-
</option>
|
|
493
|
-
))}
|
|
494
|
-
</select>
|
|
495
|
-
)}
|
|
496
|
-
|
|
497
|
-
{field.type === 'boolean' && (
|
|
498
|
-
<label>
|
|
499
|
-
<input
|
|
500
|
-
type="checkbox"
|
|
501
|
-
checked={fieldValue}
|
|
502
|
-
onChange={(e) => handleFieldChange(field.id, e.target.checked, field)}
|
|
503
|
-
/>
|
|
504
|
-
{field.checkboxLabel || field.label}
|
|
505
|
-
</label>
|
|
506
|
-
)}
|
|
507
|
-
|
|
508
|
-
{field.type === 'textarea' && (
|
|
509
|
-
<textarea
|
|
510
|
-
value={fieldValue}
|
|
511
|
-
onChange={(e) => handleFieldChange(field.id, e.target.value, field)}
|
|
512
|
-
placeholder={field.placeholder || ''}
|
|
513
|
-
className={styles.textareaField}
|
|
514
|
-
rows={field.rows || 3}
|
|
515
|
-
/>
|
|
516
|
-
)}
|
|
517
|
-
|
|
518
|
-
{field.type === 'text' && (
|
|
519
|
-
<input
|
|
520
|
-
type="text"
|
|
521
|
-
value={fieldValue}
|
|
522
|
-
onChange={(e) => handleFieldChange(field.id, e.target.value, field)}
|
|
523
|
-
placeholder={field.placeholder || ''}
|
|
524
|
-
className={styles.textField}
|
|
525
|
-
/>
|
|
526
|
-
)}
|
|
527
|
-
</div>
|
|
528
|
-
</div>
|
|
529
|
-
);
|
|
530
|
-
})}
|
|
683
|
+
{showForm && (
|
|
684
|
+
<div className={styles.section}>
|
|
685
|
+
<h4 className={styles.sectionTitle}>
|
|
686
|
+
{config.labels?.addNew || 'Add New Association'}
|
|
687
|
+
</h4>
|
|
688
|
+
<div className={styles.form}>
|
|
689
|
+
{config.fields && config.fields.map(renderField)}
|
|
531
690
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
691
|
+
<div className={styles.formActions}>
|
|
692
|
+
<button
|
|
693
|
+
type="button"
|
|
694
|
+
className={styles.secondaryButton}
|
|
695
|
+
onClick={() => setShowForm(false)}
|
|
696
|
+
>
|
|
697
|
+
{config.labels?.cancel || 'Cancel'}
|
|
698
|
+
</button>
|
|
699
|
+
<button
|
|
700
|
+
type="button"
|
|
701
|
+
className={styles.primaryButton}
|
|
702
|
+
onClick={handleAssociate}
|
|
703
|
+
disabled={!newAssociation[requiredFieldId]}
|
|
704
|
+
>
|
|
705
|
+
<Plus size={14} aria-hidden="true" />
|
|
706
|
+
{config.labels?.addButton || 'Add Association'}
|
|
707
|
+
</button>
|
|
708
|
+
</div>
|
|
542
709
|
</div>
|
|
543
710
|
</div>
|
|
544
|
-
|
|
711
|
+
)}
|
|
545
712
|
</div>
|
|
546
713
|
);
|
|
547
714
|
};
|
|
548
715
|
|
|
549
|
-
export default AssociationManager;
|
|
716
|
+
export default AssociationManager;
|
|
@@ -223,12 +223,12 @@ function VisnsAsyncSelect({
|
|
|
223
223
|
minHeight:
|
|
224
224
|
style && style.multi_select_height
|
|
225
225
|
? style.multi_select_height
|
|
226
|
-
: '
|
|
226
|
+
: 'var(--field-height, 51px)',
|
|
227
227
|
height: 'auto', // Allow height to grow with content
|
|
228
228
|
}),
|
|
229
229
|
valueContainer: (base) => ({
|
|
230
230
|
...base,
|
|
231
|
-
padding: '
|
|
231
|
+
padding: '0 1rem',
|
|
232
232
|
display: 'flex',
|
|
233
233
|
alignItems: 'flex-start',
|
|
234
234
|
flexWrap: 'wrap',
|