Merge pull request #165 from Worklenz/imp/task-list-performance-fixes
Imp/task list performance fixes
This commit is contained in:
78
worklenz-frontend/src/pages/TaskManagementDemo.tsx
Normal file
78
worklenz-frontend/src/pages/TaskManagementDemo.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user