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:
chamiakJ
2025-06-02 23:04:03 +05:30
parent 45d9049d27
commit 71638ce52a
53 changed files with 22 additions and 5186 deletions

View File

@@ -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": {

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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',

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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} />
// )
// })
];
};

View File

@@ -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 */
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
},
];

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -1,7 +0,0 @@
import React from 'react';
const ProjectViewWorkload = () => {
return <div>ProjectViewWorkload</div>;
};
export default ProjectViewWorkload;

View File

@@ -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);

View File

@@ -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 />

View File

@@ -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',
},
];
];