@visns-studio/visns-components 5.10.1 → 5.10.3

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,188 @@ 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((row) => row[groupField] === groupValue);
1254
+ };
1255
+
1256
+ const shouldShowGroupAction = (iconConfig, groupValue) => {
1257
+ // If no show conditions, always show the icon
1258
+ if (!iconConfig.show || !Array.isArray(iconConfig.show)) {
1259
+ return true;
1260
+ }
1261
+
1262
+ // Get all rows for this group
1263
+ const groupRows = getGroupRows(groupValue);
1264
+
1265
+ // Check if all show conditions are met
1266
+ return iconConfig.show.every(condition => {
1267
+ const { id, value } = condition;
1268
+
1269
+ // Check if any row in the group matches the condition
1270
+ return groupRows.some(row => {
1271
+ const rowValue = row[id];
1272
+
1273
+ // Handle null value condition
1274
+ if (value === null) {
1275
+ return rowValue === null || rowValue === undefined || rowValue === '';
1276
+ }
1277
+
1278
+ // Handle other value conditions
1279
+ return rowValue === value;
1280
+ });
1281
+ });
1282
+ };
1283
+
1284
+ const executeGroupAction = async (groupValue, iconConfig) => {
1285
+ // Check if fetch configuration is available
1286
+ if (iconConfig.fetch && iconConfig.fetch.url) {
1287
+ try {
1288
+ const payload = {
1289
+ ...iconConfig.fetch.data,
1290
+ groupLabel: groupValue
1291
+ };
1292
+
1293
+ const result = await CustomFetch(
1294
+ iconConfig.fetch.url,
1295
+ iconConfig.fetch.method || 'POST',
1296
+ payload
1297
+ );
1298
+
1299
+ if (result.error) {
1300
+ throw new Error(result.error);
1301
+ }
1302
+
1303
+ toast.success(
1304
+ `${iconConfig.label} - Group: ${groupValue} completed successfully`
1305
+ );
1306
+
1307
+ // Reload grid data if needed
1308
+ if (gridRef.current && gridRef.current.paginationProps) {
1309
+ gridRef.current.paginationProps.reload();
1310
+ }
1311
+ } catch (error) {
1312
+ console.error('Group action fetch error:', error);
1313
+ toast.error(
1314
+ `${iconConfig.label} - Group: ${groupValue} failed: ${error.message}`
1315
+ );
1316
+ }
1317
+ } else {
1318
+ // Fallback to existing switch logic if no fetch config
1319
+ toast.info(`${iconConfig.label} - Group: ${groupValue}`);
1320
+
1321
+ switch (iconConfig.id) {
1322
+ case 'clock':
1323
+ console.log(
1324
+ `Starting timer action for group: ${groupValue}`
1325
+ );
1326
+ break;
1327
+ case 'alarm':
1328
+ console.log(
1329
+ `Setting alarm action for group: ${groupValue}`
1330
+ );
1331
+ break;
1332
+ case 'check':
1333
+ console.log(
1334
+ `Mark complete action for group: ${groupValue}`
1335
+ );
1336
+ break;
1337
+ case 'edit':
1338
+ console.log(
1339
+ `Bulk edit action for group: ${groupValue}`
1340
+ );
1341
+ break;
1342
+ case 'envelope':
1343
+ console.log(
1344
+ `Send notification action for group: ${groupValue}`
1345
+ );
1346
+ break;
1347
+ default:
1348
+ console.log(
1349
+ `Custom action ${iconConfig.id} for group: ${groupValue}`
1350
+ );
1351
+ break;
1352
+ }
1353
+ }
1354
+ };
1355
+
1356
+ // Touch handlers for tooltips
1357
+ const handleTouchStart = (e, buttonId, label) => {
1358
+ // Clear any existing timer
1359
+ if (touchTimer) {
1360
+ clearTimeout(touchTimer);
1361
+ }
1362
+
1363
+ // Capture the button element and rect immediately
1364
+ const buttonElement = e.currentTarget;
1365
+ const rect = buttonElement.getBoundingClientRect();
1366
+ const x = rect.left + rect.width / 2; // Center of button
1367
+ const y = rect.top; // Top of button
1368
+
1369
+ // Set a timer for long press (500ms)
1370
+ const timer = setTimeout(() => {
1371
+ setActiveTooltip({ id: buttonId, label, x, y });
1372
+
1373
+ // Auto-close tooltip after 2 seconds
1374
+ setTimeout(() => {
1375
+ setActiveTooltip(null);
1376
+ }, 2000);
1377
+ }, 500);
1378
+
1379
+ setTouchTimer(timer);
1380
+ };
1381
+
1382
+ const handleTouchEnd = () => {
1383
+ // Clear the timer if touch ends before long press
1384
+ if (touchTimer) {
1385
+ clearTimeout(touchTimer);
1386
+ setTouchTimer(null);
1387
+ }
1388
+ };
1389
+
1390
+ const handleTouchMove = () => {
1391
+ // Cancel tooltip if user moves finger (not a long press)
1392
+ if (touchTimer) {
1393
+ clearTimeout(touchTimer);
1394
+ setTouchTimer(null);
1395
+ }
1396
+ };
1397
+
1398
+ const closeTooltip = () => {
1399
+ setActiveTooltip(null);
1400
+ };
1401
+
1543
1402
  const handleSort = (value) => {
1544
1403
  if (setConfig) {
1545
1404
  setConfig((prevState) => {
@@ -1767,212 +1626,6 @@ const DataGrid = forwardRef(
1767
1626
  [selected, setRowsSelected, onRowClick]
1768
1627
  );
1769
1628
 
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
1629
  // Helper function to determine if auto refresh should be relocated
1977
1630
  const shouldRelocateAutoRefresh = () => {
1978
1631
  const isSearchDisabled = form.searchDisable === true;
@@ -1980,36 +1633,9 @@ const DataGrid = forwardRef(
1980
1633
  const hasAutoRefresh =
1981
1634
  ajaxSetting && ajaxSetting.autoRefresh !== undefined;
1982
1635
 
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
- );
1636
+ // Hide auto refresh when it would be the sole control (instead of relocating it)
1637
+ // This prevents it from appearing under the datagrid
1638
+ return false;
2013
1639
  };
2014
1640
 
2015
1641
  const renderRowContextMenu = useCallback(
@@ -2714,8 +2340,6 @@ const DataGrid = forwardRef(
2714
2340
  let date;
2715
2341
  let filterEditor = null;
2716
2342
  let filterEditorProps = null;
2717
- let fileUrl = null;
2718
- let icons = [];
2719
2343
  let relationName;
2720
2344
  let selectedDataSource = null;
2721
2345
  let stage;
@@ -2840,1572 +2464,176 @@ const DataGrid = forwardRef(
2840
2464
 
2841
2465
  switch (column.type) {
2842
2466
  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
- };
2467
+ return renderAddressColumn({
2468
+ column,
2469
+ commonProps,
2470
+ filterEditor,
2471
+ filterEditorProps,
2472
+ });
2878
2473
  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
- };
2474
+ return renderAgeColumn({
2475
+ column,
2476
+ commonProps,
2477
+ filterEditor,
2478
+ });
2911
2479
  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
- };
2480
+ return renderArrayCountColumn({
2481
+ column,
2482
+ commonProps,
2483
+ });
2924
2484
  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
- };
2485
+ return renderBooleanColumn({
2486
+ column,
2487
+ commonProps,
2488
+ filterEditor,
2489
+ filterEditorProps,
2490
+ });
2945
2491
  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
- };
2492
+ return renderCodingColumn({
2493
+ column,
2494
+ commonProps,
2495
+ handleCoding,
2496
+ });
2958
2497
  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
- };
2498
+ return renderColourColumn({
2499
+ column,
2500
+ commonProps,
2501
+ });
2997
2502
  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
- };
2503
+ return renderCreatedByColumn({
2504
+ column,
2505
+ commonProps,
2506
+ });
3016
2507
  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
- };
2508
+ return renderCurrencyColumn({
2509
+ column,
2510
+ commonProps,
2511
+ });
3039
2512
  case 'date':
