@visns-studio/visns-components 5.9.0 → 5.9.1

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 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.9.0",
89
+ "version": "5.9.1",
90
90
  "description": "Various packages to assist in the development of our Custom Applications.",
91
91
  "main": "src/index.js",
92
92
  "files": [
@@ -86,6 +86,101 @@ import Download from './Download';
86
86
  import Form from './Form';
87
87
  import _ from 'lodash';
88
88
 
89
+ // CSS styles to ensure consistent group header heights with compact design and click indicators
90
+ const groupHeaderStyles = `
91
+ .datagrid-fixed-group-headers .group-header-fixed-height {
92
+ height: 34px !important;
93
+ min-height: 34px !important;
94
+ max-height: 34px !important;
95
+ box-sizing: border-box !important;
96
+ line-height: 1.1 !important;
97
+ flex-shrink: 0 !important;
98
+ overflow: hidden !important;
99
+ transition: background-color 0.2s ease, box-shadow 0.2s ease !important;
100
+ cursor: pointer !important;
101
+ position: relative !important;
102
+ }
103
+
104
+ /* Hover effect for entire group header */
105
+ .datagrid-fixed-group-headers .group-header-fixed-height:hover {
106
+ background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%) !important;
107
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important;
108
+ transform: translateY(-1px) !important;
109
+ }
110
+
111
+ /* Click indicator overlay */
112
+ .datagrid-fixed-group-headers .group-header-fixed-height::after {
113
+ content: '' !important;
114
+ position: absolute !important;
115
+ top: 0 !important;
116
+ left: 0 !important;
117
+ right: 0 !important;
118
+ bottom: 0 !important;
119
+ background: rgba(59, 130, 246, 0.05) !important;
120
+ opacity: 0 !important;
121
+ transition: opacity 0.2s ease !important;
122
+ pointer-events: none !important;
123
+ }
124
+
125
+ .datagrid-fixed-group-headers .group-header-fixed-height:hover::after {
126
+ opacity: 1 !important;
127
+ }
128
+
129
+ .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row {
130
+ height: 34px !important;
131
+ min-height: 34px !important;
132
+ max-height: 34px !important;
133
+ }
134
+
135
+ .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row .InovuaReactDataGrid__group-cell {
136
+ height: 34px !important;
137
+ min-height: 34px !important;
138
+ max-height: 34px !important;
139
+ padding: 0 !important;
140
+ overflow: hidden !important;
141
+ }
142
+
143
+ .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row .InovuaReactDataGrid__group-cell > div {
144
+ height: 34px !important;
145
+ min-height: 34px !important;
146
+ max-height: 34px !important;
147
+ overflow: hidden !important;
148
+ }
149
+
150
+ /* Ensure group headers maintain consistent styling during expand/collapse */
151
+ .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row--collapsed .group-header-fixed-height,
152
+ .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row--expanded .group-header-fixed-height {
153
+ height: 34px !important;
154
+ min-height: 34px !important;
155
+ max-height: 34px !important;
156
+ transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease !important;
157
+ }
158
+
159
+ /* Force consistent height for all group headers regardless of content */
160
+ .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row[data-group-by] {
161
+ height: 34px !important;
162
+ min-height: 34px !important;
163
+ max-height: 34px !important;
164
+ }
165
+
166
+ .datagrid-fixed-group-headers .InovuaReactDataGrid__group-row[data-group-by] .InovuaReactDataGrid__group-cell {
167
+ height: 34px !important;
168
+ min-height: 34px !important;
169
+ max-height: 34px !important;
170
+ }
171
+ `;
172
+
173
+ // Inject styles into the document head
174
+ if (typeof document !== 'undefined') {
175
+ const styleId = 'datagrid-group-header-styles';
176
+ if (!document.getElementById(styleId)) {
177
+ const style = document.createElement('style');
178
+ style.id = styleId;
179
+ style.textContent = groupHeaderStyles;
180
+ document.head.appendChild(style);
181
+ }
182
+ }
183
+
89
184
  const loadData = async (
90
185
  { skip, limit, sortInfo, filterValue },
91
186
  search,
@@ -488,6 +583,75 @@ const DataGrid = forwardRef(
488
583
  /** Table States */
489
584
  const dataRef = useRef(null);
490
585
  const [dSearch, setDSearch] = useState('');
586
+
587
+ /** Group expansion state */
588
+ const [collapsedGroups, setCollapsedGroups] = useState({});
589
+
590
+ /** Force re-render trigger for group header consistency */
591
+ const [groupRenderKey, setGroupRenderKey] = useState(0);
592
+
593
+ /** Prevent rapid group selection clicks */
594
+ const groupClickTimeoutRef = useRef(null);
595
+
596
+ // Effect to ensure group headers maintain consistent styling after expand/collapse
597
+ useEffect(() => {
598
+ if (ajaxSetting?.groupBy) {
599
+ // Small delay to allow ReactDataGrid to complete its rendering
600
+ const timer = setTimeout(() => {
601
+ // Force a re-render to ensure consistent styling
602
+ setGroupRenderKey((prev) => prev + 1);
603
+
604
+ // Apply styles to any group headers that might have been missed
605
+ const groupHeaders = document.querySelectorAll(
606
+ '.group-header-fixed-height'
607
+ );
608
+ groupHeaders.forEach((header) => {
609
+ header.style.height = '34px';
610
+ header.style.minHeight = '34px';
611
+ header.style.maxHeight = '34px';
612
+ header.style.boxSizing = 'border-box';
613
+ header.style.lineHeight = '1.1';
614
+ header.style.flexShrink = '0';
615
+ header.style.overflow = 'hidden';
616
+ header.style.cursor = 'pointer';
617
+ header.style.position = 'relative';
618
+ });
619
+
620
+ // Also apply to ReactDataGrid group rows to fix height inconsistency
621
+ const groupRows = document.querySelectorAll(
622
+ '.datagrid-fixed-group-headers .InovuaReactDataGrid__group-row'
623
+ );
624
+ groupRows.forEach((row) => {
625
+ row.style.height = '34px';
626
+ row.style.minHeight = '34px';
627
+ row.style.maxHeight = '34px';
628
+
629
+ // Apply to group cells within the row
630
+ const groupCells = row.querySelectorAll(
631
+ '.InovuaReactDataGrid__group-cell'
632
+ );
633
+ groupCells.forEach((cell) => {
634
+ cell.style.height = '34px';
635
+ cell.style.minHeight = '34px';
636
+ cell.style.maxHeight = '34px';
637
+ cell.style.padding = '0';
638
+ cell.style.overflow = 'hidden';
639
+
640
+ // Apply to div elements within group cells
641
+ const divs = cell.querySelectorAll('div');
642
+ divs.forEach((div) => {
643
+ div.style.height = '34px';
644
+ div.style.minHeight = '34px';
645
+ div.style.maxHeight = '34px';
646
+ div.style.overflow = 'hidden';
647
+ });
648
+ });
649
+ });
650
+ }, 50);
651
+
652
+ return () => clearTimeout(timer);
653
+ }
654
+ }, [collapsedGroups, ajaxSetting?.groupBy]);
491
655
  const dataSource = useCallback(
492
656
  (params) => {
493
657
  const sqlResult = loadData(params, dSearch, ajaxSetting);
@@ -520,7 +684,7 @@ const DataGrid = forwardRef(
520
684
 
521
685
  return sqlResult;
522
686
  },
523
- [ajaxSetting, dSearch]
687
+ [ajaxSetting, dSearch, collapsedGroups]
524
688
  );
525
689
  const [filterDataSource, setFilterDataSource] = useState([]);
526
690
  const [filterValue, setFilterValue] = useState([]);
@@ -677,9 +841,31 @@ const DataGrid = forwardRef(
677
841
  if (param.selected === true) {
678
842
  // Select all rows - this happens when header checkbox is clicked to select all
679
843
  const s = {};
680
- param.data.forEach((obj) => {
681
- s[obj.id] = obj;
682
- });
844
+
845
+ // Always prefer dataRef.current as it contains the most reliable data
846
+ // especially when grouping is active
847
+ if (dataRef.current && Array.isArray(dataRef.current)) {
848
+ dataRef.current.forEach((obj) => {
849
+ if (
850
+ obj &&
851
+ obj.id !== undefined &&
852
+ obj.id !== null
853
+ ) {
854
+ s[obj.id] = obj;
855
+ }
856
+ });
857
+ } else if (param.data && Array.isArray(param.data)) {
858
+ // Fallback to param.data if dataRef.current is not available
859
+ param.data.forEach((obj) => {
860
+ if (
861
+ obj &&
862
+ obj.id !== undefined &&
863
+ obj.id !== null
864
+ ) {
865
+ s[obj.id] = obj;
866
+ }
867
+ });
868
+ }
683
869
  setSelected(s);
684
870
  // Update parent component's rowsSelected state
685
871
  if (setRowsSelected) {
@@ -699,17 +885,148 @@ const DataGrid = forwardRef(
699
885
  } else if (typeof param.selected === 'object') {
700
886
  // Individual row selection/deselection - ReactDataGrid sends the complete new selection state
701
887
  // 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
703
- setSelected(param.selected);
888
+ // Clean the selection to remove any group objects or invalid entries
889
+ const cleanSelection = {};
890
+ Object.keys(param.selected).forEach((key) => {
891
+ const item = param.selected[key];
892
+ // Only keep items that have valid IDs and are not group objects
893
+ if (
894
+ key !== 'undefined' &&
895
+ key !== 'null' &&
896
+ item &&
897
+ !item.__group &&
898
+ item.id !== undefined
899
+ ) {
900
+ cleanSelection[key] = item;
901
+ }
902
+ });
903
+
904
+ setSelected(cleanSelection);
704
905
  // Update parent component's rowsSelected state
705
906
  if (setRowsSelected) {
706
- setRowsSelected(param.selected);
907
+ setRowsSelected(cleanSelection);
707
908
  }
708
909
  }
709
910
  },
710
911
  [selected, setRowsSelected]
711
912
  );
712
913
 
914
+ // Group selection functions
915
+ const handleGroupSelectionChange = useCallback(
916
+ (groupValue, isChecked) => {
917
+ if (!dataRef.current || !ajaxSetting?.groupBy) {
918
+ return;
919
+ }
920
+
921
+ const groupField = ajaxSetting.groupBy[0];
922
+ const groupRows = dataRef.current.filter(
923
+ (row) => row[groupField] === groupValue
924
+ );
925
+
926
+ // Clean the selected state - remove any invalid entries
927
+ const cleanSelected = {};
928
+ Object.keys(selected).forEach((key) => {
929
+ const item = selected[key];
930
+ // Only keep items that have valid IDs and are not group objects
931
+ if (
932
+ key !== 'undefined' &&
933
+ key !== 'null' &&
934
+ item &&
935
+ !item.__group &&
936
+ item.id !== undefined
937
+ ) {
938
+ cleanSelected[key] = item;
939
+ }
940
+ });
941
+ const newSelected = { ...cleanSelected };
942
+
943
+ if (isChecked) {
944
+ // Select all rows in the group
945
+ groupRows.forEach((row) => {
946
+ if (row && row.id !== undefined && row.id !== null) {
947
+ newSelected[row.id] = row;
948
+ }
949
+ });
950
+ } else {
951
+ // Deselect all rows in the group
952
+ groupRows.forEach((row) => {
953
+ if (row && row.id !== undefined && row.id !== null) {
954
+ delete newSelected[row.id];
955
+ }
956
+ });
957
+ }
958
+
959
+ // Update the selection state
960
+ setSelected(newSelected);
961
+ if (setRowsSelected) {
962
+ setRowsSelected(newSelected);
963
+ }
964
+
965
+ // Force ReactDataGrid to update its selection using the grid API
966
+ setTimeout(() => {
967
+ if (gridRef.current && gridRef.current.setSelected) {
968
+ gridRef.current.setSelected(newSelected);
969
+ } else if (gridRef.current) {
970
+ // Try alternative methods
971
+ if (gridRef.current.updateSelected) {
972
+ gridRef.current.updateSelected(newSelected);
973
+ } else if (gridRef.current.setSelection) {
974
+ gridRef.current.setSelection(newSelected);
975
+ }
976
+ }
977
+ }, 10);
978
+ },
979
+ [selected, setRowsSelected, ajaxSetting]
980
+ );
981
+
982
+ const isGroupSelected = useCallback(
983
+ (groupValue) => {
984
+ if (!dataRef.current || !ajaxSetting?.groupBy) {
985
+ return false;
986
+ }
987
+
988
+ // Clean the selected state first
989
+ const cleanSelected = {};
990
+ Object.keys(selected).forEach((key) => {
991
+ const item = selected[key];
992
+ if (
993
+ key !== 'undefined' &&
994
+ key !== 'null' &&
995
+ item &&
996
+ !item.__group &&
997
+ item.id !== undefined
998
+ ) {
999
+ cleanSelected[key] = item;
1000
+ }
1001
+ });
1002
+
1003
+ const groupField = ajaxSetting.groupBy[0];
1004
+ const groupRows = dataRef.current.filter(
1005
+ (row) => row[groupField] === groupValue
1006
+ );
1007
+
1008
+ if (groupRows.length === 0) {
1009
+ return false;
1010
+ }
1011
+
1012
+ // Filter out rows with invalid IDs and check if all valid rows are selected
1013
+ const validRows = groupRows.filter(
1014
+ (row) => row && row.id !== undefined && row.id !== null
1015
+ );
1016
+
1017
+ if (validRows.length === 0) {
1018
+ return false;
1019
+ }
1020
+
1021
+ const allSelected = validRows.every(
1022
+ (row) => cleanSelected[row.id]
1023
+ );
1024
+
1025
+ return allSelected;
1026
+ },
1027
+ [selected, ajaxSetting]
1028
+ );
1029
+
713
1030
  const findUpdatedFilter = (filters, currentValues) => {
714
1031
  for (const filter of filters) {
715
1032
  const matchingCurrentValue = currentValues.find(
@@ -1343,6 +1660,7 @@ const DataGrid = forwardRef(
1343
1660
  }
1344
1661
  } else {
1345
1662
  // For non-checkbox columns, handle normal row clicks
1663
+ // But do NOT trigger checkbox selection
1346
1664
  onRowClick(rowProps, event);
1347
1665
 
1348
1666
  // Call the original onClick handler
@@ -1352,7 +1670,7 @@ const DataGrid = forwardRef(
1352
1670
  }
1353
1671
  };
1354
1672
  },
1355
- [selected, setRowsSelected]
1673
+ [selected, setRowsSelected, onRowClick]
1356
1674
  );
1357
1675
 
1358
1676
  const exportTrigger = async () => {
@@ -1802,6 +2120,82 @@ const DataGrid = forwardRef(
1802
2120
  }
1803
2121
  }, [selected]);
1804
2122
 
2123
+ // Add styling to grouped column header with enhanced debugging
2124
+ useEffect(() => {
2125
+ if (ajaxSetting?.groupBy && ajaxSetting.groupBy.length > 0) {
2126
+ const groupedColumnName = ajaxSetting.groupBy[0];
2127
+
2128
+ // Multiple attempts to find and style the grouped column header
2129
+ const applyGroupedStyling = () => {
2130
+ const headerCells = document.querySelectorAll(
2131
+ '.InovuaReactDataGrid__header-cell'
2132
+ );
2133
+
2134
+ headerCells.forEach((cell) => {
2135
+ const cellContent = cell.textContent?.trim();
2136
+ const dataColumnId =
2137
+ cell.getAttribute('data-column-id');
2138
+
2139
+ // Find the column that matches the grouped field
2140
+ const matchingColumn = gridColumns.find(
2141
+ (col) =>
2142
+ col.name === groupedColumnName ||
2143
+ col.header === cellContent ||
2144
+ col.id === groupedColumnName ||
2145
+ dataColumnId === groupedColumnName
2146
+ );
2147
+
2148
+ if (
2149
+ matchingColumn &&
2150
+ (matchingColumn.name === groupedColumnName ||
2151
+ matchingColumn.id === groupedColumnName ||
2152
+ dataColumnId === groupedColumnName)
2153
+ ) {
2154
+ cell.classList.add(
2155
+ 'InovuaReactDataGrid__header-cell--grouped'
2156
+ );
2157
+ // Force inline styles as backup
2158
+ cell.style.background =
2159
+ 'linear-gradient(135deg, var(--primary-color, #3b82f6) 0%, var(--secondary-color, #2563eb) 100%)';
2160
+ cell.style.color = 'white';
2161
+ cell.style.fontWeight = '800';
2162
+ cell.style.textTransform = 'uppercase';
2163
+ cell.style.letterSpacing = '0.1em';
2164
+ cell.style.borderBottom =
2165
+ '4px solid var(--secondary-color, #2563eb)';
2166
+ }
2167
+ });
2168
+
2169
+ // Also apply class to the main DataGrid container for debugging
2170
+ const dataGrids = document.querySelectorAll(
2171
+ '.InovuaReactDataGrid'
2172
+ );
2173
+ dataGrids.forEach((grid) => {
2174
+ if (
2175
+ grid.classList.contains(
2176
+ 'InovuaReactDataGrid--grouped'
2177
+ ) ||
2178
+ grid.classList.contains('datagrid-with-grouping')
2179
+ ) {
2180
+ // Main checkbox is now enabled for grouped data
2181
+ }
2182
+ });
2183
+ };
2184
+
2185
+ // Apply immediately and with delays
2186
+ applyGroupedStyling();
2187
+ const timer1 = setTimeout(applyGroupedStyling, 100);
2188
+ const timer2 = setTimeout(applyGroupedStyling, 500);
2189
+ const timer3 = setTimeout(applyGroupedStyling, 1000);
2190
+
2191
+ return () => {
2192
+ clearTimeout(timer1);
2193
+ clearTimeout(timer2);
2194
+ clearTimeout(timer3);
2195
+ };
2196
+ }
2197
+ }, [ajaxSetting?.groupBy, gridColumns]);
2198
+
1805
2199
  useEffect(() => {
1806
2200
  const delayedSetDSearch = debounce(() => {
1807
2201
  setDSearch(search);
@@ -2609,7 +3003,7 @@ const DataGrid = forwardRef(
2609
3003
  >
2610
3004
  <option value="">
2611
3005
  Select{' '}
2612
- {/^[aeiouAEIOU]/.test(
3006
+ {/^[aeioAEIO]/.test(
2613
3007
  column.label
2614
3008
  )
2615
3009
  ? 'an'
@@ -3404,7 +3798,22 @@ const DataGrid = forwardRef(
3404
3798
  );
3405
3799
  }
3406
3800
 
3407
- return null;
3801
+ // Return a placeholder div to maintain consistent row height
3802
+ return (
3803
+ <div
3804
+ style={{
3805
+ width: '28px',
3806
+ height: '28px',
3807
+ display: 'flex',
3808
+ alignItems: 'center',
3809
+ justifyContent:
3810
+ 'center',
3811
+ margin: '0 auto',
3812
+ }}
3813
+ >
3814
+ {/* Empty placeholder to maintain layout */}
3815
+ </div>
3816
+ );
3408
3817
  }
3409
3818
  },
3410
3819
  };
@@ -4179,12 +4588,395 @@ const DataGrid = forwardRef(
4179
4588
  <ReactDataGrid
4180
4589
  key={`datagrid-${ajaxSetting?.url || ''}-${
4181
4590
  ajaxSetting?.groupBy ? 'grouped' : 'ungrouped'
4182
- }`}
4591
+ }-${groupRenderKey}`}
4183
4592
  {...tableSetting}
4184
4593
  columns={gridColumns}
4185
4594
  dataSource={dataSource}
4186
4595
  {...(ajaxSetting && ajaxSetting.groupBy
4187
- ? { defaultGroupBy: ajaxSetting.groupBy }
4596
+ ? {
4597
+ defaultGroupBy: ajaxSetting.groupBy,
4598
+ defaultCollapsed: true,
4599
+ collapsedGroups: collapsedGroups,
4600
+ onGroupToggle: (groupData) => {
4601
+ // Handle group expand/collapse events
4602
+ // Update collapsed groups state
4603
+ const groupValue =
4604
+ typeof groupData === 'string'
4605
+ ? groupData
4606
+ : groupData?.value ||
4607
+ groupData?.name ||
4608
+ '';
4609
+
4610
+ setCollapsedGroups((prev) => {
4611
+ const newState = { ...prev };
4612
+ if (newState[groupValue]) {
4613
+ delete newState[groupValue];
4614
+ } else {
4615
+ newState[groupValue] = true;
4616
+ }
4617
+ console.log(
4618
+ 'Updated collapsed groups:',
4619
+ newState
4620
+ );
4621
+ return newState;
4622
+ });
4623
+ },
4624
+ renderGroupTitle: (groupData) => {
4625
+ const groupValue =
4626
+ typeof groupData === 'string'
4627
+ ? groupData
4628
+ : groupData?.value ||
4629
+ groupData?.name ||
4630
+ '';
4631
+
4632
+ // Use ReactDataGrid's built-in toggle functionality
4633
+ const collapsed =
4634
+ collapsedGroups[groupValue] ||
4635
+ false;
4636
+ const toggleGroup = () => {
4637
+ // Update the collapsed state
4638
+ setCollapsedGroups((prev) => {
4639
+ const newState = { ...prev };
4640
+ if (newState[groupValue]) {
4641
+ delete newState[
4642
+ groupValue
4643
+ ];
4644
+ } else {
4645
+ newState[
4646
+ groupValue
4647
+ ] = true;
4648
+ }
4649
+ return newState;
4650
+ });
4651
+ };
4652
+ const groupCount =
4653
+ typeof groupData === 'object'
4654
+ ? groupData?.count || 0
4655
+ : 0;
4656
+ const hasCheckboxColumn =
4657
+ gridColumns.some(
4658
+ (col) =>
4659
+ col.id ===
4660
+ '__checkbox-column'
4661
+ );
4662
+
4663
+ // Handle group header click for selection
4664
+ const handleGroupHeaderClick = (
4665
+ e
4666
+ ) => {
4667
+ // Prevent rapid clicks
4668
+ if (
4669
+ groupClickTimeoutRef.current
4670
+ ) {
4671
+ console.log(
4672
+ '🖱️ Ignoring rapid click'
4673
+ );
4674
+ return;
4675
+ }
4676
+
4677
+ // Don't trigger if clicking on the expand button or checkbox
4678
+ if (
4679
+ e.target.closest(
4680
+ '.group-expand-btn'
4681
+ ) ||
4682
+ e.target.closest(
4683
+ 'input[type="checkbox"]'
4684
+ )
4685
+ ) {
4686
+ console.log(
4687
+ '🖱️ Ignoring click on expand button or checkbox'
4688
+ );
4689
+ return;
4690
+ }
4691
+
4692
+ // Set timeout to prevent rapid clicks
4693
+ groupClickTimeoutRef.current =
4694
+ setTimeout(() => {
4695
+ groupClickTimeoutRef.current =
4696
+ null;
4697
+ }, 300);
4698
+
4699
+ // Toggle selection of all items in this group
4700
+ const isCurrentlySelected =
4701
+ isGroupSelected(groupValue);
4702
+
4703
+ handleGroupSelectionChange(
4704
+ groupValue,
4705
+ !isCurrentlySelected
4706
+ );
4707
+ };
4708
+
4709
+ return (
4710
+ <div
4711
+ className="group-header-clean group-header-fixed-height"
4712
+ onClick={
4713
+ handleGroupHeaderClick
4714
+ }
4715
+ style={{
4716
+ display: 'flex',
4717
+ alignItems: 'center',
4718
+ justifyContent:
4719
+ 'space-between',
4720
+ width: '100%',
4721
+ height: '34px', // Compact height
4722
+ padding: '6px 12px', // Reduced padding for compact design
4723
+ background:
4724
+ 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
4725
+ borderLeft:
4726
+ '4px solid var(--primary-color, #3b82f6)',
4727
+ position: 'relative',
4728
+ overflow: 'hidden',
4729
+ boxShadow:
4730
+ '0 1px 3px rgba(0,0,0,0.1)',
4731
+ borderRadius:
4732
+ '0 4px 4px 0',
4733
+ cursor: 'pointer',
4734
+ boxSizing: 'border-box',
4735
+ lineHeight: '1.1', // Compact line height
4736
+ flexShrink: 0,
4737
+ transition:
4738
+ 'background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease',
4739
+ }}
4740
+ onMouseEnter={(e) => {
4741
+ e.currentTarget.style.background =
4742
+ 'linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%)';
4743
+ e.currentTarget.style.boxShadow =
4744
+ '0 2px 8px rgba(0,0,0,0.15)';
4745
+ e.currentTarget.style.transform =
4746
+ 'translateY(-1px)';
4747
+ }}
4748
+ onMouseLeave={(e) => {
4749
+ e.currentTarget.style.background =
4750
+ 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)';
4751
+ e.currentTarget.style.boxShadow =
4752
+ '0 1px 3px rgba(0,0,0,0.1)';
4753
+ e.currentTarget.style.transform =
4754
+ 'translateY(0)';
4755
+ }}
4756
+ >
4757
+ {/* Left side - Group info and expand/collapse */}
4758
+ <div
4759
+ style={{
4760
+ display: 'flex',
4761
+ alignItems: 'center',
4762
+ flex: 1,
4763
+ zIndex: 2,
4764
+ position: 'relative',
4765
+ }}
4766
+ >
4767
+ <button
4768
+ className="group-expand-btn"
4769
+ onClick={(e) => {
4770
+ e.stopPropagation();
4771
+ if (
4772
+ typeof toggleGroup ===
4773
+ 'function'
4774
+ ) {
4775
+ toggleGroup();
4776
+ } else {
4777
+ console.warn(
4778
+ 'toggleGroup is not a function:',
4779
+ toggleGroup
4780
+ );
4781
+ }
4782
+ }}
4783
+ style={{
4784
+ background:
4785
+ 'var(--primary-color, #3b82f6)',
4786
+ border: 'none',
4787
+ borderRadius:
4788
+ '3px',
4789
+ color: 'white',
4790
+ width: '20px', // More compact
4791
+ height: '20px', // More compact
4792
+ display: 'flex',
4793
+ alignItems:
4794
+ 'center',
4795
+ justifyContent:
4796
+ 'center',
4797
+ marginRight:
4798
+ '8px', // Reduced margin
4799
+ cursor: 'pointer',
4800
+ fontSize: '11px', // Smaller font
4801
+ fontWeight:
4802
+ 'bold',
4803
+ boxShadow:
4804
+ '0 1px 2px rgba(0,0,0,0.1)',
4805
+ transition:
4806
+ 'all 0.15s ease',
4807
+ }}
4808
+ onMouseEnter={(e) => {
4809
+ e.target.style.transform =
4810
+ 'scale(1.05)';
4811
+ e.target.style.background =
4812
+ 'var(--secondary-color, #2563eb)';
4813
+ }}
4814
+ onMouseLeave={(e) => {
4815
+ e.target.style.transform =
4816
+ 'scale(1)';
4817
+ e.target.style.background =
4818
+ 'var(--primary-color, #3b82f6)';
4819
+ }}
4820
+ >
4821
+ {collapsed
4822
+ ? '+'
4823
+ : '−'}
4824
+ </button>
4825
+ <div
4826
+ style={{
4827
+ display: 'flex',
4828
+ alignItems:
4829
+ 'center', // Changed to center for compact layout
4830
+ gap: '8px', // Add gap between elements
4831
+ }}
4832
+ >
4833
+ <strong
4834
+ style={{
4835
+ fontSize:
4836
+ '0.85rem', // Slightly smaller for compact design
4837
+ color: 'var(--secondary-color, #1f2937)',
4838
+ fontWeight:
4839
+ '600',
4840
+ textTransform:
4841
+ 'uppercase',
4842
+ letterSpacing:
4843
+ '0.025em',
4844
+ }}
4845
+ >
4846
+ {groupValue}
4847
+ </strong>
4848
+ {groupCount > 0 && (
4849
+ <span
4850
+ style={{
4851
+ color: '#6b7280',
4852
+ fontSize:
4853
+ '0.75rem', // Smaller for compact design
4854
+ fontWeight:
4855
+ '500',
4856
+ }}
4857
+ >
4858
+ ({groupCount}{' '}
4859
+ {groupCount ===
4860
+ 1
4861
+ ? 'item'
4862
+ : 'items'}
4863
+ )
4864
+ </span>
4865
+ )}
4866
+ {/* Click indicator */}
4867
+ <span
4868
+ style={{
4869
+ color: '#9ca3af',
4870
+ fontSize:
4871
+ '0.7rem',
4872
+ fontStyle:
4873
+ 'italic',
4874
+ opacity: 0.8,
4875
+ marginLeft:
4876
+ 'auto',
4877
+ userSelect:
4878
+ 'none',
4879
+ }}
4880
+ >
4881
+ Click to
4882
+ select/unselect
4883
+ all
4884
+ </span>
4885
+ </div>
4886
+ </div>
4887
+
4888
+ {/* Right side - Group selection */}
4889
+ {hasCheckboxColumn && (
4890
+ <div
4891
+ style={{
4892
+ display: 'flex',
4893
+ alignItems:
4894
+ 'center',
4895
+ gap: '4px', // Reduced gap for compact design
4896
+ fontSize:
4897
+ '0.75rem', // Smaller font for compact design
4898
+ background:
4899
+ 'rgba(255, 255, 255, 0.9)',
4900
+ padding:
4901
+ '3px 8px', // Reduced padding for compact design
4902
+ borderRadius:
4903
+ '12px', // Smaller border radius
4904
+ border: '1px solid rgba(59, 130, 246, 0.3)',
4905
+ zIndex: 2,
4906
+ position:
4907
+ 'relative',
4908
+ boxShadow:
4909
+ '0 1px 2px rgba(0,0,0,0.05)',
4910
+ }}
4911
+ >
4912
+ <input
4913
+ type="checkbox"
4914
+ onChange={(e) => {
4915
+ e.stopPropagation();
4916
+ handleGroupSelectionChange(
4917
+ groupValue,
4918
+ e.target
4919
+ .checked
4920
+ );
4921
+ }}
4922
+ checked={isGroupSelected(
4923
+ groupValue
4924
+ )}
4925
+ style={{
4926
+ width: '14px', // Smaller checkbox for compact design
4927
+ height: '14px',
4928
+ cursor: 'pointer',
4929
+ accentColor:
4930
+ 'var(--primary-color, #3b82f6)',
4931
+ }}
4932
+ />
4933
+ <span
4934
+ style={{
4935
+ cursor: 'pointer',
4936
+ userSelect:
4937
+ 'none',
4938
+ color: 'var(--secondary-color, #1f2937)',
4939
+ fontWeight:
4940
+ '500',
4941
+ fontSize:
4942
+ '0.7rem', // Smaller font for compact design
4943
+ }}
4944
+ onClick={(e) => {
4945
+ e.stopPropagation();
4946
+ const checkbox =
4947
+ e.target
4948
+ .previousElementSibling;
4949
+ checkbox.checked =
4950
+ !checkbox.checked;
4951
+ handleGroupSelectionChange(
4952
+ groupValue,
4953
+ checkbox.checked
4954
+ );
4955
+ }}
4956
+ >
4957
+ Select All
4958
+ </span>
4959
+ </div>
4960
+ )}
4961
+
4962
+ {/* Background pattern */}
4963
+ <div
4964
+ style={{
4965
+ position: 'absolute',
4966
+ top: 0,
4967
+ left: 0,
4968
+ right: 0,
4969
+ bottom: 0,
4970
+ background:
4971
+ 'repeating-linear-gradient(45deg, transparent, transparent 2px, rgba(59, 130, 246, 0.03) 2px, rgba(59, 130, 246, 0.03) 4px)',
4972
+ zIndex: 1,
4973
+ pointerEvents: 'none',
4974
+ }}
4975
+ />
4976
+ </div>
4977
+ );
4978
+ },
4979
+ }
4188
4980
  : {})}
4189
4981
  filterValue={filterValue}
4190
4982
  limit={limit}
@@ -4229,6 +5021,11 @@ const DataGrid = forwardRef(
4229
5021
  borderRadius: 'var(--br, 4px)',
4230
5022
  overflow: 'hidden',
4231
5023
  }}
5024
+ className={
5025
+ ajaxSetting?.groupBy
5026
+ ? 'InovuaReactDataGrid--grouped datagrid-with-grouping datagrid-fixed-group-headers'
5027
+ : 'datagrid-without-grouping'
5028
+ }
4232
5029
  checkboxColumnProps={{
4233
5030
  width: 60,
4234
5031
  minWidth: 60,
@@ -4237,7 +5034,88 @@ const DataGrid = forwardRef(
4237
5034
  sortable: false,
4238
5035
  headerAlign: 'center',
4239
5036
  textAlign: 'center',
5037
+ renderHeader: () => {
5038
+ // Custom header checkbox that works with grouped data
5039
+ const allRowsSelected =
5040
+ dataRef.current &&
5041
+ dataRef.current.length > 0 &&
5042
+ dataRef.current.every(
5043
+ (row) =>
5044
+ row &&
5045
+ row.id !== undefined &&
5046
+ row.id !== null &&
5047
+ selected[row.id]
5048
+ );
5049
+ const someRowsSelected =
5050
+ dataRef.current &&
5051
+ dataRef.current.some(
5052
+ (row) =>
5053
+ row &&
5054
+ row.id !== undefined &&
5055
+ row.id !== null &&
5056
+ selected[row.id]
5057
+ );
5058
+
5059
+ return (
5060
+ <input
5061
+ type="checkbox"
5062
+ checked={allRowsSelected}
5063
+ ref={(checkbox) => {
5064
+ if (checkbox) {
5065
+ checkbox.indeterminate =
5066
+ someRowsSelected &&
5067
+ !allRowsSelected;
5068
+ }
5069
+ }}
5070
+ onChange={(e) => {
5071
+ if (e.target.checked) {
5072
+ // Select all rows
5073
+ const s = {};
5074
+ if (
5075
+ dataRef.current &&
5076
+ Array.isArray(
5077
+ dataRef.current
5078
+ )
5079
+ ) {
5080
+ dataRef.current.forEach(
5081
+ (obj) => {
5082
+ if (
5083
+ obj &&
5084
+ obj.id !==
5085
+ undefined &&
5086
+ obj.id !==
5087
+ null
5088
+ ) {
5089
+ s[obj.id] =
5090
+ obj;
5091
+ }
5092
+ }
5093
+ );
5094
+ }
5095
+ setSelected(s);
5096
+ if (setRowsSelected) {
5097
+ setRowsSelected(s);
5098
+ }
5099
+ } else {
5100
+ // Deselect all rows
5101
+ setSelected({});
5102
+ if (setRowsSelected) {
5103
+ setRowsSelected({});
5104
+ }
5105
+ }
5106
+ }}
5107
+ style={{
5108
+ width: '20px',
5109
+ height: '20px',
5110
+ cursor: 'pointer',
5111
+ accentColor:
5112
+ 'var(--primary-color, #3b82f6)',
5113
+ }}
5114
+ />
5115
+ );
5116
+ },
4240
5117
  }}
5118
+ checkboxOnlyRowSelect={true}
4241
5119
  />
4242
5120
  ) : null}
4243
5121
  </div>
@@ -833,3 +833,53 @@
833
833
  }
834
834
  }
835
835
  }
836
+
837
+ /* Group header styles */
838
+ .groupHeader {
839
+ display: flex;
840
+ justify-content: space-between;
841
+ align-items: center;
842
+ padding: 8px 12px;
843
+ background-color: #f8fafc;
844
+ border-bottom: 1px solid #e2e8f0;
845
+ font-size: 0.9rem;
846
+ width: 100%;
847
+ }
848
+
849
+ .groupTitle {
850
+ display: flex;
851
+ align-items: center;
852
+ gap: 8px;
853
+ flex: 1;
854
+
855
+ strong {
856
+ color: var(--secondary-color, #1f2937);
857
+ font-weight: 600;
858
+ }
859
+ }
860
+
861
+ .groupCount {
862
+ color: var(--paragraph-color, #6b7280);
863
+ font-size: 0.85rem;
864
+ font-weight: normal;
865
+ }
866
+
867
+ .groupCheckbox {
868
+ display: flex;
869
+ align-items: center;
870
+ gap: 6px;
871
+ font-size: 0.85rem;
872
+ color: var(--secondary-color, #1f2937);
873
+
874
+ input[type='checkbox'] {
875
+ width: 16px;
876
+ height: 16px;
877
+ margin: 0;
878
+ cursor: pointer;
879
+ }
880
+
881
+ span {
882
+ cursor: pointer;
883
+ user-select: none;
884
+ }
885
+ }
@@ -206,3 +206,227 @@
206
206
  margin: -18px !important;
207
207
  }
208
208
  }
209
+
210
+ /* Group expand/collapse icon fixes - Enhanced for better visibility */
211
+ .InovuaReactDataGrid__group-expand-icon,
212
+ .InovuaReactDataGrid__group-collapse-icon {
213
+ display: inline-flex !important;
214
+ align-items: center !important;
215
+ justify-content: center !important;
216
+ width: 24px !important;
217
+ height: 24px !important;
218
+ margin-right: 8px !important;
219
+ cursor: pointer !important;
220
+ border-radius: 4px !important;
221
+ background-color: var(--primary-color, #3b82f6) !important;
222
+ transition: all 0.2s ease !important;
223
+ border: 1px solid rgba(255, 255, 255, 0.2) !important;
224
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important;
225
+ }
226
+
227
+ .InovuaReactDataGrid__group-expand-icon:hover,
228
+ .InovuaReactDataGrid__group-collapse-icon:hover {
229
+ background-color: var(--secondary-color, #2563eb) !important;
230
+ transform: scale(1.05) !important;
231
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important;
232
+ }
233
+
234
+ .InovuaReactDataGrid__group-expand-icon svg,
235
+ .InovuaReactDataGrid__group-collapse-icon svg {
236
+ width: 14px !important;
237
+ height: 14px !important;
238
+ fill: white !important;
239
+ color: white !important;
240
+ display: block !important;
241
+ opacity: 1 !important;
242
+ stroke: white !important;
243
+ stroke-width: 2 !important;
244
+ }
245
+
246
+ /* Enhanced group row styling */
247
+ .InovuaReactDataGrid__group-row {
248
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
249
+ border-bottom: 2px solid #e2e8f0 !important;
250
+ border-left: 4px solid var(--primary-color, #3b82f6) !important;
251
+ position: relative !important;
252
+ transition: all 0.2s ease !important;
253
+ }
254
+
255
+ .InovuaReactDataGrid__group-row:hover {
256
+ background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%) !important;
257
+ border-left-color: var(--secondary-color, #2563eb) !important;
258
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
259
+ }
260
+
261
+ /* Enhanced group cell styling */
262
+ .InovuaReactDataGrid__group-cell {
263
+ padding: 12px 16px !important;
264
+ font-weight: 700 !important;
265
+ color: var(--secondary-color, #1f2937) !important;
266
+ font-size: 0.95rem !important;
267
+ letter-spacing: 0.025em !important;
268
+ text-transform: uppercase !important;
269
+ position: relative !important;
270
+ }
271
+
272
+ /* Add a subtle pattern to group cells */
273
+ .InovuaReactDataGrid__group-cell::before {
274
+ content: '' !important;
275
+ position: absolute !important;
276
+ top: 0 !important;
277
+ left: 0 !important;
278
+ right: 0 !important;
279
+ bottom: 0 !important;
280
+ background: repeating-linear-gradient(
281
+ 45deg,
282
+ transparent,
283
+ transparent 2px,
284
+ rgba(59, 130, 246, 0.05) 2px,
285
+ rgba(59, 130, 246, 0.05) 4px
286
+ ) !important;
287
+ pointer-events: none !important;
288
+ }
289
+
290
+ /* Ensure group expand/collapse icons are visible in all themes */
291
+ .InovuaReactDataGrid__group-expand-icon .InovuaReactDataGrid__icon,
292
+ .InovuaReactDataGrid__group-collapse-icon .InovuaReactDataGrid__icon {
293
+ opacity: 1 !important;
294
+ visibility: visible !important;
295
+ display: block !important;
296
+ color: white !important;
297
+ fill: white !important;
298
+ }
299
+
300
+ /* Alternative selectors for different library versions */
301
+ .InovuaReactDataGrid .InovuaReactDataGrid__group-expand-icon,
302
+ .InovuaReactDataGrid .InovuaReactDataGrid__group-collapse-icon {
303
+ opacity: 1 !important;
304
+ visibility: visible !important;
305
+ }
306
+
307
+ /* Fix for SVG icons that might be hidden */
308
+ .InovuaReactDataGrid__group-expand-icon > *,
309
+ .InovuaReactDataGrid__group-collapse-icon > * {
310
+ opacity: 1 !important;
311
+ visibility: visible !important;
312
+ display: block !important;
313
+ color: white !important;
314
+ fill: white !important;
315
+ }
316
+
317
+ /* Enhanced styling for the grouped column header */
318
+ .InovuaReactDataGrid__header-cell--grouped {
319
+ background: linear-gradient(
320
+ 135deg,
321
+ var(--primary-color, #3b82f6) 0%,
322
+ var(--secondary-color, #2563eb) 100%
323
+ ) !important;
324
+ color: white !important;
325
+ font-weight: 800 !important;
326
+ text-transform: uppercase !important;
327
+ letter-spacing: 0.1em !important;
328
+ border-bottom: 4px solid var(--secondary-color, #2563eb) !important;
329
+ border-top: 2px solid rgba(255, 255, 255, 0.3) !important;
330
+ position: relative !important;
331
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15),
332
+ inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
333
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
334
+ font-size: 0.9rem !important;
335
+ padding: 12px 16px !important;
336
+ }
337
+
338
+ .InovuaReactDataGrid__header-cell--grouped::before {
339
+ content: '🔗' !important;
340
+ position: absolute !important;
341
+ left: 8px !important;
342
+ top: 50% !important;
343
+ transform: translateY(-50%) !important;
344
+ font-size: 16px !important;
345
+ opacity: 0.9 !important;
346
+ filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.3)) !important;
347
+ }
348
+
349
+ .InovuaReactDataGrid__header-cell--grouped::after {
350
+ content: 'GROUPED BY' !important;
351
+ position: absolute !important;
352
+ right: 8px !important;
353
+ top: 50% !important;
354
+ transform: translateY(-50%) !important;
355
+ font-size: 10px !important;
356
+ opacity: 0.8 !important;
357
+ font-weight: 600 !important;
358
+ letter-spacing: 0.05em !important;
359
+ background: rgba(255, 255, 255, 0.2) !important;
360
+ padding: 2px 6px !important;
361
+ border-radius: 10px !important;
362
+ }
363
+
364
+ .InovuaReactDataGrid__header-cell--grouped:hover {
365
+ background: linear-gradient(
366
+ 135deg,
367
+ var(--secondary-color, #2563eb) 0%,
368
+ #1d4ed8 100%
369
+ ) !important;
370
+ transform: translateY(-1px) !important;
371
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2),
372
+ inset 0 1px 0 rgba(255, 255, 255, 0.3) !important;
373
+ transition: all 0.2s ease !important;
374
+ }
375
+
376
+ /* Group selection functionality */
377
+ .group-selection-checkbox {
378
+ position: absolute !important;
379
+ right: 12px !important;
380
+ top: 50% !important;
381
+ transform: translateY(-50%) !important;
382
+ width: 18px !important;
383
+ height: 18px !important;
384
+ cursor: pointer !important;
385
+ z-index: 10 !important;
386
+ }
387
+
388
+ /* Clean up group header rows - hide all column content */
389
+ .InovuaReactDataGrid__group-row .InovuaReactDataGrid__group-cell {
390
+ position: relative !important;
391
+ padding-right: 50px !important;
392
+ }
393
+
394
+ /* PRESERVE ORIGINAL GROUPING: Clean group rows while maintaining ReactDataGrid structure */
395
+
396
+ /* Let ReactDataGrid handle the group cell spanning, but clean up any content that shouldn't be there */
397
+ /* The group cell should naturally span across all columns in ReactDataGrid */
398
+
399
+ /* Clean approach: Only hide content within group cells that shouldn't be there */
400
+ /* This preserves ReactDataGrid's natural group row spanning behavior */
401
+
402
+ /* Hide specific unwanted elements in group rows */
403
+ .InovuaReactDataGrid__group-row select,
404
+ .InovuaReactDataGrid__group-row input:not([type='checkbox']),
405
+ .InovuaReactDataGrid__group-row
406
+ button:not(.InovuaReactDataGrid__group-expand-button),
407
+ .InovuaReactDataGrid__group-row .dropdown,
408
+ .InovuaReactDataGrid__group-row .timer-button,
409
+ .InovuaReactDataGrid__group-row .action-icon,
410
+ .InovuaReactDataGrid__group-row
411
+ .btn:not(.InovuaReactDataGrid__group-expand-button),
412
+ .InovuaReactDataGrid__group-row
413
+ .icon:not(.InovuaReactDataGrid__group-expand-icon),
414
+ .InovuaReactDataGrid__group-row .cell-content,
415
+ .InovuaReactDataGrid__group-row .cell-value,
416
+ .InovuaReactDataGrid__group-row .dropdown-value {
417
+ display: none !important;
418
+ visibility: hidden !important;
419
+ }
420
+
421
+ /* Enhance group cell styling */
422
+ .InovuaReactDataGrid__group-cell {
423
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
424
+ border-left: 4px solid var(--primary-color, #3b82f6) !important;
425
+ font-weight: 600 !important;
426
+ padding: 12px 16px !important;
427
+ }
428
+
429
+ /* Additional group row cleanup */
430
+ .InovuaReactDataGrid__group-row * {
431
+ background-image: none !important;
432
+ }