@visns-studio/visns-components 5.11.4 → 5.11.6

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 CHANGED
@@ -13,6 +13,7 @@
13
13
  "@nivo/pie": "^0.99.0",
14
14
  "@tinymce/miniature": "^6.0.0",
15
15
  "@tinymce/tinymce-react": "^6.2.1",
16
+ "@uiw/react-color": "^2.6.0",
16
17
  "@visns-studio/visns-datagrid-community": "^1.0.14",
17
18
  "@visns-studio/visns-datagrid-enterprise": "^1.0.14",
18
19
  "@vitejs/plugin-react": "^4.5.2",
@@ -27,15 +28,14 @@
27
28
  "html-react-parser": "^5.2.5",
28
29
  "lodash": "^4.17.21",
29
30
  "lodash.debounce": "^4.0.8",
30
- "lucide-react": "^0.516.0",
31
+ "lucide-react": "^0.518.0",
31
32
  "moment": "^2.30.1",
32
33
  "motion": "^12.18.1",
33
34
  "numeral": "^2.0.6",
34
35
  "pluralize": "^8.0.0",
35
36
  "qrcode.react": "^4.2.0",
36
37
  "quill-image-uploader": "^1.3.0",
37
- "react-big-calendar": "^1.19.3",
38
- "react-color": "^2.19.3",
38
+ "react-big-calendar": "^1.19.4",
39
39
  "react-copy-to-clipboard": "^5.1.0",
40
40
  "react-datepicker": "^8.4.0",
41
41
  "react-dropzone": "^14.3.8",
@@ -87,7 +87,7 @@
87
87
  "react-dom": "^17.0.0 || ^18.0.0"
88
88
  },
89
89
  "name": "@visns-studio/visns-components",
90
- "version": "5.11.4",
90
+ "version": "5.11.6",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -108,5 +108,5 @@
108
108
  "url": "https://github.com/visnsstudio/visns-components/issues"
109
109
  },
110
110
  "homepage": "https://github.com/visnsstudio/visns-components#readme",
111
- "packageManager": "yarn@4.9.1"
111
+ "packageManager": "yarn@4.9.2"
112
112
  }
