Merge branch 'release/v2.0.4' of https://github.com/Worklenz/worklenz into fix/enhanced-board-sub-task-section
This commit is contained in:
295
worklenz-frontend/package-lock.json
generated
295
worklenz-frontend/package-lock.json
generated
@@ -69,11 +69,11 @@
|
|||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.0.0",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.21",
|
||||||
"postcss": "^8.5.2",
|
"postcss": "^8.5.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||||
"rollup": "^4.40.2",
|
"rollup": "^4.40.2",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.15",
|
||||||
"terser": "^5.39.0",
|
"terser": "^5.39.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
@@ -3652,6 +3652,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"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",
|
||||||
@@ -4509,13 +4521,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "1.21.7",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
@@ -4582,6 +4596,257 @@
|
|||||||
"html2canvas": "^1.0.0-rc.5"
|
"html2canvas": "^1.0.0-rc.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lightningcss": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"lightningcss-darwin-arm64": "1.30.1",
|
||||||
|
"lightningcss-darwin-x64": "1.30.1",
|
||||||
|
"lightningcss-freebsd-x64": "1.30.1",
|
||||||
|
"lightningcss-linux-arm-gnueabihf": "1.30.1",
|
||||||
|
"lightningcss-linux-arm64-gnu": "1.30.1",
|
||||||
|
"lightningcss-linux-arm64-musl": "1.30.1",
|
||||||
|
"lightningcss-linux-x64-gnu": "1.30.1",
|
||||||
|
"lightningcss-linux-x64-musl": "1.30.1",
|
||||||
|
"lightningcss-win32-arm64-msvc": "1.30.1",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.30.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"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",
|
||||||
@@ -5255,9 +5520,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier-plugin-tailwindcss": {
|
"node_modules/prettier-plugin-tailwindcss": {
|
||||||
"version": "0.6.11",
|
"version": "0.6.13",
|
||||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz",
|
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.13.tgz",
|
||||||
"integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==",
|
"integrity": "sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -7019,6 +7284,16 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwindcss/node_modules/jiti": {
|
||||||
|
"version": "1.21.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jiti": "bin/jiti.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.39.2",
|
"version": "5.39.2",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
|
||||||
|
|||||||
@@ -73,11 +73,11 @@
|
|||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.0.0",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.21",
|
||||||
"postcss": "^8.5.2",
|
"postcss": "^8.5.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||||
"rollup": "^4.40.2",
|
"rollup": "^4.40.2",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.15",
|
||||||
"terser": "^5.39.0",
|
"terser": "^5.39.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className={`
|
className={`
|
||||||
fixed z-[9999] w-72 rounded-md shadow-lg border
|
fixed z-9999 w-72 rounded-md shadow-lg border
|
||||||
${isDarkMode
|
${isDarkMode
|
||||||
? 'bg-gray-800 border-gray-600'
|
? 'bg-gray-800 border-gray-600'
|
||||||
: 'bg-white border-gray-200'
|
: 'bg-white border-gray-200'
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const Button: React.FC<ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElemen
|
|||||||
type = 'button',
|
type = 'button',
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const baseClasses = `inline-flex items-center justify-center font-medium transition-colors duration-200 focus:outline-none focus:ring-2 ${isDarkMode ? 'focus:ring-blue-400' : 'focus:ring-blue-500'} focus:ring-offset-1 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`;
|
const baseClasses = `inline-flex items-center justify-center font-medium transition-colors duration-200 focus:outline-none focus:ring-2 ${isDarkMode ? 'focus:ring-blue-400' : 'focus:ring-blue-500'} focus:ring-offset-2 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`;
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
text: isDarkMode
|
text: isDarkMode
|
||||||
@@ -42,7 +42,7 @@ const Button: React.FC<ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElemen
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
small: 'px-2 py-1 text-xs rounded',
|
small: 'px-2 py-1 text-xs rounded-sm',
|
||||||
default: 'px-3 py-2 text-sm rounded-md',
|
default: 'px-3 py-2 text-sm rounded-md',
|
||||||
large: 'px-4 py-3 text-base rounded-lg'
|
large: 'px-4 py-3 text-base rounded-lg'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const CustomColordLabel: React.FC<CustomColordLabelProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Tooltip title={label.name}>
|
<Tooltip title={label.name}>
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white flex-shrink-0 max-w-[120px]"
|
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium text-white shrink-0 max-w-[120px]"
|
||||||
style={{ backgroundColor: label.color }}
|
style={{ backgroundColor: label.color }}
|
||||||
>
|
>
|
||||||
<span className="truncate">{truncatedName}</span>
|
<span className="truncate">{truncatedName}</span>
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ const LabelsSelector: React.FC<LabelsSelectorProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className={`
|
className={`
|
||||||
fixed z-[9999] w-72 rounded-md shadow-lg border
|
fixed z-9999 w-72 rounded-md shadow-lg border
|
||||||
${isDarkMode
|
${isDarkMode
|
||||||
? 'bg-gray-800 border-gray-600'
|
? 'bg-gray-800 border-gray-600'
|
||||||
: 'bg-white border-gray-200'
|
: 'bg-white border-gray-200'
|
||||||
@@ -230,7 +230,7 @@ const LabelsSelector: React.FC<LabelsSelectorProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-full flex-shrink-0"
|
className="w-3 h-3 rounded-full shrink-0"
|
||||||
style={{ backgroundColor: label.color_code }}
|
style={{ backgroundColor: label.color_code }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const Tag: React.FC<TagProps> = ({
|
|||||||
default: 'px-2 py-1 text-xs'
|
default: 'px-2 py-1 text-xs'
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseClasses = `inline-flex items-center font-medium rounded ${sizeClasses[size]}`;
|
const baseClasses = `inline-flex items-center font-medium rounded-sm ${sizeClasses[size]}`;
|
||||||
|
|
||||||
if (variant === 'outlined') {
|
if (variant === 'outlined') {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={`relative group ${className}`}>
|
<div className={`relative group ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
<div className={`absolute ${placementClasses[placement]} px-2 py-1 text-xs text-white ${isDarkMode ? 'bg-gray-700' : 'bg-gray-900'} rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-50 pointer-events-none min-w-max`}>
|
<div className={`absolute ${placementClasses[placement]} px-2 py-1 text-xs text-white ${isDarkMode ? 'bg-gray-700' : 'bg-gray-900'} rounded-sm shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-50 pointer-events-none min-w-max`}>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy);
|
const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy);
|
||||||
const project = useAppSelector((state: RootState) => state.projectReducer.project);
|
const project = useAppSelector((state: RootState) => state.projectReducer.project);
|
||||||
const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
|
const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
|
||||||
// Load filter data
|
// Load filter data
|
||||||
useFilterDataLoader();
|
useFilterDataLoader();
|
||||||
@@ -445,18 +446,42 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
|
|
||||||
<DragOverlay>
|
<DragOverlay>
|
||||||
{activeTask && (
|
{activeTask && (
|
||||||
<EnhancedKanbanTaskCard
|
<div
|
||||||
task={activeTask}
|
style={{
|
||||||
sectionId={activeTask.status_id || ''}
|
background: themeMode === 'dark' ? '#23272f' : '#fff',
|
||||||
isDragOverlay={true}
|
borderRadius: 8,
|
||||||
/>
|
boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
|
||||||
|
padding: '12px 20px',
|
||||||
|
minWidth: 180,
|
||||||
|
maxWidth: 340,
|
||||||
|
opacity: 0.95,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 16,
|
||||||
|
color: themeMode === 'dark' ? '#fff' : '#23272f',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeTask.name}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{activeGroup && (
|
{activeGroup && (
|
||||||
<div className="group-drag-overlay">
|
<div
|
||||||
<div className="group-header-content">
|
style={{
|
||||||
<h3>{activeGroup.name}</h3>
|
background: themeMode === 'dark' ? '#23272f' : '#fff',
|
||||||
<span className="task-count">({activeGroup.tasks.length})</span>
|
borderRadius: 8,
|
||||||
</div>
|
boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
|
||||||
|
padding: '16px 24px',
|
||||||
|
minWidth: 220,
|
||||||
|
maxWidth: 320,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 12,
|
||||||
|
opacity: 0.95,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3 style={{ margin: 0, fontWeight: 600, fontSize: 18 }}>{activeGroup.name}</h3>
|
||||||
|
<span style={{ fontSize: 15, color: '#888' }}>({activeGroup.tasks.length})</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const EnhancedKanbanCreateTaskCard: React.FC<EnhancedKanbanCreateTaskCardProps>
|
|||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
}}
|
}}
|
||||||
className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
|
className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline-solid`}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
@@ -342,18 +342,6 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
style={{
|
|
||||||
minWidth: 26,
|
|
||||||
height: 26,
|
|
||||||
borderRadius: 120,
|
|
||||||
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{group.tasks.length}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{isLoading && <LoadingOutlined style={{ color: colors.darkGray }} />}
|
{isLoading && <LoadingOutlined style={{ color: colors.darkGray }} />}
|
||||||
{isEditable ? (
|
{isEditable ? (
|
||||||
@@ -403,7 +391,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{name} ({group.tasks.length})
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -480,28 +468,36 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
||||||
{group.tasks.map((task, index) => (
|
{group.tasks.map((task, index) => (
|
||||||
<React.Fragment key={task.id}>
|
<React.Fragment key={task.id}>
|
||||||
{/* Show drop indicator before task if this is the target position */}
|
{/* Drop indicator before the card if this is the drop target */}
|
||||||
{shouldShowDropIndicators && overId === task.id && (
|
{overId === task.id && (
|
||||||
<div className="drop-preview-indicator">
|
<div
|
||||||
<div className="drop-line"></div>
|
style={{
|
||||||
</div>
|
height: 20,
|
||||||
|
background: themeMode === 'dark' ? '#444' : '#e0e0e0',
|
||||||
|
borderRadius: 4,
|
||||||
|
margin: '4px 0',
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<EnhancedKanbanTaskCard
|
<EnhancedKanbanTaskCard
|
||||||
task={task}
|
task={task}
|
||||||
sectionId={group.id}
|
sectionId={group.id}
|
||||||
isActive={task.id === activeTaskId}
|
isActive={task.id === activeTaskId}
|
||||||
isDropTarget={overId === task.id}
|
isDropTarget={overId === task.id}
|
||||||
/>
|
/>
|
||||||
|
{/* Drop indicator at the end if dropping at the end of the group */}
|
||||||
{/* Show drop indicator after last task if dropping at the end */}
|
{index === group.tasks.length - 1 && overId === group.id && (
|
||||||
{shouldShowDropIndicators &&
|
<div
|
||||||
index === group.tasks.length - 1 &&
|
style={{
|
||||||
overId === group.id && (
|
height: 12,
|
||||||
<div className="drop-preview-indicator">
|
background: themeMode === 'dark' ? '#444' : '#e0e0e0',
|
||||||
<div className="drop-line"></div>
|
borderRadius: 4,
|
||||||
</div>
|
margin: '8px 0',
|
||||||
)}
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const GranttChart = React.forwardRef(({ type, date }: { type: string; date: Date
|
|||||||
style={{
|
style={{
|
||||||
background: themeWiseColor('#fff', '#141414', themeMode),
|
background: themeWiseColor('#fff', '#141414', themeMode),
|
||||||
}}
|
}}
|
||||||
className={`after:content relative z-10 after:absolute after:-right-1 after:top-0 after:-z-10 after:h-full after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent`}
|
className={`after:content relative z-10 after:absolute after:-right-1 after:top-0 after:-z-10 after:h-full after:w-1.5 after:bg-transparent after:bg-linear-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent`}
|
||||||
>
|
>
|
||||||
<GranttMembersTable
|
<GranttMembersTable
|
||||||
members={teamData}
|
members={teamData}
|
||||||
@@ -266,7 +266,7 @@ const GranttChart = React.forwardRef(({ type, date }: { type: string; date: Date
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
className={`rounded-sm outline-1 hover:outline ${themeMode === 'dark' ? 'outline-white/10' : 'outline-black/10'}`}
|
className={`rounded-xs outline-1 hover:outline-solid ${themeMode === 'dark' ? 'outline-white/10' : 'outline-black/10'}`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ const PriorityDropdown = ({ task, teamId }: PriorityDropdownProps) => {
|
|||||||
// Fallback rendering for raw priority values or when priority list is not loaded
|
// Fallback rendering for raw priority values or when priority list is not loaded
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="px-2 py-1 text-xs rounded"
|
className="px-2 py-1 text-xs rounded-sm"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: getPriorityColor(task.priority) + ALPHA_CHANNEL,
|
backgroundColor: getPriorityColor(task.priority) + ALPHA_CHANNEL,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const StatusDropdown = ({ task, teamId }: StatusDropdownProps) => {
|
|||||||
// Fallback rendering for raw status values or when status list is not loaded
|
// Fallback rendering for raw status values or when status list is not loaded
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="px-2 py-1 text-xs rounded"
|
className="px-2 py-1 text-xs rounded-sm"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: getStatusColor(task.status),
|
backgroundColor: getStatusColor(task.status),
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const TaskRowName = React.memo(
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggleExpansion(taskId)}
|
onClick={() => handleToggleExpansion(taskId)}
|
||||||
className="hover flex h-4 w-4 items-center justify-center rounded text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54] transition duration-150"
|
className="hover flex h-4 w-4 items-center justify-center rounded-sm text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54] transition duration-150"
|
||||||
>
|
>
|
||||||
{expandedTasks.includes(taskId) ? <DownOutlined /> : <RightOutlined />}
|
{expandedTasks.includes(taskId) ? <DownOutlined /> : <RightOutlined />}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const HeavyAssigneeSelector = React.lazy(() =>
|
|||||||
new Promise<{ default: React.ComponentType }>((resolve) =>
|
new Promise<{ default: React.ComponentType }>((resolve) =>
|
||||||
setTimeout(() => resolve({
|
setTimeout(() => resolve({
|
||||||
default: () => (
|
default: () => (
|
||||||
<div className="p-4 border rounded bg-blue-50">
|
<div className="p-4 border rounded-sm bg-blue-50">
|
||||||
<Text strong>🚀 Heavy Assignee Selector Loaded!</Text>
|
<Text strong>🚀 Heavy Assignee Selector Loaded!</Text>
|
||||||
<br />
|
<br />
|
||||||
<Text type="secondary">This component contains:</Text>
|
<Text type="secondary">This component contains:</Text>
|
||||||
@@ -36,7 +36,7 @@ const HeavyDatePicker = React.lazy(() =>
|
|||||||
new Promise<{ default: React.ComponentType }>((resolve) =>
|
new Promise<{ default: React.ComponentType }>((resolve) =>
|
||||||
setTimeout(() => resolve({
|
setTimeout(() => resolve({
|
||||||
default: () => (
|
default: () => (
|
||||||
<div className="p-4 border rounded bg-green-50">
|
<div className="p-4 border rounded-sm bg-green-50">
|
||||||
<Text strong>📅 Heavy Date Picker Loaded!</Text>
|
<Text strong>📅 Heavy Date Picker Loaded!</Text>
|
||||||
<br />
|
<br />
|
||||||
<Text type="secondary">This component contains:</Text>
|
<Text type="secondary">This component contains:</Text>
|
||||||
@@ -57,7 +57,7 @@ const HeavyPrioritySelector = React.lazy(() =>
|
|||||||
new Promise<{ default: React.ComponentType }>((resolve) =>
|
new Promise<{ default: React.ComponentType }>((resolve) =>
|
||||||
setTimeout(() => resolve({
|
setTimeout(() => resolve({
|
||||||
default: () => (
|
default: () => (
|
||||||
<div className="p-4 border rounded bg-orange-50">
|
<div className="p-4 border rounded-sm bg-orange-50">
|
||||||
<Text strong>🔥 Heavy Priority Selector Loaded!</Text>
|
<Text strong>🔥 Heavy Priority Selector Loaded!</Text>
|
||||||
<br />
|
<br />
|
||||||
<Text type="secondary">This component contains:</Text>
|
<Text type="secondary">This component contains:</Text>
|
||||||
@@ -78,7 +78,7 @@ const HeavyLabelsSelector = React.lazy(() =>
|
|||||||
new Promise<{ default: React.ComponentType }>((resolve) =>
|
new Promise<{ default: React.ComponentType }>((resolve) =>
|
||||||
setTimeout(() => resolve({
|
setTimeout(() => resolve({
|
||||||
default: () => (
|
default: () => (
|
||||||
<div className="p-4 border rounded bg-purple-50">
|
<div className="p-4 border rounded-sm bg-purple-50">
|
||||||
<Text strong>🏷️ Heavy Labels Selector Loaded!</Text>
|
<Text strong>🏷️ Heavy Labels Selector Loaded!</Text>
|
||||||
<br />
|
<br />
|
||||||
<Text type="secondary">This component contains:</Text>
|
<Text type="secondary">This component contains:</Text>
|
||||||
@@ -163,7 +163,7 @@ const AsanaStyleLazyDemo: React.FC = () => {
|
|||||||
<Card className="max-w-4xl mx-auto">
|
<Card className="max-w-4xl mx-auto">
|
||||||
<Title level={3}>🎯 Asana-Style Lazy Loading Demo</Title>
|
<Title level={3}>🎯 Asana-Style Lazy Loading Demo</Title>
|
||||||
|
|
||||||
<div className="mb-4 p-4 bg-gray-50 rounded">
|
<div className="mb-4 p-4 bg-gray-50 rounded-sm">
|
||||||
<Text strong>Performance Benefits:</Text>
|
<Text strong>Performance Benefits:</Text>
|
||||||
<ul className="mt-2 text-sm">
|
<ul className="mt-2 text-sm">
|
||||||
<li>✅ <strong>Faster Initial Load:</strong> Only lightweight placeholders load initially</li>
|
<li>✅ <strong>Faster Initial Load:</strong> Only lightweight placeholders load initially</li>
|
||||||
@@ -220,25 +220,25 @@ const AsanaStyleLazyDemo: React.FC = () => {
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{showComponents.assignee && (
|
{showComponents.assignee && (
|
||||||
<Suspense fallback={<div className="p-4 border rounded bg-gray-100">Loading assignee selector...</div>}>
|
<Suspense fallback={<div className="p-4 border rounded-sm bg-gray-100">Loading assignee selector...</div>}>
|
||||||
<HeavyAssigneeSelector />
|
<HeavyAssigneeSelector />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showComponents.date && (
|
{showComponents.date && (
|
||||||
<Suspense fallback={<div className="p-4 border rounded bg-gray-100">Loading date picker...</div>}>
|
<Suspense fallback={<div className="p-4 border rounded-sm bg-gray-100">Loading date picker...</div>}>
|
||||||
<HeavyDatePicker />
|
<HeavyDatePicker />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showComponents.priority && (
|
{showComponents.priority && (
|
||||||
<Suspense fallback={<div className="p-4 border rounded bg-gray-100">Loading priority selector...</div>}>
|
<Suspense fallback={<div className="p-4 border rounded-sm bg-gray-100">Loading priority selector...</div>}>
|
||||||
<HeavyPrioritySelector />
|
<HeavyPrioritySelector />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showComponents.labels && (
|
{showComponents.labels && (
|
||||||
<Suspense fallback={<div className="p-4 border rounded bg-gray-100">Loading labels selector...</div>}>
|
<Suspense fallback={<div className="p-4 border rounded-sm bg-gray-100">Loading labels selector...</div>}>
|
||||||
<HeavyLabelsSelector />
|
<HeavyLabelsSelector />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ const AssigneeDropdownContent: React.FC<AssigneeDropdownContentProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className={`
|
className={`
|
||||||
fixed z-[9999] w-72 rounded-md shadow-lg border
|
fixed z-9999 w-72 rounded-md shadow-lg border
|
||||||
${isDarkMode
|
${isDarkMode
|
||||||
? 'bg-gray-800 border-gray-600'
|
? 'bg-gray-800 border-gray-600'
|
||||||
: 'bg-white border-gray-200'
|
: 'bg-white border-gray-200'
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ const FilterDropdown: React.FC<{
|
|||||||
? (isDarkMode ? 'bg-blue-600 text-white border-blue-500' : 'bg-blue-50 text-blue-800 border-blue-300 font-semibold')
|
? (isDarkMode ? 'bg-blue-600 text-white border-blue-500' : 'bg-blue-50 text-blue-800 border-blue-300 font-semibold')
|
||||||
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
|
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
|
||||||
}
|
}
|
||||||
hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1
|
hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||||||
${isDarkMode ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'}
|
${isDarkMode ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'}
|
||||||
`}
|
`}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
@@ -414,7 +414,7 @@ const FilterDropdown: React.FC<{
|
|||||||
|
|
||||||
{/* Dropdown Panel */}
|
{/* Dropdown Panel */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className={`absolute top-full left-0 z-50 mt-1 w-64 ${themeClasses.dropdownBg} rounded-md shadow-lg border ${themeClasses.dropdownBorder}`}>
|
<div className={`absolute top-full left-0 z-50 mt-1 w-64 ${themeClasses.dropdownBg} rounded-md shadow-sm border ${themeClasses.dropdownBorder}`}>
|
||||||
{/* Search Input */}
|
{/* Search Input */}
|
||||||
{section.searchable && (
|
{section.searchable && (
|
||||||
<div className={`p-2 border-b ${themeClasses.dividerBorder}`}>
|
<div className={`p-2 border-b ${themeClasses.dividerBorder}`}>
|
||||||
@@ -546,7 +546,7 @@ const SearchFilter: React.FC<{
|
|||||||
{!isExpanded ? (
|
{!isExpanded ? (
|
||||||
<button
|
<button
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 ${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText} ${themeClasses.containerBg === 'bg-gray-800' ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'
|
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText} ${themeClasses.containerBg === 'bg-gray-800' ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<SearchOutlined className="w-3.5 h-3.5" />
|
<SearchOutlined className="w-3.5 h-3.5" />
|
||||||
@@ -579,7 +579,7 @@ const SearchFilter: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-2.5 py-1.5 text-xs font-medium text-white bg-blue-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 transition-colors duration-200"
|
className="px-2.5 py-1.5 text-xs font-medium text-white bg-blue-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
Search
|
Search
|
||||||
</button>
|
</button>
|
||||||
@@ -646,7 +646,7 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
|||||||
? (isDarkMode ? 'bg-blue-600 text-white border-blue-500' : 'bg-blue-50 text-blue-800 border-blue-300 font-semibold')
|
? (isDarkMode ? 'bg-blue-600 text-white border-blue-500' : 'bg-blue-50 text-blue-800 border-blue-300 font-semibold')
|
||||||
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
|
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
|
||||||
}
|
}
|
||||||
hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1
|
hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||||||
${isDarkMode ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'}
|
${isDarkMode ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'}
|
||||||
`}
|
`}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
@@ -666,7 +666,7 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
|||||||
|
|
||||||
{/* Dropdown Panel - matching FilterDropdown style */}
|
{/* Dropdown Panel - matching FilterDropdown style */}
|
||||||
{open && (
|
{open && (
|
||||||
<div className={`absolute top-full left-0 z-50 mt-1 w-64 ${themeClasses.dropdownBg} rounded-md shadow-lg border ${themeClasses.dropdownBorder}`}>
|
<div className={`absolute top-full left-0 z-50 mt-1 w-64 ${themeClasses.dropdownBg} rounded-md shadow-sm border ${themeClasses.dropdownBorder}`}>
|
||||||
{/* Options List */}
|
{/* Options List */}
|
||||||
<div className="max-h-48 overflow-y-auto">
|
<div className="max-h-48 overflow-y-auto">
|
||||||
{sortedFields.length === 0 ? (
|
{sortedFields.length === 0 ? (
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
/* Optimized Bulk Action Bar Styles */
|
||||||
|
.optimized-bulk-action-bar {
|
||||||
|
/* GPU acceleration for smooth animations */
|
||||||
|
will-change: transform, opacity;
|
||||||
|
transform: translateZ(0);
|
||||||
|
|
||||||
|
/* Smooth backdrop blur with fallback */
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
|
||||||
|
/* Prevent layout shifts */
|
||||||
|
contain: layout style paint;
|
||||||
|
|
||||||
|
/* Optimize for animations */
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Entrance animation */
|
||||||
|
@keyframes slideUpFadeIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(-50%) translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exit animation */
|
||||||
|
@keyframes slideDownFadeOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(-50%) translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimized-bulk-action-bar.entering {
|
||||||
|
animation: slideUpFadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimized-bulk-action-bar.exiting {
|
||||||
|
animation: slideDownFadeOut 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action button optimizations */
|
||||||
|
.bulk-action-button {
|
||||||
|
/* GPU acceleration */
|
||||||
|
will-change: transform, background-color;
|
||||||
|
transform: translateZ(0);
|
||||||
|
|
||||||
|
/* Smooth hover transitions */
|
||||||
|
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
/* Prevent text selection */
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
|
||||||
|
/* Optimize for touch */
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-button:hover {
|
||||||
|
transform: translateZ(0) scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-button:active {
|
||||||
|
transform: translateZ(0) scale(0.95);
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button loading state optimization */
|
||||||
|
.bulk-action-button.loading {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger button styling */
|
||||||
|
.bulk-action-button.danger:hover {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1) !important;
|
||||||
|
color: #ef4444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode optimizations */
|
||||||
|
.dark .bulk-action-button:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bulk-action-button.danger:hover {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Divider styling for better visual separation */
|
||||||
|
.bulk-action-divider {
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge styling optimizations */
|
||||||
|
.bulk-action-badge {
|
||||||
|
/* Smooth scaling animation */
|
||||||
|
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-badge.updating {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip optimizations */
|
||||||
|
.ant-tooltip {
|
||||||
|
/* Faster tooltip animations */
|
||||||
|
transition: opacity 0.1s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown optimizations */
|
||||||
|
.bulk-action-dropdown {
|
||||||
|
/* Smooth dropdown animations */
|
||||||
|
animation-duration: 0.2s !important;
|
||||||
|
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive optimizations */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.optimized-bulk-action-bar {
|
||||||
|
/* Adjust for mobile */
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
padding: 10px 16px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-button {
|
||||||
|
/* Smaller buttons on mobile */
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide some actions on very small screens */
|
||||||
|
.bulk-action-secondary {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.optimized-bulk-action-bar {
|
||||||
|
/* Even more compact on small screens */
|
||||||
|
bottom: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show only essential actions */
|
||||||
|
.bulk-action-tertiary {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High contrast mode support */
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
.optimized-bulk-action-bar {
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
background: var(--background-color);
|
||||||
|
backdrop-filter: none;
|
||||||
|
-webkit-backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-button {
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduced motion support */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.optimized-bulk-action-bar,
|
||||||
|
.bulk-action-button,
|
||||||
|
.bulk-action-badge {
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
will-change: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-button:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus management for accessibility */
|
||||||
|
.bulk-action-button:focus-visible {
|
||||||
|
outline: 2px solid #2563eb;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bulk-action-button:focus-visible {
|
||||||
|
outline-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance optimization classes */
|
||||||
|
.bulk-action-gpu-accelerated {
|
||||||
|
transform: translateZ(0);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-contain-layout {
|
||||||
|
contain: layout style paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner optimization */
|
||||||
|
.bulk-action-loading-spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth color transitions for theme switching */
|
||||||
|
.bulk-action-theme-transition {
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize for 60fps animations */
|
||||||
|
.bulk-action-60fps {
|
||||||
|
animation-duration: 0.25s;
|
||||||
|
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent layout thrashing during animations */
|
||||||
|
.bulk-action-stable-layout {
|
||||||
|
contain: layout;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Dropdown,
|
||||||
|
Popconfirm,
|
||||||
|
Tooltip,
|
||||||
|
Space,
|
||||||
|
Badge,
|
||||||
|
Divider
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
MoreOutlined,
|
||||||
|
RetweetOutlined,
|
||||||
|
UserAddOutlined,
|
||||||
|
InboxOutlined,
|
||||||
|
TagsOutlined,
|
||||||
|
UsergroupAddOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
CopyOutlined,
|
||||||
|
ExportOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
FlagOutlined,
|
||||||
|
BulbOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '@/app/store';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
interface OptimizedBulkActionBarProps {
|
||||||
|
selectedTaskIds: string[];
|
||||||
|
totalSelected: number;
|
||||||
|
projectId: string;
|
||||||
|
onClearSelection?: () => void;
|
||||||
|
onBulkStatusChange?: (statusId: string) => void;
|
||||||
|
onBulkPriorityChange?: (priorityId: string) => void;
|
||||||
|
onBulkPhaseChange?: (phaseId: string) => void;
|
||||||
|
onBulkAssignToMe?: () => void;
|
||||||
|
onBulkAssignMembers?: (memberIds: string[]) => void;
|
||||||
|
onBulkAddLabels?: (labelIds: string[]) => void;
|
||||||
|
onBulkArchive?: () => void;
|
||||||
|
onBulkDelete?: () => void;
|
||||||
|
onBulkDuplicate?: () => void;
|
||||||
|
onBulkExport?: () => void;
|
||||||
|
onBulkSetDueDate?: (date: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance-optimized memoized action button component
|
||||||
|
const ActionButton = React.memo<{
|
||||||
|
icon: React.ReactNode;
|
||||||
|
tooltip: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
danger?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
isDarkMode: boolean;
|
||||||
|
badge?: number;
|
||||||
|
}>(({ icon, tooltip, onClick, loading = false, danger = false, disabled = false, isDarkMode, badge }) => {
|
||||||
|
const buttonStyle = useMemo(() => ({
|
||||||
|
background: 'transparent',
|
||||||
|
color: isDarkMode ? '#e5e7eb' : '#374151',
|
||||||
|
border: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '6px',
|
||||||
|
height: '32px',
|
||||||
|
width: '32px',
|
||||||
|
fontSize: '14px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
transition: 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: disabled ? 0.5 : 1,
|
||||||
|
...(danger && {
|
||||||
|
color: '#ef4444',
|
||||||
|
}),
|
||||||
|
}), [isDarkMode, danger, disabled]);
|
||||||
|
|
||||||
|
const hoverStyle = useMemo(() => ({
|
||||||
|
backgroundColor: isDarkMode
|
||||||
|
? (danger ? 'rgba(239, 68, 68, 0.1)' : 'rgba(255, 255, 255, 0.1)')
|
||||||
|
: (danger ? 'rgba(239, 68, 68, 0.1)' : 'rgba(0, 0, 0, 0.05)'),
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
}), [isDarkMode, danger]);
|
||||||
|
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
const combinedStyle = useMemo(() => ({
|
||||||
|
...buttonStyle,
|
||||||
|
...(isHovered && !disabled ? hoverStyle : {}),
|
||||||
|
}), [buttonStyle, hoverStyle, isHovered, disabled]);
|
||||||
|
|
||||||
|
const ButtonComponent = (
|
||||||
|
<Button
|
||||||
|
icon={icon}
|
||||||
|
style={combinedStyle}
|
||||||
|
size="small"
|
||||||
|
loading={loading}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={tooltip} placement="top">
|
||||||
|
{badge && badge > 0 ? (
|
||||||
|
<Badge count={badge} size="small" offset={[-2, 2]}>
|
||||||
|
{ButtonComponent}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
ButtonComponent
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ActionButton.displayName = 'ActionButton';
|
||||||
|
|
||||||
|
// Performance-optimized main component
|
||||||
|
const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = React.memo(({
|
||||||
|
selectedTaskIds,
|
||||||
|
totalSelected,
|
||||||
|
projectId,
|
||||||
|
onClearSelection,
|
||||||
|
onBulkStatusChange,
|
||||||
|
onBulkPriorityChange,
|
||||||
|
onBulkPhaseChange,
|
||||||
|
onBulkAssignToMe,
|
||||||
|
onBulkAssignMembers,
|
||||||
|
onBulkAddLabels,
|
||||||
|
onBulkArchive,
|
||||||
|
onBulkDelete,
|
||||||
|
onBulkDuplicate,
|
||||||
|
onBulkExport,
|
||||||
|
onBulkSetDueDate,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation('task-management');
|
||||||
|
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
||||||
|
|
||||||
|
// Performance state management
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [loadingStates, setLoadingStates] = useState({
|
||||||
|
status: false,
|
||||||
|
priority: false,
|
||||||
|
phase: false,
|
||||||
|
assignToMe: false,
|
||||||
|
assignMembers: false,
|
||||||
|
labels: false,
|
||||||
|
archive: false,
|
||||||
|
delete: false,
|
||||||
|
duplicate: false,
|
||||||
|
export: false,
|
||||||
|
dueDate: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smooth entrance animation
|
||||||
|
useEffect(() => {
|
||||||
|
if (totalSelected > 0) {
|
||||||
|
// Micro-delay for smoother animation
|
||||||
|
const timer = setTimeout(() => setIsVisible(true), 50);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
} else {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
}, [totalSelected]);
|
||||||
|
|
||||||
|
// Optimized loading state updater
|
||||||
|
const updateLoadingState = useCallback((action: keyof typeof loadingStates, loading: boolean) => {
|
||||||
|
setLoadingStates(prev => ({ ...prev, [action]: loading }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Memoized handlers with loading states
|
||||||
|
const handleStatusChange = useCallback(async () => {
|
||||||
|
updateLoadingState('status', true);
|
||||||
|
try {
|
||||||
|
await onBulkStatusChange?.('new-status-id');
|
||||||
|
} finally {
|
||||||
|
updateLoadingState('status', false);
|
||||||
|
}
|
||||||
|
}, [onBulkStatusChange, updateLoadingState]);
|
||||||
|
|
||||||
|
const handlePriorityChange = useCallback(async () => {
|
||||||
|
updateLoadingState('priority', true);
|
||||||
|
try {
|
||||||
|
await onBulkPriorityChange?.('new-priority-id');
|
||||||
|
} finally {
|
||||||
|
updateLoadingState('priority', false);
|
||||||
|
}
|
||||||
|
}, [onBulkPriorityChange, updateLoadingState]);
|
||||||
|
|
||||||
|
const handlePhaseChange = useCallback(async () => {
|
||||||
|
updateLoadingState('phase', true);
|
||||||
|
try {
|
||||||
|
await onBulkPhaseChange?.('new-phase-id');
|
||||||
|
} finally {
|
||||||
|
updateLoadingState('phase', false);
|
||||||
|
}
|
||||||
|
}, [onBulkPhaseChange, updateLoadingState]);
|
||||||
|
|
||||||
|
const handleAssignToMe = useCallback(async () => {
|
||||||
|
updateLoadingState('assignToMe', true);
|
||||||
|
try {
|
||||||
|
await onBulkAssignToMe?.();
|
||||||
|
} finally {
|
||||||
|
updateLoadingState('assignToMe', false);
|
||||||
|
}
|
||||||
|
}, [onBulkAssignToMe, updateLoadingState]);
|
||||||
|
|
||||||
|
const handleArchive = useCallback(async () => {
|
||||||
|
updateLoadingState('archive', true);
|
||||||
|
try {
|
||||||
|
await onBulkArchive?.();
|
||||||
|
} finally {
|
||||||
|
updateLoadingState('archive', false);
|
||||||
|
}
|
||||||
|
}, [onBulkArchive, updateLoadingState]);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(async () => {
|
||||||
|
updateLoadingState('delete', true);
|
||||||
|
try {
|
||||||
|
await onBulkDelete?.();
|
||||||
|
} finally {
|
||||||
|
updateLoadingState('delete', false);
|
||||||
|
}
|
||||||
|
}, [onBulkDelete, updateLoadingState]);
|
||||||
|
|
||||||
|
const handleDuplicate = useCallback(async () => {
|
||||||
|
updateLoadingState('duplicate', true);
|
||||||
|
try {
|
||||||
|
await onBulkDuplicate?.();
|
||||||
|
} finally {
|
||||||
|
updateLoadingState('duplicate', false);
|
||||||
|
}
|
||||||
|
}, [onBulkDuplicate, updateLoadingState]);
|
||||||
|
|
||||||
|
const handleExport = useCallback(async () => {
|
||||||
|
updateLoadingState('export', true);
|
||||||
|
try {
|
||||||
|
await onBulkExport?.();
|
||||||
|
} finally {
|
||||||
|
updateLoadingState('export', false);
|
||||||
|
}
|
||||||
|
}, [onBulkExport, updateLoadingState]);
|
||||||
|
|
||||||
|
// Memoized styles for better performance
|
||||||
|
const containerStyle = useMemo((): React.CSSProperties => ({
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '24px',
|
||||||
|
left: '50%',
|
||||||
|
transform: `translateX(-50%) translateY(${isVisible ? '0' : '20px'})`,
|
||||||
|
zIndex: 1000,
|
||||||
|
background: isDarkMode
|
||||||
|
? 'rgba(31, 41, 55, 0.95)'
|
||||||
|
: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
backdropFilter: 'blur(12px)',
|
||||||
|
WebkitBackdropFilter: 'blur(12px)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '12px 20px',
|
||||||
|
boxShadow: isDarkMode
|
||||||
|
? '0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(55, 65, 81, 0.3)'
|
||||||
|
: '0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04), 0 0 0 1px rgba(0, 0, 0, 0.05)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px',
|
||||||
|
minWidth: 'fit-content',
|
||||||
|
maxWidth: '90vw',
|
||||||
|
opacity: isVisible ? 1 : 0,
|
||||||
|
visibility: isVisible ? 'visible' : 'hidden',
|
||||||
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
border: isDarkMode
|
||||||
|
? '1px solid rgba(55, 65, 81, 0.3)'
|
||||||
|
: '1px solid rgba(229, 231, 235, 0.8)',
|
||||||
|
}), [isDarkMode, isVisible]);
|
||||||
|
|
||||||
|
const textStyle = useMemo(() => ({
|
||||||
|
color: isDarkMode ? '#f3f4f6' : '#374151',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
marginRight: '12px',
|
||||||
|
whiteSpace: 'nowrap' as const,
|
||||||
|
}), [isDarkMode]);
|
||||||
|
|
||||||
|
// Quick actions dropdown menu
|
||||||
|
const quickActionsMenu = useMemo(() => ({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'change-status',
|
||||||
|
label: 'Change Status',
|
||||||
|
icon: <RetweetOutlined />,
|
||||||
|
onClick: handleStatusChange,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'change-priority',
|
||||||
|
label: 'Change Priority',
|
||||||
|
icon: <FlagOutlined />,
|
||||||
|
onClick: handlePriorityChange,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'change-phase',
|
||||||
|
label: 'Change Phase',
|
||||||
|
icon: <RetweetOutlined />,
|
||||||
|
onClick: handlePhaseChange,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'set-due-date',
|
||||||
|
label: 'Set Due Date',
|
||||||
|
icon: <CalendarOutlined />,
|
||||||
|
onClick: () => onBulkSetDueDate?.(new Date().toISOString()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider' as const,
|
||||||
|
key: 'divider-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'duplicate',
|
||||||
|
label: 'Duplicate Tasks',
|
||||||
|
icon: <CopyOutlined />,
|
||||||
|
onClick: handleDuplicate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'export',
|
||||||
|
label: 'Export Tasks',
|
||||||
|
icon: <ExportOutlined />,
|
||||||
|
onClick: handleExport,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}), [handleStatusChange, handlePriorityChange, handlePhaseChange, handleDuplicate, handleExport, onBulkSetDueDate]);
|
||||||
|
|
||||||
|
// Don't render if no tasks selected
|
||||||
|
if (totalSelected === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={containerStyle}>
|
||||||
|
{/* Selection Count */}
|
||||||
|
<Text style={textStyle}>
|
||||||
|
<Badge
|
||||||
|
count={totalSelected}
|
||||||
|
style={{
|
||||||
|
backgroundColor: isDarkMode ? '#3b82f6' : '#2563eb',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '11px',
|
||||||
|
height: '18px',
|
||||||
|
lineHeight: '18px',
|
||||||
|
minWidth: '18px',
|
||||||
|
marginRight: '6px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{totalSelected} {totalSelected === 1 ? 'task' : 'tasks'} selected
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Divider
|
||||||
|
type="vertical"
|
||||||
|
style={{
|
||||||
|
height: '20px',
|
||||||
|
margin: '0 8px',
|
||||||
|
borderColor: isDarkMode ? 'rgba(55, 65, 81, 0.5)' : 'rgba(229, 231, 235, 0.8)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Actions in same order as original component */}
|
||||||
|
<Space size={2}>
|
||||||
|
{/* Change Status/Priority/Phase */}
|
||||||
|
<Tooltip title="Change Status/Priority/Phase" placement="top">
|
||||||
|
<Dropdown
|
||||||
|
menu={quickActionsMenu}
|
||||||
|
trigger={['click']}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<RetweetOutlined />}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
color: isDarkMode ? '#e5e7eb' : '#374151',
|
||||||
|
border: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '6px',
|
||||||
|
height: '32px',
|
||||||
|
width: '32px',
|
||||||
|
fontSize: '14px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
transition: 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
loading={loadingStates.status || loadingStates.priority || loadingStates.phase}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Change Labels */}
|
||||||
|
<ActionButton
|
||||||
|
icon={<TagsOutlined />}
|
||||||
|
tooltip="Add Labels"
|
||||||
|
onClick={() => onBulkAddLabels?.([])}
|
||||||
|
loading={loadingStates.labels}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Assign to Me */}
|
||||||
|
<ActionButton
|
||||||
|
icon={<UserAddOutlined />}
|
||||||
|
tooltip="Assign to Me"
|
||||||
|
onClick={handleAssignToMe}
|
||||||
|
loading={loadingStates.assignToMe}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Change Assignees */}
|
||||||
|
<ActionButton
|
||||||
|
icon={<UsergroupAddOutlined />}
|
||||||
|
tooltip="Assign Members"
|
||||||
|
onClick={() => onBulkAssignMembers?.([])}
|
||||||
|
loading={loadingStates.assignMembers}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Archive */}
|
||||||
|
<ActionButton
|
||||||
|
icon={<InboxOutlined />}
|
||||||
|
tooltip="Archive"
|
||||||
|
onClick={handleArchive}
|
||||||
|
loading={loadingStates.archive}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Delete */}
|
||||||
|
<Popconfirm
|
||||||
|
title={`Delete ${totalSelected} ${totalSelected === 1 ? 'task' : 'tasks'}?`}
|
||||||
|
description="This action cannot be undone."
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
okText="Delete"
|
||||||
|
cancelText="Cancel"
|
||||||
|
okType="danger"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<ActionButton
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
tooltip="Delete"
|
||||||
|
loading={loadingStates.delete}
|
||||||
|
danger
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
|
|
||||||
|
<Divider
|
||||||
|
type="vertical"
|
||||||
|
style={{
|
||||||
|
height: '20px',
|
||||||
|
margin: '0 4px',
|
||||||
|
borderColor: isDarkMode ? 'rgba(55, 65, 81, 0.5)' : 'rgba(229, 231, 235, 0.8)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Clear Selection */}
|
||||||
|
<ActionButton
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
tooltip="Clear Selection"
|
||||||
|
onClick={onClearSelection}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
OptimizedBulkActionBarContent.displayName = 'OptimizedBulkActionBarContent';
|
||||||
|
|
||||||
|
// Portal wrapper for performance isolation
|
||||||
|
const OptimizedBulkActionBar: React.FC<OptimizedBulkActionBarProps> = React.memo((props) => {
|
||||||
|
// Only render portal if tasks are selected for better performance
|
||||||
|
if (props.totalSelected === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<OptimizedBulkActionBarContent {...props} />,
|
||||||
|
document.body,
|
||||||
|
'optimized-bulk-action-bar'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
OptimizedBulkActionBar.displayName = 'OptimizedBulkActionBar';
|
||||||
|
|
||||||
|
export default OptimizedBulkActionBar;
|
||||||
@@ -41,9 +41,38 @@ import { Task } from '@/types/task-management.types';
|
|||||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||||
import TaskRow from './task-row';
|
import TaskRow from './task-row';
|
||||||
// import BulkActionBar from './bulk-action-bar';
|
// import BulkActionBar from './bulk-action-bar';
|
||||||
|
import OptimizedBulkActionBar from './optimized-bulk-action-bar';
|
||||||
import VirtualizedTaskList from './virtualized-task-list';
|
import VirtualizedTaskList from './virtualized-task-list';
|
||||||
import { AppDispatch } from '@/app/store';
|
import { AppDispatch } from '@/app/store';
|
||||||
import { shallowEqual } from 'react-redux';
|
import { shallowEqual } from 'react-redux';
|
||||||
|
import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice';
|
||||||
|
import { taskListBulkActionsApiService } from '@/api/tasks/task-list-bulk-actions.api.service';
|
||||||
|
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||||
|
import {
|
||||||
|
evt_project_task_list_bulk_archive,
|
||||||
|
evt_project_task_list_bulk_assign_me,
|
||||||
|
evt_project_task_list_bulk_assign_members,
|
||||||
|
evt_project_task_list_bulk_change_phase,
|
||||||
|
evt_project_task_list_bulk_change_priority,
|
||||||
|
evt_project_task_list_bulk_change_status,
|
||||||
|
evt_project_task_list_bulk_delete,
|
||||||
|
evt_project_task_list_bulk_update_labels,
|
||||||
|
} from '@/shared/worklenz-analytics-events';
|
||||||
|
import {
|
||||||
|
IBulkTasksLabelsRequest,
|
||||||
|
IBulkTasksPhaseChangeRequest,
|
||||||
|
IBulkTasksPriorityChangeRequest,
|
||||||
|
IBulkTasksStatusChangeRequest,
|
||||||
|
} from '@/types/tasks/bulk-action-bar.types';
|
||||||
|
import { ITaskStatus } from '@/types/tasks/taskStatus.types';
|
||||||
|
import { ITaskPriority } from '@/types/tasks/taskPriority.types';
|
||||||
|
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
||||||
|
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
||||||
|
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
|
||||||
|
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
|
||||||
|
import alertService from '@/services/alerts/alertService';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
|
||||||
import { performanceMonitor } from '@/utils/performance-monitor';
|
import { performanceMonitor } from '@/utils/performance-monitor';
|
||||||
import debugPerformance from '@/utils/debug-performance';
|
import debugPerformance from '@/utils/debug-performance';
|
||||||
|
|
||||||
@@ -53,6 +82,7 @@ import PerformanceAnalysis from './performance-analysis';
|
|||||||
|
|
||||||
// Import drag and drop performance optimizations
|
// Import drag and drop performance optimizations
|
||||||
import './drag-drop-optimized.css';
|
import './drag-drop-optimized.css';
|
||||||
|
import './optimized-bulk-action-bar.css';
|
||||||
|
|
||||||
interface TaskListBoardProps {
|
interface TaskListBoardProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -91,6 +121,7 @@ const throttle = <T extends (...args: any[]) => void>(func: T, delay: number): T
|
|||||||
const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = '' }) => {
|
const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = '' }) => {
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const { t } = useTranslation('task-management');
|
const { t } = useTranslation('task-management');
|
||||||
|
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||||
const [dragState, setDragState] = useState<DragState>({
|
const [dragState, setDragState] = useState<DragState>({
|
||||||
activeTask: null,
|
activeTask: null,
|
||||||
activeGroupId: null,
|
activeGroupId: null,
|
||||||
@@ -119,6 +150,14 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
const selectedTaskIds = useSelector(selectSelectedTaskIds);
|
const selectedTaskIds = useSelector(selectSelectedTaskIds);
|
||||||
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
|
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
|
||||||
const error = useSelector((state: RootState) => state.taskManagement.error);
|
const error = useSelector((state: RootState) => state.taskManagement.error);
|
||||||
|
|
||||||
|
// Bulk action selectors
|
||||||
|
const statusList = useSelector((state: RootState) => state.taskStatusReducer.status);
|
||||||
|
const priorityList = useSelector((state: RootState) => state.priorityReducer.priorities);
|
||||||
|
const phaseList = useSelector((state: RootState) => state.phaseReducer.phaseList);
|
||||||
|
const labelsList = useSelector((state: RootState) => state.taskLabelsReducer.labels);
|
||||||
|
const members = useSelector((state: RootState) => state.teamMembersReducer.teamMembers);
|
||||||
|
const archived = useSelector((state: RootState) => state.taskReducer.archived);
|
||||||
|
|
||||||
// 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');
|
||||||
@@ -401,6 +440,221 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
);
|
);
|
||||||
}, [dragState.activeTask, dragState.activeGroupId, projectId, currentGrouping]);
|
}, [dragState.activeTask, dragState.activeGroupId, projectId, currentGrouping]);
|
||||||
|
|
||||||
|
// Bulk action handlers - implementing real functionality from task-list-bulk-actions-bar
|
||||||
|
const handleClearSelection = useCallback(() => {
|
||||||
|
dispatch(deselectAll());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleBulkStatusChange = useCallback(async (statusId: string) => {
|
||||||
|
if (!statusId || !projectId) return;
|
||||||
|
try {
|
||||||
|
// Find the status object
|
||||||
|
const status = statusList.find(s => s.id === statusId);
|
||||||
|
if (!status || !status.id) return;
|
||||||
|
|
||||||
|
const body: IBulkTasksStatusChangeRequest = {
|
||||||
|
tasks: selectedTaskIds,
|
||||||
|
status_id: status.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check task dependencies first
|
||||||
|
for (const taskId of selectedTaskIds) {
|
||||||
|
const canContinue = await checkTaskDependencyStatus(taskId, status.id);
|
||||||
|
if (!canContinue) {
|
||||||
|
if (selectedTaskIds.length > 1) {
|
||||||
|
alertService.warning(
|
||||||
|
'Incomplete Dependencies!',
|
||||||
|
'Some tasks were not updated. Please ensure all dependent tasks are completed before proceeding.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
alertService.error(
|
||||||
|
'Task is not completed',
|
||||||
|
'Please complete the task dependencies before proceeding'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await taskListBulkActionsApiService.changeStatus(body, projectId);
|
||||||
|
if (res.done) {
|
||||||
|
trackMixpanelEvent(evt_project_task_list_bulk_change_status);
|
||||||
|
dispatch(deselectAll());
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error changing status:', error);
|
||||||
|
}
|
||||||
|
}, [selectedTaskIds, statusList, projectId, trackMixpanelEvent, dispatch]);
|
||||||
|
|
||||||
|
const handleBulkPriorityChange = useCallback(async (priorityId: string) => {
|
||||||
|
if (!priorityId || !projectId) return;
|
||||||
|
try {
|
||||||
|
const priority = priorityList.find(p => p.id === priorityId);
|
||||||
|
if (!priority || !priority.id) return;
|
||||||
|
|
||||||
|
const body: IBulkTasksPriorityChangeRequest = {
|
||||||
|
tasks: selectedTaskIds,
|
||||||
|
priority_id: priority.id,
|
||||||
|
};
|
||||||
|
const res = await taskListBulkActionsApiService.changePriority(body, projectId);
|
||||||
|
if (res.done) {
|
||||||
|
trackMixpanelEvent(evt_project_task_list_bulk_change_priority);
|
||||||
|
dispatch(deselectAll());
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error changing priority:', error);
|
||||||
|
}
|
||||||
|
}, [selectedTaskIds, priorityList, projectId, trackMixpanelEvent, dispatch]);
|
||||||
|
|
||||||
|
const handleBulkPhaseChange = useCallback(async (phaseId: string) => {
|
||||||
|
if (!phaseId || !projectId) return;
|
||||||
|
try {
|
||||||
|
const phase = phaseList.find(p => p.id === phaseId);
|
||||||
|
if (!phase || !phase.id) return;
|
||||||
|
|
||||||
|
const body: IBulkTasksPhaseChangeRequest = {
|
||||||
|
tasks: selectedTaskIds,
|
||||||
|
phase_id: phase.id,
|
||||||
|
};
|
||||||
|
const res = await taskListBulkActionsApiService.changePhase(body, projectId);
|
||||||
|
if (res.done) {
|
||||||
|
trackMixpanelEvent(evt_project_task_list_bulk_change_phase);
|
||||||
|
dispatch(deselectAll());
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error changing phase:', error);
|
||||||
|
}
|
||||||
|
}, [selectedTaskIds, phaseList, projectId, trackMixpanelEvent, dispatch]);
|
||||||
|
|
||||||
|
const handleBulkAssignToMe = useCallback(async () => {
|
||||||
|
if (!projectId) return;
|
||||||
|
try {
|
||||||
|
const body = {
|
||||||
|
tasks: selectedTaskIds,
|
||||||
|
project_id: projectId,
|
||||||
|
};
|
||||||
|
const res = await taskListBulkActionsApiService.assignToMe(body);
|
||||||
|
if (res.done) {
|
||||||
|
trackMixpanelEvent(evt_project_task_list_bulk_assign_me);
|
||||||
|
dispatch(deselectAll());
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error assigning to me:', error);
|
||||||
|
}
|
||||||
|
}, [selectedTaskIds, projectId, trackMixpanelEvent, dispatch]);
|
||||||
|
|
||||||
|
const handleBulkAssignMembers = useCallback(async (memberIds: string[]) => {
|
||||||
|
if (!projectId || !members?.data) return;
|
||||||
|
try {
|
||||||
|
// Convert memberIds to member objects with proper type checking
|
||||||
|
const selectedMembers = members.data.filter(member =>
|
||||||
|
member.id && memberIds.includes(member.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
tasks: selectedTaskIds,
|
||||||
|
project_id: projectId,
|
||||||
|
members: selectedMembers.map(member => ({
|
||||||
|
id: member.id!,
|
||||||
|
name: member.name || '',
|
||||||
|
email: member.email || '',
|
||||||
|
avatar_url: member.avatar_url || '',
|
||||||
|
team_member_id: member.id!,
|
||||||
|
project_member_id: member.id!,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
const res = await taskListBulkActionsApiService.assignTasks(body);
|
||||||
|
if (res.done) {
|
||||||
|
trackMixpanelEvent(evt_project_task_list_bulk_assign_members);
|
||||||
|
dispatch(deselectAll());
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error assigning tasks:', error);
|
||||||
|
}
|
||||||
|
}, [selectedTaskIds, projectId, members, trackMixpanelEvent, dispatch]);
|
||||||
|
|
||||||
|
const handleBulkAddLabels = useCallback(async (labelIds: string[]) => {
|
||||||
|
if (!projectId) return;
|
||||||
|
try {
|
||||||
|
// Convert labelIds to label objects with proper type checking
|
||||||
|
const selectedLabels = labelsList.filter(label =>
|
||||||
|
label.id && labelIds.includes(label.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const body: IBulkTasksLabelsRequest = {
|
||||||
|
tasks: selectedTaskIds,
|
||||||
|
labels: selectedLabels,
|
||||||
|
text: null,
|
||||||
|
};
|
||||||
|
const res = await taskListBulkActionsApiService.assignLabels(body, projectId);
|
||||||
|
if (res.done) {
|
||||||
|
trackMixpanelEvent(evt_project_task_list_bulk_update_labels);
|
||||||
|
dispatch(deselectAll());
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
dispatch(fetchLabels());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating labels:', error);
|
||||||
|
}
|
||||||
|
}, [selectedTaskIds, projectId, labelsList, trackMixpanelEvent, dispatch]);
|
||||||
|
|
||||||
|
const handleBulkArchive = useCallback(async () => {
|
||||||
|
if (!projectId) return;
|
||||||
|
try {
|
||||||
|
const body = {
|
||||||
|
tasks: selectedTaskIds,
|
||||||
|
project_id: projectId,
|
||||||
|
};
|
||||||
|
const res = await taskListBulkActionsApiService.archiveTasks(body, archived);
|
||||||
|
if (res.done) {
|
||||||
|
trackMixpanelEvent(evt_project_task_list_bulk_archive);
|
||||||
|
dispatch(deselectAll());
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error archiving tasks:', error);
|
||||||
|
}
|
||||||
|
}, [selectedTaskIds, projectId, archived, trackMixpanelEvent, dispatch]);
|
||||||
|
|
||||||
|
const handleBulkDelete = useCallback(async () => {
|
||||||
|
if (!projectId) return;
|
||||||
|
try {
|
||||||
|
const body = {
|
||||||
|
tasks: selectedTaskIds,
|
||||||
|
project_id: projectId,
|
||||||
|
};
|
||||||
|
const res = await taskListBulkActionsApiService.deleteTasks(body, projectId);
|
||||||
|
if (res.done) {
|
||||||
|
trackMixpanelEvent(evt_project_task_list_bulk_delete);
|
||||||
|
dispatch(deselectAll());
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error deleting tasks:', error);
|
||||||
|
}
|
||||||
|
}, [selectedTaskIds, projectId, trackMixpanelEvent, dispatch]);
|
||||||
|
|
||||||
|
// Additional handlers for new actions
|
||||||
|
const handleBulkDuplicate = useCallback(async () => {
|
||||||
|
// This would need to be implemented in the API service
|
||||||
|
console.log('Bulk duplicate not yet implemented in API:', selectedTaskIds);
|
||||||
|
}, [selectedTaskIds]);
|
||||||
|
|
||||||
|
const handleBulkExport = useCallback(async () => {
|
||||||
|
// This would need to be implemented in the API service
|
||||||
|
console.log('Bulk export not yet implemented in API:', selectedTaskIds);
|
||||||
|
}, [selectedTaskIds]);
|
||||||
|
|
||||||
|
const handleBulkSetDueDate = useCallback(async (date: string) => {
|
||||||
|
// This would need to be implemented in the API service
|
||||||
|
console.log('Bulk set due date not yet implemented in API:', date, selectedTaskIds);
|
||||||
|
}, [selectedTaskIds]);
|
||||||
|
|
||||||
// Cleanup effect
|
// Cleanup effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -525,6 +779,25 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|
||||||
|
{/* Optimized Bulk Action Bar */}
|
||||||
|
<OptimizedBulkActionBar
|
||||||
|
selectedTaskIds={selectedTaskIds}
|
||||||
|
totalSelected={selectedTaskIds.length}
|
||||||
|
projectId={projectId}
|
||||||
|
onClearSelection={handleClearSelection}
|
||||||
|
onBulkStatusChange={handleBulkStatusChange}
|
||||||
|
onBulkPriorityChange={handleBulkPriorityChange}
|
||||||
|
onBulkPhaseChange={handleBulkPhaseChange}
|
||||||
|
onBulkAssignToMe={handleBulkAssignToMe}
|
||||||
|
onBulkAssignMembers={handleBulkAssignMembers}
|
||||||
|
onBulkAddLabels={handleBulkAddLabels}
|
||||||
|
onBulkArchive={handleBulkArchive}
|
||||||
|
onBulkDelete={handleBulkDelete}
|
||||||
|
onBulkDuplicate={handleBulkDuplicate}
|
||||||
|
onBulkExport={handleBulkExport}
|
||||||
|
onBulkSetDueDate={handleBulkSetDueDate}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
/* Fixed height container - Asana style */
|
/* Fixed height container - Asana style */
|
||||||
.task-groups-container-fixed {
|
.task-groups-container-fixed {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ const TaskPhaseDropdown: React.FC<TaskPhaseDropdownProps> = ({
|
|||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className={`
|
className={`
|
||||||
fixed min-w-[160px] max-w-[220px]
|
fixed min-w-[160px] max-w-[220px]
|
||||||
rounded border backdrop-blur-sm z-[9999]
|
rounded border backdrop-blur-xs z-9999
|
||||||
${isDarkMode
|
${isDarkMode
|
||||||
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
|
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
|
||||||
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
|
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ const TaskPriorityDropdown: React.FC<TaskPriorityDropdownProps> = ({
|
|||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className={`
|
className={`
|
||||||
fixed min-w-[160px] max-w-[220px]
|
fixed min-w-[160px] max-w-[220px]
|
||||||
rounded border backdrop-blur-sm z-[9999]
|
rounded border backdrop-blur-xs z-9999
|
||||||
${isDarkMode
|
${isDarkMode
|
||||||
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
|
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
|
||||||
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
|
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ const AssigneePlaceholder = React.memo<{ isDarkMode: boolean; memberCount?: numb
|
|||||||
) : (
|
) : (
|
||||||
<div className={`w-6 h-6 rounded-full ${isDarkMode ? 'bg-gray-600' : 'bg-gray-200'}`} />
|
<div className={`w-6 h-6 rounded-full ${isDarkMode ? 'bg-gray-600' : 'bg-gray-200'}`} />
|
||||||
)}
|
)}
|
||||||
<div className={`w-4 h-4 rounded ${isDarkMode ? 'bg-gray-600' : 'bg-gray-200'}`} />
|
<div className={`w-4 h-4 rounded-sm ${isDarkMode ? 'bg-gray-600' : 'bg-gray-200'}`} />
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -249,7 +249,7 @@ const LabelsPlaceholder = React.memo<{ labelCount?: number; isDarkMode: boolean
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className={`w-4 h-4 rounded ${isDarkMode ? 'bg-gray-600' : 'bg-gray-200'}`} />
|
<div className={`w-4 h-4 rounded-sm ${isDarkMode ? 'bg-gray-600' : 'bg-gray-200'}`} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
@@ -257,7 +257,7 @@ const LabelsPlaceholder = React.memo<{ labelCount?: number; isDarkMode: boolean
|
|||||||
// PERFORMANCE OPTIMIZATION: Simplified placeholders without animations under memory pressure
|
// PERFORMANCE OPTIMIZATION: Simplified placeholders without animations under memory pressure
|
||||||
const SimplePlaceholder = React.memo<{ width: number; height: number; isDarkMode: boolean }>(({ width, height, isDarkMode }) => (
|
const SimplePlaceholder = React.memo<{ width: number; height: number; isDarkMode: boolean }>(({ width, height, isDarkMode }) => (
|
||||||
<div
|
<div
|
||||||
className={`rounded ${isDarkMode ? 'bg-gray-600' : 'bg-gray-200'}`}
|
className={`rounded-sm ${isDarkMode ? 'bg-gray-600' : 'bg-gray-200'}`}
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
@@ -614,7 +614,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
? (isDarkMode ? 'bg-blue-900/20' : 'bg-blue-50')
|
? (isDarkMode ? 'bg-blue-900/20' : 'bg-blue-50')
|
||||||
: '';
|
: '';
|
||||||
const overlay = isDragOverlay
|
const overlay = isDragOverlay
|
||||||
? `rounded shadow-lg border-2 ${isDarkMode ? 'border-gray-600 shadow-2xl' : 'border-gray-300 shadow-2xl'}`
|
? `rounded-sm shadow-lg border-2 ${isDarkMode ? 'border-gray-600 shadow-2xl' : 'border-gray-300 shadow-2xl'}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -643,7 +643,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
case 'drag':
|
case 'drag':
|
||||||
return (
|
return (
|
||||||
<div key={col.key} className={`flex items-center justify-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
<div key={col.key} className={`flex items-center justify-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||||
<div className="w-4 h-4 opacity-30 bg-gray-300 rounded"></div>
|
<div className="w-4 h-4 opacity-30 bg-gray-300 rounded-sm"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -689,7 +689,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
// For non-essential columns, show minimal placeholder
|
// For non-essential columns, show minimal placeholder
|
||||||
return (
|
return (
|
||||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||||
<div className={`w-6 h-3 rounded ${isDarkMode ? 'bg-gray-700' : 'bg-gray-200'}`}></div>
|
<div className={`w-6 h-3 rounded-sm ${isDarkMode ? 'bg-gray-700' : 'bg-gray-200'}`}></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
|||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className={`
|
className={`
|
||||||
fixed min-w-[160px] max-w-[220px]
|
fixed min-w-[160px] max-w-[220px]
|
||||||
rounded border backdrop-blur-sm z-[9999]
|
rounded border backdrop-blur-xs z-9999
|
||||||
${isDarkMode
|
${isDarkMode
|
||||||
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
|
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
|
||||||
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
|
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
|
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
|
||||||
@import url("./styles/customOverrides.css");
|
@import url('./styles/customOverrides.css');
|
||||||
@import url("./styles/task-management.css");
|
@import url('./styles/task-management.css');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const ProjectViewRoadmap = () => {
|
|||||||
|
|
||||||
<Flex>
|
<Flex>
|
||||||
{/* table */}
|
{/* table */}
|
||||||
<div className="after:content relative h-fit w-full max-w-[500px] after:absolute after:-right-3 after:top-0 after:z-10 after:min-h-full after:w-3 after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent">
|
<div className="after:content relative h-fit w-full max-w-[500px] after:absolute after:-right-3 after:top-0 after:z-10 after:min-h-full after:w-3 after:bg-linear-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent">
|
||||||
<RoadmapTable />
|
<RoadmapTable />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -109,9 +109,9 @@ const RoadmapTable = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Layout styles for table and columns
|
// Layout styles for table and columns
|
||||||
const customHeaderColumnStyles = `border px-2 h-[50px] text-left z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[42px] after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent ${themeMode === 'dark' ? 'bg-[#1d1d1d] border-[#303030]' : 'bg-[#fafafa]'}`;
|
const customHeaderColumnStyles = `border px-2 h-[50px] text-left z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[42px] after:w-1.5 after:bg-transparent after:bg-linear-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent ${themeMode === 'dark' ? 'bg-[#1d1d1d] border-[#303030]' : 'bg-[#fafafa]'}`;
|
||||||
|
|
||||||
const customBodyColumnStyles = `border px-2 h-[50px] z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:min-h-[40px] after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent ${themeMode === 'dark' ? 'bg-transparent border-[#303030]' : 'bg-transparent'}`;
|
const customBodyColumnStyles = `border px-2 h-[50px] z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:min-h-[40px] after:w-1.5 after:bg-transparent after:bg-linear-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent ${themeMode === 'dark' ? 'bg-transparent border-[#303030]' : 'bg-transparent'}`;
|
||||||
|
|
||||||
const rowBackgroundStyles =
|
const rowBackgroundStyles =
|
||||||
themeMode === 'dark' ? 'even:bg-[#1b1b1b] odd:bg-[#141414]' : 'even:bg-[#f4f4f4] odd:bg-white';
|
themeMode === 'dark' ? 'even:bg-[#1b1b1b] odd:bg-[#141414]' : 'even:bg-[#f4f4f4] odd:bg-white';
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const RoadmapTaskCell = ({ task, isSubtask = false }: RoadmapTaskCellProps) => {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch(toggleTaskExpansion(id))}
|
onClick={() => dispatch(toggleTaskExpansion(id))}
|
||||||
className="hover flex h-4 w-4 items-center justify-center rounded text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
className="hover flex h-4 w-4 items-center justify-center rounded-sm text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
||||||
>
|
>
|
||||||
{task.isExpanded ? <DownOutlined /> : <RightOutlined />}
|
{task.isExpanded ? <DownOutlined /> : <RightOutlined />}
|
||||||
</button>
|
</button>
|
||||||
@@ -36,7 +36,7 @@ const RoadmapTaskCell = ({ task, isSubtask = false }: RoadmapTaskCellProps) => {
|
|||||||
return !isSubtask ? (
|
return !isSubtask ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch(toggleTaskExpansion(id))}
|
onClick={() => dispatch(toggleTaskExpansion(id))}
|
||||||
className="hover flex h-4 w-4 items-center justify-center rounded text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
className="hover flex h-4 w-4 items-center justify-center rounded-sm text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
||||||
>
|
>
|
||||||
{task.isExpanded ? <DownOutlined /> : <RightOutlined />}
|
{task.isExpanded ? <DownOutlined /> : <RightOutlined />}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ const TaskListTable = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeTask && (
|
{activeTask && (
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-lg rounded border">
|
<div className="bg-white dark:bg-gray-800 shadow-lg rounded-sm border">
|
||||||
<DraggableRow
|
<DraggableRow
|
||||||
task={activeTask}
|
task={activeTask}
|
||||||
visibleColumns={visibleColumns}
|
visibleColumns={visibleColumns}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const TaskListInstantTaskInput = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`border-t border-b-[1px] border-r-[1px]`}
|
className={`border-t border-b border-r`}
|
||||||
style={{ borderColor: token.colorBorderSecondary }}
|
style={{ borderColor: token.colorBorderSecondary }}
|
||||||
>
|
>
|
||||||
{isEdit ? (
|
{isEdit ? (
|
||||||
|
|||||||
@@ -149,10 +149,10 @@ const TaskListTable = ({
|
|||||||
const customBorderColor = themeMode === 'dark' && ' border-[#303030]';
|
const customBorderColor = themeMode === 'dark' && ' border-[#303030]';
|
||||||
|
|
||||||
const customHeaderColumnStyles = (key: string) =>
|
const customHeaderColumnStyles = (key: string) =>
|
||||||
`border px-2 text-left ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && `sticky left-[33px] z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[42px] after:w-1.5 after:bg-transparent ${scrollingTables[tableId] ? 'after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#1d1d1d] border-[#303030]' : 'bg-[#fafafa]'}`;
|
`border px-2 text-left ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && `sticky left-[33px] z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[42px] after:w-1.5 after:bg-transparent ${scrollingTables[tableId] ? 'after:bg-linear-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#1d1d1d] border-[#303030]' : 'bg-[#fafafa]'}`;
|
||||||
|
|
||||||
const customBodyColumnStyles = (key: string) =>
|
const customBodyColumnStyles = (key: string) =>
|
||||||
`border px-2 ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && `sticky left-[33px] z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:min-h-[40px] after:w-1.5 after:bg-transparent ${scrollingTables[tableId] ? 'after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#141414] border-[#303030]' : 'bg-white'}`;
|
`border px-2 ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && `sticky left-[33px] z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:min-h-[40px] after:w-1.5 after:bg-transparent ${scrollingTables[tableId] ? 'after:bg-linear-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#141414] border-[#303030]' : 'bg-white'}`;
|
||||||
|
|
||||||
// function to render the column content based on column key
|
// function to render the column content based on column key
|
||||||
const renderColumnContent = (
|
const renderColumnContent = (
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ const TaskListTableWrapper = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Collapse
|
<Collapse
|
||||||
collapsible="header"
|
collapsible="header"
|
||||||
className="border-l-[4px]"
|
className="border-l-4"
|
||||||
bordered={false}
|
bordered={false}
|
||||||
ghost={true}
|
ghost={true}
|
||||||
expandIcon={() => null}
|
expandIcon={() => null}
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ const StatusGroupTables = ({ group }: { group: ITaskListGroup }) => {
|
|||||||
{/* bulk action container ==> used tailwind to recreate the animation */}
|
{/* bulk action container ==> used tailwind to recreate the animation */}
|
||||||
{createPortal(
|
{createPortal(
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-0 left-1/2 z-20 -translate-x-1/2 ${selectedTaskIdsList.length > 0 ? 'overflow-visible' : 'h-[1px] overflow-hidden'}`}
|
className={`absolute bottom-0 left-1/2 z-20 -translate-x-1/2 ${selectedTaskIdsList.length > 0 ? 'overflow-visible' : 'h-px overflow-hidden'}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`${selectedTaskIdsList.length > 0 ? 'bottom-4' : 'bottom-0'} absolute left-1/2 z-[999] -translate-x-1/2 transition-all duration-300`}
|
className={`${selectedTaskIdsList.length > 0 ? 'bottom-4' : 'bottom-0'} absolute left-1/2 z-999 -translate-x-1/2 transition-all duration-300`}
|
||||||
>
|
>
|
||||||
<BulkTasksActionContainer
|
<BulkTasksActionContainer
|
||||||
selectedTaskIds={selectedTaskIdsList}
|
selectedTaskIds={selectedTaskIdsList}
|
||||||
|
|||||||
@@ -133,10 +133,10 @@ const TaskListTable = ({
|
|||||||
const customBorderColor = themeMode === 'dark' && ' border-[#303030]';
|
const customBorderColor = themeMode === 'dark' && ' border-[#303030]';
|
||||||
|
|
||||||
const customHeaderColumnStyles = (key: string) =>
|
const customHeaderColumnStyles = (key: string) =>
|
||||||
`border px-2 text-left ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && `sticky left-[33px] z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[42px] after:w-1.5 after:bg-transparent ${scrollingTables[tableId] ? 'after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#1d1d1d] border-[#303030]' : 'bg-[#fafafa]'}`;
|
`border px-2 text-left ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && `sticky left-[33px] z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[42px] after:w-1.5 after:bg-transparent ${scrollingTables[tableId] ? 'after:bg-linear-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#1d1d1d] border-[#303030]' : 'bg-[#fafafa]'}`;
|
||||||
|
|
||||||
const customBodyColumnStyles = (key: string) =>
|
const customBodyColumnStyles = (key: string) =>
|
||||||
`border px-2 ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && `sticky left-[33px] z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:min-h-[40px] after:w-1.5 after:bg-transparent ${scrollingTables[tableId] ? 'after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#141414] border-[#303030]' : 'bg-white'}`;
|
`border px-2 ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && `sticky left-[33px] z-10 after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:min-h-[40px] after:w-1.5 after:bg-transparent ${scrollingTables[tableId] ? 'after:bg-linear-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#141414] border-[#303030]' : 'bg-white'}`;
|
||||||
|
|
||||||
// function to render the column content based on column key
|
// function to render the column content based on column key
|
||||||
const renderColumnContent = (
|
const renderColumnContent = (
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ const TaskListTableWrapper = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Collapse
|
<Collapse
|
||||||
collapsible="header"
|
collapsible="header"
|
||||||
className="border-l-[4px]"
|
className="border-l-4"
|
||||||
bordered={false}
|
bordered={false}
|
||||||
ghost={true}
|
ghost={true}
|
||||||
expandIcon={() => null}
|
expandIcon={() => null}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const TaskCell = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleTaskExpansion(taskId)}
|
onClick={() => toggleTaskExpansion(taskId)}
|
||||||
className="hover flex h-4 w-4 items-center justify-center rounded text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
className="hover flex h-4 w-4 items-center justify-center rounded-sm text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
||||||
>
|
>
|
||||||
{expandedTasks.includes(taskId) ? <DownOutlined /> : <RightOutlined />}
|
{expandedTasks.includes(taskId) ? <DownOutlined /> : <RightOutlined />}
|
||||||
</button>
|
</button>
|
||||||
@@ -51,7 +51,7 @@ const TaskCell = ({
|
|||||||
return !isSubTask ? (
|
return !isSubTask ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleTaskExpansion(taskId)}
|
onClick={() => toggleTaskExpansion(taskId)}
|
||||||
className="hover flex h-4 w-4 items-center justify-center rounded text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
className="hover flex h-4 w-4 items-center justify-center rounded-sm text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
||||||
>
|
>
|
||||||
{expandedTasks.includes(taskId) ? <DownOutlined /> : <RightOutlined />}
|
{expandedTasks.includes(taskId) ? <DownOutlined /> : <RightOutlined />}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ const BoardSectionCardContainer = ({
|
|||||||
<Flex
|
<Flex
|
||||||
gap={16}
|
gap={16}
|
||||||
align="flex-start"
|
align="flex-start"
|
||||||
className="max-w-screen max-h-[620px] min-h-[620px] overflow-x-scroll p-[1px]"
|
className="max-w-screen max-h-[620px] min-h-[620px] overflow-x-scroll p-px"
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={datasource?.map((section: any) => section.id)}
|
items={datasource?.map((section: any) => section.id)}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ const BoardCreateSubtaskCard = ({
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
|
className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline-solid`}
|
||||||
onBlur={handleCancelNewCard}
|
onBlur={handleCancelNewCard}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ const BoardViewCreateTaskCard = ({
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
|
className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline-solid`}
|
||||||
onBlur={handleCancelNewCard}
|
onBlur={handleCancelNewCard}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ const BoardViewTaskCard = ({ task, sectionId }: IBoardViewTaskCardProps) => {
|
|||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
className={`group outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline board-task-card`}
|
className={`group outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline-solid board-task-card`}
|
||||||
data-id={task.id}
|
data-id={task.id}
|
||||||
data-dragging={isDragging ? "true" : "false"}
|
data-dragging={isDragging ? "true" : "false"}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ const TaskByMembersTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="memberList-container min-h-0 max-w-full overflow-x-auto">
|
<div className="memberList-container min-h-0 max-w-full overflow-x-auto">
|
||||||
<table className="w-full min-w-max border-collapse rounded">
|
<table className="w-full min-w-max border-collapse rounded-sm">
|
||||||
<thead
|
<thead
|
||||||
style={{
|
style={{
|
||||||
height: 42,
|
height: 42,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const TaskListTaskCell = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggleExpansion(taskId)}
|
onClick={() => handleToggleExpansion(taskId)}
|
||||||
className="hover flex h-4 w-4 items-center justify-center rounded text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
className="hover flex h-4 w-4 items-center justify-center rounded-sm text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
||||||
>
|
>
|
||||||
{task.show_sub_tasks ? <DownOutlined /> : <RightOutlined />}
|
{task.show_sub_tasks ? <DownOutlined /> : <RightOutlined />}
|
||||||
</button>
|
</button>
|
||||||
@@ -90,7 +90,7 @@ const TaskListTaskCell = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggleExpansion(taskId)}
|
onClick={() => handleToggleExpansion(taskId)}
|
||||||
className="hover flex h-4 w-4 items-center justify-center rounded text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
className="hover flex h-4 w-4 items-center justify-center rounded-sm text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54]"
|
||||||
>
|
>
|
||||||
{task.show_sub_tasks ? <DownOutlined /> : <RightOutlined />}
|
{task.show_sub_tasks ? <DownOutlined /> : <RightOutlined />}
|
||||||
</button>
|
</button>
|
||||||
@@ -100,7 +100,7 @@ const TaskListTaskCell = ({
|
|||||||
return !isSubTask ? (
|
return !isSubTask ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggleExpansion(taskId)}
|
onClick={() => handleToggleExpansion(taskId)}
|
||||||
className="hover flex h-4 w-4 items-center justify-center rounded text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54] open-task-button"
|
className="hover flex h-4 w-4 items-center justify-center rounded-sm text-[12px] hover:border hover:border-[#5587f5] hover:bg-[#d0eefa54] open-task-button"
|
||||||
>
|
>
|
||||||
{task.show_sub_tasks ? <DownOutlined /> : <RightOutlined />}
|
{task.show_sub_tasks ? <DownOutlined /> : <RightOutlined />}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -178,13 +178,13 @@ const CustomCell = React.memo(({
|
|||||||
switch (column.key) {
|
switch (column.key) {
|
||||||
case 'STATUS':
|
case 'STATUS':
|
||||||
return (
|
return (
|
||||||
<div className="px-2 py-1 text-xs rounded bg-gray-100 text-gray-600">
|
<div className="px-2 py-1 text-xs rounded-sm bg-gray-100 text-gray-600">
|
||||||
{task.status_name || task.status || 'To Do'}
|
{task.status_name || task.status || 'To Do'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'PRIORITY':
|
case 'PRIORITY':
|
||||||
return (
|
return (
|
||||||
<div className="px-2 py-1 text-xs rounded bg-gray-100 text-gray-600">
|
<div className="px-2 py-1 text-xs rounded-sm bg-gray-100 text-gray-600">
|
||||||
{task.priority_name || task.priority || 'Medium'}
|
{task.priority_name || task.priority || 'Medium'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -213,13 +213,13 @@ const CustomCell = React.memo(({
|
|||||||
switch (column.key) {
|
switch (column.key) {
|
||||||
case 'STATUS':
|
case 'STATUS':
|
||||||
return (
|
return (
|
||||||
<div className="px-2 py-1 text-xs rounded bg-red-100 text-red-600">
|
<div className="px-2 py-1 text-xs rounded-sm bg-red-100 text-red-600">
|
||||||
{task.status_name || task.status || 'Error'}
|
{task.status_name || task.status || 'Error'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'PRIORITY':
|
case 'PRIORITY':
|
||||||
return (
|
return (
|
||||||
<div className="px-2 py-1 text-xs rounded bg-red-100 text-red-600">
|
<div className="px-2 py-1 text-xs rounded-sm bg-red-100 text-red-600">
|
||||||
{task.priority_name || task.priority || 'Error'}
|
{task.priority_name || task.priority || 'Error'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1483,7 +1483,7 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
|
|||||||
case 'TASK':
|
case 'TASK':
|
||||||
return `sticky left-[48px] z-10 after:content after:absolute after:top-0 after:-right-1 after:h-full after:-z-10 after:w-1.5 after:bg-transparent ${
|
return `sticky left-[48px] z-10 after:content after:absolute after:top-0 after:-right-1 after:h-full after:-z-10 after:w-1.5 after:bg-transparent ${
|
||||||
scrollingTables[tableId]
|
scrollingTables[tableId]
|
||||||
? 'after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent'
|
? 'after:bg-linear-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent'
|
||||||
: ''
|
: ''
|
||||||
}`;
|
}`;
|
||||||
default:
|
default:
|
||||||
@@ -1886,7 +1886,7 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
|
|||||||
dropAnimation={null} // Disable drop animation
|
dropAnimation={null} // Disable drop animation
|
||||||
>
|
>
|
||||||
{dragActiveId ? (
|
{dragActiveId ? (
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-lg rounded border p-2 opacity-90">
|
<div className="bg-white dark:bg-gray-800 shadow-lg rounded-sm border p-2 opacity-90">
|
||||||
<span className="text-sm font-medium">Moving task...</span>
|
<span className="text-sm font-medium">Moving task...</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -409,7 +409,7 @@
|
|||||||
// i >= startOffset &&
|
// i >= startOffset &&
|
||||||
// i < startOffset + projectDuration
|
// i < startOffset + projectDuration
|
||||||
// ? 'empty-cell-hide'
|
// ? 'empty-cell-hide'
|
||||||
// : `empty-cell rounded-sm outline-1 hover:outline ${themeMode === 'dark' ? 'outline-white/25' : 'outline-black/25'}`
|
// : `empty-cell rounded-xs outline-1 hover:outline-solid ${themeMode === 'dark' ? 'outline-white/25' : 'outline-black/25'}`
|
||||||
// }
|
// }
|
||||||
// key={i}
|
// key={i}
|
||||||
// style={{
|
// style={{
|
||||||
@@ -687,7 +687,7 @@ export default Grant;
|
|||||||
// style={{
|
// style={{
|
||||||
// background: themeWiseColor('#fff', '#141414', themeMode),
|
// background: themeWiseColor('#fff', '#141414', themeMode),
|
||||||
// }}
|
// }}
|
||||||
// className={`after:content relative z-10 after:absolute after:-right-1 after:top-0 after:-z-10 after:h-full after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent`}
|
// className={`after:content relative z-10 after:absolute after:-right-1 after:top-0 after:-z-10 after:h-full after:w-1.5 after:bg-transparent after:bg-linear-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent`}
|
||||||
// >
|
// >
|
||||||
// <GranttMembersTable
|
// <GranttMembersTable
|
||||||
// members={members}
|
// members={members}
|
||||||
@@ -874,7 +874,7 @@ export default Grant;
|
|||||||
// i >= startOffset &&
|
// i >= startOffset &&
|
||||||
// i < startOffset + projectDuration
|
// i < startOffset + projectDuration
|
||||||
// ? 'empty-cell-hide'
|
// ? 'empty-cell-hide'
|
||||||
// : `empty-cell rounded-sm outline-1 hover:outline ${themeMode === 'dark' ? 'outline-white/10' : 'outline-black/10'}`
|
// : `empty-cell rounded-xs outline-1 hover:outline-solid ${themeMode === 'dark' ? 'outline-white/10' : 'outline-black/10'}`
|
||||||
// }
|
// }
|
||||||
// key={i}
|
// key={i}
|
||||||
// style={{
|
// style={{
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
content: [
|
||||||
|
"./src/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"./public/index.html"
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: [
|
||||||
|
'-apple-system',
|
||||||
|
'BlinkMacSystemFont',
|
||||||
|
'"Inter"',
|
||||||
|
'Roboto',
|
||||||
|
'"Helvetica Neue"',
|
||||||
|
'Arial',
|
||||||
|
'"Noto Sans"',
|
||||||
|
'sans-serif',
|
||||||
|
'"Apple Color Emoji"',
|
||||||
|
'"Segoe UI Emoji"',
|
||||||
|
'"Segoe UI Symbol"',
|
||||||
|
'"Noto Color Emoji"'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
darkMode: 'class',
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user