@visns-studio/visns-components 5.21.0 → 5.22.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
CHANGED
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
95
95
|
},
|
|
96
96
|
"name": "@visns-studio/visns-components",
|
|
97
|
-
"version": "5.
|
|
97
|
+
"version": "5.22.0",
|
|
98
98
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
99
99
|
"main": "src/index.js",
|
|
100
100
|
"files": [
|
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
renderArrayCountColumn,
|
|
96
96
|
renderColourColumn,
|
|
97
97
|
renderDropdownColumn,
|
|
98
|
+
renderMultiDropdownColumn,
|
|
98
99
|
renderImageColumn,
|
|
99
100
|
renderFileColumn,
|
|
100
101
|
renderIconsColumn,
|
|
@@ -2762,7 +2763,7 @@ const DataGrid = forwardRef(
|
|
|
2762
2763
|
500
|
|
2763
2764
|
);
|
|
2764
2765
|
|
|
2765
|
-
const getDropdownData = async (ids, url, fields, where) => {
|
|
2766
|
+
const getDropdownData = async (ids, url, fields, where, orderBy) => {
|
|
2766
2767
|
// Concatenate id array into a single string if it's an array
|
|
2767
2768
|
const uniqueId = Array.isArray(ids) ? ids.join('') : ids;
|
|
2768
2769
|
|
|
@@ -2772,6 +2773,9 @@ const DataGrid = forwardRef(
|
|
|
2772
2773
|
const res = await CustomFetch(url, 'POST', {
|
|
2773
2774
|
fields,
|
|
2774
2775
|
where,
|
|
2776
|
+
// e.g. orderBy: 'sort_order' so option order follows
|
|
2777
|
+
// the managed sort instead of the endpoint default
|
|
2778
|
+
...(orderBy && { orderBy }),
|
|
2775
2779
|
});
|
|
2776
2780
|
resolve({ [uniqueId]: res.data.data }); // Correctly use uniqueId as the key
|
|
2777
2781
|
} catch (err) {
|
|
@@ -2783,13 +2787,16 @@ const DataGrid = forwardRef(
|
|
|
2783
2787
|
|
|
2784
2788
|
useEffect(() => {
|
|
2785
2789
|
const dropdownFetches = columns
|
|
2786
|
-
.filter((column) =>
|
|
2790
|
+
.filter((column) =>
|
|
2791
|
+
['dropdown', 'multiDropdown'].includes(column.type)
|
|
2792
|
+
)
|
|
2787
2793
|
.map((column) =>
|
|
2788
2794
|
getDropdownData(
|
|
2789
2795
|
column.id,
|
|
2790
2796
|
column.url,
|
|
2791
2797
|
column.fields,
|
|
2792
|
-
column.where
|
|
2798
|
+
column.where,
|
|
2799
|
+
column.orderBy
|
|
2793
2800
|
)
|
|
2794
2801
|
);
|
|
2795
2802
|
|
|
@@ -3079,6 +3086,15 @@ const DataGrid = forwardRef(
|
|
|
3079
3086
|
dropdownData,
|
|
3080
3087
|
handleDropdownUpdate,
|
|
3081
3088
|
});
|
|
3089
|
+
case 'multiDropdown':
|
|
3090
|
+
return renderMultiDropdownColumn({
|
|
3091
|
+
column,
|
|
3092
|
+
commonProps,
|
|
3093
|
+
filterEditor,
|
|
3094
|
+
filterEditorProps,
|
|
3095
|
+
dropdownData,
|
|
3096
|
+
handleDropdownUpdate,
|
|
3097
|
+
});
|
|
3082
3098
|
case 'file':
|
|
3083
3099
|
return renderFileColumn({
|
|
3084
3100
|
column,
|
package/src/components/Form.jsx
CHANGED
|
@@ -1448,6 +1448,8 @@ function Form({
|
|
|
1448
1448
|
let fileUpload = false;
|
|
1449
1449
|
let fileKey = '';
|
|
1450
1450
|
let fileFieldSetting = '';
|
|
1451
|
+
const fileKeys = [];
|
|
1452
|
+
const fileFieldSettings = {};
|
|
1451
1453
|
|
|
1452
1454
|
if (
|
|
1453
1455
|
formSettings.customUrl &&
|
|
@@ -1518,11 +1520,14 @@ function Form({
|
|
|
1518
1520
|
) {
|
|
1519
1521
|
fileUpload = true;
|
|
1520
1522
|
fileKey = item;
|
|
1523
|
+
fileKeys.push(item);
|
|
1521
1524
|
|
|
1522
1525
|
if (formSettings.fields.length > 0) {
|
|
1523
1526
|
formSettings.fields.forEach((field) => {
|
|
1524
1527
|
if (field.id === item) {
|
|
1525
1528
|
fileFieldSetting = field;
|
|
1529
|
+
fileFieldSettings[item] =
|
|
1530
|
+
field;
|
|
1526
1531
|
}
|
|
1527
1532
|
});
|
|
1528
1533
|
}
|
|
@@ -1608,60 +1613,134 @@ function Form({
|
|
|
1608
1613
|
updatedFormData['uploadedFiles'] =
|
|
1609
1614
|
await Promise.all(uploadPromises);
|
|
1610
1615
|
} else {
|
|
1611
|
-
//
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1616
|
+
// Single-File fields. JSON forms can carry
|
|
1617
|
+
// several in one save (e.g. a bio's photo +
|
|
1618
|
+
// document); upload each and attach a
|
|
1619
|
+
// field-scoped descriptor so the backend
|
|
1620
|
+
// stores the file against that exact field.
|
|
1621
|
+
// Non-JSON forms keep the original
|
|
1622
|
+
// one-file-per-save behaviour, driven by the
|
|
1623
|
+
// request-level metadata below.
|
|
1624
|
+
const singleFileKeys = formSettings.json
|
|
1625
|
+
? fileKeys.filter(
|
|
1626
|
+
(k) =>
|
|
1627
|
+
updatedFormData[k] instanceof
|
|
1628
|
+
File
|
|
1629
|
+
)
|
|
1630
|
+
: [fileKey];
|
|
1631
|
+
|
|
1632
|
+
for (const fieldId of singleFileKeys) {
|
|
1633
|
+
const originalName =
|
|
1634
|
+
updatedFormData[fieldId].name;
|
|
1635
|
+
|
|
1636
|
+
// Optimize image if it's an image file
|
|
1637
|
+
const optimizedFile =
|
|
1638
|
+
await optimizeImageIfNeeded(
|
|
1639
|
+
updatedFormData[fieldId]
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
const toastId = toast.info(
|
|
1643
|
+
`Uploading ${originalName}`,
|
|
1644
|
+
{
|
|
1645
|
+
progress: 0,
|
|
1646
|
+
autoClose: false,
|
|
1647
|
+
}
|
|
1615
1648
|
);
|
|
1616
1649
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1650
|
+
const fileRes = await Vapor.store(
|
|
1651
|
+
optimizedFile,
|
|
1652
|
+
{
|
|
1653
|
+
progress: (progress) => {
|
|
1654
|
+
setUploadProgress(
|
|
1655
|
+
progress * 100
|
|
1656
|
+
);
|
|
1657
|
+
toast.update(toastId, {
|
|
1658
|
+
progress,
|
|
1659
|
+
});
|
|
1660
|
+
},
|
|
1661
|
+
}
|
|
1662
|
+
);
|
|
1624
1663
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1664
|
+
toast.update(toastId, {
|
|
1665
|
+
render: `${originalName} uploaded successfully!`,
|
|
1666
|
+
type: 'success',
|
|
1667
|
+
autoClose: 3000,
|
|
1668
|
+
});
|
|
1669
|
+
|
|
1670
|
+
updatedFormData['uuid'] = fileRes.uuid;
|
|
1671
|
+
updatedFormData['key'] = fileRes.key;
|
|
1672
|
+
updatedFormData['bucket'] = fileRes.bucket;
|
|
1673
|
+
updatedFormData['filename'] = originalName;
|
|
1674
|
+
updatedFormData['filesize'] =
|
|
1675
|
+
optimizedFile.size;
|
|
1676
|
+
updatedFormData['extension'] =
|
|
1677
|
+
fileRes.extension;
|
|
1678
|
+
updatedFormData['file_relationship'] =
|
|
1679
|
+
fieldId;
|
|
1680
|
+
updatedFormData['fileable_field'] = (
|
|
1681
|
+
fileFieldSettings[fieldId] ||
|
|
1682
|
+
fileFieldSetting
|
|
1683
|
+
).fileable_field;
|
|
1684
|
+
|
|
1685
|
+
if (formSettings.json) {
|
|
1686
|
+
// Replace the raw File (which would
|
|
1687
|
+
// serialise to junk in the JSON
|
|
1688
|
+
// payload) with an upload descriptor
|
|
1689
|
+
// the backend materialises into a
|
|
1690
|
+
// {file_path, file_name, ...} object.
|
|
1691
|
+
updatedFormData[fieldId] = {
|
|
1692
|
+
_vaporUpload: true,
|
|
1693
|
+
uuid: fileRes.uuid,
|
|
1694
|
+
key: fileRes.key,
|
|
1695
|
+
bucket: fileRes.bucket,
|
|
1696
|
+
filename: originalName,
|
|
1697
|
+
filesize: optimizedFile.size,
|
|
1698
|
+
extension: fileRes.extension,
|
|
1699
|
+
};
|
|
1634
1700
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
toast.update(toastId, {
|
|
1638
|
-
render: `${updatedFormData[fileKey].name} uploaded successfully!`,
|
|
1639
|
-
type: 'success',
|
|
1640
|
-
autoClose: 3000,
|
|
1641
|
-
});
|
|
1642
|
-
|
|
1643
|
-
updatedFormData['uuid'] = fileRes.uuid;
|
|
1644
|
-
updatedFormData['key'] = fileRes.key;
|
|
1645
|
-
updatedFormData['bucket'] = fileRes.bucket;
|
|
1646
|
-
updatedFormData['filename'] =
|
|
1647
|
-
updatedFormData[fileKey].name;
|
|
1648
|
-
updatedFormData['filesize'] =
|
|
1649
|
-
optimizedFile.size;
|
|
1650
|
-
updatedFormData['extension'] =
|
|
1651
|
-
fileRes.extension;
|
|
1652
|
-
updatedFormData['file_relationship'] = fileKey;
|
|
1653
|
-
updatedFormData['fileable_field'] =
|
|
1654
|
-
fileFieldSetting.fileable_field;
|
|
1701
|
+
}
|
|
1655
1702
|
}
|
|
1656
1703
|
}
|
|
1657
1704
|
|
|
1658
|
-
// Transform keys with dots into nested objects
|
|
1705
|
+
// Transform keys with dots into nested objects.
|
|
1706
|
+
// Walks every dot segment (not just two) so ids like
|
|
1707
|
+
// "transport.visit_before_after.yes_before_other" nest
|
|
1708
|
+
// correctly, and merges plain objects instead of
|
|
1709
|
+
// replacing them so sibling keys written by other
|
|
1710
|
+
// fields on the same path survive regardless of key
|
|
1711
|
+
// order.
|
|
1712
|
+
const isPlainObject = (value) =>
|
|
1713
|
+
value !== null &&
|
|
1714
|
+
typeof value === 'object' &&
|
|
1715
|
+
(value.constructor === Object ||
|
|
1716
|
+
value.constructor === undefined);
|
|
1717
|
+
|
|
1659
1718
|
let payload = Object.keys(updatedFormData).reduce(
|
|
1660
1719
|
(acc, key) => {
|
|
1661
1720
|
if (key.includes('.')) {
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1721
|
+
const segments = key.split('.');
|
|
1722
|
+
let node = acc;
|
|
1723
|
+
|
|
1724
|
+
for (
|
|
1725
|
+
let i = 0;
|
|
1726
|
+
i < segments.length - 1;
|
|
1727
|
+
i++
|
|
1728
|
+
) {
|
|
1729
|
+
if (!isPlainObject(node[segments[i]])) {
|
|
1730
|
+
node[segments[i]] = {};
|
|
1731
|
+
}
|
|
1732
|
+
node = node[segments[i]];
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const last =
|
|
1736
|
+
segments[segments.length - 1];
|
|
1737
|
+
const value = updatedFormData[key];
|
|
1738
|
+
|
|
1739
|
+
node[last] =
|
|
1740
|
+
isPlainObject(value) &&
|
|
1741
|
+
isPlainObject(node[last])
|
|
1742
|
+
? { ...node[last], ...value }
|
|
1743
|
+
: value;
|
|
1665
1744
|
} else {
|
|
1666
1745
|
acc[key] = updatedFormData[key];
|
|
1667
1746
|
}
|
|
@@ -2115,10 +2194,12 @@ function Form({
|
|
|
2115
2194
|
let value = data;
|
|
2116
2195
|
|
|
2117
2196
|
for (let key of keys) {
|
|
2118
|
-
|
|
2197
|
+
// ?? (not ||) so falsy-but-real values like
|
|
2198
|
+
// false, 0 and '' survive the walk
|
|
2199
|
+
value = value?.[key] ?? null;
|
|
2119
2200
|
|
|
2120
2201
|
// If value becomes null, break out of the loop
|
|
2121
|
-
if (
|
|
2202
|
+
if (value === null) break;
|
|
2122
2203
|
}
|
|
2123
2204
|
|
|
2124
2205
|
updatedFormData[item] = value;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import numeral from 'numeral';
|
|
2
2
|
import moment from 'moment';
|
|
3
3
|
import parse from 'html-react-parser';
|
|
4
|
+
import Select from 'react-select';
|
|
4
5
|
import { Download, AlarmClock } from 'lucide-react';
|
|
5
6
|
import { toast } from 'react-toastify';
|
|
6
7
|
import CellWithTooltip from '../cells/CellWithTooltip';
|
|
@@ -361,16 +362,54 @@ export const renderDropdownColumn = ({
|
|
|
361
362
|
filterEditor: filterEditor,
|
|
362
363
|
filterEditorProps: filterEditorProps,
|
|
363
364
|
render: ({ data }) => {
|
|
365
|
+
let optionsList =
|
|
366
|
+
dropdownData[column.id] && dropdownData[column.id].length > 0
|
|
367
|
+
? dropdownData[column.id]
|
|
368
|
+
: column.options || [];
|
|
369
|
+
|
|
370
|
+
// Keep historical selections visible/selectable: if the row's
|
|
371
|
+
// current related entry isn't in the options (e.g. it has been
|
|
372
|
+
// deactivated), prepend it so the select can still display it.
|
|
373
|
+
const currentRelated = data[column.id];
|
|
374
|
+
if (
|
|
375
|
+
currentRelated &&
|
|
376
|
+
typeof currentRelated === 'object' &&
|
|
377
|
+
currentRelated.id &&
|
|
378
|
+
!optionsList.some((o) => o.id === currentRelated.id)
|
|
379
|
+
) {
|
|
380
|
+
const currentLabel =
|
|
381
|
+
currentRelated.label ??
|
|
382
|
+
currentRelated.name ??
|
|
383
|
+
currentRelated.title;
|
|
384
|
+
if (currentLabel) {
|
|
385
|
+
optionsList = [
|
|
386
|
+
{ id: currentRelated.id, label: currentLabel },
|
|
387
|
+
...optionsList,
|
|
388
|
+
];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
364
392
|
return (
|
|
365
393
|
<select
|
|
366
394
|
name={column.id}
|
|
367
395
|
value={data[column.id]?.id || data[column.id] || ''}
|
|
368
|
-
style={{
|
|
369
|
-
padding: '7px',
|
|
396
|
+
style={{
|
|
397
|
+
padding: '7px 22px 7px 7px',
|
|
370
398
|
width: '100%',
|
|
371
399
|
border: 'none',
|
|
372
400
|
backgroundColor: 'transparent',
|
|
373
401
|
outline: 'none',
|
|
402
|
+
cursor: 'pointer',
|
|
403
|
+
// Consistent affordance across browsers: strip the
|
|
404
|
+
// native arrow and draw a subtle chevron so the
|
|
405
|
+
// cell visibly reads as a dropdown
|
|
406
|
+
appearance: 'none',
|
|
407
|
+
WebkitAppearance: 'none',
|
|
408
|
+
MozAppearance: 'none',
|
|
409
|
+
backgroundImage:
|
|
410
|
+
"url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' stroke='%23999999' stroke-width='1.5' fill='none' stroke-linecap='round'/></svg>\")",
|
|
411
|
+
backgroundRepeat: 'no-repeat',
|
|
412
|
+
backgroundPosition: 'right 6px center',
|
|
374
413
|
}}
|
|
375
414
|
onChange={(e) => {
|
|
376
415
|
e.preventDefault();
|
|
@@ -390,23 +429,23 @@ export const renderDropdownColumn = ({
|
|
|
390
429
|
}}
|
|
391
430
|
>
|
|
392
431
|
<option value="">
|
|
393
|
-
|
|
394
|
-
|
|
432
|
+
{/* fallbackKey: surface a legacy free-text value
|
|
433
|
+
(recorded before the relation existed) as the
|
|
434
|
+
placeholder so it stays readable until a managed
|
|
435
|
+
option is chosen */}
|
|
436
|
+
{!(data[column.id]?.id || data[column.id]) &&
|
|
437
|
+
column.fallbackKey &&
|
|
438
|
+
data[column.fallbackKey]
|
|
439
|
+
? String(data[column.fallbackKey])
|
|
440
|
+
: `Select ${
|
|
441
|
+
/^[aeioAEIO]/.test(column.label) ? 'an' : 'a'
|
|
442
|
+
} ${column.label}`}
|
|
395
443
|
</option>
|
|
396
|
-
{
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
</option>
|
|
402
|
-
))
|
|
403
|
-
: column.options && column.options.length > 0
|
|
404
|
-
? column.options.map((option, index) => (
|
|
405
|
-
<option value={option.id} key={index}>
|
|
406
|
-
{option.label}
|
|
407
|
-
</option>
|
|
408
|
-
))
|
|
409
|
-
: null}
|
|
444
|
+
{optionsList.map((option, index) => (
|
|
445
|
+
<option value={option.id} key={index}>
|
|
446
|
+
{option.label}
|
|
447
|
+
</option>
|
|
448
|
+
))}
|
|
410
449
|
</select>
|
|
411
450
|
);
|
|
412
451
|
},
|
|
@@ -452,6 +491,120 @@ export const renderDropdownColumn = ({
|
|
|
452
491
|
};
|
|
453
492
|
};
|
|
454
493
|
|
|
494
|
+
// MultiDropdown Column Renderer — inline editor for belongsToMany
|
|
495
|
+
// relations. Chips come from the row's loaded relation (so historical
|
|
496
|
+
// selections of deactivated entries keep displaying); options come from
|
|
497
|
+
// the column's dropdown url; saving PUTs the selected id array to
|
|
498
|
+
// column.update.key, which DynamicController syncs on the pivot.
|
|
499
|
+
export const renderMultiDropdownColumn = ({
|
|
500
|
+
column,
|
|
501
|
+
commonProps,
|
|
502
|
+
filterEditor,
|
|
503
|
+
filterEditorProps,
|
|
504
|
+
dropdownData,
|
|
505
|
+
handleDropdownUpdate,
|
|
506
|
+
}) => {
|
|
507
|
+
const columnId = Array.isArray(column.id) ? column.id.join('-') : column.id;
|
|
508
|
+
const nameFrom = column.nameFrom || 'label';
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
...commonProps,
|
|
512
|
+
name: columnId,
|
|
513
|
+
filterEditor: filterEditor,
|
|
514
|
+
filterEditorProps: filterEditorProps,
|
|
515
|
+
render: ({ data }) => {
|
|
516
|
+
const relationKey = Array.isArray(column.id)
|
|
517
|
+
? column.id[0]
|
|
518
|
+
: column.id;
|
|
519
|
+
const current = Array.isArray(data[relationKey])
|
|
520
|
+
? data[relationKey]
|
|
521
|
+
: [];
|
|
522
|
+
|
|
523
|
+
const value = current.map((entry) => ({
|
|
524
|
+
value: entry.id,
|
|
525
|
+
label: entry[nameFrom] ?? entry.label ?? entry.name ?? '',
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
const options = (
|
|
529
|
+
dropdownData[column.id]?.length
|
|
530
|
+
? dropdownData[column.id]
|
|
531
|
+
: column.options || []
|
|
532
|
+
).map((o) => ({ value: o.id, label: o.label }));
|
|
533
|
+
|
|
534
|
+
// fallbackKey: a legacy free-text value shows as the
|
|
535
|
+
// placeholder until managed entries are selected
|
|
536
|
+
const placeholder =
|
|
537
|
+
value.length === 0 &&
|
|
538
|
+
column.fallbackKey &&
|
|
539
|
+
data[column.fallbackKey]
|
|
540
|
+
? String(data[column.fallbackKey])
|
|
541
|
+
: 'Select...';
|
|
542
|
+
|
|
543
|
+
return (
|
|
544
|
+
<Select
|
|
545
|
+
isMulti
|
|
546
|
+
options={options}
|
|
547
|
+
value={value}
|
|
548
|
+
placeholder={placeholder}
|
|
549
|
+
isClearable={false}
|
|
550
|
+
menuPortalTarget={
|
|
551
|
+
typeof document !== 'undefined'
|
|
552
|
+
? document.body
|
|
553
|
+
: undefined
|
|
554
|
+
}
|
|
555
|
+
menuPlacement="auto"
|
|
556
|
+
styles={{
|
|
557
|
+
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
|
558
|
+
control: (base) => ({
|
|
559
|
+
...base,
|
|
560
|
+
border: 'none',
|
|
561
|
+
boxShadow: 'none',
|
|
562
|
+
backgroundColor: 'transparent',
|
|
563
|
+
minHeight: 'unset',
|
|
564
|
+
}),
|
|
565
|
+
valueContainer: (base) => ({
|
|
566
|
+
...base,
|
|
567
|
+
padding: '0 4px',
|
|
568
|
+
}),
|
|
569
|
+
multiValue: (base) => ({
|
|
570
|
+
...base,
|
|
571
|
+
margin: '1px 2px',
|
|
572
|
+
}),
|
|
573
|
+
multiValueLabel: (base) => ({
|
|
574
|
+
...base,
|
|
575
|
+
fontSize: '12px',
|
|
576
|
+
padding: '1px 4px',
|
|
577
|
+
}),
|
|
578
|
+
placeholder: (base) => ({
|
|
579
|
+
...base,
|
|
580
|
+
color: '#777',
|
|
581
|
+
}),
|
|
582
|
+
indicatorSeparator: () => ({ display: 'none' }),
|
|
583
|
+
dropdownIndicator: (base) => ({
|
|
584
|
+
...base,
|
|
585
|
+
padding: '0 4px',
|
|
586
|
+
}),
|
|
587
|
+
}}
|
|
588
|
+
onChange={(selected) => {
|
|
589
|
+
if (
|
|
590
|
+
column.update?.key &&
|
|
591
|
+
column.update?.url &&
|
|
592
|
+
column.update?.method
|
|
593
|
+
) {
|
|
594
|
+
handleDropdownUpdate(
|
|
595
|
+
column.update.key,
|
|
596
|
+
`${column.update.url}/${data.id}`,
|
|
597
|
+
column.update.method,
|
|
598
|
+
(selected || []).map((s) => s.value)
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}}
|
|
602
|
+
/>
|
|
603
|
+
);
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
};
|
|
607
|
+
|
|
455
608
|
// Image Column Renderer
|
|
456
609
|
export const renderImageColumn = ({ column, commonProps }) => {
|
|
457
610
|
return {
|
|
@@ -1524,6 +1677,17 @@ export const renderRelationArrayColumn = ({
|
|
|
1524
1677
|
: '';
|
|
1525
1678
|
}
|
|
1526
1679
|
|
|
1680
|
+
// fallbackKey: when the relation is empty, display a flat
|
|
1681
|
+
// field from the row instead (e.g. legacy free-text values
|
|
1682
|
+
// recorded before the relation existed)
|
|
1683
|
+
if (
|
|
1684
|
+
result === '' &&
|
|
1685
|
+
column.fallbackKey &&
|
|
1686
|
+
data[column.fallbackKey]
|
|
1687
|
+
) {
|
|
1688
|
+
result = String(data[column.fallbackKey]);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1527
1691
|
return (
|
|
1528
1692
|
<CellWithTooltip
|
|
1529
1693
|
value={result}
|
|
@@ -885,11 +885,35 @@ function GenericDetail({
|
|
|
885
885
|
size,
|
|
886
886
|
truncateLength,
|
|
887
887
|
} = item;
|
|
888
|
-
|
|
888
|
+
let value =
|
|
889
889
|
relation && relation.length > 0 && type !== 'total'
|
|
890
890
|
? getValueFromData(data, [...relation], id, type)
|
|
891
891
|
: getValueFromData(data, [], id, type);
|
|
892
892
|
|
|
893
|
+
// fallbackKey: when the resolved (relation) value is empty, fall
|
|
894
|
+
// back to a flat field on the record — keeps legacy free-text
|
|
895
|
+
// values visible after a field moves to a managed relation
|
|
896
|
+
if (
|
|
897
|
+
(value === '' || value === null || value === undefined) &&
|
|
898
|
+
item.fallbackKey &&
|
|
899
|
+
data[item.fallbackKey]
|
|
900
|
+
) {
|
|
901
|
+
value = data[item.fallbackKey];
|
|
902
|
+
}
|
|
903
|
+
// suffixKey: append a companion flat field in parentheses (e.g.
|
|
904
|
+
// the "Other" free text alongside managed selections). Skipped
|
|
905
|
+
// when the value IS that field already (fallback case above).
|
|
906
|
+
else if (
|
|
907
|
+
value !== '' &&
|
|
908
|
+
value !== null &&
|
|
909
|
+
value !== undefined &&
|
|
910
|
+
item.suffixKey &&
|
|
911
|
+
data[item.suffixKey] &&
|
|
912
|
+
data[item.suffixKey] !== value
|
|
913
|
+
) {
|
|
914
|
+
value = `${value} (${data[item.suffixKey]})`;
|
|
915
|
+
}
|
|
916
|
+
|
|
893
917
|
const stringValue = String(value ?? ''); // Convert value to a string
|
|
894
918
|
|
|
895
919
|
// Conditionally truncate the value if the 'truncate' key is present
|