@@ -13,6 +13,7 @@ const AssociationManager = ({
13
13
  }) => {
14
14
  const [associations, setAssociations] = useState([]);
15
15
  const [availableOptions, setAvailableOptions] = useState([]);
16
+ const [childOptions, setChildOptions] = useState({});
16
17
  const [loading, setLoading] = useState(true);
17
18
 
18
19
  // Dynamic state based on config
@@ -49,23 +50,42 @@ const AssociationManager = ({
49
50
 
50
51
  // Load existing associations
51
52
  const loadAssociations = async () => {
52
- if (!endpoints.list) return;
53
+ console.log('🔍 AssociationManager - loadAssociations called');
54
+ console.log('📡 Endpoints:', endpoints);
55
+ console.log('⚙️ Config:', config);
56
+ console.log('🆔 DataId:', dataId);
57
+
58
+ if (!endpoints.list) {
59
+ console.error('❌ No endpoints.list defined');
60
+ return;
61
+ }
53
62
 
54
63
  try {
55
64
  setLoading(true);
56
65
  const url = processTemplate(endpoints.list);
66
+ console.log('🌐 Loading associations from URL:', url);
67
+
57
68
  const method = endpoints.listMethod || 'GET';
69
+ console.log('📤 HTTP Method:', method);
70
+
58
71
  const response = await CustomFetch(url, method, endpoints.listParams || {});
72
+ console.log('📥 Raw API Response:', response);
59
73
 
60
74
  const dataPath = config.dataPath || 'data';
75
+ console.log('🗂️ Using dataPath:', dataPath);
76
+
61
77
  const associationsData = dataPath.split('.').reduce((obj, key) => obj?.[key], response);
78
+ console.log('📊 Extracted associations data:', associationsData);
62
79
 
63
80
  // Ensure associationsData is always an array
64
81
  const validAssociations = Array.isArray(associationsData) ? associationsData : [];
82
+ console.log('✅ Valid associations (final):', validAssociations);
83
+ console.log('📈 Associations count:', validAssociations.length);
84
+
65
85
  setAssociations(validAssociations);
66
86
  } catch (error) {
67
87
  toast.error(config.messages?.loadError || 'Failed to load associations');
68
- console.error('Error loading associations:', error);
88
+ console.error('Error loading associations:', error);
69
89
  setAssociations([]); // Set empty array on error
70
90
  } finally {
71
91
  setLoading(false);
@@ -74,20 +94,102 @@ const AssociationManager = ({
74
94
 
75
95
  // Load available options for dropdown/select fields
76
96
  const loadAvailableOptions = async () => {
77
- if (!config.optionsEndpoint) return;
97
+ console.log('🔍 AssociationManager - loadAvailableOptions called');
98
+ console.log('⚙️ Config.optionsEndpoint:', config.optionsEndpoint);
99
+
100
+ if (!config.optionsEndpoint) {
101
+ console.error('❌ No config.optionsEndpoint defined');
102
+ return;
103
+ }
78
104
 
79
105
  try {
80
106
  const url = processTemplate(config.optionsEndpoint.url);
107
+ console.log('🌐 Loading options from URL:', url);
108
+
81
109
  const method = config.optionsEndpoint.method || 'POST';
110
+ console.log('📤 HTTP Method:', method);
111
+
82
112
  const params = config.optionsEndpoint.params || {};
113
+ console.log('📋 Request params:', params);
83
114
 
84
115
  const response = await CustomFetch(url, method, params);
116
+ console.log('📥 Raw options API Response:', response);
117
+
85
118
  const dataPath = config.optionsEndpoint.dataPath || 'data';
119
+ console.log('🗂️ Using options dataPath:', dataPath);
120
+
86
121
  const optionsData = dataPath.split('.').reduce((obj, key) => obj?.[key], response) || [];
122
+ console.log('📊 Extracted options data:', optionsData);
123
+ console.log('📈 Options count:', optionsData.length);
87
124
 
88
125
  setAvailableOptions(optionsData);
89
126
  } catch (error) {
90
- console.error('Failed to load options:', error);
127
+ console.error('Failed to load options:', error);
128
+ }
129
+ };
130
+
131
+ // Load child dropdown options based on parent selection
132
+ const loadChildOptions = async (parentFieldId, parentValue, childConfig) => {
133
+ if (!parentValue || !childConfig.url) return [];
134
+
135
+ try {
136
+ console.log(`🔍 Loading child options for ${childConfig.id} based on ${parentFieldId}:`, parentValue);
137
+
138
+ const filter = {
139
+ where: [
140
+ {
141
+ id: parentFieldId,
142
+ value: parentValue,
143
+ ...(childConfig.whereHas && { whereHas: childConfig.whereHas })
144
+ }
145
+ ]
146
+ };
147
+
148
+ if (childConfig.fields) {
149
+ filter.fields = childConfig.fields;
150
+ }
151
+
152
+ console.log('📋 Child filter params:', filter);
153
+
154
+ const response = await CustomFetch(childConfig.url, 'POST', filter);
155
+ console.log('📥 Child options response:', response);
156
+
157
+ // Extract child options - response structure is {data: {data: [...]}}
158
+ const childData = response.data?.data || response.data || [];
159
+ console.log('📊 Extracted child options:', childData);
160
+
161
+ return childData;
162
+ } catch (error) {
163
+ console.error(`❌ Failed to load child options for ${childConfig.id}:`, error);
164
+ return [];
165
+ }
166
+ };
167
+
168
+ // Handle parent field changes and load child options
169
+ const handleFieldChange = async (fieldId, value, field) => {
170
+ // Update the field value
171
+ setNewAssociation(prev => ({ ...prev, [fieldId]: value }));
172
+
173
+ // Load child options if this field has children
174
+ if (field.child && Array.isArray(field.child)) {
175
+ console.log(`🔗 Parent field ${fieldId} changed, loading child options...`);
176
+
177
+ const newChildOptions = { ...childOptions };
178
+
179
+ for (const childConfig of field.child) {
180
+ const childFieldOptions = await loadChildOptions(fieldId, value?.value || value, childConfig);
181
+ newChildOptions[childConfig.id] = childFieldOptions;
182
+ console.log(`✅ Loaded ${childFieldOptions.length} options for ${childConfig.id}`);
183
+ }
184
+
185
+ setChildOptions(newChildOptions);
186
+
187
+ // Clear child field values when parent changes
188
+ const updatedAssociation = { ...newAssociation, [fieldId]: value };
189
+ field.child.forEach(childConfig => {
190
+ updatedAssociation[childConfig.id] = '';
191
+ });
192
+ setNewAssociation(updatedAssociation);
91
193
  }
92
194
  };
93
195
 
@@ -164,21 +266,29 @@ const AssociationManager = ({
164
266
  };
165
267
 
166
268
  // Set primary handler
167
- const handleSetPrimary = async (itemId) => {
269
+ const handleSetPrimary = async (association) => {
168
270
  if (!endpoints.setPrimary) return;
169
271
 
170
272
  const idField = config.primaryTargetField || config.deleteField || 'id';
273
+ const targetValue = association[idField] || association.customer_id || association.id;
171
274
 
172
- await handleAction('setPrimary', { [idField]: itemId });
275
+ console.log('🌟 Setting primary for association:', association);
276
+ console.log('🎯 Using field:', idField, 'with value:', targetValue);
277
+
278
+ await handleAction('setPrimary', { [idField]: targetValue });
173
279
  };
174
280
 
175
281
  // Remove primary handler
176
- const handleRemovePrimary = async (itemId) => {
282
+ const handleRemovePrimary = async (association) => {
177
283
  if (!endpoints.removePrimary) return;
178
284
 
179
285
  const idField = config.primaryTargetField || config.deleteField || 'id';
286
+ const targetValue = association[idField] || association.customer_id || association.id;
287
+
288
+ console.log('🌟 Removing primary for association:', association);
289
+ console.log('🎯 Using field:', idField, 'with value:', targetValue);
180
290
 
181
- await handleAction('removePrimary', { [idField]: itemId });
291
+ await handleAction('removePrimary', { [idField]: targetValue });
182
292
  };
183
293
 
184
294
  // Clear all primary statuses (useful after merge)
@@ -195,9 +305,17 @@ const AssociationManager = ({
195
305
  };
196
306
 
197
307
  useEffect(() => {
308
+ console.log('🚀 AssociationManager - Component initialized');
309
+ console.log('🆔 DataId:', dataId);
310
+ console.log('📡 Endpoints:', endpoints);
311
+ console.log('⚙️ Config:', config);
312
+
198
313
  if (dataId) {
314
+ console.log('✅ DataId exists, loading data...');
199
315
  loadAssociations();
200
316
  loadAvailableOptions();
317
+ } else {
318
+ console.warn('⚠️ No dataId provided, skipping data load');
201
319
  }
202
320
  }, [dataId]);
203
321
 
@@ -245,21 +363,31 @@ const AssociationManager = ({
245
363
  ) : (
246
364
  <div className={styles.associationsList}>
247
365
  {associations.map((association) => {
366
+ console.log('🔍 Rendering association:', association);
367
+
248
368
  const nameField = config.displayFields?.name || 'name';
249
369
  const primaryField = config.displayFields?.primary || 'is_primary';
250
370
  const relationshipField = config.displayFields?.relationship || 'relationship_type';
251
371
  const notesField = config.displayFields?.notes || 'notes';
252
372
  const dateField = config.displayFields?.date || 'associated_at';
253
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
+
254
382
  return (
255
383
  <div
256
384
  key={association.id}
257
- className={`${styles.associationItem} ${association[primaryField] ? styles.primary : ''}`}
385
+ className={`${styles.associationItem} ${Boolean(association[primaryField]) ? styles.primary : ''}`}
258
386
  >
259
387
  <div className={styles.associationInfo}>
260
388
  <div className={styles.entityName}>
261
389
  {association[nameField]}
262
- {association[primaryField] && (
390
+ {Boolean(association[primaryField]) && (
263
391
  <span className={styles.primaryBadge}>
264
392
  <Star size={12} style={{ marginRight: '4px' }} />
265
393
  {config.labels?.primaryBadge || 'Primary'}
@@ -288,21 +416,22 @@ const AssociationManager = ({
288
416
  )}
289
417
  </div>
290
418
  <div className={styles.associationActions}>
291
- {!association[primaryField] && endpoints.setPrimary && (
419
+ {console.log('🎯 Actions for:', association[nameField], 'Primary:', association[primaryField], 'Boolean:', Boolean(association[primaryField]))}
420
+ {!Boolean(association[primaryField]) && endpoints.setPrimary && (
292
421
  <button
293
422
  type="button"
294
423
  className={styles.setPrimaryBtn}
295
- onClick={() => handleSetPrimary(association.id)}
424
+ onClick={() => handleSetPrimary(association)}
296
425
  >
297
426
  <Star size={14} style={{ marginRight: '4px' }} />
298
427
  {config.labels?.setPrimary || 'Set Primary'}
299
428
  </button>
300
429
  )}
301
- {association[primaryField] && endpoints.removePrimary && (
430
+ {Boolean(association[primaryField]) && endpoints.removePrimary && (
302
431
  <button
303
432
  type="button"
304
433
  className={styles.removePrimaryBtn}
305
- onClick={() => handleRemovePrimary(association.id)}
434
+ onClick={() => handleRemovePrimary(association)}
306
435
  title={config.labels?.removePrimaryTooltip || 'Remove primary status from this association'}
307
436
  >
308
437
  <X size={14} style={{ marginRight: '4px' }} />
@@ -343,9 +472,9 @@ const AssociationManager = ({
343
472
  <MultiSelect
344
473
  settings={{ id: field.id }}
345
474
  multi={field.multi || false}
346
- onChange={(value) => setNewAssociation(prev => ({ ...prev, [field.id]: value }))}
475
+ onChange={(value) => handleFieldChange(field.id, value, field)}
347
476
  inputValue={fieldValue}
348
- options={availableOptions}
477
+ options={field.id === 'customer_id' ? availableOptions : (childOptions[field.id] || [])}
349
478
  placeholder={field.placeholder || `Select ${field.label}...`}
350
479
  />
351
480
  )}
@@ -353,13 +482,11 @@ const AssociationManager = ({
353
482
  {field.type === 'select' && (
354
483
  <select
355
484
  value={fieldValue}
356
- onChange={(e) => setNewAssociation(prev => ({
357
- ...prev,
358
- [field.id]: e.target.value
359
- }))}
485
+ onChange={(e) => handleFieldChange(field.id, e.target.value, field)}
360
486
  className={styles.selectField}
361
487
  >
362
- {field.options && field.options.map(option => (
488
+ <option value="">Select {field.label}...</option>
489
+ {(field.options || childOptions[field.id] || []).map(option => (
363
490
  <option key={option.value || option} value={option.value || option}>
364
491
  {option.label || option}
365
492
  </option>
@@ -372,10 +499,7 @@ const AssociationManager = ({
372
499
  <input
373
500
  type="checkbox"
374
501
  checked={fieldValue}
375
- onChange={(e) => setNewAssociation(prev => ({
376
- ...prev,
377
- [field.id]: e.target.checked
378
- }))}
502
+ onChange={(e) => handleFieldChange(field.id, e.target.checked, field)}
379
503
  />
380
504
  {field.checkboxLabel || field.label}
381
505
  </label>
@@ -384,10 +508,7 @@ const AssociationManager = ({
384
508
  {field.type === 'textarea' && (
385
509
  <textarea
386
510
  value={fieldValue}
387
- onChange={(e) => setNewAssociation(prev => ({
388
- ...prev,
389
- [field.id]: e.target.value
390
- }))}
511
+ onChange={(e) => handleFieldChange(field.id, e.target.value, field)}
391
512
  placeholder={field.placeholder || ''}
392
513
  className={styles.textareaField}
393
514
  rows={field.rows || 3}
@@ -398,10 +519,7 @@ const AssociationManager = ({
398
519
  <input
399
520
  type="text"
400
521
  value={fieldValue}
401
- onChange={(e) => setNewAssociation(prev => ({
402
- ...prev,
403
- [field.id]: e.target.value
404
- }))}
522
+ onChange={(e) => handleFieldChange(field.id, e.target.value, field)}
405
523
  placeholder={field.placeholder || ''}
406
524
  className={styles.textField}
407
525
  />
@@ -7,7 +7,7 @@ import Toggle from 'react-toggle';
7
7
  import moment from 'moment';
8
8
  import parse from 'html-react-parser';
9
9
  import Popup from 'reactjs-popup';
10
- import { BlockPicker } from 'react-color';
10
+ import { Sketch } from '@uiw/react-color';
11
11
  import { CopyToClipboard } from 'react-copy-to-clipboard';
12
12
  import { NumericFormat, PatternFormat } from 'react-number-format';
13
13
  import { Editor } from '@tinymce/tinymce-react';
@@ -110,12 +110,13 @@ function Field({
110
110
  right: '0px',
111
111
  bottom: '0px',
112
112
  left: '0px',
113
+ zIndex: '999998',
113
114
  };
114
115
  const popover = {
115
116
  position: 'absolute',
116
- zIndex: '2',
117
- left: '20px',
118
- marginTop: '6px',
117
+ zIndex: '999999',
118
+ left: '0',
119
+ marginTop: '8px',
119
120
  };
120
121
 
121
122
  useEffect(() => {
@@ -157,6 +158,33 @@ function Field({
157
158
  fetchAutoValue();
158
159
  }, [settings.url, settings.type, settings.id, settings.autoValueKey]);
159
160
 
161
+ // Handle profileId parameter - auto-populate field value with userProfile.id
162
+ useEffect(() => {
163
+ if (
164
+ settings.param === 'profileId' &&
165
+ (!settings.value || settings.value === '') &&
166
+ userProfile?.id &&
167
+ (!inputValue || inputValue === '')
168
+ ) {
169
+ // Simulate an onChange event to update the form data with userProfile.id
170
+ const simulatedEvent = {
171
+ target: {
172
+ dataset: { name: settings.id },
173
+ value: userProfile.id,
174
+ },
175
+ };
176
+ onChange(simulatedEvent);
177
+
178
+ // Also directly update the form data to ensure it's synchronized
179
+ if (setFormData) {
180
+ setFormData((prevState) => ({
181
+ ...prevState,
182
+ [settings.id]: userProfile.id,
183
+ }));
184
+ }
185
+ }
186
+ }, [settings.param, settings.value, settings.id, userProfile?.id, inputValue, onChange, setFormData]);
187
+
160
188
  useEffect(() => {
161
189
  const fetchOptions = () => {
162
190
  let filter = {};
@@ -429,6 +457,12 @@ function Field({
429
457
  }
430
458
  };
431
459
 
460
+ // Utility function to detect if a string is a valid hex color
461
+ const isHexColor = (str) => {
462
+ if (!str || typeof str !== 'string') return false;
463
+ return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(str);
464
+ };
465
+
432
466
  const renderRelationText = (value) => {
433
467
  let text = '';
434
468
 
@@ -446,23 +480,56 @@ function Field({
446
480
  text = processKeys(value);
447
481
  }
448
482
 
483
+ // If the text is a hex color, render it with a color preview
484
+ if (isHexColor(text)) {
485
+ return (
486
+ <div className="relation-color-container">
487
+ <div
488
+ className="relation-color-preview"
489
+ style={{ backgroundColor: text }}
490
+ title={`Color: ${text}`}
491
+ ></div>
492
+ <span className="relation-color-text">{text}</span>
493
+ </div>
494
+ );
495
+ }
496
+
449
497
  return text;
450
498
  };
451
499
 
452
500
  const renderRelationArrayText = (value) => {
453
- let text = '';
454
-
455
501
  if (value && settings.nameFrom) {
456
- value.forEach((a, b) => {
457
- if (b > 0) {
458
- text += ', ';
459
- }
460
-
461
- text += a[settings.nameFrom];
462
- });
502
+ return (
503
+ <div className="relation-array-container">
504
+ {value.map((item, index) => {
505
+ const itemValue = item[settings.nameFrom];
506
+
507
+ if (isHexColor(itemValue)) {
508
+ return (
509
+ <div key={index} className="relation-color-container inline">
510
+ <div
511
+ className="relation-color-preview"
512
+ style={{ backgroundColor: itemValue }}
513
+ title={`Color: ${itemValue}`}
514
+ ></div>
515
+ <span className="relation-color-text">{itemValue}</span>
516
+ {index < value.length - 1 && <span className="separator">, </span>}
517
+ </div>
518
+ );
519
+ }
520
+
521
+ return (
522
+ <span key={index}>
523
+ {itemValue}
524
+ {index < value.length - 1 && ', '}
525
+ </span>
526
+ );
527
+ })}
528
+ </div>
529
+ );
463
530
  }
464
531
 
465
- return text;
532
+ return '';
466
533
  };
467
534
 
468
535
  const renderInput = () => {
@@ -597,52 +664,41 @@ function Field({
597
664
  );
598
665
  case 'colour':
599
666
  return (
600
- <div className="cpicker">
601
- <div
602
- className="cpicker__box"
603
- style={
604
- inputValue && inputValue !== 'null'
605
- ? { background: inputValue }
606
- : {}
607
- }
608
- ></div>
609
- <div className="cpicker__btn">
610
- <button
611
- onClick={(e) => {
612
- e.preventDefault();
613
-
614
- setColorPickerShow(
615
- colorPickerShow === true ? false : true
667
+ <div className="cpicker-inline">
668
+ <div className="cpicker-preview-container">
669
+ <div
670
+ className="cpicker__box"
671
+ data-color={inputValue && inputValue !== 'null' ? inputValue : 'No colour selected'}
672
+ style={
673
+ inputValue && inputValue !== 'null'
674
+ ? { backgroundColor: inputValue }
675
+ : {}
676
+ }
677
+ title={inputValue && inputValue !== 'null' ? `Current colour: ${inputValue}` : 'No colour selected'}
678
+ ></div>
679
+ <span className="cpicker-value">
680
+ {inputValue && inputValue !== 'null' ? inputValue : 'No colour selected'}
681
+ </span>
682
+ </div>
683
+ <div className="cpicker-container">
684
+ <Sketch
685
+ color={
686
+ inputValue && inputValue !== 'null'
687
+ ? inputValue
688
+ : '#000000'
689
+ }
690
+ onChange={(color) => {
691
+ onChangeColour(
692
+ { target: { value: color.hex } },
693
+ { hex: color.hex },
694
+ settings.id
616
695
  );
617
696
  }}
618
- >
619
- Pick Color
620
- </button>
621
- {colorPickerShow === true ? (
622
- <div style={popover}>
623
- <div
624
- style={cover}
625
- onClick={() => {
626
- setColorPickerShow(false);
627
- }}
628
- />
629
-
630
- <BlockPicker
631
- color={
632
- inputValue && inputValue !== 'null'
633
- ? inputValue
634
- : ''
635
- }
636
- onChangeComplete={(color, event) => {
637
- onChangeColour(
638
- event,
639
- color,
640
- settings.id
641
- );
642
- }}
643
- />
644
- </div>
645
- ) : null}
697
+ style={{
698
+ boxShadow: 'none',
699
+ border: 'none'
700
+ }}
701
+ />
646
702
  </div>
647
703
  </div>
648
704
  );
@@ -1954,13 +2010,12 @@ function Field({
1954
2010
  'media',
1955
2011
  'table',
1956
2012
  'code',
1957
- 'hr',
1958
2013
  ],
1959
2014
  toolbar:
1960
2015
  'insertTextDropdown | undo redo | blocks | ' +
1961
2016
  'bold italic forecolor | alignleft aligncenter ' +
1962
2017
  'alignright alignjustify | bullist numlist outdent indent | ' +
1963
- 'hr | removeformat | table | code',
2018
+ 'removeformat | table | code',
1964
2019
  content_style:
1965
2020
  'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
1966
2021
  setup: (editor) => {
@@ -767,6 +767,25 @@ export const renderRelationColumn = ({
767
767
  typeof value === 'string' ||
768
768
  typeof value === 'number'
769
769
  ) {
770
+ // Check if the value is a hex color and render with color preview
771
+ const isHexColor = (str) => {
772
+ if (!str || typeof str !== 'string') return false;
773
+ return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(str);
774
+ };
775
+
776
+ if (isHexColor(value)) {
777
+ return (
778
+ <div className="relation-color-container">
779
+ <div
780
+ className="relation-color-preview"
781
+ style={{ backgroundColor: value }}
782
+ title={`Color: ${value}`}
783
+ ></div>
784
+ <span className="relation-color-text">{value}</span>
785
+ </div>
786
+ );
787
+ }
788
+
770
789
  return (
771
790
  <CellWithTooltip
772
791
  value={value}