@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
@@ -1,21 +1,26 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { toast } from 'react-toastify';
3
- import { Star, Trash2, Plus, Users, Calendar, AlertTriangle, X } from 'lucide-react';
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 <div className={styles.loading}>{config.messages?.loading || 'Loading associations...'}</div>;
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
- <div className={styles.header}>
329
- <h3>
330
- <Users size={20} style={{ marginRight: '8px', verticalAlign: 'middle' }} />
331
- {config.title || 'Associations'}
332
- </h3>
333
- <p>{config.description || 'Manage associations'}</p>
334
- </div>
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.multiplePrimaryWarning}>
339
- <AlertTriangle size={16} style={{ marginRight: '8px' }} />
340
- <div className={styles.warningContent}>
341
- <strong>{config.messages?.multiplePrimaryWarning || 'Multiple Primary Associations Detected'}</strong>
342
- <p>{config.messages?.multiplePrimaryDescription || 'You have multiple associations marked as primary. Please resolve this by keeping only one primary association.'}</p>
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.clearAllPrimaryBtn}
571
+ className={styles.noticeAction}
347
572
  onClick={handleClearAllPrimary}
348
573
  >
349
- {config.labels?.clearAllPrimary || 'Clear All Primary Status'}
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
- {/* Existing Associations */}
357
- <div className={styles.existingAssociations}>
358
- <h4>{config.labels?.currentAssociations || 'Current Associations'}</h4>
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
- <div className={styles.noAssociations}>
600
+ <p className={styles.empty}>
361
601
  {config.messages?.noAssociations || 'No associations found.'}
362
- </div>
602
+ </p>
363
603
  ) : (
364
- <div className={styles.associationsList}>
604
+ <div className={styles.list}>
365
605
  {associations.map((association) => {
366
- console.log('🔍 Rendering association:', association);
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.associationItem} ${Boolean(association[primaryField]) ? styles.primary : ''}`}
609
+ <div
610
+ key={association.id}
611
+ className={`${styles.row} ${isPrimary ? styles.rowPrimary : ''}`}
386
612
  >
387
- <div className={styles.associationInfo}>
613
+ <div className={styles.main}>
388
614
  <div className={styles.entityName}>
389
- {association[nameField]}
390
- {Boolean(association[primaryField]) && (
391
- <span className={styles.primaryBadge}>
392
- <Star size={12} style={{ marginRight: '4px' }} />
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[relationshipField] || association[notesField]) && (
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
- <div className={styles.associationActions}>
419
- {console.log('🎯 Actions for:', association[nameField], 'Primary:', association[primaryField], 'Boolean:', Boolean(association[primaryField]))}
420
- {!Boolean(association[primaryField]) && endpoints.setPrimary && (
631
+
632
+ <div className={styles.actions}>
633
+ {!isPrimary && endpoints.setPrimary && (
421
634
  <button
422
635
  type="button"
423
- className={styles.setPrimaryBtn}
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={14} style={{ marginRight: '4px' }} />
427
- {config.labels?.setPrimary || 'Set Primary'}
645
+ <Star size={15} aria-hidden="true" />
428
646
  </button>
429
647
  )}
430
- {Boolean(association[primaryField]) && endpoints.removePrimary && (
648
+ {isPrimary && endpoints.removePrimary && (
431
649
  <button
432
650
  type="button"
433
- className={styles.removePrimaryBtn}
651
+ className={`${styles.iconButton} ${styles.iconButtonOn}`}
434
652
  onClick={() => handleRemovePrimary(association)}
435
- title={config.labels?.removePrimaryTooltip || 'Remove primary status from this association'}
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
- <X size={14} style={{ marginRight: '4px' }} />
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.removeBtn}
444
- onClick={() => handleDissociate(association.id)}
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={14} style={{ marginRight: '4px' }} />
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
- {/* Add New Association */}
458
- <div className={styles.newAssociation}>
459
- <h4>{config.labels?.addNew || 'Add New Association'}</h4>
460
- <div className={styles.associationForm}>
461
- {config.fields && config.fields.map((field, index) => {
462
- const fieldValue = newAssociation[field.id] || '';
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
- <div className={styles.formActions}>
533
- <button
534
- type="button"
535
- className={styles.associateBtn}
536
- onClick={handleAssociate}
537
- disabled={!newAssociation[config.fields?.find(f => f.required)?.id || 'id']}
538
- >
539
- <Plus size={14} style={{ marginRight: '4px' }} />
540
- {config.labels?.addButton || 'Add Association'}
541
- </button>
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
- </div>
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
- : '50px',
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: '2px 8px',
231
+ padding: '0 1rem',
232
232
  display: 'flex',
233
233
  alignItems: 'flex-start',
234
234
  flexWrap: 'wrap',