expand sub tasks

This commit is contained in:
chamiakJ
2025-07-03 01:31:05 +05:30
parent 3bef18901a
commit ecd4d29a38
435 changed files with 13150 additions and 11087 deletions

View File

@@ -138,7 +138,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
// Prevent duplicate API calls in React StrictMode
const hasInitialized = useRef(false);
// PERFORMANCE OPTIMIZATION: Frame rate monitoring and throttling
const frameTimeRef = useRef(performance.now());
const renderCountRef = useRef(0);
@@ -159,7 +159,9 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual);
const currentGrouping = useSelector(selectCurrentGroupingV3, shallowEqual);
// Use bulk action slice for selected tasks instead of selection slice
const selectedTaskIds = useSelector((state: RootState) => state.bulkActionReducer.selectedTaskIdsList);
const selectedTaskIds = useSelector(
(state: RootState) => state.bulkActionReducer.selectedTaskIdsList
);
const selectedTasks = useSelector((state: RootState) => state.bulkActionReducer.selectedTasks);
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
const error = useSelector((state: RootState) => state.taskManagement.error);
@@ -180,7 +182,9 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const tasksById = useMemo(() => {
const map: Record<string, Task> = {};
// Cache all tasks for full functionality - performance optimizations are handled at the virtualization level
tasks.forEach(task => { map[task.id] = task; });
tasks.forEach(task => {
map[task.id] = task;
});
return map;
}, [tasks]);
@@ -204,7 +208,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const now = performance.now();
const frameTime = now - frameTimeRef.current;
renderCountRef.current++;
// If frame time is consistently over 16.67ms (60fps), enable throttling
if (frameTime > 20 && renderCountRef.current > 10) {
setShouldThrottle(true);
@@ -212,10 +216,10 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
setShouldThrottle(false);
renderCountRef.current = 0; // Reset counter
}
frameTimeRef.current = now;
};
const interval = setInterval(monitorPerformance, 100);
return () => clearInterval(interval);
}, []);
@@ -224,7 +228,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
useEffect(() => {
if (projectId && !hasInitialized.current) {
hasInitialized.current = true;
// Fetch real tasks from V3 API (minimal processing needed)
dispatch(fetchTasksV3(projectId));
}
@@ -387,7 +391,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
// Emit socket event to backend
if (connected && socket && currentDragState.activeTask) {
const currentSession = JSON.parse(localStorage.getItem('session') || '{}');
const socketData = {
from_index: sourceIndex,
to_index: finalTargetIndex,
@@ -459,9 +463,22 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
[dispatch, selectedTasks, selectedTaskIds, tasks]
);
const handleToggleSubtasks = useCallback((taskId: string) => {
// Implementation for toggling subtasks
}, []);
const handleToggleSubtasks = useCallback(
(taskId: string) => {
const task = tasksById[taskId];
if (
task &&
!task.show_sub_tasks &&
task.sub_tasks_count &&
task.sub_tasks_count > 0 &&
(!task.sub_tasks || task.sub_tasks.length === 0)
) {
dispatch(fetchSubTasks({ taskId, projectId }));
}
dispatch(toggleTaskExpansion(taskId));
},
[dispatch, projectId, tasksById]
);
// Memoized DragOverlay content for better performance
const dragOverlayContent = useMemo(() => {
@@ -485,92 +502,101 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
dispatch(clearSelection());
}, [dispatch]);
const handleBulkStatusChange = useCallback(async (statusId: string) => {
if (!statusId || !projectId) return;
try {
// Find the status object
const status = statusList.find(s => s.id === statusId);
if (!status || !status.id) return;
const handleBulkStatusChange = useCallback(
async (statusId: string) => {
if (!statusId || !projectId) return;
try {
// Find the status object
const status = statusList.find(s => s.id === statusId);
if (!status || !status.id) return;
const body: IBulkTasksStatusChangeRequest = {
tasks: selectedTaskIds,
status_id: status.id,
};
// Check task dependencies first
for (const taskId of selectedTaskIds) {
const canContinue = await checkTaskDependencyStatus(taskId, status.id);
if (!canContinue) {
if (selectedTaskIds.length > 1) {
alertService.warning(
'Incomplete Dependencies!',
'Some tasks were not updated. Please ensure all dependent tasks are completed before proceeding.'
);
} else {
alertService.error(
'Task is not completed',
'Please complete the task dependencies before proceeding'
);
const body: IBulkTasksStatusChangeRequest = {
tasks: selectedTaskIds,
status_id: status.id,
};
// Check task dependencies first
for (const taskId of selectedTaskIds) {
const canContinue = await checkTaskDependencyStatus(taskId, status.id);
if (!canContinue) {
if (selectedTaskIds.length > 1) {
alertService.warning(
'Incomplete Dependencies!',
'Some tasks were not updated. Please ensure all dependent tasks are completed before proceeding.'
);
} else {
alertService.error(
'Task is not completed',
'Please complete the task dependencies before proceeding'
);
}
return;
}
return;
}
const res = await taskListBulkActionsApiService.changeStatus(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_status);
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
} catch (error) {
logger.error('Error changing status:', error);
}
},
[selectedTaskIds, statusList, projectId, trackMixpanelEvent, dispatch]
);
const res = await taskListBulkActionsApiService.changeStatus(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_status);
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
const handleBulkPriorityChange = useCallback(
async (priorityId: string) => {
if (!priorityId || !projectId) return;
try {
const priority = priorityList.find(p => p.id === priorityId);
if (!priority || !priority.id) return;
const body: IBulkTasksPriorityChangeRequest = {
tasks: selectedTaskIds,
priority_id: priority.id,
};
const res = await taskListBulkActionsApiService.changePriority(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_priority);
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
} catch (error) {
logger.error('Error changing priority:', error);
}
} catch (error) {
logger.error('Error changing status:', error);
}
}, [selectedTaskIds, statusList, projectId, trackMixpanelEvent, dispatch]);
},
[selectedTaskIds, priorityList, projectId, trackMixpanelEvent, dispatch]
);
const handleBulkPriorityChange = useCallback(async (priorityId: string) => {
if (!priorityId || !projectId) return;
try {
const priority = priorityList.find(p => p.id === priorityId);
if (!priority || !priority.id) return;
const handleBulkPhaseChange = useCallback(
async (phaseId: string) => {
if (!phaseId || !projectId) return;
try {
const phase = phaseList.find(p => p.id === phaseId);
if (!phase || !phase.id) return;
const body: IBulkTasksPriorityChangeRequest = {
tasks: selectedTaskIds,
priority_id: priority.id,
};
const res = await taskListBulkActionsApiService.changePriority(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_priority);
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
const body: IBulkTasksPhaseChangeRequest = {
tasks: selectedTaskIds,
phase_id: phase.id,
};
const res = await taskListBulkActionsApiService.changePhase(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_phase);
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
} catch (error) {
logger.error('Error changing phase:', error);
}
} catch (error) {
logger.error('Error changing priority:', error);
}
}, [selectedTaskIds, priorityList, projectId, trackMixpanelEvent, dispatch]);
const handleBulkPhaseChange = useCallback(async (phaseId: string) => {
if (!phaseId || !projectId) return;
try {
const phase = phaseList.find(p => p.id === phaseId);
if (!phase || !phase.id) return;
const body: IBulkTasksPhaseChangeRequest = {
tasks: selectedTaskIds,
phase_id: phase.id,
};
const res = await taskListBulkActionsApiService.changePhase(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_phase);
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
} catch (error) {
logger.error('Error changing phase:', error);
}
}, [selectedTaskIds, phaseList, projectId, trackMixpanelEvent, dispatch]);
},
[selectedTaskIds, phaseList, projectId, trackMixpanelEvent, dispatch]
);
const handleBulkAssignToMe = useCallback(async () => {
if (!projectId) return;
@@ -591,63 +617,67 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
}
}, [selectedTaskIds, projectId, trackMixpanelEvent, dispatch]);
const handleBulkAssignMembers = useCallback(async (memberIds: string[]) => {
if (!projectId || !members?.data) return;
try {
// Convert memberIds to member objects with proper type checking
const selectedMembers = members.data.filter(member =>
member.id && memberIds.includes(member.id)
);
const body = {
tasks: selectedTaskIds,
project_id: projectId,
members: selectedMembers.map(member => ({
id: member.id!,
name: member.name || '',
email: member.email || '',
avatar_url: member.avatar_url || '',
team_member_id: member.id!,
project_member_id: member.id!,
})),
};
const res = await taskListBulkActionsApiService.assignTasks(body);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_assign_members);
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
} catch (error) {
logger.error('Error assigning tasks:', error);
}
}, [selectedTaskIds, projectId, members, trackMixpanelEvent, dispatch]);
const handleBulkAssignMembers = useCallback(
async (memberIds: string[]) => {
if (!projectId || !members?.data) return;
try {
// Convert memberIds to member objects with proper type checking
const selectedMembers = members.data.filter(
member => member.id && memberIds.includes(member.id)
);
const handleBulkAddLabels = useCallback(async (labelIds: string[]) => {
if (!projectId) return;
try {
// Convert labelIds to label objects with proper type checking
const selectedLabels = labelsList.filter(label =>
label.id && labelIds.includes(label.id)
);
const body: IBulkTasksLabelsRequest = {
tasks: selectedTaskIds,
labels: selectedLabels,
text: null,
};
const res = await taskListBulkActionsApiService.assignLabels(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_update_labels);
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
dispatch(fetchLabels());
const body = {
tasks: selectedTaskIds,
project_id: projectId,
members: selectedMembers.map(member => ({
id: member.id!,
name: member.name || '',
email: member.email || '',
avatar_url: member.avatar_url || '',
team_member_id: member.id!,
project_member_id: member.id!,
})),
};
const res = await taskListBulkActionsApiService.assignTasks(body);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_assign_members);
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
} catch (error) {
logger.error('Error assigning tasks:', error);
}
} catch (error) {
logger.error('Error updating labels:', error);
}
}, [selectedTaskIds, projectId, labelsList, trackMixpanelEvent, dispatch]);
},
[selectedTaskIds, projectId, members, trackMixpanelEvent, dispatch]
);
const handleBulkAddLabels = useCallback(
async (labelIds: string[]) => {
if (!projectId) return;
try {
// Convert labelIds to label objects with proper type checking
const selectedLabels = labelsList.filter(label => label.id && labelIds.includes(label.id));
const body: IBulkTasksLabelsRequest = {
tasks: selectedTaskIds,
labels: selectedLabels,
text: null,
};
const res = await taskListBulkActionsApiService.assignLabels(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_update_labels);
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
dispatch(fetchLabels());
}
} catch (error) {
logger.error('Error updating labels:', error);
}
},
[selectedTaskIds, projectId, labelsList, trackMixpanelEvent, dispatch]
);
const handleBulkArchive = useCallback(async () => {
if (!projectId) return;
@@ -696,9 +726,12 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
// This would need to be implemented in the API service
}, [selectedTaskIds]);
const handleBulkSetDueDate = useCallback(async (date: string) => {
// This would need to be implemented in the API service
}, [selectedTaskIds]);
const handleBulkSetDueDate = useCallback(
async (date: string) => {
// This would need to be implemented in the API service
},
[selectedTaskIds]
);
// Cleanup effect
useEffect(() => {
@@ -757,18 +790,20 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
</div>
) : taskGroups.length === 0 ? (
<div className="empty-container">
<Empty
<Empty
description={
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '14px', fontWeight: 500, marginBottom: '4px' }}>
No task groups available
</div>
<div style={{ fontSize: '12px', color: 'var(--task-text-secondary, #595959)' }}>
<div
style={{ fontSize: '12px', color: 'var(--task-text-secondary, #595959)' }}
>
Create tasks to see them organized in groups
</div>
</div>
}
image={Empty.PRESENTED_IMAGE_SIMPLE}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</div>
) : (
@@ -778,7 +813,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const groupTasks = group.taskIds.length;
const baseHeight = 120; // Header + column headers + add task row
const taskRowsHeight = groupTasks * 40; // 40px per task row
// PERFORMANCE OPTIMIZATION: Enhanced virtualization threshold for better UX
const shouldVirtualizeGroup = groupTasks > 25; // Increased threshold for smoother experience
const minGroupHeight = shouldVirtualizeGroup ? 200 : 120; // Minimum height for virtualized groups
@@ -815,10 +850,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
</div>
</div>
<DragOverlay
adjustScale={false}
dropAnimation={null}
>
<DragOverlay adjustScale={false} dropAnimation={null}>
{dragOverlayContent}
</DragOverlay>
</DndContext>
@@ -1321,4 +1353,4 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
);
};
export default TaskListBoard;
export default TaskListBoard;