3040
- return {
3041
- ...commonProps,
3042
- name: column.id,
3043
- filterEditor: filterEditor,
3044
- filterEditorProps: filterEditorProps,
3045
- defaultWidth: 120, // Set default width for date columns
3046
- minWidth: 120, // Set minimum width for date columns
3047
- render: ({ data }) => {
3048
- if (
3049
- data &&
3050
- data[column.id] &&
3051
- moment(data[column.id]).isValid() &&
3052
- moment(data[column.id]).format(
3053
- 'YYYY-MM-DD'
3054
- ) !== '1970-01-01'
3055
- ) {
3056
- const value = moment(
3057
- data[column.id]
3058
- ).format(
3059
- column.format || 'DD-MM-YYYY'
3060
- );
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
- }
3080
- }
3081
-
3082
- return (
3083
- <CellWithTooltip
3084
- value={data[column.id]}
3085
- columnType="date"
3086
- >
3087
- <span style={columnStyle}>
3088
- {value}
3089
- </span>
3090
- </CellWithTooltip>
3091
- );
3092
- } else {
3093
- return null;
3094
- }
3095
- },
3096
- };
2513
+ return renderDateColumn({
2514
+ column,
2515
+ commonProps,
2516
+ filterEditor,
2517
+ filterEditorProps,
2518
+ });
3097
2519
  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
