feat(task-management): enhance task list with new components and improved state management

- Introduced TaskListV2 and TaskGroupHeader components for a more organized task display.
- Implemented virtualized rendering using react-virtuoso for efficient task list handling.
- Updated Redux state management to include new selectors and improved task grouping logic.
- Added task filtering capabilities with TaskListFilters component for better user experience.
- Enhanced task selection handling and integrated drag-and-drop functionality for task rows.
- Updated package dependencies to include new libraries for icons and forms.
This commit is contained in:
chamikaJ
2025-07-03 15:31:54 +05:30
parent e26f16bbc2
commit d15c00c29b
19 changed files with 1316 additions and 602 deletions

View File

@@ -17,8 +17,10 @@
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@heroicons/react": "^2.2.0",
"@paddle/paddle-js": "^1.3.3", "@paddle/paddle-js": "^1.3.3",
"@reduxjs/toolkit": "^2.2.7", "@reduxjs/toolkit": "^2.2.7",
"@tailwindcss/forms": "^0.5.10",
"@tanstack/react-table": "^8.20.6", "@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.11.2", "@tanstack/react-virtual": "^3.11.2",
"@tinymce/tinymce-react": "^5.1.1", "@tinymce/tinymce-react": "^5.1.1",
@@ -48,6 +50,7 @@
"react-responsive": "^10.0.0", "react-responsive": "^10.0.0",
"react-router-dom": "^6.28.1", "react-router-dom": "^6.28.1",
"react-timer-hook": "^3.0.8", "react-timer-hook": "^3.0.8",
"react-virtuoso": "^4.13.0",
"react-window": "^1.8.11", "react-window": "^1.8.11",
"react-window-infinite-loader": "^1.0.10", "react-window-infinite-loader": "^1.0.10",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
@@ -73,7 +76,7 @@
"postcss": "^8.5.2", "postcss": "^8.5.2",
"prettier-plugin-tailwindcss": "^0.6.13", "prettier-plugin-tailwindcss": "^0.6.13",
"rollup": "^4.40.2", "rollup": "^4.40.2",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.17",
"terser": "^5.39.0", "terser": "^5.39.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"vite": "^6.3.5", "vite": "^6.3.5",
@@ -92,7 +95,6 @@
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@@ -1657,11 +1659,19 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@heroicons/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
"license": "MIT",
"peerDependencies": {
"react": ">= 16 || ^19.0.0-rc"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"string-width": "^5.1.2", "string-width": "^5.1.2",
@@ -1744,7 +1754,6 @@
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nodelib/fs.stat": "2.0.5", "@nodelib/fs.stat": "2.0.5",
@@ -1758,7 +1767,6 @@
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 8" "node": ">= 8"
@@ -1768,7 +1776,6 @@
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nodelib/fs.scandir": "2.1.5", "@nodelib/fs.scandir": "2.1.5",
@@ -1788,7 +1795,6 @@
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"engines": { "engines": {
@@ -2296,6 +2302,18 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tailwindcss/forms": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
"integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==",
"license": "MIT",
"dependencies": {
"mini-svg-data-uri": "^1.2.3"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
}
},
"node_modules/@tanstack/react-table": { "node_modules/@tanstack/react-table": {
"version": "8.21.3", "version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
@@ -2841,7 +2859,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -2851,7 +2868,6 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
@@ -2952,14 +2968,12 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/anymatch": { "node_modules/anymatch": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
@@ -2973,7 +2987,6 @@
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/aria-query": { "node_modules/aria-query": {
@@ -3111,7 +3124,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-arraybuffer": { "node_modules/base64-arraybuffer": {
@@ -3127,7 +3139,6 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -3140,7 +3151,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@@ -3150,7 +3160,6 @@
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.1.1"
@@ -3247,7 +3256,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
@@ -3363,7 +3371,6 @@
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"anymatch": "~3.1.2", "anymatch": "~3.1.2",
@@ -3388,7 +3395,6 @@
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
@@ -3407,7 +3413,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
@@ -3420,7 +3425,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": { "node_modules/combined-stream": {
@@ -3439,7 +3443,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
@@ -3539,7 +3542,6 @@
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
@@ -3576,7 +3578,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"cssesc": "bin/cssesc" "cssesc": "bin/cssesc"
@@ -3668,7 +3669,6 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/diff-sequences": { "node_modules/diff-sequences": {
@@ -3685,7 +3685,6 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/dom-accessibility-api": { "node_modules/dom-accessibility-api": {
@@ -3739,7 +3738,6 @@
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
@@ -3753,7 +3751,6 @@
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/engine.io-client": { "node_modules/engine.io-client": {
@@ -3943,7 +3940,6 @@
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.stat": "^2.0.2",
@@ -3960,7 +3956,6 @@
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
@@ -3973,7 +3968,6 @@
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"reusify": "^1.0.4" "reusify": "^1.0.4"
@@ -3989,7 +3983,6 @@
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
@@ -4028,7 +4021,6 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
@@ -4074,7 +4066,6 @@
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -4157,7 +4148,6 @@
"version": "10.4.5", "version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.1.0",
@@ -4178,7 +4168,6 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"is-glob": "^4.0.3" "is-glob": "^4.0.3"
@@ -4388,7 +4377,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"binary-extensions": "^2.0.0" "binary-extensions": "^2.0.0"
@@ -4416,7 +4404,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -4426,7 +4413,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -4436,7 +4422,6 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
@@ -4449,7 +4434,6 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
@@ -4459,14 +4443,12 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/jackspeak": { "node_modules/jackspeak": {
"version": "3.4.3", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/cliui": "^8.0.2" "@isaacs/cliui": "^8.0.2"
@@ -4851,7 +4833,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
@@ -4968,7 +4949,6 @@
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 8" "node": ">= 8"
@@ -4978,7 +4958,6 @@
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"braces": "^3.0.3", "braces": "^3.0.3",
@@ -5019,11 +4998,19 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
"license": "MIT",
"bin": {
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
@@ -5039,7 +5026,6 @@
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
@@ -5080,7 +5066,6 @@
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"any-promise": "^1.0.0", "any-promise": "^1.0.0",
@@ -5137,7 +5122,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -5166,7 +5150,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
@@ -5176,7 +5159,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/parent-module": { "node_modules/parent-module": {
@@ -5213,7 +5195,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -5229,7 +5210,6 @@
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"lru-cache": "^10.2.0", "lru-cache": "^10.2.0",
@@ -5246,7 +5226,6 @@
"version": "10.4.3", "version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
@@ -5306,7 +5285,6 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
@@ -5319,7 +5297,6 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -5329,7 +5306,6 @@
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
@@ -5367,7 +5343,6 @@
"version": "15.1.0", "version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"postcss-value-parser": "^4.0.0", "postcss-value-parser": "^4.0.0",
@@ -5385,7 +5360,6 @@
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"camelcase-css": "^2.0.1" "camelcase-css": "^2.0.1"
@@ -5405,7 +5379,6 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -5441,7 +5414,6 @@
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -5467,7 +5439,6 @@
"version": "6.1.2", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cssesc": "^3.0.0", "cssesc": "^3.0.0",
@@ -5481,7 +5452,6 @@
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/postcss/node_modules/nanoid": { "node_modules/postcss/node_modules/nanoid": {
@@ -5677,7 +5647,6 @@
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -6578,6 +6547,16 @@
"react-dom": ">=16.6.0" "react-dom": ">=16.6.0"
} }
}, },
"node_modules/react-virtuoso": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.13.0.tgz",
"integrity": "sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA==",
"license": "MIT",
"peerDependencies": {
"react": ">=16 || >=17 || >= 18 || >= 19",
"react-dom": ">=16 || >=17 || >= 18 || >=19"
}
},
"node_modules/react-window": { "node_modules/react-window": {
"version": "1.8.11", "version": "1.8.11",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz",
@@ -6621,7 +6600,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"pify": "^2.3.0" "pify": "^2.3.0"
@@ -6631,7 +6609,6 @@
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
@@ -6721,7 +6698,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"iojs": ">=1.0.0", "iojs": ">=1.0.0",
@@ -6816,7 +6792,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -6889,7 +6864,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
@@ -6902,7 +6876,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -6919,7 +6892,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
@@ -7063,7 +7035,6 @@
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"eastasianwidth": "^0.2.0", "eastasianwidth": "^0.2.0",
@@ -7082,7 +7053,6 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
@@ -7097,14 +7067,12 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/string-width-cjs/node_modules/strip-ansi": { "node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
@@ -7117,7 +7085,6 @@
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1" "ansi-regex": "^6.0.1"
@@ -7134,7 +7101,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
@@ -7147,7 +7113,6 @@
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@@ -7179,7 +7144,6 @@
"version": "3.35.0", "version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/gen-mapping": "^0.3.2",
@@ -7250,7 +7214,6 @@
"version": "3.4.17", "version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
@@ -7288,7 +7251,6 @@
"version": "1.21.7", "version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
@@ -7333,7 +7295,6 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"any-promise": "^1.0.0" "any-promise": "^1.0.0"
@@ -7343,7 +7304,6 @@
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"thenify": ">= 3.1.0 < 4" "thenify": ">= 3.1.0 < 4"
@@ -7466,7 +7426,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
@@ -7491,7 +7450,6 @@
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/tsconfck": { "node_modules/tsconfck": {
@@ -7586,7 +7544,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/utrie": { "node_modules/utrie": {
@@ -7868,7 +7825,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
@@ -7905,7 +7861,6 @@
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^6.1.0", "ansi-styles": "^6.1.0",
@@ -7924,7 +7879,6 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^4.0.0", "ansi-styles": "^4.0.0",
@@ -7942,14 +7896,12 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/wrap-ansi-cjs/node_modules/string-width": { "node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
@@ -7964,7 +7916,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
@@ -7977,7 +7928,6 @@
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@@ -8026,7 +7976,6 @@
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"

View File

@@ -21,8 +21,10 @@
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@heroicons/react": "^2.2.0",
"@paddle/paddle-js": "^1.3.3", "@paddle/paddle-js": "^1.3.3",
"@reduxjs/toolkit": "^2.2.7", "@reduxjs/toolkit": "^2.2.7",
"@tailwindcss/forms": "^0.5.10",
"@tanstack/react-table": "^8.20.6", "@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.11.2", "@tanstack/react-virtual": "^3.11.2",
"@tinymce/tinymce-react": "^5.1.1", "@tinymce/tinymce-react": "^5.1.1",
@@ -52,6 +54,7 @@
"react-responsive": "^10.0.0", "react-responsive": "^10.0.0",
"react-router-dom": "^6.28.1", "react-router-dom": "^6.28.1",
"react-timer-hook": "^3.0.8", "react-timer-hook": "^3.0.8",
"react-virtuoso": "^4.13.0",
"react-window": "^1.8.11", "react-window": "^1.8.11",
"react-window-infinite-loader": "^1.0.10", "react-window-infinite-loader": "^1.0.10",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
@@ -77,7 +80,7 @@
"postcss": "^8.5.2", "postcss": "^8.5.2",
"prettier-plugin-tailwindcss": "^0.6.13", "prettier-plugin-tailwindcss": "^0.6.13",
"rollup": "^4.40.2", "rollup": "^4.40.2",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.17",
"terser": "^5.39.0", "terser": "^5.39.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"vite": "^6.3.5", "vite": "^6.3.5",

View File

@@ -1,4 +1,5 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
// Auth & User // Auth & User
import authReducer from '@features/auth/authSlice'; import authReducer from '@features/auth/authSlice';
@@ -76,14 +77,14 @@ import teamMembersReducer from '@features/team-members/team-members.slice';
import groupByFilterDropdownReducer from '../features/group-by-filter-dropdown/group-by-filter-dropdown-slice'; import groupByFilterDropdownReducer from '../features/group-by-filter-dropdown/group-by-filter-dropdown-slice';
// Task Management System // Task Management System
import taskManagementReducer from '@features/task-management/task-management.slice'; import taskManagementReducer from '@/features/task-management/task-management.slice';
import groupingReducer from '@features/task-management/grouping.slice'; import groupingReducer from '@/features/task-management/grouping.slice';
import selectionReducer from '@features/task-management/selection.slice'; import selectionReducer from '@/features/task-management/selection.slice';
import homePageApiService from '@/api/home-page/home-page.api.service'; import homePageApiService from '@/api/home-page/home-page.api.service';
import { projectsApi } from '@/api/projects/projects.v1.api.service'; import { projectsApi } from '@/api/projects/projects.v1.api.service';
import projectViewReducer from '@features/project/project-view-slice'; import projectViewReducer from '@features/project/project-view-slice';
import taskManagementFields from '@features/task-management/taskListFields.slice'; import taskManagementFieldsReducer from '@features/task-management/taskListFields.slice';
export const store = configureStore({ export const store = configureStore({
middleware: getDefaultMiddleware => middleware: getDefaultMiddleware =>
@@ -172,9 +173,13 @@ export const store = configureStore({
taskManagement: taskManagementReducer, taskManagement: taskManagementReducer,
grouping: groupingReducer, grouping: groupingReducer,
taskManagementSelection: selectionReducer, taskManagementSelection: selectionReducer,
taskManagementFields, taskManagementFields: taskManagementFieldsReducer,
}, },
}); });
export type RootState = ReturnType<typeof store.getState>; export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch; export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
interface TaskGroupHeaderProps {
group: {
id: string;
name: string;
count: number;
color?: string; // Color for the group indicator
};
isCollapsed: boolean;
onToggle: () => void;
}
const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, onToggle }) => {
return (
<div
className="flex items-center px-4 py-2 bg-white dark:bg-gray-800 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 border-b border-gray-200 dark:border-gray-700"
onClick={onToggle}
>
{/* Chevron button */}
<button
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
onClick={(e) => {
e.stopPropagation();
onToggle();
}}
>
{isCollapsed ? (
<ChevronRightIcon className="h-4 w-4 text-gray-400 dark:text-gray-500" />
) : (
<ChevronDownIcon className="h-4 w-4 text-gray-400 dark:text-gray-500" />
)}
</button>
{/* Group indicator and name */}
<div className="ml-2 flex items-center gap-3 flex-1">
{/* Color indicator */}
<div
className="w-3 h-3 rounded-sm"
style={{ backgroundColor: group.color || '#94A3B8' }}
/>
{/* Group name and count */}
<div className="flex items-center justify-between flex-1">
<span className="text-sm font-medium text-gray-900 dark:text-white">
{group.name}
</span>
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded-full">
{group.count}
</span>
</div>
</div>
</div>
);
};
export default TaskGroupHeader;

