refactor(task-list): update task list components and remove deprecated files
- Replaced StatusGroupTables with TaskGroupList in multiple components to streamline task grouping functionality. - Updated imports to reflect new component structure and paths. - Removed obsolete task list components and styles to clean up the codebase. - Enhanced task list filters for improved user experience and consistency across the application.
This commit is contained in:
34
worklenz-backend/package-lock.json
generated
34
worklenz-backend/package-lock.json
generated
@@ -3528,7 +3528,6 @@
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
@@ -3546,7 +3545,6 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -3559,7 +3557,6 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -3572,14 +3569,12 @@
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
@@ -3597,7 +3592,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
@@ -3613,7 +3607,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
@@ -7649,7 +7642,6 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -8043,7 +8035,6 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
@@ -9104,7 +9095,6 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
@@ -9121,7 +9111,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -9882,8 +9871,7 @@
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.0",
|
||||
@@ -10024,7 +10012,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
@@ -12688,7 +12675,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
@@ -12844,7 +12830,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -12858,7 +12843,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
@@ -12875,7 +12859,6 @@
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@@ -12885,7 +12868,6 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -13857,7 +13839,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
|
||||
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^11.0.0",
|
||||
@@ -13877,7 +13858,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -13887,7 +13867,6 @@
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz",
|
||||
"integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
@@ -13911,7 +13890,6 @@
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
|
||||
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@@ -13927,7 +13905,6 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -14225,7 +14202,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
@@ -14237,7 +14213,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -14590,7 +14565,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -14617,7 +14591,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -15594,7 +15567,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
@@ -15684,7 +15656,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -15702,7 +15673,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -15718,7 +15688,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -15731,7 +15700,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import StatusGroupTables from '@/pages/projects/project-view-1/taskList/statusTables/StatusGroupTables';
|
||||
import TaskGroupList from '@/pages/projects/projectView/taskList/groupTables/TaskGroupList';
|
||||
import { TaskType } from '@/types/task.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import GroupByFilterDropdown from '@/pages/projects/project-view-1/taskList/taskListFilters/GroupByFilterDropdown';
|
||||
import GroupByFilterDropdown from '@/components/project-task-filters/filter-dropdowns/group-by-filter-dropdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
|
||||
const WithStartAndEndDates = () => {
|
||||
const dataSource: TaskType[] = useAppSelector(state => state.taskReducer.tasks);
|
||||
const dataSource: ITaskListGroup[] = useAppSelector(state => state.taskReducer.taskGroups);
|
||||
const groupBy = useAppSelector(state => state.taskReducer.groupBy);
|
||||
const { t } = useTranslation('schedule');
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
@@ -57,7 +59,7 @@ const WithStartAndEndDates = () => {
|
||||
<GroupByFilterDropdown />
|
||||
</div>
|
||||
<div>
|
||||
<StatusGroupTables datasource={dataSource} />
|
||||
<TaskGroupList taskGroups={dataSource} groupBy={groupBy} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,14 @@ import { TaskType } from '@/types/task.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import GroupByFilterDropdown from '@/components/project-task-filters/filter-dropdowns/group-by-filter-dropdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import StatusGroupTables from '@/pages/projects/project-view-1/taskList/statusTables/StatusGroupTables';
|
||||
import TaskGroupList from '@/pages/projects/projectView/taskList/groupTables/TaskGroupList';
|
||||
import PriorityGroupTables from '@/pages/projects/projectView/taskList/groupTables/priorityTables/PriorityGroupTables';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
|
||||
const WithStartAndEndDates = () => {
|
||||
const dataSource: ITaskListGroup[] = useAppSelector(state => state.taskReducer.taskGroups);
|
||||
const groupBy = useAppSelector(state => state.taskReducer.groupBy);
|
||||
const { t } = useTranslation('schedule');
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
@@ -60,10 +61,7 @@ const WithStartAndEndDates = () => {
|
||||
<GroupByFilterDropdown />
|
||||
</div>
|
||||
<div>
|
||||
{dataSource.map(group => (
|
||||
<StatusGroupTables key={group.id} group={group} />
|
||||
))}
|
||||
{/* <PriorityGroupTables datasource={dataSource} /> */}
|
||||
<TaskGroupList taskGroups={dataSource} groupBy={groupBy} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Divider, Empty, Flex, Popover, Typography } from 'antd';
|
||||
import { PlayCircleFilled } from '@ant-design/icons';
|
||||
import { colors } from '@/styles/colors';
|
||||
import CustomAvatar from '@components/CustomAvatar';
|
||||
import { mockTimeLogs } from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/mockTimeLogs';
|
||||
import { mockTimeLogs } from '@/shared/mockTimeLogs';
|
||||
|
||||
type TaskListTimeTrackerCellProps = {
|
||||
taskId: string | null;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
|
||||
import ProjectViewInsights from '@/pages/projects/projectView/insights/project-view-insights';
|
||||
import ProjectViewFiles from '@/pages/projects/projectView/files/project-view-files';
|
||||
import ProjectViewMembers from '@/pages/projects/projectView/members/project-view-members';
|
||||
import ProjectViewUpdates from '@/pages/projects/project-view-1/updates/project-view-updates';
|
||||
import ProjectViewUpdates from '@/pages/projects/projectView/updates/ProjectViewUpdates';
|
||||
import ProjectViewTaskList from '@/pages/projects/projectView/taskList/project-view-task-list';
|
||||
import ProjectViewBoard from '@/pages/projects/projectView/board/project-view-board';
|
||||
import ProjectViewFinance from '@/pages/projects/projectView/finance/project-view-finance';
|
||||
@@ -32,18 +32,6 @@ export const tabItems: TabItems[] = [
|
||||
isPinned: true,
|
||||
element: React.createElement(ProjectViewBoard),
|
||||
},
|
||||
// {
|
||||
// index: 2,
|
||||
// key: 'workload',
|
||||
// label: 'Workload',
|
||||
// element: React.createElement(ProjectViewWorkload),
|
||||
// },
|
||||
// {
|
||||
// index: 3,
|
||||
// key: 'roadmap',
|
||||
// label: 'Roadmap',
|
||||
// element: React.createElement(ProjectViewRoadmap),
|
||||
// },
|
||||
{
|
||||
index: 4,
|
||||
key: 'project-insights-member-overview',
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { FC } from 'react';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
export type CardType = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const Card: FC<CardType> = ({ id, title }) => {
|
||||
// useSortableに指定するidは一意になるよう設定する必要があります。s
|
||||
const { attributes, listeners, setNodeRef, transform } = useSortable({
|
||||
id: id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
margin: '10px',
|
||||
opacity: 1,
|
||||
color: '#333',
|
||||
background: 'white',
|
||||
padding: '10px',
|
||||
transform: CSS.Transform.toString(transform),
|
||||
};
|
||||
|
||||
return (
|
||||
// attributes、listenersはDOMイベントを検知するために利用します。
|
||||
// listenersを任意の領域に付与することで、ドラッグするためのハンドルを作ることもできます。
|
||||
<div ref={setNodeRef} {...attributes} {...listeners} style={style}>
|
||||
<div id={id}>
|
||||
<p>{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Card;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { FC } from 'react';
|
||||
import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import Card, { CardType } from './card';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
|
||||
export type ColumnType = {
|
||||
id: string;
|
||||
title: string;
|
||||
cards: IProjectTask[];
|
||||
};
|
||||
|
||||
const Column: FC<ColumnType> = ({ id, title, cards }) => {
|
||||
const { setNodeRef } = useDroppable({ id: id });
|
||||
return (
|
||||
// ソートを行うためのContextです。
|
||||
// strategyは4つほど存在しますが、今回は縦・横移動可能なリストを作るためrectSortingStrategyを採用
|
||||
<SortableContext id={id} items={cards} strategy={rectSortingStrategy}>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
width: '200px',
|
||||
background: 'rgba(245,247,249,1.00)',
|
||||
marginRight: '10px',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
padding: '5px 20px',
|
||||
textAlign: 'left',
|
||||
fontWeight: '500',
|
||||
color: '#575757',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
{cards.map(card => (
|
||||
<Card key={card.id} id={card.id} title={card.title}></Card>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default Column;
|
||||
@@ -1,141 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import TaskListFilters from '../taskList/taskListFilters/TaskListFilters';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { toggleDrawer } from '@/features/projects/status/StatusSlice';
|
||||
import KanbanGroup from '@/components/board/kanban-group/kanban-group';
|
||||
|
||||
const ProjectViewBoard: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { taskGroups, loadingGroups } = useAppSelector(state => state.taskReducer);
|
||||
const { statusCategories } = useAppSelector(state => state.taskStatusReducer);
|
||||
const groupBy = useAppSelector(state => state.groupByFilterDropdownReducer.groupBy);
|
||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('projectId', projectId);
|
||||
// if (projectId) {
|
||||
// const config: ITaskListConfigV2 = {
|
||||
// id: projectId,
|
||||
// field: 'id',
|
||||
// order: 'desc',
|
||||
// search: '',
|
||||
// statuses: '',
|
||||
// members: '',
|
||||
// projects: '',
|
||||
// isSubtasksInclude: false,
|
||||
// };
|
||||
// dispatch(fetchTaskGroups(config) as any);
|
||||
// }
|
||||
// if (!statusCategories.length) {
|
||||
// dispatch(fetchStatusesCategories() as any);
|
||||
// }
|
||||
}, [dispatch, projectId, groupBy]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const handleDragOver = (event: DragOverEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over) return;
|
||||
|
||||
const activeTask = active.data.current?.task;
|
||||
const overId = over.id;
|
||||
|
||||
// Find which group the task is being dragged over
|
||||
const targetGroup = taskGroups.find(
|
||||
group => group.id === overId || group.tasks.some(task => task.id === overId)
|
||||
);
|
||||
|
||||
if (targetGroup && activeTask) {
|
||||
// Here you would dispatch an action to update the task's status
|
||||
// For example:
|
||||
// dispatch(updateTaskStatus({ taskId: activeTask.id, newStatus: targetGroup.id }));
|
||||
console.log('Moving task', activeTask.id, 'to group', targetGroup.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over) return;
|
||||
|
||||
const activeTask = active.data.current?.task;
|
||||
const overId = over.id;
|
||||
|
||||
// Similar to handleDragOver, but this is where you'd make the final update
|
||||
const targetGroup = taskGroups.find(
|
||||
group => group.id === overId || group.tasks.some(task => task.id === overId)
|
||||
);
|
||||
|
||||
if (targetGroup && activeTask) {
|
||||
// Make the final update to your backend/state
|
||||
console.log('Final move of task', activeTask.id, 'to group', targetGroup.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<TaskListFilters position={'board'} />
|
||||
|
||||
<Skeleton active loading={loadingGroups}>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0 12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
marginTop: '14px',
|
||||
}}
|
||||
>
|
||||
<DndContext sensors={sensors} onDragOver={handleDragOver} onDragEnd={handleDragEnd}>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: '6px',
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
overflowX: 'scroll',
|
||||
paddingBottom: '10px',
|
||||
}}
|
||||
>
|
||||
{taskGroups.map(group => (
|
||||
<KanbanGroup
|
||||
key={group.id}
|
||||
title={group.name}
|
||||
tasks={group.tasks}
|
||||
id={group.id}
|
||||
color={group.color_code}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => dispatch(toggleDrawer())}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectViewBoard;
|
||||
@@ -1,116 +0,0 @@
|
||||
:root {
|
||||
--odd-row-color: #fff;
|
||||
--even-row-color: #4e4e4e10;
|
||||
--text-color: #181818;
|
||||
--border: 1px solid #e0e0e0;
|
||||
--stroke: #e0e0e0;
|
||||
|
||||
--calender-header-bg: #fafafa;
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
--odd-row-color: #141414;
|
||||
--even-row-color: #4e4e4e10;
|
||||
--text-color: #fff;
|
||||
--border: 1px solid #505050;
|
||||
--stroke: #505050;
|
||||
|
||||
--calender-header-bg: #1d1d1d;
|
||||
}
|
||||
|
||||
/* scroll bar size override */
|
||||
._2k9Ys {
|
||||
scrollbar-width: unset;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
/* task details side even rows */
|
||||
._34SS0:nth-of-type(even) {
|
||||
background-color: var(--even-row-color);
|
||||
}
|
||||
|
||||
/* task details side header and body */
|
||||
._3_ygE {
|
||||
border-top: var(--border);
|
||||
border-left: var(--border);
|
||||
position: relative;
|
||||
}
|
||||
._2B2zv {
|
||||
border-bottom: var(--border);
|
||||
border-left: var(--border);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
._3ZbQT {
|
||||
border: none;
|
||||
}
|
||||
|
||||
._3_ygE::after,
|
||||
._2B2zv::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -25px;
|
||||
width: 30px;
|
||||
height: 100%;
|
||||
box-shadow: inset 10px 0 8px -8px #00000026;
|
||||
}
|
||||
|
||||
/* ._3lLk3:nth-child(1),
|
||||
._WuQ0f:nth-child(1) {
|
||||
min-width: 300px !important;
|
||||
max-width: 300px !important;
|
||||
}
|
||||
|
||||
._2eZzQ,
|
||||
._WuQ0f:nth-child(3),
|
||||
._WuQ0f:last-child,
|
||||
._3lLk3:nth-child(2),
|
||||
._3lLk3:nth-child(3) {
|
||||
display: none;
|
||||
} */
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
/* calender side header */
|
||||
._35nLX {
|
||||
fill: var(--calender-header-bg);
|
||||
stroke: var(--stroke);
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
/* calender side header texts */
|
||||
._9w8d5,
|
||||
._2q1Kt {
|
||||
fill: var(--text-color);
|
||||
}
|
||||
|
||||
/* calender side odd rows */
|
||||
._2dZTy:nth-child(odd) {
|
||||
fill: var(--odd-row-color);
|
||||
}
|
||||
/* calender side even rows */
|
||||
._2dZTy:nth-child(even) {
|
||||
fill: var(--even-row-color);
|
||||
}
|
||||
|
||||
/* calender side body row lines */
|
||||
._3rUKi {
|
||||
stroke: var(--stroke);
|
||||
stroke-width: 0.3px;
|
||||
}
|
||||
|
||||
/* calender side body ticks */
|
||||
._RuwuK {
|
||||
stroke: var(--stroke);
|
||||
stroke-width: 0.3px;
|
||||
}
|
||||
|
||||
/* calender side header ticks */
|
||||
._1rLuZ {
|
||||
stroke: var(--stroke);
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.roadmap-table .ant-table-thead .ant-table-cell {
|
||||
height: 50px;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { ViewMode } from 'gantt-task-react';
|
||||
import 'gantt-task-react/dist/index.css';
|
||||
import './project-view-roadmap.css';
|
||||
import { Flex } from 'antd';
|
||||
import { useAppSelector } from '../../../../hooks/useAppSelector';
|
||||
import { TimeFilter } from './time-filter';
|
||||
import RoadmapTable from './roadmap-table/roadmap-table';
|
||||
import RoadmapGrantChart from './roadmap-grant-chart';
|
||||
|
||||
const ProjectViewRoadmap = () => {
|
||||
const [view, setView] = useState<ViewMode>(ViewMode.Day);
|
||||
|
||||
// get theme details
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
return (
|
||||
<Flex vertical className={`${themeMode === 'dark' ? 'dark-theme' : ''}`}>
|
||||
{/* time filter */}
|
||||
<TimeFilter onViewModeChange={viewMode => setView(viewMode)} />
|
||||
|
||||
<Flex>
|
||||
{/* 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">
|
||||
<RoadmapTable />
|
||||
</div>
|
||||
|
||||
{/* gantt Chart */}
|
||||
<RoadmapGrantChart view={view} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectViewRoadmap;
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Gantt, Task, ViewMode } from 'gantt-task-react';
|
||||
import React from 'react';
|
||||
import { colors } from '../../../../styles/colors';
|
||||
import {
|
||||
NewTaskType,
|
||||
updateTaskDate,
|
||||
updateTaskProgress,
|
||||
} from '../../../../features/roadmap/roadmap-slice';
|
||||
import { useAppSelector } from '../../../../hooks/useAppSelector';
|
||||
import { useAppDispatch } from '../../../../hooks/useAppDispatch';
|
||||
import { toggleTaskDrawer } from '../../../../features/tasks/tasks.slice';
|
||||
|
||||
type RoadmapGrantChartProps = {
|
||||
view: ViewMode;
|
||||
};
|
||||
|
||||
const RoadmapGrantChart = ({ view }: RoadmapGrantChartProps) => {
|
||||
// get task list from roadmap slice
|
||||
const tasks = useAppSelector(state => state.roadmapReducer.tasksList);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// column widths for each view mods
|
||||
let columnWidth = 60;
|
||||
if (view === ViewMode.Year) {
|
||||
columnWidth = 350;
|
||||
} else if (view === ViewMode.Month) {
|
||||
columnWidth = 300;
|
||||
} else if (view === ViewMode.Week) {
|
||||
columnWidth = 250;
|
||||
}
|
||||
|
||||
// function to handle double click
|
||||
const handleDoubleClick = () => {
|
||||
dispatch(toggleTaskDrawer());
|
||||
};
|
||||
|
||||
// function to handle date change
|
||||
const handleTaskDateChange = (task: Task) => {
|
||||
dispatch(updateTaskDate({ taskId: task.id, start: task.start, end: task.end }));
|
||||
};
|
||||
|
||||
// function to handle progress change
|
||||
const handleTaskProgressChange = (task: Task) => {
|
||||
dispatch(updateTaskProgress({ taskId: task.id, progress: task.progress }));
|
||||
};
|
||||
|
||||
// function to convert the tasklist comming form roadmap slice which has NewTaskType converted to Task type which is the default type of the tasks list in the grant chart
|
||||
const flattenTasks = (tasks: NewTaskType[]): Task[] => {
|
||||
const flattened: Task[] = [];
|
||||
|
||||
const addTaskAndSubTasks = (task: NewTaskType, parentExpanded: boolean) => {
|
||||
// add the task to the flattened list if its parent is expanded or it is a top-level task
|
||||
if (parentExpanded) {
|
||||
const { subTasks, isExpanded, ...rest } = task; // destructure to exclude properties not in Task type
|
||||
flattened.push(rest as Task);
|
||||
|
||||
// recursively add subtasks if this task is expanded
|
||||
if (subTasks && isExpanded) {
|
||||
subTasks.forEach(subTask => addTaskAndSubTasks(subTask as NewTaskType, true));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// top-level tasks are always visible, start with parentExpanded = true
|
||||
tasks.forEach(task => addTaskAndSubTasks(task, true));
|
||||
|
||||
return flattened;
|
||||
};
|
||||
|
||||
const flattenedTasksList = flattenTasks(tasks);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[900px] overflow-x-auto">
|
||||
<Gantt
|
||||
tasks={flattenedTasksList}
|
||||
viewMode={view}
|
||||
onDateChange={handleTaskDateChange}
|
||||
onProgressChange={handleTaskProgressChange}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
listCellWidth={''}
|
||||
columnWidth={columnWidth}
|
||||
todayColor={`rgba(64, 150, 255, 0.2)`}
|
||||
projectProgressColor={colors.limeGreen}
|
||||
projectBackgroundColor={colors.lightGreen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoadmapGrantChart;
|
||||
@@ -1,189 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DatePicker, Typography } from 'antd';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { NewTaskType, updateTaskDate } from '@features/roadmap/roadmap-slice';
|
||||
import { colors } from '@/styles/colors';
|
||||
import RoadmapTaskCell from './roadmap-task-cell';
|
||||
|
||||
const RoadmapTable = () => {
|
||||
// Get task list and expanded tasks from roadmap slice
|
||||
const tasks = useAppSelector(state => state.roadmapReducer.tasksList);
|
||||
|
||||
// Get theme data from theme slice
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// function to handle date changes
|
||||
const handleDateChange = (taskId: string, dateType: 'start' | 'end', date: Dayjs) => {
|
||||
const updatedDate = date.toDate();
|
||||
|
||||
dispatch(
|
||||
updateTaskDate({
|
||||
taskId,
|
||||
start: dateType === 'start' ? updatedDate : new Date(),
|
||||
end: dateType === 'end' ? updatedDate : new Date(),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Adjusted column type with a string or ReactNode for the title
|
||||
const columns: { key: string; title: React.ReactNode; width: number }[] = [
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Task Name',
|
||||
width: 240,
|
||||
},
|
||||
{
|
||||
key: 'start',
|
||||
title: 'Start Date',
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
key: 'end',
|
||||
title: 'End Date',
|
||||
width: 130,
|
||||
},
|
||||
];
|
||||
|
||||
// Function to render the column content based on column key
|
||||
const renderColumnContent = (
|
||||
columnKey: string,
|
||||
task: NewTaskType,
|
||||
isSubtask: boolean = false
|
||||
) => {
|
||||
switch (columnKey) {
|
||||
case 'name':
|
||||
return <RoadmapTaskCell task={task} isSubtask={isSubtask} />;
|
||||
case 'start':
|
||||
const startDayjs = task.start ? dayjs(task.start) : null;
|
||||
return (
|
||||
<DatePicker
|
||||
placeholder="Set Start Date"
|
||||
defaultValue={startDayjs}
|
||||
format={'MMM DD, YYYY'}
|
||||
suffixIcon={null}
|
||||
disabled={task.type === 'project'}
|
||||
onChange={date => handleDateChange(task.id, 'end', date)}
|
||||
style={{
|
||||
backgroundColor: colors.transparent,
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case 'end':
|
||||
const endDayjs = task.end ? dayjs(task.end) : null;
|
||||
return (
|
||||
<DatePicker
|
||||
placeholder="Set End Date"
|
||||
defaultValue={endDayjs}
|
||||
format={'MMM DD, YYYY'}
|
||||
suffixIcon={null}
|
||||
disabled={task.type === 'project'}
|
||||
onChange={date => handleDateChange(task.id, 'end', date)}
|
||||
style={{
|
||||
backgroundColor: colors.transparent,
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const dataSource = tasks.map(task => ({
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
start: task.start,
|
||||
end: task.end,
|
||||
type: task.type,
|
||||
progress: task.progress,
|
||||
subTasks: task.subTasks,
|
||||
isExpanded: task.isExpanded,
|
||||
}));
|
||||
|
||||
// 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 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 rowBackgroundStyles =
|
||||
themeMode === 'dark' ? 'even:bg-[#1b1b1b] odd:bg-[#141414]' : 'even:bg-[#f4f4f4] odd:bg-white';
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-[1000px]">
|
||||
<table className={`rounded-2 w-full min-w-max border-collapse`}>
|
||||
<thead className="h-[50px]">
|
||||
<tr>
|
||||
{/* table header */}
|
||||
{columns.map(column => (
|
||||
<th
|
||||
key={column.key}
|
||||
className={`${customHeaderColumnStyles}`}
|
||||
style={{ width: column.width, fontWeight: 500 }}
|
||||
>
|
||||
<Typography.Text style={{ fontWeight: 500 }}>{column.title}</Typography.Text>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dataSource.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center">
|
||||
No tasks available
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
dataSource.map(task => (
|
||||
<React.Fragment key={task.id}>
|
||||
<tr
|
||||
key={task.id}
|
||||
className={`group cursor-pointer ${dataSource.length === 0 ? 'h-0' : 'h-[50px]'} ${rowBackgroundStyles}`}
|
||||
>
|
||||
{columns.map(column => (
|
||||
<td
|
||||
key={column.key}
|
||||
className={`${customBodyColumnStyles}`}
|
||||
style={{
|
||||
width: column.width,
|
||||
}}
|
||||
>
|
||||
{renderColumnContent(column.key, task)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
{/* subtasks */}
|
||||
{task.isExpanded &&
|
||||
task?.subTasks?.map(subtask => (
|
||||
<tr key={`subtask-${subtask.id}`} className={`h-[50px] ${rowBackgroundStyles}`}>
|
||||
{columns.map(column => (
|
||||
<td
|
||||
key={column.key}
|
||||
className={`${customBodyColumnStyles}`}
|
||||
style={{
|
||||
width: column.width,
|
||||
}}
|
||||
>
|
||||
{renderColumnContent(column.key, subtask, true)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoadmapTable;
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Flex, Typography, Button, Tooltip } from 'antd';
|
||||
import {
|
||||
DoubleRightOutlined,
|
||||
DownOutlined,
|
||||
RightOutlined,
|
||||
ExpandAltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { NewTaskType, toggleTaskExpansion } from '@features/roadmap/roadmap-slice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { toggleTaskDrawer } from '@features/tasks/taskSlice';
|
||||
import { colors } from '@/styles/colors';
|
||||
|
||||
type RoadmapTaskCellProps = {
|
||||
task: NewTaskType;
|
||||
isSubtask?: boolean;
|
||||
};
|
||||
|
||||
const RoadmapTaskCell = ({ task, isSubtask = false }: RoadmapTaskCellProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// render the toggle arrow icon for tasks with subtasks
|
||||
const renderToggleButtonForHasSubTasks = (id: string, hasSubtasks: boolean) => {
|
||||
if (!hasSubtasks) return null;
|
||||
return (
|
||||
<button
|
||||
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]"
|
||||
>
|
||||
{task.isExpanded ? <DownOutlined /> : <RightOutlined />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// show expand button on hover for tasks without subtasks
|
||||
const renderToggleButtonForNonSubtasks = (id: string, isSubtask: boolean) => {
|
||||
return !isSubtask ? (
|
||||
<button
|
||||
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]"
|
||||
>
|
||||
{task.isExpanded ? <DownOutlined /> : <RightOutlined />}
|
||||
</button>
|
||||
) : (
|
||||
<div className="h-4 w-4"></div>
|
||||
);
|
||||
};
|
||||
|
||||
// render the double arrow icon and count label for tasks with subtasks
|
||||
const renderSubtasksCountLabel = (id: string, isSubtask: boolean, subTasksCount: number) => {
|
||||
return (
|
||||
!isSubtask && (
|
||||
<Button
|
||||
onClick={() => dispatch(toggleTaskExpansion(id))}
|
||||
size="small"
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
paddingInline: 4,
|
||||
alignItems: 'center',
|
||||
justifyItems: 'center',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<Typography.Text style={{ fontSize: 12, lineHeight: 1 }}>{subTasksCount}</Typography.Text>
|
||||
<DoubleRightOutlined style={{ fontSize: 10 }} />
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex gap={8} align="center" justify="space-between">
|
||||
<Flex gap={8} align="center">
|
||||
{!!task?.subTasks?.length ? (
|
||||
renderToggleButtonForHasSubTasks(task.id, !!task?.subTasks?.length)
|
||||
) : (
|
||||
<div className="h-4 w-4 opacity-0 group-hover:opacity-100 group-focus:opacity-100">
|
||||
{renderToggleButtonForNonSubtasks(task.id, isSubtask)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSubtask && <DoubleRightOutlined style={{ fontSize: 12 }} />}
|
||||
|
||||
<Tooltip title={task.name}>
|
||||
<Typography.Text ellipsis={{ expanded: false }} style={{ maxWidth: 100 }}>
|
||||
{task.name}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
|
||||
{renderSubtasksCountLabel(task.id, isSubtask, task?.subTasks?.length || 0)}
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ExpandAltOutlined />}
|
||||
onClick={() => {
|
||||
dispatch(toggleTaskDrawer());
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: colors.transparent,
|
||||
padding: 0,
|
||||
height: 'fit-content',
|
||||
}}
|
||||
className="hidden group-hover:block group-focus:block"
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoadmapTaskCell;
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
import 'gantt-task-react/dist/index.css';
|
||||
import { ViewMode } from 'gantt-task-react';
|
||||
import { Flex, Select } from 'antd';
|
||||
type TimeFilterProps = {
|
||||
onViewModeChange: (viewMode: ViewMode) => void;
|
||||
};
|
||||
export const TimeFilter = ({ onViewModeChange }: TimeFilterProps) => {
|
||||
// function to handle time change
|
||||
const handleChange = (value: string) => {
|
||||
switch (value) {
|
||||
case 'hour':
|
||||
return onViewModeChange(ViewMode.Hour);
|
||||
case 'quaterDay':
|
||||
return onViewModeChange(ViewMode.QuarterDay);
|
||||
case 'halfDay':
|
||||
return onViewModeChange(ViewMode.HalfDay);
|
||||
case 'day':
|
||||
return onViewModeChange(ViewMode.Day);
|
||||
case 'week':
|
||||
return onViewModeChange(ViewMode.Week);
|
||||
case 'month':
|
||||
return onViewModeChange(ViewMode.Month);
|
||||
case 'year':
|
||||
return onViewModeChange(ViewMode.Year);
|
||||
default:
|
||||
return onViewModeChange(ViewMode.Day);
|
||||
}
|
||||
};
|
||||
|
||||
const timeFilterItems = [
|
||||
{
|
||||
value: 'hour',
|
||||
label: 'Hour',
|
||||
},
|
||||
{
|
||||
value: 'quaterDay',
|
||||
label: 'Quater Day',
|
||||
},
|
||||
{
|
||||
value: 'halfDay',
|
||||
label: 'Half Day',
|
||||
},
|
||||
{
|
||||
value: 'day',
|
||||
label: 'Day',
|
||||
},
|
||||
{
|
||||
value: 'week',
|
||||
label: 'Week',
|
||||
},
|
||||
{
|
||||
value: 'month',
|
||||
label: 'Month',
|
||||
},
|
||||
{
|
||||
value: 'year',
|
||||
label: 'Year',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex gap={12} align="center" justify="flex-end" style={{ marginBlockEnd: 24 }}>
|
||||
<Select
|
||||
className="ViewModeSelect"
|
||||
style={{ minWidth: 120 }}
|
||||
placeholder="Select View Mode"
|
||||
onChange={handleChange}
|
||||
options={timeFilterItems}
|
||||
defaultValue={'day'}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,142 +0,0 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Checkbox, Flex, Tag, Tooltip } from 'antd';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
|
||||
const TaskListTable = ({
|
||||
taskListGroup,
|
||||
visibleColumns,
|
||||
onTaskSelect,
|
||||
onTaskExpand,
|
||||
}: {
|
||||
taskListGroup: ITaskListGroup;
|
||||
tableId: string;
|
||||
visibleColumns: Array<{ key: string; width: number }>;
|
||||
onTaskSelect?: (taskId: string) => void;
|
||||
onTaskExpand?: (taskId: string) => void;
|
||||
}) => {
|
||||
const [hoverRow, setHoverRow] = useState<string | null>(null);
|
||||
const tableRef = useRef<HTMLDivElement | null>(null);
|
||||
const parentRef = useRef<HTMLDivElement | null>(null);
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
// Memoize all tasks including subtasks for virtualization
|
||||
const flattenedTasks = useMemo(() => {
|
||||
return taskListGroup.tasks.reduce((acc: IProjectTask[], task: IProjectTask) => {
|
||||
acc.push(task);
|
||||
if (task.sub_tasks?.length) {
|
||||
acc.push(...task.sub_tasks.map((st: any) => ({ ...st, isSubtask: true })));
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}, [taskListGroup.tasks]);
|
||||
|
||||
// Virtual row renderer
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: flattenedTasks.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 42, // row height
|
||||
overscan: 5,
|
||||
});
|
||||
|
||||
// Memoize cell render functions
|
||||
const renderCell = useCallback(
|
||||
(columnKey: string | number, task: IProjectTask, isSubtask = false) => {
|
||||
const cellContent = {
|
||||
taskId: () => {
|
||||
const key = task.task_key?.toString() || '';
|
||||
return (
|
||||
<Tooltip title={key}>
|
||||
<Tag>{key}</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
task: () => (
|
||||
<Flex align="center" className="pl-2">
|
||||
{task.name}
|
||||
</Flex>
|
||||
),
|
||||
// Add other cell renderers as needed...
|
||||
}[columnKey];
|
||||
|
||||
return cellContent ? cellContent() : null;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Memoize header rendering
|
||||
const TableHeader = useMemo(
|
||||
() => (
|
||||
<div className="sticky top-0 z-20 flex border-b" style={{ height: 42 }}>
|
||||
<div className="sticky left-0 z-30 w-8 bg-white dark:bg-gray-900 flex items-center justify-center">
|
||||
<Checkbox />
|
||||
</div>
|
||||
{visibleColumns.map(column => (
|
||||
<div
|
||||
key={column.key}
|
||||
className="flex items-center px-3 border-r"
|
||||
style={{ width: column.width }}
|
||||
>
|
||||
{column.key}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
[visibleColumns]
|
||||
);
|
||||
|
||||
// Handle scroll shadows
|
||||
const handleScroll = useCallback((e: { target: any }) => {
|
||||
const target = e.target;
|
||||
const hasHorizontalShadow = target.scrollLeft > 0;
|
||||
target.classList.toggle('show-shadow', hasHorizontalShadow);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className="h-[400px] overflow-auto" onScroll={handleScroll}>
|
||||
{TableHeader}
|
||||
|
||||
<div
|
||||
ref={tableRef}
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map(virtualRow => {
|
||||
const task = flattenedTasks[virtualRow.index];
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
className="absolute top-0 left-0 flex w-full border-b hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
style={{
|
||||
height: 42,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="sticky left-0 z-10 w-8 flex items-center justify-center">
|
||||
{/* <Checkbox checked={task.selected} /> */}
|
||||
</div>
|
||||
{visibleColumns.map(column => (
|
||||
<div
|
||||
key={column.key}
|
||||
className={`flex items-center px-3 border-r ${
|
||||
hoverRow === task.id ? 'bg-gray-50 dark:bg-gray-800' : ''
|
||||
}`}
|
||||
style={{ width: column.width }}
|
||||
>
|
||||
{renderCell(column.key, task, task.is_sub_task)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListTable;
|
||||
@@ -1,239 +0,0 @@
|
||||
import { Avatar, Checkbox, DatePicker, Flex, Select, Tag } from 'antd';
|
||||
import { createColumnHelper, ColumnDef } from '@tanstack/react-table';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { HolderOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import StatusDropdown from '@/components/task-list-common/status-dropdown/status-dropdown';
|
||||
import Avatars from '@/components/avatars/avatars';
|
||||
import LabelsSelector from '@/components/task-list-common/labelsSelector/labels-selector';
|
||||
import CustomColorLabel from '@/components/task-list-common/labelsSelector/custom-color-label';
|
||||
import TaskRowName from '@/components/task-list-common/task-row/task-row-name/task-row-name';
|
||||
import TaskRowDescription from '@/components/task-list-common/task-row/task-row-description/task-row-description';
|
||||
import TaskRowProgress from '@/components/task-list-common/task-row/task-row-progress/task-row-progress';
|
||||
import TaskRowDueTime from '@/components/task-list-common/task-row/task-list-due-time-cell/task-row-due-time';
|
||||
import { COLUMN_KEYS } from '@/features/tasks/tasks.slice';
|
||||
|
||||
interface CreateColumnsProps {
|
||||
expandedRows: Record<string, boolean>;
|
||||
statuses: any[];
|
||||
handleTaskSelect: (taskId: string) => void;
|
||||
getCurrentSession: () => any;
|
||||
}
|
||||
|
||||
export const createColumns = ({
|
||||
expandedRows,
|
||||
statuses,
|
||||
handleTaskSelect,
|
||||
getCurrentSession,
|
||||
}: CreateColumnsProps): ColumnDef<IProjectTask, any>[] => {
|
||||
const columnHelper = createColumnHelper<IProjectTask>();
|
||||
|
||||
return [
|
||||
columnHelper.display({
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
style={{ padding: '8px 6px 8px 0!important' }}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Flex align="center" gap={4}>
|
||||
<HolderOutlined style={{ cursor: 'move' }} />
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
</Flex>
|
||||
),
|
||||
size: 47,
|
||||
minSize: 47,
|
||||
maxSize: 47,
|
||||
enablePinning: true,
|
||||
meta: {
|
||||
style: { position: 'sticky', left: 0, zIndex: 1 },
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('task_key', {
|
||||
header: 'Key',
|
||||
id: COLUMN_KEYS.KEY,
|
||||
size: 85,
|
||||
minSize: 85,
|
||||
maxSize: 85,
|
||||
enablePinning: false,
|
||||
cell: ({ row }) => (
|
||||
<Tag onClick={() => handleTaskSelect(row.original.id || '')} style={{ cursor: 'pointer' }}>
|
||||
{row.original.task_key}
|
||||
</Tag>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor('name', {
|
||||
header: 'Task',
|
||||
id: COLUMN_KEYS.NAME,
|
||||
size: 450,
|
||||
enablePinning: true,
|
||||
meta: {
|
||||
style: { position: 'sticky', left: '47px', zIndex: 1 },
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<TaskRowName
|
||||
task={row.original}
|
||||
isSubTask={false}
|
||||
expandedTasks={Object.keys(expandedRows)}
|
||||
setSelectedTaskId={() => {}}
|
||||
toggleTaskExpansion={() => {}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor('description', {
|
||||
header: 'Description',
|
||||
id: COLUMN_KEYS.DESCRIPTION,
|
||||
size: 225,
|
||||
enablePinning: false,
|
||||
cell: ({ row }) => <TaskRowDescription description={row.original.description || ''} />,
|
||||
}),
|
||||
|
||||
columnHelper.accessor('progress', {
|
||||
header: 'Progress',
|
||||
id: COLUMN_KEYS.PROGRESS,
|
||||
size: 80,
|
||||
enablePinning: false,
|
||||
cell: ({ row }) => (
|
||||
<TaskRowProgress
|
||||
progress={row.original.progress || 0}
|
||||
numberOfSubTasks={row.original.sub_tasks_count || 0}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor('names', {
|
||||
header: 'Assignees',
|
||||
id: COLUMN_KEYS.ASSIGNEES,
|
||||
size: 159,
|
||||
enablePinning: false,
|
||||
cell: ({ row }) => (
|
||||
<Flex align="center" gap={8}>
|
||||
<Avatars
|
||||
key={`${row.original.id}-assignees`}
|
||||
members={row.original.names || []}
|
||||
maxCount={3}
|
||||
/>
|
||||
<Avatar
|
||||
size={28}
|
||||
icon={<PlusOutlined />}
|
||||
className="avatar-add"
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
border: '1px dashed #c4c4c4',
|
||||
color: '#000000D9',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor('end_date', {
|
||||
header: 'Due Date',
|
||||
id: COLUMN_KEYS.DUE_DATE,
|
||||
size: 149,
|
||||
enablePinning: false,
|
||||
cell: ({ row }) => (
|
||||
<span>
|
||||
<DatePicker
|
||||
key={`${row.original.id}-end-date`}
|
||||
placeholder="Set a due date"
|
||||
suffixIcon={null}
|
||||
variant="borderless"
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor('due_time', {
|
||||
header: 'Due Time',
|
||||
id: COLUMN_KEYS.DUE_TIME,
|
||||
size: 120,
|
||||
enablePinning: false,
|
||||
cell: ({ row }) => <TaskRowDueTime dueTime={row.original.due_time || ''} />,
|
||||
}),
|
||||
|
||||
columnHelper.accessor('status', {
|
||||
header: 'Status',
|
||||
id: COLUMN_KEYS.STATUS,
|
||||
size: 120,
|
||||
enablePinning: false,
|
||||
cell: ({ row }) => (
|
||||
<StatusDropdown
|
||||
key={`${row.original.id}-status`}
|
||||
statusList={statuses}
|
||||
task={row.original}
|
||||
teamId={getCurrentSession()?.team_id || ''}
|
||||
onChange={statusId => {
|
||||
console.log('Status changed:', statusId);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor('labels', {
|
||||
header: 'Labels',
|
||||
id: COLUMN_KEYS.LABELS,
|
||||
size: 225,
|
||||
enablePinning: false,
|
||||
cell: ({ row }) => (
|
||||
<Flex>
|
||||
{row.original.labels?.map(label => (
|
||||
<CustomColorLabel key={`${row.original.id}-${label.id}`} label={label} />
|
||||
))}
|
||||
<LabelsSelector taskId={row.original.id} />
|
||||
</Flex>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor('start_date', {
|
||||
header: 'Start Date',
|
||||
id: COLUMN_KEYS.START_DATE,
|
||||
size: 149,
|
||||
enablePinning: false,
|
||||
cell: ({ row }) => (
|
||||
<span>
|
||||
<DatePicker placeholder="Set a start date" suffixIcon={null} variant="borderless" />
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor('priority', {
|
||||
header: 'Priority',
|
||||
id: COLUMN_KEYS.PRIORITY,
|
||||
size: 120,
|
||||
enablePinning: false,
|
||||
cell: ({ row }) => (
|
||||
<span>
|
||||
<Select
|
||||
variant="borderless"
|
||||
options={[
|
||||
{ value: 'high', label: 'High' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'low', label: 'Low' },
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
|
||||
// columnHelper.accessor('time_tracking', {
|
||||
// header: 'Time Tracking',
|
||||
// size: 120,
|
||||
// enablePinning: false,
|
||||
// cell: ({ row }) => (
|
||||
// <TaskRowTimeTracking taskId={row.original.id || null} />
|
||||
// )
|
||||
// })
|
||||
];
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
.table-header {
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
/* Border below header */
|
||||
}
|
||||
|
||||
.table-body {
|
||||
background-color: #ffffff;
|
||||
/* White background for body */
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
/* Use flexbox for row layout */
|
||||
align-items: center;
|
||||
/* Center items vertically */
|
||||
transition: background-color 0.2s;
|
||||
/* Smooth background transition */
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background-color: #f5f5f5;
|
||||
/* Light gray background on hover */
|
||||
}
|
||||
|
||||
/* Optional: Add styles for sticky headers */
|
||||
.table-header > div {
|
||||
position: sticky;
|
||||
/* Make header cells sticky */
|
||||
top: 0;
|
||||
/* Stick to the top */
|
||||
z-index: 1;
|
||||
/* Ensure it stays above other content */
|
||||
}
|
||||
|
||||
/* Optional: Add styles for cell borders */
|
||||
.table-row > div {
|
||||
border-right: 1px solid #d9d9d9;
|
||||
/* Right border for cells */
|
||||
}
|
||||
|
||||
.table-row > div:last-child {
|
||||
border-right: none;
|
||||
/* Remove right border for last cell */
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Checkbox, theme } from 'antd';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
flexRender,
|
||||
VisibilityState,
|
||||
Row,
|
||||
Column,
|
||||
} from '@tanstack/react-table';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import React from 'react';
|
||||
import './task-list-custom.css';
|
||||
import TaskListInstantTaskInput from './task-list-instant-task-input/task-list-instant-task-input';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { createColumns } from './task-list-columns/task-list-columns';
|
||||
|
||||
interface TaskListCustomProps {
|
||||
tasks: IProjectTask[];
|
||||
color: string;
|
||||
groupId?: string | null;
|
||||
onTaskSelect?: (taskId: string) => void;
|
||||
}
|
||||
|
||||
const TaskListCustom: React.FC<TaskListCustomProps> = ({ tasks, color, groupId, onTaskSelect }) => {
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({});
|
||||
|
||||
const statuses = useAppSelector(state => state.taskStatusReducer.status);
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { token } = theme.useToken();
|
||||
const { getCurrentSession } = useAuthService();
|
||||
|
||||
const handleExpandClick = useCallback((rowId: string) => {
|
||||
setExpandedRows(prev => ({
|
||||
...prev,
|
||||
[rowId]: !prev[rowId],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleTaskSelect = useCallback(
|
||||
(taskId: string) => {
|
||||
onTaskSelect?.(taskId);
|
||||
},
|
||||
[onTaskSelect]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
createColumns({
|
||||
expandedRows,
|
||||
statuses,
|
||||
handleTaskSelect,
|
||||
getCurrentSession,
|
||||
}),
|
||||
[expandedRows, statuses, handleTaskSelect, getCurrentSession]
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: tasks,
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
columnVisibility,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
});
|
||||
|
||||
const { rows } = table.getRowModel();
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: rows.length,
|
||||
getScrollElement: () => tableContainerRef.current,
|
||||
estimateSize: () => 50,
|
||||
overscan: 20,
|
||||
});
|
||||
|
||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||
const totalSize = rowVirtualizer.getTotalSize();
|
||||
const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0;
|
||||
const paddingBottom =
|
||||
virtualRows.length > 0 ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) : 0;
|
||||
|
||||
const columnToggleItems = columns.map(column => ({
|
||||
key: column.id as string,
|
||||
label: (
|
||||
<span>
|
||||
<Checkbox checked={table.getColumn(column.id as string)?.getIsVisible()}>
|
||||
{typeof column.header === 'string' ? column.header : column.id}
|
||||
</Checkbox>
|
||||
</span>
|
||||
),
|
||||
onClick: () => {
|
||||
const columnData = table.getColumn(column.id as string);
|
||||
if (columnData) {
|
||||
columnData.toggleVisibility();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="task-list-custom"
|
||||
style={{
|
||||
maxHeight: '80vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
borderLeft: `4px solid ${color}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={tableContainerRef}
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowX: 'auto',
|
||||
maxHeight: '100%',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 'fit-content', borderCollapse: 'collapse' }}>
|
||||
<div className="table-header">
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<div key={headerGroup.id} className="table-row">
|
||||
{headerGroup.headers.map((header, index) => (
|
||||
<div
|
||||
key={header.id}
|
||||
className={`${header.column.getIsPinned() === 'left' ? 'sticky left-0 z-10' : ''}`}
|
||||
style={{
|
||||
width: header.getSize(),
|
||||
position: index < 2 ? 'sticky' : 'relative',
|
||||
left: index === 0 ? 0 : index === 1 ? '47px' : 'auto',
|
||||
background: token.colorBgElevated,
|
||||
zIndex: 1,
|
||||
color: token.colorText,
|
||||
height: '40px',
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
textAlign: index === 0 ? 'right' : 'left',
|
||||
fontWeight: 'normal',
|
||||
padding: '8px 0px 8px 8px',
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="table-body">
|
||||
{paddingTop > 0 && <div style={{ height: `${paddingTop}px` }} />}
|
||||
{virtualRows.map(virtualRow => {
|
||||
const row = rows[virtualRow.index];
|
||||
return (
|
||||
<React.Fragment key={row.id}>
|
||||
<div
|
||||
className="table-row"
|
||||
style={{
|
||||
'&:hover div': {
|
||||
background: `${token.colorFillAlter} !important`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell, index) => (
|
||||
<div
|
||||
key={cell.id}
|
||||
className={`${cell.column.getIsPinned() === 'left' ? 'sticky left-0 z-10' : ''}`}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
position: index < 2 ? 'sticky' : 'relative',
|
||||
left: 'auto',
|
||||
background: token.colorBgContainer,
|
||||
color: token.colorText,
|
||||
height: '42px',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
padding: '8px 0px 8px 8px',
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{expandedRows[row.id] &&
|
||||
row.original.sub_tasks?.map(subTask => (
|
||||
<div
|
||||
key={subTask.task_key}
|
||||
className="table-row"
|
||||
style={{
|
||||
'&:hover div': {
|
||||
background: `${token.colorFillAlter} !important`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{columns.map((col, index) => (
|
||||
<div
|
||||
key={`${subTask.task_key}-${col.id}`}
|
||||
style={{
|
||||
width: col.getSize(),
|
||||
position: index < 2 ? 'sticky' : 'relative',
|
||||
left: index < 2 ? `${index * col.getSize()}px` : 'auto',
|
||||
background: token.colorBgContainer,
|
||||
color: token.colorText,
|
||||
height: '42px',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
paddingLeft: index === 3 ? '32px' : '8px',
|
||||
paddingRight: '8px',
|
||||
}}
|
||||
>
|
||||
{flexRender(col.cell, {
|
||||
getValue: () => subTask[col.id as keyof typeof subTask] ?? null,
|
||||
row: { original: subTask } as Row<IProjectTask>,
|
||||
column: col as Column<IProjectTask>,
|
||||
table,
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{paddingBottom > 0 && <div style={{ height: `${paddingBottom}px` }} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TaskListInstantTaskInput
|
||||
session={getCurrentSession() || null}
|
||||
groupId={groupId}
|
||||
parentTask={null}
|
||||
/>
|
||||
{/* {selectedCount > 0 && (
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: token.colorBgElevated,
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<span>{selectedCount} tasks selected</span>
|
||||
<Flex gap={8}>
|
||||
<Button icon={<EditOutlined />}>Edit</Button>
|
||||
<Button danger icon={<DeleteOutlined />}>
|
||||
Delete
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListCustom;
|
||||
@@ -1,116 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Dropdown, Input, Menu, Badge, Tooltip } from 'antd';
|
||||
import {
|
||||
RightOutlined,
|
||||
LoadingOutlined,
|
||||
EllipsisOutlined,
|
||||
EditOutlined,
|
||||
RetweetOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import { ITaskStatusCategory } from '@/types/status.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
// import WorklenzTaskListPhaseDuration from "./WorklenzTaskListPhaseDuration";
|
||||
// import WorklenzTasksProgressBar from "./WorklenzTasksProgressBar";
|
||||
|
||||
interface Props {
|
||||
group: ITaskListGroup;
|
||||
projectId: string | null;
|
||||
categories: ITaskStatusCategory[];
|
||||
}
|
||||
|
||||
const TaskListGroupSettings: React.FC<Props> = ({ group, projectId, categories }) => {
|
||||
const [edit, setEdit] = useState(false);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [isEditColProgress, setIsEditColProgress] = useState(false);
|
||||
const [isGroupByPhases, setIsGroupByPhases] = useState(false);
|
||||
const [isGroupByStatus, setIsGroupByStatus] = useState(false);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item key="edit">
|
||||
<EditOutlined className="me-2" />
|
||||
Rename
|
||||
</Menu.Item>
|
||||
{isGroupByStatus && (
|
||||
<Menu.SubMenu
|
||||
key="change-category"
|
||||
title={
|
||||
<>
|
||||
<RetweetOutlined className="me-2" />
|
||||
Change category
|
||||
</>
|
||||
}
|
||||
>
|
||||
{categories.map(item => (
|
||||
<Tooltip key={item.id} title={item.description || ''} placement="right">
|
||||
<Menu.Item
|
||||
style={{
|
||||
fontWeight: item.id === group.category_id ? 'bold' : undefined,
|
||||
}}
|
||||
>
|
||||
<Badge color={item.color_code} text={item.name || ''} />
|
||||
</Menu.Item>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const onBlurEditColumn = (group: ITaskListGroup) => {
|
||||
setEdit(false);
|
||||
};
|
||||
|
||||
const onToggleClick = () => {
|
||||
console.log('onToggleClick');
|
||||
};
|
||||
|
||||
const canDisplayActions = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-between align-items-center position-relative">
|
||||
<div className="d-flex align-items-center">
|
||||
<Button
|
||||
className={`collapse btn border-0 ${group.tasks.length ? 'active' : ''}`}
|
||||
onClick={onToggleClick}
|
||||
style={{ backgroundColor: group.color_code }}
|
||||
>
|
||||
<RightOutlined className="collapse-icon" />
|
||||
{`${group.name} (${group.tasks.length})`}
|
||||
</Button>
|
||||
|
||||
{canDisplayActions() && (
|
||||
<Dropdown
|
||||
overlay={menu}
|
||||
trigger={['click']}
|
||||
onVisibleChange={visible => setShowMenu(visible)}
|
||||
>
|
||||
<Button className="p-0" type="text">
|
||||
<EllipsisOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* {isGroupByPhases && group.name !== "Unmapped" && (
|
||||
<div className="d-flex align-items-center me-2 ms-auto">
|
||||
<WorklenzTaskListPhaseDuration group={group} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isProgressBarAvailable() && (
|
||||
<WorklenzTasksProgressBar
|
||||
todoProgress={group.todo_progress}
|
||||
doingProgress={group.doing_progress}
|
||||
doneProgress={group.done_progress}
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListGroupSettings;
|
||||
@@ -1,160 +0,0 @@
|
||||
import { Input, InputRef, theme } from 'antd';
|
||||
import React, { useState, useMemo, useRef } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { colors } from '@/styles/colors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ILocalSession } from '@/types/auth/local-session.types';
|
||||
import { ITaskCreateRequest } from '@/types/tasks/task-create-request.types';
|
||||
import {
|
||||
addTask,
|
||||
getCurrentGroup,
|
||||
GROUP_BY_PHASE_VALUE,
|
||||
GROUP_BY_PRIORITY_VALUE,
|
||||
GROUP_BY_STATUS_VALUE,
|
||||
} from '@/features/tasks/tasks.slice';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { DRAWER_ANIMATION_INTERVAL } from '@/shared/constants';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
|
||||
interface ITaskListInstantTaskInputProps {
|
||||
session: ILocalSession | null;
|
||||
groupId?: string | null;
|
||||
parentTask?: string | null;
|
||||
}
|
||||
interface IAddNewTask extends IProjectTask {
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
const TaskListInstantTaskInput = ({
|
||||
session,
|
||||
groupId = null,
|
||||
parentTask = null,
|
||||
}: ITaskListInstantTaskInputProps) => {
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
const [taskName, setTaskName] = useState<string>('');
|
||||
const [creatingTask, setCreatingTask] = useState<boolean>(false);
|
||||
const taskInputRef = useRef<InputRef>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { socket } = useSocket();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const customBorderColor = useMemo(() => themeMode === 'dark' && ' border-[#303030]', [themeMode]);
|
||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||
|
||||
const createRequestBody = (): ITaskCreateRequest | null => {
|
||||
if (!projectId || !session) return null;
|
||||
const body: ITaskCreateRequest = {
|
||||
project_id: projectId,
|
||||
name: taskName,
|
||||
reporter_id: session.id,
|
||||
team_id: session.team_id,
|
||||
};
|
||||
|
||||
const groupBy = getCurrentGroup();
|
||||
if (groupBy.value === GROUP_BY_STATUS_VALUE) {
|
||||
body.status_id = groupId || undefined;
|
||||
} else if (groupBy.value === GROUP_BY_PRIORITY_VALUE) {
|
||||
body.priority_id = groupId || undefined;
|
||||
} else if (groupBy.value === GROUP_BY_PHASE_VALUE) {
|
||||
body.phase_id = groupId || undefined;
|
||||
}
|
||||
|
||||
if (parentTask) {
|
||||
body.parent_task_id = parentTask;
|
||||
}
|
||||
console.log('createRequestBody', body);
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const reset = (scroll = true) => {
|
||||
setIsEdit(false);
|
||||
|
||||
setCreatingTask(false);
|
||||
|
||||
setTaskName('');
|
||||
setIsEdit(true);
|
||||
|
||||
setTimeout(() => {
|
||||
taskInputRef.current?.focus();
|
||||
if (scroll) window.scrollTo(0, document.body.scrollHeight);
|
||||
}, DRAWER_ANIMATION_INTERVAL); // wait for the animation end
|
||||
};
|
||||
|
||||
const onNewTaskReceived = (task: IAddNewTask) => {
|
||||
if (!groupId) return;
|
||||
console.log('onNewTaskReceived', task);
|
||||
task.groupId = groupId;
|
||||
if (groupId && task.id) {
|
||||
dispatch(addTask(task));
|
||||
reset(false);
|
||||
// if (this.map.has(task.id)) return;
|
||||
|
||||
// this.service.addTask(task, this.groupId);
|
||||
// this.reset(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addInstantTask = () => {
|
||||
if (creatingTask) return;
|
||||
console.log('addInstantTask', projectId, taskName.trim());
|
||||
if (!projectId || !session || taskName.trim() === '') return;
|
||||
|
||||
try {
|
||||
setCreatingTask(true);
|
||||
const body = createRequestBody();
|
||||
if (!body) return;
|
||||
socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
|
||||
socket?.once(SocketEvents.QUICK_TASK.toString(), (task: IProjectTask) => {
|
||||
setCreatingTask(false);
|
||||
if (task.parent_task_id) {
|
||||
}
|
||||
onNewTaskReceived(task as IAddNewTask);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setCreatingTask(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTask = () => {
|
||||
setIsEdit(false);
|
||||
addInstantTask();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border-t border-b-[1px] border-r-[1px]`}
|
||||
style={{ borderColor: token.colorBorderSecondary }}
|
||||
>
|
||||
{isEdit ? (
|
||||
<Input
|
||||
className="w-full rounded-none"
|
||||
style={{ borderColor: colors.skyBlue, height: '40px' }}
|
||||
placeholder={t('addTaskInputPlaceholder')}
|
||||
onChange={e => setTaskName(e.target.value)}
|
||||
onBlur={handleAddTask}
|
||||
onPressEnter={handleAddTask}
|
||||
ref={taskInputRef}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
onFocus={() => setIsEdit(true)}
|
||||
className="w-[300px] border-none"
|
||||
style={{ height: '34px' }}
|
||||
value={t('addTaskText')}
|
||||
ref={taskInputRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListInstantTaskInput;
|
||||
@@ -1,471 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Avatar, Checkbox, DatePicker, Flex, Tag, Tooltip, Typography } from 'antd';
|
||||
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { columnList } from '@/pages/projects/project-view-1/taskList/taskListTable/columns/columnList';
|
||||
import AddTaskListRow from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableRows/AddTaskListRow';
|
||||
|
||||
import CustomAvatar from '@/components/CustomAvatar';
|
||||
import LabelsSelector from '@components/task-list-common/labelsSelector/labels-selector';
|
||||
import { useSelectedProject } from '@/hooks/useSelectedProject';
|
||||
import StatusDropdown from '@/components/task-list-common/status-dropdown/status-dropdown';
|
||||
import PriorityDropdown from '@/components/task-list-common/priorityDropdown/priority-dropdown';
|
||||
import { simpleDateFormat } from '@/utils/simpleDateFormat';
|
||||
import { durationDateFormat } from '@/utils/durationDateFormat';
|
||||
import CustomColorLabel from '@components/task-list-common/labelsSelector/custom-color-label';
|
||||
import CustomNumberLabel from '@components/task-list-common/labelsSelector/custom-number-label';
|
||||
import PhaseDropdown from '@components/task-list-common/phaseDropdown/PhaseDropdown';
|
||||
import AssigneeSelector from '@components/task-list-common/assigneeSelector/AssigneeSelector';
|
||||
import TaskCell from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskCell';
|
||||
import AddSubTaskListRow from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableRows/AddSubTaskListRow';
|
||||
import { colors } from '@/styles/colors';
|
||||
import TimeTracker from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TimeTracker';
|
||||
import TaskContextMenu from '@/pages/projects/project-view-1/taskList/taskListTable/contextMenu/TaskContextMenu';
|
||||
import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import Avatars from '@/components/avatars/avatars';
|
||||
|
||||
const TaskListTable = ({
|
||||
taskList,
|
||||
tableId,
|
||||
}: {
|
||||
taskList: ITaskListGroup;
|
||||
tableId: string | undefined;
|
||||
}) => {
|
||||
// these states manage the necessary states
|
||||
const [hoverRow, setHoverRow] = useState<string | null>(null);
|
||||
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
||||
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
||||
const [expandedTasks, setExpandedTasks] = useState<string[]>([]);
|
||||
const [isSelectAll, setIsSelectAll] = useState(false);
|
||||
// context menu state
|
||||
const [contextMenuVisible, setContextMenuVisible] = useState(false);
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
// state to check scroll
|
||||
const [scrollingTables, setScrollingTables] = useState<{
|
||||
[key: string]: boolean;
|
||||
}>({});
|
||||
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// get data theme data from redux
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
// get the selected project details
|
||||
const selectedProject = useSelectedProject();
|
||||
|
||||
// get columns list details
|
||||
const columnsVisibility = useAppSelector(
|
||||
state => state.projectViewTaskListColumnsReducer.columnsVisibility
|
||||
);
|
||||
const visibleColumns = columnList.filter(
|
||||
column => columnsVisibility[column.key as keyof typeof columnsVisibility]
|
||||
);
|
||||
|
||||
// toggle subtasks visibility
|
||||
const toggleTaskExpansion = (taskId: string) => {
|
||||
setExpandedTasks(prev =>
|
||||
prev.includes(taskId) ? prev.filter(id => id !== taskId) : [...prev, taskId]
|
||||
);
|
||||
};
|
||||
|
||||
// toggle all task select when header checkbox click
|
||||
const toggleSelectAll = () => {
|
||||
if (isSelectAll) {
|
||||
setSelectedRows([]);
|
||||
dispatch(deselectAll());
|
||||
} else {
|
||||
// const allTaskIds =
|
||||
// task-list?.flatMap((task) => [
|
||||
// task.taskId,
|
||||
// ...(task.subTasks?.map((subtask) => subtask.taskId) || []),
|
||||
// ]) || [];
|
||||
// setSelectedRows(allTaskIds);
|
||||
// dispatch(selectTaskIds(allTaskIds));
|
||||
// console.log('selected tasks and subtasks (all):', allTaskIds);
|
||||
}
|
||||
setIsSelectAll(!isSelectAll);
|
||||
};
|
||||
|
||||
// toggle selected row
|
||||
const toggleRowSelection = (task: IProjectTask) => {
|
||||
setSelectedRows(prevSelectedRows =>
|
||||
prevSelectedRows.includes(task.id || '')
|
||||
? prevSelectedRows.filter(id => id !== task.id)
|
||||
: [...prevSelectedRows, task.id || '']
|
||||
);
|
||||
};
|
||||
|
||||
// this use effect for realtime update the selected rows
|
||||
useEffect(() => {
|
||||
console.log('Selected tasks and subtasks:', selectedRows);
|
||||
}, [selectedRows]);
|
||||
|
||||
// select one row this triggers only in handle the context menu ==> righ click mouse event
|
||||
const selectOneRow = (task: IProjectTask) => {
|
||||
setSelectedRows([task.id || '']);
|
||||
|
||||
// log the task object when selected
|
||||
if (!selectedRows.includes(task.id || '')) {
|
||||
console.log('Selected task:', task);
|
||||
}
|
||||
};
|
||||
|
||||
// handle custom task context menu
|
||||
const handleContextMenu = (e: React.MouseEvent, task: IProjectTask) => {
|
||||
e.preventDefault();
|
||||
setSelectedTaskId(task.id || '');
|
||||
selectOneRow(task);
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
||||
setContextMenuVisible(true);
|
||||
};
|
||||
|
||||
// trigger the table scrolling
|
||||
useEffect(() => {
|
||||
const tableContainer = document.querySelector(`.tasklist-container-${tableId}`);
|
||||
const handleScroll = () => {
|
||||
if (tableContainer) {
|
||||
setScrollingTables(prev => ({
|
||||
...prev,
|
||||
[tableId]: tableContainer.scrollLeft > 0,
|
||||
}));
|
||||
}
|
||||
};
|
||||
tableContainer?.addEventListener('scroll', handleScroll);
|
||||
return () => tableContainer?.removeEventListener('scroll', handleScroll);
|
||||
}, [tableId]);
|
||||
|
||||
// layout styles for table and the columns
|
||||
const customBorderColor = themeMode === 'dark' && ' border-[#303030]';
|
||||
|
||||
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]'}`;
|
||||
|
||||
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'}`;
|
||||
|
||||
// function to render the column content based on column key
|
||||
const renderColumnContent = (
|
||||
columnKey: string,
|
||||
task: IProjectTask,
|
||||
isSubtask: boolean = false
|
||||
) => {
|
||||
switch (columnKey) {
|
||||
// task ID column
|
||||
case 'taskId':
|
||||
return (
|
||||
<Tooltip title={task.task_key || ''} className="flex justify-center">
|
||||
<Tag>{task.task_key || ''}</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
// task name column
|
||||
case 'task':
|
||||
return (
|
||||
// custom task cell component
|
||||
<TaskCell
|
||||
task={task}
|
||||
isSubTask={isSubtask}
|
||||
expandedTasks={expandedTasks}
|
||||
hoverRow={hoverRow}
|
||||
setSelectedTaskId={setSelectedTaskId}
|
||||
toggleTaskExpansion={toggleTaskExpansion}
|
||||
/>
|
||||
);
|
||||
|
||||
// description column
|
||||
case 'description':
|
||||
return <Typography.Text style={{ width: 200 }}></Typography.Text>;
|
||||
|
||||
// progress column
|
||||
case 'progress': {
|
||||
return task?.progress || task?.progress === 0 ? (
|
||||
<TaskProgress progress={task?.progress} numberOfSubTasks={task?.sub_tasks?.length || 0} />
|
||||
) : (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
|
||||
// members column
|
||||
case 'members':
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Avatars members={task.names || []} />
|
||||
{/* <Avatar.Group>
|
||||
{task.assignees?.map(member => (
|
||||
<CustomAvatar key={member.id} avatarName={member.name} size={26} />
|
||||
))}
|
||||
</Avatar.Group> */}
|
||||
<AssigneeSelector taskId={selectedTaskId || '0'} />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
// labels column
|
||||
case 'labels':
|
||||
return (
|
||||
<Flex>
|
||||
{task?.labels && task?.labels?.length <= 2 ? (
|
||||
task?.labels?.map(label => <CustomColorLabel label={label} />)
|
||||
) : (
|
||||
<Flex>
|
||||
<CustomColorLabel label={task?.labels ? task.labels[0] : null} />
|
||||
<CustomColorLabel label={task?.labels ? task.labels[1] : null} />
|
||||
{/* this component show other label names */}
|
||||
<CustomNumberLabel
|
||||
// this label list get the labels without 1, 2 elements
|
||||
labelList={task?.labels ? task.labels : null}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<LabelsSelector taskId={task.id} />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
// phase column
|
||||
case 'phases':
|
||||
return <PhaseDropdown projectId={selectedProject?.id || ''} />;
|
||||
|
||||
// status column
|
||||
case 'status':
|
||||
return <StatusDropdown currentStatus={task.status || ''} />;
|
||||
|
||||
// priority column
|
||||
case 'priority':
|
||||
return <PriorityDropdown currentPriority={task.priority || ''} />;
|
||||
|
||||
// time tracking column
|
||||
case 'timeTracking':
|
||||
return <TimeTracker taskId={task.id} initialTime={task.timer_start_time || 0} />;
|
||||
|
||||
// estimation column
|
||||
case 'estimation':
|
||||
return <Typography.Text>0h 0m</Typography.Text>;
|
||||
|
||||
// start date column
|
||||
case 'startDate':
|
||||
return task.start_date ? (
|
||||
<Typography.Text>{simpleDateFormat(task.start_date)}</Typography.Text>
|
||||
) : (
|
||||
<DatePicker
|
||||
placeholder="Set a start date"
|
||||
suffixIcon={null}
|
||||
style={{ border: 'none', width: '100%', height: '100%' }}
|
||||
/>
|
||||
);
|
||||
|
||||
// due date column
|
||||
case 'dueDate':
|
||||
return task.end_date ? (
|
||||
<Typography.Text>{simpleDateFormat(task.end_date)}</Typography.Text>
|
||||
) : (
|
||||
<DatePicker
|
||||
placeholder="Set a due date"
|
||||
suffixIcon={null}
|
||||
style={{ border: 'none', width: '100%', height: '100%' }}
|
||||
/>
|
||||
);
|
||||
|
||||
// completed date column
|
||||
case 'completedDate':
|
||||
return <Typography.Text>{durationDateFormat(task.completed_at || null)}</Typography.Text>;
|
||||
|
||||
// created date column
|
||||
case 'createdDate':
|
||||
return <Typography.Text>{durationDateFormat(task.created_at || null)}</Typography.Text>;
|
||||
|
||||
// last updated column
|
||||
case 'lastUpdated':
|
||||
return <Typography.Text>{durationDateFormat(task.updated_at || null)}</Typography.Text>;
|
||||
|
||||
// recorder column
|
||||
case 'reporter':
|
||||
return <Typography.Text>{task.reporter}</Typography.Text>;
|
||||
|
||||
// default case for unsupported columns
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`border-x border-b ${customBorderColor}`}>
|
||||
<div className={`tasklist-container-${tableId} min-h-0 max-w-full overflow-x-auto`}>
|
||||
<table className={`rounded-2 w-full min-w-max border-collapse`}>
|
||||
<thead className="h-[42px]">
|
||||
<tr>
|
||||
{/* this cell render the select all task checkbox */}
|
||||
<th
|
||||
key={'selector'}
|
||||
className={`${customHeaderColumnStyles('selector')}`}
|
||||
style={{ width: 20, fontWeight: 500 }}
|
||||
>
|
||||
<Checkbox checked={isSelectAll} onChange={toggleSelectAll} />
|
||||
</th>
|
||||
{/* other header cells */}
|
||||
{visibleColumns.map(column => (
|
||||
<th
|
||||
key={column.key}
|
||||
className={`${customHeaderColumnStyles(column.key)}`}
|
||||
style={{ width: column.width, fontWeight: 500 }}
|
||||
>
|
||||
{column.key === 'phases'
|
||||
? column.columnHeader
|
||||
: t(`${column.columnHeader}Column`)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{taskList?.tasks?.map(task => (
|
||||
<React.Fragment key={task.id}>
|
||||
<tr
|
||||
key={task.id}
|
||||
onContextMenu={e => handleContextMenu(e, task)}
|
||||
className={`${taskList.tasks.length === 0 ? 'h-0' : 'h-[42px]'}`}
|
||||
>
|
||||
{/* this cell render the select the related task checkbox */}
|
||||
<td
|
||||
key={'selector'}
|
||||
className={customBodyColumnStyles('selector')}
|
||||
style={{
|
||||
width: 20,
|
||||
backgroundColor: selectedRows.includes(task.id || '')
|
||||
? themeMode === 'dark'
|
||||
? colors.skyBlue
|
||||
: '#dceeff'
|
||||
: hoverRow === task.id
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#f8f7f9'
|
||||
: themeMode === 'dark'
|
||||
? '#181818'
|
||||
: '#fff',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(task.id || '')}
|
||||
onChange={() => toggleRowSelection(task)}
|
||||
/>
|
||||
</td>
|
||||
{/* other cells */}
|
||||
{visibleColumns.map(column => (
|
||||
<td
|
||||
key={column.key}
|
||||
className={customBodyColumnStyles(column.key)}
|
||||
style={{
|
||||
width: column.width,
|
||||
backgroundColor: selectedRows.includes(task.id || '')
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#dceeff'
|
||||
: hoverRow === task.id
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#f8f7f9'
|
||||
: themeMode === 'dark'
|
||||
? '#181818'
|
||||
: '#fff',
|
||||
}}
|
||||
>
|
||||
{renderColumnContent(column.key, task)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
{/* this is for sub tasks */}
|
||||
{expandedTasks.includes(task.id || '') &&
|
||||
task?.sub_tasks?.map(subtask => (
|
||||
<tr
|
||||
key={subtask.id}
|
||||
onContextMenu={e => handleContextMenu(e, subtask)}
|
||||
className={`${taskList.tasks.length === 0 ? 'h-0' : 'h-[42px]'}`}
|
||||
>
|
||||
{/* this cell render the select the related task checkbox */}
|
||||
<td
|
||||
key={'selector'}
|
||||
className={customBodyColumnStyles('selector')}
|
||||
style={{
|
||||
width: 20,
|
||||
backgroundColor: selectedRows.includes(subtask.id || '')
|
||||
? themeMode === 'dark'
|
||||
? colors.skyBlue
|
||||
: '#dceeff'
|
||||
: hoverRow === subtask.id
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#f8f7f9'
|
||||
: themeMode === 'dark'
|
||||
? '#181818'
|
||||
: '#fff',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(subtask.id || '')}
|
||||
onChange={() => toggleRowSelection(subtask)}
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* other sub tasks cells */}
|
||||
{visibleColumns.map(column => (
|
||||
<td
|
||||
key={column.key}
|
||||
className={customBodyColumnStyles(column.key)}
|
||||
style={{
|
||||
width: column.width,
|
||||
backgroundColor: selectedRows.includes(subtask.id || '')
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#dceeff'
|
||||
: hoverRow === subtask.id || ''
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#f8f7f9'
|
||||
: themeMode === 'dark'
|
||||
? '#181818'
|
||||
: '#fff',
|
||||
}}
|
||||
>
|
||||
{renderColumnContent(column.key, subtask, true)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{expandedTasks.includes(task.id || '') && (
|
||||
<tr>
|
||||
<td colSpan={visibleColumns.length}>
|
||||
<AddSubTaskListRow />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* add a main task to the table */}
|
||||
<AddTaskListRow />
|
||||
|
||||
{/* custom task context menu */}
|
||||
<TaskContextMenu
|
||||
visible={contextMenuVisible}
|
||||
position={contextMenuPosition}
|
||||
selectedTask={selectedRows[0]}
|
||||
onClose={() => setContextMenuVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListTable;
|
||||
@@ -1,19 +0,0 @@
|
||||
.tasks-table {
|
||||
width: max-content;
|
||||
margin-left: 3px;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.flex-table {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/* custom collapse styles for content box and the left border */
|
||||
.ant-collapse-header {
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.custom-collapse-content-box .ant-collapse-content-box {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:where(.css-dev-only-do-not-override-1w6wsvq).ant-collapse-ghost
|
||||
> .ant-collapse-item
|
||||
> .ant-collapse-content
|
||||
> .ant-collapse-content-box {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import { Badge, Button, Collapse, ConfigProvider, Dropdown, Flex, Input, Typography } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { TaskType } from '@/types/task.types';
|
||||
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { colors } from '@/styles/colors';
|
||||
import './task-list-table-wrapper.css';
|
||||
import TaskListTable from '../task-list-table-old/task-list-table-old';
|
||||
import { MenuProps } from 'antd/lib';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import TaskListCustom from '../task-list-custom';
|
||||
|
||||
type TaskListTableWrapperProps = {
|
||||
taskList: ITaskListGroup;
|
||||
groupId: string | undefined;
|
||||
name: string | undefined;
|
||||
color: string | undefined;
|
||||
onRename?: (name: string) => void;
|
||||
onStatusCategoryChange?: (category: string) => void;
|
||||
};
|
||||
|
||||
const TaskListTableWrapper = ({
|
||||
taskList,
|
||||
groupId,
|
||||
name,
|
||||
color,
|
||||
onRename,
|
||||
onStatusCategoryChange,
|
||||
}: TaskListTableWrapperProps) => {
|
||||
const [tableName, setTableName] = useState<string>(name || '');
|
||||
const [isRenaming, setIsRenaming] = useState<boolean>(false);
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(true);
|
||||
|
||||
const type = 'status';
|
||||
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
// function to handle toggle expand
|
||||
const handlToggleExpand = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
// these codes only for status type tables
|
||||
// function to handle rename this functionality only available for status type tables
|
||||
const handleRename = () => {
|
||||
if (onRename) {
|
||||
onRename(tableName);
|
||||
}
|
||||
setIsRenaming(false);
|
||||
};
|
||||
|
||||
// function to handle category change
|
||||
const handleCategoryChange = (category: string) => {
|
||||
if (onStatusCategoryChange) {
|
||||
onStatusCategoryChange(category);
|
||||
}
|
||||
};
|
||||
|
||||
// find the available status for the currently active project
|
||||
const statusList = useAppSelector(state => state.statusReducer.status);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'todo':
|
||||
return '#d8d7d8';
|
||||
case 'doing':
|
||||
return '#c0d5f6';
|
||||
case 'done':
|
||||
return '#c2e4d0';
|
||||
default:
|
||||
return '#d8d7d8';
|
||||
}
|
||||
};
|
||||
|
||||
// dropdown options
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
icon: <EditOutlined />,
|
||||
label: 'Rename',
|
||||
onClick: () => setIsRenaming(true),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
icon: <RetweetOutlined />,
|
||||
label: 'Change category',
|
||||
children: statusList?.map(status => ({
|
||||
key: status.id,
|
||||
label: (
|
||||
<Flex gap={8} onClick={() => handleCategoryChange(status.category)}>
|
||||
<Badge color={getStatusColor(status.category)} />
|
||||
{status.name}
|
||||
</Flex>
|
||||
),
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
wave={{ disabled: true }}
|
||||
theme={{
|
||||
components: {
|
||||
Collapse: {
|
||||
headerPadding: 0,
|
||||
contentPadding: 0,
|
||||
},
|
||||
|
||||
Select: {
|
||||
colorBorder: colors.transparent,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Flex vertical>
|
||||
<Flex style={{ transform: 'translateY(6px)' }}>
|
||||
<Button
|
||||
className="custom-collapse-button"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
border: 'none',
|
||||
borderBottomLeftRadius: isExpanded ? 0 : 4,
|
||||
borderBottomRightRadius: isExpanded ? 0 : 4,
|
||||
color: colors.darkGray,
|
||||
}}
|
||||
icon={<RightOutlined rotate={isExpanded ? 90 : 0} />}
|
||||
onClick={handlToggleExpand}
|
||||
>
|
||||
{isRenaming ? (
|
||||
<Input
|
||||
size="small"
|
||||
value={tableName}
|
||||
onChange={e => setTableName(e.target.value)}
|
||||
onBlur={handleRename}
|
||||
onPressEnter={handleRename}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: colors.darkGray,
|
||||
}}
|
||||
>
|
||||
{['todo', 'doing', 'done', 'low', 'medium', 'high'].includes(
|
||||
tableName.replace(/\s+/g, '').toLowerCase()
|
||||
)
|
||||
? t(`${tableName.replace(/\s+/g, '').toLowerCase()}SelectorText`)
|
||||
: tableName}{' '}
|
||||
({taskList.tasks.length})
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Button>
|
||||
{type === 'status' && !isRenaming && (
|
||||
<Dropdown menu={{ items }}>
|
||||
<Button icon={<EllipsisOutlined />} className="borderless-icon-btn" />
|
||||
</Dropdown>
|
||||
)}
|
||||
</Flex>
|
||||
<Collapse
|
||||
collapsible="header"
|
||||
className="border-l-[4px]"
|
||||
bordered={false}
|
||||
ghost={true}
|
||||
expandIcon={() => null}
|
||||
activeKey={isExpanded ? groupId || '1' : undefined}
|
||||
onChange={handlToggleExpand}
|
||||
items={[
|
||||
{
|
||||
key: groupId || '1',
|
||||
className: `custom-collapse-content-box relative after:content after:absolute after:h-full after:w-1 ${color} after:z-10 after:top-0 after:left-0`,
|
||||
children: (
|
||||
<TaskListCustom
|
||||
key={groupId}
|
||||
groupId={groupId}
|
||||
tasks={taskList.tasks}
|
||||
color={color || ''}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListTableWrapper;
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Flex, Skeleton } from 'antd';
|
||||
import TaskListFilters from '@/pages/projects/project-view-1/taskList/taskListFilters/TaskListFilters';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { ITaskListConfigV2, ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { fetchTaskGroups } from '@/features/tasks/taskSlice';
|
||||
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
||||
|
||||
import { columnList } from '@/pages/projects/project-view-1/taskList/taskListTable/columns/columnList';
|
||||
import StatusGroupTables from '../taskList/statusTables/StatusGroupTables';
|
||||
|
||||
const TaskList = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { taskGroups, loadingGroups } = useAppSelector(state => state.taskReducer);
|
||||
const { statusCategories } = useAppSelector(state => state.taskStatusReducer);
|
||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||
|
||||
const columnsVisibility = useAppSelector(
|
||||
state => state.projectViewTaskListColumnsReducer.columnsVisibility
|
||||
);
|
||||
|
||||
const visibleColumns = columnList.filter(
|
||||
column => columnsVisibility[column.key as keyof typeof columnsVisibility]
|
||||
);
|
||||
|
||||
const onTaskSelect = (taskId: string) => {
|
||||
console.log('taskId:', taskId);
|
||||
};
|
||||
|
||||
const onTaskExpand = (taskId: string) => {
|
||||
console.log('taskId:', taskId);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
const config: ITaskListConfigV2 = {
|
||||
id: projectId,
|
||||
field: 'id',
|
||||
order: 'desc',
|
||||
search: '',
|
||||
statuses: '',
|
||||
members: '',
|
||||
projects: '',
|
||||
isSubtasksInclude: true,
|
||||
};
|
||||
dispatch(fetchTaskGroups(config));
|
||||
}
|
||||
if (!statusCategories.length) {
|
||||
dispatch(fetchStatusesCategories());
|
||||
}
|
||||
}, [dispatch, projectId]);
|
||||
|
||||
return (
|
||||
<Flex vertical gap={16}>
|
||||
<TaskListFilters position="list" />
|
||||
<Skeleton active loading={loadingGroups}>
|
||||
{/* {taskGroups.map((group: ITaskListGroup) => (
|
||||
|
||||
))} */}
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskList;
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Flex } from 'antd';
|
||||
import TaskListFilters from './taskListFilters/TaskListFilters';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
||||
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
||||
import { ITaskListConfigV2 } from '@/types/tasks/taskList.types';
|
||||
import TanStackTable from '../task-list/task-list-custom';
|
||||
import TaskListCustom from '../task-list/task-list-custom';
|
||||
import TaskListTableWrapper from '../task-list/task-list-table-wrapper/task-list-table-wrapper';
|
||||
|
||||
const ProjectViewTaskList = () => {
|
||||
// sample data from task reducer
|
||||
const dispatch = useAppDispatch();
|
||||
const { taskGroups, loadingGroups } = useAppSelector(state => state.taskReducer);
|
||||
const { statusCategories } = useAppSelector(state => state.taskStatusReducer);
|
||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
const config: ITaskListConfigV2 = {
|
||||
id: projectId,
|
||||
field: 'id',
|
||||
order: 'desc',
|
||||
search: '',
|
||||
statuses: '',
|
||||
members: '',
|
||||
projects: '',
|
||||
isSubtasksInclude: true,
|
||||
};
|
||||
dispatch(fetchTaskGroups(config));
|
||||
}
|
||||
if (!statusCategories.length) {
|
||||
dispatch(fetchStatusesCategories());
|
||||
}
|
||||
}, [dispatch, projectId]);
|
||||
|
||||
return (
|
||||
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||
<TaskListFilters position="list" />
|
||||
|
||||
{taskGroups.map(group => (
|
||||
<TaskListTableWrapper
|
||||
key={group.id}
|
||||
taskList={group}
|
||||
name={group.name || ''}
|
||||
color={group.color_code || ''}
|
||||
groupId={group.id || ''}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectViewTaskList;
|
||||
@@ -1,67 +0,0 @@
|
||||
import { TaskType } from '@/types/task.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { Flex } from 'antd';
|
||||
import TaskListTableWrapper from '@/pages/projects/project-view-1/taskList/taskListTable/TaskListTableWrapper';
|
||||
import { createPortal } from 'react-dom';
|
||||
import BulkTasksActionContainer from '@/features/projects/bulkActions/BulkTasksActionContainer';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
|
||||
const StatusGroupTables = ({ group }: { group: ITaskListGroup }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// get bulk action detatils
|
||||
const selectedTaskIdsList = useAppSelector(state => state.bulkActionReducer.selectedTaskIdsList);
|
||||
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
// fuction for get a color regariding the status
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'todo':
|
||||
return themeMode === 'dark' ? '#3a3a3a' : '#d8d7d8';
|
||||
case 'doing':
|
||||
return themeMode === 'dark' ? '#3d506e' : '#c0d5f6';
|
||||
case 'done':
|
||||
return themeMode === 'dark' ? '#3b6149' : '#c2e4d0';
|
||||
default:
|
||||
return themeMode === 'dark' ? '#3a3a3a' : '#d8d7d8';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex gap={24} vertical>
|
||||
{group?.tasks?.map(status => (
|
||||
<TaskListTableWrapper
|
||||
key={status.id}
|
||||
taskList={group.tasks}
|
||||
tableId={status.id || ''}
|
||||
name={status.name || ''}
|
||||
type="status"
|
||||
statusCategory={status.status || ''}
|
||||
color={getStatusColor(status.status || '')}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* bulk action container ==> used tailwind to recreate the animation */}
|
||||
{createPortal(
|
||||
<div
|
||||
className={`absolute bottom-0 left-1/2 z-20 -translate-x-1/2 ${selectedTaskIdsList.length > 0 ? 'overflow-visible' : 'h-[1px] overflow-hidden'}`}
|
||||
>
|
||||
<div
|
||||
className={`${selectedTaskIdsList.length > 0 ? 'bottom-4' : 'bottom-0'} absolute left-1/2 z-[999] -translate-x-1/2 transition-all duration-300`}
|
||||
>
|
||||
<BulkTasksActionContainer
|
||||
selectedTaskIds={selectedTaskIdsList}
|
||||
closeContainer={() => dispatch(deselectAll())}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusGroupTables;
|
||||
@@ -1,69 +0,0 @@
|
||||
import { CaretDownFilled } from '@ant-design/icons';
|
||||
import { ConfigProvider, Flex, Select } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { colors } from '@/styles/colors';
|
||||
import ConfigPhaseButton from '@features/projects/singleProject/phase/ConfigPhaseButton';
|
||||
import { useSelectedProject } from '@/hooks/useSelectedProject';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import CreateStatusButton from '@/components/project-task-filters/create-status-button/create-status-button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { setGroupBy } from '@features/group-by-filter-dropdown/group-by-filter-dropdown-slice';
|
||||
|
||||
const GroupByFilterDropdown = ({ position }: { position: 'list' | 'board' }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
type GroupTypes = 'status' | 'priority' | 'phase' | 'members' | 'list';
|
||||
|
||||
const [activeGroup, setActiveGroup] = useState<GroupTypes>('status');
|
||||
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setActiveGroup(value as GroupTypes);
|
||||
dispatch(setGroupBy(value as GroupTypes));
|
||||
};
|
||||
|
||||
// get selected project from useSelectedPro
|
||||
const selectedProject = useSelectedProject();
|
||||
|
||||
//get phases details from phases slice
|
||||
const phase =
|
||||
useAppSelector(state => state.phaseReducer.phaseList).find(
|
||||
phase => phase.projectId === selectedProject?.id
|
||||
) || null;
|
||||
|
||||
const groupDropdownMenuItems = [
|
||||
{ key: 'status', value: 'status', label: t('statusText') },
|
||||
{ key: 'priority', value: 'priority', label: t('priorityText') },
|
||||
{
|
||||
key: 'phase',
|
||||
value: 'phase',
|
||||
label: phase ? phase?.phase : t('phaseText'),
|
||||
},
|
||||
{ key: 'members', value: 'members', label: t('memberText') },
|
||||
{ key: 'list', value: 'list', label: t('listText') },
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex align="center" gap={4} style={{ marginInlineStart: 12 }}>
|
||||
{t('groupByText')}:
|
||||
<Select
|
||||
defaultValue={'status'}
|
||||
options={groupDropdownMenuItems}
|
||||
onChange={handleChange}
|
||||
suffixIcon={<CaretDownFilled />}
|
||||
dropdownStyle={{ width: 'wrap-content' }}
|
||||
/>
|
||||
{(activeGroup === 'status' || activeGroup === 'phase') && (
|
||||
<ConfigProvider wave={{ disabled: true }}>
|
||||
{activeGroup === 'phase' && <ConfigPhaseButton color={colors.skyBlue} />}
|
||||
{activeGroup === 'status' && <CreateStatusButton />}
|
||||
</ConfigProvider>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupByFilterDropdown;
|
||||
@@ -1,134 +0,0 @@
|
||||
import { CaretDownFilled } from '@ant-design/icons';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Flex,
|
||||
Input,
|
||||
InputRef,
|
||||
List,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { colors } from '@/styles/colors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
const LabelsFilterDropdown = (props: { labels: ITaskLabel[] }) => {
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
const labelInputRef = useRef<InputRef>(null);
|
||||
const [selectedCount, setSelectedCount] = useState<number>(0);
|
||||
const [filteredLabelList, setFilteredLabelList] = useState<ITaskLabel[]>(props.labels);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredLabelList(props.labels);
|
||||
}, [props.labels]);
|
||||
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
// handle selected filters count
|
||||
const handleSelectedFiltersCount = (checked: boolean) => {
|
||||
setSelectedCount(prev => (checked ? prev + 1 : prev - 1));
|
||||
};
|
||||
|
||||
// function to focus labels input
|
||||
const handleLabelsDropdownOpen = (open: boolean) => {
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
labelInputRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchQuery = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const searchText = e.currentTarget.value;
|
||||
setSearchQuery(searchText);
|
||||
if (searchText.length === 0) {
|
||||
setFilteredLabelList(props.labels);
|
||||
return;
|
||||
}
|
||||
setFilteredLabelList(
|
||||
props.labels.filter(label => label.name?.toLowerCase().includes(searchText.toLowerCase()))
|
||||
);
|
||||
};
|
||||
|
||||
// custom dropdown content
|
||||
const labelsDropdownContent = (
|
||||
<Card
|
||||
className="custom-card"
|
||||
styles={{
|
||||
body: { padding: 8, width: 260, maxHeight: 250, overflow: 'hidden', overflowY: 'auto' },
|
||||
}}
|
||||
>
|
||||
<Flex vertical gap={8}>
|
||||
<Input
|
||||
ref={labelInputRef}
|
||||
value={searchQuery}
|
||||
onChange={e => handleSearchQuery(e)}
|
||||
placeholder={t('searchInputPlaceholder')}
|
||||
/>
|
||||
|
||||
<List style={{ padding: 0 }}>
|
||||
{filteredLabelList.length ? (
|
||||
filteredLabelList.map(label => (
|
||||
<List.Item
|
||||
className={`custom-list-item ${themeMode === 'dark' ? 'dark' : ''}`}
|
||||
key={label.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 8,
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
id={label.id}
|
||||
onChange={e => handleSelectedFiltersCount(e.target.checked)}
|
||||
/>
|
||||
|
||||
<Flex gap={8}>
|
||||
<Badge color={label.color_code} />
|
||||
{label.name}
|
||||
</Flex>
|
||||
</List.Item>
|
||||
))
|
||||
) : (
|
||||
<Empty description={t('noLabelsFound')} />
|
||||
)}
|
||||
</List>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlayClassName="custom-dropdown"
|
||||
trigger={['click']}
|
||||
dropdownRender={() => labelsDropdownContent}
|
||||
onOpenChange={handleLabelsDropdownOpen}
|
||||
>
|
||||
<Button
|
||||
icon={<CaretDownFilled />}
|
||||
iconPosition="end"
|
||||
style={{
|
||||
backgroundColor: selectedCount > 0 ? colors.paleBlue : colors.transparent,
|
||||
|
||||
color: selectedCount > 0 ? colors.darkGray : 'inherit',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
{t('labelsText')}
|
||||
{selectedCount > 0 && <Badge size="small" count={selectedCount} color={colors.skyBlue} />}
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default LabelsFilterDropdown;
|
||||
@@ -1,140 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { CaretDownFilled } from '@ant-design/icons';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Flex,
|
||||
Input,
|
||||
InputRef,
|
||||
List,
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { colors } from '@/styles/colors';
|
||||
import CustomAvatar from '@components/CustomAvatar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const MembersFilterDropdown = () => {
|
||||
const [selectedCount, setSelectedCount] = useState<number>(0);
|
||||
const membersInputRef = useRef<InputRef>(null);
|
||||
|
||||
const members = useAppSelector(state => state.memberReducer.membersList);
|
||||
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
|
||||
const membersList = [
|
||||
...members,
|
||||
useAppSelector(state => state.memberReducer.owner),
|
||||
];
|
||||
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
// this is for get the current string that type on search bar
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
// used useMemo hook for re render the list when searching
|
||||
const filteredMembersData = useMemo(() => {
|
||||
return membersList.filter(member =>
|
||||
member.memberName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [membersList, searchQuery]);
|
||||
|
||||
// handle selected filters count
|
||||
const handleSelectedFiltersCount = (checked: boolean) => {
|
||||
setSelectedCount(prev => (checked ? prev + 1 : prev - 1));
|
||||
};
|
||||
|
||||
// custom dropdown content
|
||||
const membersDropdownContent = (
|
||||
<Card className="custom-card" styles={{ body: { padding: 8 } }}>
|
||||
<Flex vertical gap={8}>
|
||||
<Input
|
||||
ref={membersInputRef}
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.currentTarget.value)}
|
||||
placeholder={t('searchInputPlaceholder')}
|
||||
/>
|
||||
|
||||
<List style={{ padding: 0 }}>
|
||||
{filteredMembersData.length ? (
|
||||
filteredMembersData.map(member => (
|
||||
<List.Item
|
||||
className={`custom-list-item ${themeMode === 'dark' ? 'dark' : ''}`}
|
||||
key={member.memberId}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
id={member.memberId}
|
||||
onChange={e => handleSelectedFiltersCount(e.target.checked)}
|
||||
/>
|
||||
<div>
|
||||
<CustomAvatar avatarName={member.memberName} />
|
||||
</div>
|
||||
<Flex vertical>
|
||||
{member.memberName}
|
||||
|
||||
<Typography.Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: colors.lightGray,
|
||||
}}
|
||||
>
|
||||
{member.memberEmail}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</List.Item>
|
||||
))
|
||||
) : (
|
||||
<Empty />
|
||||
)}
|
||||
</List>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
|
||||
// function to focus members input
|
||||
const handleMembersDropdownOpen = (open: boolean) => {
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
membersInputRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlayClassName="custom-dropdown"
|
||||
trigger={['click']}
|
||||
dropdownRender={() => membersDropdownContent}
|
||||
onOpenChange={handleMembersDropdownOpen}
|
||||
>
|
||||
<Button
|
||||
icon={<CaretDownFilled />}
|
||||
iconPosition="end"
|
||||
style={{
|
||||
backgroundColor: selectedCount > 0 ? colors.paleBlue : colors.transparent,
|
||||
|
||||
color: selectedCount > 0 ? colors.darkGray : 'inherit',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
{t('membersText')}
|
||||
{selectedCount > 0 && <Badge size="small" count={selectedCount} color={colors.skyBlue} />}
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default MembersFilterDropdown;
|
||||
@@ -1,73 +0,0 @@
|
||||
import { CaretDownFilled } from '@ant-design/icons';
|
||||
import { Badge, Button, Card, Checkbox, Dropdown, List, Space } from 'antd';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { colors } from '@/styles/colors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ITaskPriority } from '@/types/tasks/taskPriority.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
const PriorityFilterDropdown = (props: { priorities: ITaskPriority[] }) => {
|
||||
const [selectedCount, setSelectedCount] = useState<number>(0);
|
||||
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
// handle selected filters count
|
||||
const handleSelectedFiltersCount = (checked: boolean) => {
|
||||
setSelectedCount(prev => (checked ? prev + 1 : prev - 1));
|
||||
};
|
||||
|
||||
// custom dropdown content
|
||||
const priorityDropdownContent = (
|
||||
<Card className="custom-card" style={{ width: 120 }} styles={{ body: { padding: 0 } }}>
|
||||
<List style={{ padding: 0 }}>
|
||||
{props.priorities?.map(item => (
|
||||
<List.Item
|
||||
className={`custom-list-item ${themeMode === 'dark' ? 'dark' : ''}`}
|
||||
key={item.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<Checkbox id={item.id} onChange={e => handleSelectedFiltersCount(e.target.checked)} />
|
||||
<Badge color={item.color_code} />
|
||||
{item.name}
|
||||
</Space>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlayClassName="custom-dropdown"
|
||||
trigger={['click']}
|
||||
dropdownRender={() => priorityDropdownContent}
|
||||
>
|
||||
<Button
|
||||
icon={<CaretDownFilled />}
|
||||
iconPosition="end"
|
||||
style={{
|
||||
backgroundColor: selectedCount > 0 ? colors.paleBlue : colors.transparent,
|
||||
|
||||
color: selectedCount > 0 ? colors.darkGray : 'inherit',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
{t('priorityText')}
|
||||
{selectedCount > 0 && <Badge size="small" count={selectedCount} color={colors.skyBlue} />}
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default PriorityFilterDropdown;
|
||||
@@ -1,54 +0,0 @@
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Dropdown, Flex, Input, InputRef, Space } from 'antd';
|
||||
import { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const SearchDropdown = () => {
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
|
||||
const searchInputRef = useRef<InputRef>(null);
|
||||
|
||||
const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
};
|
||||
|
||||
// custom dropdown content
|
||||
const searchDropdownContent = (
|
||||
<Card className="custom-card" styles={{ body: { padding: 8, width: 360 } }}>
|
||||
<Flex vertical gap={8}>
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder={t('searchInputPlaceholder')}
|
||||
onChange={handleSearchInputChange}
|
||||
/>
|
||||
<Space>
|
||||
<Button type="primary">{t('searchButton')}</Button>
|
||||
<Button>{t('resetButton')}</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
|
||||
// function to focus search input
|
||||
const handleSearchDropdownOpen = (open: boolean) => {
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
searchInputRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlayClassName="custom-dropdown"
|
||||
trigger={['click']}
|
||||
dropdownRender={() => searchDropdownContent}
|
||||
onOpenChange={handleSearchDropdownOpen}
|
||||
>
|
||||
<Button icon={<SearchOutlined />} />
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchDropdown;
|
||||
@@ -1,96 +0,0 @@
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Checkbox, Dropdown, List, Space } from 'antd';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import {
|
||||
projectViewTaskListColumnsState,
|
||||
toggleColumnVisibility,
|
||||
} from '@features/projects/singleProject/taskListColumns/taskColumnsSlice';
|
||||
import { columnList } from '../taskListTable/columns/columnList';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||
|
||||
const ShowFieldsFilterDropdown = () => {
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
const dispatch = useAppDispatch();
|
||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
const customColumns = useAppSelector(state => state.taskReducer.customColumns);
|
||||
|
||||
const changableColumnList = [
|
||||
...columnList.filter(column => !['selector', 'task'].includes(column.key)),
|
||||
...(customColumns || []).map(col => ({
|
||||
key: col.key,
|
||||
columnHeader: col.custom_column_obj.columnHeader,
|
||||
isCustomColumn: col.custom_column,
|
||||
}))
|
||||
];
|
||||
|
||||
const columnsVisibility = useAppSelector(
|
||||
state => state.projectViewTaskListColumnsReducer.columnsVisibility
|
||||
);
|
||||
|
||||
const handleColumnToggle = (columnKey: string, isCustomColumn: boolean = false) => {
|
||||
if (isCustomColumn) {
|
||||
// dispatch(toggleCustomColumnVisibility(columnKey));
|
||||
} else {
|
||||
dispatch(toggleColumnVisibility(columnKey));
|
||||
}
|
||||
trackMixpanelEvent('task_list_column_visibility_changed', {
|
||||
column: columnKey,
|
||||
isCustomColumn,
|
||||
visible: !columnsVisibility[columnKey as keyof typeof columnsVisibility],
|
||||
});
|
||||
};
|
||||
|
||||
const showFieldsDropdownContent = (
|
||||
<Card
|
||||
className="custom-card"
|
||||
style={{
|
||||
height: 300,
|
||||
overflowY: 'auto',
|
||||
minWidth: 130,
|
||||
}}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<List style={{ padding: 0 }}>
|
||||
{changableColumnList.map(col => (
|
||||
<List.Item
|
||||
key={col.key}
|
||||
className={`custom-list-item ${themeMode === 'dark' ? 'dark' : ''}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => handleColumnToggle(col.key, col.custom_column)}
|
||||
>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={
|
||||
columnsVisibility[
|
||||
col.key as keyof projectViewTaskListColumnsState['columnsVisibility']
|
||||
]
|
||||
}
|
||||
/>
|
||||
{col.custom_column
|
||||
? col.columnHeader
|
||||
: t(col.key === 'phases' ? 'phasesText' : `${col.columnHeader}Text`)}
|
||||
</Space>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown overlay={showFieldsDropdownContent} trigger={['click']} placement="bottomRight">
|
||||
<Button icon={<MoreOutlined />}>{t('showFieldsText')}</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowFieldsFilterDropdown;
|
||||
@@ -1,110 +0,0 @@
|
||||
import { CaretDownFilled, SortAscendingOutlined, SortDescendingOutlined } from '@ant-design/icons';
|
||||
import { Badge, Button, Card, Checkbox, Dropdown, List, Space } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { colors } from '../../../../../styles/colors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
const SortFilterDropdown = () => {
|
||||
const [selectedCount, setSelectedCount] = useState<number>(0);
|
||||
const [sortState, setSortState] = useState<Record<string, 'ascending' | 'descending'>>({});
|
||||
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
|
||||
// handle selected filters count
|
||||
const handleSelectedFiltersCount = (checked: boolean) => {
|
||||
setSelectedCount(prev => (checked ? prev + 1 : prev - 1));
|
||||
};
|
||||
|
||||
// fuction for handle sort
|
||||
const handleSort = (key: string) => {
|
||||
setSortState(prev => ({
|
||||
...prev,
|
||||
[key]: prev[key] === 'ascending' ? 'descending' : 'ascending',
|
||||
}));
|
||||
};
|
||||
|
||||
// sort dropdown items
|
||||
type SortFieldsType = {
|
||||
key: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const sortFieldsList: SortFieldsType[] = [
|
||||
{ key: 'task', label: t('taskText') },
|
||||
{ key: 'status', label: t('statusText') },
|
||||
{ key: 'priority', label: t('priorityText') },
|
||||
{ key: 'startDate', label: t('startDateText') },
|
||||
{ key: 'endDate', label: t('endDateText') },
|
||||
{ key: 'completedDate', label: t('completedDateText') },
|
||||
{ key: 'createdDate', label: t('createdDateText') },
|
||||
{ key: 'lastUpdated', label: t('lastUpdatedText') },
|
||||
];
|
||||
|
||||
// custom dropdown content
|
||||
const sortDropdownContent = (
|
||||
<Card className="custom-card" styles={{ body: { padding: 0 } }}>
|
||||
<List style={{ padding: 0 }}>
|
||||
{sortFieldsList.map(item => (
|
||||
<List.Item
|
||||
className={`custom-list-item ${themeMode === 'dark' ? 'dark' : ''}`}
|
||||
key={item.key}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<Checkbox
|
||||
id={item.key}
|
||||
onChange={e => handleSelectedFiltersCount(e.target.checked)}
|
||||
/>
|
||||
{item.label}
|
||||
</Space>
|
||||
<Button
|
||||
onClick={() => handleSort(item.key)}
|
||||
icon={
|
||||
sortState[item.key] === 'ascending' ? (
|
||||
<SortAscendingOutlined />
|
||||
) : (
|
||||
<SortDescendingOutlined />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlayClassName="custom-dropdown"
|
||||
trigger={['click']}
|
||||
dropdownRender={() => sortDropdownContent}
|
||||
>
|
||||
<Button
|
||||
icon={<CaretDownFilled />}
|
||||
iconPosition="end"
|
||||
style={{
|
||||
backgroundColor: selectedCount > 0 ? colors.paleBlue : colors.transparent,
|
||||
|
||||
color: selectedCount > 0 ? colors.darkGray : 'inherit',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<SortAscendingOutlined />
|
||||
{t('sortText')}
|
||||
{selectedCount > 0 && <Badge size="small" count={selectedCount} color={colors.skyBlue} />}
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortFilterDropdown;
|
||||
@@ -1,73 +0,0 @@
|
||||
import { Checkbox, Flex, Typography } from 'antd';
|
||||
import SearchDropdown from './SearchDropdown';
|
||||
import SortFilterDropdown from './SortFilterDropdown';
|
||||
import LabelsFilterDropdown from './LabelsFilterDropdown';
|
||||
import MembersFilterDropdown from './MembersFilterDropdown';
|
||||
import GroupByFilterDropdown from './GroupByFilterDropdown';
|
||||
import ShowFieldsFilterDropdown from './ShowFieldsFilterDropdown';
|
||||
import PriorityFilterDropdown from './PriorityFilterDropdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useEffect } from 'react';
|
||||
import { fetchPriorities } from '@/features/taskAttributes/taskPrioritySlice';
|
||||
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
|
||||
interface TaskListFiltersProps {
|
||||
position: 'board' | 'list';
|
||||
}
|
||||
|
||||
const TaskListFilters: React.FC<TaskListFiltersProps> = ({ position }) => {
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Selectors
|
||||
const priorities = useAppSelector(state => state.priorityReducer.priorities);
|
||||
const labels = useAppSelector(state => state.taskLabelsReducer.labels);
|
||||
|
||||
// Fetch initial data
|
||||
useEffect(() => {
|
||||
const fetchInitialData = async () => {
|
||||
if (!priorities.length) {
|
||||
await dispatch(fetchPriorities());
|
||||
}
|
||||
if (!labels.length) {
|
||||
await dispatch(fetchLabels());
|
||||
}
|
||||
};
|
||||
|
||||
fetchInitialData();
|
||||
}, [dispatch, priorities.length, labels.length]);
|
||||
|
||||
return (
|
||||
<Flex gap={8} align="center" justify="space-between">
|
||||
<Flex gap={8} wrap={'wrap'}>
|
||||
{/* search dropdown */}
|
||||
<SearchDropdown />
|
||||
{/* sort dropdown */}
|
||||
<SortFilterDropdown />
|
||||
{/* prioriy dropdown */}
|
||||
<PriorityFilterDropdown priorities={priorities} />
|
||||
{/* labels dropdown */}
|
||||
<LabelsFilterDropdown labels={labels} />
|
||||
{/* members dropdown */}
|
||||
<MembersFilterDropdown />
|
||||
{/* group by dropdown */}
|
||||
{<GroupByFilterDropdown position={position} />}
|
||||
</Flex>
|
||||
|
||||
{position === 'list' && (
|
||||
<Flex gap={12} wrap={'wrap'}>
|
||||
<Flex gap={4} align="center">
|
||||
<Checkbox />
|
||||
<Typography.Text>{t('showArchivedText')}</Typography.Text>
|
||||
</Flex>
|
||||
{/* show fields dropdown */}
|
||||
<ShowFieldsFilterDropdown />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListFilters;
|
||||
@@ -1,425 +0,0 @@
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { columnList } from './columns/columnList';
|
||||
import AddTaskListRow from './taskListTableRows/AddTaskListRow';
|
||||
import { Checkbox, Flex, Tag, Tooltip } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelectedProject } from '@/hooks/useSelectedProject';
|
||||
import TaskCell from './taskListTableCells/TaskCell';
|
||||
import AddSubTaskListRow from './taskListTableRows/AddSubTaskListRow';
|
||||
import { colors } from '@/styles/colors';
|
||||
import TaskContextMenu from './contextMenu/TaskContextMenu';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { deselectAll } from '@features/projects/bulkActions/bulkActionSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { HolderOutlined } from '@ant-design/icons';
|
||||
|
||||
const TaskListTable = ({
|
||||
taskList,
|
||||
tableId,
|
||||
}: {
|
||||
taskList: IProjectTask[] | null;
|
||||
tableId: string;
|
||||
}) => {
|
||||
// these states manage the necessary states
|
||||
const [hoverRow, setHoverRow] = useState<string | null>(null);
|
||||
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
||||
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
||||
const [expandedTasks, setExpandedTasks] = useState<string[]>([]);
|
||||
const [isSelectAll, setIsSelectAll] = useState(false);
|
||||
// context menu state
|
||||
const [contextMenuVisible, setContextMenuVisible] = useState(false);
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
// state to check scroll
|
||||
const [scrollingTables, setScrollingTables] = useState<{
|
||||
[key: string]: boolean;
|
||||
}>({});
|
||||
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// get data theme data from redux
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
// get the selected project details
|
||||
const selectedProject = useSelectedProject();
|
||||
|
||||
// get columns list details
|
||||
const columnsVisibility = useAppSelector( state => state.projectViewTaskListColumnsReducer.columnList );
|
||||
const visibleColumns = columnList.filter(
|
||||
column => columnsVisibility[column.key as keyof typeof columnsVisibility]
|
||||
);
|
||||
|
||||
// toggle subtasks visibility
|
||||
const toggleTaskExpansion = (taskId: string) => {
|
||||
setExpandedTasks(prev =>
|
||||
prev.includes(taskId) ? prev.filter(id => id !== taskId) : [...prev, taskId]
|
||||
);
|
||||
};
|
||||
|
||||
// toggle all task select when header checkbox click
|
||||
const toggleSelectAll = () => {
|
||||
if (isSelectAll) {
|
||||
setSelectedRows([]);
|
||||
dispatch(deselectAll());
|
||||
} else {
|
||||
const allTaskIds =
|
||||
taskList?.flatMap(task => [
|
||||
task.id,
|
||||
...(task.sub_tasks?.map(subtask => subtask.id) || []),
|
||||
]) || [];
|
||||
|
||||
// setSelectedRows(allTaskIds);
|
||||
// dispatch(selectTaskIds(allTaskIds));
|
||||
// console.log('selected tasks and subtasks (all):', allTaskIds);
|
||||
}
|
||||
setIsSelectAll(!isSelectAll);
|
||||
};
|
||||
|
||||
// toggle selected row
|
||||
const toggleRowSelection = (task: IProjectTask) => {
|
||||
setSelectedRows(prevSelectedRows =>
|
||||
prevSelectedRows.includes(task.id || '')
|
||||
? prevSelectedRows.filter(id => id !== task.id || '')
|
||||
: [...prevSelectedRows, task.id || '']
|
||||
);
|
||||
};
|
||||
|
||||
// this use effect for realtime update the selected rows
|
||||
useEffect(() => {
|
||||
console.log('Selected tasks and subtasks:', selectedRows);
|
||||
}, [selectedRows]);
|
||||
|
||||
// select one row this triggers only in handle the context menu ==> righ click mouse event
|
||||
const selectOneRow = (task: IProjectTask) => {
|
||||
setSelectedRows([task.id || '']);
|
||||
|
||||
// log the task object when selected
|
||||
if (!selectedRows.includes(task.id || '')) {
|
||||
console.log('Selected task:', task);
|
||||
}
|
||||
};
|
||||
|
||||
// handle custom task context menu
|
||||
const handleContextMenu = (e: React.MouseEvent, task: IProjectTask) => {
|
||||
e.preventDefault();
|
||||
setSelectedTaskId(task.id || '');
|
||||
selectOneRow(task);
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
||||
setContextMenuVisible(true);
|
||||
};
|
||||
|
||||
// trigger the table scrolling
|
||||
useEffect(() => {
|
||||
const tableContainer = document.querySelector(`.tasklist-container-${tableId}`);
|
||||
const handleScroll = () => {
|
||||
if (tableContainer) {
|
||||
setScrollingTables(prev => ({
|
||||
...prev,
|
||||
[tableId]: tableContainer.scrollLeft > 0,
|
||||
}));
|
||||
}
|
||||
};
|
||||
tableContainer?.addEventListener('scroll', handleScroll);
|
||||
return () => tableContainer?.removeEventListener('scroll', handleScroll);
|
||||
}, [tableId]);
|
||||
|
||||
// layout styles for table and the columns
|
||||
const customBorderColor = themeMode === 'dark' && ' border-[#303030]';
|
||||
|
||||
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]'}`;
|
||||
|
||||
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'}`;
|
||||
|
||||
// function to render the column content based on column key
|
||||
const renderColumnContent = (
|
||||
columnKey: string,
|
||||
task: IProjectTask,
|
||||
isSubtask: boolean = false
|
||||
) => {
|
||||
switch (columnKey) {
|
||||
// task ID column
|
||||
case 'taskId':
|
||||
return (
|
||||
<Tooltip title={task.id} className="flex justify-center">
|
||||
<Tag>{task.task_key}</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
// task column
|
||||
case 'task':
|
||||
return (
|
||||
// custom task cell component
|
||||
<TaskCell
|
||||
task={task}
|
||||
isSubTask={isSubtask}
|
||||
expandedTasks={expandedTasks}
|
||||
setSelectedTaskId={setSelectedTaskId}
|
||||
toggleTaskExpansion={toggleTaskExpansion}
|
||||
/>
|
||||
);
|
||||
|
||||
// description column
|
||||
case 'description':
|
||||
return (
|
||||
<div style={{ width: 260 }}>
|
||||
{/* <Typography.Paragraph ellipsis={{ expandable: false }} style={{ marginBlockEnd: 0 }} >
|
||||
{task.description || ''}
|
||||
</Typography.Paragraph> */}
|
||||
</div>
|
||||
);
|
||||
|
||||
// progress column
|
||||
case 'progress': {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
// members column
|
||||
case 'members':
|
||||
return <div></div>;
|
||||
|
||||
// labels column
|
||||
case 'labels':
|
||||
return <div></div>;
|
||||
|
||||
// phase column
|
||||
case 'phases':
|
||||
return <div></div>;
|
||||
|
||||
// status column
|
||||
case 'status':
|
||||
return <div></div>;
|
||||
|
||||
// priority column
|
||||
case 'priority':
|
||||
return <div></div>;
|
||||
|
||||
// // time tracking column
|
||||
// case 'timeTracking':
|
||||
// return (
|
||||
// <TimeTracker
|
||||
// taskId={task.id}
|
||||
// initialTime={task.timer_start_time || 0}
|
||||
// />
|
||||
// );
|
||||
|
||||
// estimation column
|
||||
case 'estimation':
|
||||
return <div></div>;
|
||||
|
||||
// start date column
|
||||
case 'startDate':
|
||||
return <div></div>;
|
||||
|
||||
// due date column
|
||||
case 'dueDate':
|
||||
return <div></div>;
|
||||
|
||||
// completed date column
|
||||
case 'completedDate':
|
||||
return <div></div>;
|
||||
|
||||
// created date column
|
||||
case 'createdDate':
|
||||
return <div></div>;
|
||||
|
||||
// last updated column
|
||||
case 'lastUpdated':
|
||||
return <div></div>;
|
||||
|
||||
// recorder column
|
||||
case 'reporter':
|
||||
return <div></div>;
|
||||
|
||||
// default case for unsupported columns
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`border-x border-b ${customBorderColor}`}>
|
||||
<div className={`tasklist-container-${tableId} min-h-0 max-w-full overflow-x-auto`}>
|
||||
<table className={`rounded-2 w-full min-w-max border-collapse`}>
|
||||
<thead className="h-[42px]">
|
||||
<tr>
|
||||
{/* this cell render the select all task checkbox */}
|
||||
<th
|
||||
key={'selector'}
|
||||
className={`${customHeaderColumnStyles('selector')}`}
|
||||
style={{ width: 56, fontWeight: 500 }}
|
||||
>
|
||||
<Flex justify="flex-end">
|
||||
<Checkbox checked={isSelectAll} onChange={toggleSelectAll} />
|
||||
</Flex>
|
||||
</th>
|
||||
{/* other header cells */}
|
||||
{visibleColumns.map(column => (
|
||||
<th
|
||||
key={column.key}
|
||||
className={`${customHeaderColumnStyles(column.key)}`}
|
||||
style={{ width: column.width, fontWeight: 500 }}
|
||||
>
|
||||
{column.key === 'phases'
|
||||
? column.columnHeader
|
||||
: t(`${column.columnHeader}Column`)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{taskList?.map(task => (
|
||||
<React.Fragment key={task.id}>
|
||||
<tr
|
||||
key={task.id}
|
||||
onContextMenu={e => handleContextMenu(e, task)}
|
||||
className={`${taskList.length === 0 ? 'h-0' : 'h-[42px]'}`}
|
||||
>
|
||||
{/* this cell render the select the related task checkbox */}
|
||||
<td
|
||||
key={'selector'}
|
||||
className={customBodyColumnStyles('selector')}
|
||||
style={{
|
||||
width: 56,
|
||||
backgroundColor: selectedRows.includes(task.id || '')
|
||||
? themeMode === 'dark'
|
||||
? colors.skyBlue
|
||||
: '#dceeff'
|
||||
: hoverRow === task.id
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#f8f7f9'
|
||||
: themeMode === 'dark'
|
||||
? '#181818'
|
||||
: '#fff',
|
||||
}}
|
||||
>
|
||||
<Flex gap={8} align="center">
|
||||
<HolderOutlined />
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(task.id || '')}
|
||||
onChange={() => toggleRowSelection(task)}
|
||||
/>
|
||||
</Flex>
|
||||
</td>
|
||||
{/* other cells */}
|
||||
{visibleColumns.map(column => (
|
||||
<td
|
||||
key={column.key}
|
||||
className={customBodyColumnStyles(column.key)}
|
||||
style={{
|
||||
width: column.width,
|
||||
backgroundColor: selectedRows.includes(task.id || '')
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#dceeff'
|
||||
: hoverRow === task.id
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#f8f7f9'
|
||||
: themeMode === 'dark'
|
||||
? '#181818'
|
||||
: '#fff',
|
||||
}}
|
||||
>
|
||||
{renderColumnContent(column.key, task)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
{/* this is for sub tasks */}
|
||||
{expandedTasks.includes(task.id || '') &&
|
||||
task?.sub_tasks?.map(subtask => (
|
||||
<tr
|
||||
key={subtask.id}
|
||||
onContextMenu={e => handleContextMenu(e, subtask)}
|
||||
onMouseEnter={() => setHoverRow(subtask.id || '')}
|
||||
onMouseLeave={() => setHoverRow(null)}
|
||||
className={`${taskList.length === 0 ? 'h-0' : 'h-[42px]'}`}
|
||||
>
|
||||
{/* this cell render the select the related task checkbox */}
|
||||
<td
|
||||
key={'selector'}
|
||||
className={customBodyColumnStyles('selector')}
|
||||
style={{
|
||||
width: 20,
|
||||
backgroundColor: selectedRows.includes(subtask.id || '')
|
||||
? themeMode === 'dark'
|
||||
? colors.skyBlue
|
||||
: '#dceeff'
|
||||
: hoverRow === subtask.id
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#f8f7f9'
|
||||
: themeMode === 'dark'
|
||||
? '#181818'
|
||||
: '#fff',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(subtask.id || '')}
|
||||
onChange={() => toggleRowSelection(subtask)}
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* other sub tasks cells */}
|
||||
{visibleColumns.map(column => (
|
||||
<td
|
||||
key={column.key}
|
||||
className={customBodyColumnStyles(column.key)}
|
||||
style={{
|
||||
width: column.width,
|
||||
backgroundColor: selectedRows.includes(subtask.id || '')
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#dceeff'
|
||||
: hoverRow === subtask.id
|
||||
? themeMode === 'dark'
|
||||
? '#000'
|
||||
: '#f8f7f9'
|
||||
: themeMode === 'dark'
|
||||
? '#181818'
|
||||
: '#fff',
|
||||
}}
|
||||
>
|
||||
{renderColumnContent(column.key, subtask, true)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{expandedTasks.includes(task.id || '') && (
|
||||
<tr>
|
||||
<td colSpan={visibleColumns.length}>
|
||||
<AddSubTaskListRow />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* add a main task to the table */}
|
||||
<AddTaskListRow />
|
||||
|
||||
{/* custom task context menu */}
|
||||
<TaskContextMenu
|
||||
visible={contextMenuVisible}
|
||||
position={contextMenuPosition}
|
||||
selectedTask={selectedRows[0]}
|
||||
onClose={() => setContextMenuVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListTable;
|
||||
@@ -1,216 +0,0 @@
|
||||
import { Badge, Button, Collapse, ConfigProvider, Dropdown, Flex, Input, Typography } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { TaskType } from '../../../../../types/task.types';
|
||||
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { colors } from '../../../../../styles/colors';
|
||||
import './taskListTableWrapper.css';
|
||||
import TaskListTable from './TaskListTable';
|
||||
import { MenuProps } from 'antd/lib';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
|
||||
type TaskListTableWrapperProps = {
|
||||
taskList: IProjectTask[];
|
||||
tableId: string;
|
||||
type: string;
|
||||
name: string;
|
||||
color: string;
|
||||
statusCategory?: string | null;
|
||||
priorityCategory?: string | null;
|
||||
onRename?: (name: string) => void;
|
||||
onStatusCategoryChange?: (category: string) => void;
|
||||
};
|
||||
|
||||
const TaskListTableWrapper = ({
|
||||
taskList,
|
||||
tableId,
|
||||
name,
|
||||
type,
|
||||
color,
|
||||
statusCategory = null,
|
||||
priorityCategory = null,
|
||||
onRename,
|
||||
onStatusCategoryChange,
|
||||
}: TaskListTableWrapperProps) => {
|
||||
const [tableName, setTableName] = useState<string>(name);
|
||||
const [isRenaming, setIsRenaming] = useState<boolean>(false);
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(true);
|
||||
const [currentCategory, setCurrentCategory] = useState<string | null>(statusCategory);
|
||||
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
// function to handle toggle expand
|
||||
const handlToggleExpand = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
// this is for get the color for every typed tables
|
||||
const getBgColorClassName = (type: string) => {
|
||||
switch (type) {
|
||||
case 'status':
|
||||
if (currentCategory === 'todo')
|
||||
return themeMode === 'dark' ? 'after:bg-[#3a3a3a]' : 'after:bg-[#d8d7d8]';
|
||||
else if (currentCategory === 'doing')
|
||||
return themeMode === 'dark' ? 'after:bg-[#3d506e]' : 'after:bg-[#c0d5f6]';
|
||||
else if (currentCategory === 'done')
|
||||
return themeMode === 'dark' ? 'after:bg-[#3b6149]' : 'after:bg-[#c2e4d0]';
|
||||
else return themeMode === 'dark' ? 'after:bg-[#3a3a3a]' : 'after:bg-[#d8d7d8]';
|
||||
|
||||
case 'priority':
|
||||
if (priorityCategory === 'low')
|
||||
return themeMode === 'dark' ? 'after:bg-[#3b6149]' : 'after:bg-[#c2e4d0]';
|
||||
else if (priorityCategory === 'medium')
|
||||
return themeMode === 'dark' ? 'after:bg-[#916c33]' : 'after:bg-[#f9e3b1]';
|
||||
else if (priorityCategory === 'high')
|
||||
return themeMode === 'dark' ? 'after:bg-[#8b3a3b]' : 'after:bg-[#f6bfc0]';
|
||||
else return themeMode === 'dark' ? 'after:bg-[#916c33]' : 'after:bg-[#f9e3b1]';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// these codes only for status type tables
|
||||
// function to handle rename this functionality only available for status type tables
|
||||
const handleRename = () => {
|
||||
if (onRename) {
|
||||
onRename(tableName);
|
||||
}
|
||||
setIsRenaming(false);
|
||||
};
|
||||
|
||||
// function to handle category change
|
||||
const handleCategoryChange = (category: string) => {
|
||||
setCurrentCategory(category);
|
||||
if (onStatusCategoryChange) {
|
||||
onStatusCategoryChange(category);
|
||||
}
|
||||
};
|
||||
|
||||
// find the available status for the currently active project
|
||||
const statusList = useAppSelector(state => state.statusReducer.status);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'todo':
|
||||
return '#d8d7d8';
|
||||
case 'doing':
|
||||
return '#c0d5f6';
|
||||
case 'done':
|
||||
return '#c2e4d0';
|
||||
default:
|
||||
return '#d8d7d8';
|
||||
}
|
||||
};
|
||||
|
||||
// dropdown options
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
icon: <EditOutlined />,
|
||||
label: 'Rename',
|
||||
onClick: () => setIsRenaming(true),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
icon: <RetweetOutlined />,
|
||||
label: 'Change category',
|
||||
children: statusList?.map(status => ({
|
||||
key: status.id,
|
||||
label: (
|
||||
<Flex gap={8} onClick={() => handleCategoryChange(status.category)}>
|
||||
<Badge color={getStatusColor(status.category)} />
|
||||
{status.name}
|
||||
</Flex>
|
||||
),
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
wave={{ disabled: true }}
|
||||
theme={{
|
||||
components: {
|
||||
Collapse: {
|
||||
headerPadding: 0,
|
||||
contentPadding: 0,
|
||||
},
|
||||
|
||||
Select: {
|
||||
colorBorder: colors.transparent,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Flex vertical>
|
||||
<Flex style={{ transform: 'translateY(6px)' }}>
|
||||
<Button
|
||||
className="custom-collapse-button"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
border: 'none',
|
||||
borderBottomLeftRadius: isExpanded ? 0 : 4,
|
||||
borderBottomRightRadius: isExpanded ? 0 : 4,
|
||||
color: themeMode === 'dark' ? '#ffffffd9' : colors.darkGray,
|
||||
}}
|
||||
icon={<RightOutlined rotate={isExpanded ? 90 : 0} />}
|
||||
onClick={handlToggleExpand}
|
||||
>
|
||||
{isRenaming ? (
|
||||
<Input
|
||||
size="small"
|
||||
value={tableName}
|
||||
onChange={e => setTableName(e.target.value)}
|
||||
onBlur={handleRename}
|
||||
onPressEnter={handleRename}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: themeMode === 'dark' ? '#ffffffd9' : colors.darkGray,
|
||||
}}
|
||||
>
|
||||
{/* check the default values available in the table names ==> this check for localization */}
|
||||
{['todo', 'doing', 'done', 'low', 'medium', 'high'].includes(
|
||||
tableName.replace(/\s+/g, '').toLowerCase()
|
||||
)
|
||||
? t(`${tableName.replace(/\s+/g, '').toLowerCase()}SelectorText`)
|
||||
: tableName}{' '}
|
||||
({taskList.length})
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Button>
|
||||
{type === 'status' && !isRenaming && (
|
||||
<Dropdown menu={{ items }}>
|
||||
<Button icon={<EllipsisOutlined />} className="borderless-icon-btn" />
|
||||
</Dropdown>
|
||||
)}
|
||||
</Flex>
|
||||
<Collapse
|
||||
collapsible="header"
|
||||
className="border-l-[4px]"
|
||||
bordered={false}
|
||||
ghost={true}
|
||||
expandIcon={() => null}
|
||||
activeKey={isExpanded ? '1' : undefined}
|
||||
onChange={handlToggleExpand}
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
className: `custom-collapse-content-box relative after:content after:absolute after:h-full after:w-1 ${getBgColorClassName(type)} after:z-10 after:top-0 after:left-0`,
|
||||
children: <TaskListTable taskList={taskList} tableId={tableId} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListTableWrapper;
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import PhaseHeader from '../../../../../../features/projects/singleProject/phase/PhaseHeader';
|
||||
|
||||
export type CustomTableColumnsType = {
|
||||
key: string;
|
||||
columnHeader: ReactNode | null;
|
||||
width: number;
|
||||
};
|
||||
|
||||
const phaseHeader = React.createElement(PhaseHeader);
|
||||
|
||||
export const columnList: CustomTableColumnsType[] = [
|
||||
{ key: 'taskId', columnHeader: 'key', width: 20 },
|
||||
{ key: 'task', columnHeader: 'task', width: 400 },
|
||||
{
|
||||
key: 'description',
|
||||
columnHeader: 'description',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'progress',
|
||||
columnHeader: 'progress',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
key: 'members',
|
||||
columnHeader: 'members',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'labels',
|
||||
columnHeader: 'labels',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'phases',
|
||||
columnHeader: phaseHeader,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
columnHeader: 'status',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
columnHeader: 'priority',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
key: 'timeTracking',
|
||||
columnHeader: 'timeTracking',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'estimation',
|
||||
columnHeader: 'estimation',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'startDate',
|
||||
columnHeader: 'startDate',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'dueDate',
|
||||
columnHeader: 'dueDate',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'completedDate',
|
||||
columnHeader: 'completedDate',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'createdDate',
|
||||
columnHeader: 'createdDate',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'lastUpdated',
|
||||
columnHeader: 'lastUpdated',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'reporter',
|
||||
columnHeader: 'reporter',
|
||||
width: 150,
|
||||
},
|
||||
];
|
||||
@@ -1,90 +0,0 @@
|
||||
import {
|
||||
DeleteOutlined,
|
||||
DoubleRightOutlined,
|
||||
InboxOutlined,
|
||||
RetweetOutlined,
|
||||
UserAddOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Badge, Dropdown, Flex, Typography } from 'antd';
|
||||
import { MenuProps } from 'antd/lib';
|
||||
import React from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
type TaskContextMenuProps = {
|
||||
visible: boolean;
|
||||
position: { x: number; y: number };
|
||||
selectedTask: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const TaskContextMenu = ({ visible, position, selectedTask, onClose }: TaskContextMenuProps) => {
|
||||
// find the available status for the currently active project
|
||||
const statusList = useAppSelector(state => state.statusReducer.status);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'todo':
|
||||
return '#d8d7d8';
|
||||
case 'doing':
|
||||
return '#c0d5f6';
|
||||
case 'done':
|
||||
return '#c2e4d0';
|
||||
default:
|
||||
return '#d8d7d8';
|
||||
}
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
icon: <UserAddOutlined />,
|
||||
label: ' Assign to me',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
icon: <RetweetOutlined />,
|
||||
label: 'Move to',
|
||||
children: statusList?.map(status => ({
|
||||
key: status.id,
|
||||
label: (
|
||||
<Flex gap={8}>
|
||||
<Badge color={getStatusColor(status.category)} />
|
||||
{status.name}
|
||||
</Flex>
|
||||
),
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
icon: <InboxOutlined />,
|
||||
label: 'Archive',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
icon: <DoubleRightOutlined />,
|
||||
label: 'Convert to Sub task',
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
icon: <DeleteOutlined />,
|
||||
label: ' Delete',
|
||||
},
|
||||
];
|
||||
|
||||
return visible ? (
|
||||
<Dropdown menu={{ items }} trigger={['contextMenu']} open={visible} onOpenChange={onClose}>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: position.y,
|
||||
left: position.x,
|
||||
zIndex: 1000,
|
||||
width: 1,
|
||||
height: 1,
|
||||
}}
|
||||
></div>
|
||||
</Dropdown>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default TaskContextMenu;
|
||||
@@ -1,121 +0,0 @@
|
||||
// TaskNameCell.tsx
|
||||
import React from 'react';
|
||||
import { Flex, Typography, Button } from 'antd';
|
||||
import {
|
||||
DoubleRightOutlined,
|
||||
DownOutlined,
|
||||
RightOutlined,
|
||||
ExpandAltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { colors } from '@/styles/colors';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
|
||||
type TaskCellProps = {
|
||||
task: IProjectTask;
|
||||
isSubTask?: boolean;
|
||||
expandedTasks: string[];
|
||||
setSelectedTaskId: (taskId: string) => void;
|
||||
toggleTaskExpansion: (taskId: string) => void;
|
||||
};
|
||||
|
||||
const TaskCell = ({
|
||||
task,
|
||||
isSubTask = false,
|
||||
expandedTasks,
|
||||
setSelectedTaskId,
|
||||
toggleTaskExpansion,
|
||||
}: TaskCellProps) => {
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// render the toggle arrow icon for tasks with subtasks
|
||||
const renderToggleButtonForHasSubTasks = (taskId: string, hasSubtasks: boolean) => {
|
||||
if (!hasSubtasks) return null;
|
||||
return (
|
||||
<button
|
||||
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]"
|
||||
>
|
||||
{expandedTasks.includes(taskId) ? <DownOutlined /> : <RightOutlined />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// show expand button on hover for tasks without subtasks
|
||||
const renderToggleButtonForNonSubtasks = (taskId: string, isSubTask: boolean) => {
|
||||
return !isSubTask ? (
|
||||
<button
|
||||
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]"
|
||||
>
|
||||
{expandedTasks.includes(taskId) ? <DownOutlined /> : <RightOutlined />}
|
||||
</button>
|
||||
) : (
|
||||
<div className="h-4 w-4"></div>
|
||||
);
|
||||
};
|
||||
|
||||
// render the double arrow icon and count label for tasks with subtasks
|
||||
const renderSubtasksCountLabel = (taskId: string, isSubTask: boolean, subTasksCount: number) => {
|
||||
return (
|
||||
!isSubTask && (
|
||||
<Button
|
||||
onClick={() => toggleTaskExpansion(taskId)}
|
||||
size="small"
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
paddingInline: 4,
|
||||
alignItems: 'center',
|
||||
justifyItems: 'center',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<Typography.Text style={{ fontSize: 12, lineHeight: 1 }}>{subTasksCount}</Typography.Text>
|
||||
<DoubleRightOutlined style={{ fontSize: 10 }} />
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex align="center" justify="space-between">
|
||||
<Flex gap={8} align="center">
|
||||
{!!task?.sub_tasks?.length && task.id ? (
|
||||
renderToggleButtonForHasSubTasks(task.id, !!task?.sub_tasks?.length)
|
||||
) : (
|
||||
<div className="h-4 w-4"></div>
|
||||
)}
|
||||
|
||||
{isSubTask && <DoubleRightOutlined style={{ fontSize: 12 }} />}
|
||||
|
||||
<Typography.Text ellipsis={{ expanded: false }}>{task.name}</Typography.Text>
|
||||
|
||||
{renderSubtasksCountLabel(task.id || '', isSubTask, task?.sub_tasks?.length || 0)}
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ExpandAltOutlined />}
|
||||
onClick={() => {
|
||||
setSelectedTaskId(task.id || '');
|
||||
dispatch(setShowTaskDrawer(true));
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: colors.transparent,
|
||||
padding: 0,
|
||||
height: 'fit-content',
|
||||
}}
|
||||
>
|
||||
{t('openButton')}
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskCell;
|
||||
@@ -1,22 +0,0 @@
|
||||
/* Set the stroke width to 9px for the progress circle */
|
||||
.task-progress.ant-progress-circle .ant-progress-circle-path {
|
||||
stroke-width: 9px !important; /* Adjust the stroke width */
|
||||
}
|
||||
|
||||
/* Adjust the inner check mark for better alignment and visibility */
|
||||
.task-progress.ant-progress-circle.ant-progress-status-success .ant-progress-inner .anticon-check {
|
||||
font-size: 8px; /* Adjust font size for the check mark */
|
||||
color: green; /* Optional: Set a color */
|
||||
transform: translate(-50%, -50%); /* Center align */
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
padding: 0;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
/* Adjust the text inside the progress circle */
|
||||
.task-progress.ant-progress-circle .ant-progress-text {
|
||||
font-size: 10px; /* Ensure the text size fits well */
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Progress, Tooltip } from 'antd';
|
||||
import React from 'react';
|
||||
import './TaskProgress.css';
|
||||
|
||||
type TaskProgressProps = {
|
||||
progress: number;
|
||||
numberOfSubTasks: number;
|
||||
};
|
||||
|
||||
const TaskProgress = ({ progress = 0, numberOfSubTasks = 0 }: TaskProgressProps) => {
|
||||
const totalTasks = numberOfSubTasks + 1;
|
||||
const completedTasks = 0;
|
||||
|
||||
const size = progress === 100 ? 21 : 26;
|
||||
|
||||
return (
|
||||
<Tooltip title={`${completedTasks} / ${totalTasks}`}>
|
||||
<Progress
|
||||
percent={progress}
|
||||
type="circle"
|
||||
size={size}
|
||||
style={{ cursor: 'default' }}
|
||||
className="task-progress"
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskProgress;
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Divider, Empty, Flex, Popover, Typography } from 'antd';
|
||||
import { PlayCircleFilled } from '@ant-design/icons';
|
||||
import { colors } from '../../../../../../styles/colors';
|
||||
import CustomAvatar from '../../../../../../components/CustomAvatar';
|
||||
import { mockTimeLogs } from './mockTimeLogs';
|
||||
|
||||
type TimeTrackerProps = {
|
||||
taskId: string | null | undefined;
|
||||
initialTime?: number;
|
||||
};
|
||||
|
||||
const TimeTracker = ({ taskId, initialTime = 0 }: TimeTrackerProps) => {
|
||||
const minutes = Math.floor(initialTime / 60);
|
||||
const seconds = initialTime % 60;
|
||||
const formattedTime = `${minutes}m ${seconds}s`;
|
||||
|
||||
const timeTrackingLogCard =
|
||||
initialTime > 0 ? (
|
||||
<Flex vertical style={{ width: 400, height: 300, overflowY: 'scroll' }}>
|
||||
{mockTimeLogs.map(log => (
|
||||
<React.Fragment key={log.logId}>
|
||||
<Flex gap={8} align="center">
|
||||
<CustomAvatar avatarName={log.username} />
|
||||
|
||||
<Flex vertical>
|
||||
<Typography>
|
||||
<Typography.Text strong>{log.username}</Typography.Text>
|
||||
<Typography.Text>{` logged ${log.duration} ${
|
||||
log.via ? `via ${log.via}` : ''
|
||||
}`}</Typography.Text>
|
||||
</Typography>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{log.date}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Divider style={{ marginBlock: 12 }} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Flex>
|
||||
) : (
|
||||
<Empty style={{ width: 400 }} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<PlayCircleFilled style={{ color: colors.skyBlue, fontSize: 16 }} />
|
||||
<Popover
|
||||
title={
|
||||
<Typography.Text style={{ fontWeight: 500 }}>
|
||||
Time Tracking Log
|
||||
<Divider style={{ marginBlockStart: 8, marginBlockEnd: 12 }} />
|
||||
</Typography.Text>
|
||||
}
|
||||
content={timeTrackingLogCard}
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Typography.Text style={{ cursor: 'pointer' }}>{formattedTime}</Typography.Text>
|
||||
</Popover>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeTracker;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Input } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { colors } from '@/styles/colors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const AddSubTaskListRow = () => {
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
// get data theme data from redux
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const customBorderColor = themeMode === 'dark' && ' border-[#303030]';
|
||||
|
||||
return (
|
||||
<div className={`border-t ${customBorderColor}`}>
|
||||
{isEdit ? (
|
||||
<Input
|
||||
className="h-12 w-full rounded-none"
|
||||
style={{ borderColor: colors.skyBlue }}
|
||||
placeholder={t('addTaskInputPlaceholder')}
|
||||
onBlur={() => setIsEdit(false)}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
onFocus={() => setIsEdit(true)}
|
||||
className="w-[300px] border-none"
|
||||
value={t('addSubTaskText')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddSubTaskListRow;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Input } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { colors } from '@/styles/colors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const AddTaskListRow = () => {
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
// get data theme data from redux
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const customBorderColor = themeMode === 'dark' && ' border-[#303030]';
|
||||
|
||||
return (
|
||||
<div className={`border-t ${customBorderColor}`}>
|
||||
{isEdit ? (
|
||||
<Input
|
||||
className="h-12 w-full rounded-none"
|
||||
style={{ borderColor: colors.skyBlue }}
|
||||
placeholder={t('addTaskInputPlaceholder')}
|
||||
onBlur={() => setIsEdit(false)}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
onFocus={() => setIsEdit(true)}
|
||||
className="w-[300px] border-none"
|
||||
value={t('addTaskText')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTaskListRow;
|
||||
@@ -1,15 +0,0 @@
|
||||
/* custom collapse styles for content box and the left border */
|
||||
.ant-collapse-header {
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.custom-collapse-content-box .ant-collapse-content-box {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:where(.css-dev-only-do-not-override-1w6wsvq).ant-collapse-ghost
|
||||
> .ant-collapse-item
|
||||
> .ant-collapse-content
|
||||
> .ant-collapse-content-box {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
.mentions-light .mentions {
|
||||
background-color: #e9e2e2;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.mentions-dark .mentions {
|
||||
background-color: #2c2c2c;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.tooltip-comment .mentions {
|
||||
background-color: transparent;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
import { Button, ConfigProvider, Flex, Form, Mentions, Skeleton, Space, Tooltip, Typography } from 'antd';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import CustomAvatar from '@components/CustomAvatar';
|
||||
import { colors } from '@/styles/colors';
|
||||
import {
|
||||
IMentionMemberSelectOption,
|
||||
IMentionMemberViewModel,
|
||||
} from '@/types/project/projectComments.types';
|
||||
import { projectsApiService } from '@/api/projects/projects.api.service';
|
||||
import { projectCommentsApiService } from '@/api/projects/comments/project-comments.api.service';
|
||||
import { IProjectUpdateCommentViewModel } from '@/types/project/project.types';
|
||||
import { calculateTimeDifference } from '@/utils/calculate-time-difference';
|
||||
import { getUserSession } from '@/utils/session-helper';
|
||||
import './project-view-updates.css';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
const MAX_COMMENT_LENGTH = 2000;
|
||||
|
||||
const ProjectViewUpdates = () => {
|
||||
const { projectId } = useParams();
|
||||
const [characterLength, setCharacterLength] = useState<number>(0);
|
||||
const [isCommentBoxExpand, setIsCommentBoxExpand] = useState<boolean>(false);
|
||||
const [members, setMembers] = useState<IMentionMemberViewModel[]>([]);
|
||||
const [selectedMembers, setSelectedMembers] = useState<{ id: string; name: string }[]>([]);
|
||||
const [comments, setComments] = useState<IProjectUpdateCommentViewModel[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isLoadingComments, setIsLoadingComments] = useState<boolean>(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [commentValue, setCommentValue] = useState<string>('');
|
||||
const theme = useAppSelector(state => state.themeReducer.mode);
|
||||
const { refreshTimestamp } = useAppSelector(state => state.projectReducer);
|
||||
|
||||
const { t } = useTranslation('project-view-updates');
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const getMembers = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await projectCommentsApiService.getMentionMembers(projectId, 1, 15, null, null, null);
|
||||
if (res.done) {
|
||||
setMembers(res.body as IMentionMemberViewModel[]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch members:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const getComments = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
setIsLoadingComments(true);
|
||||
const res = await projectCommentsApiService.getByProjectId(projectId);
|
||||
if (res.done) {
|
||||
setComments(res.body);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch comments:', error);
|
||||
} finally {
|
||||
setIsLoadingComments(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const handleAddComment = async () => {
|
||||
if (!projectId || characterLength === 0) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
if (!commentValue) {
|
||||
console.error('Comment content is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = {
|
||||
project_id: projectId,
|
||||
team_id: getUserSession()?.team_id,
|
||||
content: commentValue.trim(),
|
||||
mentions: selectedMembers
|
||||
};
|
||||
|
||||
const res = await projectCommentsApiService.createProjectComment(body);
|
||||
if (res.done) {
|
||||
await getComments();
|
||||
handleCancel();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add comment:', error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setCommentValue('');
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void getMembers();
|
||||
void getComments();
|
||||
}, [getMembers, getComments,refreshTimestamp]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
form.resetFields(['comment']);
|
||||
setCharacterLength(0);
|
||||
setIsCommentBoxExpand(false);
|
||||
setSelectedMembers([]);
|
||||
}, [form]);
|
||||
|
||||
const mentionsOptions =
|
||||
members?.map(member => ({
|
||||
value: member.id,
|
||||
label: member.name,
|
||||
})) ?? [];
|
||||
|
||||
const memberSelectHandler = useCallback((member: IMentionMemberSelectOption) => {
|
||||
if (!member?.value || !member?.label) return;
|
||||
setSelectedMembers(prev =>
|
||||
prev.some(mention => mention.id === member.value)
|
||||
? prev
|
||||
: [...prev, { id: member.value, name: member.label }]
|
||||
);
|
||||
|
||||
setCommentValue(prev => {
|
||||
const parts = prev.split('@');
|
||||
const lastPart = parts[parts.length - 1];
|
||||
const mentionText = member.label;
|
||||
// Keep only the part before the @ and add the new mention
|
||||
return prev.slice(0, prev.length - lastPart.length) + mentionText;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCommentChange = useCallback((value: string) => {
|
||||
// Only update the value without trying to replace mentions
|
||||
setCommentValue(value);
|
||||
setCharacterLength(value.trim().length);
|
||||
}, []);
|
||||
|
||||
const handleDeleteComment = useCallback(
|
||||
async (commentId: string | undefined) => {
|
||||
if (!commentId) return;
|
||||
try {
|
||||
const res = await projectCommentsApiService.deleteComment(commentId);
|
||||
if (res.done) {
|
||||
void getComments();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete comment:', error);
|
||||
}
|
||||
},
|
||||
[getComments]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex gap={24} vertical>
|
||||
<Flex vertical gap={16}>
|
||||
{
|
||||
isLoadingComments ? (
|
||||
<Skeleton active />
|
||||
):
|
||||
comments.map(comment => (
|
||||
<Flex key={comment.id} gap={8}>
|
||||
<CustomAvatar avatarName={comment.created_by || ''} />
|
||||
<Flex vertical flex={1}>
|
||||
<Space>
|
||||
<Typography.Text strong style={{ fontSize: 13, color: colors.lightGray }}>
|
||||
{comment.created_by || ''}
|
||||
</Typography.Text>
|
||||
<Tooltip title={comment.created_at}>
|
||||
<Typography.Text style={{ fontSize: 13, color: colors.deepLightGray }}>
|
||||
{calculateTimeDifference(comment.created_at || '')}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<Typography.Paragraph
|
||||
style={{ margin: '8px 0' }}
|
||||
ellipsis={{ rows: 3, expandable: true }}
|
||||
>
|
||||
<div className={`mentions-${theme === 'dark' ? 'dark' : 'light'}`} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comment.content || '') }} />
|
||||
</Typography.Paragraph>
|
||||
<ConfigProvider
|
||||
wave={{ disabled: true }}
|
||||
|
||||
theme={{
|
||||
components: {
|
||||
Button: {
|
||||
defaultColor: colors.lightGray,
|
||||
defaultHoverColor: colors.darkGray,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
shape="circle"
|
||||
type="text"
|
||||
size='small'
|
||||
onClick={() => handleDeleteComment(comment.id)}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</Flex>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Form onFinish={handleAddComment}>
|
||||
<Form.Item>
|
||||
<Mentions
|
||||
value={commentValue}
|
||||
placeholder={t('inputPlaceholder')}
|
||||
loading={isLoading}
|
||||
options={mentionsOptions}
|
||||
autoSize
|
||||
maxLength={MAX_COMMENT_LENGTH}
|
||||
onSelect={option => memberSelectHandler(option as IMentionMemberSelectOption)}
|
||||
onClick={() => setIsCommentBoxExpand(true)}
|
||||
onChange={handleCommentChange}
|
||||
prefix="@"
|
||||
split=""
|
||||
style={{
|
||||
minHeight: isCommentBoxExpand ? 180 : 60,
|
||||
paddingBlockEnd: 24,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 4,
|
||||
right: 12,
|
||||
color: colors.lightGray,
|
||||
}}
|
||||
>{`${characterLength}/${MAX_COMMENT_LENGTH}`}</span>
|
||||
</Form.Item>
|
||||
|
||||
{isCommentBoxExpand && (
|
||||
<Form.Item>
|
||||
<Flex gap={8} justify="flex-end">
|
||||
<Button onClick={handleCancel} disabled={isSubmitting}>
|
||||
{t('cancelButton')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isSubmitting}
|
||||
disabled={characterLength === 0}
|
||||
htmlType="submit"
|
||||
>
|
||||
{t('addButton')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectViewUpdates;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const ProjectViewWorkload = () => {
|
||||
return <div>ProjectViewWorkload</div>;
|
||||
};
|
||||
|
||||
export default ProjectViewWorkload;
|
||||
@@ -1,241 +0,0 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import Flex from 'antd/es/flex';
|
||||
import Badge from 'antd/es/badge';
|
||||
import Button from 'antd/es/button';
|
||||
import Dropdown from 'antd/es/dropdown';
|
||||
import Input from 'antd/es/input';
|
||||
import Typography from 'antd/es/typography';
|
||||
import { MenuProps } from 'antd/es/menu';
|
||||
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@ant-design/icons';
|
||||
|
||||
import { colors } from '@/styles/colors';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import Collapsible from '@/components/collapsible/collapsible';
|
||||
import TaskListTable from '../../task-list-table/task-list-table';
|
||||
import { IGroupBy, updateTaskGroupColor } from '@/features/tasks/tasks.slice';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
|
||||
import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service';
|
||||
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
||||
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
|
||||
import { fetchStatuses } from '@/features/taskAttributes/taskStatusSlice';
|
||||
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||
import { evt_project_board_column_setting_click } from '@/shared/worklenz-analytics-events';
|
||||
import { ALPHA_CHANNEL } from '@/shared/constants';
|
||||
import useIsProjectManager from '@/hooks/useIsProjectManager';
|
||||
import logger from '@/utils/errorLogger';
|
||||
|
||||
interface TaskGroupProps {
|
||||
taskGroup: ITaskListGroup;
|
||||
groupBy: string;
|
||||
color: string;
|
||||
activeId?: string | null;
|
||||
}
|
||||
|
||||
const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
taskGroup,
|
||||
groupBy,
|
||||
color,
|
||||
activeId
|
||||
}) => {
|
||||
const { t } = useTranslation('task-list-table');
|
||||
const dispatch = useAppDispatch();
|
||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||
const isProjectManager = useIsProjectManager();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [groupName, setGroupName] = useState(taskGroup.name || '');
|
||||
|
||||
const { projectId } = useAppSelector((state: any) => state.projectReducer);
|
||||
const themeMode = useAppSelector((state: any) => state.themeReducer.mode);
|
||||
|
||||
// Memoize droppable configuration
|
||||
const { setNodeRef } = useDroppable({
|
||||
id: taskGroup.id,
|
||||
data: {
|
||||
type: 'group',
|
||||
groupId: taskGroup.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Memoize task count
|
||||
const taskCount = useMemo(() => taskGroup.tasks?.length || 0, [taskGroup.tasks]);
|
||||
|
||||
// Memoize dropdown items
|
||||
const dropdownItems: MenuProps['items'] = useMemo(() => {
|
||||
if (groupBy !== IGroupBy.STATUS || !isProjectManager) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'rename',
|
||||
label: t('renameText'),
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => setIsRenaming(true),
|
||||
},
|
||||
{
|
||||
key: 'change-category',
|
||||
label: t('changeCategoryText'),
|
||||
icon: <RetweetOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: 'todo',
|
||||
label: t('todoText'),
|
||||
onClick: () => handleStatusCategoryChange('0'),
|
||||
},
|
||||
{
|
||||
key: 'doing',
|
||||
label: t('doingText'),
|
||||
onClick: () => handleStatusCategoryChange('1'),
|
||||
},
|
||||
{
|
||||
key: 'done',
|
||||
label: t('doneText'),
|
||||
onClick: () => handleStatusCategoryChange('2'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}, [groupBy, isProjectManager, t]);
|
||||
|
||||
const handleStatusCategoryChange = async (category: string) => {
|
||||
if (!projectId || !taskGroup.id) return;
|
||||
|
||||
try {
|
||||
await statusApiService.updateStatus({
|
||||
id: taskGroup.id,
|
||||
category_id: category,
|
||||
project_id: projectId,
|
||||
});
|
||||
|
||||
dispatch(fetchStatuses());
|
||||
trackMixpanelEvent(evt_project_board_column_setting_click, {
|
||||
column_id: taskGroup.id,
|
||||
action: 'change_category',
|
||||
category,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error updating status category:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRename = async () => {
|
||||
if (!projectId || !taskGroup.id || !groupName.trim()) return;
|
||||
|
||||
try {
|
||||
if (groupBy === IGroupBy.STATUS) {
|
||||
await statusApiService.updateStatus({
|
||||
id: taskGroup.id,
|
||||
name: groupName.trim(),
|
||||
project_id: projectId,
|
||||
});
|
||||
dispatch(fetchStatuses());
|
||||
} else if (groupBy === IGroupBy.PHASE) {
|
||||
const phaseData: ITaskPhase = {
|
||||
id: taskGroup.id,
|
||||
name: groupName.trim(),
|
||||
project_id: projectId,
|
||||
color_code: taskGroup.color_code,
|
||||
};
|
||||
await phasesApiService.updatePhase(phaseData);
|
||||
dispatch(fetchPhasesByProjectId(projectId));
|
||||
}
|
||||
|
||||
setIsRenaming(false);
|
||||
} catch (error) {
|
||||
logger.error('Error renaming group:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleColorChange = async (newColor: string) => {
|
||||
if (!projectId || !taskGroup.id) return;
|
||||
|
||||
try {
|
||||
const baseColor = newColor.endsWith(ALPHA_CHANNEL)
|
||||
? newColor.slice(0, -ALPHA_CHANNEL.length)
|
||||
: newColor;
|
||||
|
||||
if (groupBy === IGroupBy.PHASE) {
|
||||
const phaseData: ITaskPhase = {
|
||||
id: taskGroup.id,
|
||||
name: taskGroup.name || '',
|
||||
project_id: projectId,
|
||||
color_code: baseColor,
|
||||
};
|
||||
await phasesApiService.updatePhase(phaseData);
|
||||
dispatch(fetchPhasesByProjectId(projectId));
|
||||
}
|
||||
|
||||
dispatch(updateTaskGroupColor({
|
||||
groupId: taskGroup.id,
|
||||
color: baseColor,
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error updating group color:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef}>
|
||||
<Flex vertical>
|
||||
{/* Group Header */}
|
||||
<Flex style={{ transform: 'translateY(6px)' }}>
|
||||
<Button
|
||||
className="custom-collapse-button"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
border: 'none',
|
||||
borderBottomLeftRadius: isExpanded ? 0 : 4,
|
||||
borderBottomRightRadius: isExpanded ? 0 : 4,
|
||||
color: colors.darkGray,
|
||||
minWidth: 200,
|
||||
}}
|
||||
icon={<RightOutlined rotate={isExpanded ? 90 : 0} />}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isRenaming ? (
|
||||
<Input
|
||||
size="small"
|
||||
value={groupName}
|
||||
onChange={e => setGroupName(e.target.value)}
|
||||
onBlur={handleRename}
|
||||
onPressEnter={handleRename}
|
||||
onClick={e => e.stopPropagation()}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text style={{ fontSize: 14, fontWeight: 600 }}>
|
||||
{taskGroup.name} ({taskCount})
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{dropdownItems.length > 0 && !isRenaming && (
|
||||
<Dropdown menu={{ items: dropdownItems }} trigger={['click']}>
|
||||
<Button icon={<EllipsisOutlined />} className="borderless-icon-btn" />
|
||||
</Dropdown>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* Task List */}
|
||||
<Collapsible isOpen={isExpanded}>
|
||||
<TaskListTable
|
||||
taskList={taskGroup.tasks || []}
|
||||
tableId={taskGroup.id}
|
||||
groupBy={groupBy}
|
||||
color={color}
|
||||
activeId={activeId}
|
||||
/>
|
||||
</Collapsible>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TaskGroup);
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Button, Flex, Select, Typography } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import StatusGroupTables from '../../../projects/project-view-1/taskList/statusTables/StatusGroupTables';
|
||||
import TaskGroupList from '@/pages/projects/projectView/taskList/groupTables/TaskGroupList';
|
||||
import { TaskType } from '../../../../types/task.types';
|
||||
import { useAppSelector } from '../../../../hooks/useAppSelector';
|
||||
import { PageHeader } from '@ant-design/pro-components';
|
||||
import { ArrowLeftOutlined, CaretDownFilled } from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import SearchDropdown from '../../../projects/project-view-1/taskList/taskListFilters/SearchDropdown';
|
||||
import TaskListFilters from '@/pages/projects/projectView/taskList/task-list-filters/task-list-filters';
|
||||
import { useSelectedProject } from '../../../../hooks/useSelectedProject';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toggleDrawer as togglePhaseDrawer } from '../../../../features/projects/singleProject/phase/phases.slice';
|
||||
import { toggleDrawer } from '../../../../features/projects/status/StatusSlice';
|
||||
import { useAppDispatch } from '../../../../hooks/useAppDispatch';
|
||||
import React from 'react';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
|
||||
const PhaseDrawer = React.lazy(() => import('@features/projects/singleProject/phase/PhaseDrawer'));
|
||||
const StatusDrawer = React.lazy(
|
||||
@@ -20,7 +21,8 @@ const StatusDrawer = React.lazy(
|
||||
);
|
||||
|
||||
const ProjectTemplateEditView = () => {
|
||||
const dataSource: TaskType[] = useAppSelector(state => state.taskReducer.tasks);
|
||||
const dataSource: ITaskListGroup[] = useAppSelector(state => state.taskReducer.taskGroups);
|
||||
const groupBy = useAppSelector(state => state.taskReducer.groupBy);
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { templateId, templateName } = useParams();
|
||||
@@ -40,7 +42,7 @@ const ProjectTemplateEditView = () => {
|
||||
//get phases details from phases slice
|
||||
const phase =
|
||||
useAppSelector(state => state.phaseReducer.phaseList).find(
|
||||
phase => phase.projectId === selectedProject?.id
|
||||
phase => phase.id === selectedProject?.id
|
||||
) || null;
|
||||
|
||||
const groupDropdownMenuItems = [
|
||||
@@ -49,7 +51,7 @@ const ProjectTemplateEditView = () => {
|
||||
{
|
||||
key: 'phase',
|
||||
value: 'phase',
|
||||
label: phase ? phase?.phase : t('phaseText'),
|
||||
label: phase ? phase?.name : t('phaseText'),
|
||||
},
|
||||
];
|
||||
return (
|
||||
@@ -68,7 +70,7 @@ const ProjectTemplateEditView = () => {
|
||||
/>
|
||||
<Flex vertical gap={16}>
|
||||
<Flex gap={8} wrap={'wrap'}>
|
||||
<SearchDropdown />
|
||||
<TaskListFilters position="list" />
|
||||
<Flex align="center" gap={4} style={{ marginInlineStart: 12 }}>
|
||||
{t('groupByText')}:
|
||||
<Select
|
||||
@@ -91,8 +93,7 @@ const ProjectTemplateEditView = () => {
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<StatusGroupTables datasource={dataSource} />
|
||||
{/* <PriorityGroupTables datasource={dataSource} /> */}
|
||||
<TaskGroupList taskGroups={dataSource} groupBy={groupBy} />
|
||||
</Flex>
|
||||
{/* phase drawer */}
|
||||
<PhaseDrawer />
|
||||
|
||||
@@ -39,4 +39,4 @@ export const mockTimeLogs: TimeLog[] = [
|
||||
date: 'Sep 12, 2023, 8:30:19 AM - Sep 12, 2023, 8:30:49 AM',
|
||||
via: 'Timer',
|
||||
},
|
||||
];
|
||||
];
|
||||
Reference in New Issue
Block a user