refactor(task-management): enhance TaskGroup and TaskRow components for improved functionality and styling

- Updated TaskGroup to include new props for task selection and toggling subtasks.
- Refactored TaskRow to improve layout and styling, including fixed and scrollable columns.
- Replaced drag handle icon and adjusted task metadata display for better clarity.
- Enhanced overall styling for better responsiveness and dark mode support.
This commit is contained in:
chamiakJ
2025-06-19 09:21:55 +05:30
parent d0310ded28
commit 82aa207e0d
3 changed files with 839 additions and 220 deletions

View File

@@ -1,14 +1,15 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { Card, Button, Typography, Badge, Collapse, Space, Tooltip } from 'antd'; import { Button, Typography, Badge, Space, Tooltip } from 'antd';
import { import {
CaretRightOutlined, CaretRightOutlined,
CaretDownOutlined, CaretDownOutlined,
PlusOutlined, PlusOutlined,
MoreOutlined, MoreOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { ITaskListGroup, IProjectTask } from '@/types/tasks/taskList.types'; import { ITaskListGroup } from '@/types/tasks/taskList.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { IGroupBy } from '@/features/tasks/tasks.slice'; import { IGroupBy } from '@/features/tasks/tasks.slice';
import TaskRow from './TaskRow'; import TaskRow from './TaskRow';
@@ -21,6 +22,8 @@ interface TaskGroupProps {
selectedTaskIds: string[]; selectedTaskIds: string[];
onAddTask?: (groupId: string) => void; onAddTask?: (groupId: string) => void;
onToggleCollapse?: (groupId: string) => void; onToggleCollapse?: (groupId: string) => void;
onSelectTask?: (taskId: string, selected: boolean) => void;
onToggleSubtasks?: (taskId: string) => void;
} }
const TaskGroup: React.FC<TaskGroupProps> = ({ const TaskGroup: React.FC<TaskGroupProps> = ({
@@ -30,6 +33,8 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
selectedTaskIds, selectedTaskIds,
onAddTask, onAddTask,
onToggleCollapse, onToggleCollapse,
onSelectTask,
onToggleSubtasks,
}) => { }) => {
const [isCollapsed, setIsCollapsed] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false);
@@ -46,7 +51,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
// Calculate group statistics // Calculate group statistics
const completedTasks = group.tasks.filter(task => const completedTasks = group.tasks.filter(task =>
task.status_category === 'DONE' || task.complete_ratio === 100 task.status_category?.is_done || task.complete_ratio === 100
).length; ).length;
const totalTasks = group.tasks.length; const totalTasks = group.tasks.length;
const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
@@ -81,101 +86,158 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
}; };
return ( return (
<Card <div
ref={setNodeRef} ref={setNodeRef}
className={`task-group ${isOver ? 'drag-over' : ''}`} className={`task-group ${isOver ? 'drag-over' : ''}`}
style={{ style={{
borderLeft: `4px solid ${getGroupColor()}`,
backgroundColor: isOver ? '#f0f8ff' : undefined, backgroundColor: isOver ? '#f0f8ff' : undefined,
}} }}
styles={{
body: { padding: 0 },
}}
> >
{/* Group Header */} {/* Group Header Row */}
<div className="group-header px-4 py-3 border-b border-gray-200"> <div className="task-group-header">
<div className="flex items-center justify-between"> <div className="task-group-header-row">
<div className="flex items-center space-x-3"> <div className="task-table-fixed-columns">
<Button <div className="task-table-cell task-table-cell-drag" style={{ width: '40px' }}>
type="text" <Button
size="small" type="text"
icon={isCollapsed ? <CaretRightOutlined /> : <CaretDownOutlined />} size="small"
onClick={handleToggleCollapse} icon={isCollapsed ? <CaretRightOutlined /> : <CaretDownOutlined />}
className="p-0 w-6 h-6 flex items-center justify-center" onClick={handleToggleCollapse}
/> className="p-0 w-6 h-6 flex items-center justify-center"
/>
<div className="flex items-center space-x-2"> </div>
<div className="task-table-cell task-table-cell-checkbox" style={{ width: '40px' }}>
<div <div
className="w-3 h-3 rounded-full" className="w-3 h-3 rounded-full"
style={{ backgroundColor: getGroupColor() }} style={{ backgroundColor: getGroupColor() }}
/> />
<Text strong className="text-base">
{group.name}
</Text>
<Badge count={totalTasks} showZero style={{ backgroundColor: '#f0f0f0', color: '#666' }} />
</div> </div>
<div className="task-table-cell task-table-cell-key" style={{ width: '80px' }}></div>
{completionRate > 0 && ( <div className="task-table-cell task-table-cell-task" style={{ width: '220px' }}>
<Text type="secondary" className="text-sm"> <div className="flex items-center space-x-2 flex-1">
{completionRate}% complete <Text strong className="text-sm">
</Text> {group.name}
)} </Text>
<Badge count={totalTasks} showZero style={{ backgroundColor: '#f0f0f0', color: '#666' }} />
{completionRate > 0 && (
<Text type="secondary" className="text-xs">
{completionRate}% complete
</Text>
)}
</div>
</div>
</div>
<div className="task-table-scrollable-columns">
<div className="task-table-cell" style={{ width: '120px' }}></div>
<div className="task-table-cell" style={{ width: '150px' }}></div>
<div className="task-table-cell" style={{ width: '150px' }}></div>
<div className="task-table-cell" style={{ width: '100px' }}></div>
<div className="task-table-cell" style={{ width: '100px' }}></div>
<div className="task-table-cell" style={{ width: '120px' }}>
<Space>
<Tooltip title="Add task to this group">
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={handleAddTask}
className="opacity-60 hover:opacity-100"
/>
</Tooltip>
<Button
type="text"
size="small"
icon={<MoreOutlined />}
className="opacity-60 hover:opacity-100"
/>
</Space>
</div>
</div> </div>
<Space>
<Tooltip title="Add task to this group">
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={handleAddTask}
className="opacity-60 hover:opacity-100"
/>
</Tooltip>
<Button
type="text"
size="small"
icon={<MoreOutlined />}
className="opacity-60 hover:opacity-100"
/>
</Space>
</div> </div>
{/* Progress Bar */} {/* Progress Bar */}
{totalTasks > 0 && ( {totalTasks > 0 && !isCollapsed && (
<div className="mt-2"> <div className="task-group-progress">
<div className="w-full bg-gray-200 rounded-full h-1"> <div className="task-table-fixed-columns">
<div <div style={{ width: '380px', padding: '0 12px' }}>
className="h-1 rounded-full transition-all duration-300" <div className="w-full bg-gray-200 rounded-full h-1">
style={{ <div
width: `${completionRate}%`, className="h-1 rounded-full transition-all duration-300"
backgroundColor: getGroupColor(), style={{
}} width: `${completionRate}%`,
/> backgroundColor: getGroupColor(),
}}
/>
</div>
</div>
</div> </div>
</div> </div>
)} )}
</div> </div>
{/* Column Headers */}
{!isCollapsed && totalTasks > 0 && (
<div className="task-group-column-headers">
<div className="task-group-column-headers-row">
<div className="task-table-fixed-columns">
<div className="task-table-cell task-table-header-cell" style={{ width: '40px' }}></div>
<div className="task-table-cell task-table-header-cell" style={{ width: '40px' }}></div>
<div className="task-table-cell task-table-header-cell" style={{ width: '80px' }}>
<Text className="column-header-text">Key</Text>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '220px' }}>
<Text className="column-header-text">Task</Text>
</div>
</div>
<div className="task-table-scrollable-columns">
<div className="task-table-cell task-table-header-cell" style={{ width: '120px' }}>
<Text className="column-header-text">Progress</Text>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
<Text className="column-header-text">Members</Text>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
<Text className="column-header-text">Labels</Text>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
<Text className="column-header-text">Status</Text>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
<Text className="column-header-text">Priority</Text>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '120px' }}>
<Text className="column-header-text">Time Tracking</Text>
</div>
</div>
</div>
</div>
)}
{/* Tasks List */} {/* Tasks List */}
{!isCollapsed && ( {!isCollapsed && (
<div className="tasks-container"> <div className="task-group-body">
{group.tasks.length === 0 ? ( {group.tasks.length === 0 ? (
<div className="p-8 text-center text-gray-500"> <div className="task-group-empty">
<Text type="secondary">No tasks in this group</Text> <div className="task-table-fixed-columns">
<br /> <div style={{ width: '380px', padding: '20px 12px' }}>
<Button <div className="text-center text-gray-500">
type="link" <Text type="secondary">No tasks in this group</Text>
icon={<PlusOutlined />} <br />
onClick={handleAddTask} <Button
className="mt-2" type="link"
> icon={<PlusOutlined />}
Add first task onClick={handleAddTask}
</Button> className="mt-2"
>
Add first task
</Button>
</div>
</div>
</div>
</div> </div>
) : ( ) : (
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}> <SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
<div className="divide-y divide-gray-100"> <div className="task-group-tasks">
{group.tasks.map((task, index) => ( {group.tasks.map((task, index) => (
<TaskRow <TaskRow
key={task.id} key={task.id}
@@ -185,6 +247,8 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
currentGrouping={currentGrouping} currentGrouping={currentGrouping}
isSelected={selectedTaskIds.includes(task.id!)} isSelected={selectedTaskIds.includes(task.id!)}
index={index} index={index}
onSelect={onSelectTask}
onToggleSubtasks={onToggleSubtasks}
/> />
))} ))}
</div> </div>
@@ -192,7 +256,155 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
)} )}
</div> </div>
)} )}
</Card>
<style>{`
.task-group {
border: 1px solid var(--task-border-primary, #e8e8e8);
border-radius: 8px;
margin-bottom: 16px;
background: var(--task-bg-primary, white);
box-shadow: 0 1px 3px var(--task-shadow, rgba(0, 0, 0, 0.1));
overflow: hidden;
transition: all 0.3s ease;
}
.task-group:last-child {
margin-bottom: 0;
}
.task-group-header {
background: var(--task-bg-tertiary, #f8f9fa);
border-bottom: 1px solid var(--task-border-primary, #e8e8e8);
transition: background-color 0.3s ease;
}
.task-group-header-row {
display: flex;
height: 42px;
max-height: 42px;
overflow: hidden;
}
.task-group-progress {
display: flex;
height: 20px;
align-items: center;
background: var(--task-bg-tertiary, #f8f9fa);
border-bottom: 1px solid var(--task-border-primary, #e8e8e8);
transition: background-color 0.3s ease;
}
.task-group-column-headers {
background: var(--task-bg-secondary, #f5f5f5);
border-bottom: 1px solid var(--task-border-tertiary, #d9d9d9);
transition: background-color 0.3s ease;
}
.task-group-column-headers-row {
display: flex;
height: 32px;
max-height: 32px;
overflow: hidden;
}
.task-table-header-cell {
background: var(--task-bg-secondary, #f5f5f5);
font-weight: 600;
color: var(--task-text-secondary, #595959);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--task-border-tertiary, #d9d9d9);
height: 32px;
max-height: 32px;
overflow: hidden;
transition: all 0.3s ease;
}
.column-header-text {
font-size: 11px;
font-weight: 600;
color: var(--task-text-secondary, #595959);
text-transform: uppercase;
letter-spacing: 0.5px;
transition: color 0.3s ease;
}
.task-group-body {
background: var(--task-bg-primary, white);
transition: background-color 0.3s ease;
}
.task-group-empty {
display: flex;
height: 80px;
align-items: center;
background: var(--task-bg-primary, white);
transition: background-color 0.3s ease;
}
.task-group-tasks {
background: var(--task-bg-primary, white);
transition: background-color 0.3s ease;
}
.task-table-fixed-columns {
display: flex;
background: inherit;
position: sticky;
left: 0;
z-index: 9;
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
transition: border-color 0.3s ease;
}
.task-table-scrollable-columns {
display: flex;
overflow-x: auto;
}
.task-table-cell {
display: flex;
align-items: center;
padding: 0 12px;
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
font-size: 12px;
white-space: nowrap;
height: 42px;
max-height: 42px;
min-height: 42px;
overflow: hidden;
color: var(--task-text-primary, #262626);
transition: all 0.3s ease;
}
.task-table-cell:last-child {
border-right: none;
}
.drag-over {
background-color: var(--task-drag-over-bg, #f0f8ff) !important;
border-color: var(--task-drag-over-border, #40a9ff) !important;
}
/* Ensure buttons and components fit within row height */
.task-group .ant-btn {
height: auto;
max-height: 32px;
line-height: 1.2;
}
.task-group .ant-badge {
height: auto;
line-height: 1.2;
}
/* Dark mode specific adjustments */
.dark .task-group,
[data-theme="dark"] .task-group {
box-shadow: 0 1px 3px var(--task-shadow, rgba(0, 0, 0, 0.3));
}
`}</style>
</div>
); );
}; };

