feat(components): introduce new UI components and enhance Vite configuration
- Added AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLabel, CustomNumberLabel, LabelsSelector, Progress, Tag, and Tooltip components for improved UI functionality. - Updated Vite configuration to change the development server port to 5173 and removed unnecessary interop settings for module compatibility. - Enhanced task management components to utilize new task structure and improve performance.
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Button, Typography } from 'antd';
|
||||
import { PlusOutlined, RightOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { IGroupBy, COLUMN_KEYS } from '@/features/tasks/tasks.slice';
|
||||
import { TaskGroup as TaskGroupType, Task } from '@/types/task-management.types';
|
||||
import { taskManagementSelectors } from '@/features/task-management/task-management.slice';
|
||||
import { RootState } from '@/app/store';
|
||||
import TaskRow from './task-row';
|
||||
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
|
||||
@@ -14,9 +13,9 @@ import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-tabl
|
||||
const { Text } = Typography;
|
||||
|
||||
interface TaskGroupProps {
|
||||
group: ITaskListGroup;
|
||||
group: TaskGroupType;
|
||||
projectId: string;
|
||||
currentGrouping: IGroupBy;
|
||||
currentGrouping: 'status' | 'priority' | 'phase';
|
||||
selectedTaskIds: string[];
|
||||
onAddTask?: (groupId: string) => void;
|
||||
onToggleCollapse?: (groupId: string) => void;
|
||||
@@ -34,7 +33,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
onSelectTask,
|
||||
onToggleSubtasks,
|
||||
}) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isCollapsed, setIsCollapsed] = useState(group.collapsed || false);
|
||||
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: group.id,
|
||||
@@ -44,41 +43,37 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
},
|
||||
});
|
||||
|
||||
// Get column visibility from Redux store
|
||||
const columns = useSelector((state: RootState) => state.taskReducer.columns);
|
||||
|
||||
// Helper function to check if a column is visible
|
||||
const isColumnVisible = (columnKey: string) => {
|
||||
const column = columns.find(col => col.key === columnKey);
|
||||
return column ? column.pinned : true; // Default to visible if column not found
|
||||
};
|
||||
|
||||
// Get task IDs for sortable context
|
||||
const taskIds = group.tasks.map(task => task.id!);
|
||||
// Get all tasks from the store
|
||||
const allTasks = useSelector(taskManagementSelectors.selectAll);
|
||||
|
||||
// Get tasks for this group using memoization for performance
|
||||
const groupTasks = useMemo(() => {
|
||||
return group.taskIds
|
||||
.map(taskId => allTasks.find(task => task.id === taskId))
|
||||
.filter((task): task is Task => task !== undefined);
|
||||
}, [group.taskIds, allTasks]);
|
||||
|
||||
// Calculate group statistics
|
||||
const completedTasks = group.tasks.filter(
|
||||
task => task.status_category?.is_done || task.complete_ratio === 100
|
||||
).length;
|
||||
const totalTasks = group.tasks.length;
|
||||
const completedTasks = useMemo(() => {
|
||||
return groupTasks.filter(task => task.progress === 100).length;
|
||||
}, [groupTasks]);
|
||||
|
||||
const totalTasks = groupTasks.length;
|
||||
const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
|
||||
|
||||
// Get group color based on grouping type
|
||||
const getGroupColor = () => {
|
||||
if (group.color_code) return group.color_code;
|
||||
if (group.color) return group.color;
|
||||
|
||||
// Fallback colors based on group value
|
||||
switch (currentGrouping) {
|
||||
case 'status':
|
||||
return group.id === 'todo' ? '#faad14' : group.id === 'doing' ? '#1890ff' : '#52c41a';
|
||||
return group.groupValue === 'todo' ? '#faad14' :
|
||||
group.groupValue === 'doing' ? '#1890ff' : '#52c41a';
|
||||
case 'priority':
|
||||
return group.id === 'critical'
|
||||
? '#ff4d4f'
|
||||
: group.id === 'high'
|
||||
? '#fa8c16'
|
||||
: group.id === 'medium'
|
||||
? '#faad14'
|
||||
: '#52c41a';
|
||||
return group.groupValue === 'critical' ? '#ff4d4f' :
|
||||
group.groupValue === 'high' ? '#fa8c16' :
|
||||
group.groupValue === 'medium' ? '#faad14' : '#52c41a';
|
||||
case 'phase':
|
||||
return '#722ed1';
|
||||
default:
|
||||
@@ -118,7 +113,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
className="task-group-header-button"
|
||||
/>
|
||||
<Text strong className="task-group-header-text">
|
||||
{group.name} ({totalTasks})
|
||||
{group.title} ({totalTasks})
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,36 +143,24 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="task-table-scrollable-columns">
|
||||
{isColumnVisible(COLUMN_KEYS.PROGRESS) && (
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '90px' }}>
|
||||
<Text className="column-header-text">Progress</Text>
|
||||
</div>
|
||||
)}
|
||||
{isColumnVisible(COLUMN_KEYS.ASSIGNEES) && (
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
|
||||
<Text className="column-header-text">Members</Text>
|
||||
</div>
|
||||
)}
|
||||
{isColumnVisible(COLUMN_KEYS.LABELS) && (
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
|
||||
<Text className="column-header-text">Labels</Text>
|
||||
</div>
|
||||
)}
|
||||
{isColumnVisible(COLUMN_KEYS.STATUS) && (
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
||||
<Text className="column-header-text">Status</Text>
|
||||
</div>
|
||||
)}
|
||||
{isColumnVisible(COLUMN_KEYS.PRIORITY) && (
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
||||
<Text className="column-header-text">Priority</Text>
|
||||
</div>
|
||||
)}
|
||||
{isColumnVisible(COLUMN_KEYS.TIME_TRACKING) && (
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '120px' }}>
|
||||
<Text className="column-header-text">Time Tracking</Text>
|
||||
</div>
|
||||
)}
|
||||
<div className="task-table-cell task-table-header-cell" style={{ width: '90px' }}>
|
||||
<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: '200px' }}>
|
||||
<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>
|
||||
@@ -189,7 +172,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
className="task-group-body"
|
||||
style={{ borderLeft: `4px solid ${getGroupColor()}` }}
|
||||
>
|
||||
{group.tasks.length === 0 ? (
|
||||
{groupTasks.length === 0 ? (
|
||||
<div className="task-group-empty">
|
||||
<div className="task-table-fixed-columns">
|
||||
<div style={{ width: '380px', padding: '20px 12px' }}>
|
||||
@@ -209,16 +192,16 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
||||
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
|
||||
<div className="task-group-tasks">
|
||||
{group.tasks.map((task, index) => (
|
||||
{groupTasks.map((task, index) => (
|
||||
<TaskRow
|
||||
key={task.id}
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
groupId={group.id}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={selectedTaskIds.includes(task.id!)}
|
||||
isSelected={selectedTaskIds.includes(task.id)}
|
||||
index={index}
|
||||
onSelect={onSelectTask}
|
||||
onToggleSubtasks={onToggleSubtasks}
|
||||
|
||||
@@ -17,13 +17,25 @@ import {
|
||||
} from '@dnd-kit/sortable';
|
||||
import { Card, Spin, Empty } from 'antd';
|
||||
import { RootState } from '@/app/store';
|
||||
import {
|
||||
IGroupBy,
|
||||
setGroup,
|
||||
fetchTaskGroups,
|
||||
import {
|
||||
taskManagementSelectors,
|
||||
reorderTasks,
|
||||
} from '@/features/tasks/tasks.slice';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
moveTaskToGroup,
|
||||
setLoading,
|
||||
fetchTasks
|
||||
} from '@/features/task-management/task-management.slice';
|
||||
import {
|
||||
selectTaskGroups,
|
||||
selectCurrentGrouping,
|
||||
setCurrentGrouping
|
||||
} from '@/features/task-management/grouping.slice';
|
||||
import {
|
||||
selectSelectedTaskIds,
|
||||
toggleTaskSelection,
|
||||
clearSelection
|
||||
} from '@/features/task-management/selection.slice';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||
import TaskGroup from './task-group';
|
||||
import TaskRow from './task-row';
|
||||
import BulkActionBar from './bulk-action-bar';
|
||||
@@ -38,7 +50,7 @@ interface TaskListBoardProps {
|
||||
}
|
||||
|
||||
interface DragState {
|
||||
activeTask: IProjectTask | null;
|
||||
activeTask: Task | null;
|
||||
activeGroupId: string | null;
|
||||
}
|
||||
|
||||
@@ -49,18 +61,16 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
activeGroupId: null,
|
||||
});
|
||||
|
||||
// Redux selectors
|
||||
const {
|
||||
taskGroups,
|
||||
loadingGroups,
|
||||
error,
|
||||
groupBy,
|
||||
search,
|
||||
archived,
|
||||
} = useSelector((state: RootState) => state.taskReducer);
|
||||
// Enable real-time socket updates for task changes
|
||||
useTaskSocketHandlers();
|
||||
|
||||
// Selection state
|
||||
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([]);
|
||||
// Redux selectors using new task management slices
|
||||
const tasks = useSelector(taskManagementSelectors.selectAll);
|
||||
const taskGroups = useSelector(selectTaskGroups);
|
||||
const currentGrouping = useSelector(selectCurrentGrouping);
|
||||
const selectedTaskIds = useSelector(selectSelectedTaskIds);
|
||||
const loading = useSelector((state: RootState) => state.taskManagement.loading);
|
||||
const error = useSelector((state: RootState) => state.taskManagement.error);
|
||||
|
||||
// Drag and Drop sensors
|
||||
const sensors = useSensors(
|
||||
@@ -77,24 +87,25 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
// Fetch task groups when component mounts or dependencies change
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
dispatch(fetchTaskGroups(projectId));
|
||||
// Fetch real tasks from API
|
||||
dispatch(fetchTasks(projectId));
|
||||
}
|
||||
}, [dispatch, projectId, groupBy, search, archived]);
|
||||
}, [dispatch, projectId, currentGrouping]);
|
||||
|
||||
// Memoized calculations
|
||||
const allTaskIds = useMemo(() => {
|
||||
return taskGroups.flatMap(group => group.tasks.map(task => task.id!));
|
||||
}, [taskGroups]);
|
||||
return tasks.map(task => task.id);
|
||||
}, [tasks]);
|
||||
|
||||
const totalTasksCount = useMemo(() => {
|
||||
return taskGroups.reduce((sum, group) => sum + group.tasks.length, 0);
|
||||
}, [taskGroups]);
|
||||
return tasks.length;
|
||||
}, [tasks]);
|
||||
|
||||
const hasSelection = selectedTaskIds.length > 0;
|
||||
|
||||
// Handlers
|
||||
const handleGroupingChange = (newGroupBy: IGroupBy) => {
|
||||
dispatch(setGroup(newGroupBy));
|
||||
const handleGroupingChange = (newGroupBy: typeof currentGrouping) => {
|
||||
dispatch(setCurrentGrouping(newGroupBy));
|
||||
};
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
@@ -102,15 +113,17 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const taskId = active.id as string;
|
||||
|
||||
// Find the task and its group
|
||||
let activeTask: IProjectTask | null = null;
|
||||
const activeTask = tasks.find(t => t.id === taskId) || null;
|
||||
let activeGroupId: string | null = null;
|
||||
|
||||
for (const group of taskGroups) {
|
||||
const task = group.tasks.find(t => t.id === taskId);
|
||||
if (task) {
|
||||
activeTask = task;
|
||||
activeGroupId = group.id;
|
||||
break;
|
||||
if (activeTask) {
|
||||
// Determine group ID based on current grouping
|
||||
if (currentGrouping === 'status') {
|
||||
activeGroupId = `status-${activeTask.status}`;
|
||||
} else if (currentGrouping === 'priority') {
|
||||
activeGroupId = `priority-${activeTask.priority}`;
|
||||
} else if (currentGrouping === 'phase') {
|
||||
activeGroupId = `phase-${activeTask.phase}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,71 +152,76 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const activeTaskId = active.id as string;
|
||||
const overContainer = over.id as string;
|
||||
|
||||
// Determine if dropping on a group or task
|
||||
const overGroup = taskGroups.find(g => g.id === overContainer);
|
||||
// Parse the group ID to get group type and value
|
||||
const parseGroupId = (groupId: string) => {
|
||||
const [groupType, ...groupValueParts] = groupId.split('-');
|
||||
return {
|
||||
groupType: groupType as 'status' | 'priority' | 'phase',
|
||||
groupValue: groupValueParts.join('-')
|
||||
};
|
||||
};
|
||||
|
||||
// Determine target group
|
||||
let targetGroupId = overContainer;
|
||||
let targetIndex = -1;
|
||||
|
||||
if (!overGroup) {
|
||||
// Dropping on a task, find which group it belongs to
|
||||
for (const group of taskGroups) {
|
||||
const taskIndex = group.tasks.findIndex(t => t.id === overContainer);
|
||||
if (taskIndex !== -1) {
|
||||
targetGroupId = group.id;
|
||||
targetIndex = taskIndex;
|
||||
break;
|
||||
}
|
||||
// Check if dropping on a task or a group
|
||||
const targetTask = tasks.find(t => t.id === overContainer);
|
||||
if (targetTask) {
|
||||
// Dropping on a task, determine its group
|
||||
if (currentGrouping === 'status') {
|
||||
targetGroupId = `status-${targetTask.status}`;
|
||||
} else if (currentGrouping === 'priority') {
|
||||
targetGroupId = `priority-${targetTask.priority}`;
|
||||
} else if (currentGrouping === 'phase') {
|
||||
targetGroupId = `phase-${targetTask.phase}`;
|
||||
}
|
||||
|
||||
// Find the index of the target task within its group
|
||||
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
||||
if (targetGroup) {
|
||||
targetIndex = targetGroup.taskIds.indexOf(targetTask.id);
|
||||
}
|
||||
}
|
||||
|
||||
const sourceGroupInfo = parseGroupId(dragState.activeGroupId);
|
||||
const targetGroupInfo = parseGroupId(targetGroupId);
|
||||
|
||||
// If moving between different groups, update the task's group property
|
||||
if (dragState.activeGroupId !== targetGroupId) {
|
||||
dispatch(moveTaskToGroup({
|
||||
taskId: activeTaskId,
|
||||
groupType: targetGroupInfo.groupType,
|
||||
groupValue: targetGroupInfo.groupValue
|
||||
}));
|
||||
}
|
||||
|
||||
// Handle reordering within the same group or between groups
|
||||
const sourceGroup = taskGroups.find(g => g.id === dragState.activeGroupId);
|
||||
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
||||
|
||||
if (!sourceGroup || !targetGroup) return;
|
||||
if (sourceGroup && targetGroup) {
|
||||
const sourceIndex = sourceGroup.taskIds.indexOf(activeTaskId);
|
||||
const finalTargetIndex = targetIndex === -1 ? targetGroup.taskIds.length : targetIndex;
|
||||
|
||||
const sourceIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId);
|
||||
if (sourceIndex === -1) return;
|
||||
// Calculate new order values
|
||||
const allTasksInTargetGroup = targetGroup.taskIds.map(id => tasks.find(t => t.id === id)!);
|
||||
const newOrder = allTasksInTargetGroup.map((task, index) => {
|
||||
if (index < finalTargetIndex) return task.order;
|
||||
if (index === finalTargetIndex) return dragState.activeTask!.order;
|
||||
return task.order + 1;
|
||||
});
|
||||
|
||||
// Calculate new positions
|
||||
const finalTargetIndex = targetIndex === -1 ? targetGroup.tasks.length : targetIndex;
|
||||
|
||||
// Create updated task arrays
|
||||
const updatedSourceTasks = [...sourceGroup.tasks];
|
||||
const [movedTask] = updatedSourceTasks.splice(sourceIndex, 1);
|
||||
|
||||
let updatedTargetTasks: IProjectTask[];
|
||||
if (sourceGroup.id === targetGroup.id) {
|
||||
// Moving within the same group
|
||||
updatedTargetTasks = updatedSourceTasks;
|
||||
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
|
||||
} else {
|
||||
// Moving between different groups
|
||||
updatedTargetTasks = [...targetGroup.tasks];
|
||||
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
|
||||
// Dispatch reorder action
|
||||
dispatch(reorderTasks({
|
||||
taskIds: [activeTaskId, ...allTasksInTargetGroup.map(t => t.id)],
|
||||
newOrder: [dragState.activeTask!.order, ...newOrder]
|
||||
}));
|
||||
}
|
||||
|
||||
// Dispatch the reorder action
|
||||
dispatch(reorderTasks({
|
||||
activeGroupId: sourceGroup.id,
|
||||
overGroupId: targetGroup.id,
|
||||
fromIndex: sourceIndex,
|
||||
toIndex: finalTargetIndex,
|
||||
task: movedTask,
|
||||
updatedSourceTasks,
|
||||
updatedTargetTasks,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSelectTask = (taskId: string, selected: boolean) => {
|
||||
setSelectedTaskIds(prev => {
|
||||
if (selected) {
|
||||
return [...prev, taskId];
|
||||
} else {
|
||||
return prev.filter(id => id !== taskId);
|
||||
}
|
||||
});
|
||||
dispatch(toggleTaskSelection(taskId));
|
||||
};
|
||||
|
||||
const handleToggleSubtasks = (taskId: string) => {
|
||||
@@ -240,15 +258,15 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
<BulkActionBar
|
||||
selectedTaskIds={selectedTaskIds}
|
||||
totalSelected={selectedTaskIds.length}
|
||||
currentGrouping={groupBy}
|
||||
currentGrouping={currentGrouping as any}
|
||||
projectId={projectId}
|
||||
onClearSelection={() => setSelectedTaskIds([])}
|
||||
onClearSelection={() => dispatch(clearSelection())}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Task Groups Container */}
|
||||
<div className="task-groups-container">
|
||||
{loadingGroups ? (
|
||||
{loading ? (
|
||||
<Card>
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Spin size="large" />
|
||||
@@ -275,7 +293,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
key={group.id}
|
||||
group={group}
|
||||
projectId={projectId}
|
||||
currentGrouping={groupBy}
|
||||
currentGrouping={currentGrouping}
|
||||
selectedTaskIds={selectedTaskIds}
|
||||
onSelectTask={handleSelectTask}
|
||||
onToggleSubtasks={handleToggleSubtasks}
|
||||
@@ -289,7 +307,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
task={dragState.activeTask}
|
||||
projectId={projectId}
|
||||
groupId={dragState.activeGroupId!}
|
||||
currentGrouping={groupBy}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={false}
|
||||
isDragOverlay
|
||||
/>
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Checkbox, Avatar, Tag, Progress, Typography, Space, Button, Tooltip } from 'antd';
|
||||
import {
|
||||
HolderOutlined,
|
||||
EyeOutlined,
|
||||
MessageOutlined,
|
||||
PaperClipOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { IGroupBy, COLUMN_KEYS } from '@/features/tasks/tasks.slice';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { RootState } from '@/app/store';
|
||||
|
||||
const { Text } = Typography;
|
||||
import { AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLabel, CustomNumberLabel, LabelsSelector, Progress, Tag, Tooltip } from '@/components';
|
||||
|
||||
interface TaskRowProps {
|
||||
task: IProjectTask;
|
||||
task: Task;
|
||||
projectId: string;
|
||||
groupId: string;
|
||||
currentGrouping: IGroupBy;
|
||||
currentGrouping: 'status' | 'priority' | 'phase';
|
||||
isSelected: boolean;
|
||||
isDragOverlay?: boolean;
|
||||
index?: number;
|
||||
@@ -28,7 +24,7 @@ interface TaskRowProps {
|
||||
onToggleSubtasks?: (taskId: string) => void;
|
||||
}
|
||||
|
||||
const TaskRow: React.FC<TaskRowProps> = ({
|
||||
const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
task,
|
||||
projectId,
|
||||
groupId,
|
||||
@@ -47,7 +43,7 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: task.id!,
|
||||
id: task.id,
|
||||
data: {
|
||||
type: 'task',
|
||||
taskId: task.id,
|
||||
@@ -56,33 +52,32 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
||||
disabled: isDragOverlay,
|
||||
});
|
||||
|
||||
// Get column visibility from Redux store
|
||||
const columns = useSelector((state: RootState) => state.taskReducer.columns);
|
||||
// Get theme from Redux store
|
||||
const themeMode = useSelector((state: RootState) => state.themeReducer?.mode || 'light');
|
||||
|
||||
// Helper function to check if a column is visible
|
||||
const isColumnVisible = (columnKey: string) => {
|
||||
const column = columns.find(col => col.key === columnKey);
|
||||
return column ? column.pinned : true; // Default to visible if column not found
|
||||
};
|
||||
// Memoize derived values for performance
|
||||
const isDarkMode = useMemo(() => themeMode === 'dark', [themeMode]);
|
||||
|
||||
const style = {
|
||||
// Memoize style calculations
|
||||
const style = useMemo(() => ({
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
}), [transform, transition, isDragging]);
|
||||
|
||||
const handleSelectChange = (checked: boolean) => {
|
||||
onSelect?.(task.id!, checked);
|
||||
};
|
||||
// Memoize event handlers to prevent unnecessary re-renders
|
||||
const handleSelectChange = useCallback((checked: boolean) => {
|
||||
onSelect?.(task.id, checked);
|
||||
}, [onSelect, task.id]);
|
||||
|
||||
const handleToggleSubtasks = () => {
|
||||
onToggleSubtasks?.(task.id!);
|
||||
};
|
||||
const handleToggleSubtasks = useCallback(() => {
|
||||
onToggleSubtasks?.(task.id);
|
||||
}, [onToggleSubtasks, task.id]);
|
||||
|
||||
// Format due date
|
||||
const formatDueDate = (dateString?: string) => {
|
||||
if (!dateString) return null;
|
||||
const date = new Date(dateString);
|
||||
// Format due date - memoized for performance
|
||||
const dueDate = useMemo(() => {
|
||||
if (!task.dueDate) return null;
|
||||
const date = new Date(task.dueDate);
|
||||
const now = new Date();
|
||||
const diffTime = date.getTime() - now.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
@@ -96,557 +91,286 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
||||
} else {
|
||||
return { text: `Due ${date.toLocaleDateString()}`, color: 'default' };
|
||||
}
|
||||
}, [task.dueDate]);
|
||||
|
||||
// Memoize assignees for AvatarGroup to prevent unnecessary re-renders
|
||||
const avatarGroupMembers = useMemo(() => {
|
||||
return task.assignees?.map(assigneeId => ({
|
||||
id: assigneeId,
|
||||
team_member_id: assigneeId,
|
||||
name: assigneeId // TODO: Map to actual user names
|
||||
})) || [];
|
||||
}, [task.assignees]);
|
||||
|
||||
// Memoize class names for better performance
|
||||
const containerClassName = useMemo(() => `
|
||||
border-b transition-all duration-300
|
||||
${isDarkMode
|
||||
? `border-gray-700 bg-gray-900 hover:bg-gray-800 ${isSelected ? 'bg-blue-900/20' : ''}`
|
||||
: `border-gray-200 bg-white hover:bg-gray-50 ${isSelected ? 'bg-blue-50' : ''}`
|
||||
}
|
||||
${isSelected ? 'border-l-4 border-l-blue-500' : ''}
|
||||
${isDragOverlay
|
||||
? `rounded shadow-lg ${isDarkMode ? 'bg-gray-900 border border-gray-600' : 'bg-white border border-gray-300'}`
|
||||
: ''
|
||||
}
|
||||
`, [isDarkMode, isSelected, isDragOverlay]);
|
||||
|
||||
const fixedColumnsClassName = useMemo(() => `
|
||||
flex sticky left-0 z-10 border-r-2 shadow-sm ${isDarkMode ? 'bg-gray-900 border-gray-700' : 'bg-white border-gray-200'}
|
||||
`, [isDarkMode]);
|
||||
|
||||
const taskNameClassName = useMemo(() => `
|
||||
text-sm font-medium flex-1
|
||||
overflow-hidden text-ellipsis whitespace-nowrap transition-colors duration-300
|
||||
${isDarkMode ? 'text-gray-100' : 'text-gray-900'}
|
||||
${task.progress === 100
|
||||
? `line-through ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`
|
||||
: ''
|
||||
}
|
||||
`, [isDarkMode, task.progress]);
|
||||
|
||||
// Get priority color
|
||||
const getPriorityColor = (priority: string) => {
|
||||
const colors = {
|
||||
critical: '#ff4d4f',
|
||||
high: '#ff7a45',
|
||||
medium: '#faad14',
|
||||
low: '#52c41a',
|
||||
};
|
||||
return colors[priority as keyof typeof colors] || '#d9d9d9';
|
||||
};
|
||||
|
||||
const dueDate = formatDueDate(task.end_date);
|
||||
// Get status color
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
todo: '#f0f0f0',
|
||||
doing: '#1890ff',
|
||||
done: '#52c41a',
|
||||
};
|
||||
return colors[status as keyof typeof colors] || '#d9d9d9';
|
||||
};
|
||||
|
||||
// Create adapter for LabelsSelector to work with new Task type
|
||||
const taskAdapter = useMemo(() => {
|
||||
// Convert new Task type to IProjectTask for compatibility
|
||||
return {
|
||||
id: task.id,
|
||||
name: task.title,
|
||||
parent_task_id: null, // TODO: Add parent task support
|
||||
all_labels: task.labels?.map(label => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color_code: label.color
|
||||
})) || [],
|
||||
labels: task.labels?.map(label => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color_code: label.color
|
||||
})) || [],
|
||||
} as any; // Type assertion for compatibility
|
||||
}, [task.id, task.title, task.labels]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`task-row ${isSelected ? 'task-row-selected' : ''} ${isDragOverlay ? 'task-row-drag-overlay' : ''}`}
|
||||
className={containerClassName}
|
||||
>
|
||||
<div className="task-row-content">
|
||||
<div className="flex h-10 max-h-10 overflow-visible relative min-w-[1200px]">
|
||||
{/* Fixed Columns */}
|
||||
<div className="task-table-fixed-columns">
|
||||
<div className={fixedColumnsClassName}>
|
||||
{/* Drag Handle */}
|
||||
<div className="task-table-cell task-table-cell-drag" style={{ width: '40px' }}>
|
||||
<div className={`w-10 flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<Button
|
||||
type="text"
|
||||
variant="text"
|
||||
size="small"
|
||||
icon={<HolderOutlined />}
|
||||
className="drag-handle opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
|
||||
className="opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
|
||||
isDarkMode={isDarkMode}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Selection Checkbox */}
|
||||
<div className="task-table-cell task-table-cell-checkbox" style={{ width: '40px' }}>
|
||||
<div className={`w-10 flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => handleSelectChange(e.target.checked)}
|
||||
onChange={handleSelectChange}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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 className={`w-20 flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<Tag
|
||||
backgroundColor={isDarkMode ? "#374151" : "#f0f0f0"}
|
||||
color={isDarkMode ? "#d1d5db" : "#666"}
|
||||
className="truncate whitespace-nowrap max-w-full"
|
||||
>
|
||||
{task.task_key}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* Task Name */}
|
||||
<div className="task-table-cell task-table-cell-task" style={{ width: '475px' }}>
|
||||
<div className="task-content">
|
||||
<div className="task-header">
|
||||
<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 className="w-[475px] flex items-center px-2">
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center h-full overflow-hidden">
|
||||
<div className="flex items-center gap-2 h-5 overflow-hidden">
|
||||
<span className={taskNameClassName}>
|
||||
{task.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable Columns */}
|
||||
<div className="task-table-scrollable-columns">
|
||||
<div className="flex flex-1 min-w-0">
|
||||
{/* Progress */}
|
||||
{isColumnVisible(COLUMN_KEYS.PROGRESS) && (
|
||||
<div className="task-table-cell" style={{ width: '90px' }}>
|
||||
{task.complete_ratio !== undefined && task.complete_ratio >= 0 && (
|
||||
<div className="task-progress">
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={task.complete_ratio}
|
||||
size={32}
|
||||
strokeColor={task.complete_ratio === 100 ? '#52c41a' : '#1890ff'}
|
||||
strokeWidth={4}
|
||||
showInfo={true}
|
||||
format={(percent) => <span style={{ fontSize: '10px', fontWeight: '500' }}>{percent}%</span>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={`w-[90px] flex items-center justify-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
{task.progress !== undefined && task.progress >= 0 && (
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={task.progress}
|
||||
size={24}
|
||||
strokeColor={task.progress === 100 ? '#52c41a' : '#1890ff'}
|
||||
strokeWidth={2}
|
||||
showInfo={true}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Members */}
|
||||
{isColumnVisible(COLUMN_KEYS.ASSIGNEES) && (
|
||||
<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"
|
||||
>
|
||||
{assignee.name?.charAt(0)?.toUpperCase()}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
<div className={`w-[150px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
{avatarGroupMembers.length > 0 && (
|
||||
<AvatarGroup
|
||||
members={avatarGroupMembers}
|
||||
size={24}
|
||||
maxCount={3}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className={`
|
||||
w-6 h-6 rounded-full border border-dashed flex items-center justify-center
|
||||
transition-colors duration-200
|
||||
${isDarkMode
|
||||
? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400'
|
||||
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
// TODO: Implement assignee selector functionality
|
||||
console.log('Add assignee clicked for task:', task.id);
|
||||
}}
|
||||
>
|
||||
<span className="text-xs">+</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
{isColumnVisible(COLUMN_KEYS.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 className={`w-[200px] max-w-[200px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center gap-1 flex-wrap h-full w-full overflow-visible relative">
|
||||
{task.labels?.map((label, index) => (
|
||||
label.end && label.names && label.name ? (
|
||||
<CustomNumberLabel
|
||||
key={`${label.id}-${index}`}
|
||||
labelList={label.names}
|
||||
namesString={label.name}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
) : (
|
||||
<CustomColordLabel
|
||||
key={`${label.id}-${index}`}
|
||||
label={label}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
<LabelsSelector
|
||||
task={taskAdapter}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
{isColumnVisible(COLUMN_KEYS.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>
|
||||
)}
|
||||
<div className={`w-[100px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<Tag
|
||||
backgroundColor={getStatusColor(task.status)}
|
||||
color="white"
|
||||
className="text-xs font-medium uppercase"
|
||||
>
|
||||
{task.status}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
{isColumnVisible(COLUMN_KEYS.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 className={`w-[100px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: getPriorityColor(task.priority) }}
|
||||
/>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
{task.priority}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time Tracking */}
|
||||
<div className={`w-[120px] flex items-center px-2 border-r ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center gap-2 h-full overflow-hidden">
|
||||
{task.timeTracking?.logged && task.timeTracking.logged > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`} />
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
{typeof task.timeTracking.logged === 'number'
|
||||
? `${task.timeTracking.logged}h`
|
||||
: task.timeTracking.logged
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Time Tracking */}
|
||||
{isColumnVisible(COLUMN_KEYS.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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtasks */}
|
||||
{task.show_sub_tasks && task.sub_tasks && task.sub_tasks.length > 0 && (
|
||||
<div className="task-subtasks">
|
||||
{task.sub_tasks.map((subtask) => (
|
||||
<TaskRow
|
||||
key={subtask.id}
|
||||
task={subtask}
|
||||
projectId={projectId}
|
||||
groupId={groupId}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={isSelected}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</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: 40px;
|
||||
max-height: 40px;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
min-width: 1200px; /* Ensure minimum width for all columns */
|
||||
}
|
||||
|
||||
.task-table-fixed-columns {
|
||||
display: flex;
|
||||
background: var(--task-bg-primary, white);
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
border-right: 2px solid var(--task-border-primary, #e8e8e8);
|
||||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.task-table-scrollable-columns {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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: 40px;
|
||||
max-height: 40px;
|
||||
min-height: 40px;
|
||||
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: 14px;
|
||||
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;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.task-progress .ant-progress {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}, (prevProps, nextProps) => {
|
||||
// Custom comparison function for React.memo
|
||||
// Only re-render if these specific props change
|
||||
const labelsEqual = prevProps.task.labels.length === nextProps.task.labels.length &&
|
||||
prevProps.task.labels.every((label, index) =>
|
||||
label.id === nextProps.task.labels[index]?.id &&
|
||||
label.name === nextProps.task.labels[index]?.name &&
|
||||
label.color === nextProps.task.labels[index]?.color &&
|
||||
label.end === nextProps.task.labels[index]?.end &&
|
||||
JSON.stringify(label.names) === JSON.stringify(nextProps.task.labels[index]?.names)
|
||||
);
|
||||
|
||||
return (
|
||||
prevProps.task.id === nextProps.task.id &&
|
||||
prevProps.task.assignees === nextProps.task.assignees &&
|
||||
prevProps.task.title === nextProps.task.title &&
|
||||
prevProps.task.progress === nextProps.task.progress &&
|
||||
prevProps.task.status === nextProps.task.status &&
|
||||
prevProps.task.priority === nextProps.task.priority &&
|
||||
labelsEqual &&
|
||||
prevProps.isSelected === nextProps.isSelected &&
|
||||
prevProps.isDragOverlay === nextProps.isDragOverlay &&
|
||||
prevProps.groupId === nextProps.groupId
|
||||
);
|
||||
});
|
||||
|
||||
TaskRow.displayName = 'TaskRow';
|
||||
|
||||
export default TaskRow;
|
||||
Reference in New Issue
Block a user