@visns-studio/visns-components 5.10.2 → 5.10.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.
@@ -30,7 +30,6 @@ import React, {
30
30
  import { useNavigate } from 'react-router-dom';
31
31
  import debounce from 'lodash.debounce';
32
32
  import moment from 'moment';
33
- import numeral from 'numeral';
34
33
  import Popup from 'reactjs-popup';
35
34
  import 'reactjs-popup/dist/index.css';
36
35
  import parse from 'html-react-parser';
@@ -40,10 +39,6 @@ import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
40
39
  import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
41
40
  import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
42
41
  import { toast } from 'react-toastify';
43
- import imageCompression from 'browser-image-compression';
44
- import Vapor from 'laravel-vapor';
45
- import Lightbox from 'yet-another-react-lightbox';
46
- import { Tooltip } from 'react-tooltip';
47
42
  import fetchUtil from '../../utils/fetchUtil';
48
43
  import { confirmDialog } from '../utils/ConfirmDialog';
49
44
  import {
@@ -54,10 +49,8 @@ import {
54
49
  BookOpen,
55
50
  Check,
56
51
  CircleCheck,
57
- CircleX,
58
52
  Clock,
59
53
  CloudDownload,
60
- CloudUpload,
61
54
  Copy,
62
55
  Edit,
63
56
  Envelope,
@@ -68,108 +61,57 @@ import {
68
61
  LinkOut,
69
62
  LockOn,
70
63
  Network,
71
- Plus,
72
64
  Ribbon,
73
- Search,
74
65
  ShippingBoxV1,
75
- Sort,
76
66
  SoundOn,
77
67
  TriangleAlert,
78
68
  } from 'akar-icons';
79
69
  import styles from './styles/DataGrid.module.scss';
80
70
 
81
71
  import '@visns-studio/visns-datagrid-enterprise/index.css';
82
- import 'yet-another-react-lightbox/styles.css';
83
72
 
84
73
  import CustomFetch from './Fetch';
85
74
  import Download from './Download';
86
75
  import Form from './Form';
76
+ import CellWithTooltip from './cells/CellWithTooltip';
77
+ import DataGridSearch from './controls/DataGridSearch';
78
+ import AutoRefreshControls from './controls/AutoRefreshControls';
79
+ import AudioPlayer from './controls/AudioPlayer';
80
+ import GalleryModal from './modals/GalleryModal';
81
+ import {
82
+ renderBooleanColumn,
83
+ renderCurrencyColumn,
84
+ renderDateColumn,
85
+ renderDateTimeColumn,
86
+ renderArrayCountColumn,
87
+ renderColourColumn,
88
+ renderDropdownColumn,
89
+ renderImageColumn,
90
+ renderFileColumn,
91
+ renderIconsColumn,
92
+ renderJsonColumn,
93
+ renderNumberColumn,
94
+ renderOptionColumn,
95
+ renderPlaceholderColumn,
96
+ renderAgeColumn,
97
+ renderCodingColumn,
98
+ renderRelationColumn,
99
+ renderRelationArrayColumn,
100
+ renderRichTextColumn,
101
+ renderStageColumn,
102
+ renderTimeColumn,
103
+ renderTimerColumn,
104
+ renderUrlColumn,
105
+ renderInputTextColumn,
106
+ renderAddressColumn,
107
+ renderCreatedByColumn,
108
+ renderDateRangeColumn,
109
+ renderStageCounterColumn,
110
+ } from './columns/ColumnRenderers.jsx';
87
111
  import _ from 'lodash';
88
112
 
89
- // CSS styles to ensure consistent group header heights with compact design and click indicators
90
- // Also includes improved multi-line content handling for data rows
91
- const groupHeaderStyles = `
92
- .datagrid-fixed-group-headers .group-header-fixed-height {
93
- height: 34px !important;
94
- min-height: 34px !important;
95
- max-height: 34px !important;
96
- box-sizing: border-box !important;
97
- line-height: 1.1 !important;
98
- flex-shrink: 0 !important;
99
- overflow: hidden !important;
100
- transition: background-color 0.2s ease, box-shadow 0.2s ease !important;
101
- cursor: pointer !important;
102
- position: relative !important;
103
- }
104
-
105
- /* Hover effect for entire group header */
106
- .datagrid-fixed-group-headers .group-header-fixed-height:hover {
107
- background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%) !important;
108
- box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important;
109
- transform: translateY(-1px) !important;
110
- }
111
-
112
- /* Click indicator overlay */
113
- .datagrid-fixed-group-headers .group-header-fixed-height::after {
114
- content: '' !important;
115
- position: absolute !important;
116
- top: 0 !important;
117
- left: 0 !important;
118
- right: 0 !important;
119
- bottom: 0 !important;
120
- background: rgba(59, 130, 246, 0.05) !important;
121
- opacity: 0 !important;
122
- transition: opacity 0.2s ease !important;
123
- pointer-events: none !important;
124
- }
125
-
126
- .datagrid-fixed-group-headers .group-header-fixed-height:hover::after {
127
- opacity: 1 !important;
128
- }
129
-
130
- .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row {
131
- height: 34px !important;
132
- min-height: 34px !important;
133
- max-height: 34px !important;
134
- }
135
-
136
- .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row .InovuaReactDataGrid__group-cell {
137
- height: 34px !important;
138
- min-height: 34px !important;
139
- max-height: 34px !important;
140
- padding: 0 !important;
141
- overflow: hidden !important;
142
- }
143
-
144
- .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row .InovuaReactDataGrid__group-cell > div {
145
- height: 34px !important;
146
- min-height: 34px !important;
147
- max-height: 34px !important;
148
- overflow: hidden !important;
149
- }
150
-
151
- /* Ensure group headers maintain consistent styling during expand/collapse */
152
- .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row--collapsed .group-header-fixed-height,
153
- .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row--expanded .group-header-fixed-height {
154
- height: 34px !important;
155
- min-height: 34px !important;
156
- max-height: 34px !important;
157
- transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease !important;
158
- }
159
-
160
- /* Force consistent height for all group headers regardless of content */
161
- .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row[data-group-by] {
162
- height: 34px !important;
163
- min-height: 34px !important;
164
- max-height: 34px !important;
165
- }
166
-
167
- .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row[data-group-by] .InovuaReactDataGrid__group-cell {
168
- height: 34px !important;
169
- min-height: 34px !important;
170
- max-height: 34px !important;
171
- }
172
-
113
+ // Enhanced multi-line content support for data cells
114
+ const dataGridStyles = `
173
115
  /* Enhanced multi-line content support for data cells */
174
116
  .InovuaReactDataGrid__cell {
175
117
  word-wrap: break-word;
@@ -202,7 +144,7 @@ if (typeof document !== 'undefined') {
202
144
  if (!document.getElementById(styleId)) {
203
145
  const style = document.createElement('style');
204
146
  style.id = styleId;
205
- style.textContent = groupHeaderStyles;
147
+ style.textContent = dataGridStyles;
206
148
  document.head.appendChild(style);
207
149
  }
208
150
  }
@@ -268,123 +210,6 @@ const loadData = async (
268
210
  }
269
211
  };
270
212
 
271
- // Utility function to check if text content is truncated
272
- const isTextTruncated = (element) => {
273
- if (!element) return false;
274
- return (
275
- element.scrollWidth > element.clientWidth ||
276
- element.scrollHeight > element.clientHeight
277
- );
278
- };
279
-
280
- // Utility function to format cell value for tooltip display
281
- const formatCellValueForTooltip = (value, columnType) => {
282
- if (value === null || value === undefined || value === '') {
283
- return '';
284
- }
285
-
286
- switch (columnType) {
287
- case 'date':
288
- case 'datetime':
289
- if (
290
- moment(value).isValid() &&
291
- moment(value).format('YYYY-MM-DD') !== '1970-01-01'
292
- ) {
293
- return moment(value).format('DD-MM-YYYY HH:mm');
294
- }
295
- return String(value);
296
- case 'currency':
297
- return numeral(value).format('$0,0.00');
298
- case 'number':
299
- return numeral(value).format('0,0');
300
- case 'boolean':
301
- return value ? 'Yes' : 'No';
302
- case 'richtext':
303
- // Strip HTML tags for tooltip
304
- const tempDiv = document.createElement('div');
305
- tempDiv.innerHTML = value;
306
- return tempDiv.textContent || tempDiv.innerText || '';
307
- default:
308
- return String(value);
309
- }
310
- };
311
-
312
- // Component to wrap cell content with tooltip functionality
313
- const CellWithTooltip = ({
314
- children,
315
- value,
316
- columnType = 'text',
317
- className = '',
318
- }) => {
319
- const cellRef = useRef(null);
320
- const [showTooltip, setShowTooltip] = useState(false);
321
- const [tooltipId] = useState(
322
- () => `cell-tooltip-${Math.random().toString(36).substr(2, 9)}`
323
- );
324
-
325
- useEffect(() => {
326
- const checkTruncation = () => {
327
- if (cellRef.current) {
328
- const isTruncated = isTextTruncated(cellRef.current);
329
- setShowTooltip(isTruncated);
330
- }
331
- };
332
-
333
- // Check truncation on mount and when content changes
334
- checkTruncation();
335
-
336
- // Also check on window resize
337
- const handleResize = () => checkTruncation();
338
- window.addEventListener('resize', handleResize);
339
-
340
- return () => {
341
- window.removeEventListener('resize', handleResize);
342
- };
343
- }, [children, value]);
344
-
345
- const tooltipContent = formatCellValueForTooltip(value, columnType);
346
-
347
- // Only show tooltip if content is truncated and we have meaningful content
348
- const shouldShowTooltip =
349
- showTooltip && tooltipContent && tooltipContent.trim() !== '';
350
-
351
- return (
352
- <div
353
- ref={cellRef}
354
- className={className}
355
- style={{
356
- overflow: 'hidden',
357
- textOverflow: 'ellipsis',
358
- whiteSpace: 'nowrap',
359
- width: '100%',
360
- }}
361
- {...(shouldShowTooltip && {
362
- 'data-tooltip-id': tooltipId,
363
- 'data-tooltip-html': tooltipContent,
364
- })}
365
- >
366
- {children}
367
- {shouldShowTooltip && (
368
- <Tooltip
369
- id={tooltipId}
370
- style={{
371
- backgroundColor: 'rgba(0, 0, 0, 0.9)',
372
- color: 'white',
373
- borderRadius: '6px',
374
- padding: '8px 12px',
375
- fontSize: '13px',
376
- maxWidth: '500px',
377
- wordWrap: 'break-word',
378
- zIndex: 1000,
379
- }}
380
- place="top"
381
- offset={5}
382
- />
383
- )}
384
- </div>
385
- );
386
- };
387
-
388
213
  const DataGrid = forwardRef(
389
214
  (
390
215
  {
@@ -413,21 +238,16 @@ const DataGrid = forwardRef(
413
238
  const navigate = useNavigate();
414
239
 
415
240
  /** Audio */
416
- const audioRef = useRef(null);
417
241
  const [audioSource, setAudioSource] = useState('');
418
242
 
419
- useEffect(() => {
420
- if (audioSource) {
421
- audioRef.current.play();
422
- }
423
- }, [audioSource]);
424
-
425
243
  /** Gallery Modal States */
426
244
  const [modalGalleryShow, setModalGalleryShow] = useState(false);
427
245
  const [galleryData, setGalleryData] = useState([]);
428
- const [uploadProgress, setUploadProgress] = useState(0);
429
246
  const [currentItem, setCurrentItem] = useState(null);
430
- const [isUploading, setIsUploading] = useState(false);
247
+
248
+ /** Touch Tooltip States */
249
+ const [activeTooltip, setActiveTooltip] = useState(null);
250
+ const [touchTimer, setTouchTimer] = useState(null);
431
251
 
432
252
  /** Lightbox States */
433
253
  const [lightboxOpen, setLightboxOpen] = useState(false);
@@ -442,149 +262,6 @@ const DataGrid = forwardRef(
442
262
  setModalGalleryShow(false);
443
263
  };
444
264
 
445
- const handleFileUpload = async (files) => {
446
- if (!currentItem || !currentItem.settings.upload) return;
447
-
448
- setIsUploading(true);
449
- const uploadPromises = [];
450
-
451
- for (let i = 0; i < files.length; i++) {
452
- const file = files[i];
453
-
454
- try {
455
- // Optimize image if needed
456
- let optimizedFile = file;
457
- if (file.type.startsWith('image/')) {
458
- const options = {
459
- maxSizeMB: 1,
460
- maxWidthOrHeight: 1920,
461
- useWebWorker: true,
462
- };
463
- try {
464
- optimizedFile = await imageCompression(
465
- file,
466
- options
467
- );
468
- } catch (err) {
469
- console.error('Image compression failed:', err);
470
- // Continue with original file if compression fails
471
- }
472
- }
473
-
474
- // Upload to Vapor
475
- const uploadPromise = new Promise((resolve, reject) => {
476
- Vapor.store(optimizedFile, {
477
- progress: (progress) => {
478
- setUploadProgress(progress * 100);
479
- },
480
- })
481
- .then((response) => {
482
- // Construct the URL with the key parameter
483
- const uploadSettings =
484
- currentItem.settings.upload;
485
- const itemData = currentItem.data;
486
- const keyValue = itemData[uploadSettings.key];
487
- const url = keyValue
488
- ? `${uploadSettings.url}/${keyValue}`
489
- : uploadSettings.url;
490
-
491
- // Make the PUT request
492
- CustomFetch(
493
- url,
494
- uploadSettings.method || 'POST',
495
- {
496
- uuid: response.uuid,
497
- key: response.key,
498
- bucket: response.bucket,
499
- filename: file.name,
500
- filesize: optimizedFile.size,
501
- extension: response.extension,
502
- file_relationship: uploadSettings.id,
503
- },
504
- (result) => {
505
- if (result.error === '') {
506
- resolve();
507
- } else {
508
- toast.error(String(result.error));
509
- reject(result.error);
510
- }
511
- }
512
- );
513
- })
514
- .catch(reject);
515
- });
516
-
517
- uploadPromises.push(uploadPromise);
518
- } catch (error) {
519
- console.error('Error uploading file:', error);
520
- toast.error(
521
- `Error uploading ${file.name}: ${
522
- error.message || 'Unknown error'
523
- }`
524
- );
525
- }
526
- }
527
-
528
- try {
529
- await Promise.all(uploadPromises);
530
- toast.success('Files uploaded successfully');
531
-
532
- // Refresh the gallery data
533
- if (currentItem && currentItem.settings && currentItem.data) {
534
- const s = currentItem.settings;
535
- const d = currentItem.data;
536
-
537
- // Function to access nested property using a series of keys
538
- const getNestedValue = (data, keys) =>
539
- keys.reduce(
540
- (currentValue, key) =>
541
- currentValue && currentValue[key] !== undefined
542
- ? currentValue[key]
543
- : undefined,
544
- data
545
- );
546
-
547
- // Fetch updated data
548
- try {
549
- const res = await CustomFetch(
550
- `${form.url}/${d[form.primaryKey]}`,
551
- 'GET',
552
- {}
553
- );
554
-
555
- if (res.data) {
556
- // Extract the updated nested value
557
- const updatedNestedValue = getNestedValue(
558
- res.data,
559
- s.relation
560
- );
561
-
562
- // Update gallery data
563
- if (
564
- updatedNestedValue &&
565
- Array.isArray(updatedNestedValue)
566
- ) {
567
- const updatedGalleryArray =
568
- updatedNestedValue.map((item) => ({
569
- src: item[s.key],
570
- caption: item.file_name,
571
- }));
572
-
573
- setGalleryData(updatedGalleryArray);
574
- }
575
- }
576
- } catch (error) {
577
- console.error('Error refreshing gallery data:', error);
578
- }
579
- }
580
- } catch (error) {
581
- console.error('Error in upload process:', error);
582
- } finally {
583
- setIsUploading(false);
584
- setUploadProgress(0);
585
- }
586
- };
587
-
588
265
  /** Modal States */
589
266
  const [formData, setFormData] = useState({});
590
267
  const [formType, setFormType] = useState('');
@@ -689,6 +366,7 @@ const DataGrid = forwardRef(
689
366
  return () => clearTimeout(timer);
690
367
  }
691
368
  }, [collapsedGroups, ajaxSetting?.groupBy]);
369
+
692
370
  const dataSource = useCallback(
693
371
  (params) => {
694
372
  const sqlResult = loadData(params, dSearch, ajaxSetting);
@@ -1160,8 +838,7 @@ const DataGrid = forwardRef(
1160
838
  setFilterValue(filters);
1161
839
  }, []);
1162
840
 
1163
- const handleChangeSearch = (e) => {
1164
- const { value } = e.target;
841
+ const handleChangeSearch = (value) => {
1165
842
  setSearch(value);
1166
843
  };
1167
844
 
@@ -1540,6 +1217,194 @@ const DataGrid = forwardRef(
1540
1217
  gridRef.current.paginationProps.reload();
1541
1218
  };
1542
1219
 
1220
+ const handleGroupAction = async (groupValue, iconConfig) => {
1221
+ // Get all rows for this group
1222
+ const groupRows = getGroupRows(groupValue);
1223
+
1224
+ // Show confirmation dialog to prevent accidental clicks
1225
+ const confirmMessage = `Are you sure you want to perform "${iconConfig.label}" action on group "${groupValue}" (${groupRows.length} rows)?`;
1226
+
1227
+ confirmDialog({
1228
+ title: 'Confirm Group Action',
1229
+ message: confirmMessage,
1230
+ buttons: [
1231
+ {
1232
+ label: 'Yes',
1233
+ onClick: () => {
1234
+ executeGroupAction(groupValue, iconConfig);
1235
+ },
1236
+ },
1237
+ {
1238
+ label: 'No',
1239
+ onClick: () => {
1240
+ // User cancelled
1241
+ },
1242
+ },
1243
+ ],
1244
+ });
1245
+ };
1246
+
1247
+ const getGroupRows = (groupValue) => {
1248
+ if (!dataRef.current || !ajaxSetting?.groupBy) {
1249
+ return [];
1250
+ }
1251
+
1252
+ const groupField = ajaxSetting.groupBy[0];
1253
+ return dataRef.current.filter(
1254
+ (row) => row[groupField] === groupValue
1255
+ );
1256
+ };
1257
+
1258
+ const shouldShowGroupAction = (iconConfig, groupValue) => {
1259
+ // If no show conditions, always show the icon
1260
+ if (!iconConfig.show || !Array.isArray(iconConfig.show)) {
1261
+ return true;
1262
+ }
1263
+
1264
+ // Get all rows for this group
1265
+ const groupRows = getGroupRows(groupValue);
1266
+
1267
+ // Check if all show conditions are met
1268
+ return iconConfig.show.every((condition) => {
1269
+ const { id, value } = condition;
1270
+
1271
+ // Check if any row in the group matches the condition
1272
+ return groupRows.some((row) => {
1273
+ const rowValue = row[id];
1274
+
1275
+ // Handle null value condition
1276
+ if (value === null) {
1277
+ return (
1278
+ rowValue === null ||
1279
+ rowValue === undefined ||
1280
+ rowValue === ''
1281
+ );
1282
+ }
1283
+
1284
+ // Handle other value conditions
1285
+ return rowValue === value;
1286
+ });
1287
+ });
1288
+ };
1289
+
1290
+ const executeGroupAction = async (groupValue, iconConfig) => {
1291
+ // Check if fetch configuration is available
1292
+ if (iconConfig.fetch && iconConfig.fetch.url) {
1293
+ try {
1294
+ const payload = {
1295
+ ...iconConfig.fetch.data,
1296
+ groupLabel: groupValue,
1297
+ };
1298
+
1299
+ const result = await CustomFetch(
1300
+ iconConfig.fetch.url,
1301
+ iconConfig.fetch.method || 'POST',
1302
+ payload
1303
+ );
1304
+
1305
+ if (result.error) {
1306
+ throw new Error(result.error);
1307
+ }
1308
+
1309
+ toast.success(
1310
+ `${iconConfig.label} - Group: ${groupValue} completed successfully`
1311
+ );
1312
+
1313
+ // Reload grid data if needed
1314
+ if (gridRef.current && gridRef.current.paginationProps) {
1315
+ gridRef.current.paginationProps.reload();
1316
+ }
1317
+ } catch (error) {
1318
+ console.error('Group action fetch error:', error);
1319
+ toast.error(
1320
+ `${iconConfig.label} - Group: ${groupValue} failed: ${error.message}`
1321
+ );
1322
+ }
1323
+ } else {
1324
+ // Fallback to existing switch logic if no fetch config
1325
+ toast.info(`${iconConfig.label} - Group: ${groupValue}`);
1326
+
1327
+ switch (iconConfig.id) {
1328
+ case 'clock':
1329
+ console.log(
1330
+ `Starting timer action for group: ${groupValue}`
1331
+ );
1332
+ break;
1333
+ case 'alarm':
1334
+ console.log(
1335
+ `Setting alarm action for group: ${groupValue}`
1336
+ );
1337
+ break;
1338
+ case 'check':
1339
+ console.log(
1340
+ `Mark complete action for group: ${groupValue}`
1341
+ );
1342
+ break;
1343
+ case 'edit':
1344
+ console.log(
1345
+ `Bulk edit action for group: ${groupValue}`
1346
+ );
1347
+ break;
1348
+ case 'envelope':
1349
+ console.log(
1350
+ `Send notification action for group: ${groupValue}`
1351
+ );
1352
+ break;
1353
+ default:
1354
+ console.log(
1355
+ `Custom action ${iconConfig.id} for group: ${groupValue}`
1356
+ );
1357
+ break;
1358
+ }
1359
+ }
1360
+ };
1361
+
1362
+ // Touch handlers for tooltips
1363
+ const handleTouchStart = (e, buttonId, label) => {
1364
+ // Clear any existing timer
1365
+ if (touchTimer) {
1366
+ clearTimeout(touchTimer);
1367
+ }
1368
+
1369
+ // Capture the button element and rect immediately
1370
+ const buttonElement = e.currentTarget;
1371
+ const rect = buttonElement.getBoundingClientRect();
1372
+ const x = rect.left + rect.width / 2; // Center of button
1373
+ const y = rect.top; // Top of button
1374
+
1375
+ // Set a timer for long press (500ms)
1376
+ const timer = setTimeout(() => {
1377
+ setActiveTooltip({ id: buttonId, label, x, y });
1378
+
1379
+ // Auto-close tooltip after 2 seconds
1380
+ setTimeout(() => {
1381
+ setActiveTooltip(null);
1382
+ }, 2000);
1383
+ }, 500);
1384
+
1385
+ setTouchTimer(timer);
1386
+ };
1387
+
1388
+ const handleTouchEnd = () => {
1389
+ // Clear the timer if touch ends before long press
1390
+ if (touchTimer) {
1391
+ clearTimeout(touchTimer);
1392
+ setTouchTimer(null);
1393
+ }
1394
+ };
1395
+
1396
+ const handleTouchMove = () => {
1397
+ // Cancel tooltip if user moves finger (not a long press)
1398
+ if (touchTimer) {
1399
+ clearTimeout(touchTimer);
1400
+ setTouchTimer(null);
1401
+ }
1402
+ };
1403
+
1404
+ const closeTooltip = () => {
1405
+ setActiveTooltip(null);
1406
+ };
1407
+
1543
1408
  const handleSort = (value) => {
1544
1409
  if (setConfig) {
1545
1410
  setConfig((prevState) => {
@@ -1767,212 +1632,6 @@ const DataGrid = forwardRef(
1767
1632
  [selected, setRowsSelected, onRowClick]
1768
1633
  );
1769
1634
 
1770
- const exportTrigger = async () => {
1771
- try {
1772
- toast.success('Starting export please wait...');
1773
-
1774
- let payload;
1775
-
1776
- if (form.export.useData) {
1777
- payload = { data: dataRef.current };
1778
- } else {
1779
- payload = {
1780
- filter: filterValue,
1781
- paramId: paramValue || '',
1782
- };
1783
- }
1784
-
1785
- const res = await Download(form.export.url, 'POST', payload);
1786
-
1787
- if (res.data.error) {
1788
- toast.error(res.data.error);
1789
- } else {
1790
- // Assuming the response contains a Blob or similar data
1791
- if (res.data && res.data instanceof Blob) {
1792
- const fileName = `${form.export.filename}`;
1793
- saveAs(res.data, fileName);
1794
- } else {
1795
- // Handle the case where the response is not a Blob
1796
- toast.error('Invalid file format received.');
1797
- }
1798
- }
1799
- } catch (error) {
1800
- console.info(error);
1801
- }
1802
- };
1803
-
1804
- const sortTrigger = () => {
1805
- let baseUrl = form.url.replace('/ajax', '');
1806
- let sortUrl = paramValue
1807
- ? `${baseUrl}/${paramValue}/sort`
1808
- : `${baseUrl}/sort`;
1809
-
1810
- if (form.sort?.url) {
1811
- sortUrl = form.sort.url;
1812
- }
1813
-
1814
- const newState = {
1815
- form,
1816
- page: {
1817
- data: pageData,
1818
- setting: pageSetting,
1819
- },
1820
- paramValue,
1821
- };
1822
-
1823
- navigate(sortUrl, { state: newState });
1824
- };
1825
-
1826
- const renderCreateButton = () => {
1827
- if (form.createDisable && form.createDisable === true) {
1828
- return null;
1829
- }
1830
-
1831
- const isSearchEnabled = !(form.searchDisable === true);
1832
- const buttonClassName = isSearchEnabled
1833
- ? styles.actionButtons
1834
- : styles.actionButtonsStandalone;
1835
-
1836
- return (
1837
- <div className={buttonClassName}>
1838
- {form.export && form.export.url && (
1839
- <button className={styles.btn} onClick={exportTrigger}>
1840
- <LinkOut
1841
- strokeWidth={2}
1842
- size={16}
1843
- style={{ marginRight: '10px' }}
1844
- />{' '}
1845
- Export
1846
- </button>
1847
- )}
1848
- {form.sort && form.sort.title && (
1849
- <button className={styles.btn} onClick={sortTrigger}>
1850
- <Sort
1851
- strokeWidth={2}
1852
- size={16}
1853
- style={{ marginRight: '10px' }}
1854
- />{' '}
1855
- Sort Order
1856
- </button>
1857
- )}
1858
-
1859
- {(form.create &&
1860
- form.create.title &&
1861
- form.create.title !== '') ||
1862
- (form.url && form.create?.title !== '') ? (
1863
- <button
1864
- className={styles.btn}
1865
- onClick={(e) => {
1866
- e.preventDefault();
1867
-
1868
- if (form.create) {
1869
- if (form.create.url) {
1870
- // Check if URL is external
1871
- if (
1872
- form.create.url.startsWith(
1873
- 'http://'
1874
- ) ||
1875
- form.create.url.startsWith(
1876
- 'https://'
1877
- )
1878
- ) {
1879
- window.open(
1880
- form.create.url,
1881
- '_blank'
1882
- );
1883
- } else {
1884
- navigate(form.create.url);
1885
- }
1886
- } else {
1887
- modalOpen('create', 0);
1888
- }
1889
- } else {
1890
- if (form.url) {
1891
- window.open(
1892
- form.url,
1893
- '',
1894
- 'width=1440,height=1024,menubar=1,resizable=1,status=1,titlebar=1,toolbar=1'
1895
- );
1896
- }
1897
- }
1898
- }}
1899
- >
1900
- <Plus
1901
- strokeWidth={2}
1902
- size={16}
1903
- style={{ marginRight: '10px' }}
1904
- />
1905
- Add New
1906
- </button>
1907
- ) : form.url && form.modal === false ? (
1908
- <button
1909
- className={styles.btn}
1910
- onClick={(e) => {
1911
- e.preventDefault();
1912
-
1913
- window.open(
1914
- form.url,
1915
- '',
1916
- 'width=1440,height=1024,menubar=1,resizable=1,status=1,titlebar=1,toolbar=1'
1917
- );
1918
- }}
1919
- >
1920
- <Plus
1921
- strokeWidth={2}
1922
- size={16}
1923
- style={{ marginRight: '10px' }}
1924
- />
1925
- Add New
1926
- </button>
1927
- ) : null}
1928
- </div>
1929
- );
1930
- };
1931
-
1932
- const renderSearch = () => {
1933
- const isSearchEnabled = !(form.searchDisable === true);
1934
- const isCreateDisabled = form.createDisable === true;
1935
-
1936
- // Don't render anything if both search and create are disabled
1937
- if (!isSearchEnabled && isCreateDisabled) {
1938
- return null;
1939
- }
1940
-
1941
- // If search is disabled but create button is enabled, render only the button with its own container
1942
- if (!isSearchEnabled && !isCreateDisabled) {
1943
- return (
1944
- <div className={styles.actionButtonsContainer}>
1945
- {renderCreateButton()}
1946
- </div>
1947
- );
1948
- }
1949
-
1950
- // If search is enabled, render the full search container with both search and buttons
1951
- return (
1952
- <div className={styles.searchContainer}>
1953
- <div className={styles.searchInputWrapper}>
1954
- <div
1955
- className={styles['icon-search']}
1956
- style={{ left: 25, color: '#000' }}
1957
- >
1958
- <Search strokeWidth={2} size={14} />
1959
- </div>
1960
- <input
1961
- type="text"
1962
- placeholder="Search"
1963
- value={search}
1964
- onChange={handleChangeSearch}
1965
- style={{ borderRadius: 0, paddingLeft: 55 }}
1966
- className={styles.searchInput}
1967
- />
1968
- </div>
1969
- <div className={styles.actionButtonsWrapper}>
1970
- {!isCreateDisabled && renderCreateButton()}
1971
- </div>
1972
- </div>
1973
- );
1974
- };
1975
-
1976
1635
  // Helper function to determine if auto refresh should be relocated
1977
1636
  const shouldRelocateAutoRefresh = () => {
1978
1637
  const isSearchDisabled = form.searchDisable === true;
@@ -1980,36 +1639,9 @@ const DataGrid = forwardRef(
1980
1639
  const hasAutoRefresh =
1981
1640
  ajaxSetting && ajaxSetting.autoRefresh !== undefined;
1982
1641
 
1983
- // Relocate only if auto refresh is the sole control (no search, no create button)
1984
- return hasAutoRefresh && isSearchDisabled && isCreateDisabled;
1985
- };
1986
-
1987
- const renderAutoRefreshControls = (isRelocated = false) => {
1988
- // Only show auto-refresh if the setting is present in ajaxSetting
1989
- if (!ajaxSetting || ajaxSetting.autoRefresh === undefined) {
1990
- return null;
1991
- }
1992
-
1993
- const containerClass = isRelocated
1994
- ? styles.autoRefreshContainerRelocated
1995
- : styles.autoRefreshContainer;
1996
-
1997
- return (
1998
- <div className={containerClass}>
1999
- <label className={styles.autoRefreshLabel}>
2000
- <input
2001
- type="checkbox"
2002
- checked={autoRefreshEnabled}
2003
- onChange={() =>
2004
- setAutoRefreshEnabled(!autoRefreshEnabled)
2005
- }
2006
- className={styles.autoRefreshCheckbox}
2007
- />
2008
- Auto Refresh{' '}
2009
- {refreshInterval && `(${refreshInterval}s)`}
2010
- </label>
2011
- </div>
2012
- );
1642
+ // Hide auto refresh when it would be the sole control (instead of relocating it)
1643
+ // This prevents it from appearing under the datagrid
1644
+ return false;
2013
1645
  };
2014
1646
 
2015
1647
  const renderRowContextMenu = useCallback(
@@ -2714,8 +2346,6 @@ const DataGrid = forwardRef(
2714
2346
  let date;
2715
2347
  let filterEditor = null;
2716
2348
  let filterEditorProps = null;
2717
- let fileUrl = null;
2718
- let icons = [];
2719
2349
  let relationName;
2720
2350
  let selectedDataSource = null;
2721
2351
  let stage;
@@ -2840,252 +2470,259 @@ const DataGrid = forwardRef(
2840
2470
 
2841
2471
  switch (column.type) {
2842
2472
  case 'address':
2843
- return {
2844
- ...commonProps,
2845
- name: `${column.type}.${column.id}`,
2846
- filterEditor: filterEditor,
2847
- filterEditorProps: filterEditorProps,
2848
- render: ({ data }) => {
2849
- const addressFields = [
2850
- 'address1',
2851
- 'address2',
2852
- 'suburb',
2853
- 'postcode',
2854
- ];
2855
-
2856
- const addressValue = addressFields
2857
- .filter((field) => data[field])
2858
- .map((field) => data[field])
2859
- .join(' ');
2860
-
2861
- const stateValue =
2862
- data.state?.name ||
2863
- data.state ||
2864
- '';
2865
-
2866
- const value = `${addressValue} ${stateValue}`;
2867
-
2868
- return (
2869
- <CellWithTooltip
2870
- value={value}
2871
- columnType="address"
2872
- >
2873
- <span>{value}</span>
2874
- </CellWithTooltip>
2875
- );
2876
- },
2877
- };
2473
+ return renderAddressColumn({
2474
+ column,
2475
+ commonProps,
2476
+ filterEditor,
2477
+ filterEditorProps,
2478
+ });
2878
2479
  case 'age':
2879
- return {
2880
- ...commonProps,
2881
- type: 'number',
2882
- filterEditor: filterEditor,
2883
- name: `${column.type}.${column.id}`,
2884
- render: ({ data }) => {
2885
- value = column.id.reduce((acc, id) => {
2886
- if (acc === '') {
2887
- return data[id];
2888
- } else {
2889
- return acc ? acc[id] : '';
2890
- }
2891
- }, '');
2892
-
2893
- if (value !== '' && value !== null) {
2894
- date = moment(value);
2895
-
2896
- return (
2897
- <span>
2898
- {moment().diff(
2899
- date,
2900
- column.duration
2901
- ? column.duration
2902
- : 'years'
2903
- )}
2904
- </span>
2905
- );
2906
- } else {
2907
- return null;
2908
- }
2909
- },
2910
- };
2480
+ return renderAgeColumn({
2481
+ column,
2482
+ commonProps,
2483
+ filterEditor,
2484
+ });
2911
2485
  case 'arrayCount':
2912
- return {
2913
- ...commonProps,
2914
- name: `${column.type}.${column.id}`,
2915
- sortable: false,
2916
- render: ({ data }) => {
2917
- if (data && data[column.id]) {
2918
- return data[column.id].length;
2919
- } else {
2920
- return null;
2921
- }
2922
- },
2923
- };
2486
+ return renderArrayCountColumn({
2487
+ column,
2488
+ commonProps,
2489
+ });
2924
2490
  case 'boolean':
2925
- return {
2926
- ...commonProps,
2927
- filterEditor: filterEditor,
2928
- filterEditorProps: filterEditorProps,
2929
- name: `${column.type}.${column.id}`,
2930
- sortable: false,
2931
- render: ({ data }) => {
2932
- const value = data[column.id]
2933
- ? 'Yes'
2934
- : 'No';
2935
- return (
2936
- <CellWithTooltip
2937
- value={data[column.id]}
2938
- columnType="boolean"
2939
- >
2940
- <span>{value}</span>
2941
- </CellWithTooltip>
2942
- );
2943
- },
2944
- };
2491
+ return renderBooleanColumn({
2492
+ column,
2493
+ commonProps,
2494
+ filterEditor,
2495
+ filterEditorProps,
2496
+ });
2945
2497
  case 'coding':
2946
- return {
2947
- ...commonProps,
2948
- name: `${column.type}.${column.id}`,
2949
- sortable: false,
2950
- render: ({ data }) => {
2951
- if (data) {
2952
- return handleCoding(column, data);
2953
- } else {
2954
- return null;
2955
- }
2956
- },
2957
- };
2498
+ return renderCodingColumn({
2499
+ column,
2500
+ commonProps,
2501
+ handleCoding,
2502
+ });
2958
2503
  case 'colour':
2959
- return {
2960
- ...commonProps,
2961
- name: `${column.type}.${column.id}`,
2962
- sortable: false,
2963
- render: ({ data }) => {
2964
- if (data && data[column.id]) {
2965
- return (
2966
- <div
2967
- style={{
2968
- width: '25px',
2969
- height: '25px',
2970
- borderRadius: '10px',
2971
- overflow: 'hidden',
2972
- }}
2973
- >
2974
- <div
2975
- style={{
2976
- background:
2977
- data[
2978
- column.id
2979
- ] +
2980
- ' none repeat scroll 0% 0%',
2981
- height: '100%',
2982
- width: '100%',
2983
- cursor: 'pointer',
2984
- position:
2985
- 'relative',
2986
- outline:
2987
- 'currentcolor none medium',
2988
- }}
2989
- ></div>
2990
- </div>
2991
- );
2992
- } else {
2993
- return null;
2994
- }
2995
- },
2996
- };
2504
+ return renderColourColumn({
2505
+ column,
2506
+ commonProps,
2507
+ });
2997
2508
  case 'created_by':
2998
- return {
2999
- ...commonProps,
3000
- name: `${column.type}.${column.id}`,
3001
- sortable: false,
3002
- render: ({ data }) => {
3003
- if (
3004
- data.audits[0] &&
3005
- data.audits[0].user &&
3006
- data.audits[0].user.name
3007
- ) {
3008
- value = data.audits[0].user.name;
3009
-
3010
- return <span>{value}</span>;
3011
- } else {
3012
- return null;
3013
- }
3014
- },
3015
- };
2509
+ return renderCreatedByColumn({
2510
+ column,
2511
+ commonProps,
2512
+ });
3016
2513
  case 'currency':
3017
- return {
3018
- ...commonProps,
3019
- name: column.id,
3020
- render: ({ data }) => {
3021
- if (data) {
3022
- data = numeral(
3023
- data[column.id]
3024
- ).format('$0,0.00');
3025
-
3026
- return (
3027
- <CellWithTooltip
3028
- value={data[column.id]}
3029
- columnType="currency"
3030
- >
3031
- <span>{data}</span>
3032
- </CellWithTooltip>
3033
- );
3034
- } else {
3035
- return null;
3036
- }
3037
- },
3038
- };
2514
+ return renderCurrencyColumn({
2515
+ column,
2516
+ commonProps,
2517
+ });
3039
2518
  case 'date':
2519
+ return renderDateColumn({
2520
+ column,
2521
+ commonProps,
2522
+ filterEditor,
2523
+ filterEditorProps,
2524
+ });
2525
+ case 'daterange':
2526
+ return renderDateRangeColumn({
2527
+ column,
2528
+ commonProps,
2529
+ filterEditor,
2530
+ filterEditorProps,
2531
+ });
2532
+ case 'datetime':
2533
+ return renderDateTimeColumn({
2534
+ column,
2535
+ commonProps,
2536
+ filterEditor,
2537
+ filterEditorProps,
2538
+ });
2539
+ case 'dropdown':
2540
+ return renderDropdownColumn({
2541
+ column,
2542
+ commonProps,
2543
+ filterEditor,
2544
+ filterEditorProps,
2545
+ dropdownData,
2546
+ handleDropdownUpdate,
2547
+ });
2548
+ case 'file':
2549
+ return renderFileColumn({
2550
+ column,
2551
+ commonProps,
2552
+ handleDownload,
2553
+ styles,
2554
+ });
2555
+ case 'icons':
2556
+ return renderIconsColumn({
2557
+ column,
2558
+ commonProps,
2559
+ });
2560
+ case 'image':
2561
+ return renderImageColumn({
2562
+ column,
2563
+ commonProps,
2564
+ });
2565
+ case 'json':
2566
+ return renderJsonColumn({
2567
+ column,
2568
+ commonProps,
2569
+ });
2570
+ case 'number':
2571
+ return renderNumberColumn({
2572
+ column,
2573
+ commonProps,
2574
+ });
2575
+ case 'option':
2576
+ return renderOptionColumn({
2577
+ column,
2578
+ commonProps,
2579
+ filterEditor,
2580
+ filterEditorProps,
2581
+ });
2582
+ case 'placeholder':
2583
+ return renderPlaceholderColumn({
2584
+ column,
2585
+ commonProps,
2586
+ });
2587
+ case 'relation':
2588
+ return renderRelationColumn({
2589
+ column,
2590
+ commonProps,
2591
+ filterEditor,
2592
+ filterEditorProps,
2593
+ relationName,
2594
+ });
2595
+ case 'relationArray':
2596
+ return renderRelationArrayColumn({
2597
+ column,
2598
+ commonProps,
2599
+ filterEditor,
2600
+ filterEditorProps,
2601
+ relationName,
2602
+ });
2603
+ case 'richtext':
2604
+ return renderRichTextColumn({
2605
+ column,
2606
+ commonProps,
2607
+ });
2608
+ case 'stage':
2609
+ return renderStageColumn({
2610
+ column,
2611
+ commonProps,
2612
+ });
2613
+ case 'time':
2614
+ return renderTimeColumn({
2615
+ column,
2616
+ commonProps,
2617
+ });
2618
+ case 'timer':
2619
+ return renderTimerColumn({
2620
+ column,
2621
+ commonProps,
2622
+ handleTimer,
2623
+ form,
2624
+ columns,
2625
+ styles,
2626
+ });
2627
+ case 'stageCounter':
2628
+ return renderStageCounterColumn({
2629
+ column,
2630
+ commonProps,
2631
+ navigate,
2632
+ });
2633
+ case 'url':
2634
+ return renderUrlColumn({ column, commonProps });
2635
+ case 'input_text':
2636
+ return renderInputTextColumn({
2637
+ column,
2638
+ commonProps,
2639
+ localInputValuesRef,
2640
+ setLocalInputValues,
2641
+ handleInputUpdate,
2642
+ });
2643
+ default:
2644
+ columnId = Array.isArray(column.id)
2645
+ ? column.id.join('-')
2646
+ : column.id;
2647
+
3040
2648
  return {
3041
2649
  ...commonProps,
3042
- name: column.id,
2650
+ name: columnId,
3043
2651
  filterEditor: filterEditor,
3044
2652
  filterEditorProps: filterEditorProps,
3045
- defaultWidth: 120, // Set default width for date columns
3046
- minWidth: 120, // Set minimum width for date columns
2653
+ className:
2654
+ column.wordWrap === true
2655
+ ? 'cell-word-wrap'
2656
+ : '',
3047
2657
  render: ({ data }) => {
3048
2658
  if (
3049
2659
  data &&
3050
2660
  data[column.id] &&
3051
- moment(data[column.id]).isValid() &&
3052
- moment(data[column.id]).format(
3053
- 'YYYY-MM-DD'
3054
- ) !== '1970-01-01'
2661
+ data[column.id] !== ''
3055
2662
  ) {
3056
- const value = moment(
2663
+ let content = String(
3057
2664
  data[column.id]
3058
- ).format(
3059
- column.format || 'DD-MM-YYYY'
3060
2665
  );
3061
- columnStyle = {};
3062
-
3063
- if (
3064
- column.active &&
3065
- column.active.id &&
3066
- column.active.value ===
3067
- 'expired'
3068
- ) {
3069
- if (
3070
- data[column.active.id] &&
3071
- moment(
3072
- data[column.active.id]
3073
- ).isBefore(moment())
3074
- ) {
3075
- columnStyle = {
3076
- color: column.active
3077
- .colour,
3078
- };
3079
- }
2666
+
2667
+ // Debug logging for wordWrap cell rendering
2668
+ if (column.wordWrap === true) {
2669
+ console.log(
2670
+ 'DataGrid DEBUG - Rendering WordWrap Cell:',
2671
+ {
2672
+ columnId,
2673
+ contentLength:
2674
+ content.length,
2675
+ contentPreview:
2676
+ content.substring(
2677
+ 0,
2678
+ 50
2679
+ ) + '...',
2680
+ wordWrapProp:
2681
+ column.wordWrap,
2682
+ }
2683
+ );
2684
+ }
2685
+
2686
+ // If removeFormat is true, strip HTML tags
2687
+ if (column.removeFormat) {
2688
+ content = content.replace(
2689
+ /<\/?[^>]+(>|$)/g,
2690
+ ''
2691
+ ); // Simple regex to strip HTML tags
2692
+ } else {
2693
+ // Replace p tags with br tags
2694
+ content = content
2695
+ .replace(/<p>/g, '')
2696
+ .replace(/<\/p>/g, '<br>');
2697
+
2698
+ // Ensure <br /> tags are properly handled (convert to <br> for consistency)
2699
+ content = content
2700
+ .replace(/<br \/>/g, '<br>')
2701
+ .replace(/<br\/>/g, '<br>');
2702
+ }
2703
+
2704
+ // If truncate has a value, limit content to specified number of characters
2705
+ if (column.truncate) {
2706
+ content =
2707
+ content.length >
2708
+ column.truncate
2709
+ ? content.substring(
2710
+ 0,
2711
+ column.truncate
2712
+ ) + '...'
2713
+ : content;
3080
2714
  }
3081
2715
 
3082
2716
  return (
3083
2717
  <CellWithTooltip
3084
2718
  value={data[column.id]}
3085
- columnType="date"
2719
+ columnType="text"
2720
+ wordWrap={
2721
+ column.wordWrap === true
2722
+ }
3086
2723
  >
3087
- <span style={columnStyle}>
3088
- {value}
2724
+ <span>
2725
+ {parse(content)}
3089
2726
  </span>
3090
2727
  </CellWithTooltip>
3091
2728
  );
@@ -3094,1417 +2731,40 @@ const DataGrid = forwardRef(
3094
2731
  }
3095
2732
  },
3096
2733
  };
3097
- case 'daterange':
3098
- return {
3099
- ...commonProps,
3100
- name: `${column.id[0]}.${column.id[1]}.`,
3101
- filterEditor: filterEditor,
3102
- filterEditorProps: filterEditorProps,
3103
- defaultWidth: 200, // Set default width for daterange columns
3104
- minWidth: 200, // Set minimum width for daterange columns
3105
- render: ({ data }) => {
3106
- if (
3107
- data &&
3108
- data[column.id[0]] &&
3109
- moment(
3110
- data[column.id[0]]
3111
- ).isValid() &&
3112
- moment(data[column.id[0]]).format(
3113
- 'YYYY-MM-DD'
3114
- ) !== '1970-01-01'
3115
- ) {
3116
- value = moment(
3117
- data[column.id[0]]
3118
- ).format(
3119
- column.format || 'DD-MM-YYYY'
3120
- );
3121
- }
2734
+ }
2735
+ };
3122
2736
 
3123
- if (
3124
- data &&
3125
- data[column.id[1]] &&
3126
- moment(
3127
- data[column.id[1]]
3128
- ).isValid() &&
3129
- moment(data[column.id[1]]).format(
3130
- 'YYYY-MM-DD'
3131
- ) !== '1970-01-01'
3132
- ) {
3133
- value += ` - ${moment(
3134
- data[column.id[1]]
3135
- ).format(
3136
- column.format || 'DD-MM-YYYY'
3137
- )}`;
3138
- }
2737
+ const newColumns = columns.map(renderColumn);
3139
2738
 
3140
- return <span>{value}</span>;
3141
- },
3142
- };
3143
- case 'datetime':
3144
- return {
3145
- ...commonProps,
3146
- name: column.id,
3147
- filterEditor: filterEditor,
3148
- filterEditorProps: filterEditorProps,
3149
- defaultWidth: 160, // Set default width for datetime columns
3150
- minWidth: 160, // Set minimum width for datetime columns
3151
- render: ({ data }) => {
3152
- if (
3153
- data &&
3154
- data[column.id] &&
3155
- moment(data[column.id]).isValid() &&
3156
- moment(data[column.id]).format(
3157
- 'YYYY-MM-DD'
3158
- ) !== '1970-01-01'
3159
- ) {
3160
- data = moment(
3161
- data[column.id]
3162
- ).format(
3163
- column.format
3164
- ? column.format
3165
- : 'DD-MM-YYYY hh:mm A'
3166
- );
2739
+ if (settings.length > 0) {
2740
+ newColumns.push({
2741
+ name: 'setting',
2742
+ header: 'Action',
2743
+ defaultWidth: 100,
2744
+ textAlign: 'center',
2745
+ textVerticalAlign:
2746
+ style && style.text_vertical_align
2747
+ ? style.text_vertical_align
2748
+ : 'center',
2749
+ render: ({ data }) => {
2750
+ return (
2751
+ <div className={styles.tdactions}>
2752
+ {settings.map((setting) =>
2753
+ renderSetting(setting, data)
2754
+ )}
2755
+ </div>
2756
+ );
2757
+ },
2758
+ });
2759
+ }
3167
2760
 
3168
- return <span>{data}</span>;
3169
- } else {
3170
- return null;
3171
- }
3172
- },
3173
- };
3174
- case 'dropdown':
3175
- columnId = Array.isArray(column.id)
3176
- ? column.id.join('-')
3177
- : column.id;
2761
+ setGridColumns(newColumns);
3178
2762
 
3179
- return {
3180
- ...commonProps,
3181
- name: columnId,
3182
- filterEditor: filterEditor,
3183
- filterEditorProps: filterEditorProps,
3184
- render: ({ data }) => {
3185
- return (
3186
- <select
3187
- name={column.id}
3188
- value={
3189
- data[column.id]?.id || ''
3190
- }
3191
- style={{ padding: '7px' }}
3192
- onChange={(e) => {
3193
- e.preventDefault();
3194
-
3195
- if (
3196
- (column.update?.key,
3197
- column.update?.url,
3198
- column.update?.method)
3199
- ) {
3200
- handleDropdownUpdate(
3201
- column.update.key,
3202
- `${column.update.url}/${data.id}`,
3203
- column.update
3204
- .method,
3205
- e.target.value
3206
- );
3207
- }
3208
- }}
3209
- >
3210
- <option value="">
3211
- Select{' '}
3212
- {/^[aeioAEIO]/.test(
3213
- column.label
3214
- )
3215
- ? 'an'
3216
- : 'a'}{' '}
3217
- {column.label}
3218
- </option>
3219
- {dropdownData[column.id] &&
3220
- dropdownData[column.id].length >
3221
- 0
3222
- ? dropdownData[
3223
- column.id
3224
- ].map((option, index) => (
3225
- <option
3226
- value={option.id}
3227
- key={index}
3228
- >
3229
- {option.label}
3230
- </option>
3231
- ))
3232
- : null}
3233
- </select>
3234
- );
3235
- },
3236
- };
3237
- case 'file':
3238
- return {
3239
- ...commonProps,
3240
- name: column.id,
3241
- sortable: false,
3242
- render: ({ data }) => {
3243
- if (data && data[column.id]) {
3244
- // Check if the data is an array of files
3245
- if (
3246
- Array.isArray(data[column.id])
3247
- ) {
3248
- // Return a span for each file in the array with added margin
3249
- return data[column.id].map(
3250
- (file, index) => (
3251
- <span
3252
- key={index} // Use index as a key; consider using unique ids if available
3253
- onClick={(e) => {
3254
- e.stopPropagation();
3255
- e.preventDefault();
3256
- handleDownload(
3257
- file
3258
- );
3259
- }}
3260
- style={{
3261
- marginRight:
3262
- '10px',
3263
- display:
3264
- 'inline-block',
3265
- }} // Added margin right and display style
3266
- >
3267
- <CloudDownload
3268
- data-tooltip-id={`system-tooltip`}
3269
- data-tooltip-content={`Download File ${file.file_name}`}
3270
- strokeWidth={2}
3271
- size={14}
3272
- className={
3273
- styles.tdaction
3274
- }
3275
- />
3276
- </span>
3277
- )
3278
- );
3279
- } else {
3280
- // Return a single span for a single file with added margin
3281
- return (
3282
- <span
3283
- onClick={(e) => {
3284
- e.stopPropagation();
3285
- e.preventDefault();
3286
- handleDownload(
3287
- data[column.id]
3288
- );
3289
- }}
3290
- style={{
3291
- marginRight: '10px',
3292
- display:
3293
- 'inline-block',
3294
- }} // Added margin right and display style for consistency
3295
- >
3296
- <CloudDownload
3297
- data-tooltip-id="system-tooltip"
3298
- data-tooltip-content={`Download File ${
3299
- data[column.id]
3300
- .file_name
3301
- }`}
3302
- strokeWidth={2}
3303
- size={14}
3304
- className={
3305
- styles.tdaction
3306
- }
3307
- />
3308
- </span>
3309
- );
3310
- }
3311
- } else {
3312
- return null;
3313
- }
3314
- },
3315
- };
3316
- case 'icons':
3317
- return {
3318
- ...commonProps,
3319
- name: column.id,
3320
- sortable: false,
3321
- render: ({ data }) => {
3322
- icons = [];
3323
-
3324
- if (
3325
- column.icons &&
3326
- column.icons.length > 0
3327
- ) {
3328
- column.icons.forEach((icon) => {
3329
- icons.push(
3330
- <img
3331
- src={icon.url}
3332
- alt={icon.name}
3333
- data-tooltip-id="system-tooltip"
3334
- data-tooltip-content={
3335
- icon.name
3336
- }
3337
- />
3338
- );
3339
- });
3340
- }
3341
-
3342
- if (data) {
3343
- return <span>{icons}</span>;
3344
- } else {
3345
- return null;
3346
- }
3347
- },
3348
- };
3349
- case 'image':
3350
- return {
3351
- ...commonProps,
3352
- name: column.id,
3353
- render: ({ data }) => {
3354
- if (data && data[column.id]) {
3355
- fileUrl =
3356
- data[column.id][0]?.file_url ||
3357
- data[column.id]?.file_url;
3358
-
3359
- if (fileUrl) {
3360
- return (
3361
- <span>
3362
- {parse(
3363
- `<img width="150" src="${fileUrl}" />`
3364
- )}
3365
- </span>
3366
- );
3367
- }
3368
- }
3369
-
3370
- return null;
3371
- },
3372
- };
3373
- case 'json':
3374
- return {
3375
- ...commonProps,
3376
- name: `${column.id}.${column.jsonData}`,
3377
- render: ({ data }) => {
3378
- if (
3379
- data &&
3380
- data[column.id] &&
3381
- data[column.id][column.jsonData]
3382
- ) {
3383
- data =
3384
- data[column.id][
3385
- column.jsonData
3386
- ];
3387
-
3388
- return (
3389
- <CellWithTooltip
3390
- value={data}
3391
- columnType="json"
3392
- >
3393
- <span>{data}</span>
3394
- </CellWithTooltip>
3395
- );
3396
- } else {
3397
- return null;
3398
- }
3399
- },
3400
- };
3401
- case 'number':
3402
- return {
3403
- ...commonProps,
3404
- name: column.id,
3405
- type: 'number',
3406
- };
3407
- case 'option':
3408
- return {
3409
- ...commonProps,
3410
- name: column.id,
3411
- filterEditor: filterEditor,
3412
- filterEditorProps: filterEditorProps,
3413
- render: ({ data }) => {
3414
- let newData;
3415
-
3416
- if (
3417
- data &&
3418
- column.options &&
3419
- column.options[data[column.id]]
3420
- ) {
3421
- if (
3422
- column.key &&
3423
- data[column.key] > 0 &&
3424
- column.options[
3425
- data[column.id]
3426
- ].includes('#')
3427
- ) {
3428
- newData = column.options[
3429
- data[column.id]
3430
- ].replace(
3431
- /#/g,
3432
- data[column.key]
3433
- );
3434
- } else {
3435
- newData =
3436
- column.options[
3437
- data[column.id]
3438
- ];
3439
- }
3440
-
3441
- return (
3442
- <CellWithTooltip
3443
- value={newData}
3444
- columnType="option"
3445
- >
3446
- <span>{newData}</span>
3447
- </CellWithTooltip>
3448
- );
3449
- } else {
3450
- return null;
3451
- }
3452
- },
3453
- };
3454
- case 'placeholder':
3455
- return {
3456
- ...commonProps,
3457
- name: column.id,
3458
- sortable: false,
3459
- render: ({ data }) => {
3460
- if (data) {
3461
- return (
3462
- <CellWithTooltip
3463
- value={column.placeholder}
3464
- columnType="placeholder"
3465
- >
3466
- <span>
3467
- {column.placeholder}
3468
- </span>
3469
- </CellWithTooltip>
3470
- );
3471
- } else {
3472
- return null;
3473
- }
3474
- },
3475
- };
3476
- case 'relation':
3477
- return {
3478
- ...commonProps,
3479
- name: relationName,
3480
- sortable: false,
3481
- filterEditor: filterEditor,
3482
- filterEditorProps: filterEditorProps,
3483
- render: ({ data }) => {
3484
- value = column.id.reduce((acc, id) => {
3485
- if (acc === '') {
3486
- if (data[id]) {
3487
- return data[id];
3488
- } else {
3489
- return '';
3490
- }
3491
- } else {
3492
- if (acc && acc[id]) {
3493
- return acc[id];
3494
- } else {
3495
- return '';
3496
- }
3497
- }
3498
- }, '');
3499
-
3500
- if (value) {
3501
- if (Array.isArray(value)) {
3502
- if (value[0]) {
3503
- value = value[0];
3504
- }
3505
- }
3506
-
3507
- if (
3508
- Array.isArray(column.nameFrom)
3509
- ) {
3510
- value = column.nameFrom
3511
- .map(
3512
- (nf) =>
3513
- value?.[nf] ?? ''
3514
- )
3515
- .join(' ');
3516
- } else {
3517
- if (
3518
- value &&
3519
- value.hasOwnProperty(
3520
- column.nameFrom
3521
- )
3522
- ) {
3523
- value =
3524
- value[column.nameFrom];
3525
- }
3526
- }
3527
-
3528
- if (
3529
- column.placeholder &&
3530
- column.placeholder !== ''
3531
- ) {
3532
- return (
3533
- <span>
3534
- {column.placeholder}
3535
- </span>
3536
- );
3537
- } else {
3538
- if (
3539
- column.hasOwnProperty(
3540
- 'format'
3541
- )
3542
- ) {
3543
- if (column.format) {
3544
- value =
3545
- moment(
3546
- value
3547
- ).format(
3548
- 'DD-MM-YYYY'
3549
- );
3550
- }
3551
- }
3552
-
3553
- if (
3554
- typeof value === 'string' ||
3555
- typeof value === 'number'
3556
- ) {
3557
- return (
3558
- <CellWithTooltip
3559
- value={value}
3560
- columnType="relation"
3561
- >
3562
- <span>{value}</span>
3563
- </CellWithTooltip>
3564
- );
3565
- } else {
3566
- return null;
3567
- }
3568
- }
3569
- } else {
3570
- return null;
3571
- }
3572
- },
3573
- };
3574
- case 'relationArray':
3575
- return {
3576
- ...commonProps,
3577
- name: relationName,
3578
- sortable: false,
3579
- filterEditor: filterEditor,
3580
- filterEditorProps: filterEditorProps,
3581
- render: ({ data }) => {
3582
- const relationValue = column.id.reduce(
3583
- (acc, id) =>
3584
- acc === ''
3585
- ? data[id]
3586
- : acc && acc[id]
3587
- ? acc[id]
3588
- : '',
3589
- ''
3590
- );
3591
-
3592
- if (
3593
- relationValue &&
3594
- Array.isArray(relationValue) &&
3595
- relationValue.length > 0
3596
- ) {
3597
- const nameProperties =
3598
- Array.isArray(column.nameFrom)
3599
- ? column.nameFrom
3600
- : [column.nameFrom];
3601
-
3602
- const values = relationValue
3603
- .map((rv) => {
3604
- if (
3605
- column.childId &&
3606
- Array.isArray(
3607
- column.childId
3608
- )
3609
- ) {
3610
- childValue = rv;
3611
-
3612
- column.childId.forEach(
3613
- (a) => {
3614
- if (
3615
- childValue[
3616
- a
3617
- ]
3618
- ) {
3619
- childValue =
3620
- childValue[
3621
- a
3622
- ];
3623
- }
3624
- }
3625
- );
3626
-
3627
- if (childValue) {
3628
- const propertyValues =
3629
- nameProperties
3630
- .map(
3631
- (
3632
- prop
3633
- ) =>
3634
- childValue[
3635
- prop
3636
- ]
3637
- )
3638
- .filter(
3639
- (
3640
- value
3641
- ) =>
3642
- value !==
3643
- undefined &&
3644
- value !==
3645
- null
3646
- );
3647
- return propertyValues.join(
3648
- ' '
3649
- );
3650
- }
3651
- } else {
3652
- const propertyValues =
3653
- nameProperties
3654
- .map(
3655
- (prop) =>
3656
- rv[prop]
3657
- )
3658
- .filter(
3659
- (value) =>
3660
- value !==
3661
- undefined &&
3662
- value !==
3663
- null
3664
- );
3665
- return propertyValues.join(
3666
- ' '
3667
- );
3668
- }
3669
- })
3670
- .filter(
3671
- (value) => value !== ''
3672
- );
3673
-
3674
- // if (values.length > 0) {
3675
- let result = '';
3676
-
3677
- if (
3678
- column.relation?.id &&
3679
- column.relation?.key
3680
- ) {
3681
- const relationValue =
3682
- column.relation.key.reduce(
3683
- (acc, id) =>
3684
- acc === ''
3685
- ? data[id]
3686
- : acc && acc[id]
3687
- ? acc[id]
3688
- : '',
3689
- ''
3690
- );
3691
-
3692
- result = `${
3693
- relationValue[
3694
- column.relation.id
3695
- ]
3696
- }<br />`;
3697
- }
3698
-
3699
- if (
3700
- column.where &&
3701
- column.where.id &&
3702
- column.where.value
3703
- ) {
3704
- if (
3705
- data[column.where.id] ===
3706
- column.where.value
3707
- ) {
3708
- result +=
3709
- values.join('<br />');
3710
- }
3711
- } else {
3712
- result += values.join('<br />');
3713
- }
3714
-
3715
- return (
3716
- <CellWithTooltip
3717
- value={result}
3718
- columnType="relationArray"
3719
- >
3720
- <span>{parse(result)}</span>
3721
- </CellWithTooltip>
3722
- );
3723
- // }
3724
- } else {
3725
- let result = '';
3726
-
3727
- if (
3728
- column.relation?.id &&
3729
- column.relation?.key
3730
- ) {
3731
- const relationValue =
3732
- column.relation.key.reduce(
3733
- (acc, id) =>
3734
- acc === ''
3735
- ? data[id]
3736
- : acc && acc[id]
3737
- ? acc[id]
3738
- : '',
3739
- ''
3740
- );
3741
-
3742
- result =
3743
- relationValue &&
3744
- relationValue[
3745
- column.relation.id
3746
- ]
3747
- ? relationValue[
3748
- column.relation.id
3749
- ]
3750
- : '';
3751
- }
3752
-
3753
- return (
3754
- <CellWithTooltip
3755
- value={result}
3756
- columnType="relationArray"
3757
- >
3758
- <span>{parse(result)}</span>
3759
- </CellWithTooltip>
3760
- );
3761
- }
3762
-
3763
- return null;
3764
- },
3765
- };
3766
- case 'richtext':
3767
- return {
3768
- ...commonProps,
3769
- name: column.id,
3770
- render: ({ data }) => {
3771
- if (data && data[column.id]) {
3772
- return (
3773
- <CellWithTooltip
3774
- value={data[column.id]}
3775
- columnType="richtext"
3776
- >
3777
- <span>
3778
- {parse(data[column.id])}
3779
- </span>
3780
- </CellWithTooltip>
3781
- );
3782
- } else {
3783
- return null;
3784
- }
3785
- },
3786
- };
3787
- case 'stage':
3788
- return {
3789
- ...commonProps,
3790
- name: column.id,
3791
- sortable: false,
3792
- render: ({ data }) => {
3793
- stage = '';
3794
-
3795
- column.keyIds.forEach((a, b) => {
3796
- if (data[a] === 1) {
3797
- stage = column.valueIds[b];
3798
- }
3799
- });
3800
-
3801
- return (
3802
- <CellWithTooltip
3803
- value={stage}
3804
- columnType="stage"
3805
- >
3806
- <span>{stage}</span>
3807
- </CellWithTooltip>
3808
- );
3809
- },
3810
- };
3811
- case 'time':
3812
- return {
3813
- ...commonProps,
3814
- name: column.id,
3815
- render: ({ data }) => {
3816
- if (
3817
- data &&
3818
- data[column.id] &&
3819
- moment(data[column.id]).isValid() &&
3820
- moment(data[column.id]).format(
3821
- 'YYYY-MM-DD'
3822
- ) !== '1970-01-01'
3823
- ) {
3824
- data = moment(
3825
- data[column.id]
3826
- ).format(
3827
- column.format
3828
- ? column.format
3829
- : 'hh:mm A'
3830
- );
3831
-
3832
- return (
3833
- <CellWithTooltip
3834
- value={data[column.id]}
3835
- columnType="time"
3836
- >
3837
- <span>{data}</span>
3838
- </CellWithTooltip>
3839
- );
3840
- } else {
3841
- return null;
3842
- }
3843
- },
3844
- };
3845
- case 'timer':
3846
- return {
3847
- ...commonProps,
3848
- name: column.id,
3849
- render: ({ data }) => {
3850
- if (
3851
- data &&
3852
- data[column.id] &&
3853
- moment(data[column.id]).isValid() &&
3854
- moment(data[column.id]).format(
3855
- 'YYYY-MM-DD'
3856
- ) !== '1970-01-01'
3857
- ) {
3858
- data = moment(
3859
- data[column.id]
3860
- ).format(
3861
- column.format
3862
- ? column.format
3863
- : 'DD-MM-YYYY hh:mm A'
3864
- );
3865
-
3866
- return (
3867
- <CellWithTooltip
3868
- value={data[column.id]}
3869
- columnType="timer"
3870
- >
3871
- <span>{data}</span>
3872
- </CellWithTooltip>
3873
- );
3874
- } else {
3875
- let shouldRenderButton = false;
3876
-
3877
- if (
3878
- !column.hasOwnProperty(
3879
- 'require'
3880
- ) ||
3881
- column.require.length === 0
3882
- ) {
3883
- shouldRenderButton = true;
3884
- } else {
3885
- for (const req of column.require) {
3886
- if (
3887
- req.hasOwnProperty('id')
3888
- ) {
3889
- const hasValue =
3890
- req.hasOwnProperty(
3891
- 'value'
3892
- );
3893
- if (
3894
- (hasValue &&
3895
- data[req.id] ===
3896
- req.value) ||
3897
- (!hasValue &&
3898
- data[req.id])
3899
- ) {
3900
- shouldRenderButton = true;
3901
- } else {
3902
- shouldRenderButton = false;
3903
- break;
3904
- }
3905
- }
3906
- }
3907
- }
3908
-
3909
- if (shouldRenderButton) {
3910
- return (
3911
- <button
3912
- className={
3913
- styles.clockButton
3914
- }
3915
- onClick={(e) => {
3916
- e.stopPropagation();
3917
- e.preventDefault();
3918
-
3919
- // Check if every required field in column.validate exists and is truthy in data
3920
- if (
3921
- column.validate &&
3922
- column.validate
3923
- .length > 0
3924
- ) {
3925
- const missingFields =
3926
- column.validate.filter(
3927
- (v) =>
3928
- !data[
3929
- v
3930
- ]
3931
- );
3932
-
3933
- if (
3934
- missingFields.length ===
3935
- 0
3936
- ) {
3937
- handleTimer(
3938
- data[
3939
- form
3940
- .primaryKey
3941
- ],
3942
- column.id,
3943
- data
3944
- );
3945
- } else {
3946
- // Format field names to be more readable
3947
- const formattedFields =
3948
- missingFields.map(
3949
- (
3950
- field
3951
- ) => {
3952
- // Try to find the column with this field ID to get its label
3953
- const fieldColumn =
3954
- columns.find(
3955
- (
3956
- col
3957
- ) =>
3958
- col.id ===
3959
- field
3960
- );
3961
- // Use the label if found, otherwise use the field ID
3962
- return (
3963
- fieldColumn?.label ||
3964
- field
3965
- );
3966
- }
3967
- );
3968
-
3969
- const fieldList =
3970
- formattedFields.join(
3971
- ', '
3972
- );
3973
- toast.error(
3974
- `Please complete the following required field${
3975
- missingFields.length >
3976
- 1
3977
- ? 's'
3978
- : ''
3979
- } before starting the timer: ${fieldList}`
3980
- );
3981
- }
3982
- } else {
3983
- handleTimer(
3984
- data[
3985
- form
3986
- .primaryKey
3987
- ],
3988
- column.id,
3989
- data
3990
- );
3991
- }
3992
- }}
3993
- >
3994
- <Alarm
3995
- data-tooltip-id="system-tooltip"
3996
- data-tooltip-content={`${column.label} Timer`}
3997
- strokeWidth={2}
3998
- size={14}
3999
- style={{
4000
- color: 'white',
4001
- }}
4002
- />
4003
- </button>
4004
- );
4005
- }
4006
-
4007
- // Return a placeholder div to maintain consistent row height
4008
- return (
4009
- <div
4010
- style={{
4011
- width: '28px',
4012
- height: '28px',
4013
- display: 'flex',
4014
- alignItems: 'center',
4015
- justifyContent:
4016
- 'center',
4017
- margin: '0 auto',
4018
- }}
4019
- >
4020
- {/* Empty placeholder to maintain layout */}
4021
- </div>
4022
- );
4023
- }
4024
- },
4025
- };
4026
- case 'stageCounter':
4027
- return {
4028
- ...commonProps,
4029
- name: column.id,
4030
- sortable: false,
4031
- render: ({ data }) => {
4032
- if (
4033
- data &&
4034
- column.stageConfig &&
4035
- column.stageConfig.key
4036
- ) {
4037
- // Check if the data has the specified key and it's an array
4038
- const stageData =
4039
- data[column.stageConfig.key];
4040
-
4041
- if (
4042
- stageData &&
4043
- Array.isArray(stageData)
4044
- ) {
4045
- const stageCount =
4046
- stageData.length;
4047
-
4048
- // Create a clickable element if link is provided
4049
- const handleClick = (e) => {
4050
- e.stopPropagation();
4051
- e.preventDefault();
4052
-
4053
- if (
4054
- column.link &&
4055
- column.link.url &&
4056
- column.link.key
4057
- ) {
4058
- navigate(
4059
- `${
4060
- column.link.url
4061
- }${
4062
- data[
4063
- column.link
4064
- .key
4065
- ]
4066
- }`
4067
- );
4068
- }
4069
- };
4070
-
4071
- // Create an array of squares based on the stage data
4072
- const squares = [];
4073
- for (
4074
- let i = 0;
4075
- i < stageCount;
4076
- i++
4077
- ) {
4078
- // Get the stage item
4079
- const stageItem =
4080
- stageData[i];
4081
-
4082
- // Get the label value from the stage item using stageLabelKey if provided
4083
- let labelValue = '';
4084
- if (
4085
- column.stageConfig
4086
- .stageLabelKey &&
4087
- stageItem &&
4088
- stageItem[
4089
- column.stageConfig
4090
- .stageLabelKey
4091
- ] !== undefined
4092
- ) {
4093
- // Get the value from the stage item
4094
- const rawValue =
4095
- stageItem[
4096
- column
4097
- .stageConfig
4098
- .stageLabelKey
4099
- ];
4100
-
4101
- // If it's an object with a name property, use that
4102
- if (
4103
- rawValue &&
4104
- typeof rawValue ===
4105
- 'object' &&
4106
- rawValue.name
4107
- ) {
4108
- labelValue =
4109
- rawValue.name.charAt(
4110
- 0
4111
- );
4112
- }
4113
- // If it's a string, use the first character
4114
- else if (
4115
- typeof rawValue ===
4116
- 'string' &&
4117
- rawValue.length > 0
4118
- ) {
4119
- labelValue =
4120
- rawValue.charAt(
4121
- 0
4122
- );
4123
- }
4124
- // Otherwise use the raw value if it can be converted to string
4125
- else if (
4126
- rawValue !== null &&
4127
- rawValue !==
4128
- undefined
4129
- ) {
4130
- labelValue =
4131
- String(
4132
- rawValue
4133
- ).charAt(0);
4134
- }
4135
- }
4136
- // If no stageLabelKey is provided or no value is found, leave it blank
4137
-
4138
- // Create tooltip content from the full value if available
4139
- let tooltipContent = `Stage ${
4140
- i + 1
4141
- }`;
4142
- if (
4143
- column.stageConfig
4144
- .stageLabelKey &&
4145
- stageItem &&
4146
- stageItem[
4147
- column.stageConfig
4148
- .stageLabelKey
4149
- ]
4150
- ) {
4151
- const rawValue =
4152
- stageItem[
4153
- column
4154
- .stageConfig
4155
- .stageLabelKey
4156
- ];
4157
- if (
4158
- rawValue &&
4159
- typeof rawValue ===
4160
- 'object' &&
4161
- rawValue.name
4162
- ) {
4163
- tooltipContent = `${
4164
- rawValue.name
4165
- } (Stage ${i + 1})`;
4166
- } else if (
4167
- rawValue !== null &&
4168
- rawValue !==
4169
- undefined
4170
- ) {
4171
- tooltipContent = `Stage ${
4172
- i + 1
4173
- })`;
4174
- }
4175
- }
4176
-
4177
- // Determine the background color based on stageColour configuration
4178
- let backgroundColor =
4179
- 'var(--primary-color, #4a90e2)';
4180
-
4181
- if (
4182
- column.stageConfig
4183
- .stageColour &&
4184
- column.stageConfig
4185
- .stageColour.key &&
4186
- column.stageConfig
4187
- .stageColour
4188
- .options &&
4189
- stageItem
4190
- ) {
4191
- const colourKey =
4192
- column.stageConfig
4193
- .stageColour
4194
- .key;
4195
- const colourOptions =
4196
- column.stageConfig
4197
- .stageColour
4198
- .options;
4199
-
4200
- // Get the value for the color key
4201
- if (
4202
- stageItem[
4203
- colourKey
4204
- ] !== undefined
4205
- ) {
4206
- const keyValue =
4207
- stageItem[
4208
- colourKey
4209
- ];
4210
-
4211
- // Find matching option
4212
- const matchingOption =
4213
- colourOptions.find(
4214
- (
4215
- option
4216
- ) => {
4217
- // Handle both direct value and object with id
4218
- if (
4219
- typeof keyValue ===
4220
- 'object' &&
4221
- keyValue !==
4222
- null
4223
- ) {
4224
- // Use loose equality (==) to handle type coercion between string and number
4225
- return (
4226
- keyValue.id ==
4227
- option.id
4228
- );
4229
- }
4230
- // Use loose equality (==) to handle type coercion between string and number
4231
- return (
4232
- keyValue ==
4233
- option.id
4234
- );
4235
- }
4236
- );
4237
-
4238
- // Use the color from the matching option if found
4239
- if (
4240
- matchingOption &&
4241
- matchingOption.colour
4242
- ) {
4243
- backgroundColor =
4244
- matchingOption.colour;
4245
- }
4246
- }
4247
- }
4248
-
4249
- squares.push(
4250
- <div
4251
- key={i}
4252
- style={{
4253
- width: '20px',
4254
- height: '20px',
4255
- backgroundColor:
4256
- backgroundColor,
4257
- borderRadius:
4258
- '3px',
4259
- margin: '0 2px',
4260
- display: 'flex',
4261
- justifyContent:
4262
- 'center',
4263
- alignItems:
4264
- 'center',
4265
- color: 'white',
4266
- fontSize:
4267
- '11px',
4268
- fontWeight:
4269
- 'bold',
4270
- }}
4271
- data-tooltip-id="system-tooltip"
4272
- data-tooltip-content={
4273
- tooltipContent
4274
- }
4275
- >
4276
- {labelValue}
4277
- </div>
4278
- );
4279
- }
4280
-
4281
- return (
4282
- <div
4283
- onClick={handleClick}
4284
- style={{
4285
- display: 'flex',
4286
- flexWrap: 'wrap',
4287
- justifyContent:
4288
- 'center',
4289
- alignItems:
4290
- 'center',
4291
- cursor: column.link
4292
- ? 'pointer'
4293
- : 'default',
4294
- gap: '4px',
4295
- padding: '2px 0',
4296
- maxWidth: '100%',
4297
- }}
4298
- >
4299
- {squares}
4300
- </div>
4301
- );
4302
- }
4303
- }
4304
- return null;
4305
- },
4306
- };
4307
- case 'url':
4308
- return {
4309
- ...commonProps,
4310
- name: column.id,
4311
- render: ({ data }) => {
4312
- if (data && data[column.id]) {
4313
- return (
4314
- <CellWithTooltip
4315
- value={data[column.id]}
4316
- columnType="url"
4317
- >
4318
- <span>
4319
- <a
4320
- href={
4321
- data[column.id]
4322
- }
4323
- target="_blank"
4324
- >
4325
- Link
4326
- </a>
4327
- </span>
4328
- </CellWithTooltip>
4329
- );
4330
- } else {
4331
- return null;
4332
- }
4333
- },
4334
- };
4335
- case 'input_text':
4336
- return {
4337
- ...commonProps,
4338
- name: column.id,
4339
- render: ({ data }) => {
4340
- const inputKey = `${data.id}-${column.id}`;
4341
-
4342
- // Use the ref value first, then fall back to data value
4343
- const currentValue =
4344
- localInputValuesRef.current[
4345
- inputKey
4346
- ] !== undefined
4347
- ? localInputValuesRef.current[
4348
- inputKey
4349
- ]
4350
- : data[column.id] || '';
4351
-
4352
- return (
4353
- <input
4354
- type="text"
4355
- name={column.id}
4356
- value={currentValue}
4357
- style={{
4358
- padding: '7px',
4359
- width: '100%',
4360
- }}
4361
- onChange={(e) => {
4362
- const newValue =
4363
- e.target.value;
4364
-
4365
- // Update both state and ref
4366
- localInputValuesRef.current =
4367
- {
4368
- ...localInputValuesRef.current,
4369
- [inputKey]:
4370
- newValue,
4371
- };
4372
-
4373
- setLocalInputValues(
4374
- (prev) => ({
4375
- ...prev,
4376
- [inputKey]:
4377
- newValue,
4378
- })
4379
- );
4380
-
4381
- if (
4382
- column.update?.key &&
4383
- column.update?.url &&
4384
- column.update?.method
4385
- ) {
4386
- handleInputUpdate(
4387
- column.update.key,
4388
- `${column.update.url}/${data.id}`,
4389
- column.update
4390
- .method,
4391
- newValue,
4392
- inputKey
4393
- );
4394
- }
4395
- }}
4396
- onBlur={() => {
4397
- if (
4398
- column.update?.key &&
4399
- column.update?.url &&
4400
- column.update?.method
4401
- ) {
4402
- handleInputUpdate.flush();
4403
- }
4404
- }}
4405
- />
4406
- );
4407
- },
4408
- };
4409
- default:
4410
- columnId = Array.isArray(column.id)
4411
- ? column.id.join('-')
4412
- : column.id;
4413
-
4414
- return {
4415
- ...commonProps,
4416
- name: columnId,
4417
- filterEditor: filterEditor,
4418
- filterEditorProps: filterEditorProps,
4419
- render: ({ data }) => {
4420
- if (
4421
- data &&
4422
- data[column.id] &&
4423
- data[column.id] !== ''
4424
- ) {
4425
- let content = String(
4426
- data[column.id]
4427
- );
4428
-
4429
- // If removeFormat is true, strip HTML tags
4430
- if (column.removeFormat) {
4431
- content = content.replace(
4432
- /<\/?[^>]+(>|$)/g,
4433
- ''
4434
- ); // Simple regex to strip HTML tags
4435
- } else {
4436
- // Replace p tags with br tags
4437
- content = content
4438
- .replace(/<p>/g, '')
4439
- .replace(/<\/p>/g, '<br>');
4440
-
4441
- // Ensure <br /> tags are properly handled (convert to <br> for consistency)
4442
- content = content
4443
- .replace(/<br \/>/g, '<br>')
4444
- .replace(/<br\/>/g, '<br>');
4445
- }
4446
-
4447
- // If truncate has a value, limit content to specified number of characters
4448
- if (column.truncate) {
4449
- content =
4450
- content.length >
4451
- column.truncate
4452
- ? content.substring(
4453
- 0,
4454
- column.truncate
4455
- ) + '...'
4456
- : content;
4457
- }
4458
-
4459
- return (
4460
- <CellWithTooltip
4461
- value={data[column.id]}
4462
- columnType="text"
4463
- >
4464
- <span>
4465
- {parse(content)}
4466
- </span>
4467
- </CellWithTooltip>
4468
- );
4469
- } else {
4470
- return null;
4471
- }
4472
- },
4473
- };
4474
- }
4475
- };
4476
-
4477
- const newColumns = columns.map(renderColumn);
4478
-
4479
- if (settings.length > 0) {
4480
- newColumns.push({
4481
- name: 'setting',
4482
- header: 'Action',
4483
- defaultWidth: 100,
4484
- textAlign: 'center',
4485
- textVerticalAlign:
4486
- style && style.text_vertical_align
4487
- ? style.text_vertical_align
4488
- : 'center',
4489
- render: ({ data }) => {
4490
- return (
4491
- <div className={styles.tdactions}>
4492
- {settings.map((setting) =>
4493
- renderSetting(setting, data)
4494
- )}
4495
- </div>
4496
- );
4497
- },
4498
- });
4499
- }
4500
-
4501
- setGridColumns(newColumns);
4502
-
4503
- const newFilterValue = columns
4504
- .filter((column) => column.filter && column.filter.type) // Only consider columns with a filter type
4505
- .map((column) => {
4506
- // Map these filtered columns to a new object array
4507
- let filterName = '';
2763
+ const newFilterValue = columns
2764
+ .filter((column) => column.filter && column.filter.type) // Only consider columns with a filter type
2765
+ .map((column) => {
2766
+ // Map these filtered columns to a new object array
2767
+ let filterName = '';
4508
2768
 
4509
2769
  if (Array.isArray(column.id)) {
4510
2770
  filterName = column.id.reduce(
@@ -4767,16 +3027,36 @@ const DataGrid = forwardRef(
4767
3027
  <>
4768
3028
  {/* Top container with search, action buttons, and auto-refresh */}
4769
3029
  {/* Only show top container if there are controls to display */}
4770
- {(renderSearch() ||
3030
+ {(!form.searchDisable ||
3031
+ !form.createDisable ||
4771
3032
  (!shouldRelocateAutoRefresh() &&
4772
- renderAutoRefreshControls())) && (
3033
+ ajaxSetting &&
3034
+ ajaxSetting.autoRefresh !== undefined)) && (
4773
3035
  <div className={styles.dataGridTopContainer}>
4774
3036
  <div className={styles.dataGridTopLeftContainer}>
4775
- {renderSearch()}
3037
+ <DataGridSearch
3038
+ form={form}
3039
+ search={search}
3040
+ onSearchChange={handleChangeSearch}
3041
+ onModalOpen={modalOpen}
3042
+ dataRef={dataRef}
3043
+ filterValue={filterValue}
3044
+ paramValue={paramValue}
3045
+ pageData={pageData}
3046
+ pageSetting={pageSetting}
3047
+ />
4776
3048
  </div>
4777
3049
  <div className={styles.dataGridTopRightContainer}>
4778
- {!shouldRelocateAutoRefresh() &&
4779
- renderAutoRefreshControls()}
3050
+ {!shouldRelocateAutoRefresh() && (
3051
+ <AutoRefreshControls
3052
+ ajaxSetting={ajaxSetting}
3053
+ autoRefreshEnabled={autoRefreshEnabled}
3054
+ setAutoRefreshEnabled={
3055
+ setAutoRefreshEnabled
3056
+ }
3057
+ refreshInterval={refreshInterval}
3058
+ />
3059
+ )}
4780
3060
  </div>
4781
3061
  </div>
4782
3062
  )}
@@ -4907,64 +3187,19 @@ const DataGrid = forwardRef(
4907
3187
 
4908
3188
  return (
4909
3189
  <div
4910
- className="group-header-clean group-header-fixed-height"
3190
+ className={`group-header-clean group-header-fixed-height ${styles.groupHeaderContainer}`}
4911
3191
  onClick={
4912
3192
  handleGroupHeaderClick
4913
3193
  }
4914
- style={{
4915
- display: 'flex',
4916
- alignItems: 'center',
4917
- justifyContent:
4918
- 'space-between',
4919
- width: '100%',
4920
- height: '34px', // Compact height
4921
- padding: '6px 12px', // Reduced padding for compact design
4922
- background:
4923
- 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
4924
- borderLeft:
4925
- '4px solid var(--primary-color, #3b82f6)',
4926
- position: 'relative',
4927
- overflow: 'hidden',
4928
- boxShadow:
4929
- '0 1px 3px rgba(0,0,0,0.1)',
4930
- borderRadius:
4931
- '0 4px 4px 0',
4932
- cursor: 'pointer',
4933
- boxSizing: 'border-box',
4934
- lineHeight: '1.1', // Compact line height
4935
- flexShrink: 0,
4936
- transition:
4937
- 'background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease',
4938
- }}
4939
- onMouseEnter={(e) => {
4940
- e.currentTarget.style.background =
4941
- 'linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%)';
4942
- e.currentTarget.style.boxShadow =
4943
- '0 2px 8px rgba(0,0,0,0.15)';
4944
- e.currentTarget.style.transform =
4945
- 'translateY(-1px)';
4946
- }}
4947
- onMouseLeave={(e) => {
4948
- e.currentTarget.style.background =
4949
- 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)';
4950
- e.currentTarget.style.boxShadow =
4951
- '0 1px 3px rgba(0,0,0,0.1)';
4952
- e.currentTarget.style.transform =
4953
- 'translateY(0)';
4954
- }}
4955
3194
  >
4956
3195
  {/* Left side - Group info and expand/collapse */}
4957
3196
  <div
4958
- style={{
4959
- display: 'flex',
4960
- alignItems: 'center',
4961
- flex: 1,
4962
- zIndex: 2,
4963
- position: 'relative',
4964
- }}
3197
+ className={
3198
+ styles.groupActionContainer
3199
+ }
4965
3200
  >
4966
3201
  <button
4967
- className="group-expand-btn"
3202
+ className={`group-expand-btn ${styles.groupExpandBtn}`}
4968
3203
  onClick={(e) => {
4969
3204
  e.stopPropagation();
4970
3205
  if (
@@ -4972,110 +3207,165 @@ const DataGrid = forwardRef(
4972
3207
  'function'
4973
3208
  ) {
4974
3209
  toggleGroup();
4975
- } else {
4976
- console.warn(
4977
- 'toggleGroup is not a function:',
4978
- toggleGroup
4979
- );
4980
3210
  }
4981
3211
  }}
4982
- style={{
4983
- background:
4984
- 'var(--primary-color, #3b82f6)',
4985
- border: 'none',
4986
- borderRadius:
4987
- '3px',
4988
- color: 'white',
4989
- width: '20px', // More compact
4990
- height: '20px', // More compact
4991
- display: 'flex',
4992
- alignItems:
4993
- 'center',
4994
- justifyContent:
4995
- 'center',
4996
- marginRight:
4997
- '8px', // Reduced margin
4998
- cursor: 'pointer',
4999
- fontSize: '11px', // Smaller font
5000
- fontWeight:
5001
- 'bold',
5002
- boxShadow:
5003
- '0 1px 2px rgba(0,0,0,0.1)',
5004
- transition:
5005
- 'all 0.15s ease',
5006
- }}
5007
- onMouseEnter={(e) => {
5008
- e.target.style.transform =
5009
- 'scale(1.05)';
5010
- e.target.style.background =
5011
- 'var(--secondary-color, #2563eb)';
5012
- }}
5013
- onMouseLeave={(e) => {
5014
- e.target.style.transform =
5015
- 'scale(1)';
5016
- e.target.style.background =
5017
- 'var(--primary-color, #3b82f6)';
3212
+ onTouchStart={(e) => {
3213
+ e.stopPropagation();
3214
+ handleTouchStart(
3215
+ e,
3216
+ 'group-expand',
3217
+ collapsed
3218
+ ? 'Expand group'
3219
+ : 'Collapse group'
3220
+ );
5018
3221
  }}
3222
+ onTouchEnd={
3223
+ handleTouchEnd
3224
+ }
3225
+ onTouchMove={
3226
+ handleTouchMove
3227
+ }
3228
+ data-tooltip-id="system-tooltip"
3229
+ data-tooltip-content={
3230
+ collapsed
3231
+ ? 'Expand group'
3232
+ : 'Collapse group'
3233
+ }
5019
3234
  >
5020
3235
  {collapsed
5021
3236
  ? '+'
5022
3237
  : '−'}
5023
3238
  </button>
3239
+ {/* Dynamic group action buttons */}
3240
+ {ajaxSetting?.groupBySetting?.icons?.map(
3241
+ (
3242
+ iconConfig,
3243
+ index
3244
+ ) => {
3245
+ // Get the icon component based on the id
3246
+ const getIconComponent =
3247
+ (iconId) => {
3248
+ switch (
3249
+ iconId
3250
+ ) {
3251
+ case 'clock':
3252
+ return Clock;
3253
+ case 'alarm':
3254
+ return Alarm;
3255
+ case 'check':
3256
+ return Check;
3257
+ case 'edit':
3258
+ return Edit;
3259
+ case 'envelope':
3260
+ return Envelope;
3261
+ case 'file':
3262
+ return File;
3263
+ case 'image':
3264
+ return Image;
3265
+ case 'copy':
3266
+ return Copy;
3267
+ case 'cloudUpload':
3268
+ return CloudDownload;
3269
+ case 'cloudDownload':
3270
+ return CloudDownload;
3271
+ default:
3272
+ return Clock; // Default fallback
3273
+ }
3274
+ };
3275
+
3276
+ // Check if this icon should be shown based on show conditions
3277
+ if (
3278
+ !shouldShowGroupAction(
3279
+ iconConfig,
3280
+ groupValue
3281
+ )
3282
+ ) {
3283
+ return null;
3284
+ }
3285
+
3286
+ const IconComponent =
3287
+ getIconComponent(
3288
+ iconConfig.id
3289
+ );
3290
+
3291
+ // Generate different color classes for multiple buttons
3292
+ const colorClasses =
3293
+ [
3294
+ 'success',
3295
+ 'warning',
3296
+ 'info',
3297
+ 'purple',
3298
+ 'pink',
3299
+ ];
3300
+ const colorClass =
3301
+ colorClasses[
3302
+ index %
3303
+ colorClasses.length
3304
+ ];
3305
+
3306
+ return (
3307
+ <button
3308
+ key={`group-action-${index}`}
3309
+ className={`group-action-btn group-${iconConfig.id}-btn ${styles.groupActionBtn} ${styles[colorClass]}`}
3310
+ onClick={(
3311
+ e
3312
+ ) => {
3313
+ e.stopPropagation();
3314
+ handleGroupAction(
3315
+ groupValue,
3316
+ iconConfig
3317
+ );
3318
+ }}
3319
+ onTouchStart={(
3320
+ e
3321
+ ) => {
3322
+ e.stopPropagation();
3323
+ handleTouchStart(
3324
+ e,
3325
+ `group-${iconConfig.id}-${index}`,
3326
+ iconConfig.label
3327
+ );
3328
+ }}
3329
+ onTouchEnd={
3330
+ handleTouchEnd
3331
+ }
3332
+ onTouchMove={
3333
+ handleTouchMove
3334
+ }
3335
+ data-tooltip-id="system-tooltip"
3336
+ data-tooltip-content={
3337
+ iconConfig.label
3338
+ }
3339
+ >
3340
+ <IconComponent
3341
+ strokeWidth={
3342
+ 2
3343
+ }
3344
+ size={
3345
+ 14
3346
+ }
3347
+ />
3348
+ </button>
3349
+ );
3350
+ }
3351
+ ) || null}
5024
3352
  <div
5025
- style={{
5026
- display: 'flex',
5027
- alignItems:
5028
- 'center', // Changed to center for compact layout
5029
- gap: '8px', // Add gap between elements
5030
- }}
3353
+ className={
3354
+ styles.groupInfo
3355
+ }
5031
3356
  >
5032
3357
  <strong
5033
- style={{
5034
- fontSize:
5035
- '0.85rem', // Slightly smaller for compact design
5036
- color: 'var(--secondary-color, #1f2937)',
5037
- fontWeight:
5038
- '600',
5039
- textTransform:
5040
- 'uppercase',
5041
- letterSpacing:
5042
- '0.025em',
5043
- }}
3358
+ className={
3359
+ styles.groupValue
3360
+ }
5044
3361
  >
5045
3362
  {groupValue}
5046
3363
  </strong>
5047
- {groupCount > 0 && (
5048
- <span
5049
- style={{
5050
- color: '#6b7280',
5051
- fontSize:
5052
- '0.75rem', // Smaller for compact design
5053
- fontWeight:
5054
- '500',
5055
- }}
5056
- >
5057
- ({groupCount}{' '}
5058
- {groupCount ===
5059
- 1
5060
- ? 'item'
5061
- : 'items'}
5062
- )
5063
- </span>
5064
- )}
5065
3364
  {/* Click indicator */}
5066
3365
  <span
5067
- style={{
5068
- color: '#9ca3af',
5069
- fontSize:
5070
- '0.7rem',
5071
- fontStyle:
5072
- 'italic',
5073
- opacity: 0.8,
5074
- marginLeft:
5075
- 'auto',
5076
- userSelect:
5077
- 'none',
5078
- }}
3366
+ className={
3367
+ styles.groupClickIndicator
3368
+ }
5079
3369
  >
5080
3370
  Click to
5081
3371
  select/unselect
@@ -5084,93 +3374,11 @@ const DataGrid = forwardRef(
5084
3374
  </div>
5085
3375
  </div>
5086
3376
 
5087
- {/* Right side - Group selection */}
5088
- {hasCheckboxColumn && (
5089
- <div
5090
- style={{
5091
- display: 'flex',
5092
- alignItems:
5093
- 'center',
5094
- gap: '4px', // Reduced gap for compact design
5095
- fontSize:
5096
- '0.75rem', // Smaller font for compact design
5097
- background:
5098
- 'rgba(255, 255, 255, 0.9)',
5099
- padding:
5100
- '3px 8px', // Reduced padding for compact design
5101
- borderRadius:
5102
- '12px', // Smaller border radius
5103
- border: '1px solid rgba(59, 130, 246, 0.3)',
5104
- zIndex: 2,
5105
- position:
5106
- 'relative',
5107
- boxShadow:
5108
- '0 1px 2px rgba(0,0,0,0.05)',
5109
- }}
5110
- >
5111
- <input
5112
- type="checkbox"
5113
- onChange={(e) => {
5114
- e.stopPropagation();
5115
- handleGroupSelectionChange(
5116
- groupValue,
5117
- e.target
5118
- .checked
5119
- );
5120
- }}
5121
- checked={isGroupSelected(
5122
- groupValue
5123
- )}
5124
- style={{
5125
- width: '14px', // Smaller checkbox for compact design
5126
- height: '14px',
5127
- cursor: 'pointer',
5128
- accentColor:
5129
- 'var(--primary-color, #3b82f6)',
5130
- }}
5131
- />
5132
- <span
5133
- style={{
5134
- cursor: 'pointer',
5135
- userSelect:
5136
- 'none',
5137
- color: 'var(--secondary-color, #1f2937)',
5138
- fontWeight:
5139
- '500',
5140
- fontSize:
5141
- '0.7rem', // Smaller font for compact design
5142
- }}
5143
- onClick={(e) => {
5144
- e.stopPropagation();
5145
- const checkbox =
5146
- e.target
5147
- .previousElementSibling;
5148
- checkbox.checked =
5149
- !checkbox.checked;
5150
- handleGroupSelectionChange(
5151
- groupValue,
5152
- checkbox.checked
5153
- );
5154
- }}
5155
- >
5156
- Select All
5157
- </span>
5158
- </div>
5159
- )}
5160
-
5161
3377
  {/* Background pattern */}
5162
3378
  <div
5163
- style={{
5164
- position: 'absolute',
5165
- top: 0,
5166
- left: 0,
5167
- right: 0,
5168
- bottom: 0,
5169
- background:
5170
- 'repeating-linear-gradient(45deg, transparent, transparent 2px, rgba(59, 130, 246, 0.03) 2px, rgba(59, 130, 246, 0.03) 4px)',
5171
- zIndex: 1,
5172
- pointerEvents: 'none',
5173
- }}
3379
+ className={
3380
+ styles.groupHeaderPattern
3381
+ }
5174
3382
  />
5175
3383
  </div>
5176
3384
  );
@@ -5319,13 +3527,19 @@ const DataGrid = forwardRef(
5319
3527
  </div>
5320
3528
 
5321
3529
  {/* Relocated auto refresh control when it's the only control */}
5322
- {/* {shouldRelocateAutoRefresh() && (
3530
+ {shouldRelocateAutoRefresh() && (
5323
3531
  <div className={styles.dataGridBottomContainer}>
5324
- {renderAutoRefreshControls(true)}
3532
+ <AutoRefreshControls
3533
+ ajaxSetting={ajaxSetting}
3534
+ autoRefreshEnabled={autoRefreshEnabled}
3535
+ setAutoRefreshEnabled={setAutoRefreshEnabled}
3536
+ refreshInterval={refreshInterval}
3537
+ isRelocated={true}
3538
+ />
5325
3539
  </div>
5326
- )} */}
3540
+ )}
5327
3541
 
5328
- {audioSource && <audio src={audioSource} ref={audioRef} />}
3542
+ <AudioPlayer audioSource={audioSource} />
5329
3543
  <Popup
5330
3544
  open={modalShow}
5331
3545
  onClose={modalClose}
@@ -5373,231 +3587,38 @@ const DataGrid = forwardRef(
5373
3587
  </div>
5374
3588
  </div>
5375
3589
  </Popup>
5376
- <Popup
5377
- open={modalGalleryShow}
5378
- onClose={modalGalleryClose}
5379
- closeOnDocumentClick={false}
5380
- contentStyle={{
5381
- width: windowWidth < 768 ? '95vw' : '80vw',
5382
- minWidth: windowWidth < 768 ? '95vw' : '600px',
5383
- maxWidth: '1200px',
5384
- }}
5385
- overlayStyle={{ background: 'rgba(0, 0, 0, 0.7)' }}
5386
- >
3590
+ <GalleryModal
3591
+ modalGalleryShow={modalGalleryShow}
3592
+ modalGalleryClose={modalGalleryClose}
3593
+ galleryData={galleryData}
3594
+ setGalleryData={setGalleryData}
3595
+ currentItem={currentItem}
3596
+ form={form}
3597
+ lightboxOpen={lightboxOpen}
3598
+ setLightboxOpen={setLightboxOpen}
3599
+ lightboxIndex={lightboxIndex}
3600
+ setLightboxIndex={setLightboxIndex}
3601
+ />
3602
+
3603
+ {/* Touch Tooltip Overlay */}
3604
+ {activeTooltip && (
5387
3605
  <div
5388
- className={`${styles.modalwrap} ${styles['top--modal']}`}
3606
+ className={styles.touchTooltip}
3607
+ style={{
3608
+ position: 'fixed',
3609
+ left: `${activeTooltip.x}px`,
3610
+ top: `${activeTooltip.y - 50}px`,
3611
+ transform: 'translateX(-50%)', // Center horizontally on button
3612
+ zIndex: 9999,
3613
+ pointerEvents: 'none',
3614
+ }}
3615
+ onClick={closeTooltip}
5389
3616
  >
5390
- <div className={styles.modal}>
5391
- <div className={styles.modal__header}>
5392
- <h1>Gallery</h1>
5393
- <button
5394
- className={styles.modal__close}
5395
- onClick={modalGalleryClose}
5396
- >
5397
- <CircleX strokeWidth={1} size={24} />
5398
- </button>
5399
- </div>
5400
- <div
5401
- className={`${styles.modal__content} ${styles['modal__content--gallery']}`}
5402
- >
5403
- {currentItem?.settings?.upload && (
5404
- <div className={styles.uploadSection}>
5405
- <h3>
5406
- <Image
5407
- strokeWidth={1.5}
5408
- size={18}
5409
- />
5410
- Upload New Images
5411
- </h3>
5412
- <div
5413
- className={styles.dropzoneContainer}
5414
- >
5415
- {isUploading && (
5416
- <div
5417
- className={
5418
- styles.progressBar
5419
- }
5420
- >
5421
- <div
5422
- className={
5423
- styles.progressBarInner
5424
- }
5425
- style={{
5426
- width: `${uploadProgress}%`,
5427
- }}
5428
- ></div>
5429
- </div>
5430
- )}
5431
- <div
5432
- className={styles.dropzone}
5433
- onClick={() =>
5434
- document
5435
- .getElementById(
5436
- 'gallery-file-upload'
5437
- )
5438
- .click()
5439
- }
5440
- onDragOver={(e) => {
5441
- e.preventDefault();
5442
- e.stopPropagation();
5443
- }}
5444
- onDragEnter={(e) => {
5445
- e.preventDefault();
5446
- e.stopPropagation();
5447
- }}
5448
- onDragLeave={(e) => {
5449
- e.preventDefault();
5450
- e.stopPropagation();
5451
- }}
5452
- onDrop={(e) => {
5453
- e.preventDefault();
5454
- e.stopPropagation();
5455
- const files =
5456
- e.dataTransfer.files;
5457
- if (files.length > 0) {
5458
- handleFileUpload(files);
5459
- }
5460
- }}
5461
- >
5462
- <CloudUpload
5463
- strokeWidth={1.5}
5464
- size={24}
5465
- />
5466
- <p>
5467
- Drag and drop files here or
5468
- click to select files
5469
- </p>
5470
- <input
5471
- id="gallery-file-upload"
5472
- type="file"
5473
- multiple
5474
- style={{ display: 'none' }}
5475
- onChange={(e) => {
5476
- if (
5477
- e.target.files
5478
- .length > 0
5479
- ) {
5480
- handleFileUpload(
5481
- e.target.files
5482
- );
5483
- e.target.value =
5484
- null; // Reset input
5485
- }
5486
- }}
5487
- accept="image/*"
5488
- />
5489
- </div>
5490
- </div>
5491
- </div>
5492
- )}
5493
- <div className={styles.formcontainer}>
5494
- {galleryData.length > 0 ? (
5495
- <div
5496
- className={styles.galleryContainer}
5497
- >
5498
- <div
5499
- className={
5500
- styles.galleryInstructions
5501
- }
5502
- >
5503
- <Image
5504
- strokeWidth={1.5}
5505
- size={16}
5506
- />
5507
- <span>
5508
- Tap any image to enlarge
5509
- </span>
5510
- </div>
5511
- <div
5512
- className={
5513
- styles.customGalleryGrid
5514
- }
5515
- >
5516
- {galleryData.map(
5517
- (img, index) => (
5518
- <div
5519
- key={`gallery-item-${index}`}
5520
- className={
5521
- styles.galleryItem
5522
- }
5523
- onClick={() => {
5524
- setLightboxIndex(
5525
- index
5526
- );
5527
- setLightboxOpen(
5528
- true
5529
- );
5530
- }}
5531
- >
5532
- <img
5533
- src={img.src}
5534
- alt={
5535
- img.caption ||
5536
- `Image ${
5537
- index +
5538
- 1
5539
- }`
5540
- }
5541
- />
5542
- {img.caption && (
5543
- <div
5544
- className={
5545
- styles.imageCaption
5546
- }
5547
- >
5548
- {
5549
- img.caption
5550
- }
5551
- </div>
5552
- )}
5553
- <div
5554
- className={
5555
- styles.galleryImageOverlay
5556
- }
5557
- >
5558
- <span>
5559
- <Image
5560
- strokeWidth={
5561
- 1.5
5562
- }
5563
- size={
5564
- 16
5565
- }
5566
- style={{
5567
- marginRight:
5568
- '6px',
5569
- }}
5570
- />
5571
- Enlarge
5572
- </span>
5573
- </div>
5574
- </div>
5575
- )
5576
- )}
5577
- </div>
5578
- </div>
5579
- ) : (
5580
- <div className={styles.noDataMessage}>
5581
- <Image
5582
- strokeWidth={1.5}
5583
- size={20}
5584
- />
5585
- No images available in the gallery
5586
- </div>
5587
- )}
5588
- </div>
5589
- </div>
3617
+ <div className={styles.touchTooltipContent}>
3618
+ {activeTooltip.label}
5590
3619
  </div>
5591
3620
  </div>
5592
- </Popup>
5593
-
5594
- {/* Lightbox for full-size image viewing */}
5595
- <Lightbox
5596
- open={lightboxOpen}
5597
- close={() => setLightboxOpen(false)}
5598
- index={lightboxIndex}
5599
- slides={galleryData.map((img) => ({ src: img.src }))}
5600
- />
3621
+ )}
5601
3622
  </>
5602
3623
  );
5603
3624
  }