fix(ui): fix sync folder selection jump issue; add auto-select children/parents and expand/collapse all folders button #21

This commit is contained in:
rustmailer
2025-11-30 05:51:17 +08:00
parent 1cfc12324f
commit 82397ab0cd
26 changed files with 1237 additions and 781 deletions
+5
View File
@@ -13,7 +13,11 @@
"knip": "knip"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@hookform/resolvers": "^3.9.1",
"@mui/material": "^7.3.5",
"@mui/x-tree-view": "^8.19.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
@@ -37,6 +41,7 @@
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.4",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@react-spring/web": "^10.0.3",
"@tabler/icons-react": "^3.24.0",
"@tanstack/react-query": "^5.62.3",
"@tanstack/react-router": "^1.86.1",
+564 -17
View File
@@ -8,9 +8,21 @@ importers:
.:
dependencies:
'@emotion/react':
specifier: ^11.14.0
version: 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/styled':
specifier: ^11.14.1
version: 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@hookform/resolvers':
specifier: ^3.9.1
version: 3.9.1(react-hook-form@7.54.0(react@18.3.1))
'@mui/material':
specifier: ^7.3.5
version: 7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@mui/x-tree-view':
specifier: ^8.19.0
version: 8.19.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-accordion':
specifier: ^1.2.2
version: 1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -80,6 +92,9 @@ importers:
'@radix-ui/react-visually-hidden':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-spring/web':
specifier: ^10.0.3
version: 10.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tabler/icons-react':
specifier: ^3.24.0
version: 3.24.0(react@18.3.1)
@@ -350,10 +365,6 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.26.0':
resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
@@ -378,6 +389,70 @@ packages:
resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==}
engines: {node: '>=6.9.0'}
'@base-ui-components/utils@0.1.2':
resolution: {integrity: sha512-aEitDGpMsYO2qnSpYOwZNykn9Rzn2ioyEVk2fyDRH7t+TIHVKpp9CeV7SPTq43M9mMSDxQ+7UeZJVkrj2dCVIQ==}
peerDependencies:
'@types/react': ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@types/react':
optional: true
'@emotion/babel-plugin@11.13.5':
resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
'@emotion/cache@11.14.0':
resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==}
'@emotion/hash@0.9.2':
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
'@emotion/is-prop-valid@1.4.0':
resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==}
'@emotion/memoize@0.9.0':
resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==}
'@emotion/react@11.14.0':
resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==}
peerDependencies:
'@types/react': '*'
react: '>=16.8.0'
peerDependenciesMeta:
'@types/react':
optional: true
'@emotion/serialize@1.3.3':
resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==}
'@emotion/sheet@1.4.0':
resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==}
'@emotion/styled@11.14.1':
resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==}
peerDependencies:
'@emotion/react': ^11.0.0-rc.0
'@types/react': '*'
react: '>=16.8.0'
peerDependenciesMeta:
'@types/react':
optional: true
'@emotion/unitless@0.10.0':
resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==}
'@emotion/use-insertion-effect-with-fallbacks@1.2.0':
resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==}
peerDependencies:
react: '>=16.8.0'
'@emotion/utils@1.4.2':
resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==}
'@emotion/weak-memoize@0.4.0':
resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==}
'@esbuild/aix-ppc64@0.23.1':
resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
engines: {node: '>=18'}
@@ -722,6 +797,9 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@floating-ui/utils@0.2.8':
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
@@ -775,6 +853,108 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@mui/core-downloads-tracker@7.3.5':
resolution: {integrity: sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==}
'@mui/material@7.3.5':
resolution: {integrity: sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@mui/material-pigment-css': ^7.3.5
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/material-pigment-css':
optional: true
'@types/react':
optional: true
'@mui/private-theming@7.3.5':
resolution: {integrity: sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/styled-engine@7.3.5':
resolution: {integrity: sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
'@emotion/styled': ^11.3.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/system@7.3.5':
resolution: {integrity: sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
'@mui/types@7.4.8':
resolution: {integrity: sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/utils@7.3.5':
resolution: {integrity: sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/x-internals@8.19.0':
resolution: {integrity: sha512-mMmiyJAN5fW27srXJjhXhXJa+w2xGO45rwcjws6OQc9rdXGdJqRXhBwJd+OT7J1xwSdFIIUhjZRTz1KAfCSGBg==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
'@mui/x-tree-view@8.19.0':
resolution: {integrity: sha512-1JPIczqK5qdDmo4p8KEpR4XZKxDHkw5dBoY1o+FNUdKmOipklQvg0+XL5wY75pjwzADngZR/6RYW08bhkt+Z3Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.9.0
'@emotion/styled': ^11.8.1
'@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0
'@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -879,6 +1059,9 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
'@radix-ui/number@1.1.0':
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
@@ -1648,6 +1831,33 @@ packages:
'@radix-ui/rect@1.1.0':
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
'@react-spring/animated@10.0.3':
resolution: {integrity: sha512-7MrxADV3vaUADn2V9iYhaIL6iOWRx9nCJjYrsk2AHD2kwPr6fg7Pt0v+deX5RnCDmCKNnD6W5fasiyM8D+wzJQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-spring/core@10.0.3':
resolution: {integrity: sha512-D4DwNO68oohDf/0HG2G0Uragzb9IA1oXblxrd6MZAcBcUQG2EHUWXewjdECMPLNmQvlYVyyBRH6gPxXM5DX7DQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-spring/rafz@10.0.3':
resolution: {integrity: sha512-Ri2/xqt8OnQ2iFKkxKMSF4Nqv0LSWnxXT4jXFzBDsHgeeH/cHxTLupAWUwmV9hAGgmEhBmh5aONtj3J6R/18wg==}
'@react-spring/shared@10.0.3':
resolution: {integrity: sha512-geCal66nrkaQzUVhPkGomylo+Jpd5VPK8tPMEDevQEfNSWAQP15swHm+MCRG4wVQrQlTi9lOzKzpRoTL3CA84Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-spring/types@10.0.3':
resolution: {integrity: sha512-H5Ixkd2OuSIgHtxuHLTt7aJYfhMXKXT/rK32HPD/kSrOB6q6ooeiWAXkBy7L8F3ZxdkBb9ini9zP9UwnEFzWgQ==}
'@react-spring/web@10.0.3':
resolution: {integrity: sha512-ndU+kWY81rHsT7gTFtCJ6mrVhaJ6grFmgTnENipzmKqot4HGf5smPNK+cZZJqoGeDsj9ZsiWPW4geT/NyD484A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@rollup/rollup-android-arm-eabi@4.26.0':
resolution: {integrity: sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==}
cpu: [arm]
@@ -2020,14 +2230,25 @@ packages:
'@types/node@22.10.1':
resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==}
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
'@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
'@types/react-dom@18.3.5':
resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==}
peerDependencies:
'@types/react': ^18.0.0
'@types/react-transition-group@4.4.12':
resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==}
peerDependencies:
'@types/react': '*'
'@types/react@18.3.18':
resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==}
@@ -2243,6 +2464,10 @@ packages:
babel-dead-code-elimination@1.0.6:
resolution: {integrity: sha512-JxFi9qyRJpN0LjEbbjbN8g0ux71Qppn9R8Qe3k6QzHg2CaKsbUQtbn307LQGiDLGjV6JCtEFqfxzVig9MyDCHQ==}
babel-plugin-macros@3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'}
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -2358,9 +2583,16 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cosmiconfig@7.1.0:
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
engines: {node: '>=10'}
cross-spawn@7.0.5:
resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==}
engines: {node: '>= 8'}
@@ -2491,6 +2723,9 @@ packages:
resolution: {integrity: sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==}
engines: {node: '>=10.13.0'}
error-ex@1.3.4:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
@@ -2624,6 +2859,9 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
find-root@1.1.0:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -2731,6 +2969,9 @@ packages:
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
@@ -2780,6 +3021,9 @@ packages:
is-alphanumerical@2.0.1:
resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
@@ -3166,6 +3410,10 @@ packages:
parse-entities@4.0.2:
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
parse-ms@4.0.0:
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
engines: {node: '>=18'}
@@ -3185,6 +3433,10 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -3377,6 +3629,9 @@ packages:
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-is@19.2.0:
resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==}
react-markdown@10.1.0:
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
peerDependencies:
@@ -3456,9 +3711,6 @@ packages:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
@@ -3475,6 +3727,9 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -3592,6 +3847,9 @@ packages:
style-to-object@1.0.8:
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
stylis@4.2.0:
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -3891,6 +4149,10 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
yaml@2.6.0:
resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==}
engines: {node: '>= 14'}
@@ -4049,10 +4311,6 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9
'@babel/runtime@7.26.0':
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.28.4': {}
'@babel/template@7.25.9':
@@ -4098,6 +4356,100 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@base-ui-components/utils@0.1.2(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@floating-ui/utils': 0.2.10
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
reselect: 5.1.1
use-sync-external-store: 1.6.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.18
'@emotion/babel-plugin@11.13.5':
dependencies:
'@babel/helper-module-imports': 7.25.9
'@babel/runtime': 7.28.4
'@emotion/hash': 0.9.2
'@emotion/memoize': 0.9.0
'@emotion/serialize': 1.3.3
babel-plugin-macros: 3.1.0
convert-source-map: 1.9.0
escape-string-regexp: 4.0.0
find-root: 1.1.0
source-map: 0.5.7
stylis: 4.2.0
transitivePeerDependencies:
- supports-color
'@emotion/cache@11.14.0':
dependencies:
'@emotion/memoize': 0.9.0
'@emotion/sheet': 1.4.0
'@emotion/utils': 1.4.2
'@emotion/weak-memoize': 0.4.0
stylis: 4.2.0
'@emotion/hash@0.9.2': {}
'@emotion/is-prop-valid@1.4.0':
dependencies:
'@emotion/memoize': 0.9.0
'@emotion/memoize@0.9.0': {}
'@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@emotion/babel-plugin': 11.13.5
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1)
'@emotion/utils': 1.4.2
'@emotion/weak-memoize': 0.4.0
hoist-non-react-statics: 3.3.2
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
transitivePeerDependencies:
- supports-color
'@emotion/serialize@1.3.3':
dependencies:
'@emotion/hash': 0.9.2
'@emotion/memoize': 0.9.0
'@emotion/unitless': 0.10.0
'@emotion/utils': 1.4.2
csstype: 3.1.3
'@emotion/sheet@1.4.0': {}
'@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@emotion/babel-plugin': 11.13.5
'@emotion/is-prop-valid': 1.4.0
'@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/serialize': 1.3.3
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1)
'@emotion/utils': 1.4.2
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
transitivePeerDependencies:
- supports-color
'@emotion/unitless@0.10.0': {}
'@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)':
dependencies:
react: 18.3.1
'@emotion/utils@1.4.2': {}
'@emotion/weak-memoize@0.4.0': {}
'@esbuild/aix-ppc64@0.23.1':
optional: true
@@ -4303,6 +4655,8 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@floating-ui/utils@0.2.10': {}
'@floating-ui/utils@0.2.8': {}
'@hookform/resolvers@3.9.1(react-hook-form@7.54.0(react@18.3.1))':
@@ -4354,6 +4708,115 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@mui/core-downloads-tracker@7.3.5': {}
'@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/core-downloads-tracker': 7.3.5
'@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@mui/types': 7.4.8(@types/react@18.3.18)
'@mui/utils': 7.3.5(@types/react@18.3.18)(react@18.3.1)
'@popperjs/core': 2.11.8
'@types/react-transition-group': 4.4.12(@types/react@18.3.18)
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-is: 19.2.0
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
'@mui/private-theming@7.3.5(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/utils': 7.3.5(@types/react@18.3.18)(react@18.3.1)
prop-types: 15.8.1
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
'@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/sheet': 1.4.0
csstype: 3.1.3
prop-types: 15.8.1
react: 18.3.1
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/private-theming': 7.3.5(@types/react@18.3.18)(react@18.3.1)
'@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1)
'@mui/types': 7.4.8(@types/react@18.3.18)
'@mui/utils': 7.3.5(@types/react@18.3.18)(react@18.3.1)
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
react: 18.3.1
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
'@mui/types@7.4.8(@types/react@18.3.18)':
dependencies:
'@babel/runtime': 7.28.4
optionalDependencies:
'@types/react': 18.3.18
'@mui/utils@7.3.5(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/types': 7.4.8(@types/react@18.3.18)
'@types/prop-types': 15.7.15
clsx: 2.1.1
prop-types: 15.8.1
react: 18.3.1
react-is: 19.2.0
optionalDependencies:
'@types/react': 18.3.18
'@mui/x-internals@8.19.0(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/utils': 7.3.5(@types/react@18.3.18)(react@18.3.1)
react: 18.3.1
reselect: 5.1.1
use-sync-external-store: 1.6.0(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
'@mui/x-tree-view@8.19.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@base-ui-components/utils': 0.1.2(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@mui/utils': 7.3.5(@types/react@18.3.18)(react@18.3.1)
'@mui/x-internals': 8.19.0(@types/react@18.3.18)(react@18.3.1)
'@types/react-transition-group': 4.4.12(@types/react@18.3.18)
clsx: 2.1.1
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -4430,6 +4893,8 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@popperjs/core@2.11.8': {}
'@radix-ui/number@1.1.0': {}
'@radix-ui/primitive@1.1.0': {}
@@ -5201,6 +5666,38 @@ snapshots:
'@radix-ui/rect@1.1.0': {}
'@react-spring/animated@10.0.3(react@18.3.1)':
dependencies:
'@react-spring/shared': 10.0.3(react@18.3.1)
'@react-spring/types': 10.0.3
react: 18.3.1
'@react-spring/core@10.0.3(react@18.3.1)':
dependencies:
'@react-spring/animated': 10.0.3(react@18.3.1)
'@react-spring/shared': 10.0.3(react@18.3.1)
'@react-spring/types': 10.0.3
react: 18.3.1
'@react-spring/rafz@10.0.3': {}
'@react-spring/shared@10.0.3(react@18.3.1)':
dependencies:
'@react-spring/rafz': 10.0.3
'@react-spring/types': 10.0.3
react: 18.3.1
'@react-spring/types@10.0.3': {}
'@react-spring/web@10.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@react-spring/animated': 10.0.3(react@18.3.1)
'@react-spring/core': 10.0.3(react@18.3.1)
'@react-spring/shared': 10.0.3(react@18.3.1)
'@react-spring/types': 10.0.3
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@rollup/rollup-android-arm-eabi@4.26.0':
optional: true
@@ -5526,12 +6023,20 @@ snapshots:
dependencies:
undici-types: 6.20.0
'@types/parse-json@4.0.2': {}
'@types/prop-types@15.7.14': {}
'@types/prop-types@15.7.15': {}
'@types/react-dom@18.3.5(@types/react@18.3.18)':
dependencies:
'@types/react': 18.3.18
'@types/react-transition-group@4.4.12(@types/react@18.3.18)':
dependencies:
'@types/react': 18.3.18
'@types/react@18.3.18':
dependencies:
'@types/prop-types': 15.7.14
@@ -5822,6 +6327,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
babel-plugin-macros@3.1.0:
dependencies:
'@babel/runtime': 7.28.4
cosmiconfig: 7.1.0
resolve: 1.22.8
bail@2.0.2: {}
balanced-match@1.0.2: {}
@@ -5934,8 +6445,18 @@ snapshots:
concat-map@0.0.1: {}
convert-source-map@1.9.0: {}
convert-source-map@2.0.0: {}
cosmiconfig@7.1.0:
dependencies:
'@types/parse-json': 4.0.2
import-fresh: 3.3.0
parse-json: 5.2.0
path-type: 4.0.0
yaml: 1.10.2
cross-spawn@7.0.5:
dependencies:
path-key: 3.1.1
@@ -6024,7 +6545,7 @@ snapshots:
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.26.0
'@babel/runtime': 7.28.4
csstype: 3.1.3
eastasianwidth@0.2.0: {}
@@ -6046,6 +6567,10 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.2.1
error-ex@1.3.4:
dependencies:
is-arrayish: 0.2.1
es-module-lexer@1.7.0:
optional: true
@@ -6237,6 +6762,8 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
find-root@1.1.0: {}
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -6350,6 +6877,10 @@ snapshots:
dependencies:
'@types/hast': 3.0.4
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
@@ -6391,6 +6922,8 @@ snapshots:
is-alphabetical: 2.0.1
is-decimal: 2.0.1
is-arrayish@0.2.1: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
@@ -6450,8 +6983,7 @@ snapshots:
json-buffer@3.0.1: {}
json-parse-even-better-errors@2.3.1:
optional: true
json-parse-even-better-errors@2.3.1: {}
json-schema-traverse@0.4.1: {}
@@ -6965,6 +7497,13 @@ snapshots:
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
parse-json@5.2.0:
dependencies:
'@babel/code-frame': 7.26.2
error-ex: 1.3.4
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parse-ms@4.0.0: {}
path-exists@4.0.0: {}
@@ -6978,6 +7517,8 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.2
path-type@4.0.0: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -7098,6 +7639,8 @@ snapshots:
react-is@18.3.1: {}
react-is@19.2.0: {}
react-markdown@10.1.0(@types/react@18.3.18)(react@18.3.1):
dependencies:
'@types/hast': 3.0.4
@@ -7159,7 +7702,7 @@ snapshots:
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.0
'@babel/runtime': 7.28.4
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
@@ -7198,8 +7741,6 @@ snapshots:
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
regenerator-runtime@0.14.1: {}
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
@@ -7237,6 +7778,8 @@ snapshots:
require-from-string@2.0.2:
optional: true
reselect@5.1.1: {}
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -7371,6 +7914,8 @@ snapshots:
dependencies:
inline-style-parser: 0.2.4
stylis@4.2.0: {}
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.5
@@ -7701,6 +8246,8 @@ snapshots:
yallist@3.1.1: {}
yaml@1.10.2: {}
yaml@2.6.0: {}
yocto-queue@0.1.0: {}
+2 -2
View File
@@ -82,7 +82,7 @@ export function EnvelopeListPagination({
<SelectValue placeholder={pageSize} />
</SelectTrigger>
<SelectContent side='top'>
{[10, 20, 30, 40, 50].map((size) => (
{[10, 20, 30, 40, 50, 100].map((size) => (
<SelectItem key={size} value={`${size}`}>
{size}
</SelectItem>
@@ -91,7 +91,7 @@ export function EnvelopeListPagination({
</Select>
</div>
<div className='flex items-center justify-center text-sm font-medium'>
{t("table.page")} {pageIndex + 1} {t("table.of")} {pageCount}
{t("table.page")} {pageIndex + 1} {t("table.of")} {pageCount}
</div>
<div className='flex items-center space-x-2'>
<Button
-529
View File
@@ -1,529 +0,0 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from 'react'
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronRight } from 'lucide-react'
import { cva } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const treeVariants = cva(
'group hover:before:opacity-100 before:absolute before:rounded-lg before:left-0 px-2 before:w-full before:opacity-0 before:bg-accent/70 before:h-[2rem] before:-z-10'
)
const selectedTreeVariants = cva(
'before:opacity-100 before:bg-accent/70 text-accent-foreground'
)
interface TreeDataItem {
id: string
name: string
icon?: React.ComponentType<{ className?: string }>
openIcon?: React.ComponentType<{ className?: string }>
children?: TreeDataItem[]
badge?: React.ReactNode,
attributes?: React.ReactNode,
onClick?: () => void
}
type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
data: TreeDataItem[] | TreeDataItem
onSelectChange?: (item: TreeDataItem | undefined) => void
onSelectItemsChange?: (items: TreeDataItem[]) => void
expandAll?: boolean
multiple?: boolean
clickRowToSelect?: boolean
initialSelectedItemIds?: string[]
defaultNodeIcon?: React.ComponentType<{ className?: string }>
defaultLeafIcon?: React.ComponentType<{ className?: string }>
}
const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
(
{
data,
onSelectChange,
onSelectItemsChange,
expandAll,
defaultLeafIcon,
defaultNodeIcon,
clickRowToSelect = true,
className,
multiple,
initialSelectedItemIds = [],
...props
},
ref
) => {
const [selectedItemIds, setSelectedItemIds] = React.useState<Set<string>>(
new Set(initialSelectedItemIds)
);
const callbacksRef = React.useRef({
onSelectChange,
onSelectItemsChange
});
React.useEffect(() => {
callbacksRef.current = {
onSelectChange,
onSelectItemsChange
};
}, [onSelectChange, onSelectItemsChange]);
const handleSelectChange = React.useCallback(
(item: TreeDataItem | undefined) => {
if (!item) return;
setSelectedItemIds(prev => {
const newSet = new Set(prev);
if (newSet.has(item.id)) {
newSet.delete(item.id);
} else {
if (!multiple) {
newSet.clear();
}
newSet.add(item.id);
}
setTimeout(() => {
if (callbacksRef.current.onSelectChange) {
callbacksRef.current.onSelectChange(newSet.has(item.id) ? item : undefined);
}
if (callbacksRef.current.onSelectItemsChange) {
const selectedItems = Array.from(newSet)
.map(id => findItemById(data, id))
.filter(Boolean) as TreeDataItem[];
callbacksRef.current.onSelectItemsChange(selectedItems);
}
}, 0);
return newSet;
});
},
[multiple, onSelectChange, onSelectItemsChange, data]
);
const expandedItemIds = React.useMemo(() => {
if (!initialSelectedItemIds || initialSelectedItemIds.length === 0) {
return [] as string[]
}
const ids: string[] = []
function walkTreeItems(
items: TreeDataItem[] | TreeDataItem,
targetIds: string[]
) {
if (Array.isArray(items)) {
for (let i = 0; i < items.length; i++) {
ids.push(items[i]!.id)
if (walkTreeItems(items[i]!, targetIds) && !expandAll) {
return true
}
if (!expandAll) ids.pop()
}
} else if (!expandAll && targetIds.includes(items.id)) {
return true
} else if (items.children) {
return walkTreeItems(items.children, targetIds)
}
}
walkTreeItems(data, initialSelectedItemIds)
return ids
}, [data, expandAll, initialSelectedItemIds])
return (
<div className={cn('overflow-hidden relative p-2', className)}>
<TreeItem
data={data}
ref={ref}
clickRowToSelect={clickRowToSelect}
selectedItemIds={selectedItemIds}
handleSelectChange={handleSelectChange}
expandedItemIds={expandedItemIds}
defaultLeafIcon={defaultLeafIcon}
defaultNodeIcon={defaultNodeIcon}
{...props}
/>
</div>
)
}
)
TreeView.displayName = 'TreeView'
// Helper function to find item by ID in tree
function findItemById(items: TreeDataItem[] | TreeDataItem, id: string): TreeDataItem | undefined {
if (Array.isArray(items)) {
for (const item of items) {
const found = findItemById(item, id);
if (found) return found;
}
} else {
if (items.id === id) return items;
if (items.children) {
return findItemById(items.children, id);
}
}
return undefined;
}
type TreeItemProps = TreeProps & {
selectedItemIds: Set<string>
handleSelectChange: (item: TreeDataItem | undefined) => void
expandedItemIds: string[]
clickRowToSelect?: boolean
defaultNodeIcon?: React.ComponentType<{ className?: string }>
defaultLeafIcon?: React.ComponentType<{ className?: string }>
}
const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
(
{
className,
data,
selectedItemIds,
handleSelectChange,
clickRowToSelect,
expandedItemIds,
defaultNodeIcon,
defaultLeafIcon,
...props
},
ref
) => {
if (!Array.isArray(data)) {
data = [data]
}
return (
<div ref={ref} role="tree" className={className} {...props}>
<ul>
{data.map((item) => (
<li key={item.id}>
{item.children ? (
<TreeNode
item={item}
selectedItemIds={selectedItemIds}
expandedItemIds={expandedItemIds}
clickRowToSelect={clickRowToSelect}
handleSelectChange={handleSelectChange}
defaultNodeIcon={defaultNodeIcon}
defaultLeafIcon={defaultLeafIcon}
/>
) : (
<TreeLeaf
item={item}
clickRowToSelect={clickRowToSelect}
selectedItemIds={selectedItemIds}
handleSelectChange={handleSelectChange}
defaultLeafIcon={defaultLeafIcon}
/>
)}
</li>
))}
</ul>
</div>
)
}
)
TreeItem.displayName = 'TreeItem'
interface TreeNodeProps {
item: TreeDataItem
handleSelectChange: (item: TreeDataItem | undefined) => void
expandedItemIds: string[]
clickRowToSelect?: boolean
selectedItemIds: Set<string>
defaultNodeIcon?: React.ComponentType<{ className?: string }>
defaultLeafIcon?: React.ComponentType<{ className?: string }>
}
const TreeNode = ({
item,
handleSelectChange,
expandedItemIds,
clickRowToSelect,
selectedItemIds,
defaultNodeIcon,
defaultLeafIcon
}: TreeNodeProps) => {
const [value, setValue] = React.useState(
expandedItemIds.includes(item.id) ? [item.id] : []
)
const isSelected = selectedItemIds.has(item.id);
return (
<AccordionPrimitive.Root
type="multiple"
value={value}
onValueChange={(s) => setValue(s)}
>
<AccordionPrimitive.Item value={item.id}>
<AccordionTrigger
className={cn(
"flex items-center w-full py-2",
treeVariants(),
isSelected && selectedTreeVariants()
)}
onClick={(e) => {
e.stopPropagation();
if (clickRowToSelect) {
handleSelectChange(item);
}
item.onClick?.();
}}
>
<div className="flex items-center min-w-0 flex-shrink-0">
<TreeIcon
item={item}
isSelected={isSelected}
isOpen={value.includes(item.id)}
default={defaultNodeIcon}
onCheck={() => { handleSelectChange(item) }}
/>
<span className="ml-2 text-sm truncate">
{item.name}
</span>
</div>
{item.attributes && (
<span className="mx-auto text-sm text-muted-foreground whitespace-nowrap">
{item.attributes}
</span>
)}
{item.badge && (
<TreeBadge isSelected={isSelected}>
{item.badge}
</TreeBadge>
)}
</AccordionTrigger>
<AccordionContent className="ml-4 pl-1 border-l">
<TreeItem
data={item.children ? item.children : item}
selectedItemIds={selectedItemIds}
clickRowToSelect={clickRowToSelect}
handleSelectChange={handleSelectChange}
expandedItemIds={expandedItemIds}
defaultLeafIcon={defaultLeafIcon}
defaultNodeIcon={defaultNodeIcon}
/>
</AccordionContent>
</AccordionPrimitive.Item>
</AccordionPrimitive.Root>
)
}
// function hasSelectedChildrenRecursive(item: TreeDataItem, selectedItemIds: Set<string>): boolean {
// if (!item.children) return false;
// return item.children.some(child =>
// selectedItemIds.has(child.id) ||
// (child.children && hasSelectedChildrenRecursive(child, selectedItemIds)));
// }
interface TreeLeafProps extends React.HTMLAttributes<HTMLDivElement> {
item: TreeDataItem
selectedItemIds: Set<string>
clickRowToSelect?: boolean
handleSelectChange: (item: TreeDataItem | undefined) => void
defaultLeafIcon?: React.ComponentType<{ className?: string }>
}
const TreeLeaf = React.forwardRef<HTMLDivElement, TreeLeafProps>(
(
{
className,
item,
clickRowToSelect,
selectedItemIds,
handleSelectChange,
defaultLeafIcon,
...props
},
ref
) => {
return (
<div
ref={ref}
className={cn(
"ml-5 flex items-center py-2 cursor-pointer before:right-1",
treeVariants(),
className,
selectedItemIds.has(item.id) && selectedTreeVariants()
)}
onClick={(e) => {
e.stopPropagation();
if (clickRowToSelect) {
handleSelectChange(item);
}
item.onClick?.();
}}
{...props}
>
<div className="flex items-center min-w-0 flex-shrink-0">
<TreeIcon
item={item}
isSelected={selectedItemIds.has(item.id)}
default={defaultLeafIcon}
onCheck={() => { handleSelectChange(item) }}
/>
<span className="ml-2 text-sm truncate">
{item.name}
</span>
</div>
{item.attributes && (
<span className="mx-auto text-sm text-muted-foreground whitespace-nowrap">
{item.attributes}
</span>
)}
{item.badge && (
<TreeBadge isSelected={selectedItemIds.has(item.id)}>
{item.badge}
</TreeBadge>
)}
</div>
)
}
)
TreeLeaf.displayName = 'TreeLeaf'
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header>
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 w-full items-center py-2 transition-all first:[&[data-state=open]>svg]:rotate-90',
className
)}
{...props}
onClick={(e) => {
e.stopPropagation()
if (props.onClick) {
props.onClick(e)
}
}}
>
<ChevronRight className="h-4 w-4 shrink-0 transition-transform duration-200 text-accent-foreground/50 mr-1" />
{children}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
'overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
className
)}
{...props}
>
<div className="pb-1 pt-0">{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
interface TreeIconProps {
item: TreeDataItem;
isOpen?: boolean;
isSelected?: boolean;
default?: React.ComponentType<{ className?: string }>;
onCheck?: (checked: boolean) => void;
}
const TreeIcon = ({
item,
isOpen,
isSelected,
default: defaultIcon,
onCheck,
}: TreeIconProps) => {
let Icon = defaultIcon;
if (isOpen && item.openIcon) {
Icon = item.openIcon;
} else if (item.icon) {
Icon = item.icon;
}
return (
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={isSelected}
onChange={() => onCheck?.(!isSelected)}
onClick={(e) => e.stopPropagation()}
className={cn(
"h-4 w-4 rounded border border-primary dark:border-white shadow transition-all duration-200",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
isSelected
? "bg-black dark:bg-white text-primary-foreground"
: "bg-transparent",
"appearance-none cursor-pointer flex items-center justify-center relative",
"after:content-[''] after:w-1.5 after:h-2",
"after:border-r-2 after:border-b-2 after:rotate-45 after:mt-[-2px]",
isSelected
? "after:block after:border-white dark:after:border-black after:z-10"
: "after:hidden"
)}
/>
{Icon && <Icon className="h-4 w-4 shrink-0" />}
</div>
);
};
interface TreeBadgeProps {
children: React.ReactNode
isSelected: boolean
showOnSelectedOnly?: boolean
}
const TreeBadge = ({
children,
isSelected,
showOnSelectedOnly = false
}: TreeBadgeProps) => {
return (
<div
className={cn(
showOnSelectedOnly
? isSelected
? 'block'
: 'hidden'
: 'block',
'absolute right-3 group-hover:block'
)}
>
{children}
</div>
)
}
export { TreeView, type TreeDataItem }
@@ -32,14 +32,113 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { AccountModel } from '../data/schema'
import { toast } from '@/hooks/use-toast'
import { list_mailboxes, MailboxData } from '@/api/mailbox/api'
import { buildTree } from '@/lib/build-tree'
import { TreeDataItem, TreeView } from '@/components/tree-view'
import { buildTree, ExtendedTreeItemProps } from '@/lib/build-tree'
import { Skeleton } from '@/components/ui/skeleton'
import { update_account } from '@/api/account/api'
import { ToastAction } from '@/components/ui/toast'
import axios, { AxiosError } from 'axios'
import { ScrollArea } from '@/components/ui/scroll-area'
import { useTranslation } from 'react-i18next'
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
import { useTheme } from '@/context/theme-context'
import React from 'react'
import Collapse from '@mui/material/Collapse';
import { styled } from '@mui/material/styles';
import { TreeItemCheckbox, TreeItemContent, TreeItemIconContainer, TreeItemLabel, TreeItemRoot } from '@mui/x-tree-view/TreeItem'
import { TreeItemDragAndDropOverlay, TreeItemIcon, TreeItemProvider, TreeViewBaseItem, TreeViewSelectionPropagation, useTreeItem, useTreeItemModel, UseTreeItemParameters } from '@mui/x-tree-view'
import { animated, useSpring } from '@react-spring/web';
import { TransitionProps } from '@mui/material/transitions'
function getParentIds(tree: TreeViewBaseItem[]): string[] {
const result: string[] = [];
function traverse(nodes: TreeViewBaseItem[]) {
for (const node of nodes) {
if (node.children && node.children.length > 0) {
result.push(node.id);
traverse(node.children);
}
}
}
traverse(tree);
return result;
}
interface CustomLabelProps {
exists?: number;
attributes?: { attr: string; extension: string | null }[],
children: React.ReactNode;
icon?: React.ElementType;
expandable?: boolean;
}
function CustomLabel({
expandable,
exists,
attributes,
children,
...other
}: CustomLabelProps) {
return (
<TreeItemLabel
{...other}
sx={{
display: 'flex',
alignItems: 'center',
}}
>
<span className="font-medium text-sm text-inherit">
{children}
</span>
<div className="flex gap-2 ml-auto mr-3 opacity-70 text-xs">
{attributes?.map((attr) => {
const text =
attr.attr === 'Extension'
? attr.extension
: attr.attr;
return (
<span key={attr.attr} className="text-inherit">
{text}
</span>
);
})}
</div>
{exists !== undefined && (
<span
className="text-sm opacity-60 min-w-[40px] text-right text-inherit"
>
{exists}
</span>
)}
</TreeItemLabel>
);
}
const CustomCollapse = styled(Collapse)({
padding: 0,
});
const AnimatedCollapse = animated(CustomCollapse);
function TransitionComponent(props: TransitionProps) {
const style = useSpring({
to: {
opacity: props.in ? 1 : 0,
transform: `translate3d(0,${props.in ? 0 : 20}px,0)`,
},
});
return <AnimatedCollapse style={style} {...props} />;
}
interface CustomTreeItemProps
extends Omit<UseTreeItemParameters, 'rootRef'>,
Omit<React.HTMLAttributes<HTMLLIElement>, 'onFocus'> { }
interface Props {
open: boolean
@@ -48,15 +147,24 @@ interface Props {
}
export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
const [selectedFolders, setSelectedFolders] = useState<string[]>(currentRow.sync_folders || []);
const [selectedItems, setSelectedItems] = React.useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [allIds, setAllIds] = useState<string[]>([]);
const [expandedItems, setExpandedItems] = useState<string[]>([]);
const [itemsWithChildren, setItemsWithChildren] = useState<string[]>([]);
const [mailboxes, setMailboxes] = useState<MailboxData[]>([]);
const [selectionPropagation, setSelectionPropagation] =
React.useState<TreeViewSelectionPropagation>({
parents: false,
descendants: true,
});
const [treeData, setTreeData] = useState<TreeViewBaseItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const queryClient = useQueryClient();
const { t } = useTranslation()
const { theme } = useTheme()
useEffect(() => {
if (!open) return;
@@ -67,6 +175,18 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
const data = await list_mailboxes(currentRow.id, true);
if (!cancelled) {
setMailboxes(data);
const allIds = data.map(mailbox => String(mailbox.id));
setAllIds(allIds);
const tree = buildTree(data);
setTreeData(tree);
const itemsWithChildren = getParentIds(tree);
setItemsWithChildren(itemsWithChildren);
setExpandedItems(itemsWithChildren);
const sync_folders = data
.filter(mailbox => currentRow.sync_folders.includes(mailbox.name))
.map(mailbox => mailbox.id.toString());
setSelectedItems(sync_folders);
setError(undefined);
}
} catch (err: any) {
@@ -77,11 +197,6 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
} else {
setError(err.message);
}
} else {
console.error('Other error:', err);
}
if (!cancelled) {
setMailboxes([]);
}
} finally {
if (!cancelled) setIsLoading(false);
@@ -93,61 +208,90 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
};
}, [currentRow, open]);
// Convert mailbox names to IDs for initial selection
const initialSelectedItemIds = useMemo(() => {
if (!mailboxes) return [];
return mailboxes
.filter(mailbox => selectedFolders.includes(mailbox.name))
.map(mailbox => mailbox.id.toString());
}, [mailboxes, selectedFolders]);
const treeData = useMemo(() => {
if (!mailboxes) return [];
return buildTree(mailboxes, undefined, true, true);
}, [mailboxes]);
const handleSelectItems = useCallback((selectedItems: TreeDataItem[]) => {
const allMailboxes = mailboxes || [];
const selected = selectedItems
.map(item => mailboxes?.find(m => m.id === parseInt(item.id, 10))?.name)
.filter(Boolean) as string[];
const handleExpandedItemsChange = (
_event: React.SyntheticEvent | null,
itemIds: string[],
) => {
setExpandedItems(itemIds);
};
const allMailSelected = selected.some(selectedName => {
const mailbox = allMailboxes.find(m => m.name === selectedName);
if (!mailbox) return false;
return mailbox.attributes.some(a => a.attr === 'All');
const handleExpandClick = () => {
setExpandedItems((oldExpanded) =>
oldExpanded.length === 0 ? itemsWithChildren : [],
);
};
const CustomTreeItem = useMemo(() => {
return React.forwardRef(function CustomTreeItem(
props: CustomTreeItemProps,
ref: React.Ref<HTMLLIElement>,
) {
const { id, itemId, label, disabled, children, ...other } = props;
const {
getContextProviderProps,
getRootProps,
getContentProps,
getIconContainerProps,
getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
getDragAndDropOverlayProps,
status,
} = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
const item = useTreeItemModel<ExtendedTreeItemProps>(itemId)!;
return (
<TreeItemProvider {...getContextProviderProps()}>
<TreeItemRoot {...getRootProps(other)}>
<TreeItemContent {...getContentProps()}>
<TreeItemIconContainer {...getIconContainerProps()}>
<TreeItemIcon status={status} />
</TreeItemIconContainer>
<TreeItemCheckbox {...getCheckboxProps()} />
<CustomLabel
{...getLabelProps({
exists: item.exists,
attributes: item.attributes,
expandable: status.expandable && status.expanded,
})}
/>
<TreeItemDragAndDropOverlay {...getDragAndDropOverlayProps()} />
</TreeItemContent>
{children && <TransitionComponent {...getGroupTransitionProps()} />}
</TreeItemRoot>
</TreeItemProvider>
);
});
}, [theme]);
if (allMailSelected) {
const handleSelectAll = useCallback(() => {
const selectedSet = new Set(allIds);
const selectedAllMailbox = mailboxes.find((mb) =>
selectedSet.has(String(mb.id)) &&
mb.attributes?.some(a => a.attr === 'All')
);
if (selectedAllMailbox) {
toast({
title: t('accounts.allMailFolderSelected'),
description: t('accounts.allMailFolderSelectedDesc'),
action: <ToastAction altText={t('common.ok')}>{t('common.ok')}</ToastAction>,
});
}
setSelectedFolders(selected);
}, [mailboxes]);
const handleSelectAll = useCallback(() => {
if (!mailboxes) return;
const validFolderNames = mailboxes
.filter(mailbox => {
const isAllMail = mailbox.attributes.some(a => a.attr === 'All');
if (isAllMail) return false;
return true;
})
.map(m => m.name);
setSelectedFolders(validFolderNames);
if (validFolderNames.length < mailboxes.length) {
toast({
description: t('accounts.allMailSkipped'),
});
}
}, [mailboxes]);
setSelectedItems(allIds);
}, [allIds]);
const handleDeselectAll = useCallback(() => {
setSelectedFolders([]);
setSelectedItems([]);
}, []);
@@ -184,8 +328,30 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
console.error(error);
}
const handleSelectedItemsChange = (
_event: React.SyntheticEvent | null,
newSelectedItems: string[],
) => {
const selectedSet = new Set(newSelectedItems);
const selectedAllMailbox = mailboxes.find((mb) =>
selectedSet.has(String(mb.id)) &&
mb.attributes?.some(a => a.attr === 'All')
);
if (selectedAllMailbox) {
toast({
title: t('accounts.allMailFolderSelected'),
description: t('accounts.allMailFolderSelectedDesc'),
action: <ToastAction altText={t('common.ok')}>{t('common.ok')}</ToastAction>,
});
}
setSelectedItems(newSelectedItems);
};
const handleSubmit = async () => {
if (selectedFolders.length === 0) {
if (selectedItems.length === 0) {
toast({
title: t('common.error'),
description: t('accounts.selectAtLeastOneFolder'),
@@ -194,14 +360,23 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
return;
}
setIsSubmitting(true);
const selectedNames: string[] = [];
const idSet = new Set(selectedItems);
for (const mailbox of mailboxes) {
if (idSet.has(String(mailbox.id))) {
selectedNames.push(mailbox.name);
}
}
updateMutation.mutate({
sync_folders: selectedFolders,
sync_folders: selectedNames,
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl">
<DialogContent className="sm:max-w-3xl">
<DialogHeader>
<DialogTitle>{t('accounts.selectSyncFolders')}</DialogTitle>
<DialogDescription>
@@ -210,13 +385,13 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center justify-between pt-2">
<div className="flex gap-2">
<div className="flex flex-col pt-2 gap-2">
<div className="flex gap-2 flex-wrap">
<Button
variant="outline"
size="sm"
onClick={handleSelectAll}
disabled={isLoading || !mailboxes || mailboxes.length === 0}
disabled={isLoading || !allIds || allIds.length === 0}
className="h-8"
>
<CheckSquare className="w-4 h-4 mr-2" />
@@ -226,23 +401,62 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
variant="outline"
size="sm"
onClick={handleDeselectAll}
disabled={isLoading || selectedFolders.length === 0}
disabled={isLoading || allIds.length === 0}
className="h-8"
>
<Square className="w-4 h-4 mr-2" />
{t('common.deselectAll')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
setSelectionPropagation(prev => ({
...prev,
descendants: !prev.descendants,
}))
}
disabled={isLoading}
className="h-8"
>
{selectionPropagation.descendants ? <CheckSquare className="w-4 h-4 mr-2" /> : <Square className="w-4 h-4 mr-2" />}
{t('accounts.folderSync.autoSelectDescendants')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
setSelectionPropagation(prev => ({
...prev,
parents: !prev.parents,
}))
}
disabled={isLoading}
className="h-8"
>
{selectionPropagation.parents ? <CheckSquare className="w-4 h-4 mr-2" /> : <Square className="w-4 h-4 mr-2" />}
{t('accounts.folderSync.autoSelectParents')}
</Button>
<Button
variant="outline"
size="sm"
onClick={handleExpandClick}
disabled={isLoading}>
{expandedItems.length === 0 ? t('accounts.folderSync.expandAll') : t('accounts.folderSync.collapseAll')}
</Button>
</div>
<div className="text-sm text-muted-foreground">
{t('accounts.foldersSelected', { count: selectedFolders.length })}
{t('accounts.foldersSelected', { count: selectedItems.length })}
</div>
</div>
<ScrollArea className="h-[30rem] w-full pr-4 -mr-4 py-1">
<ScrollArea className="h-[35rem] w-full pr-4 -mr-4 py-1">
{isLoading && (
<div className="p-8 space-y-8">
<div className="flex flex-col items-center gap-3 text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="text-sm font-medium">Loading mailbox folders</span>
<span className="text-sm font-medium">{t('accounts.folderSync.loadingMailboxFolders')}</span>
</div>
<div className="space-y-2">
@@ -253,14 +467,16 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
</div>
)}
{!isLoading && (
<TreeView
key={selectedFolders.length > 0 ? `tree-${selectedFolders.length}-${selectedFolders[0]}` : 'tree-empty'}
data={treeData}
multiple
expandAll
clickRowToSelect={false}
initialSelectedItemIds={initialSelectedItemIds}
onSelectItemsChange={handleSelectItems}
<RichTreeView
multiSelect
checkboxSelection
items={treeData}
expandedItems={expandedItems}
onExpandedItemsChange={handleExpandedItemsChange}
selectionPropagation={selectionPropagation}
selectedItems={selectedItems}
onSelectedItemsChange={handleSelectedItemsChange}
slots={{ item: CustomTreeItem }}
/>
)}
{error && (
@@ -60,7 +60,6 @@ export function MailList({
}
}
const hasSelected = (mailId: number) => {
return selected.has(mailId);
}
@@ -77,15 +76,14 @@ export function MailList({
});
}
if (isLoading) {
return (
<div className="divide-y divide-border">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 px-3 py-2.5">
<Skeleton className="h-4 w-4 rounded-full" />
<Skeleton className="h-4 flex-1 max-w-xs" />
<Skeleton className="h-3 w-16 ml-auto" />
<div key={i} className="flex items-center gap-2 px-2 py-1.5">
<Skeleton className="h-3 w-3 rounded-full" />
<Skeleton className="h-3 flex-1 max-w-xs" />
<Skeleton className="h-2.5 w-12 ml-auto" />
</div>
))}
</div>
@@ -95,7 +93,7 @@ export function MailList({
return (
<div className="divide-y divide-border">
{items.length > 0 && (
<div className="flex items-center gap-3 px-3 py-2 bg-muted/30">
<div className="flex items-center gap-2 px-2 py-1 bg-muted/30">
<Checkbox
checked={
selected.size === items.length && items.length > 0
@@ -123,7 +121,7 @@ export function MailList({
<div
key={index}
className={cn(
"group flex items-center gap-3 px-3 py-2.5 cursor-pointer transition-colors",
"group flex items-center gap-2 px-2 py-1.5 cursor-pointer transition-colors",
"hover:bg-accent/50",
isSelected && "bg-accent"
)}
@@ -140,12 +138,11 @@ export function MailList({
onClick={(e) => e.stopPropagation()}
className="h-4 w-4 shrink-0"
/>
<MailIcon className="h-4 w-4 text-muted-foreground shrink-0" />
<MailIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0 grid grid-cols-1 sm:grid-cols-12 gap-1 sm:gap-0">
{/* LEFT AREA: From + Subject + Tags */}
<div className="col-span-1 sm:col-span-8 flex flex-col min-w-0">
<div className="flex items-center gap-2 min-w-0">
<div className="col-span-1 sm:col-span-8 flex flex-col min-w-0 gap-1">
<div className="flex items-center gap-1 min-w-0">
<p className="text-sm font-medium truncate">{item.from}</p>
<h3 className="text-sm text-muted-foreground truncate hidden sm:block">
{item.subject}
@@ -156,7 +153,7 @@ export function MailList({
</h3>
{/* TAGS BELOW SUBJECT */}
<div className="flex flex-wrap gap-1 mt-0.5">
<div className="flex flex-wrap gap-1 mt-0.25">
{item.tags?.map((tag, i) => (
<Badge
key={i}
@@ -169,10 +166,10 @@ export function MailList({
</div>
{/* RIGHT AREA actions & meta */}
<div className="col-span-1 sm:col-span-4 flex items-center justify-end gap-2 text-xs text-muted-foreground">
<div className="col-span-1 sm:col-span-4 flex items-center justify-end gap-1 text-xs text-muted-foreground">
{hasAttachments && (
<div className="flex items-center gap-1">
<Paperclip className="h-3.5 w-3.5" />
<Paperclip className="h-3 w-3" />
<span>{item.attachments?.length}</span>
</div>
)}
@@ -190,7 +187,7 @@ export function MailList({
}}
className="p-1 rounded hover:bg-destructive/10 hover:text-destructive transition-all"
>
<Trash2 className="h-3.5 w-3.5" />
<Trash2 className="h-3 w-3" />
</button>
</div>
</div>
@@ -200,4 +197,4 @@ export function MailList({
{totalSelected > 0 && <MailBulkActions />}
</div>
)
}
}
+165 -13
View File
@@ -27,11 +27,9 @@ import {
import { Separator } from "@/components/ui/separator"
import { TooltipProvider } from "@/components/ui/tooltip"
import { AccountSwitcher } from "./account-switcher"
import { TreeView } from "@/components/tree-view"
import { ScrollArea } from "@/components/ui/scroll-area"
import { list_mailboxes, MailboxData } from "@/api/mailbox/api"
import { useQuery } from "@tanstack/react-query"
import { buildTree } from "../../../lib/build-tree"
import { Skeleton } from "@/components/ui/skeleton"
import MailboxProvider, { MailboxDialogType } from "../context"
import useDialogState from "@/hooks/use-dialog-state"
@@ -44,6 +42,14 @@ import { EnvelopeDeleteDialog } from "./delete-dialog"
import Logo from '@/assets/logo.svg'
import { EmailEnvelope } from "@/api"
import { EnvelopeListPagination } from "@/components/pagination"
import { RichTreeView, TreeItemCheckbox, TreeItemContent, TreeItemDragAndDropOverlay, TreeItemIcon, TreeItemIconContainer, TreeItemLabel, TreeItemProvider, TreeItemRoot, useTreeItem, useTreeItemModel, UseTreeItemParameters } from "@mui/x-tree-view"
import { buildTree, ExtendedTreeItemProps } from "@/lib/build-tree"
import { useTheme } from "@/context/theme-context"
import { styled } from "@mui/material/styles"
import { animated, useSpring } from "@react-spring/web"
import { TransitionProps } from "@mui/material/transitions"
import Collapse from "@mui/material/Collapse"
import { FolderIcon } from "lucide-react"
interface MailProps {
@@ -73,6 +79,83 @@ const useListMessages = ({ accountId, mailboxId, page, page_size }: ListMessages
};
interface CustomLabelProps {
exists?: number;
attributes?: { attr: string; extension: string | null }[],
children: React.ReactNode;
icon?: React.ElementType;
expandable?: boolean;
}
function CustomLabel({
expandable,
exists,
attributes,
children,
...other
}: CustomLabelProps) {
return (
<TreeItemLabel
{...other}
sx={{
display: 'flex',
alignItems: 'center',
}}
>
<FolderIcon className="mr-2"/>
<span className="font-medium text-sm text-inherit">
{children}
</span>
{/* <div className="flex gap-2 ml-auto mr-3 opacity-70 text-xs">
{attributes?.map((attr) => {
const text =
attr.attr === 'Extension'
? attr.extension
: attr.attr;
return (
<span key={attr.attr} className="text-inherit">
{text}
</span>
);
})}
</div>
{exists !== undefined && (
<span
className="text-sm opacity-60 min-w-[40px] text-right text-inherit"
>
{exists}
</span>
)} */}
</TreeItemLabel>
);
}
const CustomCollapse = styled(Collapse)({
padding: 0,
});
const AnimatedCollapse = animated(CustomCollapse);
function TransitionComponent(props: TransitionProps) {
const style = useSpring({
to: {
opacity: props.in ? 1 : 0,
transform: `translate3d(0,${props.in ? 0 : 20}px,0)`,
},
});
return <AnimatedCollapse style={style} {...props} />;
}
interface CustomTreeItemProps
extends Omit<UseTreeItemParameters, 'rootRef'>,
Omit<React.HTMLAttributes<HTMLLIElement>, 'onFocus'> { }
export function Mail({
defaultLayout = [20, 80],
defaultCollapsed = false,
@@ -88,6 +171,7 @@ export function Mail({
const [pageSize, setPageSize] = React.useState(30);
const [deleteIds, setDeleteIds] = React.useState<Set<number>>(() => new Set());
const [selected, setSelected] = React.useState<Set<number>>(() => new Set());
const { theme } = useTheme()
const { data: mailboxes, isLoading: isMailboxesLoading } = useQuery({
queryKey: ['account-mailboxes', `${selectedAccountId}`],
@@ -95,6 +179,9 @@ export function Mail({
enabled: !!selectedAccountId,
})
const tree = buildTree(mailboxes ?? []);
const { data: envelopes, isLoading: isMessagesLoading, isError, error } = useListMessages({
accountId: selectedAccountId,
mailboxId: selectedMailbox?.id,
@@ -126,6 +213,65 @@ export function Mail({
}
}, [isError, error]);
const handleItemSelectionToggle = (
_event: React.SyntheticEvent | null,
itemId: string,
isSelected: boolean,
) => {
if (isSelected) {
setSelectedMailbox(mailboxes?.find(m => String(m.id) === itemId))
setPage(0);
}
};
const CustomTreeItem = React.useMemo(() => {
return React.forwardRef(function CustomTreeItem(
props: CustomTreeItemProps,
ref: React.Ref<HTMLLIElement>,
) {
const { id, itemId, label, disabled, children, ...other } = props;
const {
getContextProviderProps,
getRootProps,
getContentProps,
getIconContainerProps,
getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
getDragAndDropOverlayProps,
status,
} = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
const item = useTreeItemModel<ExtendedTreeItemProps>(itemId)!;
return (
<TreeItemProvider {...getContextProviderProps()}>
<TreeItemRoot {...getRootProps(other)}>
<TreeItemContent {...getContentProps()}>
<TreeItemIconContainer {...getIconContainerProps()}>
<TreeItemIcon status={status} />
</TreeItemIconContainer>
<TreeItemCheckbox {...getCheckboxProps()} />
<CustomLabel
{...getLabelProps({
exists: item.exists,
attributes: item.attributes,
expandable: status.expandable && status.expanded,
})}
/>
<TreeItemDragAndDropOverlay {...getDragAndDropOverlayProps()} />
</TreeItemContent>
{children && <TransitionComponent {...getGroupTransitionProps()} />}
</TreeItemRoot>
</TreeItemProvider>
);
});
}, [theme]);
return (
<MailboxProvider value={{ open, setOpen, currentMailbox: selectedMailbox, selectedAccountId, setCurrentMailbox: setSelectedMailbox, currentEnvelope: selectedEvelope, setCurrentEnvelope: setSelectedEvelope, deleteIds, setDeleteIds, selected, setSelected }}>
<TooltipProvider delayDuration={0}>
@@ -184,17 +330,23 @@ export function Mail({
))}
</div>
) : (
<TreeView
data={buildTree(mailboxes ?? [])}
clickRowToSelect={true}
onSelectChange={(item) => {
if (item) {
setSelectedMailbox(mailboxes?.find(m => m.id === parseInt(item.id, 10)))
setPage(0);
} else {
setSelectedMailbox(undefined)
}
}}
// <TreeView
// data={buildTree(mailboxes ?? [])}
// clickRowToSelect={true}
// onSelectChange={(item) => {
// if (item) {
// setSelectedMailbox(mailboxes?.find(m => m.id === parseInt(item.id, 10)))
// setPage(0);
// } else {
// setSelectedMailbox(undefined)
// }
// }}
// />
<RichTreeView
//checkboxSelection
items={tree}
onItemSelectionToggle={handleItemSelectionToggle}
slots={{ item: CustomTreeItem }}
/>
)}
</ScrollArea>
+24 -31
View File
@@ -21,7 +21,7 @@ import { cn, formatBytes } from "@/lib/utils"
import { formatDistanceToNow } from "date-fns"
import { MailIcon, MoreVertical, Paperclip, TagIcon, Trash2 } from "lucide-react"
import { Skeleton } from "@/components/ui/skeleton"
import { Checkbox } from "@/components/ui/checkbox" // shadcn Checkbox
import { Checkbox } from "@/components/ui/checkbox"
import { EmailEnvelope } from "@/api"
import { useSearchContext } from "./context"
import { MailBulkActions } from "./bulk-actions"
@@ -62,6 +62,7 @@ export function MailList({
});
}
}
const toggleToDelete = (accountId: number, mailId: number) => {
setToDelete(prev => {
const next = new Map(prev);
@@ -115,11 +116,11 @@ export function MailList({
return (
<div className="divide-y divide-border">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 p-3">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 w-4 rounded-full" />
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-3 w-20" />
<div key={i} className="flex items-center gap-2 px-2 py-1.5">
<Skeleton className="h-3 w-3" />
<Skeleton className="h-3 w-3 rounded-full" />
<Skeleton className="h-3 flex-1" />
<Skeleton className="h-2.5 w-16" />
</div>
))}
</div>
@@ -129,7 +130,7 @@ export function MailList({
return (
<div className="divide-y divide-border">
{items.length > 0 && (
<div className="flex items-center gap-3 px-3 py-2 bg-muted/30">
<div className="flex items-center gap-2 px-2 py-1 bg-muted/30">
<Checkbox
checked={
totalSelected === items.length && items.length > 0
@@ -158,7 +159,7 @@ export function MailList({
<div
key={index}
className={cn(
"flex items-center gap-3 px-3 py-2.5 cursor-pointer transition-colors",
"flex items-center gap-2 px-2 py-1.5 cursor-pointer transition-colors",
"hover:bg-accent/50",
isSelectedRow && "bg-accent"
)}
@@ -175,41 +176,35 @@ export function MailList({
className="h-4 w-4 shrink-0"
/>
<MailIcon className="h-4 w-4 text-muted-foreground shrink-0" />
<MailIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0 grid grid-cols-1 sm:grid-cols-12 gap-1 sm:gap-0">
{/* LEFT AREA: From + Subject + Tags */}
<div className="col-span-1 sm:col-span-8 flex flex-col min-w-0">
{/* from + subject (large screen side by side, small screen subject hidden) */}
<div className="flex items-center gap-2 min-w-0">
<div className="col-span-1 sm:col-span-8 flex flex-col min-w-0 gap-0.5">
<div className="flex items-center gap-1 min-w-0">
<p className="text-sm font-medium truncate">{item.from}</p>
{/* subject on large screens */}
<h3 className="text-sm text-muted-foreground truncate hidden sm:block">
{item.subject}
</h3>
</div>
{/* subject on small screens */}
<h3 className="text-sm text-muted-foreground truncate sm:hidden">
{item.subject}
</h3>
{/* TAGS (always below on small screen, inline on large screen) */}
<div className="flex flex-wrap gap-1 mt-0.5">
{item.tags?.map((tag, index) => (
<Badge className="px-1 py-0.5 text-[10px] h-auto leading-none" key={index}>{tag}</Badge>
<div className="flex flex-wrap gap-1 mt-0.25">
{item.tags?.map((tag, i) => (
<Badge className="px-1 py-0.5 text-[10px] h-auto leading-none" key={i}>{tag}</Badge>
))}
</div>
</div>
{/* RIGHT AREA actions & meta */}
<div className="col-span-1 sm:col-span-4 flex items-center justify-end gap-2 text-xs text-muted-foreground">
<div className="col-span-1 sm:col-span-4 flex items-center justify-end gap-1 text-xs text-muted-foreground">
{hasAttachments && (
<div className="flex items-center gap-1">
<Paperclip className="h-3.5 w-3.5" />
<Paperclip className="h-3 w-3" />
<span>{item.attachments?.length}</span>
</div>
)}
@@ -225,14 +220,14 @@ export function MailList({
<Button
variant="ghost"
size="icon"
className="h-7 w-7 p-0 hover:bg-muted rounded-md"
className="h-6 w-6 p-0 hover:bg-muted rounded-md"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuItem
onClick={(e) => e.stopPropagation()}
onSelect={(e) => {
@@ -241,7 +236,7 @@ export function MailList({
setOpen("edit-tags");
}}
>
<TagIcon className="ml-2 h-4 w-4" />
<TagIcon className="ml-2 h-3.5 w-3.5" />
{t('search.editTag')}
</DropdownMenuItem>
@@ -253,7 +248,7 @@ export function MailList({
handleDelete(item);
}}
>
<Trash2 className="ml-2 h-4 w-4" />
<Trash2 className="ml-2 h-3.5 w-3.5" />
{t('common.delete')}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -263,9 +258,7 @@ export function MailList({
</div>
)
})}
{totalSelected > 0 && (
<MailBulkActions />
)}
{totalSelected > 0 && <MailBulkActions />}
</div>
)
}
}
+78 -88
View File
@@ -18,100 +18,90 @@
import { MailboxData } from "@/api/mailbox/api";
import { TreeDataItem } from "@/components/tree-view";
import { Badge } from '@/components/ui/badge'
import { FolderClosed, FolderOpen } from "lucide-react";
import React from 'react';
import { TreeViewBaseItem } from '@mui/x-tree-view/models';
type BadgeContentFunction = (item: MailboxData) => React.ReactNode;
export const buildTree = (data: MailboxData[], badgeContent?: BadgeContentFunction, showAttributes?: boolean, showExists?: boolean): TreeDataItem[] => {
const root: TreeDataItem = {
id: 'root',
name: 'Root',
icon: FolderClosed,
openIcon: FolderOpen,
children: [], // Ensure children is initialized as an array
};
export type ExtendedTreeItemProps = {
exists?: number;
attributes?: { attr: string; extension: string | null }[],
id: string;
label: string;
};
const nodeMap: { [key: string]: TreeDataItem } = {};
data.sort((a, b) => a.name.localeCompare(b.name));
data.forEach((item) => {
const { id, name, delimiter, exists, attributes } = item;
const badge = showExists ? (badgeContent ? badgeContent(item) : React.createElement(Badge, {
className: 'text-[12px]',
variant: 'secondary',
}, exists)) : null;
const attributesNode = showAttributes
? attributes.map((item, index) =>
React.createElement(
Badge,
{
key: index,
className: 'text-[12px] mr-1 last:mr-0',
variant: 'secondary',
},
item.attr === 'Extension' ? item.extension || '' : item.attr
)
)
: null;
// const badge = badgeContent || React.createElement(Badge, {
// className: 'text-xs',
// }, exists);
// If there is no delimiter, add the item directly as a child of the root
if (!delimiter) {
root.children!.push({
id: id.toString(),
name,
icon: FolderClosed,
openIcon: FolderOpen,
badge,
attributes: showAttributes ? attributesNode : null,
children: undefined, // Leaf node, so children is undefined
});
return;
export function buildTree(items: MailboxData[]): TreeViewBaseItem<ExtendedTreeItemProps>[] {
const nodeByName = new Map<string, TreeViewBaseItem<ExtendedTreeItemProps>>();
for (const mb of items) {
if (!mb.name) continue;
const delimiter = mb.delimiter ?? '/';
const parts = mb.name.split(delimiter);
let currentFullName = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
currentFullName = currentFullName ? `${currentFullName}${delimiter}${part}` : part;
if (!nodeByName.has(currentFullName)) {
nodeByName.set(currentFullName, {
id: currentFullName,
label: part,
exists: mb.exists,
attributes: mb.attributes,
children: [],
});
}
if (i === parts.length - 1) {
const node = nodeByName.get(currentFullName)!;
node.id = String(mb.id);
}
}
}
const roots: TreeViewBaseItem<ExtendedTreeItemProps>[] = [];
for (const [fullName, node] of nodeByName.entries()) {
const delim = mbDelimiterOrDefault(fullName, items);
const lastDelimIndex = fullName.lastIndexOf(delim);
if (lastDelimIndex === -1) {
roots.push(node);
continue;
}
// Split the name into parts based on the delimiter
const parts = name.split(delimiter); // dir/sub1/sub2
let currentParent = root;
const parentFullName = fullName.substring(0, lastDelimIndex);
const parentNode = nodeByName.get(parentFullName);
// Traverse or create nodes for each part of the path
parts.forEach((part, index) => {
const path = parts.slice(0, index + 1).join(delimiter);
// If the node already exists, set it as the current parent
if (nodeMap[path]) {
currentParent = nodeMap[path];
} else {
// Determine if this is a leaf node (last part of the path)
const isLeaf = index === parts.length - 1;
// Create a new node
const newNode: TreeDataItem = {
id: isLeaf ? id.toString() : path, // Use item.id for leaf nodes, path for non-leaf nodes
name: part,
icon: FolderClosed,
openIcon: FolderOpen,
badge,
attributes: showAttributes ? attributesNode : null,
children: isLeaf ? undefined : [], // Ensure children is initialized as an array for non-leaf nodes
};
// Ensure currentParent.children is initialized as an array
if (!currentParent.children) {
currentParent.children = [];
}
// Add the new node to the current parent's children
currentParent.children.push(newNode);
currentParent = newNode; // Update the current parent to the new node
nodeMap[path] = newNode; // Store the node in the map for quick lookup
if (parentNode) {
parentNode.children = parentNode.children ?? [];
if (!parentNode.children.includes(node)) {
parentNode.children.push(node);
}
});
});
} else {
roots.push(node);
}
}
// Return the children of the root as the final tree structure
return root.children!;
};
const sortNodes = (nodes: TreeViewBaseItem[]) => {
nodes.sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
for (const n of nodes) {
if (n.children && n.children.length) sortNodes(n.children);
}
};
const uniqueRoots = Array.from(new Set(roots));
sortNodes(uniqueRoots);
return uniqueRoots;
}
function mbDelimiterOrDefault(fullName: string, items: MailboxData[]): string {
const mb = items.find(it => it.name === fullName || fullName.startsWith(it.name + (it.delimiter ?? '/')));
if (mb?.delimiter) return mb.delimiter;
const withDelim = items.find(it => it.delimiter);
if (withDelim?.delimiter) return withDelim.delimiter;
return '.';
}
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "لا توجد بيانات",
"noFolders": "لا توجد مجلدات للمزامنة",
"batches": "دفعات"
"batches": "دفعات",
"autoSelectDescendants": "تحديد العناصر الفرعية تلقائيًا",
"autoSelectParents": "تحديد العناصر الأصلية تلقائيًا",
"expandAll": "توسيع الكل",
"collapseAll": "طي الكل",
"loadingMailboxFolders": "جارٍ تحميل مجلدات البريد…"
},
"viewDetails": "عرض التفاصيل",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Ingen data",
"noFolders": "Ingen mapper at synkronisere",
"batches": "batcher"
"batches": "batcher",
"autoSelectDescendants": "Vælg efterkommere automatisk",
"autoSelectParents": "Vælg forældre automatisk",
"expandAll": "Udvid alle",
"collapseAll": "Skjul alle",
"loadingMailboxFolders": "Indlæser postkassemapper…"
},
"viewDetails": "vis detaljer",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Keine Daten",
"noFolders": "Keine Ordner zum Synchronisieren",
"batches": "Batches"
"batches": "Batches",
"autoSelectDescendants": "Unterelemente automatisch auswählen",
"autoSelectParents": "Elternelemente automatisch auswählen",
"expandAll": "Alle erweitern",
"collapseAll": "Alle reduzieren",
"loadingMailboxFolders": "Postfachordner werden geladen…"
},
"viewDetails": "Details anzeigen",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "No Data",
"noFolders": "No folders to sync",
"batches": "batches"
"batches": "batches",
"autoSelectDescendants": "Auto select descendants",
"autoSelectParents": "Auto select parents",
"expandAll": "Expand all",
"collapseAll": "Collapse all",
"loadingMailboxFolders": "Loading mailbox folders…"
},
"viewDetails": "view details",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Sin datos",
"noFolders": "No hay carpetas para sincronizar",
"batches": "lotes"
"batches": "lotes",
"autoSelectDescendants": "Seleccionar automáticamente los descendientes",
"autoSelectParents": "Seleccionar automáticamente los padres",
"expandAll": "Expandir todo",
"collapseAll": "Contraer todo",
"loadingMailboxFolders": "Cargando carpetas del buzón…"
},
"viewDetails": "ver detalles",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Ei tietoja",
"noFolders": "Ei synkronoitavia kansioita",
"batches": "erät"
"batches": "erät",
"autoSelectDescendants": "Valitse alisolmut automaattisesti",
"autoSelectParents": "Valitse yläsolmut automaattisesti",
"expandAll": "Laajenna kaikki",
"collapseAll": "Kutista kaikki",
"loadingMailboxFolders": "Ladataan postilaatikon kansioita…"
},
"viewDetails": "katso tiedot",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Aucune Donnée",
"noFolders": "Aucun dossier à synchroniser",
"batches": "lots"
"batches": "lots",
"autoSelectDescendants": "Sélectionner automatiquement les descendants",
"autoSelectParents": "Sélectionner automatiquement les parents",
"expandAll": "Tout développer",
"collapseAll": "Tout réduire",
"loadingMailboxFolders": "Chargement des dossiers de la boîte…"
},
"viewDetails": "voir les détails",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Nessun Dato",
"noFolders": "Nessuna cartella da sincronizzare",
"batches": "lotti"
"batches": "lotti",
"autoSelectDescendants": "Seleziona automaticamente i discendenti",
"autoSelectParents": "Seleziona automaticamente i genitori",
"expandAll": "Espandi tutto",
"collapseAll": "Comprimi tutto",
"loadingMailboxFolders": "Caricamento delle cartelle della casella…"
},
"viewDetails": "visualizza dettagli",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "データなし",
"noFolders": "同期するフォルダーがありません",
"batches": "バッチ"
"batches": "バッチ",
"autoSelectDescendants": "子項目を自動選択",
"autoSelectParents": "親項目を自動選択",
"expandAll": "すべて展開",
"collapseAll": "すべて折りたたむ",
"loadingMailboxFolders": "メールボックスフォルダーを読み込み中…"
},
"viewDetails": "詳細を見る",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "데이터 없음",
"noFolders": "동기화할 폴더가 없습니다",
"batches": "배치"
"batches": "배치",
"autoSelectDescendants": "하위 항목 자동 선택",
"autoSelectParents": "상위 항목 자동 선택",
"expandAll": "모두 펼치기",
"collapseAll": "모두 접기",
"loadingMailboxFolders": "메일 폴더 로딩 중…"
},
"viewDetails": "세부 정보 보기",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Geen Gegevens",
"noFolders": "Geen mappen om te synchroniseren",
"batches": "batches"
"batches": "batches",
"autoSelectDescendants": "Automatisch onderliggende items selecteren",
"autoSelectParents": "Automatisch bovenliggende items selecteren",
"expandAll": "Alles uitklappen",
"collapseAll": "Alles inklappen",
"loadingMailboxFolders": "Postvakken laden…"
},
"viewDetails": "details bekijken",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Ingen data",
"noFolders": "Ingen mapper å synkronisere",
"batches": "partier"
"batches": "partier",
"autoSelectDescendants": "Velg etterkommere automatisk",
"autoSelectParents": "Velg foreldre automatisk",
"expandAll": "Utvid alle",
"collapseAll": "Skjul alle",
"loadingMailboxFolders": "Laster inn postkassemapper…"
},
"viewDetails": "se detaljer",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Sem Dados",
"noFolders": "Nenhuma pasta para sincronizar",
"batches": "Lotes"
"batches": "Lotes",
"autoSelectDescendants": "Selecionar automaticamente os descendentes",
"autoSelectParents": "Selecionar automaticamente os pais",
"expandAll": "Expandir tudo",
"collapseAll": "Recolher tudo",
"loadingMailboxFolders": "Carregando pastas da caixa de correio…"
},
"viewDetails": "Ver Detalhes",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Нет данных",
"noFolders": "Нет папок для синхронизации",
"batches": "пакетов"
"batches": "пакетов",
"autoSelectDescendants": "Автоматически выбирать дочерние элементы",
"autoSelectParents": "Автоматически выбирать родительские элементы",
"expandAll": "Развернуть все",
"collapseAll": "Свернуть все",
"loadingMailboxFolders": "Загрузка папок почтового ящика…"
},
"viewDetails": "смотреть детали",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "Inga data",
"noFolders": "Inga mappar att synkronisera",
"batches": "satser"
"batches": "satser",
"autoSelectDescendants": "Välj underordnade automatiskt",
"autoSelectParents": "Välj överordnade automatiskt",
"expandAll": "Expandera alla",
"collapseAll": "Komprimera alla",
"loadingMailboxFolders": "Laddar brevlådemappar…"
},
"viewDetails": "visa detaljer",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "無資料",
"noFolders": "沒有可同步的資料夾",
"batches": "批次"
"batches": "批次",
"autoSelectDescendants": "自動選取子項",
"autoSelectParents": "自動選取父項",
"expandAll": "全部展開",
"collapseAll": "全部收合",
"loadingMailboxFolders": "正在載入郵件資料夾…"
},
"viewDetails": "檢視詳細資訊",
"runningState": {
+6 -1
View File
@@ -255,7 +255,12 @@
"folderSync": {
"noData": "无数据",
"noFolders": "没有需要同步的文件夹",
"batches": "批次"
"batches": "批次",
"autoSelectDescendants": "自动选择子项",
"autoSelectParents": "自动选择父项",
"expandAll": "全部展开",
"collapseAll": "全部收起",
"loadingMailboxFolders": "正在加载邮箱文件夹…"
},
"viewDetails": "查看详情",
"runningState": {