expand sub tasks
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user