View File

@@ -0,0 +1,264 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { GroupedVirtuoso } from 'react-virtuoso';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import {
selectAllTasksArray,
selectGroups,
selectGrouping,
selectLoading,
selectError,
selectSelectedPriorities,
selectSearch,
fetchTasksV3,
} from '@/features/task-management/task-management.slice';
import {
selectCurrentGrouping,
selectCollapsedGroups,
selectIsGroupCollapsed,
toggleGroupCollapsed,
} from '@/features/task-management/grouping.slice';
import {
selectSelectedTaskIds,
selectLastSelectedTaskId,
selectIsTaskSelected,
selectTask,
deselectTask,
toggleTaskSelection,
selectRange,
clearSelection,
} from '@/features/task-management/selection.slice';
import TaskRow from './TaskRow';
import TaskGroupHeader from './TaskGroupHeader';
import { Task, TaskGroup } from '@/types/task-management.types';
import { RootState } from '@/app/store';
import { TaskListField } from '@/types/task-list-field.types';
import { useParams } from 'react-router-dom';
import ImprovedTaskFilters from '@/components/task-management/improved-task-filters';
import { Bars3Icon } from '@heroicons/react/24/outline';
import { COLUMN_KEYS } from '@/features/tasks/tasks.slice';
// Base column configuration
const BASE_COLUMNS = [
{ id: 'dragHandle', label: '', width: '32px', isSticky: true, key: 'dragHandle' },
{ id: 'taskKey', label: 'Key', width: '100px', isSticky: true, key: COLUMN_KEYS.KEY },
{ id: 'title', label: 'Title', width: '300px', isSticky: true, key: COLUMN_KEYS.NAME },
{ id: 'status', label: 'Status', width: '120px', key: COLUMN_KEYS.STATUS },
{ id: 'assignees', label: 'Assignees', width: '150px', key: COLUMN_KEYS.ASSIGNEES },
{ id: 'priority', label: 'Priority', width: '120px', key: COLUMN_KEYS.PRIORITY },
{ id: 'dueDate', label: 'Due Date', width: '120px', key: COLUMN_KEYS.DUE_DATE },
{ id: 'progress', label: 'Progress', width: '120px', key: COLUMN_KEYS.PROGRESS },
{ id: 'labels', label: 'Labels', width: '150px', key: COLUMN_KEYS.LABELS },
{ id: 'phase', label: 'Phase', width: '120px', key: COLUMN_KEYS.PHASE },
{ id: 'timeTracking', label: 'Time Tracking', width: '120px', key: COLUMN_KEYS.TIME_TRACKING },
{ id: 'estimation', label: 'Estimation', width: '120px', key: COLUMN_KEYS.ESTIMATION },
{ id: 'startDate', label: 'Start Date', width: '120px', key: COLUMN_KEYS.START_DATE },
{ id: 'dueTime', label: 'Due Time', width: '120px', key: COLUMN_KEYS.DUE_TIME },
{ id: 'completedDate', label: 'Completed Date', width: '120px', key: COLUMN_KEYS.COMPLETED_DATE },
{ id: 'createdDate', label: 'Created Date', width: '120px', key: COLUMN_KEYS.CREATED_DATE },
{ id: 'lastUpdated', label: 'Last Updated', width: '120px', key: COLUMN_KEYS.LAST_UPDATED },
{ id: 'reporter', label: 'Reporter', width: '120px', key: COLUMN_KEYS.REPORTER },
];
type ColumnStyle = {
width: string;
position?: 'static' | 'relative' | 'absolute' | 'sticky' | 'fixed';
left?: number;
backgroundColor?: string;
zIndex?: number;
};
interface TaskListV2Props {
projectId: string;
}
const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
const dispatch = useAppDispatch();
const { projectId: urlProjectId } = useParams();
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
// Selectors
const tasks = useAppSelector(selectAllTasksArray);
const groups = useAppSelector(selectGroups);
const grouping = useAppSelector(selectGrouping);
const loading = useAppSelector(selectLoading);
const error = useAppSelector(selectError);
const selectedPriorities = useAppSelector(selectSelectedPriorities);
const searchQuery = useAppSelector(selectSearch);
const currentGrouping = useAppSelector(selectCurrentGrouping);
const selectedTaskIds = useAppSelector(selectSelectedTaskIds);
const lastSelectedTaskId = useAppSelector(selectLastSelectedTaskId);
const fields = useAppSelector(state => state.taskManagementFields) || [];
// Filter visible columns based on fields
const visibleColumns = useMemo(() => {
return BASE_COLUMNS.filter(column => {
// Always show drag handle, task key, and title
if (column.isSticky) return true;
// Check if field is visible
const field = fields.find(f => f.key === column.key);
return field?.visible ?? false;
});
}, [fields]);
// Effects
useEffect(() => {
if (urlProjectId) {
dispatch(fetchTasksV3(urlProjectId));
}
}, [dispatch, urlProjectId]);
// Handlers
const handleTaskSelect = useCallback((taskId: string, event: React.MouseEvent) => {
if (event.ctrlKey || event.metaKey) {
dispatch(toggleTaskSelection(taskId));
} else if (event.shiftKey && lastSelectedTaskId) {
const taskIds = tasks.map(t => t.id);
const startIdx = taskIds.indexOf(lastSelectedTaskId);
const endIdx = taskIds.indexOf(taskId);
const rangeIds = taskIds.slice(
Math.min(startIdx, endIdx),
Math.max(startIdx, endIdx) + 1
);
dispatch(selectRange(rangeIds));
} else {
dispatch(clearSelection());
dispatch(selectTask(taskId));
}
}, [dispatch, lastSelectedTaskId, tasks]);
const handleGroupCollapse = useCallback((groupId: string) => {
setCollapsedGroups(prev => {
const next = new Set(prev);
if (next.has(groupId)) {
next.delete(groupId);
} else {
next.add(groupId);
}
return next;
});
}, []);
// Memoized values
const groupCounts = useMemo(() => {
return groups.map(group => {
const visibleTasks = tasks.filter(task => group.taskIds.includes(task.id));
return visibleTasks.length;
});
}, [groups, tasks]);
const visibleGroups = useMemo(() => {
return groups.filter(group => !collapsedGroups.has(group.id));
}, [groups, collapsedGroups]);
// Render functions
const renderGroup = useCallback((groupIndex: number) => {
const group = groups[groupIndex];
return (
<TaskGroupHeader
group={{
id: group.id,
name: group.title,
count: groupCounts[groupIndex],
color: group.color,
}}
isCollapsed={collapsedGroups.has(group.id)}
onToggle={() => handleGroupCollapse(group.id)}
/>
);
}, [groups, groupCounts, collapsedGroups, handleGroupCollapse]);
const renderTask = useCallback((taskIndex: number) => {
const task = tasks[taskIndex];
return (
<TaskRow
task={task}
visibleColumns={visibleColumns}
/>
);
}, [tasks, visibleColumns]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
// Log data for debugging
console.log('Rendering with:', {
groups,
tasks,
groupCounts
});
return (
<div className="flex flex-col h-screen bg-white dark:bg-gray-900">
{/* Task Filters */}
<div className="flex-none px-4 py-3">
<ImprovedTaskFilters position="list" />
</div>
{/* Column Headers */}
<div className="overflow-x-auto">
<div className="flex-none border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<div className="flex items-center min-w-max px-4 py-2">
{visibleColumns.map((column, index) => {
const columnStyle: ColumnStyle = {
width: column.width,
...(column.isSticky ? {
position: 'sticky',
left: index === 0 ? 0 : index === 1 ? 32 : 132,
backgroundColor: 'inherit',
zIndex: 2,
} : {}),
};
return (
<div
key={column.id}
className="text-xs font-medium text-gray-500 dark:text-gray-400"
style={columnStyle}
>
{column.id === 'dragHandle' ? (
<Bars3Icon className="w-4 h-4 text-gray-400" />
) : (
column.label
)}
</div>
);
})}
</div>
</div>
{/* Task List */}
<div className="flex-1 overflow-hidden">
<GroupedVirtuoso
style={{ height: 'calc(100vh - 200px)' }}
groupCounts={groupCounts}
groupContent={renderGroup}
itemContent={renderTask}
components={{
Group: ({ children, ...props }) => (
<div
{...props}
className="sticky top-0 z-10 bg-white dark:bg-gray-800"
>
{children}
</div>
),
List: React.forwardRef(({ style, children }, ref) => (
<div
ref={ref as any}
style={style}
className="divide-y divide-gray-200 dark:divide-gray-700"
>
{children}
</div>
)),
}}
/>
</div>
</div>
</div>
);
};
export default TaskListV2;