- }
3122
-
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
- }
3139
-
3140
- return <span>{value}</span>;
3141
- },
3142
- };
2520
+ return renderDateRangeColumn({
2521
+ column,
2522
+ commonProps,
2523
+ filterEditor,
2524
+ filterEditorProps,
2525
+ });
3143
2526
  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
- );
3167
-
3168
- return <span>{data}</span>;
3169
- } else {
3170
- return null;
3171
- }
3172
- },
3173
- };
2527
+ return renderDateTimeColumn({
2528
+ column,
2529
+ commonProps,
2530
+ filterEditor,
2531
+ filterEditorProps,
2532
+ });
3174
2533
  case 'dropdown':
3175
- columnId = Array.isArray(column.id)
3176
- ? column.id.join('-')
3177
- : column.id;
3178
-
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
- };
2534
+ return renderDropdownColumn({
2535
+ column,
2536
+ commonProps,
2537
+ filterEditor,
2538
+ filterEditorProps,
2539
+ dropdownData,
2540
+ handleDropdownUpdate,
2541
+ });
3237
2542
  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
- };
2543
+ return renderFileColumn({
2544
+ column,
2545
+ commonProps,
2546
+ handleDownload,
2547
+ styles,
2548
+ });
3316
2549
  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
- };
2550
+ return renderIconsColumn({
2551
+ column,
2552
+ commonProps,
2553
+ });
3349
2554
  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
- };
2555
+ return renderImageColumn({
2556
+ column,
2557
+ commonProps,
2558
+ });
3373
2559
  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
- };
2560
+ return renderJsonColumn({
2561
+ column,
2562
+ commonProps,
2563
+ });
3401
2564
  case 'number':
3402
- return {
3403
- ...commonProps,
3404
- name: column.id,
3405
- type: 'number',
3406
- };
2565
+ return renderNumberColumn({
2566
+ column,
2567
+ commonProps,
2568
+ });
3407
2569
  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
- };
2570
+ return renderOptionColumn({
2571
+ column,
2572
+ commonProps,
2573
+ filterEditor,
2574
+ filterEditorProps,
2575
+ });
2576
+ case 'placeholder':
2577
+ return renderPlaceholderColumn({
2578
+ column,
2579
+ commonProps,
2580
+ });
2581
+ case 'relation':
2582
+ return renderRelationColumn({
2583
+ column,
2584
+ commonProps,
2585
+ filterEditor,
2586
+ filterEditorProps,
2587
+ relationName,
2588
+ });
3574
2589
  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
- };
2590
+ return renderRelationArrayColumn({
2591
+ column,
2592
+ commonProps,
2593
+ filterEditor,
2594
+ filterEditorProps,
2595
+ relationName,
2596
+ });
3766
2597
  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
- };
2598
+ return renderRichTextColumn({
2599
+ column,
2600
+ commonProps,
2601
+ });
3787
2602
  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