View File

@@ -26,10 +26,9 @@ import {
setGroup, setGroup,
fetchTaskGroups, fetchTaskGroups,
reorderTasks, reorderTasks,
collapseAllGroups,
expandAllGroups,
} from '@/features/tasks/tasks.slice'; } from '@/features/tasks/tasks.slice';
import { IProjectTask, ITaskListGroup } from '@/types/tasks/taskList.types'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { ITaskListGroup } from '@/types/tasks/taskList.types';
import TaskGroup from './TaskGroup'; import TaskGroup from './TaskGroup';
import TaskRow from './TaskRow'; import TaskRow from './TaskRow';
import BulkActionBar from './BulkActionBar'; import BulkActionBar from './BulkActionBar';
@@ -66,9 +65,8 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
archived, archived,
} = useSelector((state: RootState) => state.taskReducer); } = useSelector((state: RootState) => state.taskReducer);
// Selection state (assuming you have a selection slice) // Selection state
// const selectedTaskIds = useSelector((state: RootState) => state.selection?.selectedTaskIds || []); const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([]);
const selectedTaskIds: string[] = []; // Temporary placeholder
// Drag and Drop sensors // Drag and Drop sensors
const sensors = useSensors( const sensors = useSensors(
@@ -204,12 +202,12 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const handleCollapseAll = () => { const handleCollapseAll = () => {
// This would need to be implemented in the tasks slice // This would need to be implemented in the tasks slice
// dispatch(collapseAllGroups()); console.log('Collapse all groups');
}; };
const handleExpandAll = () => { const handleExpandAll = () => {
// This would need to be implemented in the tasks slice // This would need to be implemented in the tasks slice
// dispatch(expandAllGroups()); console.log('Expand all groups');
}; };
const handleRefresh = () => { const handleRefresh = () => {
@@ -218,6 +216,21 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
} }
}; };
const handleSelectTask = (taskId: string, selected: boolean) => {
setSelectedTaskIds(prev => {
if (selected) {
return [...prev, taskId];
} else {
return prev.filter(id => id !== taskId);
}
});
};
const handleToggleSubtasks = (taskId: string) => {
// Implementation for toggling subtasks
console.log('Toggle subtasks for task:', taskId);
};
if (error) { if (error) {
return ( return (
<Card className={className}> <Card className={className}>
@@ -285,7 +298,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
/> />
)} )}
{/* Task Groups */} {/* Task Groups Container */}
<div className="task-groups-container"> <div className="task-groups-container">
{loadingGroups ? ( {loadingGroups ? (
<Card> <Card>
@@ -308,7 +321,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<div className="space-y-4"> <div className="task-groups">
{taskGroups.map((group) => ( {taskGroups.map((group) => (
<TaskGroup <TaskGroup
key={group.id} key={group.id}
@@ -316,6 +329,8 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
projectId={projectId} projectId={projectId}
currentGrouping={groupBy} currentGrouping={groupBy}
selectedTaskIds={selectedTaskIds} selectedTaskIds={selectedTaskIds}
onSelectTask={handleSelectTask}
onToggleSubtasks={handleToggleSubtasks}
/> />
))} ))}
</div> </div>
@@ -335,6 +350,60 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
</DndContext> </DndContext>
)} )}
</div> </div>
<style>{`
.task-groups-container {
overflow-x: auto;
max-height: calc(100vh - 300px);
overflow-y: auto;
background: var(--task-bg-secondary, #f5f5f5);
padding: 16px;
border-radius: 8px;
transition: background-color 0.3s ease;
}
.task-groups {
min-width: fit-content;
}
/* Dark mode support */
:root {
--task-bg-primary: #ffffff;
--task-bg-secondary: #f5f5f5;
--task-bg-tertiary: #f8f9fa;
--task-border-primary: #e8e8e8;
--task-border-secondary: #f0f0f0;
--task-border-tertiary: #d9d9d9;
--task-text-primary: #262626;
--task-text-secondary: #595959;
--task-text-tertiary: #8c8c8c;
--task-shadow: rgba(0, 0, 0, 0.1);
--task-hover-bg: #fafafa;
--task-selected-bg: #e6f7ff;
--task-selected-border: #1890ff;
--task-drag-over-bg: #f0f8ff;
--task-drag-over-border: #40a9ff;
}
.dark .task-groups-container,
[data-theme="dark"] .task-groups-container {
--task-bg-primary: #1f1f1f;
--task-bg-secondary: #141414;
--task-bg-tertiary: #262626;
--task-border-primary: #303030;
--task-border-secondary: #404040;
--task-border-tertiary: #505050;
--task-text-primary: #ffffff;
--task-text-secondary: #d9d9d9;
--task-text-tertiary: #8c8c8c;
--task-shadow: rgba(0, 0, 0, 0.3);
--task-hover-bg: #2a2a2a;
--task-selected-bg: #1a2332;
--task-selected-border: #1890ff;
--task-drag-over-bg: #1a2332;
--task-drag-over-border: #40a9ff;
}
`}</style>
</div> </div>
); );
}; };