View File

@@ -0,0 +1,261 @@
import React from 'react';
import { Task } from '@/types/task-management.types';
import Avatar from '@/components/Avatar';
import { format } from 'date-fns';
import { Bars3Icon } from '@heroicons/react/24/outline';
import { ClockIcon } from '@heroicons/react/24/outline';
interface TaskRowProps {
task: Task;
visibleColumns: Array<{
id: string;
width: string;
isSticky?: boolean;
}>;
}
const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
const renderColumn = (columnId: string, width: string, isSticky?: boolean, index?: number) => {
const baseStyle = {
width,
...(isSticky ? {
position: 'sticky' as const,
left: index === 0 ? 0 : index === 1 ? 32 : 132,
backgroundColor: 'inherit',
zIndex: 1,
} : {}),
};
switch (columnId) {
case 'dragHandle':
return (
<div
className="cursor-move flex items-center justify-center"
style={baseStyle}
>
<Bars3Icon className="w-4 h-4 text-gray-400" />
</div>
);
case 'taskKey':
return (
<div
className="flex items-center"
style={baseStyle}
>
<span className="text-sm font-medium text-gray-900 dark:text-white whitespace-nowrap">
{task.task_key}
</span>
</div>
);
case 'title':
return (
<div
className="flex items-center"
style={baseStyle}
>
<span className="text-sm text-gray-700 dark:text-gray-300 truncate">
{task.title || task.name}
</span>
</div>
);
case 'status':
return (
<div style={baseStyle}>
<span
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
style={{
backgroundColor: task.statusColor ? `${task.statusColor}20` : 'rgb(229, 231, 235)',
color: task.statusColor || 'rgb(31, 41, 55)',
}}
>
{task.status}
</span>
</div>
);
case 'assignees':
return (
<div className="flex items-center gap-1" style={baseStyle}>
{task.assignee_names?.slice(0, 3).map((assignee, index) => (
<Avatar
key={index}
name={assignee.name || ''}
size="small"
className="ring-2 ring-white dark:ring-gray-900"
/>
))}
{(task.assignee_names?.length || 0) > 3 && (
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
+{task.assignee_names!.length - 3}
</span>
)}
</div>
);
case 'priority':
return (
<div style={baseStyle}>
<span
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
style={{
backgroundColor: task.priorityColor ? `${task.priorityColor}20` : 'rgb(229, 231, 235)',
color: task.priorityColor || 'rgb(31, 41, 55)',
}}
>
{task.priority}
</span>
</div>
);
case 'dueDate':
return (
<div style={baseStyle}>
{task.dueDate && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(task.dueDate), 'MMM d')}
</span>
)}
</div>
);
case 'progress':
return (
<div style={baseStyle}>
<div className="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${task.progress}%` }}
/>
</div>
</div>
);
case 'labels':
return (
<div className="flex items-center gap-1" style={baseStyle}>
{task.labels?.slice(0, 2).map((label, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
style={{
backgroundColor: label.color ? `${label.color}20` : 'rgb(229, 231, 235)',
color: label.color || 'rgb(31, 41, 55)',
}}
>
{label.name}
</span>
))}
{(task.labels?.length || 0) > 2 && (
<span className="text-xs text-gray-500 dark:text-gray-400">
+{task.labels!.length - 2}
</span>
)}
</div>
);
case 'phase':
return (
<div style={baseStyle}>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
{task.phase}
</span>
</div>
);
case 'timeTracking':
return (
<div className="flex items-center gap-1" style={baseStyle}>
<ClockIcon className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-500 dark:text-gray-400">
{task.timeTracking?.logged || 0}h
</span>
{task.timeTracking?.estimated && (
<span className="text-sm text-gray-400 dark:text-gray-500">
/{task.timeTracking.estimated}h
</span>
)}
</div>
);
case 'estimation':
return (
<div style={baseStyle}>
{task.timeTracking.estimated && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{task.timeTracking.estimated}h
</span>
)}
</div>
);
case 'startDate':
return (
<div style={baseStyle}>
{task.startDate && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(task.startDate), 'MMM d')}
</span>
)}
</div>
);
case 'completedDate':
return (
<div style={baseStyle}>
{task.completedAt && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(task.completedAt), 'MMM d')}
</span>
)}
</div>
);
case 'createdDate':
return (
<div style={baseStyle}>
{task.createdAt && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(task.createdAt), 'MMM d')}
</span>
)}
</div>
);
case 'lastUpdated':
return (
<div style={baseStyle}>
{task.updatedAt && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(task.updatedAt), 'MMM d')}
</span>
)}
</div>
);
case 'reporter':
return (
<div style={baseStyle}>
{task.reporter && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{task.reporter}
</span>
)}
</div>
);
default:
return null;
}
};
return (
<div className="flex items-center min-w-max px-4 py-2 hover:bg-gray-50 dark:hover:bg-gray-800">
{visibleColumns.map((column, index) => renderColumn(column.id, column.width, column.isSticky, index))}
</div>
);
};
export default TaskRow;

View File

