@visns-studio/visns-components 5.9.1 → 5.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.1",
89
+ "version": "5.9.3",
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,6 @@
1
1
  import '../styles/global.css';
2
2
 
3
- import React, { useState, useEffect } from 'react';
3
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
4
4
  import { useNavigate } from 'react-router-dom';
5
5
  import { motion } from 'framer-motion';
6
6
  import { toast } from 'react-toastify';
@@ -86,8 +86,15 @@ function Form({
86
86
  const [formData, setFormData] = useState({});
87
87
  const [inputClass, setInputClass] = useState({});
88
88
  const [tableData, setTableData] = useState({ columns: [], dataSource: [] });
89
+ const [tableProps, setTableProps] = useState({});
89
90
  const [uploadProgress, setUploadProgress] = useState(0);
90
91
 
92
+ // Group selection state management
93
+ const [selected, setSelected] = useState({});
94
+ const [collapsedGroups, setCollapsedGroups] = useState({});
95
+ const dataRef = useRef([]);
96
+ const groupClickTimeoutRef = useRef(null);
97
+
91
98
  const childDropdownCallback = (data) => {
92
99
  let _form = { ...formSettings };
93
100
 
@@ -539,6 +546,163 @@ function Form({
539
546
  .join(' ');
540
547
  };
541
548
 
549
+ // Group selection functions
550
+ const handleGroupSelectionChange = useCallback(
551
+ (groupValue, isChecked) => {
552
+ if (!dataRef.current || !tableProps?.groupBy) {
553
+ return;
554
+ }
555
+
556
+ const groupField = tableProps.groupBy[0];
557
+ const groupRows = dataRef.current.filter(
558
+ (row) => row[groupField] === groupValue
559
+ );
560
+
561
+ // Clean the selected state - remove any invalid entries
562
+ const cleanSelected = {};
563
+ Object.keys(selected).forEach((key) => {
564
+ const item = selected[key];
565
+ // Only keep items that have valid IDs and are not group objects
566
+ if (
567
+ key !== 'undefined' &&
568
+ key !== 'null' &&
569
+ item &&
570
+ !item.__group &&
571
+ item.id !== undefined
572
+ ) {
573
+ cleanSelected[key] = item;
574
+ }
575
+ });
576
+ const newSelected = { ...cleanSelected };
577
+
578
+ if (isChecked) {
579
+ // Select all rows in the group
580
+ groupRows.forEach((row) => {
581
+ if (row && row.id !== undefined && row.id !== null) {
582
+ newSelected[row.id] = row;
583
+ }
584
+ });
585
+ } else {
586
+ // Deselect all rows in the group
587
+ groupRows.forEach((row) => {
588
+ if (row && row.id !== undefined && row.id !== null) {
589
+ delete newSelected[row.id];
590
+ }
591
+ });
592
+ }
593
+
594
+ // Update the selection state
595
+ setSelected(newSelected);
596
+ },
597
+ [selected, tableProps?.groupBy]
598
+ );
599
+
600
+ const isGroupSelected = useCallback(
601
+ (groupValue) => {
602
+ if (!dataRef.current || !tableProps?.groupBy) {
603
+ return false;
604
+ }
605
+
606
+ // Clean the selected state first
607
+ const cleanSelected = {};
608
+ Object.keys(selected).forEach((key) => {
609
+ const item = selected[key];
610
+ if (
611
+ key !== 'undefined' &&
612
+ key !== 'null' &&
613
+ item &&
614
+ !item.__group &&
615
+ item.id !== undefined
616
+ ) {
617
+ cleanSelected[key] = item;
618
+ }
619
+ });
620
+
621
+ const groupField = tableProps.groupBy[0];
622
+ const groupRows = dataRef.current.filter(
623
+ (row) => row[groupField] === groupValue
624
+ );
625
+
626
+ if (groupRows.length === 0) {
627
+ return false;
628
+ }
629
+
630
+ // Filter out rows with invalid IDs and check if all valid rows are selected
631
+ const validRows = groupRows.filter(
632
+ (row) => row && row.id !== undefined && row.id !== null
633
+ );
634
+
635
+ if (validRows.length === 0) {
636
+ return false;
637
+ }
638
+
639
+ return validRows.every((row) => cleanSelected[row.id]);
640
+ },
641
+ [selected, tableProps?.groupBy]
642
+ );
643
+
644
+ // Handle selection changes from ReactDataGrid
645
+ const handleSelectionChange = useCallback(
646
+ (param) => {
647
+ // Handle the selection change based on the parameter type
648
+ if (param.selected === true) {
649
+ // Select all rows - this happens when header checkbox is clicked to select all
650
+ const s = {};
651
+
652
+ // Always prefer dataRef.current as it contains the most reliable data
653
+ // especially when grouping is active
654
+ if (dataRef.current && Array.isArray(dataRef.current)) {
655
+ dataRef.current.forEach((obj) => {
656
+ if (
657
+ obj &&
658
+ obj.id !== undefined &&
659
+ obj.id !== null
660
+ ) {
661
+ s[obj.id] = obj;
662
+ }
663
+ });
664
+ } else if (param.data && Array.isArray(param.data)) {
665
+ // Fallback to param.data if dataRef.current is not available
666
+ param.data.forEach((obj) => {
667
+ if (
668
+ obj &&
669
+ obj.id !== undefined &&
670
+ obj.id !== null
671
+ ) {
672
+ s[obj.id] = obj;
673
+ }
674
+ });
675
+ }
676
+
677
+ setSelected(s);
678
+ } else if (param.selected === false) {
679
+ // Deselect all rows
680
+ setSelected({});
681
+ } else if (typeof param.selected === 'object') {
682
+ // Individual row selection/deselection - ReactDataGrid sends the complete new selection state
683
+ // ReactDataGrid sends the complete new selection state, not just the changed row
684
+ // Clean the selection to remove any group objects or invalid entries
685
+ const cleanSelection = {};
686
+ Object.keys(param.selected).forEach((key) => {
687
+ const item = param.selected[key];
688
+ // Only keep items that have valid IDs and are not group objects
689
+ if (
690
+ key !== 'undefined' &&
691
+ key !== 'null' &&
692
+ item &&
693
+ !item.__group &&
694
+ item.id !== undefined
695
+ ) {
696
+ cleanSelection[key] = item;
697
+ }
698
+ });
699
+
700
+ setSelected(cleanSelection);
701
+ }
702
+ },
703
+ [selected]
704
+ );
705
+
542
706
  const handleDownload = async (e) => {
543
707
  try {
544
708
  if (e) {
@@ -987,6 +1151,10 @@ function Form({
987
1151
  formSettings?.save?.return?.type === 'table' &&
988
1152
  formSettings?.save?.return?.param
989
1153
  ) {
1154
+ if ( formSettings.save.return.props ) {
1155
+ setTableProps(formSettings.save.return.props);
1156
+ }
1157
+
990
1158
  if (
991
1159
  res.data[formSettings.save.return.param]
992
1160
  ?.length > 0
@@ -1366,6 +1534,13 @@ function Form({
1366
1534
  fetchData();
1367
1535
  }, []);
1368
1536
 
1537
+ // Update dataRef when tableData changes
1538
+ useEffect(() => {
1539
+ if (tableData.dataSource && Array.isArray(tableData.dataSource)) {
1540
+ dataRef.current = tableData.dataSource;
1541
+ }
1542
+ }, [tableData.dataSource]);
1543
+
1369
1544
  useEffect(() => {
1370
1545
  formSettings.fields.forEach((field) => {
1371
1546
  const showConditions = field.show || [];
@@ -1541,6 +1716,395 @@ function Form({
1541
1716
  dataSource={tableData.dataSource}
1542
1717
  style={{ minHeight: 550 }}
1543
1718
  showActiveRowIndicator={false}
1719
+ selected={selected}
1720
+ onSelectionChange={handleSelectionChange}
1721
+ {...(tableProps && tableProps.groupBy
1722
+ ? {
1723
+ defaultGroupBy: tableProps.groupBy,
1724
+ defaultCollapsed: true,
1725
+ collapsedGroups: collapsedGroups,
1726
+ onGroupToggle: (groupData) => {
1727
+ // Handle group expand/collapse events
1728
+ // Update collapsed groups state
1729
+ const groupValue =
1730
+ typeof groupData === 'string'
1731
+ ? groupData
1732
+ : groupData?.value ||
1733
+ groupData?.name ||
1734
+ '';
1735
+
1736
+ setCollapsedGroups((prev) => {
1737
+ const newState = { ...prev };
1738
+ if (newState[groupValue]) {
1739
+ delete newState[groupValue];
1740
+ } else {
1741
+ newState[groupValue] = true;
1742
+ }
1743
+ console.log(
1744
+ 'Updated collapsed groups:',
1745
+ newState
1746
+ );
1747
+ return newState;
1748
+ });
1749
+ },
1750
+ renderGroupTitle: (groupData) => {
1751
+ const groupValue =
1752
+ typeof groupData === 'string'
1753
+ ? groupData
1754
+ : groupData?.value ||
1755
+ groupData?.name ||
1756
+ '';
1757
+
1758
+ // Use ReactDataGrid's built-in toggle functionality
1759
+ const collapsed =
1760
+ collapsedGroups[groupValue] ||
1761
+ false;
1762
+ const toggleGroup = () => {
1763
+ // Update the collapsed state
1764
+ setCollapsedGroups((prev) => {
1765
+ const newState = { ...prev };
1766
+ if (newState[groupValue]) {
1767
+ delete newState[
1768
+ groupValue
1769
+ ];
1770
+ } else {
1771
+ newState[
1772
+ groupValue
1773
+ ] = true;
1774
+ }
1775
+ return newState;
1776
+ });
1777
+ };
1778
+ const groupCount =
1779
+ typeof groupData === 'object'
1780
+ ? groupData?.count || 0
1781
+ : 0;
1782
+ const hasCheckboxColumn =
1783
+ tableData.columns.some(
1784
+ (col) =>
1785
+ col.id ===
1786
+ '__checkbox-column'
1787
+ );
1788
+
1789
+ // Handle group header click for selection
1790
+ const handleGroupHeaderClick = (
1791
+ e
1792
+ ) => {
1793
+ // Prevent rapid clicks
1794
+ if (
1795
+ groupClickTimeoutRef.current
1796
+ ) {
1797
+ console.log(
1798
+ '🖱️ Ignoring rapid click'
1799
+ );
1800
+ return;
1801
+ }
1802
+
1803
+ // Don't trigger if clicking on the expand button or checkbox
1804
+ if (
1805
+ e.target.closest(
1806
+ '.group-expand-btn'
1807
+ ) ||
1808
+ e.target.closest(
1809
+ 'input[type="checkbox"]'
1810
+ )
1811
+ ) {
1812
+ console.log(
1813
+ '🖱️ Ignoring click on expand button or checkbox'
1814
+ );
1815
+ return;
1816
+ }
1817
+
1818
+ // Set timeout to prevent rapid clicks
1819
+ groupClickTimeoutRef.current =
1820
+ setTimeout(() => {
1821
+ groupClickTimeoutRef.current =
1822
+ null;
1823
+ }, 300);
1824
+
1825
+ // Toggle selection of all items in this group
1826
+ const isCurrentlySelected =
1827
+ isGroupSelected(groupValue);
1828
+
1829
+ handleGroupSelectionChange(
1830
+ groupValue,
1831
+ !isCurrentlySelected
1832
+ );
1833
+ };
1834
+
1835
+ return (
1836
+ <div
1837
+ className="group-header-clean group-header-fixed-height"
1838
+ onClick={
1839
+ handleGroupHeaderClick
1840
+ }
1841
+ style={{
1842
+ display: 'flex',
1843
+ alignItems: 'center',
1844
+ justifyContent:
1845
+ 'space-between',
1846
+ width: '100%',
1847
+ height: '34px', // Compact height
1848
+ padding: '6px 12px', // Reduced padding for compact design
1849
+ background:
1850
+ 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
1851
+ borderLeft:
1852
+ '4px solid var(--primary-color, #3b82f6)',
1853
+ position: 'relative',
1854
+ overflow: 'hidden',
1855
+ boxShadow:
1856
+ '0 1px 3px rgba(0,0,0,0.1)',
1857
+ borderRadius:
1858
+ '0 4px 4px 0',
1859
+ cursor: 'pointer',
1860
+ boxSizing: 'border-box',
1861
+ lineHeight: '1.1', // Compact line height
1862
+ flexShrink: 0,
1863
+ transition:
1864
+ 'background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease',
1865
+ }}
1866
+ onMouseEnter={(e) => {
1867
+ e.currentTarget.style.background =
1868
+ 'linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%)';
1869
+ e.currentTarget.style.boxShadow =
1870
+ '0 2px 8px rgba(0,0,0,0.15)';
1871
+ e.currentTarget.style.transform =
1872
+ 'translateY(-1px)';
1873
+ }}
1874
+ onMouseLeave={(e) => {
1875
+ e.currentTarget.style.background =
1876
+ 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)';
1877
+ e.currentTarget.style.boxShadow =
1878
+ '0 1px 3px rgba(0,0,0,0.1)';
1879
+ e.currentTarget.style.transform =
1880
+ 'translateY(0)';
1881
+ }}
1882
+ >
1883
+ {/* Left side - Group info and expand/collapse */}
1884
+ <div
1885
+ style={{
1886
+ display: 'flex',
1887
+ alignItems: 'center',
1888
+ flex: 1,
1889
+ zIndex: 2,
1890
+ position: 'relative',
1891
+ }}
1892
+ >
1893
+ <button
1894
+ className="group-expand-btn"
1895
+ onClick={(e) => {
1896
+ e.preventDefault();
1897
+ e.stopPropagation();
1898
+ if (
1899
+ typeof toggleGroup ===
1900
+ 'function'
1901
+ ) {
1902
+ toggleGroup();
1903
+ } else {
1904
+ console.warn(
1905
+ 'toggleGroup is not a function:',
1906
+ toggleGroup
1907
+ );
1908
+ }
1909
+ }}
1910
+ style={{
1911
+ background:
1912
+ 'var(--primary-color, #3b82f6)',
1913
+ border: 'none',
1914
+ borderRadius:
1915
+ '3px',
1916
+ color: 'white',
1917
+ width: '20px', // More compact
1918
+ height: '20px', // More compact
1919
+ display: 'flex',
1920
+ alignItems:
1921
+ 'center',
1922
+ justifyContent:
1923
+ 'center',
1924
+ marginRight:
1925
+ '8px', // Reduced margin
1926
+ cursor: 'pointer',
1927
+ fontSize: '11px', // Smaller font
1928
+ fontWeight:
1929
+ 'bold',
1930
+ boxShadow:
1931
+ '0 1px 2px rgba(0,0,0,0.1)',
1932
+ transition:
1933
+ 'all 0.15s ease',
1934
+ }}
1935
+ onMouseEnter={(e) => {
1936
+ e.target.style.transform =
1937
+ 'scale(1.05)';
1938
+ e.target.style.background =
1939
+ 'var(--secondary-color, #2563eb)';
1940
+ }}
1941
+ onMouseLeave={(e) => {
1942
+ e.target.style.transform =
1943
+ 'scale(1)';
1944
+ e.target.style.background =
1945
+ 'var(--primary-color, #3b82f6)';
1946
+ }}
1947
+ >
1948
+ {collapsed
1949
+ ? '+'
1950
+ : '−'}
1951
+ </button>
1952
+ <div
1953
+ style={{
1954
+ display: 'flex',
1955
+ alignItems:
1956
+ 'center', // Changed to center for compact layout
1957
+ gap: '8px', // Add gap between elements
1958
+ }}
1959
+ >
1960
+ <strong
1961
+ style={{
1962
+ fontSize:
1963
+ '0.85rem', // Slightly smaller for compact design
1964
+ color: 'var(--secondary-color, #1f2937)',
1965
+ fontWeight:
1966
+ '600',
1967
+ textTransform:
1968
+ 'uppercase',
1969
+ letterSpacing:
1970
+ '0.025em',
1971
+ }}
1972
+ >
1973
+ {groupValue}
1974
+ </strong>
1975
+ {groupCount > 0 && (
1976
+ <span
1977
+ style={{
1978
+ color: '#6b7280',
1979
+ fontSize:
1980
+ '0.75rem', // Smaller for compact design
1981
+ fontWeight:
1982
+ '500',
1983
+ }}
1984
+ >
1985
+ ({groupCount}{' '}
1986
+ {groupCount ===
1987
+ 1
1988
+ ? 'item'
1989
+ : 'items'}
1990
+ )
1991
+ </span>
1992
+ )}
1993
+ {/* Click indicator */}
1994
+ <span
1995
+ style={{
1996
+ color: '#9ca3af',
1997
+ fontSize:
1998
+ '0.7rem',
1999
+ fontStyle:
2000
+ 'italic',
2001
+ opacity: 0.8,
2002
+ marginLeft:
2003
+ 'auto',
2004
+ userSelect:
2005
+ 'none',
2006
+ }}
2007
+ >
2008
+ Click to
2009
+ select/unselect
2010
+ all
2011
+ </span>
2012
+ </div>
2013
+ </div>
2014
+
2015
+ {/* Right side - Group selection */}
2016
+ {hasCheckboxColumn && (
2017
+ <div
2018
+ style={{
2019
+ display: 'flex',
2020
+ alignItems:
2021
+ 'center',
2022
+ gap: '4px', // Reduced gap for compact design
2023
+ fontSize:
2024
+ '0.75rem', // Smaller font for compact design
2025
+ background:
2026
+ 'rgba(255, 255, 255, 0.9)',
2027
+ padding:
2028
+ '3px 8px', // Reduced padding for compact design
2029
+ borderRadius:
2030
+ '12px', // Smaller border radius
2031
+ border: '1px solid rgba(59, 130, 246, 0.3)',
2032
+ zIndex: 2,
2033
+ position:
2034
+ 'relative',
2035
+ boxShadow:
2036
+ '0 1px 2px rgba(0,0,0,0.05)',
2037
+ }}
2038
+ >
2039
+ <input
2040
+ type="checkbox"
2041
+ onChange={(e) => {
2042
+ e.stopPropagation();
2043
+ handleGroupSelectionChange(
2044
+ groupValue,
2045
+ e.target
2046
+ .checked
2047
+ );
2048
+ }}
2049
+ checked={isGroupSelected(
2050
+ groupValue
2051
+ )}
2052
+ style={{
2053
+ width: '14px', // Smaller checkbox for compact design
2054
+ height: '14px',
2055
+ cursor: 'pointer',
2056
+ accentColor:
2057
+ 'var(--primary-color, #3b82f6)',
2058
+ }}
2059
+ />
2060
+ <span
2061
+ style={{
2062
+ cursor: 'pointer',
2063
+ userSelect:
2064
+ 'none',
2065
+ color: 'var(--secondary-color, #1f2937)',
2066
+ fontWeight:
2067
+ '500',
2068
+ fontSize:
2069
+ '0.7rem', // Smaller font for compact design
2070
+ }}
2071
+ onClick={(e) => {
2072
+ e.stopPropagation();
2073
+ const checkbox =
2074
+ e.target
2075
+ .previousElementSibling;
2076
+ checkbox.checked =
2077
+ !checkbox.checked;
2078
+ handleGroupSelectionChange(
2079
+ groupValue,
2080
+ checkbox.checked
2081
+ );
2082
+ }}
2083
+ >
2084
+ Select All
2085
+ </span>
2086
+ </div>
2087
+ )}
2088
+
2089
+ {/* Background pattern */}
2090
+ <div
2091
+ style={{
2092
+ position: 'absolute',
2093
+ top: 0,
2094
+ left: 0,
2095
+ right: 0,
2096
+ bottom: 0,
2097
+ background:
2098
+ 'repeating-linear-gradient(45deg, transparent, transparent 2px, rgba(59, 130, 246, 0.03) 2px, rgba(59, 130, 246, 0.03) 4px)',
2099
+ zIndex: 1,
2100
+ pointerEvents: 'none',
2101
+ }}
2102
+ />
2103
+ </div>
2104
+ );
2105
+ },
2106
+ }
2107
+ : {})}
1544
2108
  />
1545
2109
  </div>
1546
2110
  )}
@@ -430,3 +430,7 @@
430
430
  .InovuaReactDataGrid__group-row * {
431
431
  background-image: none !important;
432
432
  }
433
+
434
+ .InovuaReactDataGrid__group-toolbar {
435
+ display: none !important;
436
+ }