View File

@@ -3,13 +3,13 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { Checkbox, Avatar, Tag, Progress, Typography, Space, Button, Tooltip } from 'antd'; import { Checkbox, Avatar, Tag, Progress, Typography, Space, Button, Tooltip } from 'antd';
import { import {
DragOutlined, HolderOutlined,
EyeOutlined, EyeOutlined,
MessageOutlined, MessageOutlined,
PaperClipOutlined, PaperClipOutlined,
ClockCircleOutlined, ClockCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { IProjectTask } from '@/types/tasks/taskList.types'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { IGroupBy } from '@/features/tasks/tasks.slice'; import { IGroupBy } from '@/features/tasks/tasks.slice';
const { Text } = Typography; const { Text } = Typography;
@@ -90,113 +90,94 @@ const TaskRow: React.FC<TaskRowProps> = ({
const dueDate = formatDueDate(task.end_date); const dueDate = formatDueDate(task.end_date);
return ( return (
<div <>
ref={setNodeRef} <div
style={style} ref={setNodeRef}
className={`task-row p-4 hover:bg-gray-50 ${isSelected ? 'bg-blue-50 border-l-2 border-l-blue-500' : ''} ${isDragOverlay ? 'shadow-lg bg-white rounded-md border' : ''}`} style={style}
> className={`task-row ${isSelected ? 'task-row-selected' : ''} ${isDragOverlay ? 'task-row-drag-overlay' : ''}`}
<div className="flex items-center space-x-3"> >
{/* Drag Handle */} <div className="task-row-content">
<Button {/* Fixed Columns */}
type="text" <div className="task-table-fixed-columns">
size="small" {/* Drag Handle */}
icon={<DragOutlined />} <div className="task-table-cell task-table-cell-drag" style={{ width: '40px' }}>
className="drag-handle opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing" <Button
{...attributes} type="text"
{...listeners} size="small"
/> icon={<HolderOutlined />}
className="drag-handle opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
{...attributes}
{...listeners}
/>
</div>
{/* Selection Checkbox */} {/* Selection Checkbox */}
<Checkbox <div className="task-table-cell task-table-cell-checkbox" style={{ width: '40px' }}>
checked={isSelected} <Checkbox
onChange={(e) => handleSelectChange(e.target.checked)} checked={isSelected}
/> onChange={(e) => handleSelectChange(e.target.checked)}
/>
</div>
{/* Task Content */} {/* Task Key */}
<div className="flex-1 min-w-0"> <div className="task-table-cell task-table-cell-key" style={{ width: '80px' }}>
<div className="flex items-start justify-between"> {task.project_id && task.task_key && (
<div className="flex-1 min-w-0"> <Text code className="task-key">
{/* Task Name and Key */} {task.task_key}
<div className="flex items-center space-x-2 mb-1">
{task.project_id && (
<Text code className="text-xs text-gray-500">
{task.key}
</Text>
)}
<Text
strong
className={`text-sm ${task.complete_ratio === 100 ? 'line-through text-gray-500' : ''}`}
>
{task.name}
</Text> </Text>
{task.sub_tasks_count && task.sub_tasks_count > 0 && (
<Button
type="text"
size="small"
onClick={handleToggleSubtasks}
className="text-xs text-gray-500 px-1 h-5"
>
{task.show_sub_tasks ? '' : '+'} {task.sub_tasks_count}
</Button>
)}
</div>
{/* Description (if exists) */}
{task.description && (
<Text type="secondary" className="text-xs line-clamp-1">
{task.description}
</Text>
)}
{/* Labels */}
{task.labels && task.labels.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1">
{task.labels.slice(0, 3).map((label) => (
<Tag
key={label.id}
color={label.color_code}
className="text-xs m-0 px-1 py-0 text-white"
style={{
backgroundColor: label.color_code,
border: 'none',
fontSize: '10px',
}}
>
{label.name}
</Tag>
))}
{task.labels.length > 3 && (
<Text type="secondary" className="text-xs">
+{task.labels.length - 3} more
</Text>
)}
</div>
)} )}
</div> </div>
{/* Task Metadata */} {/* Task Name */}
<div className="flex items-center space-x-3 ml-4"> <div className="task-table-cell task-table-cell-task" style={{ width: '220px' }}>
{/* Progress */} <div className="task-content">
{task.complete_ratio !== undefined && task.complete_ratio > 0 && ( <div className="task-header">
<div className="w-16"> <Text
strong
className={`task-name ${task.complete_ratio === 100 ? 'task-completed' : ''}`}
>
{task.name}
</Text>
{task.sub_tasks_count && task.sub_tasks_count > 0 && (
<Button
type="text"
size="small"
onClick={handleToggleSubtasks}
className="subtask-toggle"
>
{task.show_sub_tasks ? '' : '+'} {task.sub_tasks_count}
</Button>
)}
</div>
</div>
</div>
</div>
{/* Scrollable Columns */}
<div className="task-table-scrollable-columns">
{/* Progress */}
<div className="task-table-cell" style={{ width: '120px' }}>
{task.complete_ratio !== undefined && task.complete_ratio >= 0 && (
<div className="task-progress">
<Progress <Progress
percent={task.complete_ratio} percent={task.complete_ratio}
size="small" size="small"
showInfo={false} showInfo={false}
strokeColor={task.complete_ratio === 100 ? '#52c41a' : '#1890ff'} strokeColor={task.complete_ratio === 100 ? '#52c41a' : '#1890ff'}
/> />
<Text className="task-progress-text">{task.complete_ratio}%</Text>
</div> </div>
)} )}
</div>
{/* Assignees */} {/* Members */}
<div className="task-table-cell" style={{ width: '150px' }}>
{task.assignees && task.assignees.length > 0 && ( {task.assignees && task.assignees.length > 0 && (
<Avatar.Group size="small" maxCount={3}> <Avatar.Group size="small" maxCount={3}>
{task.assignees.map((assignee) => ( {task.assignees.map((assignee) => (
<Tooltip key={assignee.id} title={assignee.name}> <Tooltip key={assignee.id} title={assignee.name}>
<Avatar <Avatar
size="small" size="small"
src={assignee.avatar_url}
style={{ backgroundColor: assignee.color_code }}
> >
{assignee.name?.charAt(0)?.toUpperCase()} {assignee.name?.charAt(0)?.toUpperCase()}
</Avatar> </Avatar>
@@ -204,59 +185,87 @@ const TaskRow: React.FC<TaskRowProps> = ({
))} ))}
</Avatar.Group> </Avatar.Group>
)} )}
</div>
{/* Priority Indicator */} {/* Labels */}
{task.priority_color && ( <div className="task-table-cell" style={{ width: '150px' }}>
<div {task.labels && task.labels.length > 0 && (
className="w-2 h-2 rounded-full" <div className="task-labels-column">
style={{ backgroundColor: task.priority_color }} {task.labels.slice(0, 3).map((label) => (
title={`Priority: ${task.priority}`} <Tag
/> key={label.id}
)} className="task-label"
style={{
{/* Due Date */} backgroundColor: label.color_code,
{dueDate && ( border: 'none',
<div className="flex items-center space-x-1"> color: 'white',
<ClockCircleOutlined className="text-xs text-gray-400" /> }}
<Text >
className={`text-xs ${ {label.name}
dueDate.color === 'error' ? 'text-red-500' : </Tag>
dueDate.color === 'warning' ? 'text-orange-500' : ))}
'text-gray-500' {task.labels.length > 3 && (
}`} <Text type="secondary" className="task-labels-more">
> +{task.labels.length - 3}
{dueDate.text} </Text>
</Text> )}
</div> </div>
)} )}
</div>
{/* Task Indicators */} {/* Status */}
<Space size={0}> <div className="task-table-cell" style={{ width: '100px' }}>
{task.comments_count && task.comments_count > 0 && ( {task.status_name && (
<div className="flex items-center space-x-1"> <div
<MessageOutlined className="text-xs text-gray-400" /> className="task-status"
<Text className="text-xs text-gray-500">{task.comments_count}</Text> style={{
backgroundColor: task.status_color,
color: 'white',
}}
>
{task.status_name}
</div>
)}
</div>
{/* Priority */}
<div className="task-table-cell" style={{ width: '100px' }}>
{task.priority_name && (
<div className="task-priority">
<div
className="task-priority-indicator"
style={{ backgroundColor: task.priority_color }}
/>
<Text className="task-priority-text">{task.priority_name}</Text>
</div>
)}
</div>
{/* Time Tracking */}
<div className="task-table-cell" style={{ width: '120px' }}>
<div className="task-time-tracking">
{task.time_spent_string && (
<div className="task-time-spent">
<ClockCircleOutlined className="task-time-icon" />
<Text className="task-time-text">{task.time_spent_string}</Text>
</div> </div>
)} )}
{/* Task Indicators */}
{task.attachments_count && task.attachments_count > 0 && ( <div className="task-indicators">
<div className="flex items-center space-x-1"> {task.comments_count && task.comments_count > 0 && (
<PaperClipOutlined className="text-xs text-gray-400" /> <div className="task-indicator">
<Text className="text-xs text-gray-500">{task.attachments_count}</Text> <MessageOutlined />
</div> <span>{task.comments_count}</span>
)} </div>
</Space> )}
{task.attachments_count && task.attachments_count > 0 && (
{/* View/Edit Button */} <div className="task-indicator">
<Button <PaperClipOutlined />
type="text" <span>{task.attachments_count}</span>
size="small" </div>
icon={<EyeOutlined />} )}
className="opacity-60 hover:opacity-100" </div>
onClick={() => { </div>
// Handle task view/edit
}}
/>
</div> </div>
</div> </div>
</div> </div>
@@ -264,7 +273,7 @@ const TaskRow: React.FC<TaskRowProps> = ({
{/* Subtasks */} {/* Subtasks */}
{task.show_sub_tasks && task.sub_tasks && task.sub_tasks.length > 0 && ( {task.show_sub_tasks && task.sub_tasks && task.sub_tasks.length > 0 && (
<div className="mt-3 ml-8 pl-4 border-l-2 border-gray-200"> <div className="task-subtasks">
{task.sub_tasks.map((subtask) => ( {task.sub_tasks.map((subtask) => (
<TaskRow <TaskRow
key={subtask.id} key={subtask.id}
@@ -278,7 +287,336 @@ const TaskRow: React.FC<TaskRowProps> = ({
))} ))}
</div> </div>
)} )}
</div>
<style>{`
.task-row {
border-bottom: 1px solid var(--task-border-secondary, #f0f0f0);
background: var(--task-bg-primary, white);
transition: all 0.3s ease;
}
.task-row:hover {
background-color: var(--task-hover-bg, #fafafa);
}
.task-row-selected {
background-color: var(--task-selected-bg, #e6f7ff);
border-left: 3px solid var(--task-selected-border, #1890ff);
}
.task-row-drag-overlay {
background: var(--task-bg-primary, white);
border: 1px solid var(--task-border-tertiary, #d9d9d9);
border-radius: 4px;
box-shadow: 0 4px 12px var(--task-shadow, rgba(0, 0, 0, 0.15));
}
.task-row-content {
display: flex;
height: 42px;
max-height: 42px;
overflow: hidden;
}
.task-table-fixed-columns {
display: flex;
background: inherit;
position: sticky;
left: 0;
z-index: 8;
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
transition: border-color 0.3s ease;
}
.task-table-scrollable-columns {
display: flex;
overflow-x: auto;
}
.task-table-cell {
display: flex;
align-items: center;
padding: 0 8px;
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
font-size: 12px;
white-space: nowrap;
height: 42px;
max-height: 42px;
min-height: 42px;
overflow: hidden;
color: var(--task-text-primary, #262626);
transition: all 0.3s ease;
}
.task-table-cell:last-child {
border-right: none;
}
.task-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
overflow: hidden;
}
.task-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 1px;
height: 20px;
overflow: hidden;
}
.task-key {
font-size: 10px;
color: var(--task-text-tertiary, #666);
background: var(--task-bg-secondary, #f0f0f0);
padding: 1px 4px;
border-radius: 2px;
transition: all 0.3s ease;
}
.task-name {
font-size: 13px;
font-weight: 500;
color: var(--task-text-primary, #262626);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.3s ease;
}
.task-completed {
text-decoration: line-through;
color: var(--task-text-tertiary, #8c8c8c);
}
.subtask-toggle {
font-size: 10px;
color: var(--task-text-tertiary, #666);
padding: 0 4px;
height: 16px;
line-height: 16px;
transition: color 0.3s ease;
}
.task-labels {
display: flex;
gap: 2px;
flex-wrap: nowrap;
overflow: hidden;
height: 18px;
align-items: center;
}
.task-label {
font-size: 10px;
padding: 0 4px;
height: 16px;
line-height: 16px;
border-radius: 2px;
margin: 0;
}
.task-label-small {
font-size: 9px;
padding: 0 3px;
height: 14px;
line-height: 14px;
border-radius: 2px;
margin: 0;
}
.task-labels-more {
font-size: 10px;
color: var(--task-text-tertiary, #8c8c8c);
transition: color 0.3s ease;
}
.task-progress {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
height: 100%;
}
.task-progress .ant-progress {
flex: 1;
}
.task-progress-text {
font-size: 10px;
color: var(--task-text-tertiary, #666);
min-width: 24px;
transition: color 0.3s ease;
}
.task-labels-column {
display: flex;
gap: 2px;
flex-wrap: nowrap;
overflow: hidden;
height: 100%;
align-items: center;
}
.task-status {
font-size: 10px;
padding: 2px 6px;
border-radius: 2px;
font-weight: 500;
text-transform: uppercase;
}
.task-priority {
display: flex;
align-items: center;
gap: 4px;
}
.task-priority-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.task-priority-text {
font-size: 11px;
color: var(--task-text-tertiary, #666);
transition: color 0.3s ease;
}
.task-time-tracking {
display: flex;
align-items: center;
gap: 8px;
height: 100%;
overflow: hidden;
}
.task-time-spent {
display: flex;
align-items: center;
gap: 2px;
}
.task-time-icon {
font-size: 10px;
color: var(--task-text-tertiary, #8c8c8c);
transition: color 0.3s ease;
}
.task-time-text {
font-size: 10px;
color: var(--task-text-tertiary, #666);
transition: color 0.3s ease;
}
.task-indicators {
display: flex;
gap: 6px;
}
.task-indicator {
display: flex;
align-items: center;
gap: 2px;
font-size: 10px;
color: var(--task-text-tertiary, #8c8c8c);
transition: color 0.3s ease;
}
.task-subtasks {
margin-left: 40px;
border-left: 2px solid var(--task-border-secondary, #f0f0f0);
transition: border-color 0.3s ease;
}
.drag-handle {
opacity: 0.4;
transition: opacity 0.2s;
}
.drag-handle:hover {
opacity: 1;
}
/* Ensure buttons and components fit within row height */
.task-row .ant-btn {
height: auto;
max-height: 24px;
padding: 0 4px;
line-height: 1.2;
}
.task-row .ant-checkbox-wrapper {
height: 24px;
align-items: center;
}
.task-row .ant-avatar-group {
height: 24px;
align-items: center;
}
.task-row .ant-avatar {
width: 24px !important;
height: 24px !important;
line-height: 24px !important;
font-size: 10px !important;
}
.task-row .ant-tag {
margin: 0;
padding: 0 4px;
height: 16px;
line-height: 16px;
border-radius: 2px;
}
.task-row .ant-progress {
margin: 0;
line-height: 1;
}
.task-row .ant-progress-line {
height: 6px !important;
}
.task-row .ant-progress-bg {
height: 6px !important;
}
/* Dark mode specific adjustments for Ant Design components */
.dark .task-row .ant-progress-bg,
[data-theme="dark"] .task-row .ant-progress-bg {
background-color: var(--task-border-primary, #303030) !important;
}
.dark .task-row .ant-checkbox-wrapper,
[data-theme="dark"] .task-row .ant-checkbox-wrapper {
color: var(--task-text-primary, #ffffff);
}
.dark .task-row .ant-btn,
[data-theme="dark"] .task-row .ant-btn {
color: var(--task-text-secondary, #d9d9d9);
border-color: transparent;
}
.dark .task-row .ant-btn:hover,
[data-theme="dark"] .task-row .ant-btn:hover {
color: var(--task-text-primary, #ffffff);
background-color: var(--task-hover-bg, #2a2a2a);
}
`}</style>
</>
); );
}; };