@visns-studio/visns-components 5.8.19 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,24 @@
1
1
  import '../styles/global.css';
2
2
  import './styles/global-datagrid.css';
3
3
 
4
+ /**
5
+ * DataGrid Component
6
+ *
7
+ * This component provides a data grid with modal form editing capabilities.
8
+ *
9
+ * Modal Vertical Alignment:
10
+ * To position the modal toward the top of the viewport instead of center, add the `verticalAlign: "top"` property to the form object.
11
+ * Example:
12
+ * {
13
+ * form: {
14
+ * verticalAlign: "top",
15
+ * primaryKey: "id",
16
+ * url: "/ajax/jobs",
17
+ * // other form properties
18
+ * }
19
+ * }
20
+ */
21
+
4
22
  import React, {
5
23
  forwardRef,
6
24
  useCallback,
@@ -25,6 +43,7 @@ import { toast } from 'react-toastify';
25
43
  import imageCompression from 'browser-image-compression';
26
44
  import Vapor from 'laravel-vapor';
27
45
  import Lightbox from 'yet-another-react-lightbox';
46
+ import { Tooltip } from 'react-tooltip';
28
47
  import fetchUtil from '../../utils/fetchUtil';
29
48
  import { confirmDialog } from '../utils/ConfirmDialog';
30
49
  import {
@@ -128,6 +147,123 @@ const loadData = async (
128
147
  }
129
148
  };
130
149
 
150
+ // Utility function to check if text content is truncated
151
+ const isTextTruncated = (element) => {
152
+ if (!element) return false;
153
+ return (
154
+ element.scrollWidth > element.clientWidth ||
155
+ element.scrollHeight > element.clientHeight
156
+ );
157
+ };
158
+
159
+ // Utility function to format cell value for tooltip display
160
+ const formatCellValueForTooltip = (value, columnType) => {
161
+ if (value === null || value === undefined || value === '') {
162
+ return '';
163
+ }
164
+
165
+ switch (columnType) {
166
+ case 'date':
167
+ case 'datetime':
168
+ if (
169
+ moment(value).isValid() &&
170
+ moment(value).format('YYYY-MM-DD') !== '1970-01-01'
171
+ ) {
172
+ return moment(value).format('DD-MM-YYYY HH:mm');
173
+ }
174
+ return String(value);
175
+ case 'currency':
176
+ return numeral(value).format('$0,0.00');
177
+ case 'number':
178
+ return numeral(value).format('0,0');
179
+ case 'boolean':
180
+ return value ? 'Yes' : 'No';
181
+ case 'richtext':
182
+ // Strip HTML tags for tooltip
183
+ const tempDiv = document.createElement('div');
184
+ tempDiv.innerHTML = value;
185
+ return tempDiv.textContent || tempDiv.innerText || '';
186
+ default:
187
+ return String(value);
188
+ }
189
+ };
190
+
191
+ // Component to wrap cell content with tooltip functionality
192
+ const CellWithTooltip = ({
193
+ children,
194
+ value,
195
+ columnType = 'text',
196
+ className = '',
197
+ }) => {
198
+ const cellRef = useRef(null);
199
+ const [showTooltip, setShowTooltip] = useState(false);
200
+ const [tooltipId] = useState(
201
+ () => `cell-tooltip-${Math.random().toString(36).substr(2, 9)}`
202
+ );
203
+
204
+ useEffect(() => {
205
+ const checkTruncation = () => {
206
+ if (cellRef.current) {
207
+ const isTruncated = isTextTruncated(cellRef.current);
208
+ setShowTooltip(isTruncated);
209
+ }
210
+ };
211
+
212
+ // Check truncation on mount and when content changes
213
+ checkTruncation();
214
+
215
+ // Also check on window resize
216
+ const handleResize = () => checkTruncation();
217
+ window.addEventListener('resize', handleResize);
218
+
219
+ return () => {
220
+ window.removeEventListener('resize', handleResize);
221
+ };
222
+ }, [children, value]);
223
+
224
+ const tooltipContent = formatCellValueForTooltip(value, columnType);
225
+
226
+ // Only show tooltip if content is truncated and we have meaningful content
227
+ const shouldShowTooltip =
228
+ showTooltip && tooltipContent && tooltipContent.trim() !== '';
229
+
230
+ return (
231
+ <div
232
+ ref={cellRef}
233
+ className={className}
234
+ style={{
235
+ overflow: 'hidden',
236
+ textOverflow: 'ellipsis',
237
+ whiteSpace: 'nowrap',
238
+ width: '100%',
239
+ }}
240
+ {...(shouldShowTooltip && {
241
+ 'data-tooltip-id': tooltipId,
242
+ 'data-tooltip-content': tooltipContent,
243
+ })}
244
+ >
245
+ {children}
246
+ {shouldShowTooltip && (
247
+ <Tooltip
248
+ id={tooltipId}
249
+ style={{
250
+ backgroundColor: 'rgba(0, 0, 0, 0.9)',
251
+ color: 'white',
252
+ borderRadius: '6px',
253
+ padding: '8px 12px',
254
+ fontSize: '13px',
255
+ maxWidth: '300px',
256
+ wordWrap: 'break-word',
257
+ zIndex: 9999,
258
+ }}
259
+ place="top"
260
+ offset={5}
261
+ />
262
+ )}
263
+ </div>
264
+ );
265
+ };
266
+
131
267
  const DataGrid = forwardRef(
132
268
  (
133
269
  {
@@ -360,6 +496,18 @@ const DataGrid = forwardRef(
360
496
  }
361
497
 
362
498
  sqlResult.then((res) => {
499
+ // Sort data by grouping field if grouping is enabled
500
+ if (ajaxSetting?.groupBy?.length > 0) {
501
+ const groupField = ajaxSetting.groupBy[0]; // groupBy is array of strings
502
+ res.data.sort((a, b) => {
503
+ const aValue = a[groupField] || '';
504
+ const bValue = b[groupField] || '';
505
+ return aValue
506
+ .toString()
507
+ .localeCompare(bValue.toString());
508
+ });
509
+ }
510
+
363
511
  if (setTotal) {
364
512
  setTotal(res.count);
365
513
  }
@@ -525,47 +673,33 @@ const DataGrid = forwardRef(
525
673
 
526
674
  const handleSelectionChange = useCallback(
527
675
  (param) => {
528
- // If this is a new selection (not a deselection)
529
- if (
530
- param.selected === true ||
676
+ // Handle the selection change based on the parameter type
677
+ if (param.selected === true) {
678
+ // Select all rows - this happens when header checkbox is clicked to select all
679
+ const s = {};
680
+ param.data.forEach((obj) => {
681
+ s[obj.id] = obj;
682
+ });
683
+ setSelected(s);
684
+ // Update parent component's rowsSelected state
685
+ if (setRowsSelected) {
686
+ setRowsSelected(s);
687
+ }
688
+ } else if (
689
+ param.selected === false ||
531
690
  (typeof param.selected === 'object' &&
532
- Object.keys(param.selected).length > 0)
691
+ Object.keys(param.selected).length === 0)
533
692
  ) {
534
- // Process the selection directly without confirmation
535
- if (param.selected === true) {
536
- const s = {};
537
- param.data.forEach((obj) => {
538
- s[obj.id] = obj;
539
- });
540
- setSelected(s);
541
- // Update parent component's rowsSelected state
542
- if (setRowsSelected) {
543
- setRowsSelected(s);
544
- }
545
- } else if (typeof param.selected === 'object') {
546
- const selectedKey = Object.keys(param.selected)[0];
547
- const isSelectedAlready =
548
- selected.hasOwnProperty(selectedKey);
549
-
550
- let newSelected;
551
- if (isSelectedAlready) {
552
- newSelected = {};
553
- setSelected(newSelected);
554
- } else {
555
- newSelected = {
556
- ...selected,
557
- [selectedKey]: param.selected[selectedKey],
558
- };
559
- setSelected(newSelected);
560
- }
561
-
562
- // Update parent component's rowsSelected state
563
- if (setRowsSelected) {
564
- setRowsSelected(newSelected);
565
- }
693
+ // Deselect all rows - this happens when header checkbox is clicked to deselect all
694
+ setSelected({});
695
+ // Update parent component's rowsSelected state
696
+ if (setRowsSelected) {
697
+ setRowsSelected({});
566
698
  }
567
- } else {
568
- // Handle deselection directly without confirmation
699
+ } else if (typeof param.selected === 'object') {
700
+ // Individual row selection/deselection - ReactDataGrid sends the complete new selection state
701
+ // ReactDataGrid sends the complete new selection state, not just the changed row
702
+ // So we can directly use param.selected as the new selection state
569
703
  setSelected(param.selected);
570
704
  // Update parent component's rowsSelected state
571
705
  if (setRowsSelected) {
@@ -574,7 +708,7 @@ const DataGrid = forwardRef(
574
708
  }
575
709
  },
576
710
  [selected, setRowsSelected]
577
- ); // Including selected and setRowsSelected in the dependency array
711
+ );
578
712
 
579
713
  const findUpdatedFilter = (filters, currentValues) => {
580
714
  for (const filter of filters) {
@@ -1141,35 +1275,85 @@ const DataGrid = forwardRef(
1141
1275
  }
1142
1276
  }, []);
1143
1277
 
1144
- const onRenderRow = useCallback((rowProps) => {
1145
- // save the original handlers to be called later
1146
- const { onClick } = rowProps;
1278
+ const onRenderRow = useCallback(
1279
+ (rowProps) => {
1280
+ // save the original handlers to be called later
1281
+ const { onClick } = rowProps;
1147
1282
 
1148
- rowProps.onClick = (event) => {
1149
- onRowClick(rowProps, event);
1283
+ rowProps.onClick = (event) => {
1284
+ const cellElement = event.target.closest(
1285
+ '.InovuaReactDataGrid__cell'
1286
+ );
1150
1287
 
1151
- const cellElement = event.target.closest(
1152
- '.InovuaReactDataGrid__cell'
1153
- );
1288
+ // Add null check to prevent errors when cellElement is null
1289
+ if (!cellElement) {
1290
+ return; // Exit early if no cell element was found
1291
+ }
1154
1292
 
1155
- // Add null check to prevent errors when cellElement is null
1156
- if (!cellElement) {
1157
- return; // Exit early if no cell element was found
1158
- }
1293
+ const columnIndex = Array.from(
1294
+ cellElement.parentNode.children
1295
+ ).indexOf(cellElement);
1159
1296
 
1160
- const columnIndex = Array.from(
1161
- cellElement.parentNode.children
1162
- ).indexOf(cellElement);
1297
+ const column = rowProps.columns[columnIndex];
1163
1298
 
1164
- const column = rowProps.columns[columnIndex];
1299
+ if (column.id === '__checkbox-column') {
1300
+ // For checkbox column clicks, we need to handle them specially
1301
+ // to ensure consistent behavior whether clicking checkbox or cell area
1165
1302
 
1166
- if (column.id === '__checkbox-column') {
1167
- if (onClick) {
1168
- onClick(event);
1303
+ // Check if clicking directly on the checkbox input
1304
+ const isCheckboxInput = event.target.matches(
1305
+ 'input[type="checkbox"]'
1306
+ );
1307
+
1308
+ if (!isCheckboxInput) {
1309
+ // If clicking on empty space in checkbox cell, simulate checkbox click
1310
+ // by manually toggling the selection state
1311
+ event.preventDefault();
1312
+ event.stopPropagation();
1313
+
1314
+ const rowId =
1315
+ rowProps.data[rowProps.idProperty || 'id'];
1316
+ const isCurrentlySelected =
1317
+ selected.hasOwnProperty(rowId);
1318
+
1319
+ let newSelected;
1320
+ if (isCurrentlySelected) {
1321
+ // Remove from selection
1322
+ newSelected = { ...selected };
1323
+ delete newSelected[rowId];
1324
+ } else {
1325
+ // Add to selection
1326
+ newSelected = {
1327
+ ...selected,
1328
+ [rowId]: rowProps.data,
1329
+ };
1330
+ }
1331
+
1332
+ setSelected(newSelected);
1333
+ if (setRowsSelected) {
1334
+ setRowsSelected(newSelected);
1335
+ }
1336
+
1337
+ return; // Don't call the original onClick
1338
+ }
1339
+
1340
+ // If clicking directly on checkbox input, let ReactDataGrid handle it
1341
+ if (onClick) {
1342
+ onClick(event);
1343
+ }
1344
+ } else {
1345
+ // For non-checkbox columns, handle normal row clicks
1346
+ onRowClick(rowProps, event);
1347
+
1348
+ // Call the original onClick handler
1349
+ if (onClick) {
1350
+ onClick(event);
1351
+ }
1169
1352
  }
1170
- }
1171
- };
1172
- }, []);
1353
+ };
1354
+ },
1355
+ [selected, setRowsSelected]
1356
+ );
1173
1357
 
1174
1358
  const exportTrigger = async () => {
1175
1359
  try {
@@ -1403,14 +1587,29 @@ const DataGrid = forwardRef(
1403
1587
  );
1404
1588
  };
1405
1589
 
1406
- const renderAutoRefreshControls = () => {
1590
+ // Helper function to determine if auto refresh should be relocated
1591
+ const shouldRelocateAutoRefresh = () => {
1592
+ const isSearchDisabled = form.searchDisable === true;
1593
+ const isCreateDisabled = form.createDisable === true;
1594
+ const hasAutoRefresh =
1595
+ ajaxSetting && ajaxSetting.autoRefresh !== undefined;
1596
+
1597
+ // Relocate only if auto refresh is the sole control (no search, no create button)
1598
+ return hasAutoRefresh && isSearchDisabled && isCreateDisabled;
1599
+ };
1600
+
1601
+ const renderAutoRefreshControls = (isRelocated = false) => {
1407
1602
  // Only show auto-refresh if the setting is present in ajaxSetting
1408
1603
  if (!ajaxSetting || ajaxSetting.autoRefresh === undefined) {
1409
1604
  return null;
1410
1605
  }
1411
1606
 
1607
+ const containerClass = isRelocated
1608
+ ? styles.autoRefreshContainerRelocated
1609
+ : styles.autoRefreshContainer;
1610
+
1412
1611
  return (
1413
- <div className={styles.autoRefreshContainer}>
1612
+ <div className={containerClass}>
1414
1613
  <label className={styles.autoRefreshLabel}>
1415
1614
  <input
1416
1615
  type="checkbox"
@@ -1469,10 +1668,11 @@ const DataGrid = forwardRef(
1469
1668
  'var(--light-bg-color, #f5f5f5)',
1470
1669
  borderRadius: '4px',
1471
1670
  padding: '4px',
1472
- width: '22px',
1473
- height: '22px',
1671
+ width: '28px',
1672
+ height: '28px',
1474
1673
  boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
1475
1674
  margin: '0 1px',
1675
+ cursor: 'pointer',
1476
1676
  }}
1477
1677
  >
1478
1678
  <img
@@ -1481,8 +1681,8 @@ const DataGrid = forwardRef(
1481
1681
  data-tooltip-id="system-tooltip"
1482
1682
  data-tooltip-content={tooltipContent}
1483
1683
  style={{
1484
- maxHeight: '16px',
1485
- maxWidth: '16px',
1684
+ maxHeight: '20px',
1685
+ maxWidth: '20px',
1486
1686
  objectFit: 'contain',
1487
1687
  }}
1488
1688
  />
@@ -1517,10 +1717,11 @@ const DataGrid = forwardRef(
1517
1717
  'var(--light-bg-color, #f5f5f5)',
1518
1718
  borderRadius: '4px',
1519
1719
  padding: '4px',
1520
- width: '22px',
1521
- height: '22px',
1720
+ width: '28px',
1721
+ height: '28px',
1522
1722
  boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
1523
1723
  margin: '0 1px',
1724
+ cursor: 'pointer',
1524
1725
  }}
1525
1726
  >
1526
1727
  <IconComponent
@@ -2064,7 +2265,14 @@ const DataGrid = forwardRef(
2064
2265
 
2065
2266
  const value = `${addressValue} ${stateValue}`;
2066
2267
 
2067
- return <span>{value}</span>;
2268
+ return (
2269
+ <CellWithTooltip
2270
+ value={value}
2271
+ columnType="address"
2272
+ >
2273
+ <span>{value}</span>
2274
+ </CellWithTooltip>
2275
+ );
2068
2276
  },
2069
2277
  };
2070
2278
  case 'age':
@@ -2121,11 +2329,17 @@ const DataGrid = forwardRef(
2121
2329
  name: `${column.type}.${column.id}`,
2122
2330
  sortable: false,
2123
2331
  render: ({ data }) => {
2124
- if (data[column.id]) {
2125
- return 'Yes';
2126
- } else {
2127
- return 'No';
2128
- }
2332
+ const value = data[column.id]
2333
+ ? 'Yes'
2334
+ : 'No';
2335
+ return (
2336
+ <CellWithTooltip
2337
+ value={data[column.id]}
2338
+ columnType="boolean"
2339
+ >
2340
+ <span>{value}</span>
2341
+ </CellWithTooltip>
2342
+ );
2129
2343
  },
2130
2344
  };
2131
2345
  case 'coding':
@@ -2209,7 +2423,14 @@ const DataGrid = forwardRef(
2209
2423
  data[column.id]
2210
2424
  ).format('$0,0.00');
2211
2425
 
2212
- return <span>{data}</span>;
2426
+ return (
2427
+ <CellWithTooltip
2428
+ value={data[column.id]}
2429
+ columnType="currency"
2430
+ >
2431
+ <span>{data}</span>
2432
+ </CellWithTooltip>
2433
+ );
2213
2434
  } else {
2214
2435
  return null;
2215
2436
  }
@@ -2259,9 +2480,14 @@ const DataGrid = forwardRef(
2259
2480
  }
2260
2481
 
2261
2482
  return (
2262
- <span style={columnStyle}>
2263
- {value}
2264
- </span>
2483
+ <CellWithTooltip
2484
+ value={data[column.id]}
2485
+ columnType="date"
2486
+ >
2487
+ <span style={columnStyle}>
2488
+ {value}
2489
+ </span>
2490
+ </CellWithTooltip>
2265
2491
  );
2266
2492
  } else {
2267
2493
  return null;
@@ -2382,7 +2608,13 @@ const DataGrid = forwardRef(
2382
2608
  }}
2383
2609
  >
2384
2610
  <option value="">
2385
- Select an Option
2611
+ Select{' '}
2612
+ {/^[aeiouAEIOU]/.test(
2613
+ column.label
2614
+ )
2615
+ ? 'an'
2616
+ : 'a'}{' '}
2617
+ {column.label}
2386
2618
  </option>
2387
2619
  {dropdownData[column.id] &&
2388
2620
  dropdownData[column.id].length >
@@ -2436,7 +2668,7 @@ const DataGrid = forwardRef(
2436
2668
  data-tooltip-id={`system-tooltip`}
2437
2669
  data-tooltip-content={`Download File ${file.file_name}`}
2438
2670
  strokeWidth={2}
2439
- size={18}
2671
+ size={14}
2440
2672
  className={
2441
2673
  styles.tdaction
2442
2674
  }
@@ -2468,7 +2700,7 @@ const DataGrid = forwardRef(
2468
2700
  .file_name
2469
2701
  }`}
2470
2702
  strokeWidth={2}
2471
- size={18}
2703
+ size={14}
2472
2704
  className={
2473
2705
  styles.tdaction
2474
2706
  }
@@ -2553,7 +2785,14 @@ const DataGrid = forwardRef(
2553
2785
  column.jsonData
2554
2786
  ];
2555
2787
 
2556
- return <span>{data}</span>;
2788
+ return (
2789
+ <CellWithTooltip
2790
+ value={data}
2791
+ columnType="json"
2792
+ >
2793
+ <span>{data}</span>
2794
+ </CellWithTooltip>
2795
+ );
2557
2796
  } else {
2558
2797
  return null;
2559
2798
  }
@@ -2599,7 +2838,14 @@ const DataGrid = forwardRef(
2599
2838
  ];
2600
2839
  }
2601
2840
 
2602
- return <span>{newData}</span>;
2841
+ return (
2842
+ <CellWithTooltip
2843
+ value={newData}
2844
+ columnType="option"
2845
+ >
2846
+ <span>{newData}</span>
2847
+ </CellWithTooltip>
2848
+ );
2603
2849
  } else {
2604
2850
  return null;
2605
2851
  }
@@ -2613,9 +2859,14 @@ const DataGrid = forwardRef(
2613
2859
  render: ({ data }) => {
2614
2860
  if (data) {
2615
2861
  return (
2616
- <span>
2617
- {column.placeholder}
2618
- </span>
2862
+ <CellWithTooltip
2863
+ value={column.placeholder}
2864
+ columnType="placeholder"
2865
+ >
2866
+ <span>
2867
+ {column.placeholder}
2868
+ </span>
2869
+ </CellWithTooltip>
2619
2870
  );
2620
2871
  } else {
2621
2872
  return null;
@@ -2703,7 +2954,14 @@ const DataGrid = forwardRef(
2703
2954
  typeof value === 'string' ||
2704
2955
  typeof value === 'number'
2705
2956
  ) {
2706
- return <span>{value}</span>;
2957
+ return (
2958
+ <CellWithTooltip
2959
+ value={value}
2960
+ columnType="relation"
2961
+ >
2962
+ <span>{value}</span>
2963
+ </CellWithTooltip>
2964
+ );
2707
2965
  } else {
2708
2966
  return null;
2709
2967
  }
@@ -2854,7 +3112,14 @@ const DataGrid = forwardRef(
2854
3112
  result += values.join('<br />');
2855
3113
  }
2856
3114
 
2857
- return <span>{parse(result)}</span>;
3115
+ return (
3116
+ <CellWithTooltip
3117
+ value={result}
3118
+ columnType="relationArray"
3119
+ >
3120
+ <span>{parse(result)}</span>
3121
+ </CellWithTooltip>
3122
+ );
2858
3123
  // }
2859
3124
  } else {
2860
3125
  let result = '';
@@ -2885,7 +3150,14 @@ const DataGrid = forwardRef(
2885
3150
  : '';
2886
3151
  }
2887
3152
 
2888
- return <span>{parse(result)}</span>;
3153
+ return (
3154
+ <CellWithTooltip
3155
+ value={result}
3156
+ columnType="relationArray"
3157
+ >
3158
+ <span>{parse(result)}</span>
3159
+ </CellWithTooltip>
3160
+ );
2889
3161
  }
2890
3162
 
2891
3163
  return null;
@@ -2898,9 +3170,14 @@ const DataGrid = forwardRef(
2898
3170
  render: ({ data }) => {
2899
3171
  if (data && data[column.id]) {
2900
3172
  return (
2901
- <span>
2902
- {parse(data[column.id])}
2903
- </span>
3173
+ <CellWithTooltip
3174
+ value={data[column.id]}
3175
+ columnType="richtext"
3176
+ >
3177
+ <span>
3178
+ {parse(data[column.id])}
3179
+ </span>
3180
+ </CellWithTooltip>
2904
3181
  );
2905
3182
  } else {
2906
3183
  return null;
@@ -2921,7 +3198,14 @@ const DataGrid = forwardRef(
2921
3198
  }
2922
3199
  });
2923
3200
 
2924
- return <span>{stage}</span>;
3201
+ return (
3202
+ <CellWithTooltip
3203
+ value={stage}
3204
+ columnType="stage"
3205
+ >
3206
+ <span>{stage}</span>
3207
+ </CellWithTooltip>
3208
+ );
2925
3209
  },
2926
3210
  };
2927
3211
  case 'time':
@@ -2945,7 +3229,14 @@ const DataGrid = forwardRef(
2945
3229
  : 'hh:mm A'
2946
3230
  );
2947
3231
 
2948
- return <span>{data}</span>;
3232
+ return (
3233
+ <CellWithTooltip
3234
+ value={data[column.id]}
3235
+ columnType="time"
3236
+ >
3237
+ <span>{data}</span>
3238
+ </CellWithTooltip>
3239
+ );
2949
3240
  } else {
2950
3241
  return null;
2951
3242
  }
@@ -2972,7 +3263,14 @@ const DataGrid = forwardRef(
2972
3263
  : 'DD-MM-YYYY hh:mm A'
2973
3264
  );
2974
3265
 
2975
- return <span>{data}</span>;
3266
+ return (
3267
+ <CellWithTooltip
3268
+ value={data[column.id]}
3269
+ columnType="timer"
3270
+ >
3271
+ <span>{data}</span>
3272
+ </CellWithTooltip>
3273
+ );
2976
3274
  } else {
2977
3275
  let shouldRenderButton = false;
2978
3276
 
@@ -3097,7 +3395,7 @@ const DataGrid = forwardRef(
3097
3395
  data-tooltip-id="system-tooltip"
3098
3396
  data-tooltip-content={`${column.label} Timer`}
3099
3397
  strokeWidth={2}
3100
- size={18}
3398
+ size={14}
3101
3399
  style={{
3102
3400
  color: 'white',
3103
3401
  }}
@@ -3398,14 +3696,21 @@ const DataGrid = forwardRef(
3398
3696
  render: ({ data }) => {
3399
3697
  if (data && data[column.id]) {
3400
3698
  return (
3401
- <span>
3402
- <a
3403
- href={data[column.id]}
3404
- target="_blank"
3405
- >
3406
- Link
3407
- </a>
3408
- </span>
3699
+ <CellWithTooltip
3700
+ value={data[column.id]}
3701
+ columnType="url"
3702
+ >
3703
+ <span>
3704
+ <a
3705
+ href={
3706
+ data[column.id]
3707
+ }
3708
+ target="_blank"
3709
+ >
3710
+ Link
3711
+ </a>
3712
+ </span>
3713
+ </CellWithTooltip>
3409
3714
  );
3410
3715
  } else {
3411
3716
  return null;
@@ -3537,7 +3842,14 @@ const DataGrid = forwardRef(
3537
3842
  }
3538
3843
 
3539
3844
  return (
3540
- <span>{parse(content)}</span>
3845
+ <CellWithTooltip
3846
+ value={data[column.id]}
3847
+ columnType="text"
3848
+ >
3849
+ <span>
3850
+ {parse(content)}
3851
+ </span>
3852
+ </CellWithTooltip>
3541
3853
  );
3542
3854
  } else {
3543
3855
  return null;
@@ -3771,7 +4083,7 @@ const DataGrid = forwardRef(
3771
4083
  setLimit(ajaxSetting.take);
3772
4084
  } else {
3773
4085
  const calculateRows = (height) =>
3774
- Math.floor((height - 100) / 40);
4086
+ Math.floor((height - 100) / 32);
3775
4087
 
3776
4088
  const rowCount = calculateRows(minHeight);
3777
4089
  setLimit(rowCount);
@@ -3846,14 +4158,20 @@ const DataGrid = forwardRef(
3846
4158
  }}
3847
4159
  >
3848
4160
  {/* Top container with search, action buttons, and auto-refresh */}
3849
- <div className={styles.dataGridTopContainer}>
3850
- <div className={styles.dataGridTopLeftContainer}>
3851
- {renderSearch()}
3852
- </div>
3853
- <div className={styles.dataGridTopRightContainer}>
3854
- {renderAutoRefreshControls()}
4161
+ {/* Only show top container if there are controls to display */}
4162
+ {(renderSearch() ||
4163
+ (!shouldRelocateAutoRefresh() &&
4164
+ renderAutoRefreshControls())) && (
4165
+ <div className={styles.dataGridTopContainer}>
4166
+ <div className={styles.dataGridTopLeftContainer}>
4167
+ {renderSearch()}
4168
+ </div>
4169
+ <div className={styles.dataGridTopRightContainer}>
4170
+ {!shouldRelocateAutoRefresh() &&
4171
+ renderAutoRefreshControls()}
4172
+ </div>
3855
4173
  </div>
3856
- </div>
4174
+ )}
3857
4175
 
3858
4176
  {/* DataGrid container with its own border */}
3859
4177
  <div className={styles.dataGridContainer}>
@@ -3877,7 +4195,7 @@ const DataGrid = forwardRef(
3877
4195
  }
3878
4196
  headerProps={headerProps}
3879
4197
  idProperty="id"
3880
- minRowHeight={40}
4198
+ minRowHeight={32}
3881
4199
  onFilterValueChange={(fv) => {
3882
4200
  handleFilterChange(fv, filterValue);
3883
4201
  }}
@@ -3911,9 +4229,26 @@ const DataGrid = forwardRef(
3911
4229
  borderRadius: 'var(--br, 4px)',
3912
4230
  overflow: 'hidden',
3913
4231
  }}
4232
+ checkboxColumnProps={{
4233
+ width: 60,
4234
+ minWidth: 60,
4235
+ maxWidth: 60,
4236
+ resizable: false,
4237
+ sortable: false,
4238
+ headerAlign: 'center',
4239
+ textAlign: 'center',
4240
+ }}
3914
4241
  />
3915
4242
  ) : null}
3916
4243
  </div>
4244
+
4245
+ {/* Relocated auto refresh control when it's the only control */}
4246
+ {shouldRelocateAutoRefresh() && (
4247
+ <div className={styles.dataGridBottomContainer}>
4248
+ {renderAutoRefreshControls(true)}
4249
+ </div>
4250
+ )}
4251
+
3917
4252
  {audioSource && <audio src={audioSource} ref={audioRef} />}
3918
4253
  <Popup
3919
4254
  open={modalShow}
@@ -3923,25 +4258,44 @@ const DataGrid = forwardRef(
3923
4258
  width: windowWidth < 768 ? '95vw' : '80vw',
3924
4259
  minWidth: windowWidth < 768 ? '95vw' : '600px',
3925
4260
  maxWidth: '1200px',
4261
+ ...(formData.verticalAlign === 'top' && {
4262
+ position: 'fixed',
4263
+ top: '5vh',
4264
+ left: '50%',
4265
+ transform: 'translateX(-50%)',
4266
+ margin: '0',
4267
+ }),
3926
4268
  }}
3927
4269
  nested
3928
4270
  >
3929
- <Form
3930
- ajaxSetting={ajaxSetting}
3931
- api={api}
3932
- closeModal={modalClose}
3933
- columnId={formId}
3934
- fetchTable={handleReload}
3935
- formSettings={formData}
3936
- formType={formType}
3937
- gridRef={gridRef}
3938
- mapbox={mapbox}
3939
- modalOpen={modalOpen}
3940
- paramValue={paramValue}
3941
- style={style}
3942
- updateForm={setFormData}
3943
- userProfile={userProfile}
3944
- />
4271
+ <div
4272
+ className={`${styles.modalwrap} ${
4273
+ styles['top--modal']
4274
+ } ${styles.modalWide} ${
4275
+ formData.verticalAlign === 'top'
4276
+ ? 'modal-top-aligned'
4277
+ : ''
4278
+ }`}
4279
+ >
4280
+ <div className={styles.modal}>
4281
+ <Form
4282
+ ajaxSetting={ajaxSetting}
4283
+ api={api}
4284
+ closeModal={modalClose}
4285
+ columnId={formId}
4286
+ fetchTable={handleReload}
4287
+ formSettings={formData}
4288
+ formType={formType}
4289
+ gridRef={gridRef}
4290
+ mapbox={mapbox}
4291
+ modalOpen={modalOpen}
4292
+ paramValue={paramValue}
4293
+ style={style}
4294
+ updateForm={setFormData}
4295
+ userProfile={userProfile}
4296
+ />
4297
+ </div>
4298
+ </div>
3945
4299
  </Popup>
3946
4300
  <Popup
3947
4301
  open={modalGalleryShow}