@@ -1,7 +1,7 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react'; import React, { useState, useMemo, useCallback, useEffect } from 'react';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { useSelector } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { import {
Button, Button,
Typography, Typography,
@@ -11,12 +11,15 @@ import {
DownOutlined, DownOutlined,
} from '@/shared/antd-imports'; } from '@/shared/antd-imports';
import { TaskGroup as TaskGroupType, Task } from '@/types/task-management.types'; import { TaskGroup as TaskGroupType, Task } from '@/types/task-management.types';
import { taskManagementSelectors } from '@/features/task-management/task-management.slice'; import { taskManagementSelectors, selectAllTasks } from '@/features/task-management/task-management.slice';
import { RootState } from '@/app/store'; import { RootState } from '@/app/store';
import TaskRow from './task-row'; import TaskRow from './task-row';
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row'; import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
import { TaskListField } from '@/features/task-management/taskListFields.slice'; import { TaskListField } from '@/features/task-management/taskListFields.slice';
import { Checkbox } from '@/components'; import { Checkbox } from '@/components';
import { selectIsGroupCollapsed, toggleGroupCollapsed } from '@/features/task-management/grouping.slice';
import { selectIsTaskSelected } from '@/features/task-management/selection.slice';
import { Draggable } from 'react-beautiful-dnd';
const { Text } = Typography; const { Text } = Typography;
@@ -58,6 +61,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(
onSelectTask, onSelectTask,
onToggleSubtasks, onToggleSubtasks,
}) => { }) => {
const dispatch = useDispatch();
const [isCollapsed, setIsCollapsed] = useState(group.collapsed || false); const [isCollapsed, setIsCollapsed] = useState(group.collapsed || false);
const { setNodeRef, isOver } = useDroppable({ const { setNodeRef, isOver } = useDroppable({
@@ -69,7 +73,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(
}); });
// Get all tasks from the store // Get all tasks from the store
const allTasks = useSelector(taskManagementSelectors.selectAll); const allTasks = useSelector(selectAllTasks);
// Get theme from Redux store // Get theme from Redux store
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark'); const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
@@ -328,19 +332,29 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}> <SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
<div className="task-group-tasks"> <div className="task-group-tasks">
{groupTasks.map((task, index) => ( {groupTasks.map((task, index) => (
<TaskRow <Draggable key={task.id} draggableId={task.id} index={index}>
key={task.id} {(provided, snapshot) => (
task={task} <div
projectId={projectId} ref={provided.innerRef}
groupId={group.id} {...provided.draggableProps}
currentGrouping={currentGrouping} {...provided.dragHandleProps}
isSelected={selectedTaskIds.includes(task.id)} className={`task-row-wrapper ${snapshot.isDragging ? 'dragging' : ''}`}
index={index} >
onSelect={onSelectTask} <TaskRow
onToggleSubtasks={onToggleSubtasks} task={task}
fixedColumns={visibleFixedColumns} projectId={projectId}
scrollableColumns={visibleScrollableColumns} groupId={group.id}
/> currentGrouping={currentGrouping}
isSelected={selectedTaskIds.includes(task.id)}
index={index}
onSelect={onSelectTask}
onToggleSubtasks={onToggleSubtasks}
fixedColumns={visibleFixedColumns}
scrollableColumns={visibleScrollableColumns}
/>
</div>
)}
</Draggable>
))} ))}
</div> </div>
</SortableContext> </SortableContext>

View File

@@ -17,35 +17,50 @@ import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
import { Card, Spin, Empty, Alert } from 'antd'; import { Card, Spin, Empty, Alert } from 'antd';
import { RootState } from '@/app/store'; import { RootState } from '@/app/store';
import { import {
taskManagementSelectors, selectAllTasks,
selectGroups,
selectGrouping,
selectLoading,
selectError,
selectSelectedPriorities,
selectSearch,
reorderTasks, reorderTasks,
moveTaskToGroup, moveTaskToGroup,
moveTaskBetweenGroups,
optimisticTaskMove, optimisticTaskMove,
reorderTasksInGroup, reorderTasksInGroup,
setLoading, setLoading,
fetchTasks, setError,
fetchTasksV3, setSelectedPriorities,
selectTaskGroupsV3, setSearch,
selectCurrentGroupingV3, resetTaskManagement,
fetchSubTasks,
toggleTaskExpansion, toggleTaskExpansion,
addSubtaskToParent,
fetchTasksV3,
} from '@/features/task-management/task-management.slice'; } from '@/features/task-management/task-management.slice';
import { import {
selectTaskGroups,
selectCurrentGrouping, selectCurrentGrouping,
setCurrentGrouping, selectCollapsedGroups,
selectIsGroupCollapsed,
toggleGroupCollapsed,
expandAllGroups,
collapseAllGroups,
} from '@/features/task-management/grouping.slice'; } from '@/features/task-management/grouping.slice';
import { import {
selectSelectedTaskIds, selectSelectedTaskIds,
selectLastSelectedTaskId,
selectIsTaskSelected,
selectTask,
deselectTask,
toggleTaskSelection, toggleTaskSelection,
selectRange,
clearSelection, clearSelection,
} from '@/features/task-management/selection.slice'; } from '@/features/task-management/selection.slice';
import { import {
selectTaskIds,
selectTasks, selectTasks,
deselectAll as deselectAllBulk, deselectAll as deselectAllBulk,
} from '@/features/projects/bulkActions/bulkActionSlice'; } from '@/features/projects/bulkActions/bulkActionSlice';
import { Task } from '@/types/task-management.types'; import { Task, TaskGroup } from '@/types/task-management.types';
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers'; import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
import { useSocket } from '@/socket/socketContext'; import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events'; import { SocketEvents } from '@/shared/socket-events';
@@ -157,16 +172,19 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const { socket, connected } = useSocket(); const { socket, connected } = useSocket();
// Redux selectors using V3 API (pre-processed data, minimal loops) // Redux selectors using V3 API (pre-processed data, minimal loops)
const tasks = useSelector(taskManagementSelectors.selectAll); const tasks = useSelector(selectAllTasks);
const groups = useSelector(selectGroups);
const grouping = useSelector(selectGrouping);
const loading = useSelector(selectLoading);
const error = useSelector(selectError);
const selectedPriorities = useSelector(selectSelectedPriorities);
const searchQuery = useSelector(selectSearch);
const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual); const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual);
const currentGrouping = useSelector(selectCurrentGroupingV3, shallowEqual); const currentGrouping = useSelector(selectCurrentGrouping);
// Use bulk action slice for selected tasks instead of selection slice const collapsedGroups = useSelector(selectCollapsedGroups);
const selectedTaskIds = useSelector( const selectedTaskIds = useSelector(selectSelectedTaskIds);
(state: RootState) => state.bulkActionReducer.selectedTaskIdsList const lastSelectedTaskId = useSelector(selectLastSelectedTaskId);
);
const selectedTasks = useSelector((state: RootState) => state.bulkActionReducer.selectedTasks); const selectedTasks = useSelector((state: RootState) => state.bulkActionReducer.selectedTasks);
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
const error = useSelector((state: RootState) => state.taskManagement.error);
// Bulk action selectors // Bulk action selectors
const statusList = useSelector((state: RootState) => state.taskStatusReducer.status); const statusList = useSelector((state: RootState) => state.taskStatusReducer.status);

View File

@@ -0,0 +1,54 @@
import React from 'react';
interface TaskListFiltersProps {
selectedPriorities: string[];
onPriorityChange: (priorities: string[]) => void;
searchQuery: string;
onSearchChange: (query: string) => void;
}
const TaskListFilters: React.FC<TaskListFiltersProps> = ({
selectedPriorities,
onPriorityChange,
searchQuery,
onSearchChange,
}) => {
const priorities = ['High', 'Medium', 'Low'];
return (
<div className="task-list-filters">
<div className="filter-group">
<label>Priority:</label>
<div className="priority-filters">
{priorities.map(priority => (
<label key={priority} className="priority-filter">
<input
type="checkbox"
checked={selectedPriorities.includes(priority)}
onChange={e => {
const newPriorities = e.target.checked
? [...selectedPriorities, priority]
: selectedPriorities.filter(p => p !== priority);
onPriorityChange(newPriorities);
}}
/>
<span>{priority}</span>
</label>
))}
</div>
</div>
<div className="filter-group">
<label>Search:</label>
<input
type="text"
value={searchQuery}
onChange={e => onSearchChange(e.target.value)}
placeholder="Search tasks..."
className="search-input"
/>
</div>
</div>
);
};
export default TaskListFilters;

View File

@@ -0,0 +1,94 @@
import React from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Task, TaskGroup } from '@/types/task-management.types';
import TaskRow from './task-row';
interface TaskListGroupProps {
group: TaskGroup;
tasks: Task[];
isCollapsed: boolean;
onCollapse: () => void;
onTaskSelect: (taskId: string, event: React.MouseEvent) => void;
selectedTaskIds: string[];
projectId: string;
currentGrouping: 'status' | 'priority' | 'phase';
}
const TaskListGroup: React.FC<TaskListGroupProps> = ({
group,
tasks,
isCollapsed,
onCollapse,
onTaskSelect,
selectedTaskIds,
projectId,
currentGrouping,
}) => {
const groupStyle = {
backgroundColor: group.color ? `${group.color}10` : undefined,
borderColor: group.color,
};
const headerStyle = {
backgroundColor: group.color ? `${group.color}20` : undefined,
};
return (
<div className="task-list-group" style={groupStyle}>
<div className="group-header" style={headerStyle} onClick={onCollapse}>
<div className="group-title">
<span className={`collapse-icon ${isCollapsed ? 'collapsed' : ''}`}>
{isCollapsed ? '►' : '▼'}
</span>
<h3>{group.title}</h3>
<span className="task-count">({tasks.length})</span>
</div>
</div>
{!isCollapsed && (
<div className="task-list">
{tasks.map((task, index) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: task.id,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
key={task.id}
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={`task-row-wrapper ${isDragging ? 'dragging' : ''}`}
>
<TaskRow
task={task}
projectId={projectId}
groupId={group.id}
currentGrouping={currentGrouping}
isSelected={selectedTaskIds.includes(task.id)}
onSelect={(taskId, selected) => onTaskSelect(taskId, {} as React.MouseEvent)}
index={index}
/>
</div>
);
})}
</div>
)}
</div>
);
};
export default TaskListGroup;

View File

@@ -0,0 +1,32 @@
import React from 'react';
interface TaskListHeaderProps {
onExpandAll: () => void;
onCollapseAll: () => void;
}
const TaskListHeader: React.FC<TaskListHeaderProps> = ({
onExpandAll,
onCollapseAll,
}) => {
return (
<div className="task-list-header">
<div className="header-actions">
<button
className="btn btn-secondary btn-sm"
onClick={onExpandAll}
>
Expand All
</button>
<button
className="btn btn-secondary btn-sm ml-2"
onClick={onCollapseAll}
>
Collapse All
</button>
</div>
</div>
);
};
export default TaskListHeader;

View File

@@ -9,6 +9,14 @@ import {
taskManagementSelectors, taskManagementSelectors,
toggleTaskExpansion, toggleTaskExpansion,
fetchSubTasks, fetchSubTasks,
selectAllTasks,
selectTaskIds,
selectGroups,
selectGrouping,
selectLoading,
selectError,
selectSelectedPriorities,
selectSearch,
} from '@/features/task-management/task-management.slice'; } from '@/features/task-management/task-management.slice';
import { toggleGroupCollapsed } from '@/features/task-management/grouping.slice'; import { toggleGroupCollapsed } from '@/features/task-management/grouping.slice';
import { Task } from '@/types/task-management.types'; import { Task } from '@/types/task-management.types';

View File