- };
2603
+ return renderStageColumn({
2604
+ column,
2605
+ commonProps,
2606
+ });
3811
2607
  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
- };
2608
+ return renderTimeColumn({
2609
+ column,
2610
+ commonProps,
2611
+ });
3845
2612
  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
- };
2613
+ return renderTimerColumn({
2614
+ column,
2615
+ commonProps,
2616
+ handleTimer,
2617
+ form,
2618
+ columns,
2619
+ styles,
2620
+ });
4026
2621
  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
- };
2622
+ return renderStageCounterColumn({
2623
+ column,
2624
+ commonProps,
2625
+ navigate,
2626
+ });
4307
2627
  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
- };
2628
+ return renderUrlColumn({ column, commonProps });
4335
2629
  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
- };
2630
+ return renderInputTextColumn({
2631
+ column,
2632
+ commonProps,
2633
+ localInputValuesRef,
2634
+ setLocalInputValues,
2635
+ handleInputUpdate,
2636
+ });
4409
2637
  default:
4410
2638
  columnId = Array.isArray(column.id)
4411
2639
  ? column.id.join('-')
@@ -4767,16 +2995,36 @@ const DataGrid = forwardRef(
4767
2995
  <>
4768
2996
  {/* Top container with search, action buttons, and auto-refresh */}
4769
2997
  {/* Only show top container if there are controls to display */}
4770
- {(renderSearch() ||
2998
+ {(!form.searchDisable ||
2999
+ !form.createDisable ||
4771
3000
  (!shouldRelocateAutoRefresh() &&
4772
- renderAutoRefreshControls())) && (
3001
+ ajaxSetting &&
3002
+ ajaxSetting.autoRefresh !== undefined)) && (
4773
3003
  <div className={styles.dataGridTopContainer}>
4774
3004
  <div className={styles.dataGridTopLeftContainer}>
4775
- {renderSearch()}
3005
+ <DataGridSearch
3006
+ form={form}
3007
+ search={search}
3008
+ onSearchChange={handleChangeSearch}
3009
+ onModalOpen={modalOpen}
3010
+ dataRef={dataRef}
3011
+ filterValue={filterValue}
3012
+ paramValue={paramValue}
3013
+ pageData={pageData}
3014
+ pageSetting={pageSetting}
3015
+ />
4776
3016
  </div>
4777
3017
  <div className={styles.dataGridTopRightContainer}>
4778
- {!shouldRelocateAutoRefresh() &&
4779
- renderAutoRefreshControls()}
3018
+ {!shouldRelocateAutoRefresh() && (
3019
+ <AutoRefreshControls
3020
+ ajaxSetting={ajaxSetting}
3021
+ autoRefreshEnabled={autoRefreshEnabled}
3022
+ setAutoRefreshEnabled={
3023
+ setAutoRefreshEnabled
3024
+ }
3025
+ refreshInterval={refreshInterval}
3026
+ />
3027
+ )}
4780
3028
  </div>
4781
3029
  </div>
4782
3030
  )}
@@ -4907,64 +3155,19 @@ const DataGrid = forwardRef(
4907
3155
 
4908
3156
  return (
4909
3157
  <div
4910
- className="group-header-clean group-header-fixed-height"
3158
+ className={`group-header-clean group-header-fixed-height ${styles.groupHeaderContainer}`}
4911
3159
  onClick={
4912
3160
  handleGroupHeaderClick
4913
3161
  }
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
3162
  >
4956
3163
  {/* Left side - Group info and expand/collapse */}
4957
3164
  <div
4958
- style={{
4959
- display: 'flex',
4960
- alignItems: 'center',
4961
- flex: 1,
4962
- zIndex: 2,
4963
- position: 'relative',
4964
- }}
3165
+ className={
3166
+ styles.groupActionContainer
3167
+ }
4965
3168
  >
4966
3169
  <button
4967
- className="group-expand-btn"
3170
+ className={`group-expand-btn ${styles.groupExpandBtn}`}
4968
3171
  onClick={(e) => {
4969
3172
  e.stopPropagation();
4970
3173
  if (
@@ -4972,110 +3175,160 @@ const DataGrid = forwardRef(
4972
3175
  'function'
4973
3176
  ) {
4974
3177
  toggleGroup();
4975
- } else {
4976
- console.warn(
4977
- 'toggleGroup is not a function:',
4978
- toggleGroup
4979
- );
4980
3178
  }
4981
3179
  }}
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)';
3180
+ onTouchStart={(e) => {
3181
+ e.stopPropagation();
3182
+ handleTouchStart(
3183
+ e,
3184
+ 'group-expand',
3185
+ collapsed
3186
+ ? 'Expand group'
3187
+ : 'Collapse group'
3188
+ );
5018
3189
  }}
3190
+ onTouchEnd={
3191
+ handleTouchEnd
3192
+ }
3193
+ onTouchMove={
3194
+ handleTouchMove
3195
+ }
3196
+ data-tooltip-id="system-tooltip"
3197
+ data-tooltip-content={
3198
+ collapsed
3199
+ ? 'Expand group'
3200
+ : 'Collapse group'
3201
+ }
5019
3202
  >
5020
3203
  {collapsed
5021
3204
  ? '+'
5022
3205
  : '−'}
5023
3206
  </button>
3207
+ {/* Dynamic group action buttons */}
3208
+ {ajaxSetting?.groupBySetting?.icons?.map(
3209
+ (
3210
+ iconConfig,
3211
+ index
3212
+ ) => {
3213
+ // Get the icon component based on the id
3214
+ const getIconComponent =
3215
+ (iconId) => {
3216
+ switch (
3217
+ iconId
3218
+ ) {
3219
+ case 'clock':
3220
+ return Clock;
3221
+ case 'alarm':
3222
+ return Alarm;
3223
+ case 'check':
3224
+ return Check;
3225
+ case 'edit':
3226
+ return Edit;
3227
+ case 'envelope':
3228
+ return Envelope;
3229
+ case 'file':
3230
+ return File;
3231
+ case 'image':
3232
+ return Image;
3233
+ case 'copy':
3234
+ return Copy;
3235
+ case 'cloudUpload':
3236
+ return CloudDownload;
3237
+ case 'cloudDownload':
3238
+ return CloudDownload;
3239
+ default:
3240
+ return Clock; // Default fallback
3241
+ }
3242
+ };
3243
+
3244
+ // Check if this icon should be shown based on show conditions
3245
+ if (!shouldShowGroupAction(iconConfig, groupValue)) {
3246
+ return null;
3247
+ }
3248
+
3249
+ const IconComponent =
3250
+ getIconComponent(
3251
+ iconConfig.id
3252
+ );
3253
+
3254
+ // Generate different color classes for multiple buttons
3255
+ const colorClasses =
3256
+ [
3257
+ 'success',
3258
+ 'warning',
3259
+ 'info',
3260
+ 'purple',
3261
+ 'pink',
3262
+ ];
3263
+ const colorClass =
3264
+ colorClasses[
3265
+ index %
3266
+ colorClasses.length
3267
+ ];
3268
+
3269
+ return (
3270
+ <button
3271
+ key={`group-action-${index}`}
3272
+ className={`group-action-btn group-${iconConfig.id}-btn ${styles.groupActionBtn} ${styles[colorClass]}`}
3273
+ onClick={(
3274
+ e
3275
+ ) => {
3276
+ e.stopPropagation();
3277
+ handleGroupAction(
3278
+ groupValue,
3279
+ iconConfig
3280
+ );
3281
+ }}
3282
+ onTouchStart={(
3283
+ e
3284
+ ) => {
3285
+ e.stopPropagation();
3286
+ handleTouchStart(
3287
+ e,
3288
+ `group-${iconConfig.id}-${index}`,
3289
+ iconConfig.label
3290
+ );
3291
+ }}
3292
+ onTouchEnd={
3293
+ handleTouchEnd
3294
+ }
3295
+ onTouchMove={
3296
+ handleTouchMove
3297
+ }
3298
+ data-tooltip-id="system-tooltip"
3299
+ data-tooltip-content={
3300
+ iconConfig.label
3301
+ }
3302
+ >
3303
+ <IconComponent
3304
+ strokeWidth={
3305
+ 2
3306
+ }
3307
+ size={
3308
+ 14
3309
+ }
3310
+ />
3311
+ </button>
3312
+ );
3313
+ }
3314
+ ) || null}
5024
3315
  <div
5025
- style={{
5026
- display: 'flex',
5027
- alignItems:
5028
- 'center', // Changed to center for compact layout
5029
- gap: '8px', // Add gap between elements
5030
- }}
3316
+ className={
3317
+ styles.groupInfo
3318
+ }
5031
3319
  >
5032
3320
  <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
- }}
3321
+ className={
3322
+ styles.groupValue
3323
+ }
5044
3324
  >
5045
3325
  {groupValue}
5046
3326
  </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
3327
  {/* Click indicator */}
5066
3328
  <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
- }}
3329
+ className={
3330
+ styles.groupClickIndicator
3331
+ }
5079
3332
  >
