@visns-studio/visns-components 5.12.2 → 5.12.4
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/README.md +2 -2
- package/package.json +1 -1
- package/src/components/DataGrid.jsx +95 -51
- package/src/components/columns/ColumnRenderers.jsx +322 -20
- package/src/components/generic/GenericDetail.jsx +200 -3
- package/src/components/generic/GenericIndex.jsx +31 -18
- package/src/components/generic/StagePopupModal.jsx +451 -0
- package/src/components/styles/StagePopupModal.module.scss +234 -0
- package/src/components/utils/ConfirmDialog.js +1 -1
- package/src/index.js +2 -0
|
@@ -10,6 +10,127 @@ import {
|
|
|
10
10
|
DEFAULT_INTELLIGENT_SORTING_CONFIG,
|
|
11
11
|
} from '../../utils/relationshipSortingUtils';
|
|
12
12
|
|
|
13
|
+
// Stage Toggle Utility Functions
|
|
14
|
+
const buildToggleUrl = (urlTemplate, stageItem, rowData) => {
|
|
15
|
+
if (!urlTemplate) return null;
|
|
16
|
+
|
|
17
|
+
let url = urlTemplate;
|
|
18
|
+
|
|
19
|
+
// Replace common placeholders
|
|
20
|
+
url = url.replace('{id}', rowData.id || '');
|
|
21
|
+
url = url.replace('{stage_id}', stageItem?.id || '');
|
|
22
|
+
|
|
23
|
+
// Replace any other field placeholders from rowData
|
|
24
|
+
Object.keys(rowData).forEach(key => {
|
|
25
|
+
url = url.replace(`{${key}}`, rowData[key] || '');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Replace any field placeholders from stageItem
|
|
29
|
+
if (stageItem) {
|
|
30
|
+
Object.keys(stageItem).forEach(key => {
|
|
31
|
+
url = url.replace(`{stage_${key}}`, stageItem[key] || '');
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return url;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getStageStatus = (stageItem, toggleConfig) => {
|
|
39
|
+
if (!toggleConfig || !toggleConfig.statusField) return null;
|
|
40
|
+
|
|
41
|
+
const currentValue = stageItem[toggleConfig.statusField];
|
|
42
|
+
const isActive = currentValue === toggleConfig.activeStatusValue;
|
|
43
|
+
const isInactive = currentValue === toggleConfig.inactiveStatusValue;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
isActive,
|
|
47
|
+
isInactive,
|
|
48
|
+
currentValue,
|
|
49
|
+
targetValue: isActive ? toggleConfig.inactiveStatusValue : toggleConfig.activeStatusValue
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleStageToggle = async (stageItem, toggleConfig, rowData, onUpdate) => {
|
|
54
|
+
if (!toggleConfig || !toggleConfig.updateUrl) {
|
|
55
|
+
console.warn('Stage toggle configuration missing updateUrl');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const stageStatus = getStageStatus(stageItem, toggleConfig);
|
|
61
|
+
if (!stageStatus) {
|
|
62
|
+
console.warn('Unable to determine stage status');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Debug logging
|
|
67
|
+
console.log('Stage toggle debug:', {
|
|
68
|
+
stageItem,
|
|
69
|
+
toggleConfig,
|
|
70
|
+
stageStatus,
|
|
71
|
+
currentValue: stageStatus.currentValue,
|
|
72
|
+
targetValue: stageStatus.targetValue,
|
|
73
|
+
isActive: stageStatus.isActive
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const url = buildToggleUrl(toggleConfig.updateUrl, stageItem, rowData);
|
|
77
|
+
if (!url) {
|
|
78
|
+
console.warn('Unable to build toggle URL');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const method = toggleConfig.updateMethod || 'PATCH';
|
|
83
|
+
const updateKey = toggleConfig.updateKey || toggleConfig.statusField;
|
|
84
|
+
|
|
85
|
+
// Prepare request body
|
|
86
|
+
const body = {
|
|
87
|
+
[updateKey]: stageStatus.targetValue
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Get CSRF token if available
|
|
91
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
92
|
+
|
|
93
|
+
// Prepare headers
|
|
94
|
+
const headers = {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
'Accept': 'application/json',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (csrfToken) {
|
|
100
|
+
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Make API call
|
|
104
|
+
const response = await fetch(url, {
|
|
105
|
+
method,
|
|
106
|
+
headers,
|
|
107
|
+
body: JSON.stringify(body),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result = await response.json();
|
|
115
|
+
|
|
116
|
+
// Show success message
|
|
117
|
+
const actionText = stageStatus.isActive ? 'disabled' : 'enabled';
|
|
118
|
+
toast.success(`Stage ${actionText} successfully`);
|
|
119
|
+
|
|
120
|
+
// Call update callback if provided
|
|
121
|
+
if (onUpdate && typeof onUpdate === 'function') {
|
|
122
|
+
onUpdate(result);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Stage toggle error:', error);
|
|
129
|
+
toast.error(`Failed to toggle stage: ${error.message}`);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
13
134
|
// Boolean Column Renderer
|
|
14
135
|
export const renderBooleanColumn = ({
|
|
15
136
|
column,
|
|
@@ -1030,20 +1151,139 @@ export const renderRichTextColumn = ({ column, commonProps }) => {
|
|
|
1030
1151
|
};
|
|
1031
1152
|
|
|
1032
1153
|
// Stage Column Renderer
|
|
1033
|
-
export const renderStageColumn = ({ column, commonProps }) => {
|
|
1154
|
+
export const renderStageColumn = ({ column, commonProps, onUpdate }) => {
|
|
1034
1155
|
return {
|
|
1035
1156
|
...commonProps,
|
|
1036
1157
|
name: column.id,
|
|
1037
1158
|
sortable: false,
|
|
1038
1159
|
render: ({ data }) => {
|
|
1039
1160
|
let stage = '';
|
|
1161
|
+
let activeStageIndex = -1;
|
|
1040
1162
|
|
|
1041
1163
|
column.keyIds.forEach((a, b) => {
|
|
1042
1164
|
if (data[a] === 1) {
|
|
1043
1165
|
stage = column.valueIds[b];
|
|
1166
|
+
activeStageIndex = b;
|
|
1044
1167
|
}
|
|
1045
1168
|
});
|
|
1046
1169
|
|
|
1170
|
+
// Check if toggle functionality is enabled
|
|
1171
|
+
const isToggleable = column.toggleable && column.toggleConfig;
|
|
1172
|
+
|
|
1173
|
+
// Handle stage click for toggling
|
|
1174
|
+
const handleStageClick = async (e, stageIndex) => {
|
|
1175
|
+
e.stopPropagation();
|
|
1176
|
+
e.preventDefault();
|
|
1177
|
+
|
|
1178
|
+
if (!isToggleable) return;
|
|
1179
|
+
|
|
1180
|
+
try {
|
|
1181
|
+
// For basic stage column, we toggle the specific stage field
|
|
1182
|
+
const stageKey = column.keyIds[stageIndex];
|
|
1183
|
+
const currentValue = data[stageKey];
|
|
1184
|
+
const targetValue = currentValue === 1 ? 0 : 1;
|
|
1185
|
+
|
|
1186
|
+
const url = buildToggleUrl(column.toggleConfig.updateUrl, null, data);
|
|
1187
|
+
if (!url) {
|
|
1188
|
+
console.warn('Unable to build toggle URL for stage column');
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const method = column.toggleConfig.updateMethod || 'PATCH';
|
|
1193
|
+
const updateKey = column.toggleConfig.updateKey || stageKey;
|
|
1194
|
+
|
|
1195
|
+
// Prepare request body
|
|
1196
|
+
const body = {
|
|
1197
|
+
[updateKey]: targetValue
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
// Get CSRF token if available
|
|
1201
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
1202
|
+
|
|
1203
|
+
// Prepare headers
|
|
1204
|
+
const headers = {
|
|
1205
|
+
'Content-Type': 'application/json',
|
|
1206
|
+
'Accept': 'application/json',
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
if (csrfToken) {
|
|
1210
|
+
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Make API call
|
|
1214
|
+
const response = await fetch(url, {
|
|
1215
|
+
method,
|
|
1216
|
+
headers,
|
|
1217
|
+
body: JSON.stringify(body),
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
if (!response.ok) {
|
|
1221
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const result = await response.json();
|
|
1225
|
+
|
|
1226
|
+
// Show success message
|
|
1227
|
+
const stageLabel = column.valueIds[stageIndex];
|
|
1228
|
+
const actionText = currentValue === 1 ? 'disabled' : 'enabled';
|
|
1229
|
+
toast.success(`Stage "${stageLabel}" ${actionText} successfully`);
|
|
1230
|
+
|
|
1231
|
+
// Call update callback if provided
|
|
1232
|
+
if (onUpdate && typeof onUpdate === 'function') {
|
|
1233
|
+
onUpdate(result);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
console.error('Stage toggle error:', error);
|
|
1238
|
+
toast.error(`Failed to toggle stage: ${error.message}`);
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
// Create clickable stage buttons if toggleable
|
|
1243
|
+
if (isToggleable) {
|
|
1244
|
+
const stageButtons = column.keyIds.map((keyId, index) => {
|
|
1245
|
+
const isActive = data[keyId] === 1;
|
|
1246
|
+
const stageLabel = column.valueIds[index];
|
|
1247
|
+
|
|
1248
|
+
return (
|
|
1249
|
+
<button
|
|
1250
|
+
key={index}
|
|
1251
|
+
onClick={(e) => handleStageClick(e, index)}
|
|
1252
|
+
style={{
|
|
1253
|
+
padding: '4px 8px',
|
|
1254
|
+
margin: '0 2px',
|
|
1255
|
+
borderRadius: '3px',
|
|
1256
|
+
border: '1px solid #ccc',
|
|
1257
|
+
backgroundColor: isActive ? 'var(--primary-color, #4a90e2)' : '#f8f9fa',
|
|
1258
|
+
color: isActive ? 'white' : '#333',
|
|
1259
|
+
fontSize: '11px',
|
|
1260
|
+
fontWeight: 'bold',
|
|
1261
|
+
cursor: 'pointer',
|
|
1262
|
+
opacity: isActive ? 1 : 0.7,
|
|
1263
|
+
transition: 'all 0.2s ease',
|
|
1264
|
+
}}
|
|
1265
|
+
onMouseEnter={(e) => {
|
|
1266
|
+
e.target.style.transform = 'scale(1.05)';
|
|
1267
|
+
}}
|
|
1268
|
+
onMouseLeave={(e) => {
|
|
1269
|
+
e.target.style.transform = 'scale(1)';
|
|
1270
|
+
}}
|
|
1271
|
+
data-tooltip-id="system-tooltip"
|
|
1272
|
+
data-tooltip-content={`Click to ${isActive ? 'disable' : 'enable'} ${stageLabel}`}
|
|
1273
|
+
>
|
|
1274
|
+
{stageLabel}
|
|
1275
|
+
</button>
|
|
1276
|
+
);
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
return (
|
|
1280
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2px' }}>
|
|
1281
|
+
{stageButtons}
|
|
1282
|
+
</div>
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// Default non-toggleable behavior
|
|
1047
1287
|
return (
|
|
1048
1288
|
<CellWithTooltip value={stage} columnType="stage">
|
|
1049
1289
|
<span>{stage}</span>
|
|
@@ -1592,7 +1832,7 @@ export const renderGalleryColumn = ({
|
|
|
1592
1832
|
};
|
|
1593
1833
|
};
|
|
1594
1834
|
|
|
1595
|
-
export const renderStageCounterColumn = ({ column, commonProps, navigate }) => {
|
|
1835
|
+
export const renderStageCounterColumn = ({ column, commonProps, navigate, onUpdate }) => {
|
|
1596
1836
|
return {
|
|
1597
1837
|
...commonProps,
|
|
1598
1838
|
name: column.id,
|
|
@@ -1605,16 +1845,21 @@ export const renderStageCounterColumn = ({ column, commonProps, navigate }) => {
|
|
|
1605
1845
|
if (stageData && Array.isArray(stageData)) {
|
|
1606
1846
|
const stageCount = stageData.length;
|
|
1607
1847
|
|
|
1608
|
-
//
|
|
1609
|
-
const
|
|
1848
|
+
// Check if toggle functionality is enabled
|
|
1849
|
+
const isToggleable = column.stageConfig?.toggleable && column.stageConfig?.toggleConfig;
|
|
1850
|
+
|
|
1851
|
+
// Container click handler for navigation
|
|
1852
|
+
const handleContainerClick = (e) => {
|
|
1610
1853
|
e.stopPropagation();
|
|
1611
1854
|
e.preventDefault();
|
|
1612
1855
|
|
|
1856
|
+
// Only navigate if toggle is not enabled or if link is specifically configured
|
|
1613
1857
|
if (
|
|
1614
1858
|
column.link &&
|
|
1615
1859
|
column.link.url &&
|
|
1616
1860
|
column.link.key &&
|
|
1617
|
-
navigate
|
|
1861
|
+
navigate &&
|
|
1862
|
+
!isToggleable
|
|
1618
1863
|
) {
|
|
1619
1864
|
navigate(
|
|
1620
1865
|
`${column.link.url}${data[column.link.key]}`
|
|
@@ -1622,6 +1867,26 @@ export const renderStageCounterColumn = ({ column, commonProps, navigate }) => {
|
|
|
1622
1867
|
}
|
|
1623
1868
|
};
|
|
1624
1869
|
|
|
1870
|
+
// Individual stage click handler for toggling
|
|
1871
|
+
const handleStageClick = async (e, stageItem, stageIndex) => {
|
|
1872
|
+
e.stopPropagation();
|
|
1873
|
+
e.preventDefault();
|
|
1874
|
+
|
|
1875
|
+
if (isToggleable) {
|
|
1876
|
+
try {
|
|
1877
|
+
await handleStageToggle(
|
|
1878
|
+
stageItem,
|
|
1879
|
+
column.stageConfig.toggleConfig,
|
|
1880
|
+
data,
|
|
1881
|
+
onUpdate
|
|
1882
|
+
);
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
// Error already handled in handleStageToggle
|
|
1885
|
+
console.error('Stage toggle failed:', error);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
|
|
1625
1890
|
// Create an array of squares based on the stage data
|
|
1626
1891
|
const squares = [];
|
|
1627
1892
|
for (let i = 0; i < stageCount; i++) {
|
|
@@ -1730,24 +1995,61 @@ export const renderStageCounterColumn = ({ column, commonProps, navigate }) => {
|
|
|
1730
1995
|
}
|
|
1731
1996
|
}
|
|
1732
1997
|
|
|
1998
|
+
// Get stage status for visual feedback
|
|
1999
|
+
const stageStatus = isToggleable ? getStageStatus(stageItem, column.stageConfig.toggleConfig) : null;
|
|
2000
|
+
const isStageActive = stageStatus?.isActive ?? true; // Default to active if not toggleable
|
|
2001
|
+
|
|
2002
|
+
// Debug logging for visual state
|
|
2003
|
+
if (isToggleable) {
|
|
2004
|
+
console.log('Visual state debug:', {
|
|
2005
|
+
stageItem,
|
|
2006
|
+
stageStatus,
|
|
2007
|
+
isStageActive,
|
|
2008
|
+
toggleConfig: column.stageConfig.toggleConfig
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// Apply visual feedback for inactive stages
|
|
2013
|
+
const stageStyle = {
|
|
2014
|
+
width: '20px',
|
|
2015
|
+
height: '20px',
|
|
2016
|
+
backgroundColor: backgroundColor,
|
|
2017
|
+
borderRadius: '3px',
|
|
2018
|
+
margin: '0 2px',
|
|
2019
|
+
display: 'flex',
|
|
2020
|
+
justifyContent: 'center',
|
|
2021
|
+
alignItems: 'center',
|
|
2022
|
+
color: 'white',
|
|
2023
|
+
fontSize: '11px',
|
|
2024
|
+
fontWeight: 'bold',
|
|
2025
|
+
cursor: isToggleable ? 'pointer' : 'default',
|
|
2026
|
+
opacity: isStageActive ? 1 : 0.5,
|
|
2027
|
+
transition: 'opacity 0.2s ease, transform 0.1s ease',
|
|
2028
|
+
border: isToggleable ? '1px solid rgba(255,255,255,0.3)' : 'none',
|
|
2029
|
+
};
|
|
2030
|
+
|
|
2031
|
+
// Enhanced tooltip for toggleable stages
|
|
2032
|
+
const enhancedTooltipContent = isToggleable
|
|
2033
|
+
? `${tooltipContent} - Click to ${isStageActive ? 'disable' : 'enable'}`
|
|
2034
|
+
: tooltipContent;
|
|
2035
|
+
|
|
1733
2036
|
squares.push(
|
|
1734
2037
|
<div
|
|
1735
2038
|
key={i}
|
|
1736
|
-
style={
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
fontWeight: 'bold',
|
|
2039
|
+
style={stageStyle}
|
|
2040
|
+
onClick={(e) => isToggleable ? handleStageClick(e, stageItem, i) : undefined}
|
|
2041
|
+
onMouseEnter={(e) => {
|
|
2042
|
+
if (isToggleable) {
|
|
2043
|
+
e.target.style.transform = 'scale(1.1)';
|
|
2044
|
+
}
|
|
2045
|
+
}}
|
|
2046
|
+
onMouseLeave={(e) => {
|
|
2047
|
+
if (isToggleable) {
|
|
2048
|
+
e.target.style.transform = 'scale(1)';
|
|
2049
|
+
}
|
|
1748
2050
|
}}
|
|
1749
2051
|
data-tooltip-id="system-tooltip"
|
|
1750
|
-
data-tooltip-content={
|
|
2052
|
+
data-tooltip-content={enhancedTooltipContent}
|
|
1751
2053
|
>
|
|
1752
2054
|
{labelValue}
|
|
1753
2055
|
</div>
|
|
@@ -1756,13 +2058,13 @@ export const renderStageCounterColumn = ({ column, commonProps, navigate }) => {
|
|
|
1756
2058
|
|
|
1757
2059
|
return (
|
|
1758
2060
|
<div
|
|
1759
|
-
onClick={
|
|
2061
|
+
onClick={handleContainerClick}
|
|
1760
2062
|
style={{
|
|
1761
2063
|
display: 'flex',
|
|
1762
2064
|
flexWrap: 'wrap',
|
|
1763
2065
|
justifyContent: 'center',
|
|
1764
2066
|
alignItems: 'center',
|
|
1765
|
-
cursor: column.link ? 'pointer' : 'default',
|
|
2067
|
+
cursor: (column.link && !isToggleable) ? 'pointer' : 'default',
|
|
1766
2068
|
gap: '4px',
|
|
1767
2069
|
padding: '2px 0',
|
|
1768
2070
|
maxWidth: '100%',
|