@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.
@@ -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
- // Create a clickable element if link is provided
1609
- const handleClick = (e) => {
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
- width: '20px',
1738
- height: '20px',
1739
- backgroundColor: backgroundColor,
1740
- borderRadius: '3px',
1741
- margin: '0 2px',
1742
- display: 'flex',
1743
- justifyContent: 'center',
1744
- alignItems: 'center',
1745
- color: 'white',
1746
- fontSize: '11px',
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={tooltipContent}
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={handleClick}
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%',