5080
3333
  Click to
5081
3334
  select/unselect
@@ -5084,93 +3337,11 @@ const DataGrid = forwardRef(
5084
3337
  </div>
5085
3338
  </div>
5086
3339
 
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
3340
  {/* Background pattern */}
5162
3341
  <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
- }}
3342
+ className={
3343
+ styles.groupHeaderPattern
3344
+ }
5174
3345
  />
5175
3346
  </div>
5176
3347
  );
@@ -5319,13 +3490,19 @@ const DataGrid = forwardRef(
5319
3490
  </div>
5320
3491
 
5321
3492
  {/* Relocated auto refresh control when it's the only control */}
5322
- {/* {shouldRelocateAutoRefresh() && (
3493
+ {shouldRelocateAutoRefresh() && (
5323
3494
  <div className={styles.dataGridBottomContainer}>
5324
- {renderAutoRefreshControls(true)}
3495
+ <AutoRefreshControls
3496
+ ajaxSetting={ajaxSetting}
3497
+ autoRefreshEnabled={autoRefreshEnabled}
3498
+ setAutoRefreshEnabled={setAutoRefreshEnabled}
3499
+ refreshInterval={refreshInterval}
3500
+ isRelocated={true}
3501
+ />
5325
3502
  </div>
5326
- )} */}
3503
+ )}
5327
3504
 
