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:
@@ -1,14 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
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 {
|
||||
CaretRightOutlined,
|
||||
CaretDownOutlined,
|
||||
PlusOutlined,
|
||||
MoreOutlined,
|
||||
} 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 TaskRow from './TaskRow';
|
||||
|
||||
@@ -21,6 +22,8 @@ interface TaskGroupProps {
|
||||
selectedTaskIds: string[];
|
||||
onAddTask?: (groupId: string) => void;
|
||||
onToggleCollapse?: (groupId: string) => void;
|
||||
onSelectTask?: (taskId: string, selected: boolean) => void;
|
||||
onToggleSubtasks?: (taskId: string) => void;
|
||||
}
|
||||
|
||||
const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
@@ -30,6 +33,8 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
selectedTaskIds,
|
||||
onAddTask,
|
||||
onToggleCollapse,
|
||||
onSelectTask,
|
||||
onToggleSubtasks,
|
||||
}) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
@@ -46,7 +51,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
|
||||
// Calculate group statistics
|
||||
const completedTasks = group.tasks.filter(task =>
|
||||
task.status_category === 'DONE' || task.complete_ratio === 100
|
||||
task.status_category?.is_done || task.complete_ratio === 100
|
||||
).length;
|
||||
const totalTasks = group.tasks.length;
|
||||
const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
|
||||
@@ -81,21 +86,18 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`task-group ${isOver ? 'drag-over' : ''}`}
|
||||
style={{
|
||||
borderLeft: `4px solid ${getGroupColor()}`,
|
||||
backgroundColor: isOver ? '#f0f8ff' : undefined,
|
||||
}}
|
||||
styles={{
|
||||
body: { padding: 0 },
|
||||
}}
|
||||
>
|
||||
{/* Group Header */}
|
||||
<div className="group-header px-4 py-3 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Group Header Row */}
|
||||
<div className="task-group-header">
|
||||
<div className="task-group-header-row">
|
||||
<div className="task-table-fixed-columns">
|
||||
<div className="task-table-cell task-table-cell-drag" style={{ width: '40px' }}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
@@ -103,25 +105,35 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
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
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: getGroupColor() }}
|
||||
/>
|
||||
<Text strong className="text-base">
|
||||
</div>
|
||||
<div className="task-table-cell task-table-cell-key" style={{ width: '80px' }}></div>
|
||||
<div className="task-table-cell task-table-cell-task" style={{ width: '220px' }}>
|
||||
<div className="flex items-center space-x-2 flex-1">
|
||||
<Text strong className="text-sm">
|
||||
{group.name}
|
||||
</Text>
|
||||
<Badge count={totalTasks} showZero style={{ backgroundColor: '#f0f0f0', color: '#666' }} />
|
||||
</div>
|
||||
|
||||
{completionRate > 0 && (
|
||||
<Text type="secondary" className="text-sm">
|
||||
<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
|
||||
@@ -140,10 +152,14 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{totalTasks > 0 && (
|
||||
<div className="mt-2">
|
||||
{totalTasks > 0 && !isCollapsed && (
|
||||
<div className="task-group-progress">
|
||||
<div className="task-table-fixed-columns">
|
||||
<div style={{ width: '380px', padding: '0 12px' }}>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1">
|
||||
<div
|
||||
className="h-1 rounded-full transition-all duration-300"
|
||||
@@ -154,14 +170,57 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
/>
|
||||
</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 */}
|
||||
{!isCollapsed && (
|
||||
<div className="tasks-container">
|
||||
<div className="task-group-body">
|
||||
{group.tasks.length === 0 ? (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<div className="task-group-empty">
|
||||
<div className="task-table-fixed-columns">
|
||||
<div style={{ width: '380px', padding: '20px 12px' }}>
|
||||
<div className="text-center text-gray-500">
|
||||
<Text type="secondary">No tasks in this group</Text>
|
||||
<br />
|
||||
<Button
|
||||
@@ -173,9 +232,12 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
Add first task
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
||||
<div className="divide-y divide-gray-100">
|
||||
<div className="task-group-tasks">
|
||||
{group.tasks.map((task, index) => (
|
||||
<TaskRow
|
||||
key={task.id}
|
||||
@@ -185,6 +247,8 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={selectedTaskIds.includes(task.id!)}
|
||||
index={index}
|
||||
onSelect={onSelectTask}
|
||||
onToggleSubtasks={onToggleSubtasks}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -192,7 +256,155 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -26,10 +26,9 @@ import {
|
||||
setGroup,
|
||||
fetchTaskGroups,
|
||||
reorderTasks,
|
||||
collapseAllGroups,
|
||||
expandAllGroups,
|
||||
} 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 TaskRow from './TaskRow';
|
||||
import BulkActionBar from './BulkActionBar';
|
||||
@@ -66,9 +65,8 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
archived,
|
||||
} = useSelector((state: RootState) => state.taskReducer);
|
||||
|
||||
// Selection state (assuming you have a selection slice)
|
||||
// const selectedTaskIds = useSelector((state: RootState) => state.selection?.selectedTaskIds || []);
|
||||
const selectedTaskIds: string[] = []; // Temporary placeholder
|
||||
// Selection state
|
||||
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([]);
|
||||
|
||||
// Drag and Drop sensors
|
||||
const sensors = useSensors(
|
||||
@@ -204,12 +202,12 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
|
||||
const handleCollapseAll = () => {
|
||||
// This would need to be implemented in the tasks slice
|
||||
// dispatch(collapseAllGroups());
|
||||
console.log('Collapse all groups');
|
||||
};
|
||||
|
||||
const handleExpandAll = () => {
|
||||
// This would need to be implemented in the tasks slice
|
||||
// dispatch(expandAllGroups());
|
||||
console.log('Expand all groups');
|
||||
};
|
||||
|
||||
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) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
@@ -285,7 +298,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Task Groups */}
|
||||
{/* Task Groups Container */}
|
||||
<div className="task-groups-container">
|
||||
{loadingGroups ? (
|
||||
<Card>
|
||||
@@ -308,7 +321,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="task-groups">
|
||||
{taskGroups.map((group) => (
|
||||
<TaskGroup
|
||||
key={group.id}
|
||||
@@ -316,6 +329,8 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
projectId={projectId}
|
||||
currentGrouping={groupBy}
|
||||
selectedTaskIds={selectedTaskIds}
|
||||
onSelectTask={handleSelectTask}
|
||||
onToggleSubtasks={handleToggleSubtasks}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -335,6 +350,60 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
</DndContext>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,13 +3,13 @@ import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Checkbox, Avatar, Tag, Progress, Typography, Space, Button, Tooltip } from 'antd';
|
||||
import {
|
||||
DragOutlined,
|
||||
HolderOutlined,
|
||||
EyeOutlined,
|
||||
MessageOutlined,
|
||||
PaperClipOutlined,
|
||||
ClockCircleOutlined,
|
||||
} 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';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -90,42 +90,51 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
||||
const dueDate = formatDueDate(task.end_date);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
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' : ''}`}
|
||||
className={`task-row ${isSelected ? 'task-row-selected' : ''} ${isDragOverlay ? 'task-row-drag-overlay' : ''}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="task-row-content">
|
||||
{/* Fixed Columns */}
|
||||
<div className="task-table-fixed-columns">
|
||||
{/* Drag Handle */}
|
||||
<div className="task-table-cell task-table-cell-drag" style={{ width: '40px' }}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DragOutlined />}
|
||||
icon={<HolderOutlined />}
|
||||
className="drag-handle opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Selection Checkbox */}
|
||||
<div className="task-table-cell task-table-cell-checkbox" style={{ width: '40px' }}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => handleSelectChange(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Task Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Task Name and Key */}
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
{task.project_id && (
|
||||
<Text code className="text-xs text-gray-500">
|
||||
{task.key}
|
||||
{/* Task Key */}
|
||||
<div className="task-table-cell task-table-cell-key" style={{ width: '80px' }}>
|
||||
{task.project_id && task.task_key && (
|
||||
<Text code className="task-key">
|
||||
{task.task_key}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Task Name */}
|
||||
<div className="task-table-cell task-table-cell-task" style={{ width: '220px' }}>
|
||||
<div className="task-content">
|
||||
<div className="task-header">
|
||||
<Text
|
||||
strong
|
||||
className={`text-sm ${task.complete_ratio === 100 ? 'line-through text-gray-500' : ''}`}
|
||||
className={`task-name ${task.complete_ratio === 100 ? 'task-completed' : ''}`}
|
||||
>
|
||||
{task.name}
|
||||
</Text>
|
||||
@@ -134,69 +143,41 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleToggleSubtasks}
|
||||
className="text-xs text-gray-500 px-1 h-5"
|
||||
className="subtask-toggle"
|
||||
>
|
||||
{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 */}
|
||||
<div className="flex items-center space-x-3 ml-4">
|
||||
{/* Scrollable Columns */}
|
||||
<div className="task-table-scrollable-columns">
|
||||
{/* Progress */}
|
||||
{task.complete_ratio !== undefined && task.complete_ratio > 0 && (
|
||||
<div className="w-16">
|
||||
<div className="task-table-cell" style={{ width: '120px' }}>
|
||||
{task.complete_ratio !== undefined && task.complete_ratio >= 0 && (
|
||||
<div className="task-progress">
|
||||
<Progress
|
||||
percent={task.complete_ratio}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
strokeColor={task.complete_ratio === 100 ? '#52c41a' : '#1890ff'}
|
||||
/>
|
||||
<Text className="task-progress-text">{task.complete_ratio}%</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Assignees */}
|
||||
{/* Members */}
|
||||
<div className="task-table-cell" style={{ width: '150px' }}>
|
||||
{task.assignees && task.assignees.length > 0 && (
|
||||
<Avatar.Group size="small" maxCount={3}>
|
||||
{task.assignees.map((assignee) => (
|
||||
<Tooltip key={assignee.id} title={assignee.name}>
|
||||
<Avatar
|
||||
size="small"
|
||||
src={assignee.avatar_url}
|
||||
style={{ backgroundColor: assignee.color_code }}
|
||||
>
|
||||
{assignee.name?.charAt(0)?.toUpperCase()}
|
||||
</Avatar>
|
||||
@@ -204,59 +185,87 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
||||
))}
|
||||
</Avatar.Group>
|
||||
)}
|
||||
|
||||
{/* Priority Indicator */}
|
||||
{task.priority_color && (
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: task.priority_color }}
|
||||
title={`Priority: ${task.priority}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Due Date */}
|
||||
{dueDate && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<ClockCircleOutlined className="text-xs text-gray-400" />
|
||||
<Text
|
||||
className={`text-xs ${
|
||||
dueDate.color === 'error' ? 'text-red-500' :
|
||||
dueDate.color === 'warning' ? 'text-orange-500' :
|
||||
'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{dueDate.text}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Task Indicators */}
|
||||
<Space size={0}>
|
||||
{task.comments_count && task.comments_count > 0 && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<MessageOutlined className="text-xs text-gray-400" />
|
||||
<Text className="text-xs text-gray-500">{task.comments_count}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{task.attachments_count && task.attachments_count > 0 && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<PaperClipOutlined className="text-xs text-gray-400" />
|
||||
<Text className="text-xs text-gray-500">{task.attachments_count}</Text>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{/* View/Edit Button */}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
className="opacity-60 hover:opacity-100"
|
||||
onClick={() => {
|
||||
// Handle task view/edit
|
||||
{/* Labels */}
|
||||
<div className="task-table-cell" style={{ width: '150px' }}>
|
||||
{task.labels && task.labels.length > 0 && (
|
||||
<div className="task-labels-column">
|
||||
{task.labels.slice(0, 3).map((label) => (
|
||||
<Tag
|
||||
key={label.id}
|
||||
className="task-label"
|
||||
style={{
|
||||
backgroundColor: label.color_code,
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{label.name}
|
||||
</Tag>
|
||||
))}
|
||||
{task.labels.length > 3 && (
|
||||
<Text type="secondary" className="task-labels-more">
|
||||
+{task.labels.length - 3}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="task-table-cell" style={{ width: '100px' }}>
|
||||
{task.status_name && (
|
||||
<div
|
||||
className="task-status"
|
||||
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>
|
||||
)}
|
||||
{/* Task Indicators */}
|
||||
<div className="task-indicators">
|
||||
{task.comments_count && task.comments_count > 0 && (
|
||||
<div className="task-indicator">
|
||||
<MessageOutlined />
|
||||
<span>{task.comments_count}</span>
|
||||
</div>
|
||||
)}
|
||||
{task.attachments_count && task.attachments_count > 0 && (
|
||||
<div className="task-indicator">
|
||||
<PaperClipOutlined />
|
||||
<span>{task.attachments_count}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,7 +273,7 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
||||
|
||||
{/* Subtasks */}
|
||||
{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) => (
|
||||
<TaskRow
|
||||
key={subtask.id}
|
||||
@@ -278,7 +287,336 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
||||
))}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user