Merge pull request #165 from Worklenz/imp/task-list-performance-fixes

Imp/task list performance fixes
This commit is contained in:
Chamika J
2025-06-20 08:40:14 +05:30
committed by GitHub
18 changed files with 4082 additions and 32 deletions

View File

@@ -0,0 +1,78 @@
import React, { useEffect } from 'react';
import { Layout, Typography, Card, Space, Alert } from 'antd';
import { useDispatch } from 'react-redux';
import TaskListBoard from '@/components/task-management/TaskListBoard';
import { AppDispatch } from '@/app/store';
const { Header, Content } = Layout;
const { Title, Paragraph } = Typography;
const TaskManagementDemo: React.FC = () => {
const dispatch = useDispatch<AppDispatch>();
// Mock project ID for demo
const demoProjectId = 'demo-project-123';
useEffect(() => {
// Initialize demo data if needed
// You might want to populate some sample tasks here for demonstration
}, [dispatch]);
return (
<Layout className="min-h-screen bg-gray-50">
<Header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4">
<Title level={2} className="mb-0 text-gray-800">
Enhanced Task Management System
</Title>
</div>
</Header>
<Content className="max-w-7xl mx-auto px-4 py-6 w-full">
<Space direction="vertical" size="large" className="w-full">
{/* Introduction */}
<Card>
<Title level={3}>Task Management Features</Title>
<Paragraph>
This enhanced task management system provides a comprehensive interface for managing tasks
with the following key features:
</Paragraph>
<ul className="list-disc list-inside space-y-1 text-gray-700">
<li><strong>Dynamic Grouping:</strong> Group tasks by Status, Priority, or Phase</li>
<li><strong>Drag & Drop:</strong> Reorder tasks within groups or move between groups</li>
<li><strong>Multi-select:</strong> Select multiple tasks for bulk operations</li>
<li><strong>Bulk Actions:</strong> Change status, priority, assignees, or delete multiple tasks</li>
<li><strong>Subtasks:</strong> Expandable subtask support with progress tracking</li>
<li><strong>Real-time Updates:</strong> Live updates via WebSocket connections</li>
<li><strong>Rich Task Display:</strong> Progress bars, assignees, labels, due dates, and more</li>
</ul>
</Card>
{/* Usage Instructions */}
<Alert
message="Demo Instructions"
description={
<div>
<p><strong>Grouping:</strong> Use the dropdown to switch between Status, Priority, and Phase grouping.</p>
<p><strong>Drag & Drop:</strong> Click and drag tasks to reorder within groups or move between groups.</p>
<p><strong>Selection:</strong> Click checkboxes to select tasks, then use bulk actions in the blue bar.</p>
<p><strong>Subtasks:</strong> Click the +/- buttons next to task names to expand/collapse subtasks.</p>
</div>
}
type="info"
showIcon
className="mb-4"
/>
{/* Task List Board */}
<TaskListBoard
projectId={demoProjectId}
className="task-management-demo"
/>
</Space>
</Content>
</Layout>
);
};
export default TaskManagementDemo;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import TaskListBoard from '@/components/task-management/TaskListBoard';
const ProjectViewEnhancedTasks: React.FC = () => {
const { project } = useAppSelector(state => state.projectReducer);
if (!project?.id) {
return (
<div className="p-4 text-center text-gray-500">
Project not found
</div>
);
}
return (
<div className="project-view-enhanced-tasks">
<TaskListBoard projectId={project.id} />
</div>
);
};
export default ProjectViewEnhancedTasks;

View File

@@ -1,5 +1,7 @@
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 { colors } from '@/styles/colors';
import { useTranslation } from 'react-i18next';
@@ -31,7 +33,10 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
const [isEdit, setIsEdit] = useState<boolean>(false);
const [taskName, setTaskName] = useState<string>('');
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 containerRef = useRef<HTMLDivElement>(null);
const dispatch = useAppDispatch();
const currentSession = useAuthService().getCurrentSession();
@@ -43,13 +48,62 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
const customBorderColor = useMemo(() => themeMode === 'dark' && ' border-[#303030]', [themeMode]);
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 => {
if (!projectId || !currentSession) return null;
const body: ITaskCreateRequest = {
project_id: projectId,
id: '',
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,
task_key: '',
labels: [],
assignees: [],
names: [],
sub_tasks_count: 0,
manual_progress: false,
progress_value: null,
weight: null,
reporter_id: currentSession.id,
};
const groupBy = getCurrentGroup();
@@ -69,10 +123,14 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
const reset = (scroll = true) => {
setIsEdit(false);
setCreatingTask(false);
setTaskName('');
setError('');
if (taskCreationTimeout) {
clearTimeout(taskCreationTimeout);
setTaskCreationTimeout(null);
}
setIsEdit(true);
setTimeout(() => {
@@ -81,6 +139,16 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
}, DRAWER_ANIMATION_INTERVAL);
};
const cancelEdit = () => {
setIsEdit(false);
setTaskName('');
setError('');
if (taskCreationTimeout) {
clearTimeout(taskCreationTimeout);
setTaskCreationTimeout(null);
}
};
const onNewTaskReceived = (task: IAddNewTask) => {
if (!groupId) return;
@@ -106,49 +174,210 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
};
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 {
setCreatingTask(true);
setError('');
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));
// Handle success response
socket?.once(SocketEvents.QUICK_TASK.toString(), (task: IProjectTask) => {
clearTimeout(timeout);
setTaskCreationTimeout(null);
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) {
console.error('Error adding task:', error);
setCreatingTask(false);
setError('An unexpected error occurred. Please try again.');
}
};
const handleAddTask = () => {
setIsEdit(false);
if (creatingTask) return; // Prevent multiple submissions
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 (
<div>
<div className="add-task-row-container" ref={containerRef}>
{isEdit ? (
<Input
className="h-12 w-full rounded-none"
style={{ borderColor: colors.skyBlue }}
placeholder={t('addTaskInputPlaceholder')}
onChange={e => setTaskName(e.target.value)}
onBlur={handleAddTask}
onPressEnter={handleAddTask}
ref={taskInputRef}
/>
<div className="add-task-input-container">
<Input
className="add-task-input"
style={{
borderColor: error ? '#ff4d4f' : colors.skyBlue,
paddingRight: creatingTask ? '32px' : '12px'
}}
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
onFocus={() => setIsEdit(true)}
className="w-[300px] border-none"
value={parentTask ? t('addSubTaskText') : t('addTaskText')}
ref={taskInputRef}
/>
<div
className="add-task-label"
onClick={() => setIsEdit(true)}
>
<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>
);
};