5328
- {audioSource && <audio src={audioSource} ref={audioRef} />}
3505
+ <AudioPlayer audioSource={audioSource} />
5329
3506
  <Popup
5330
3507
  open={modalShow}
5331
3508
  onClose={modalClose}
@@ -5373,231 +3550,38 @@ const DataGrid = forwardRef(
5373
3550
  </div>
5374
3551
  </div>
5375
3552
  </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
- >
3553
+ <GalleryModal
3554
+ modalGalleryShow={modalGalleryShow}
3555
+ modalGalleryClose={modalGalleryClose}
3556
+ galleryData={galleryData}
3557
+ setGalleryData={setGalleryData}
3558
+ currentItem={currentItem}
3559
+ form={form}
3560
+ lightboxOpen={lightboxOpen}
3561
+ setLightboxOpen={setLightboxOpen}
3562
+ lightboxIndex={lightboxIndex}
3563
+ setLightboxIndex={setLightboxIndex}
3564
+ />
3565
+
3566
+ {/* Touch Tooltip Overlay */}
3567
+ {activeTooltip && (
5387
3568
  <div
5388
- className={`${styles.modalwrap} ${styles['top--modal']}`}
3569
+ className={styles.touchTooltip}
3570
+ style={{
3571
+ position: 'fixed',
3572
+ left: `${activeTooltip.x}px`,
3573
+ top: `${activeTooltip.y - 50}px`,
3574
+ transform: 'translateX(-50%)', // Center horizontally on button
3575
+ zIndex: 9999,
3576
+ pointerEvents: 'none',
3577
+ }}
3578
+ onClick={closeTooltip}
5389
3579
  >
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>
3580
+ <div className={styles.touchTooltipContent}>
3581
+ {activeTooltip.label}
5590
3582
  </div>
5591
3583
  </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
- />
3584
+ )}
5601
3585
  </>
5602
3586
  );
5603
3587
  }