@visns-studio/visns-components 5.8.19 → 5.8.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/crm/DataGrid.jsx +197 -86
- package/src/components/crm/generic/GenericDetail.jsx +32 -2
- package/src/components/crm/styles/DataGrid.module.scss +22 -0
- package/src/components/crm/styles/TableFilter.module.scss +32 -1
- package/src/components/crm/styles/global-datagrid.css +168 -0
- package/src/components/styles/global.css +29 -0
- package/src/examples/ImprovedCheckboxDataGrid.jsx +269 -0
package/package.json
CHANGED
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
87
87
|
},
|
|
88
88
|
"name": "@visns-studio/visns-components",
|
|
89
|
-
"version": "5.8.
|
|
89
|
+
"version": "5.8.20",
|
|
90
90
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
91
91
|
"main": "src/index.js",
|
|
92
92
|
"files": [
|
|
@@ -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,
|
|
@@ -525,47 +543,33 @@ const DataGrid = forwardRef(
|
|
|
525
543
|
|
|
526
544
|
const handleSelectionChange = useCallback(
|
|
527
545
|
(param) => {
|
|
528
|
-
//
|
|
529
|
-
if (
|
|
530
|
-
|
|
546
|
+
// Handle the selection change based on the parameter type
|
|
547
|
+
if (param.selected === true) {
|
|
548
|
+
// Select all rows - this happens when header checkbox is clicked to select all
|
|
549
|
+
const s = {};
|
|
550
|
+
param.data.forEach((obj) => {
|
|
551
|
+
s[obj.id] = obj;
|
|
552
|
+
});
|
|
553
|
+
setSelected(s);
|
|
554
|
+
// Update parent component's rowsSelected state
|
|
555
|
+
if (setRowsSelected) {
|
|
556
|
+
setRowsSelected(s);
|
|
557
|
+
}
|
|
558
|
+
} else if (
|
|
559
|
+
param.selected === false ||
|
|
531
560
|
(typeof param.selected === 'object' &&
|
|
532
|
-
Object.keys(param.selected).length
|
|
561
|
+
Object.keys(param.selected).length === 0)
|
|
533
562
|
) {
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
}
|
|
563
|
+
// Deselect all rows - this happens when header checkbox is clicked to deselect all
|
|
564
|
+
setSelected({});
|
|
565
|
+
// Update parent component's rowsSelected state
|
|
566
|
+
if (setRowsSelected) {
|
|
567
|
+
setRowsSelected({});
|
|
566
568
|
}
|
|
567
|
-
} else {
|
|
568
|
-
//
|
|
569
|
+
} else if (typeof param.selected === 'object') {
|
|
570
|
+
// Individual row selection/deselection - ReactDataGrid sends the complete new selection state
|
|
571
|
+
// ReactDataGrid sends the complete new selection state, not just the changed row
|
|
572
|
+
// So we can directly use param.selected as the new selection state
|
|
569
573
|
setSelected(param.selected);
|
|
570
574
|
// Update parent component's rowsSelected state
|
|
571
575
|
if (setRowsSelected) {
|
|
@@ -574,7 +578,7 @@ const DataGrid = forwardRef(
|
|
|
574
578
|
}
|
|
575
579
|
},
|
|
576
580
|
[selected, setRowsSelected]
|
|
577
|
-
);
|
|
581
|
+
);
|
|
578
582
|
|
|
579
583
|
const findUpdatedFilter = (filters, currentValues) => {
|
|
580
584
|
for (const filter of filters) {
|
|
@@ -1141,35 +1145,85 @@ const DataGrid = forwardRef(
|
|
|
1141
1145
|
}
|
|
1142
1146
|
}, []);
|
|
1143
1147
|
|
|
1144
|
-
const onRenderRow = useCallback(
|
|
1145
|
-
|
|
1146
|
-
|
|
1148
|
+
const onRenderRow = useCallback(
|
|
1149
|
+
(rowProps) => {
|
|
1150
|
+
// save the original handlers to be called later
|
|
1151
|
+
const { onClick } = rowProps;
|
|
1147
1152
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1153
|
+
rowProps.onClick = (event) => {
|
|
1154
|
+
const cellElement = event.target.closest(
|
|
1155
|
+
'.InovuaReactDataGrid__cell'
|
|
1156
|
+
);
|
|
1150
1157
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1158
|
+
// Add null check to prevent errors when cellElement is null
|
|
1159
|
+
if (!cellElement) {
|
|
1160
|
+
return; // Exit early if no cell element was found
|
|
1161
|
+
}
|
|
1154
1162
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1163
|
+
const columnIndex = Array.from(
|
|
1164
|
+
cellElement.parentNode.children
|
|
1165
|
+
).indexOf(cellElement);
|
|
1159
1166
|
|
|
1160
|
-
|
|
1161
|
-
cellElement.parentNode.children
|
|
1162
|
-
).indexOf(cellElement);
|
|
1167
|
+
const column = rowProps.columns[columnIndex];
|
|
1163
1168
|
|
|
1164
|
-
|
|
1169
|
+
if (column.id === '__checkbox-column') {
|
|
1170
|
+
// For checkbox column clicks, we need to handle them specially
|
|
1171
|
+
// to ensure consistent behavior whether clicking checkbox or cell area
|
|
1172
|
+
|
|
1173
|
+
// Check if clicking directly on the checkbox input
|
|
1174
|
+
const isCheckboxInput = event.target.matches(
|
|
1175
|
+
'input[type="checkbox"]'
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
if (!isCheckboxInput) {
|
|
1179
|
+
// If clicking on empty space in checkbox cell, simulate checkbox click
|
|
1180
|
+
// by manually toggling the selection state
|
|
1181
|
+
event.preventDefault();
|
|
1182
|
+
event.stopPropagation();
|
|
1183
|
+
|
|
1184
|
+
const rowId =
|
|
1185
|
+
rowProps.data[rowProps.idProperty || 'id'];
|
|
1186
|
+
const isCurrentlySelected =
|
|
1187
|
+
selected.hasOwnProperty(rowId);
|
|
1188
|
+
|
|
1189
|
+
let newSelected;
|
|
1190
|
+
if (isCurrentlySelected) {
|
|
1191
|
+
// Remove from selection
|
|
1192
|
+
newSelected = { ...selected };
|
|
1193
|
+
delete newSelected[rowId];
|
|
1194
|
+
} else {
|
|
1195
|
+
// Add to selection
|
|
1196
|
+
newSelected = {
|
|
1197
|
+
...selected,
|
|
1198
|
+
[rowId]: rowProps.data,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
setSelected(newSelected);
|
|
1203
|
+
if (setRowsSelected) {
|
|
1204
|
+
setRowsSelected(newSelected);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return; // Don't call the original onClick
|
|
1208
|
+
}
|
|
1165
1209
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1210
|
+
// If clicking directly on checkbox input, let ReactDataGrid handle it
|
|
1211
|
+
if (onClick) {
|
|
1212
|
+
onClick(event);
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
// For non-checkbox columns, handle normal row clicks
|
|
1216
|
+
onRowClick(rowProps, event);
|
|
1217
|
+
|
|
1218
|
+
// Call the original onClick handler
|
|
1219
|
+
if (onClick) {
|
|
1220
|
+
onClick(event);
|
|
1221
|
+
}
|
|
1169
1222
|
}
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1223
|
+
};
|
|
1224
|
+
},
|
|
1225
|
+
[selected, setRowsSelected]
|
|
1226
|
+
);
|
|
1173
1227
|
|
|
1174
1228
|
const exportTrigger = async () => {
|
|
1175
1229
|
try {
|
|
@@ -1403,14 +1457,29 @@ const DataGrid = forwardRef(
|
|
|
1403
1457
|
);
|
|
1404
1458
|
};
|
|
1405
1459
|
|
|
1406
|
-
|
|
1460
|
+
// Helper function to determine if auto refresh should be relocated
|
|
1461
|
+
const shouldRelocateAutoRefresh = () => {
|
|
1462
|
+
const isSearchDisabled = form.searchDisable === true;
|
|
1463
|
+
const isCreateDisabled = form.createDisable === true;
|
|
1464
|
+
const hasAutoRefresh =
|
|
1465
|
+
ajaxSetting && ajaxSetting.autoRefresh !== undefined;
|
|
1466
|
+
|
|
1467
|
+
// Relocate only if auto refresh is the sole control (no search, no create button)
|
|
1468
|
+
return hasAutoRefresh && isSearchDisabled && isCreateDisabled;
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
const renderAutoRefreshControls = (isRelocated = false) => {
|
|
1407
1472
|
// Only show auto-refresh if the setting is present in ajaxSetting
|
|
1408
1473
|
if (!ajaxSetting || ajaxSetting.autoRefresh === undefined) {
|
|
1409
1474
|
return null;
|
|
1410
1475
|
}
|
|
1411
1476
|
|
|
1477
|
+
const containerClass = isRelocated
|
|
1478
|
+
? styles.autoRefreshContainerRelocated
|
|
1479
|
+
: styles.autoRefreshContainer;
|
|
1480
|
+
|
|
1412
1481
|
return (
|
|
1413
|
-
<div className={
|
|
1482
|
+
<div className={containerClass}>
|
|
1414
1483
|
<label className={styles.autoRefreshLabel}>
|
|
1415
1484
|
<input
|
|
1416
1485
|
type="checkbox"
|
|
@@ -3846,14 +3915,20 @@ const DataGrid = forwardRef(
|
|
|
3846
3915
|
}}
|
|
3847
3916
|
>
|
|
3848
3917
|
{/* Top container with search, action buttons, and auto-refresh */}
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
<div className={styles.
|
|
3854
|
-
{
|
|
3918
|
+
{/* Only show top container if there are controls to display */}
|
|
3919
|
+
{(renderSearch() ||
|
|
3920
|
+
(!shouldRelocateAutoRefresh() &&
|
|
3921
|
+
renderAutoRefreshControls())) && (
|
|
3922
|
+
<div className={styles.dataGridTopContainer}>
|
|
3923
|
+
<div className={styles.dataGridTopLeftContainer}>
|
|
3924
|
+
{renderSearch()}
|
|
3925
|
+
</div>
|
|
3926
|
+
<div className={styles.dataGridTopRightContainer}>
|
|
3927
|
+
{!shouldRelocateAutoRefresh() &&
|
|
3928
|
+
renderAutoRefreshControls()}
|
|
3929
|
+
</div>
|
|
3855
3930
|
</div>
|
|
3856
|
-
|
|
3931
|
+
)}
|
|
3857
3932
|
|
|
3858
3933
|
{/* DataGrid container with its own border */}
|
|
3859
3934
|
<div className={styles.dataGridContainer}>
|
|
@@ -3911,9 +3986,26 @@ const DataGrid = forwardRef(
|
|
|
3911
3986
|
borderRadius: 'var(--br, 4px)',
|
|
3912
3987
|
overflow: 'hidden',
|
|
3913
3988
|
}}
|
|
3989
|
+
checkboxColumnProps={{
|
|
3990
|
+
width: 60,
|
|
3991
|
+
minWidth: 60,
|
|
3992
|
+
maxWidth: 60,
|
|
3993
|
+
resizable: false,
|
|
3994
|
+
sortable: false,
|
|
3995
|
+
headerAlign: 'center',
|
|
3996
|
+
textAlign: 'center',
|
|
3997
|
+
}}
|
|
3914
3998
|
/>
|
|
3915
3999
|
) : null}
|
|
3916
4000
|
</div>
|
|
4001
|
+
|
|
4002
|
+
{/* Relocated auto refresh control when it's the only control */}
|
|
4003
|
+
{shouldRelocateAutoRefresh() && (
|
|
4004
|
+
<div className={styles.dataGridBottomContainer}>
|
|
4005
|
+
{renderAutoRefreshControls(true)}
|
|
4006
|
+
</div>
|
|
4007
|
+
)}
|
|
4008
|
+
|
|
3917
4009
|
{audioSource && <audio src={audioSource} ref={audioRef} />}
|
|
3918
4010
|
<Popup
|
|
3919
4011
|
open={modalShow}
|
|
@@ -3923,25 +4015,44 @@ const DataGrid = forwardRef(
|
|
|
3923
4015
|
width: windowWidth < 768 ? '95vw' : '80vw',
|
|
3924
4016
|
minWidth: windowWidth < 768 ? '95vw' : '600px',
|
|
3925
4017
|
maxWidth: '1200px',
|
|
4018
|
+
...(formData.verticalAlign === 'top' && {
|
|
4019
|
+
position: 'fixed',
|
|
4020
|
+
top: '5vh',
|
|
4021
|
+
left: '50%',
|
|
4022
|
+
transform: 'translateX(-50%)',
|
|
4023
|
+
margin: '0',
|
|
4024
|
+
}),
|
|
3926
4025
|
}}
|
|
3927
4026
|
nested
|
|
3928
4027
|
>
|
|
3929
|
-
<
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
4028
|
+
<div
|
|
4029
|
+
className={`${styles.modalwrap} ${
|
|
4030
|
+
styles['top--modal']
|
|
4031
|
+
} ${styles.modalWide} ${
|
|
4032
|
+
formData.verticalAlign === 'top'
|
|
4033
|
+
? 'modal-top-aligned'
|
|
4034
|
+
: ''
|
|
4035
|
+
}`}
|
|
4036
|
+
>
|
|
4037
|
+
<div className={styles.modal}>
|
|
4038
|
+
<Form
|
|
4039
|
+
ajaxSetting={ajaxSetting}
|
|
4040
|
+
api={api}
|
|
4041
|
+
closeModal={modalClose}
|
|
4042
|
+
columnId={formId}
|
|
4043
|
+
fetchTable={handleReload}
|
|
4044
|
+
formSettings={formData}
|
|
4045
|
+
formType={formType}
|
|
4046
|
+
gridRef={gridRef}
|
|
4047
|
+
mapbox={mapbox}
|
|
4048
|
+
modalOpen={modalOpen}
|
|
4049
|
+
paramValue={paramValue}
|
|
4050
|
+
style={style}
|
|
4051
|
+
updateForm={setFormData}
|
|
4052
|
+
userProfile={userProfile}
|
|
4053
|
+
/>
|
|
4054
|
+
</div>
|
|
4055
|
+
</div>
|
|
3945
4056
|
</Popup>
|
|
3946
4057
|
<Popup
|
|
3947
4058
|
open={modalGalleryShow}
|
|
@@ -17,6 +17,20 @@ import '../../styles/global.css';
|
|
|
17
17
|
* // other form properties
|
|
18
18
|
* }
|
|
19
19
|
* }
|
|
20
|
+
*
|
|
21
|
+
* Modal Vertical Alignment:
|
|
22
|
+
* To position the modal toward the top of the viewport instead of center, add the `verticalAlign: "top"` property to the form object.
|
|
23
|
+
* Example:
|
|
24
|
+
* {
|
|
25
|
+
* id: 'overview',
|
|
26
|
+
* type: 'overview',
|
|
27
|
+
* form: {
|
|
28
|
+
* verticalAlign: "top",
|
|
29
|
+
* primaryKey: "id",
|
|
30
|
+
* url: "/ajax/jobs",
|
|
31
|
+
* // other form properties
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
20
34
|
*/
|
|
21
35
|
|
|
22
36
|
import React, { useEffect, useRef, useState } from 'react';
|
|
@@ -2753,9 +2767,25 @@ function GenericDetail({
|
|
|
2753
2767
|
open={modalShow}
|
|
2754
2768
|
onClose={modalClose}
|
|
2755
2769
|
closeOnDocumentClick={false}
|
|
2756
|
-
contentStyle={{
|
|
2770
|
+
contentStyle={{
|
|
2771
|
+
width: '80vw',
|
|
2772
|
+
maxWidth: '1200px',
|
|
2773
|
+
...(formData.verticalAlign === 'top' && {
|
|
2774
|
+
position: 'fixed',
|
|
2775
|
+
top: '5vh',
|
|
2776
|
+
left: '50%',
|
|
2777
|
+
transform: 'translateX(-50%)',
|
|
2778
|
+
margin: '0',
|
|
2779
|
+
}),
|
|
2780
|
+
}}
|
|
2757
2781
|
>
|
|
2758
|
-
<div
|
|
2782
|
+
<div
|
|
2783
|
+
className={`modalwrap top--modal modalWide ${
|
|
2784
|
+
formData.verticalAlign === 'top'
|
|
2785
|
+
? 'modal-top-aligned'
|
|
2786
|
+
: ''
|
|
2787
|
+
}`}
|
|
2788
|
+
>
|
|
2759
2789
|
<div className="modal">
|
|
2760
2790
|
<Form
|
|
2761
2791
|
closeModal={modalClose}
|
|
@@ -594,6 +594,19 @@
|
|
|
594
594
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
|
595
595
|
}
|
|
596
596
|
|
|
597
|
+
/* Relocated auto-refresh controls (when placed at bottom) */
|
|
598
|
+
.autoRefreshContainerRelocated {
|
|
599
|
+
display: inline-flex;
|
|
600
|
+
align-items: center;
|
|
601
|
+
padding: 0 15px;
|
|
602
|
+
height: 38px;
|
|
603
|
+
background-color: #f9fafb;
|
|
604
|
+
border: 1px solid #d1d5db;
|
|
605
|
+
border-radius: var(--br, 4px);
|
|
606
|
+
margin: 0;
|
|
607
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
|
608
|
+
}
|
|
609
|
+
|
|
597
610
|
.autoRefreshLabel {
|
|
598
611
|
display: flex;
|
|
599
612
|
align-items: center;
|
|
@@ -720,6 +733,15 @@
|
|
|
720
733
|
}
|
|
721
734
|
}
|
|
722
735
|
|
|
736
|
+
/* Bottom container for relocated controls */
|
|
737
|
+
.dataGridBottomContainer {
|
|
738
|
+
display: flex;
|
|
739
|
+
justify-content: flex-end;
|
|
740
|
+
align-items: center;
|
|
741
|
+
margin-top: 8px;
|
|
742
|
+
padding: 0 5px;
|
|
743
|
+
}
|
|
744
|
+
|
|
723
745
|
/* Original filter input styles kept for backward compatibility */
|
|
724
746
|
.filterInput {
|
|
725
747
|
display: flex;
|
|
@@ -193,7 +193,38 @@
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/* Media queries for responsive design */
|
|
196
|
-
|
|
196
|
+
|
|
197
|
+
/* Tablet view (961px to 1024px) - Keep filter expanded in left container */
|
|
198
|
+
@media (max-width: 1024px) and (min-width: 961px) {
|
|
199
|
+
/* Ensure filter remains visible and properly styled for tablets */
|
|
200
|
+
.tableFilter {
|
|
201
|
+
display: block;
|
|
202
|
+
/* Maintain normal styling for tablet view */
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.mobileToggle {
|
|
206
|
+
display: none; /* Hide hamburger menu on tablets */
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Slightly adjust padding for better tablet experience */
|
|
210
|
+
.parentLink {
|
|
211
|
+
padding: 9px 16px;
|
|
212
|
+
font-size: 0.9rem;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.link {
|
|
216
|
+
padding: 9px 16px;
|
|
217
|
+
font-size: 0.85rem;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.childLink {
|
|
221
|
+
padding: 7px 16px 7px 26px;
|
|
222
|
+
font-size: 0.85rem;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* Phone view (960px and below) - Use hamburger menu when navigation moves to top */
|
|
227
|
+
@media (max-width: 960px) {
|
|
197
228
|
.mobileToggle {
|
|
198
229
|
display: flex;
|
|
199
230
|
}
|
|
@@ -32,3 +32,171 @@
|
|
|
32
32
|
outline: none !important;
|
|
33
33
|
box-shadow: 0 0 0 2px rgba(var(--primary-rgb, 59, 130, 246), 0.2) !important;
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/* Checkbox column improvements for touch devices */
|
|
37
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column'] {
|
|
38
|
+
padding: 8px 12px !important;
|
|
39
|
+
min-width: 60px !important;
|
|
40
|
+
width: 60px !important;
|
|
41
|
+
max-width: 60px !important;
|
|
42
|
+
position: relative !important;
|
|
43
|
+
cursor: pointer !important;
|
|
44
|
+
display: flex !important;
|
|
45
|
+
align-items: center !important;
|
|
46
|
+
justify-content: center !important;
|
|
47
|
+
/* Ensure the entire cell is clickable */
|
|
48
|
+
user-select: none !important;
|
|
49
|
+
-webkit-user-select: none !important;
|
|
50
|
+
-moz-user-select: none !important;
|
|
51
|
+
-ms-user-select: none !important;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Improve checkbox touch targets */
|
|
55
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
56
|
+
input[type='checkbox'] {
|
|
57
|
+
width: 20px !important;
|
|
58
|
+
height: 20px !important;
|
|
59
|
+
margin: 0 !important;
|
|
60
|
+
cursor: pointer !important;
|
|
61
|
+
position: relative !important;
|
|
62
|
+
z-index: 2 !important;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Create larger touch target area around checkbox */
|
|
66
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
67
|
+
.InovuaReactDataGrid__checkbox-wrapper,
|
|
68
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
69
|
+
.InovuaReactDataGrid__checkbox {
|
|
70
|
+
display: flex !important;
|
|
71
|
+
align-items: center !important;
|
|
72
|
+
justify-content: center !important;
|
|
73
|
+
min-height: 44px !important;
|
|
74
|
+
min-width: 44px !important;
|
|
75
|
+
padding: 12px !important;
|
|
76
|
+
margin: -12px !important;
|
|
77
|
+
cursor: pointer !important;
|
|
78
|
+
border-radius: 4px !important;
|
|
79
|
+
transition: background-color 0.2s ease !important;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Touch-friendly hover effect for the entire cell */
|
|
83
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']:hover {
|
|
84
|
+
background-color: rgba(var(--primary-rgb, 59, 130, 246), 0.05) !important;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Touch-friendly hover effect for checkbox wrapper */
|
|
88
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
89
|
+
.InovuaReactDataGrid__checkbox-wrapper:hover,
|
|
90
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
91
|
+
.InovuaReactDataGrid__checkbox:hover {
|
|
92
|
+
background-color: rgba(var(--primary-rgb, 59, 130, 246), 0.1) !important;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Header checkbox improvements */
|
|
96
|
+
.InovuaReactDataGrid__header-cell[data-column-id='__checkbox-column'] {
|
|
97
|
+
padding: 8px 12px !important;
|
|
98
|
+
min-width: 60px !important;
|
|
99
|
+
width: 60px !important;
|
|
100
|
+
max-width: 60px !important;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.InovuaReactDataGrid__header-cell[data-column-id='__checkbox-column']
|
|
104
|
+
input[type='checkbox'] {
|
|
105
|
+
width: 20px !important;
|
|
106
|
+
height: 20px !important;
|
|
107
|
+
margin: 0 !important;
|
|
108
|
+
cursor: pointer !important;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Prevent text selection in checkbox column */
|
|
112
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column'] {
|
|
113
|
+
user-select: none !important;
|
|
114
|
+
-webkit-user-select: none !important;
|
|
115
|
+
-moz-user-select: none !important;
|
|
116
|
+
-ms-user-select: none !important;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Additional checkbox styling for better UX */
|
|
120
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
121
|
+
.InovuaReactDataGrid__cell-content {
|
|
122
|
+
display: flex !important;
|
|
123
|
+
align-items: center !important;
|
|
124
|
+
justify-content: center !important;
|
|
125
|
+
width: 100% !important;
|
|
126
|
+
height: 100% !important;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Focus styles for accessibility */
|
|
130
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
131
|
+
input[type='checkbox']:focus {
|
|
132
|
+
outline: 2px solid rgba(var(--primary-rgb, 59, 130, 246), 0.5) !important;
|
|
133
|
+
outline-offset: 2px !important;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Ensure checkbox column doesn't interfere with row hover */
|
|
137
|
+
.InovuaReactDataGrid__row:hover
|
|
138
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column'] {
|
|
139
|
+
background-color: inherit !important;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Mobile-specific improvements */
|
|
143
|
+
@media (max-width: 768px) {
|
|
144
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column'] {
|
|
145
|
+
min-width: 70px !important;
|
|
146
|
+
width: 70px !important;
|
|
147
|
+
max-width: 70px !important;
|
|
148
|
+
padding: 12px 16px !important;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
152
|
+
input[type='checkbox'] {
|
|
153
|
+
width: 24px !important;
|
|
154
|
+
height: 24px !important;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
158
|
+
.InovuaReactDataGrid__checkbox-wrapper,
|
|
159
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
160
|
+
.InovuaReactDataGrid__checkbox {
|
|
161
|
+
min-height: 48px !important;
|
|
162
|
+
min-width: 48px !important;
|
|
163
|
+
padding: 16px !important;
|
|
164
|
+
margin: -16px !important;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.InovuaReactDataGrid__header-cell[data-column-id='__checkbox-column'] {
|
|
168
|
+
min-width: 70px !important;
|
|
169
|
+
width: 70px !important;
|
|
170
|
+
max-width: 70px !important;
|
|
171
|
+
padding: 12px 16px !important;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.InovuaReactDataGrid__header-cell[data-column-id='__checkbox-column']
|
|
175
|
+
input[type='checkbox'] {
|
|
176
|
+
width: 24px !important;
|
|
177
|
+
height: 24px !important;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Touch device specific improvements */
|
|
182
|
+
@media (hover: none) and (pointer: coarse) {
|
|
183
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column'] {
|
|
184
|
+
padding: 16px !important;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
188
|
+
input[type='checkbox'] {
|
|
189
|
+
width: 28px !important;
|
|
190
|
+
height: 28px !important;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
194
|
+
.InovuaReactDataGrid__checkbox-wrapper,
|
|
195
|
+
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
196
|
+
.InovuaReactDataGrid__checkbox {
|
|
197
|
+
min-height: 52px !important;
|
|
198
|
+
min-width: 52px !important;
|
|
199
|
+
padding: 18px !important;
|
|
200
|
+
margin: -18px !important;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -228,6 +228,35 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
|
|
|
228
228
|
max-width: 1200px !important;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/* Top-aligned modal styles */
|
|
232
|
+
.modal-top-aligned {
|
|
233
|
+
max-height: 90vh !important;
|
|
234
|
+
overflow-y: auto !important;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Ensure modal content is scrollable when top-aligned */
|
|
238
|
+
.modal-top-aligned .modal__content {
|
|
239
|
+
max-height: calc(90vh - 60px) !important;
|
|
240
|
+
overflow-y: auto !important;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Responsive adjustments for top-aligned modals */
|
|
244
|
+
@media (max-width: 768px) {
|
|
245
|
+
.popup-content[style*='top: 5vh'] {
|
|
246
|
+
top: 2vh !important;
|
|
247
|
+
width: 95vw !important;
|
|
248
|
+
max-width: none !important;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.modal-top-aligned {
|
|
252
|
+
max-height: 95vh !important;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.modal-top-aligned .modal__content {
|
|
256
|
+
max-height: calc(95vh - 60px) !important;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
231
260
|
.AutocompletePlace-results {
|
|
232
261
|
z-index: 999;
|
|
233
262
|
width: 500px;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import DataGrid from '../components/crm/DataGrid';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Example demonstrating the improved checkbox column functionality in DataGrid
|
|
6
|
+
*
|
|
7
|
+
* Key improvements:
|
|
8
|
+
* 1. Larger touch targets for mobile devices
|
|
9
|
+
* 2. Fixed unintended "uncheck all" behavior when clicking empty space
|
|
10
|
+
* 3. Better visual feedback for clickable areas
|
|
11
|
+
* 4. Maintained existing selection functionality
|
|
12
|
+
*/
|
|
13
|
+
const ImprovedCheckboxDataGrid = () => {
|
|
14
|
+
const [rowsSelected, setRowsSelected] = useState({});
|
|
15
|
+
const [tableData, setTableData] = useState([]);
|
|
16
|
+
const [total, setTotal] = useState(0);
|
|
17
|
+
|
|
18
|
+
// Example configuration with checkbox functionality enabled
|
|
19
|
+
const exampleConfig = {
|
|
20
|
+
ajaxSetting: {
|
|
21
|
+
url: '/api/example/data',
|
|
22
|
+
take: 25,
|
|
23
|
+
sortBy: 'id',
|
|
24
|
+
sort: 'desc',
|
|
25
|
+
where: [],
|
|
26
|
+
},
|
|
27
|
+
columns: [
|
|
28
|
+
{
|
|
29
|
+
id: 'id',
|
|
30
|
+
label: 'ID',
|
|
31
|
+
type: 'text',
|
|
32
|
+
minWidth: 80,
|
|
33
|
+
maxWidth: 80,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'name',
|
|
37
|
+
label: 'Name',
|
|
38
|
+
type: 'text',
|
|
39
|
+
flex: 1,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'email',
|
|
43
|
+
label: 'Email',
|
|
44
|
+
type: 'text',
|
|
45
|
+
flex: 1,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'status',
|
|
49
|
+
label: 'Status',
|
|
50
|
+
type: 'text',
|
|
51
|
+
minWidth: 100,
|
|
52
|
+
maxWidth: 100,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
settings: [
|
|
56
|
+
{
|
|
57
|
+
id: 'edit',
|
|
58
|
+
label: 'Edit',
|
|
59
|
+
icon: 'edit',
|
|
60
|
+
url: '/edit/{id}',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'delete',
|
|
64
|
+
label: 'Delete',
|
|
65
|
+
icon: 'delete',
|
|
66
|
+
url: '/delete/{id}',
|
|
67
|
+
method: 'DELETE',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
// Enable checkbox column functionality
|
|
71
|
+
tableSetting: {
|
|
72
|
+
checkboxColumn: true,
|
|
73
|
+
enableSelection: true,
|
|
74
|
+
},
|
|
75
|
+
functions: {
|
|
76
|
+
checkboxUpdate: {
|
|
77
|
+
url: '/api/bulk-update',
|
|
78
|
+
method: 'POST',
|
|
79
|
+
data: {},
|
|
80
|
+
message: {
|
|
81
|
+
warning: 'Please select at least one item to update.',
|
|
82
|
+
success: 'Items updated successfully!',
|
|
83
|
+
error: 'Failed to update items.',
|
|
84
|
+
},
|
|
85
|
+
label: 'Update Selected',
|
|
86
|
+
},
|
|
87
|
+
checkboxDelete: {
|
|
88
|
+
url: '/api/bulk-delete',
|
|
89
|
+
method: 'DELETE',
|
|
90
|
+
data: {},
|
|
91
|
+
message: {
|
|
92
|
+
warning: 'Please select at least one item to delete.',
|
|
93
|
+
success: 'Items deleted successfully!',
|
|
94
|
+
error: 'Failed to delete items.',
|
|
95
|
+
},
|
|
96
|
+
label: 'Delete Selected',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleBulkUpdate = () => {
|
|
102
|
+
const selectedCount = Object.keys(rowsSelected).length;
|
|
103
|
+
console.log(
|
|
104
|
+
`Bulk update triggered for ${selectedCount} items:`,
|
|
105
|
+
rowsSelected
|
|
106
|
+
);
|
|
107
|
+
// In a real implementation, this would call the API
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleBulkDelete = () => {
|
|
111
|
+
const selectedCount = Object.keys(rowsSelected).length;
|
|
112
|
+
console.log(
|
|
113
|
+
`Bulk delete triggered for ${selectedCount} items:`,
|
|
114
|
+
rowsSelected
|
|
115
|
+
);
|
|
116
|
+
// In a real implementation, this would call the API
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div style={{ padding: '20px' }}>
|
|
121
|
+
<h2>Improved Checkbox DataGrid Example</h2>
|
|
122
|
+
|
|
123
|
+
<div style={{ marginBottom: '20px' }}>
|
|
124
|
+
<h3>Key Improvements:</h3>
|
|
125
|
+
<ul>
|
|
126
|
+
<li>
|
|
127
|
+
<strong>Fixed checkbox cell clicks:</strong> Clicking
|
|
128
|
+
anywhere in a checkbox cell (including empty space) now
|
|
129
|
+
properly toggles ONLY that row
|
|
130
|
+
</li>
|
|
131
|
+
<li>
|
|
132
|
+
<strong>Eliminated "clear all" bug:</strong> No more
|
|
133
|
+
accidental clearing of all selections when clicking
|
|
134
|
+
checkbox cell areas
|
|
135
|
+
</li>
|
|
136
|
+
<li>
|
|
137
|
+
<strong>Consistent toggle behavior:</strong> Both
|
|
138
|
+
checkbox input clicks and cell area clicks now behave
|
|
139
|
+
identically
|
|
140
|
+
</li>
|
|
141
|
+
<li>
|
|
142
|
+
<strong>Multiple selection support:</strong> Can
|
|
143
|
+
properly select/deselect individual rows when multiple
|
|
144
|
+
rows are already selected
|
|
145
|
+
</li>
|
|
146
|
+
<li>
|
|
147
|
+
<strong>Header-only bulk actions:</strong>{' '}
|
|
148
|
+
Select/unselect all only works from header checkbox
|
|
149
|
+
</li>
|
|
150
|
+
<li>
|
|
151
|
+
<strong>Touch-friendly targets:</strong> Larger
|
|
152
|
+
clickable areas for mobile devices
|
|
153
|
+
</li>
|
|
154
|
+
</ul>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div style={{ marginBottom: '20px' }}>
|
|
158
|
+
<p>
|
|
159
|
+
<strong>Selected items:</strong>{' '}
|
|
160
|
+
{Object.keys(rowsSelected).length}
|
|
161
|
+
</p>
|
|
162
|
+
{Object.keys(rowsSelected).length > 0 && (
|
|
163
|
+
<div style={{ display: 'flex', gap: '10px' }}>
|
|
164
|
+
<button
|
|
165
|
+
onClick={handleBulkUpdate}
|
|
166
|
+
style={{
|
|
167
|
+
padding: '8px 16px',
|
|
168
|
+
backgroundColor: '#3b82f6',
|
|
169
|
+
color: 'white',
|
|
170
|
+
border: 'none',
|
|
171
|
+
borderRadius: '4px',
|
|
172
|
+
cursor: 'pointer',
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
Update Selected ({Object.keys(rowsSelected).length})
|
|
176
|
+
</button>
|
|
177
|
+
<button
|
|
178
|
+
onClick={handleBulkDelete}
|
|
179
|
+
style={{
|
|
180
|
+
padding: '8px 16px',
|
|
181
|
+
backgroundColor: '#ef4444',
|
|
182
|
+
color: 'white',
|
|
183
|
+
border: 'none',
|
|
184
|
+
borderRadius: '4px',
|
|
185
|
+
cursor: 'pointer',
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
Delete Selected ({Object.keys(rowsSelected).length})
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div
|
|
195
|
+
style={{
|
|
196
|
+
border: '1px solid #d1d5db',
|
|
197
|
+
borderRadius: '8px',
|
|
198
|
+
overflow: 'hidden',
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
<DataGrid
|
|
202
|
+
{...exampleConfig}
|
|
203
|
+
setRowsSelected={setRowsSelected}
|
|
204
|
+
setTableData={setTableData}
|
|
205
|
+
setTotal={setTotal}
|
|
206
|
+
gridHeight={400}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div
|
|
211
|
+
style={{
|
|
212
|
+
marginTop: '20px',
|
|
213
|
+
fontSize: '14px',
|
|
214
|
+
color: '#6b7280',
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<h4>Testing Instructions:</h4>
|
|
218
|
+
<ol>
|
|
219
|
+
<li>
|
|
220
|
+
<strong>Test checkbox input clicks:</strong> Click
|
|
221
|
+
directly on checkbox inputs - should toggle individual
|
|
222
|
+
rows
|
|
223
|
+
</li>
|
|
224
|
+
<li>
|
|
225
|
+
<strong>Test cell area clicks:</strong> Click on empty
|
|
226
|
+
space within checkbox cells - should behave identically
|
|
227
|
+
to checkbox clicks
|
|
228
|
+
</li>
|
|
229
|
+
<li>
|
|
230
|
+
<strong>Test multiple selections:</strong> Select 2-3
|
|
231
|
+
rows, then try to deselect individual rows - should work
|
|
232
|
+
properly
|
|
233
|
+
</li>
|
|
234
|
+
<li>
|
|
235
|
+
<strong>Test header checkbox:</strong> Click header
|
|
236
|
+
checkbox to select/unselect ALL rows
|
|
237
|
+
</li>
|
|
238
|
+
<li>
|
|
239
|
+
<strong>Test mixed interactions:</strong> Mix checkbox
|
|
240
|
+
input clicks and cell area clicks - should behave
|
|
241
|
+
consistently
|
|
242
|
+
</li>
|
|
243
|
+
<li>
|
|
244
|
+
<strong>Touch devices:</strong> Verify larger touch
|
|
245
|
+
targets work properly on mobile devices
|
|
246
|
+
</li>
|
|
247
|
+
</ol>
|
|
248
|
+
|
|
249
|
+
<h4>Expected Behavior:</h4>
|
|
250
|
+
<ul>
|
|
251
|
+
<li>
|
|
252
|
+
✅ Clicking any part of a checkbox cell toggles that
|
|
253
|
+
specific row
|
|
254
|
+
</li>
|
|
255
|
+
<li>
|
|
256
|
+
✅ Header checkbox controls select/unselect all
|
|
257
|
+
functionality
|
|
258
|
+
</li>
|
|
259
|
+
<li>✅ No more unexpected "uncheck all" behavior</li>
|
|
260
|
+
<li>
|
|
261
|
+
✅ Consistent behavior across desktop and mobile devices
|
|
262
|
+
</li>
|
|
263
|
+
</ul>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export default ImprovedCheckboxDataGrid;
|