refactor(tasks-controller): enhance getTasksV3 method for performance and clarity
- Improved logging for performance tracking in the getTasksV3 method. - Streamlined progress refresh logic and removed unnecessary calculations to optimize initial load times. - Unified query handling by aligning with the getList method for consistency. - Transformed response structure to maintain compatibility with V3 format while ensuring efficient data processing. - Added memoized selectors in the frontend for better performance and reduced re-renders.
This commit is contained in:
@@ -1041,61 +1041,62 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async getTasksV3(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async getTasksV3(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const isSubTasks = !!req.query.parent_task;
|
console.log(`[PERFORMANCE] getTasksV3 method called for project ${req.params.id}`);
|
||||||
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
|
||||||
const archived = req.query.archived === "true";
|
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Skip expensive progress calculation by default
|
// PERFORMANCE OPTIMIZATION: Skip expensive progress calculation by default
|
||||||
// Progress values are already calculated and stored in the database
|
// Progress values are already calculated and stored in the database
|
||||||
// Only refresh if explicitly requested via refresh_progress=true query parameter
|
// Only refresh if explicitly requested via refresh_progress=true query parameter
|
||||||
// This dramatically improves initial load performance (from ~2-5s to ~200-500ms)
|
if (req.query.refresh_progress === "true" && req.params.id) {
|
||||||
const shouldRefreshProgress = req.query.refresh_progress === "true";
|
console.log(`[PERFORMANCE] Starting progress refresh for project ${req.params.id} (getTasksV3)`);
|
||||||
|
|
||||||
if (shouldRefreshProgress && req.params.id) {
|
|
||||||
const progressStartTime = performance.now();
|
const progressStartTime = performance.now();
|
||||||
await this.refreshProjectTaskProgressValues(req.params.id);
|
await this.refreshProjectTaskProgressValues(req.params.id);
|
||||||
const progressEndTime = performance.now();
|
const progressEndTime = performance.now();
|
||||||
console.log(`[PERFORMANCE] Progress refresh completed in ${(progressEndTime - progressStartTime).toFixed(2)}ms`);
|
console.log(`[PERFORMANCE] Progress refresh completed in ${(progressEndTime - progressStartTime).toFixed(2)}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryStartTime = performance.now();
|
const isSubTasks = !!req.query.parent_task;
|
||||||
|
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
||||||
|
|
||||||
|
// Add customColumns flag to query params (same as getList)
|
||||||
|
req.query.customColumns = "true";
|
||||||
|
|
||||||
|
// Use the exact same database query as getList method
|
||||||
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
||||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task, req.user?.id] : [req.params.id || null, req.user?.id];
|
const params = isSubTasks ? [req.params.id || null, req.query.parent_task, req.user?.id] : [req.params.id || null, req.user?.id];
|
||||||
|
|
||||||
const result = await db.query(q, params);
|
const result = await db.query(q, params);
|
||||||
const tasks = [...result.rows];
|
const tasks = [...result.rows];
|
||||||
const queryEndTime = performance.now();
|
|
||||||
console.log(`[PERFORMANCE] Main query completed in ${(queryEndTime - queryStartTime).toFixed(2)}ms for ${tasks.length} tasks`);
|
|
||||||
|
|
||||||
// Get groups metadata dynamically from database
|
// Use the same groups query as getList method
|
||||||
const groupsStartTime = performance.now();
|
|
||||||
const groups = await this.getGroups(groupBy, req.params.id);
|
const groups = await this.getGroups(groupBy, req.params.id);
|
||||||
const groupsEndTime = performance.now();
|
const map = groups.reduce((g: { [x: string]: ITaskGroup }, group) => {
|
||||||
console.log(`[PERFORMANCE] Groups query completed in ${(groupsEndTime - groupsStartTime).toFixed(2)}ms`);
|
if (group.id)
|
||||||
|
g[group.id] = new TaskListGroup(group);
|
||||||
|
return g;
|
||||||
|
}, {});
|
||||||
|
|
||||||
// Create priority value to name mapping
|
// Use the same updateMapByGroup method as getList
|
||||||
|
await this.updateMapByGroup(tasks, groupBy, map);
|
||||||
|
|
||||||
|
// Calculate progress for groups (same as getList)
|
||||||
|
const updatedGroups = Object.keys(map).map(key => {
|
||||||
|
const group = map[key];
|
||||||
|
TasksControllerV2.updateTaskProgresses(group);
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
...group
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform to V3 response format while maintaining the same data processing
|
||||||
const priorityMap: Record<string, string> = {
|
const priorityMap: Record<string, string> = {
|
||||||
"0": "low",
|
"0": "low",
|
||||||
"1": "medium",
|
"1": "medium",
|
||||||
"2": "high"
|
"2": "high"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create status category mapping based on actual status names from database
|
// Transform all tasks to V3 format
|
||||||
const statusCategoryMap: Record<string, string> = {};
|
|
||||||
for (const group of groups) {
|
|
||||||
if (groupBy === GroupBy.STATUS && group.id) {
|
|
||||||
// Use the actual status name from database, convert to lowercase for consistency
|
|
||||||
statusCategoryMap[group.id] = group.name.toLowerCase().replace(/\s+/g, "_");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform tasks with all necessary data preprocessing
|
|
||||||
const transformStartTime = performance.now();
|
|
||||||
const transformedTasks = tasks.map((task, index) => {
|
const transformedTasks = tasks.map((task, index) => {
|
||||||
// Update task with calculated values (lightweight version)
|
|
||||||
TasksControllerV2.updateTaskViewModel(task);
|
|
||||||
task.index = index;
|
|
||||||
|
|
||||||
// Convert time values
|
// Convert time values
|
||||||
const convertTimeValue = (value: any): number => {
|
const convertTimeValue = (value: any): number => {
|
||||||
if (typeof value === "number") return value;
|
if (typeof value === "number") return value;
|
||||||
@@ -1118,11 +1119,8 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
task_key: task.task_key || "",
|
task_key: task.task_key || "",
|
||||||
title: task.name || "",
|
title: task.name || "",
|
||||||
description: task.description || "",
|
description: task.description || "",
|
||||||
// Use dynamic status mapping from database
|
status: task.status || "todo",
|
||||||
status: statusCategoryMap[task.status] || task.status,
|
|
||||||
// Pre-processed priority using mapping
|
|
||||||
priority: priorityMap[task.priority_value?.toString()] || "medium",
|
priority: priorityMap[task.priority_value?.toString()] || "medium",
|
||||||
// Use actual phase name from database
|
|
||||||
phase: task.phase_name || "Development",
|
phase: task.phase_name || "Development",
|
||||||
progress: typeof task.complete_ratio === "number" ? task.complete_ratio : 0,
|
progress: typeof task.complete_ratio === "number" ? task.complete_ratio : 0,
|
||||||
assignees: task.assignees?.map((a: any) => a.team_member_id) || [],
|
assignees: task.assignees?.map((a: any) => a.team_member_id) || [],
|
||||||
@@ -1146,7 +1144,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
logged: convertTimeValue(task.time_spent),
|
logged: convertTimeValue(task.time_spent),
|
||||||
},
|
},
|
||||||
customFields: {},
|
customFields: {},
|
||||||
custom_column_values: task.custom_column_values || {}, // Include custom column values
|
custom_column_values: task.custom_column_values || {},
|
||||||
createdAt: task.created_at || new Date().toISOString(),
|
createdAt: task.created_at || new Date().toISOString(),
|
||||||
updatedAt: task.updated_at || new Date().toISOString(),
|
updatedAt: task.updated_at || new Date().toISOString(),
|
||||||
order: typeof task.sort_order === "number" ? task.sort_order : 0,
|
order: typeof task.sort_order === "number" ? task.sort_order : 0,
|
||||||
@@ -1165,128 +1163,55 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
schedule_id: task.schedule_id || null,
|
schedule_id: task.schedule_id || null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const transformEndTime = performance.now();
|
|
||||||
|
|
||||||
// Create groups based on dynamic data from database
|
// Transform groups to V3 format while preserving the getList logic
|
||||||
const groupingStartTime = performance.now();
|
const responseGroups = updatedGroups.map(group => {
|
||||||
const groupedResponse: Record<string, any> = {};
|
// Create status category mapping for consistent group naming
|
||||||
|
let groupValue = group.name;
|
||||||
|
if (groupBy === GroupBy.STATUS) {
|
||||||
|
groupValue = group.name.toLowerCase().replace(/\s+/g, "_");
|
||||||
|
} else if (groupBy === GroupBy.PRIORITY) {
|
||||||
|
groupValue = group.name.toLowerCase();
|
||||||
|
} else if (groupBy === GroupBy.PHASE) {
|
||||||
|
groupValue = group.name.toLowerCase().replace(/\s+/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize groups from database data
|
// Transform tasks in this group to V3 format
|
||||||
groups.forEach(group => {
|
const groupTasks = group.tasks.map(task => {
|
||||||
const groupKey = groupBy === GroupBy.STATUS
|
const foundTask = transformedTasks.find(t => t.id === task.id);
|
||||||
? group.name.toLowerCase().replace(/\s+/g, "_")
|
return foundTask || task;
|
||||||
: groupBy === GroupBy.PRIORITY
|
});
|
||||||
? priorityMap[(group as any).value?.toString()] || group.name.toLowerCase()
|
|
||||||
: group.name.toLowerCase().replace(/\s+/g, "_");
|
|
||||||
|
|
||||||
groupedResponse[groupKey] = {
|
return {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
title: group.name,
|
title: group.name,
|
||||||
groupType: groupBy,
|
groupType: groupBy,
|
||||||
groupValue: groupKey,
|
groupValue,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
tasks: [],
|
tasks: groupTasks,
|
||||||
taskIds: [],
|
taskIds: groupTasks.map((task: any) => task.id),
|
||||||
color: group.color_code || this.getDefaultGroupColor(groupBy, groupKey),
|
color: group.color_code || this.getDefaultGroupColor(groupBy, groupValue),
|
||||||
// Include additional metadata from database
|
// Include additional metadata from database
|
||||||
category_id: group.category_id,
|
category_id: group.category_id,
|
||||||
start_date: group.start_date,
|
start_date: group.start_date,
|
||||||
end_date: group.end_date,
|
end_date: group.end_date,
|
||||||
sort_index: (group as any).sort_index,
|
sort_index: (group as any).sort_index,
|
||||||
|
// Include progress information from getList logic
|
||||||
|
todo_progress: group.todo_progress,
|
||||||
|
doing_progress: group.doing_progress,
|
||||||
|
done_progress: group.done_progress,
|
||||||
};
|
};
|
||||||
});
|
}).filter(group => group.tasks.length > 0 || req.query.include_empty === "true");
|
||||||
|
|
||||||
// Distribute tasks into groups
|
|
||||||
const unmappedTasks: any[] = [];
|
|
||||||
|
|
||||||
transformedTasks.forEach(task => {
|
|
||||||
let groupKey: string;
|
|
||||||
let taskAssigned = false;
|
|
||||||
|
|
||||||
if (groupBy === GroupBy.STATUS) {
|
|
||||||
groupKey = task.status;
|
|
||||||
if (groupedResponse[groupKey]) {
|
|
||||||
groupedResponse[groupKey].tasks.push(task);
|
|
||||||
groupedResponse[groupKey].taskIds.push(task.id);
|
|
||||||
taskAssigned = true;
|
|
||||||
}
|
|
||||||
} else if (groupBy === GroupBy.PRIORITY) {
|
|
||||||
groupKey = task.priority;
|
|
||||||
if (groupedResponse[groupKey]) {
|
|
||||||
groupedResponse[groupKey].tasks.push(task);
|
|
||||||
groupedResponse[groupKey].taskIds.push(task.id);
|
|
||||||
taskAssigned = true;
|
|
||||||
}
|
|
||||||
} else if (groupBy === GroupBy.PHASE) {
|
|
||||||
// For phase grouping, check if task has a valid phase
|
|
||||||
if (task.phase && task.phase.trim() !== "") {
|
|
||||||
groupKey = task.phase.toLowerCase().replace(/\s+/g, "_");
|
|
||||||
if (groupedResponse[groupKey]) {
|
|
||||||
groupedResponse[groupKey].tasks.push(task);
|
|
||||||
groupedResponse[groupKey].taskIds.push(task.id);
|
|
||||||
taskAssigned = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If task doesn't have a valid phase, add to unmapped
|
|
||||||
if (!taskAssigned) {
|
|
||||||
unmappedTasks.push(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create unmapped group if there are tasks without proper phase assignment
|
|
||||||
if (unmappedTasks.length > 0 && groupBy === GroupBy.PHASE) {
|
|
||||||
groupedResponse[UNMAPPED.toLowerCase()] = {
|
|
||||||
id: UNMAPPED,
|
|
||||||
title: UNMAPPED,
|
|
||||||
groupType: groupBy,
|
|
||||||
groupValue: UNMAPPED.toLowerCase(),
|
|
||||||
collapsed: false,
|
|
||||||
tasks: unmappedTasks,
|
|
||||||
taskIds: unmappedTasks.map(task => task.id),
|
|
||||||
color: "#fbc84c69", // Orange color with transparency
|
|
||||||
category_id: null,
|
|
||||||
start_date: null,
|
|
||||||
end_date: null,
|
|
||||||
sort_index: 999, // Put unmapped group at the end
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort tasks within each group by order
|
|
||||||
Object.values(groupedResponse).forEach((group: any) => {
|
|
||||||
group.tasks.sort((a: any, b: any) => a.order - b.order);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert to array format expected by frontend, maintaining database order
|
|
||||||
const responseGroups = groups
|
|
||||||
.map(group => {
|
|
||||||
const groupKey = groupBy === GroupBy.STATUS
|
|
||||||
? group.name.toLowerCase().replace(/\s+/g, "_")
|
|
||||||
: groupBy === GroupBy.PRIORITY
|
|
||||||
? priorityMap[(group as any).value?.toString()] || group.name.toLowerCase()
|
|
||||||
: group.name.toLowerCase().replace(/\s+/g, "_");
|
|
||||||
|
|
||||||
return groupedResponse[groupKey];
|
|
||||||
})
|
|
||||||
.filter(group => group && (group.tasks.length > 0 || req.query.include_empty === "true"));
|
|
||||||
|
|
||||||
// Add unmapped group to the end if it exists
|
|
||||||
if (groupedResponse[UNMAPPED.toLowerCase()]) {
|
|
||||||
responseGroups.push(groupedResponse[UNMAPPED.toLowerCase()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupingEndTime = performance.now();
|
|
||||||
|
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
const totalTime = endTime - startTime;
|
const totalTime = endTime - startTime;
|
||||||
|
console.log(`[PERFORMANCE] getTasksV3 method completed in ${totalTime.toFixed(2)}ms for project ${req.params.id} with ${transformedTasks.length} tasks`);
|
||||||
|
|
||||||
// Log warning if request is taking too long
|
// Log warning if this method is taking too long
|
||||||
if (totalTime > 1000) {
|
if (totalTime > 1000) {
|
||||||
console.warn(`[PERFORMANCE WARNING] Slow request detected: ${totalTime.toFixed(2)}ms for project ${req.params.id} with ${transformedTasks.length} tasks`);
|
console.warn(`[PERFORMANCE WARNING] getTasksV3 method taking ${totalTime.toFixed(2)}ms - Consider optimizing the query or data processing!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[PERFORMANCE] getTasksV3 completed in ${totalTime.toFixed(2)}ms for project ${req.params.id} with ${transformedTasks.length} tasks`);
|
|
||||||
|
|
||||||
return res.status(200).send(new ServerResponse(true, {
|
return res.status(200).send(new ServerResponse(true, {
|
||||||
groups: responseGroups,
|
groups: responseGroups,
|
||||||
allTasks: transformedTasks,
|
allTasks: transformedTasks,
|
||||||
|
|||||||
@@ -109,7 +109,14 @@ export const selectCurrentGrouping = (state: RootState) => state.grouping.curren
|
|||||||
export const selectCustomPhases = (state: RootState) => state.grouping.customPhases;
|
export const selectCustomPhases = (state: RootState) => state.grouping.customPhases;
|
||||||
export const selectGroupOrder = (state: RootState) => state.grouping.groupOrder;
|
export const selectGroupOrder = (state: RootState) => state.grouping.groupOrder;
|
||||||
export const selectGroupStates = (state: RootState) => state.grouping.groupStates;
|
export const selectGroupStates = (state: RootState) => state.grouping.groupStates;
|
||||||
export const selectCollapsedGroups = (state: RootState) => new Set(state.grouping.collapsedGroups);
|
export const selectCollapsedGroupsArray = (state: RootState) => state.grouping.collapsedGroups;
|
||||||
|
|
||||||
|
// Memoized selector to prevent unnecessary re-renders
|
||||||
|
export const selectCollapsedGroups = createSelector(
|
||||||
|
[selectCollapsedGroupsArray],
|
||||||
|
(collapsedGroupsArray) => new Set(collapsedGroupsArray)
|
||||||
|
);
|
||||||
|
|
||||||
export const selectIsGroupCollapsed = (state: RootState, groupId: string) =>
|
export const selectIsGroupCollapsed = (state: RootState, groupId: string) =>
|
||||||
state.grouping.collapsedGroups.includes(groupId);
|
state.grouping.collapsedGroups.includes(groupId);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
createAsyncThunk,
|
createAsyncThunk,
|
||||||
EntityState,
|
EntityState,
|
||||||
EntityId,
|
EntityId,
|
||||||
|
createSelector,
|
||||||
} from '@reduxjs/toolkit';
|
} from '@reduxjs/toolkit';
|
||||||
import { Task, TaskManagementState, TaskGroup, TaskGrouping } from '@/types/task-management.types';
|
import { Task, TaskManagementState, TaskGroup, TaskGrouping } from '@/types/task-management.types';
|
||||||
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
||||||
@@ -1142,7 +1143,12 @@ export const {
|
|||||||
|
|
||||||
// Export the selectors
|
// Export the selectors
|
||||||
export const selectAllTasks = (state: RootState) => state.taskManagement.entities;
|
export const selectAllTasks = (state: RootState) => state.taskManagement.entities;
|
||||||
export const selectAllTasksArray = (state: RootState) => Object.values(state.taskManagement.entities);
|
|
||||||
|
// Memoized selector to prevent unnecessary re-renders
|
||||||
|
export const selectAllTasksArray = createSelector(
|
||||||
|
[selectAllTasks],
|
||||||
|
(entities) => Object.values(entities)
|
||||||
|
);
|
||||||
export const selectTaskById = (state: RootState, taskId: string) => state.taskManagement.entities[taskId];
|
export const selectTaskById = (state: RootState, taskId: string) => state.taskManagement.entities[taskId];
|
||||||
export const selectTaskIds = (state: RootState) => state.taskManagement.ids;
|
export const selectTaskIds = (state: RootState) => state.taskManagement.ids;
|
||||||
export const selectGroups = (state: RootState) => state.taskManagement.groups;
|
export const selectGroups = (state: RootState) => state.taskManagement.groups;
|
||||||
@@ -1153,15 +1159,21 @@ export const selectSelectedPriorities = (state: RootState) => state.taskManageme
|
|||||||
export const selectSearch = (state: RootState) => state.taskManagement.search;
|
export const selectSearch = (state: RootState) => state.taskManagement.search;
|
||||||
export const selectSubtaskLoading = (state: RootState, taskId: string) => state.taskManagement.loadingSubtasks[taskId] || false;
|
export const selectSubtaskLoading = (state: RootState, taskId: string) => state.taskManagement.loadingSubtasks[taskId] || false;
|
||||||
|
|
||||||
// Memoized selectors
|
// Memoized selectors to prevent unnecessary re-renders
|
||||||
export const selectTasksByStatus = (state: RootState, status: string) =>
|
export const selectTasksByStatus = createSelector(
|
||||||
Object.values(state.taskManagement.entities).filter(task => task.status === status);
|
[selectAllTasksArray, (_state: RootState, status: string) => status],
|
||||||
|
(tasks, status) => tasks.filter(task => task.status === status)
|
||||||
|
);
|
||||||
|
|
||||||
export const selectTasksByPriority = (state: RootState, priority: string) =>
|
export const selectTasksByPriority = createSelector(
|
||||||
Object.values(state.taskManagement.entities).filter(task => task.priority === priority);
|
[selectAllTasksArray, (_state: RootState, priority: string) => priority],
|
||||||
|
(tasks, priority) => tasks.filter(task => task.priority === priority)
|
||||||
|
);
|
||||||
|
|
||||||
export const selectTasksByPhase = (state: RootState, phase: string) =>
|
export const selectTasksByPhase = createSelector(
|
||||||
Object.values(state.taskManagement.entities).filter(task => task.phase === phase);
|
[selectAllTasksArray, (_state: RootState, phase: string) => phase],
|
||||||
|
(tasks, phase) => tasks.filter(task => task.phase === phase)
|
||||||
|
);
|
||||||
|
|
||||||
// Add archived selector
|
// Add archived selector
|
||||||
export const selectArchived = (state: RootState) => state.taskManagement.archived;
|
export const selectArchived = (state: RootState) => state.taskManagement.archived;
|
||||||
|
|||||||
Reference in New Issue
Block a user