refactor(task-management): enhance TaskGroup and TaskRow components with column visibility and improved layout
- Integrated Redux for column visibility management in TaskGroup and TaskRow components. - Simplified the rendering of task details based on column visibility settings. - Updated styling for better consistency and responsiveness across task rows and groups. - Removed unused imports and components to streamline the codebase.
This commit is contained in:
@@ -1,17 +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 { Button, Typography, Badge, Space, Tooltip } from 'antd';
|
import { useSelector } from 'react-redux';
|
||||||
import {
|
import { Button, Typography } from 'antd';
|
||||||
CaretRightOutlined,
|
import { PlusOutlined, RightOutlined, DownOutlined } from '@ant-design/icons';
|
||||||
CaretDownOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
MoreOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
import { IGroupBy } from '@/features/tasks/tasks.slice';
|
import { IGroupBy, COLUMN_KEYS } from '@/features/tasks/tasks.slice';
|
||||||
|
import { RootState } from '@/app/store';
|
||||||
import TaskRow from './TaskRow';
|
import TaskRow from './TaskRow';
|
||||||
|
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -46,12 +44,21 @@ 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
|
// Get task IDs for sortable context
|
||||||
const taskIds = group.tasks.map(task => task.id!);
|
const taskIds = group.tasks.map(task => task.id!);
|
||||||
|
|
||||||
// Calculate group statistics
|
// Calculate group statistics
|
||||||
const completedTasks = group.tasks.filter(task =>
|
const completedTasks = group.tasks.filter(
|
||||||
task.status_category?.is_done || task.complete_ratio === 100
|
task => 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;
|
||||||
@@ -59,16 +66,19 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
// Get group color based on grouping type
|
// Get group color based on grouping type
|
||||||
const getGroupColor = () => {
|
const getGroupColor = () => {
|
||||||
if (group.color_code) return group.color_code;
|
if (group.color_code) return group.color_code;
|
||||||
|
|
||||||
// Fallback colors based on group value
|
// Fallback colors based on group value
|
||||||
switch (currentGrouping) {
|
switch (currentGrouping) {
|
||||||
case 'status':
|
case 'status':
|
||||||
return group.id === 'todo' ? '#faad14' :
|
return group.id === 'todo' ? '#faad14' : group.id === 'doing' ? '#1890ff' : '#52c41a';
|
||||||
group.id === 'doing' ? '#1890ff' : '#52c41a';
|
|
||||||
case 'priority':
|
case 'priority':
|
||||||
return group.id === 'critical' ? '#ff4d4f' :
|
return group.id === 'critical'
|
||||||
group.id === 'high' ? '#fa8c16' :
|
? '#ff4d4f'
|
||||||
group.id === 'medium' ? '#faad14' : '#52c41a';
|
: group.id === 'high'
|
||||||
|
? '#fa8c16'
|
||||||
|
: group.id === 'medium'
|
||||||
|
? '#faad14'
|
||||||
|
: '#52c41a';
|
||||||
case 'phase':
|
case 'phase':
|
||||||
return '#722ed1';
|
return '#722ed1';
|
||||||
default:
|
default:
|
||||||
@@ -86,7 +96,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
className={`task-group ${isOver ? 'drag-over' : ''}`}
|
className={`task-group ${isOver ? 'drag-over' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
@@ -96,83 +106,19 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
{/* Group Header Row */}
|
{/* Group Header Row */}
|
||||||
<div className="task-group-header">
|
<div className="task-group-header">
|
||||||
<div className="task-group-header-row">
|
<div className="task-group-header-row">
|
||||||
<div className="task-table-fixed-columns">
|
<div className="flex items-center p-3">
|
||||||
<div className="task-table-cell task-table-cell-drag" style={{ width: '40px' }}>
|
<Button
|
||||||
<Button
|
type="text"
|
||||||
type="text"
|
size="small"
|
||||||
size="small"
|
icon={isCollapsed ? <RightOutlined /> : <DownOutlined />}
|
||||||
icon={isCollapsed ? <CaretRightOutlined /> : <CaretDownOutlined />}
|
onClick={handleToggleCollapse}
|
||||||
onClick={handleToggleCollapse}
|
className="p-0 w-6 h-6 flex items-center justify-center mr-2"
|
||||||
className="p-0 w-6 h-6 flex items-center justify-center"
|
/>
|
||||||
/>
|
<Text strong className="text-sm">
|
||||||
</div>
|
{group.name} ({totalTasks})
|
||||||
<div className="task-table-cell task-table-cell-checkbox" style={{ width: '40px' }}>
|
</Text>
|
||||||
<div
|
|
||||||
className="w-3 h-3 rounded-full"
|
|
||||||
style={{ backgroundColor: getGroupColor() }}
|
|
||||||
/>
|
|
||||||
</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' }} />
|
|
||||||
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
|
||||||
{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"
|
|
||||||
style={{
|
|
||||||
width: `${completionRate}%`,
|
|
||||||
backgroundColor: getGroupColor(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Column Headers */}
|
{/* Column Headers */}
|
||||||
@@ -180,8 +126,14 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
<div className="task-group-column-headers">
|
<div className="task-group-column-headers">
|
||||||
<div className="task-group-column-headers-row">
|
<div className="task-group-column-headers-row">
|
||||||
<div className="task-table-fixed-columns">
|
<div className="task-table-fixed-columns">
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '40px' }}></div>
|
<div
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '40px' }}></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' }}>
|
<div className="task-table-cell task-table-header-cell" style={{ width: '80px' }}>
|
||||||
<Text className="column-header-text">Key</Text>
|
<Text className="column-header-text">Key</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,24 +142,36 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="task-table-scrollable-columns">
|
<div className="task-table-scrollable-columns">
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '120px' }}>
|
{isColumnVisible(COLUMN_KEYS.PROGRESS) && (
|
||||||
<Text className="column-header-text">Progress</Text>
|
<div className="task-table-cell task-table-header-cell" style={{ width: '90px' }}>
|
||||||
</div>
|
<Text className="column-header-text">Progress</Text>
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
|
</div>
|
||||||
<Text className="column-header-text">Members</Text>
|
)}
|
||||||
</div>
|
{isColumnVisible(COLUMN_KEYS.ASSIGNEES) && (
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
|
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
|
||||||
<Text className="column-header-text">Labels</Text>
|
<Text className="column-header-text">Members</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
)}
|
||||||
<Text className="column-header-text">Status</Text>
|
{isColumnVisible(COLUMN_KEYS.LABELS) && (
|
||||||
</div>
|
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
<Text className="column-header-text">Labels</Text>
|
||||||
<Text className="column-header-text">Priority</Text>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="task-table-cell task-table-header-cell" style={{ width: '120px' }}>
|
{isColumnVisible(COLUMN_KEYS.STATUS) && (
|
||||||
<Text className="column-header-text">Time Tracking</Text>
|
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
|
||||||
</div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,6 +218,11 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Add Task Row - Always show when not collapsed */}
|
||||||
|
<div className="task-group-add-task">
|
||||||
|
<AddTaskListRow groupId={group.id} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -280,8 +249,8 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
|
|
||||||
.task-group-header-row {
|
.task-group-header-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 42px;
|
height: 40px;
|
||||||
max-height: 42px;
|
max-height: 40px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,8 +271,8 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
|
|
||||||
.task-group-column-headers-row {
|
.task-group-column-headers-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
height: 40px;
|
||||||
max-height: 32px;
|
max-height: 40px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +316,21 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-group-add-task {
|
||||||
|
background: var(--task-bg-primary, white);
|
||||||
|
border-top: 1px solid var(--task-border-secondary, #f0f0f0);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 0 12px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-group-add-task:hover {
|
||||||
|
background: var(--task-hover-bg, #fafafa);
|
||||||
|
}
|
||||||
|
|
||||||
.task-table-fixed-columns {
|
.task-table-fixed-columns {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
@@ -369,9 +353,9 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
|
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
height: 42px;
|
height: 40px;
|
||||||
max-height: 42px;
|
max-height: 40px;
|
||||||
min-height: 42px;
|
min-height: 40px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--task-text-primary, #262626);
|
color: var(--task-text-primary, #262626);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
@@ -408,4 +392,4 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TaskGroup;
|
export default TaskGroup;
|
||||||
|
|||||||
@@ -13,30 +13,24 @@ import {
|
|||||||
useSensors,
|
useSensors,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import {
|
import {
|
||||||
SortableContext,
|
|
||||||
verticalListSortingStrategy,
|
|
||||||
sortableKeyboardCoordinates,
|
sortableKeyboardCoordinates,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { Card, Button, Select, Space, Typography, Spin, Empty } from 'antd';
|
import { Card, Spin, Empty } from 'antd';
|
||||||
import { ExpandOutlined, CompressOutlined, PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
import {
|
import {
|
||||||
IGroupBy,
|
IGroupBy,
|
||||||
GROUP_BY_OPTIONS,
|
|
||||||
setGroup,
|
setGroup,
|
||||||
fetchTaskGroups,
|
fetchTaskGroups,
|
||||||
reorderTasks,
|
reorderTasks,
|
||||||
} from '@/features/tasks/tasks.slice';
|
} from '@/features/tasks/tasks.slice';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.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';
|
||||||
import GroupingSelector from './GroupingSelector';
|
|
||||||
import { AppDispatch } from '@/app/store';
|
import { AppDispatch } from '@/app/store';
|
||||||
|
|
||||||
const { Title } = Typography;
|
// Import the TaskListFilters component
|
||||||
const { Option } = Select;
|
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
|
||||||
|
|
||||||
interface TaskListBoardProps {
|
interface TaskListBoardProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -200,21 +194,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCollapseAll = () => {
|
|
||||||
// This would need to be implemented in the tasks slice
|
|
||||||
console.log('Collapse all groups');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExpandAll = () => {
|
|
||||||
// This would need to be implemented in the tasks slice
|
|
||||||
console.log('Expand all groups');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
|
||||||
if (projectId) {
|
|
||||||
dispatch(fetchTaskGroups(projectId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectTask = (taskId: string, selected: boolean) => {
|
const handleSelectTask = (taskId: string, selected: boolean) => {
|
||||||
setSelectedTaskIds(prev => {
|
setSelectedTaskIds(prev => {
|
||||||
@@ -244,48 +224,15 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`task-list-board ${className}`}>
|
<div className={`task-list-board ${className}`}>
|
||||||
{/* Header Controls */}
|
{/* Task Filters */}
|
||||||
<Card
|
<Card
|
||||||
size="small"
|
size="small"
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
styles={{ body: { padding: '12px 16px' } }}
|
styles={{ body: { padding: '12px 16px' } }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<React.Suspense fallback={<div>Loading filters...</div>}>
|
||||||
<div className="flex items-center space-x-4">
|
<TaskListFilters position="list" />
|
||||||
<Title level={4} className="mb-0">
|
</React.Suspense>
|
||||||
Tasks ({totalTasksCount})
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<GroupingSelector
|
|
||||||
currentGrouping={groupBy}
|
|
||||||
onChange={handleGroupingChange}
|
|
||||||
options={GROUP_BY_OPTIONS}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<CompressOutlined />}
|
|
||||||
onClick={handleCollapseAll}
|
|
||||||
title="Collapse All Groups"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<ExpandOutlined />}
|
|
||||||
onClick={handleExpandAll}
|
|
||||||
title="Expand All Groups"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={handleRefresh}
|
|
||||||
loading={loadingGroups}
|
|
||||||
title="Refresh"
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Bulk Action Bar */}
|
{/* Bulk Action Bar */}
|
||||||
@@ -356,8 +303,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
max-height: calc(100vh - 300px);
|
max-height: calc(100vh - 300px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: var(--task-bg-secondary, #f5f5f5);
|
padding: 8px 8px 8px 0;
|
||||||
padding: 16px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { Checkbox, Avatar, Tag, Progress, Typography, Space, Button, Tooltip } from 'antd';
|
import { Checkbox, Avatar, Tag, Progress, Typography, Space, Button, Tooltip } from 'antd';
|
||||||
import {
|
import {
|
||||||
HolderOutlined,
|
HolderOutlined,
|
||||||
@@ -10,7 +11,8 @@ import {
|
|||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
import { IGroupBy } from '@/features/tasks/tasks.slice';
|
import { IGroupBy, COLUMN_KEYS } from '@/features/tasks/tasks.slice';
|
||||||
|
import { RootState } from '@/app/store';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -54,6 +56,15 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
|||||||
disabled: isDragOverlay,
|
disabled: isDragOverlay,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
@@ -156,117 +167,131 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
|||||||
{/* Scrollable Columns */}
|
{/* Scrollable Columns */}
|
||||||
<div className="task-table-scrollable-columns">
|
<div className="task-table-scrollable-columns">
|
||||||
{/* Progress */}
|
{/* Progress */}
|
||||||
<div className="task-table-cell" style={{ width: '120px' }}>
|
{isColumnVisible(COLUMN_KEYS.PROGRESS) && (
|
||||||
{task.complete_ratio !== undefined && task.complete_ratio >= 0 && (
|
<div className="task-table-cell" style={{ width: '90px' }}>
|
||||||
<div className="task-progress">
|
{task.complete_ratio !== undefined && task.complete_ratio >= 0 && (
|
||||||
<Progress
|
<div className="task-progress">
|
||||||
percent={task.complete_ratio}
|
<Progress
|
||||||
size="small"
|
type="circle"
|
||||||
showInfo={false}
|
percent={task.complete_ratio}
|
||||||
strokeColor={task.complete_ratio === 100 ? '#52c41a' : '#1890ff'}
|
size={32}
|
||||||
/>
|
strokeColor={task.complete_ratio === 100 ? '#52c41a' : '#1890ff'}
|
||||||
<Text className="task-progress-text">{task.complete_ratio}%</Text>
|
strokeWidth={4}
|
||||||
</div>
|
showInfo={true}
|
||||||
)}
|
format={(percent) => <span style={{ fontSize: '10px', fontWeight: '500' }}>{percent}%</span>}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{/* 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"
|
|
||||||
>
|
|
||||||
{assignee.name?.charAt(0)?.toUpperCase()}
|
|
||||||
</Avatar>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</Avatar.Group>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Task Indicators */}
|
</div>
|
||||||
<div className="task-indicators">
|
)}
|
||||||
{task.comments_count && task.comments_count > 0 && (
|
|
||||||
<div className="task-indicator">
|
{/* Members */}
|
||||||
<MessageOutlined />
|
{isColumnVisible(COLUMN_KEYS.ASSIGNEES) && (
|
||||||
<span>{task.comments_count}</span>
|
<div className="task-table-cell" style={{ width: '150px' }}>
|
||||||
</div>
|
{task.assignees && task.assignees.length > 0 && (
|
||||||
)}
|
<Avatar.Group size="small" maxCount={3}>
|
||||||
{task.attachments_count && task.attachments_count > 0 && (
|
{task.assignees.map((assignee) => (
|
||||||
<div className="task-indicator">
|
<Tooltip key={assignee.id} title={assignee.name}>
|
||||||
<PaperClipOutlined />
|
<Avatar
|
||||||
<span>{task.attachments_count}</span>
|
size="small"
|
||||||
|
>
|
||||||
|
{assignee.name?.charAt(0)?.toUpperCase()}
|
||||||
|
</Avatar>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Avatar.Group>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
</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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -313,8 +338,8 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
|||||||
|
|
||||||
.task-row-content {
|
.task-row-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 42px;
|
height: 40px;
|
||||||
max-height: 42px;
|
max-height: 40px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,9 +365,9 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
|||||||
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
|
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
height: 42px;
|
height: 40px;
|
||||||
max-height: 42px;
|
max-height: 40px;
|
||||||
min-height: 42px;
|
min-height: 40px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--task-text-primary, #262626);
|
color: var(--task-text-primary, #262626);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
@@ -441,13 +466,13 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
|||||||
.task-progress {
|
.task-progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-progress .ant-progress {
|
.task-progress .ant-progress {
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-progress-text {
|
.task-progress-text {
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ export const tabItems: TabItems[] = [
|
|||||||
key: 'tasks-list',
|
key: 'tasks-list',
|
||||||
label: 'Task List',
|
label: 'Task List',
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
element: React.createElement(ProjectViewTaskList),
|
element: React.createElement(ProjectViewEnhancedTasks),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 1,
|
index: 1,
|
||||||
key: 'enhanced-tasks',
|
key: 'task-list-v1',
|
||||||
label: 'Enhanced Tasks',
|
label: 'Task List v1',
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
element: React.createElement(ProjectViewEnhancedTasks),
|
element: React.createElement(ProjectViewTaskList),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 2,
|
index: 2,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Input, { InputRef } from 'antd/es/input';
|
import Input, { InputRef } from 'antd/es/input';
|
||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState, useEffect } from 'react';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { colors } from '@/styles/colors';
|
import { colors } from '@/styles/colors';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -31,7 +33,10 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
|
|||||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||||
const [taskName, setTaskName] = useState<string>('');
|
const [taskName, setTaskName] = useState<string>('');
|
||||||
const [creatingTask, setCreatingTask] = useState<boolean>(false);
|
const [creatingTask, setCreatingTask] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [taskCreationTimeout, setTaskCreationTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||||
const taskInputRef = useRef<InputRef>(null);
|
const taskInputRef = useRef<InputRef>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const currentSession = useAuthService().getCurrentSession();
|
const currentSession = useAuthService().getCurrentSession();
|
||||||
|
|
||||||
@@ -43,13 +48,62 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
|
|||||||
const customBorderColor = useMemo(() => themeMode === 'dark' && ' border-[#303030]', [themeMode]);
|
const customBorderColor = useMemo(() => themeMode === 'dark' && ' border-[#303030]', [themeMode]);
|
||||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||||
|
|
||||||
|
// Cleanup timeout on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (taskCreationTimeout) {
|
||||||
|
clearTimeout(taskCreationTimeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [taskCreationTimeout]);
|
||||||
|
|
||||||
|
// Handle click outside to cancel edit mode
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
isEdit &&
|
||||||
|
!creatingTask &&
|
||||||
|
containerRef.current &&
|
||||||
|
!containerRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
cancelEdit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isEdit, creatingTask]);
|
||||||
|
|
||||||
const createRequestBody = (): ITaskCreateRequest | null => {
|
const createRequestBody = (): ITaskCreateRequest | null => {
|
||||||
if (!projectId || !currentSession) return null;
|
if (!projectId || !currentSession) return null;
|
||||||
const body: ITaskCreateRequest = {
|
const body: ITaskCreateRequest = {
|
||||||
project_id: projectId,
|
id: '',
|
||||||
name: taskName,
|
name: taskName,
|
||||||
reporter_id: currentSession.id,
|
description: '',
|
||||||
|
status_id: '',
|
||||||
|
priority: '',
|
||||||
|
start_date: '',
|
||||||
|
end_date: '',
|
||||||
|
total_hours: 0,
|
||||||
|
total_minutes: 0,
|
||||||
|
billable: false,
|
||||||
|
phase_id: '',
|
||||||
|
parent_task_id: undefined,
|
||||||
|
project_id: projectId,
|
||||||
team_id: currentSession.team_id,
|
team_id: currentSession.team_id,
|
||||||
|
task_key: '',
|
||||||
|
labels: [],
|
||||||
|
assignees: [],
|
||||||
|
names: [],
|
||||||
|
sub_tasks_count: 0,
|
||||||
|
manual_progress: false,
|
||||||
|
progress_value: null,
|
||||||
|
weight: null,
|
||||||
|
reporter_id: currentSession.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const groupBy = getCurrentGroup();
|
const groupBy = getCurrentGroup();
|
||||||
@@ -69,10 +123,14 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
|
|||||||
|
|
||||||
const reset = (scroll = true) => {
|
const reset = (scroll = true) => {
|
||||||
setIsEdit(false);
|
setIsEdit(false);
|
||||||
|
|
||||||
setCreatingTask(false);
|
setCreatingTask(false);
|
||||||
|
|
||||||
setTaskName('');
|
setTaskName('');
|
||||||
|
setError('');
|
||||||
|
if (taskCreationTimeout) {
|
||||||
|
clearTimeout(taskCreationTimeout);
|
||||||
|
setTaskCreationTimeout(null);
|
||||||
|
}
|
||||||
|
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -81,6 +139,16 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
|
|||||||
}, DRAWER_ANIMATION_INTERVAL);
|
}, DRAWER_ANIMATION_INTERVAL);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
setIsEdit(false);
|
||||||
|
setTaskName('');
|
||||||
|
setError('');
|
||||||
|
if (taskCreationTimeout) {
|
||||||
|
clearTimeout(taskCreationTimeout);
|
||||||
|
setTaskCreationTimeout(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onNewTaskReceived = (task: IAddNewTask) => {
|
const onNewTaskReceived = (task: IAddNewTask) => {
|
||||||
if (!groupId) return;
|
if (!groupId) return;
|
||||||
|
|
||||||
@@ -106,49 +174,210 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addInstantTask = async () => {
|
const addInstantTask = async () => {
|
||||||
if (creatingTask || !projectId || !currentSession || taskName.trim() === '') return;
|
// Validation
|
||||||
|
if (creatingTask || !projectId || !currentSession) return;
|
||||||
|
|
||||||
|
const trimmedTaskName = taskName.trim();
|
||||||
|
if (trimmedTaskName === '') {
|
||||||
|
setError('Task name cannot be empty');
|
||||||
|
taskInputRef.current?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setCreatingTask(true);
|
setCreatingTask(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
const body = createRequestBody();
|
const body = createRequestBody();
|
||||||
if (!body) return;
|
if (!body) {
|
||||||
|
setError('Failed to create task. Please try again.');
|
||||||
|
setCreatingTask(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timeout for task creation (10 seconds)
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setCreatingTask(false);
|
||||||
|
setError('Task creation timed out. Please try again.');
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
setTaskCreationTimeout(timeout);
|
||||||
|
|
||||||
socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
|
socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
|
||||||
|
|
||||||
|
// Handle success response
|
||||||
socket?.once(SocketEvents.QUICK_TASK.toString(), (task: IProjectTask) => {
|
socket?.once(SocketEvents.QUICK_TASK.toString(), (task: IProjectTask) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
setTaskCreationTimeout(null);
|
||||||
setCreatingTask(false);
|
setCreatingTask(false);
|
||||||
onNewTaskReceived(task as IAddNewTask);
|
|
||||||
|
if (task && task.id) {
|
||||||
|
onNewTaskReceived(task as IAddNewTask);
|
||||||
|
} else {
|
||||||
|
setError('Failed to create task. Please try again.');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle error response
|
||||||
|
socket?.once('error', (errorData: any) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
setTaskCreationTimeout(null);
|
||||||
|
setCreatingTask(false);
|
||||||
|
const errorMessage = errorData?.message || 'Failed to create task';
|
||||||
|
setError(errorMessage);
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding task:', error);
|
console.error('Error adding task:', error);
|
||||||
setCreatingTask(false);
|
setCreatingTask(false);
|
||||||
|
setError('An unexpected error occurred. Please try again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddTask = () => {
|
const handleAddTask = () => {
|
||||||
setIsEdit(false);
|
if (creatingTask) return; // Prevent multiple submissions
|
||||||
addInstantTask();
|
addInstantTask();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
cancelEdit();
|
||||||
|
} else if (e.key === 'Enter' && !creatingTask) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddTask();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTaskName(e.target.value);
|
||||||
|
if (error) setError(''); // Clear error when user starts typing
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="add-task-row-container" ref={containerRef}>
|
||||||
{isEdit ? (
|
{isEdit ? (
|
||||||
<Input
|
<div className="add-task-input-container">
|
||||||
className="h-12 w-full rounded-none"
|
<Input
|
||||||
style={{ borderColor: colors.skyBlue }}
|
className="add-task-input"
|
||||||
placeholder={t('addTaskInputPlaceholder')}
|
style={{
|
||||||
onChange={e => setTaskName(e.target.value)}
|
borderColor: error ? '#ff4d4f' : colors.skyBlue,
|
||||||
onBlur={handleAddTask}
|
paddingRight: creatingTask ? '32px' : '12px'
|
||||||
onPressEnter={handleAddTask}
|
}}
|
||||||
ref={taskInputRef}
|
placeholder={t('addTaskInputPlaceholder')}
|
||||||
/>
|
value={taskName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
ref={taskInputRef}
|
||||||
|
autoFocus
|
||||||
|
disabled={creatingTask}
|
||||||
|
/>
|
||||||
|
{creatingTask && (
|
||||||
|
<div className="add-task-loading">
|
||||||
|
<Spin
|
||||||
|
size="small"
|
||||||
|
indicator={<LoadingOutlined style={{ fontSize: 14 }} spin />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<div className="add-task-error">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<div
|
||||||
onFocus={() => setIsEdit(true)}
|
className="add-task-label"
|
||||||
className="w-[300px] border-none"
|
onClick={() => setIsEdit(true)}
|
||||||
value={parentTask ? t('addSubTaskText') : t('addTaskText')}
|
>
|
||||||
ref={taskInputRef}
|
<span className="add-task-text">
|
||||||
/>
|
{parentTask ? t('addSubTaskText') : t('addTaskText')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.add-task-row-container {
|
||||||
|
width: 100%;
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-task-input-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-task-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid ${colors.skyBlue};
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 12px;
|
||||||
|
margin: 2px 0;
|
||||||
|
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-task-input:disabled {
|
||||||
|
background-color: var(--task-bg-secondary, #f5f5f5);
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-task-loading {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-task-error {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-left: 2px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-task-label {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-task-label:hover {
|
||||||
|
background: var(--task-hover-bg, #fafafa);
|
||||||
|
border-color: var(--task-border-tertiary, #d9d9d9);
|
||||||
|
color: var(--task-text-secondary, #595959);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-task-text {
|
||||||
|
font-size: 14px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
.dark .add-task-label,
|
||||||
|
[data-theme="dark"] .add-task-label {
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .add-task-label:hover,
|
||||||
|
[data-theme="dark"] .add-task-label:hover {
|
||||||
|
background: var(--task-hover-bg, #2a2a2a);
|
||||||
|
border-color: var(--task-border-tertiary, #505050);
|
||||||
|
color: var(--task-text-secondary, #d9d9d9);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user