@@ -1,10 +1,22 @@
import { createSlice, PayloadAction, createSelector } from '@reduxjs/toolkit'; import { createSlice, PayloadAction, createSelector } from '@reduxjs/toolkit';
import { GroupingState, TaskGroup } from '@/types/task-management.types'; import { GroupingState, TaskGroup, TaskGrouping } from '@/types/task-management.types';
import { RootState } from '@/app/store'; import { RootState } from '@/app/store';
import { taskManagementSelectors } from './task-management.slice'; import { selectAllTasks } from './task-management.slice';
interface GroupingState {
currentGrouping: TaskGrouping | null;
customPhases: string[];
groupOrder: {
status: string[];
priority: string[];
phase: string[];
};
groupStates: Record<string, { collapsed: boolean }>;
collapsedGroups: Set<string>;
}
const initialState: GroupingState = { const initialState: GroupingState = {
currentGrouping: 'status', currentGrouping: null,
customPhases: ['Planning', 'Development', 'Testing', 'Deployment'], customPhases: ['Planning', 'Development', 'Testing', 'Deployment'],
groupOrder: { groupOrder: {
status: ['todo', 'doing', 'done'], status: ['todo', 'doing', 'done'],
@@ -12,13 +24,14 @@ const initialState: GroupingState = {
phase: ['Planning', 'Development', 'Testing', 'Deployment'], phase: ['Planning', 'Development', 'Testing', 'Deployment'],
}, },
groupStates: {}, groupStates: {},
collapsedGroups: new Set(),
}; };
const groupingSlice = createSlice({ const groupingSlice = createSlice({
name: 'grouping', name: 'grouping',
initialState, initialState,
reducers: { reducers: {
setCurrentGrouping: (state, action: PayloadAction<'status' | 'priority' | 'phase'>) => { setCurrentGrouping: (state, action: PayloadAction<TaskGrouping | null>) => {
state.currentGrouping = action.payload; state.currentGrouping = action.payload;
}, },
@@ -48,10 +61,13 @@ const groupingSlice = createSlice({
toggleGroupCollapsed: (state, action: PayloadAction<string>) => { toggleGroupCollapsed: (state, action: PayloadAction<string>) => {
const groupId = action.payload; const groupId = action.payload;
if (!state.groupStates[groupId]) { const collapsedGroups = new Set(state.collapsedGroups);
state.groupStates[groupId] = { collapsed: false }; if (collapsedGroups.has(groupId)) {
collapsedGroups.delete(groupId);
} else {
collapsedGroups.add(groupId);
} }
state.groupStates[groupId].collapsed = !state.groupStates[groupId].collapsed; state.collapsedGroups = collapsedGroups;
}, },
setGroupCollapsed: (state, action: PayloadAction<{ groupId: string; collapsed: boolean }>) => { setGroupCollapsed: (state, action: PayloadAction<{ groupId: string; collapsed: boolean }>) => {
@@ -62,16 +78,12 @@ const groupingSlice = createSlice({
state.groupStates[groupId].collapsed = collapsed; state.groupStates[groupId].collapsed = collapsed;
}, },
collapseAllGroups: state => { collapseAllGroups: (state, action: PayloadAction<string[]>) => {
Object.keys(state.groupStates).forEach(groupId => { state.collapsedGroups = new Set(action.payload);
state.groupStates[groupId].collapsed = true;
});
}, },
expandAllGroups: state => { expandAllGroups: state => {
Object.keys(state.groupStates).forEach(groupId => { state.collapsedGroups = new Set();
state.groupStates[groupId].collapsed = false;
});
}, },
resetGrouping: () => initialState, resetGrouping: () => initialState,
@@ -96,6 +108,9 @@ export const selectCurrentGrouping = (state: RootState) => state.grouping.curren
export const selectCustomPhases = (state: RootState) => state.grouping.customPhases; export const selectCustomPhases = (state: RootState) => state.grouping.customPhases;
export const selectGroupOrder = (state: RootState) => state.grouping.groupOrder; export const selectGroupOrder = (state: RootState) => state.grouping.groupOrder;
export const selectGroupStates = (state: RootState) => state.grouping.groupStates; export const selectGroupStates = (state: RootState) => state.grouping.groupStates;
export const selectCollapsedGroups = (state: RootState) => state.grouping.collapsedGroups;
export const selectIsGroupCollapsed = (state: RootState, groupId: string) =>
state.grouping.collapsedGroups.has(groupId);
// Complex selectors using createSelector for memoization // Complex selectors using createSelector for memoization
export const selectCurrentGroupOrder = createSelector( export const selectCurrentGroupOrder = createSelector(
@@ -104,12 +119,7 @@ export const selectCurrentGroupOrder = createSelector(
); );
export const selectTaskGroups = createSelector( export const selectTaskGroups = createSelector(
[ [selectAllTasks, selectCurrentGrouping, selectCurrentGroupOrder, selectGroupStates],
taskManagementSelectors.selectAll,
selectCurrentGrouping,
selectCurrentGroupOrder,
selectGroupStates,
],
(tasks, currentGrouping, groupOrder, groupStates) => { (tasks, currentGrouping, groupOrder, groupStates) => {
const groups: TaskGroup[] = []; const groups: TaskGroup[] = [];
@@ -154,7 +164,7 @@ export const selectTaskGroups = createSelector(
); );
export const selectTasksByCurrentGrouping = createSelector( export const selectTasksByCurrentGrouping = createSelector(
[taskManagementSelectors.selectAll, selectCurrentGrouping], [selectAllTasks, selectCurrentGrouping],
(tasks, currentGrouping) => { (tasks, currentGrouping) => {
const grouped: Record<string, typeof tasks> = {}; const grouped: Record<string, typeof tasks> = {};

View File

@@ -1,121 +1,71 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { SelectionState } from '@/types/task-management.types'; import { TaskSelection } from '@/types/task-management.types';
import { RootState } from '@/app/store'; import { RootState } from '@/app/store';
const initialState: SelectionState = { const initialState: TaskSelection = {
selectedTaskIds: [], selectedTaskIds: [],
lastSelectedId: null, lastSelectedTaskId: null,
}; };
const selectionSlice = createSlice({ const selectionSlice = createSlice({
name: 'selection', name: 'taskManagementSelection',
initialState, initialState,
reducers: { reducers: {
toggleTaskSelection: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
const index = state.selectedTaskIds.indexOf(taskId);
if (index === -1) {
state.selectedTaskIds.push(taskId);
} else {
state.selectedTaskIds.splice(index, 1);
}
state.lastSelectedId = taskId;
},
selectTask: (state, action: PayloadAction<string>) => { selectTask: (state, action: PayloadAction<string>) => {
const taskId = action.payload; const taskId = action.payload;
if (!state.selectedTaskIds.includes(taskId)) { if (!state.selectedTaskIds.includes(taskId)) {
state.selectedTaskIds.push(taskId); state.selectedTaskIds.push(taskId);
} }
state.lastSelectedId = taskId; state.lastSelectedTaskId = taskId;
}, },
deselectTask: (state, action: PayloadAction<string>) => { deselectTask: (state, action: PayloadAction<string>) => {
const taskId = action.payload; const taskId = action.payload;
state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== taskId); state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== taskId);
if (state.lastSelectedId === taskId) { if (state.lastSelectedTaskId === taskId) {
state.lastSelectedId = state.selectedTaskIds[state.selectedTaskIds.length - 1] || null; state.lastSelectedTaskId = state.selectedTaskIds[state.selectedTaskIds.length - 1] || null;
} }
}, },
toggleTaskSelection: (state, action: PayloadAction<string>) => {
selectMultipleTasks: (state, action: PayloadAction<string[]>) => { const taskId = action.payload;
const index = state.selectedTaskIds.indexOf(taskId);
if (index === -1) {
state.selectedTaskIds.push(taskId);
state.lastSelectedTaskId = taskId;
} else {
state.selectedTaskIds.splice(index, 1);
state.lastSelectedTaskId = state.selectedTaskIds[state.selectedTaskIds.length - 1] || null;
}
},
selectRange: (state, action: PayloadAction<string[]>) => {
const taskIds = action.payload; const taskIds = action.payload;
// Add new task IDs that aren't already selected const uniqueIds = Array.from(new Set([...state.selectedTaskIds, ...taskIds]));
taskIds.forEach(id => { state.selectedTaskIds = uniqueIds;
if (!state.selectedTaskIds.includes(id)) { state.lastSelectedTaskId = taskIds[taskIds.length - 1];
state.selectedTaskIds.push(id);
}
});
state.lastSelectedId = taskIds[taskIds.length - 1] || state.lastSelectedId;
}, },
selectRangeTasks: (
state,
action: PayloadAction<{ startId: string; endId: string; allTaskIds: string[] }>
) => {
const { startId, endId, allTaskIds } = action.payload;
const startIndex = allTaskIds.indexOf(startId);
const endIndex = allTaskIds.indexOf(endId);
if (startIndex !== -1 && endIndex !== -1) {
const [start, end] =
startIndex <= endIndex ? [startIndex, endIndex] : [endIndex, startIndex];
const rangeIds = allTaskIds.slice(start, end + 1);
// Add range IDs that aren't already selected
rangeIds.forEach(id => {
if (!state.selectedTaskIds.includes(id)) {
state.selectedTaskIds.push(id);
}
});
state.lastSelectedId = endId;
}
},
selectAllTasks: (state, action: PayloadAction<string[]>) => {
state.selectedTaskIds = action.payload;
state.lastSelectedId = action.payload[action.payload.length - 1] || null;
},
clearSelection: state => { clearSelection: state => {
state.selectedTaskIds = []; state.selectedTaskIds = [];
state.lastSelectedId = null; state.lastSelectedTaskId = null;
}, },
resetSelection: state => {
setSelection: (state, action: PayloadAction<string[]>) => { state.selectedTaskIds = [];
state.selectedTaskIds = action.payload; state.lastSelectedTaskId = null;
state.lastSelectedId = action.payload[action.payload.length - 1] || null;
}, },
resetSelection: () => initialState,
}, },
}); });
export const { export const {
toggleTaskSelection,
selectTask, selectTask,
deselectTask, deselectTask,
selectMultipleTasks, toggleTaskSelection,
selectRangeTasks, selectRange,
selectAllTasks,
clearSelection, clearSelection,
setSelection,
resetSelection, resetSelection,
} = selectionSlice.actions; } = selectionSlice.actions;
// Selectors // Selectors
export const selectSelectedTaskIds = (state: RootState) => export const selectSelectedTaskIds = (state: RootState) => state.taskManagementSelection.selectedTaskIds;
state.taskManagementSelection.selectedTaskIds; export const selectLastSelectedTaskId = (state: RootState) => state.taskManagementSelection.lastSelectedTaskId;
export const selectLastSelectedId = (state: RootState) => export const selectIsTaskSelected = (state: RootState, taskId: string) =>
state.taskManagementSelection.lastSelectedId;
export const selectHasSelection = (state: RootState) =>
state.taskManagementSelection.selectedTaskIds.length > 0;
export const selectSelectionCount = (state: RootState) =>
state.taskManagementSelection.selectedTaskIds.length;
export const selectIsTaskSelected = (taskId: string) => (state: RootState) =>
state.taskManagementSelection.selectedTaskIds.includes(taskId); state.taskManagementSelection.selectedTaskIds.includes(taskId);
export default selectionSlice.reducer; export default selectionSlice.reducer;

View File

@@ -3,8 +3,10 @@ import {
createEntityAdapter, createEntityAdapter,
PayloadAction, PayloadAction,
createAsyncThunk, createAsyncThunk,
EntityState,
EntityId,
} from '@reduxjs/toolkit'; } from '@reduxjs/toolkit';
import { Task, TaskManagementState } from '@/types/task-management.types'; import { Task, TaskManagementState, TaskGroup, TaskGrouping } from '@/types/task-management.types';
import { RootState } from '@/app/store'; import { RootState } from '@/app/store';
import { import {
tasksApiService, tasksApiService,
@@ -13,6 +15,24 @@ import {
} from '@/api/tasks/tasks.api.service'; } from '@/api/tasks/tasks.api.service';
import logger from '@/utils/errorLogger'; import logger from '@/utils/errorLogger';
// Helper function to safely convert time values
const convertTimeValue = (value: any): number => {
if (typeof value === 'number') return value;
if (typeof value === 'string') {
const parsed = parseFloat(value);
return isNaN(parsed) ? 0 : parsed;
}
if (typeof value === 'object' && value !== null) {
// Handle time objects like {hours: 2, minutes: 30}
if ('hours' in value || 'minutes' in value) {
const hours = Number(value.hours || 0);
const minutes = Number(value.minutes || 0);
return hours + minutes / 60;
}
}
return 0;
};
export enum IGroupBy { export enum IGroupBy {
STATUS = 'status', STATUS = 'status',
PRIORITY = 'priority', PRIORITY = 'priority',
@@ -21,17 +41,16 @@ export enum IGroupBy {
} }
// Entity adapter for normalized state // Entity adapter for normalized state
const tasksAdapter = createEntityAdapter<Task>({ const tasksAdapter = createEntityAdapter<Task>();
sortComparer: (a, b) => a.order - b.order,
});
// Get the initial state from the adapter
const initialState: TaskManagementState = { const initialState: TaskManagementState = {
entities: {},
ids: [], ids: [],
entities: {},
loading: false, loading: false,
error: null, error: null,
groups: [], groups: [],
grouping: null, grouping: undefined,
selectedPriorities: [], selectedPriorities: [],
search: '', search: '',
}; };
@@ -47,7 +66,7 @@ export const fetchTasks = createAsyncThunk(
const config: ITaskListConfigV2 = { const config: ITaskListConfigV2 = {
id: projectId, id: projectId,
archived: false, archived: false,
group: currentGrouping, group: currentGrouping || '',
field: '', field: '',
order: '', order: '',
search: '', search: '',
@@ -167,24 +186,18 @@ export const fetchTasksV3 = createAsyncThunk(
// Get selected labels from taskReducer // Get selected labels from taskReducer
const selectedLabels = state.taskReducer.labels const selectedLabels = state.taskReducer.labels
? state.taskReducer.labels .filter((l: any) => l.selected && l.id)
.filter(l => l.selected) .map((l: any) => l.id)
.map(l => l.id) .join(' ');
.join(' ')
: '';
// Get selected assignees from taskReducer // Get selected assignees from taskReducer
const selectedAssignees = state.taskReducer.taskAssignees const selectedAssignees = state.taskReducer.taskAssignees
? state.taskReducer.taskAssignees .filter((m: any) => m.selected && m.id)
.filter(m => m.selected) .map((m: any) => m.id)
.map(m => m.id) .join(' ');
.join(' ')
: '';
// Get selected priorities from taskReducer (consistent with other slices) // Get selected priorities from taskReducer
const selectedPriorities = state.taskReducer.priorities const selectedPriorities = state.taskReducer.priorities.join(' ');
? state.taskReducer.priorities.join(' ')
: '';
// Get search value from taskReducer // Get search value from taskReducer
const searchValue = state.taskReducer.search || ''; const searchValue = state.taskReducer.search || '';
@@ -192,7 +205,7 @@ export const fetchTasksV3 = createAsyncThunk(
const config: ITaskListConfigV2 = { const config: ITaskListConfigV2 = {
id: projectId, id: projectId,
archived: false, archived: false,
group: currentGrouping, group: currentGrouping || '',
field: '', field: '',
order: '', order: '',
search: searchValue, search: searchValue,
@@ -206,10 +219,82 @@ export const fetchTasksV3 = createAsyncThunk(
const response = await tasksApiService.getTaskListV3(config); const response = await tasksApiService.getTaskListV3(config);
// Minimal processing - tasks are already processed by backend // Log raw response for debugging
console.log('Raw API response:', response.body);
// Ensure tasks are properly normalized
const tasks = response.body.allTasks.map((task: any) => {
const now = new Date().toISOString();
return {
id: task.id,
title: task.name || '',
description: task.description || '',
status: task.status || 'todo',
priority: task.priority || 'medium',
phase: task.phase || 'Development',
progress: typeof task.complete_ratio === 'number' ? task.complete_ratio : 0,
assignees: task.assignees?.map((a: { team_member_id: string }) => a.team_member_id) || [],
assignee_names: task.assignee_names || task.names || [],
labels: task.labels?.map((l: { id: string; label_id: string; name: string; color_code: string; end: boolean; names: string[] }) => ({
id: l.id || l.label_id,
name: l.name,
color: l.color_code || '#1890ff',
end: l.end,
names: l.names,
})) || [],
due_date: task.end_date || '',
timeTracking: {
estimated: convertTimeValue(task.total_time),
logged: convertTimeValue(task.time_spent),
},
created_at: task.created_at || now,
updated_at: task.updated_at || now,
order: typeof task.sort_order === 'number' ? task.sort_order : 0,
sub_tasks: task.sub_tasks || [],
sub_tasks_count: task.sub_tasks_count || 0,
show_sub_tasks: task.show_sub_tasks || false,
parent_task_id: task.parent_task_id || '',
weight: task.weight || 0,
color: task.color || '',
statusColor: task.status_color || '',
priorityColor: task.priority_color || '',
comments_count: task.comments_count || 0,
attachments_count: task.attachments_count || 0,
has_dependencies: !!task.has_dependencies,
schedule_id: task.schedule_id || null,
} as Task;
});
// Map groups to match TaskGroup interface
const mappedGroups = response.body.groups.map((group: any) => ({
id: group.id,
title: group.title,
taskIds: group.taskIds || [],
type: group.groupType as 'status' | 'priority' | 'phase' | 'members',
color: group.color,
}));
// Log normalized data for debugging
console.log('Normalized data:', {
tasks,
groups: mappedGroups,
grouping: response.body.grouping,
totalTasks: response.body.totalTasks,
});
// Verify task IDs match group taskIds
const taskIds = new Set(tasks.map(t => t.id));
const groupTaskIds = new Set(mappedGroups.flatMap(g => g.taskIds));
console.log('Task ID verification:', {
taskIds: Array.from(taskIds),
groupTaskIds: Array.from(groupTaskIds),
allTaskIdsInGroups: Array.from(groupTaskIds).every(id => taskIds.has(id)),
allGroupTaskIdsInTasks: Array.from(taskIds).every(id => groupTaskIds.has(id)),
});
return { return {
tasks: response.body.allTasks, tasks: tasks,
groups: response.body.groups, groups: mappedGroups,
grouping: response.body.grouping, grouping: response.body.grouping,
totalTasks: response.body.totalTasks, totalTasks: response.body.totalTasks,
}; };
@@ -351,288 +436,148 @@ export const updateTaskWithSubtasks = createAsyncThunk(
} }
); );
// Create the slice
const taskManagementSlice = createSlice({ const taskManagementSlice = createSlice({
name: 'taskManagement', name: 'taskManagement',
initialState: tasksAdapter.getInitialState(initialState), initialState,
reducers: { reducers: {
// Basic CRUD operations
setTasks: (state, action: PayloadAction<Task[]>) => { setTasks: (state, action: PayloadAction<Task[]>) => {
tasksAdapter.setAll(state, action.payload); const tasks = action.payload;
state.loading = false; state.ids = tasks.map(task => task.id);
state.error = null; state.entities = tasks.reduce((acc, task) => {
acc[task.id] = task;
return acc;
}, {} as Record<string, Task>);
}, },
addTask: (state, action: PayloadAction<Task>) => { addTask: (state, action: PayloadAction<Task>) => {
tasksAdapter.addOne(state, action.payload); const task = action.payload;
state.ids.push(task.id);
state.entities[task.id] = task;
}, },
addTaskToGroup: (state, action: PayloadAction<{ task: Task; groupId: string }>) => {
addTaskToGroup: (state, action: PayloadAction<{ task: Task; groupId?: string }>) => {
const { task, groupId } = action.payload; const { task, groupId } = action.payload;
state.ids.push(task.id);
// Add to entity adapter state.entities[task.id] = task;
tasksAdapter.addOne(state, task); const group = state.groups.find(g => g.id === groupId);
if (group) {
// Add to groups array for V3 API compatibility group.taskIds.push(task.id);
if (state.groups && state.groups.length > 0) {
// Find the target group using the provided UUID
const targetGroup = state.groups.find(group => {
// If a specific groupId (UUID) is provided, use it directly
if (groupId && group.id === groupId) {
return true;
}
return false;
});
if (targetGroup) {
// Add task ID to the end of the group's taskIds array (newest last)
targetGroup.taskIds.push(task.id);
// Also add to the tasks array if it exists (for backward compatibility)
if ((targetGroup as any).tasks) {
(targetGroup as any).tasks.push(task);
}
}
} }
}, },
updateTask: (state, action: PayloadAction<Task>) => {
updateTask: (state, action: PayloadAction<{ id: string; changes: Partial<Task> }>) => { const task = action.payload;
tasksAdapter.updateOne(state, { state.entities[task.id] = task;
id: action.payload.id, },
changes: { deleteTask: (state, action: PayloadAction<string>) => {
...action.payload.changes, const taskId = action.payload;
updatedAt: new Date().toISOString(), delete state.entities[taskId];
}, state.ids = state.ids.filter(id => id !== taskId);
state.groups = state.groups.map(group => ({
...group,
taskIds: group.taskIds.filter(id => id !== taskId),
}));
},
bulkUpdateTasks: (state, action: PayloadAction<Task[]>) => {
action.payload.forEach(task => {
state.entities[task.id] = task;
}); });
}, },
deleteTask: (state, action: PayloadAction<string>) => {
tasksAdapter.removeOne(state, action.payload);
},
// Bulk operations
bulkUpdateTasks: (state, action: PayloadAction<{ ids: string[]; changes: Partial<Task> }>) => {
const { ids, changes } = action.payload;
const updates = ids.map(id => ({
id,
changes: {
...changes,
updatedAt: new Date().toISOString(),
},
}));
tasksAdapter.updateMany(state, updates);
},
bulkDeleteTasks: (state, action: PayloadAction<string[]>) => { bulkDeleteTasks: (state, action: PayloadAction<string[]>) => {
tasksAdapter.removeMany(state, action.payload); const taskIds = action.payload;
}, taskIds.forEach(taskId => {
delete state.entities[taskId];
// Optimized drag and drop operations });
reorderTasks: (state, action: PayloadAction<{ taskIds: string[]; newOrder: number[] }>) => { state.ids = state.ids.filter(id => !taskIds.includes(id));
const { taskIds, newOrder } = action.payload; state.groups = state.groups.map(group => ({
...group,
// Batch update for better performance taskIds: group.taskIds.filter(id => !taskIds.includes(id)),
const updates = taskIds.map((id, index) => ({
id,
changes: {
order: newOrder[index],
updatedAt: new Date().toISOString(),
},
})); }));
tasksAdapter.updateMany(state, updates);
}, },
reorderTasks: (state, action: PayloadAction<{ taskIds: string[]; groupId: string }>) => {
moveTaskToGroup: ( const { taskIds, groupId } = action.payload;
state, const group = state.groups.find(g => g.id === groupId);
action: PayloadAction<{ if (group) {
taskId: string; group.taskIds = taskIds;
groupType: 'status' | 'priority' | 'phase';
groupValue: string;
}>
) => {
const { taskId, groupType, groupValue } = action.payload;
const changes: Partial<Task> = {
updatedAt: new Date().toISOString(),
};
// Update the appropriate field based on group type
if (groupType === 'status') {
changes.status = groupValue as Task['status'];
} else if (groupType === 'priority') {
changes.priority = groupValue as Task['priority'];
} else if (groupType === 'phase') {
changes.phase = groupValue;
} }
tasksAdapter.updateOne(state, { id: taskId, changes });
}, },
moveTaskToGroup: (state, action: PayloadAction<{ taskId: string; groupId: string }>) => {
// New action to move task between groups with proper group management const { taskId, groupId } = action.payload;
state.groups = state.groups.map(group => ({
...group,
taskIds:
group.id === groupId
? [...group.taskIds, taskId]
: group.taskIds.filter(id => id !== taskId),
}));
},
moveTaskBetweenGroups: ( moveTaskBetweenGroups: (
state, state,
action: PayloadAction<{ action: PayloadAction<{
taskId: string; taskId: string;
fromGroupId: string; sourceGroupId: string;
toGroupId: string; targetGroupId: string;
taskUpdate: Partial<Task>;
}> }>
) => { ) => {
const { taskId, fromGroupId, toGroupId, taskUpdate } = action.payload; const { taskId, sourceGroupId, targetGroupId } = action.payload;
state.groups = state.groups.map(group => ({
// Update the task entity with new values ...group,
tasksAdapter.updateOne(state, { taskIds:
id: taskId, group.id === targetGroupId
changes: { ? [...group.taskIds, taskId]
...taskUpdate, : group.id === sourceGroupId
updatedAt: new Date().toISOString(), ? group.taskIds.filter(id => id !== taskId)
}, : group.taskIds,
}); }));
// Update groups if they exist
if (state.groups && state.groups.length > 0) {
// Remove task from old group
const fromGroup = state.groups.find(group => group.id === fromGroupId);
if (fromGroup) {
fromGroup.taskIds = fromGroup.taskIds.filter(id => id !== taskId);
}
// Add task to new group
const toGroup = state.groups.find(group => group.id === toGroupId);
if (toGroup) {
// Add to the end of the group (newest last)
toGroup.taskIds.push(taskId);
}
}
}, },
// Optimistic update for drag operations - reduces perceived lag
optimisticTaskMove: ( optimisticTaskMove: (
state,
action: PayloadAction<{ taskId: string; newGroupId: string; newIndex: number }>
) => {
const { taskId, newGroupId, newIndex } = action.payload;
const task = state.entities[taskId];
if (task) {
// Parse group ID to determine new values
const [groupType, ...groupValueParts] = newGroupId.split('-');
const groupValue = groupValueParts.join('-');
const changes: Partial<Task> = {
order: newIndex,
updatedAt: new Date().toISOString(),
};
// Update group-specific field
if (groupType === 'status') {
changes.status = groupValue as Task['status'];
} else if (groupType === 'priority') {
changes.priority = groupValue as Task['priority'];
} else if (groupType === 'phase') {
changes.phase = groupValue;
}
// Update the task entity
tasksAdapter.updateOne(state, { id: taskId, changes });
// Update groups if they exist
if (state.groups && state.groups.length > 0) {
// Find the target group
const targetGroup = state.groups.find(group => group.id === newGroupId);
if (targetGroup) {
// Remove task from all groups first
state.groups.forEach(group => {
group.taskIds = group.taskIds.filter(id => id !== taskId);
});
// Add task to target group at the specified index
if (newIndex >= targetGroup.taskIds.length) {
targetGroup.taskIds.push(taskId);
} else {
targetGroup.taskIds.splice(newIndex, 0, taskId);
}
}
}
}
},
// Proper reorder action that handles both task entities and group arrays
reorderTasksInGroup: (
state, state,
action: PayloadAction<{ action: PayloadAction<{
taskId: string; taskId: string;
fromGroupId: string; sourceGroupId: string;
toGroupId: string; targetGroupId: string;
fromIndex: number;
toIndex: number;
groupType: 'status' | 'priority' | 'phase';
groupValue: string;
}> }>
) => { ) => {
const { taskId, fromGroupId, toGroupId, fromIndex, toIndex, groupType, groupValue } = const { taskId, sourceGroupId, targetGroupId } = action.payload;
action.payload; state.groups = state.groups.map(group => ({
...group,
// Update the task entity taskIds:
const changes: Partial<Task> = { group.id === targetGroupId
order: toIndex, ? [...group.taskIds, taskId]
updatedAt: new Date().toISOString(), : group.id === sourceGroupId
}; ? group.taskIds.filter(id => id !== taskId)
: group.taskIds,
// Update group-specific field }));
if (groupType === 'status') { },
changes.status = groupValue as Task['status']; reorderTasksInGroup: (
} else if (groupType === 'priority') { state,
changes.priority = groupValue as Task['priority']; action: PayloadAction<{ taskIds: string[]; groupId: string }>
} else if (groupType === 'phase') { ) => {
changes.phase = groupValue; const { taskIds, groupId } = action.payload;
} const group = state.groups.find(g => g.id === groupId);
if (group) {
tasksAdapter.updateOne(state, { id: taskId, changes }); group.taskIds = taskIds;
// Update groups if they exist
if (state.groups && state.groups.length > 0) {
// Remove task from source group
const fromGroup = state.groups.find(group => group.id === fromGroupId);
if (fromGroup) {
fromGroup.taskIds = fromGroup.taskIds.filter(id => id !== taskId);
}
// Add task to target group
const toGroup = state.groups.find(group => group.id === toGroupId);
if (toGroup) {
if (toIndex >= toGroup.taskIds.length) {
toGroup.taskIds.push(taskId);
} else {
toGroup.taskIds.splice(toIndex, 0, taskId);
}
}
} }
}, },
// Loading states
setLoading: (state, action: PayloadAction<boolean>) => { setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload; state.loading = action.payload;
}, },
setError: (state, action: PayloadAction<string | null>) => { setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload; state.error = action.payload;
state.loading = false;
}, },
// Filter actions
setSelectedPriorities: (state, action: PayloadAction<string[]>) => { setSelectedPriorities: (state, action: PayloadAction<string[]>) => {
state.selectedPriorities = action.payload; state.selectedPriorities = action.payload;
}, },
// Search action
setSearch: (state, action: PayloadAction<string>) => { setSearch: (state, action: PayloadAction<string>) => {
state.search = action.payload; state.search = action.payload;
}, },
// Reset action
resetTaskManagement: state => { resetTaskManagement: state => {
return tasksAdapter.getInitialState(initialState); state.loading = false;
state.error = null;
state.groups = [];
state.grouping = undefined;
state.selectedPriorities = [];
state.search = '';
state.ids = [];
state.entities = {};
}, },
toggleTaskExpansion: (state, action: PayloadAction<string>) => { toggleTaskExpansion: (state, action: PayloadAction<string>) => {
const task = state.entities[action.payload]; const task = state.entities[action.payload];
@@ -640,37 +585,25 @@ const taskManagementSlice = createSlice({
task.show_sub_tasks = !task.show_sub_tasks; task.show_sub_tasks = !task.show_sub_tasks;
} }
}, },
addSubtaskToParent: (state, action: PayloadAction<{ subtask: Task; parentTaskId: string }>) => { addSubtaskToParent: (
const { subtask, parentTaskId } = action.payload; state,
const parentTask = state.entities[parentTaskId]; action: PayloadAction<{ parentId: string; subtask: Task }>
if (parentTask) { ) => {
if (!parentTask.sub_tasks) { const { parentId, subtask } = action.payload;
parentTask.sub_tasks = []; const parent = state.entities[parentId];
if (parent) {
state.ids.push(subtask.id);
state.entities[subtask.id] = subtask;
if (!parent.sub_tasks) {
parent.sub_tasks = [];
} }
parentTask.sub_tasks.push(subtask); parent.sub_tasks.push(subtask);
parentTask.sub_tasks_count = (parentTask.sub_tasks_count || 0) + 1; parent.sub_tasks_count = (parent.sub_tasks_count || 0) + 1;
// Ensure the parent task is expanded to show the new subtask
parentTask.show_sub_tasks = true;
// Add the subtask to the main entities as well
tasksAdapter.addOne(state, subtask);
} }
}, },
}, },
extraReducers: builder => { extraReducers: builder => {
builder builder
.addCase(fetchTasks.pending, state => {
state.loading = true;
state.error = null;
})
.addCase(fetchTasks.fulfilled, (state, action) => {
state.loading = false;
state.error = null;
tasksAdapter.setAll(state, action.payload);
})
.addCase(fetchTasks.rejected, (state, action) => {
state.loading = false;
state.error = (action.payload as string) || 'Failed to fetch tasks';
})
.addCase(fetchTasksV3.pending, state => { .addCase(fetchTasksV3.pending, state => {
state.loading = true; state.loading = true;
state.error = null; state.error = null;
@@ -678,47 +611,68 @@ const taskManagementSlice = createSlice({
.addCase(fetchTasksV3.fulfilled, (state, action) => { .addCase(fetchTasksV3.fulfilled, (state, action) => {
state.loading = false; state.loading = false;
state.error = null; state.error = null;
// Tasks are already processed by backend, minimal setup needed
tasksAdapter.setAll(state, action.payload.tasks); // Ensure we have tasks before updating state
state.groups = action.payload.groups; if (action.payload.tasks && action.payload.tasks.length > 0) {
state.grouping = action.payload.grouping; // Update tasks
const tasks = action.payload.tasks;
state.ids = tasks.map(task => task.id);
state.entities = tasks.reduce((acc, task) => {
acc[task.id] = task;
return acc;
}, {} as Record<string, Task>);
// Update groups
state.groups = action.payload.groups;
state.grouping = action.payload.grouping;
// Verify task IDs match group taskIds
const taskIds = new Set(Object.keys(state.entities));
const groupTaskIds = new Set(state.groups.flatMap(g => g.taskIds));
// Ensure all tasks have IDs and all group taskIds exist
const validTaskIds = new Set(Object.keys(state.entities));
state.groups = state.groups.map((group: TaskGroup) => ({
...group,
taskIds: group.taskIds.filter((id: string) => validTaskIds.has(id)),
}));
} else {
// Set empty state but don't show error
state.ids = [];
state.entities = {};
state.groups = [];
}
}) })
.addCase(fetchTasksV3.rejected, (state, action) => { .addCase(fetchTasksV3.rejected, (state, action) => {
state.loading = false; state.loading = false;
state.error = (action.payload as string) || 'Failed to fetch tasks'; // Provide a more descriptive error message
state.error = action.error.message || action.payload || 'An error occurred while fetching tasks. Please try again.';
// Clear task data on error to prevent stale state
state.ids = [];
state.entities = {};
state.groups = [];
})
.addCase(fetchSubTasks.pending, (state, action) => {
// Don't set global loading state for subtasks
state.error = null;
}) })
.addCase(fetchSubTasks.fulfilled, (state, action) => { .addCase(fetchSubTasks.fulfilled, (state, action) => {
const { parentTaskId, subtasks } = action.payload; const { parentTaskId, subtasks } = action.payload;
const parentTask = state.entities[parentTaskId]; const parentTask = state.entities[parentTaskId];
if (parentTask) { if (parentTask) {
parentTask.sub_tasks = subtasks; parentTask.sub_tasks = subtasks;
parentTask.sub_tasks_count = subtasks.length;
parentTask.show_sub_tasks = true; parentTask.show_sub_tasks = true;
// Add subtasks to the main entities as well
tasksAdapter.addMany(state, subtasks);
} }
}) })
.addCase(refreshTaskProgress.pending, state => { .addCase(fetchSubTasks.rejected, (state, action) => {
// Don't set loading to true for refresh to avoid UI blocking // Set error but don't clear task data
state.error = null; state.error = action.error.message || action.payload || 'Failed to fetch subtasks. Please try again.';
})
.addCase(refreshTaskProgress.fulfilled, state => {
state.error = null;
// Progress refresh completed successfully
})
.addCase(refreshTaskProgress.rejected, (state, action) => {
state.error = (action.payload as string) || 'Failed to refresh task progress';
})
.addCase(updateTaskWithSubtasks.fulfilled, (state, action) => {
const { taskId, subtasks } = action.payload;
const task = state.entities[taskId];
if (task) {
task.sub_tasks = subtasks;
task.show_sub_tasks = true;
}
}); });
}, },
}); });
// Export the slice reducer and actions
export const { export const {
setTasks, setTasks,
addTask, addTask,
@@ -741,25 +695,30 @@ export const {
addSubtaskToParent, addSubtaskToParent,
} = taskManagementSlice.actions; } = taskManagementSlice.actions;
export default taskManagementSlice.reducer; // Export the selectors
export const selectAllTasks = (state: RootState) => state.taskManagement.entities;
export const selectAllTasksArray = (state: RootState) => Object.values(state.taskManagement.entities);
export const selectTaskById = (state: RootState, taskId: string) => state.taskManagement.entities[taskId];
export const selectTaskIds = (state: RootState) => state.taskManagement.ids;
export const selectGroups = (state: RootState) => state.taskManagement.groups;
export const selectGrouping = (state: RootState) => state.taskManagement.grouping;
export const selectLoading = (state: RootState) => state.taskManagement.loading;
export const selectError = (state: RootState) => state.taskManagement.error;
export const selectSelectedPriorities = (state: RootState) => state.taskManagement.selectedPriorities;
export const selectSearch = (state: RootState) => state.taskManagement.search;
// Selectors // Memoized selectors
export const taskManagementSelectors = tasksAdapter.getSelectors<RootState>(
state => state.taskManagement
);
// Enhanced selectors for better performance
export const selectTasksByStatus = (state: RootState, status: string) => export const selectTasksByStatus = (state: RootState, status: string) =>
taskManagementSelectors.selectAll(state).filter(task => task.status === status); Object.values(state.taskManagement.entities).filter(task => task.status === status);
export const selectTasksByPriority = (state: RootState, priority: string) => export const selectTasksByPriority = (state: RootState, priority: string) =>
taskManagementSelectors.selectAll(state).filter(task => task.priority === priority); Object.values(state.taskManagement.entities).filter(task => task.priority === priority);
export const selectTasksByPhase = (state: RootState, phase: string) => export const selectTasksByPhase = (state: RootState, phase: string) =>
taskManagementSelectors.selectAll(state).filter(task => task.phase === phase); Object.values(state.taskManagement.entities).filter(task => task.phase === phase);
export const selectTasksLoading = (state: RootState) => state.taskManagement.loading; // Export the reducer as default
export const selectTasksError = (state: RootState) => state.taskManagement.error; export default taskManagementSlice.reducer;
// V3 API selectors - no processing needed, data is pre-processed by backend // V3 API selectors - no processing needed, data is pre-processed by backend
export const selectTaskGroupsV3 = (state: RootState) => state.taskManagement.groups; export const selectTaskGroupsV3 = (state: RootState) => state.taskManagement.groups;

View File

@@ -4,6 +4,7 @@ import { InlineSuspenseFallback } from '@/components/suspense-fallback/suspense-
// Import core components synchronously to avoid suspense in main tabs // Import core components synchronously to avoid suspense in main tabs
import ProjectViewEnhancedTasks from '@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks'; import ProjectViewEnhancedTasks from '@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks';
import ProjectViewEnhancedBoard from '@/pages/projects/projectView/enhancedBoard/project-view-enhanced-board'; import ProjectViewEnhancedBoard from '@/pages/projects/projectView/enhancedBoard/project-view-enhanced-board';
import TaskListV2 from '@/components/task-list-v2/TaskListV2';
// Lazy load less critical components // Lazy load less critical components
const ProjectViewInsights = React.lazy( const ProjectViewInsights = React.lazy(
@@ -35,7 +36,7 @@ export const tabItems: TabItems[] = [
key: 'tasks-list', key: 'tasks-list',
label: 'Task List', label: 'Task List',
isPinned: true, isPinned: true,
element: React.createElement(ProjectViewEnhancedTasks), element: React.createElement(TaskListV2),
}, },
{ {
index: 1, index: 1,

View File

@@ -119,7 +119,7 @@ const ProjectView = React.memo(() => {
return () => { return () => {
resetAllProjectData(); resetAllProjectData();
}; };
}, []); // Empty dependency array - only runs on mount/unmount }, [resetAllProjectData]);
// Effect for handling route changes (when navigating away from project view) // Effect for handling route changes (when navigating away from project view)
useEffect(() => { useEffect(() => {
@@ -358,10 +358,10 @@ const ProjectView = React.memo(() => {
minHeight: '36px', minHeight: '36px',
}} }}
tabBarGutter={0} tabBarGutter={0}
destroyInactiveTabPane={true} // Destroy inactive tabs to save memory destroyInactiveTabPane={true}
animated={{ animated={{
inkBar: true, inkBar: true,
tabPane: false, // Disable content animation for better performance tabPane: false,
}} }}
size="small" size="small"
type="card" type="card"

View File

@@ -0,0 +1,28 @@
export interface TaskListField {
id: string;
name: string;
type: 'text' | 'number' | 'date' | 'select' | 'multiselect' | 'checkbox';
isVisible: boolean;
order: number;
width?: number;
options?: {
id: string;
label: string;
value: string;
color?: string;
}[];
}
export interface TaskListFieldGroup {
id: string;
name: string;
fields: TaskListField[];
order: number;
}
export interface TaskListFieldState {
fields: TaskListField[];
groups: TaskListFieldGroup[];
loading: boolean;
error: string | null;
}

View File

@@ -1,43 +1,44 @@
import { InlineMember } from './teamMembers/inlineMember.types'; import { InlineMember } from './teamMembers/inlineMember.types';
import { EntityState } from '@reduxjs/toolkit';
export interface Task { export interface Task {
id: string; id: string;
task_key: string;
title: string; title: string;
description?: string; description?: string;
status: 'todo' | 'doing' | 'done'; status: string;
priority: 'critical' | 'high' | 'medium' | 'low'; priority: string;
phase: string; // Custom phases like 'planning', 'development', 'testing', 'deployment' phase?: string;
progress: number; // 0-100 assignee?: string;
assignees: string[]; due_date?: string;
assignee_names?: InlineMember[]; created_at: string;
labels: Label[]; updated_at: string;
startDate?: string; // Start date for the task sub_tasks?: Task[];
dueDate?: string; // Due date for the task
completedAt?: string; // When the task was completed
reporter?: string; // Who reported/created the task
timeTracking: {
estimated?: number;
logged: number;
};
customFields: Record<string, any>;
createdAt: string;
updatedAt: string;
order: number;
// Subtask-related properties
sub_tasks_count?: number; sub_tasks_count?: number;
show_sub_tasks?: boolean; show_sub_tasks?: boolean;
sub_tasks?: Task[]; parent_task_id?: string;
progress?: number;
weight?: number;
color?: string;
statusColor?: string;
priorityColor?: string;
labels?: { id: string; name: string; color: string }[];
comments_count?: number;
attachments_count?: number;
has_dependencies?: boolean;
schedule_id?: string | null;
order?: number;
// Add any other task properties as needed
} }
export interface TaskGroup { export interface TaskGroup {
id: string; id: string;
title: string; title: string;
groupType: 'status' | 'priority' | 'phase';
groupValue: string; // The actual value for the group (e.g., 'todo', 'high', 'development')
collapsed: boolean;
taskIds: string[]; taskIds: string[];
color?: string; // For visual distinction type?: 'status' | 'priority' | 'phase' | 'members';
color?: string;
collapsed?: boolean;
groupValue?: string;
// Add any other group properties as needed
} }
export interface GroupingConfig { export interface GroupingConfig {
@@ -73,14 +74,14 @@ export interface Label {
// Redux State Interfaces // Redux State Interfaces
export interface TaskManagementState { export interface TaskManagementState {
entities: Record<string, Task>;
ids: string[]; ids: string[];
entities: Record<string, Task>;
loading: boolean; loading: boolean;
error: string | null; error: string | null;
groups: TaskGroup[]; // Pre-processed groups from V3 API groups: TaskGroup[];
grouping: string | null; // Current grouping from V3 API grouping: string | undefined;
selectedPriorities: string[]; // Selected priority filters selectedPriorities: string[];
search: string; // Search query for filtering tasks search: string;
} }
export interface TaskGroupsState { export interface TaskGroupsState {
@@ -89,15 +90,20 @@ export interface TaskGroupsState {
} }
export interface GroupingState { export interface GroupingState {
currentGrouping: 'status' | 'priority' | 'phase'; currentGrouping: TaskGrouping | null;
customPhases: string[]; collapsedGroups: Set<string>;
groupOrder: Record<string, string[]>;
groupStates: Record<string, { collapsed: boolean }>; // Persist group states
} }
export interface SelectionState { export interface TaskGrouping {
id: string;
name: string;
field: string;
collapsed?: boolean;
}
export interface TaskSelection {
selectedTaskIds: string[]; selectedTaskIds: string[];
lastSelectedId: string | null; lastSelectedTaskId: string | null;
} }
export interface ColumnsState { export interface ColumnsState {