feat(task-management): enhance real-time updates and performance optimizations

- Implemented CSS styles to prevent flickering during socket updates, ensuring stable content visibility.
- Modified `TaskRow` component to improve loading behavior and prevent blank content during real-time updates.
- Enhanced socket handlers to update task management state immediately upon receiving real-time data, reducing the need for unnecessary refetches.
- Introduced logic to track loading state, ensuring consistent rendering and improved user experience during task updates.
This commit is contained in:
chamikaJ
2025-06-27 15:17:29 +05:30
parent 8b63c1cf9e
commit 30edda1762
3 changed files with 128 additions and 19 deletions

View File

@@ -36,6 +36,22 @@
will-change: transform; will-change: transform;
} }
/* REAL-TIME UPDATES: Prevent flickering during socket updates */
.task-row-optimized.stable-content {
contain: layout style;
will-change: transform;
/* Prevent content from disappearing during real-time updates */
min-height: 40px;
transition: none; /* Disable transitions during real-time updates */
}
.task-row-optimized.stable-content * {
contain: layout;
will-change: auto;
/* Ensure content stays visible */
opacity: 1 !important;
}
/* Optimize initial render performance */ /* Optimize initial render performance */
.task-row-optimized.initial-load * { .task-row-optimized.initial-load * {
contain: layout; contain: layout;

View File

@@ -179,9 +179,11 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
scrollableColumns, scrollableColumns,
}) => { }) => {
// PERFORMANCE OPTIMIZATION: Implement progressive loading // PERFORMANCE OPTIMIZATION: Implement progressive loading
const [isFullyLoaded, setIsFullyLoaded] = useState(false); // Immediately load first few tasks to prevent blank content for visible items
const [isFullyLoaded, setIsFullyLoaded] = useState((index !== undefined && index < 10) || false);
const [isIntersecting, setIsIntersecting] = useState(false); const [isIntersecting, setIsIntersecting] = useState(false);
const rowRef = useRef<HTMLDivElement>(null); const rowRef = useRef<HTMLDivElement>(null);
const hasBeenFullyLoadedOnce = useRef((index !== undefined && index < 10) || false); // Track if we've ever been fully loaded
// PERFORMANCE OPTIMIZATION: Only connect to socket after component is visible // PERFORMANCE OPTIMIZATION: Only connect to socket after component is visible
const { socket, connected } = useSocket(); const { socket, connected } = useSocket();
@@ -200,16 +202,18 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
// PERFORMANCE OPTIMIZATION: Intersection Observer for lazy loading // PERFORMANCE OPTIMIZATION: Intersection Observer for lazy loading
useEffect(() => { useEffect(() => {
if (!rowRef.current) return; // Skip intersection observer if already fully loaded
if (!rowRef.current || hasBeenFullyLoadedOnce.current) return;
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
const [entry] = entries; const [entry] = entries;
if (entry.isIntersecting && !isIntersecting) { if (entry.isIntersecting && !isIntersecting && !hasBeenFullyLoadedOnce.current) {
setIsIntersecting(true); setIsIntersecting(true);
// Delay full loading slightly to prioritize visible content // Delay full loading slightly to prioritize visible content
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
setIsFullyLoaded(true); setIsFullyLoaded(true);
hasBeenFullyLoadedOnce.current = true; // Mark as fully loaded once
}, 50); }, 50);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
@@ -227,10 +231,18 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
return () => { return () => {
observer.disconnect(); observer.disconnect();
}; };
}, [isIntersecting]); }, [isIntersecting, hasBeenFullyLoadedOnce.current]);
// PERFORMANCE OPTIMIZATION: Skip expensive operations during initial render // PERFORMANCE OPTIMIZATION: Skip expensive operations during initial render
const shouldRenderFull = isFullyLoaded || isDragOverlay || editTaskName; // Once fully loaded, always render full to prevent blanking during real-time updates
const shouldRenderFull = isFullyLoaded || hasBeenFullyLoadedOnce.current || isDragOverlay || editTaskName;
// REAL-TIME UPDATES: Ensure content stays loaded during socket updates
useEffect(() => {
if (shouldRenderFull && !hasBeenFullyLoadedOnce.current) {
hasBeenFullyLoadedOnce.current = true;
}
}, [shouldRenderFull]);
// Optimized drag and drop setup with better performance // Optimized drag and drop setup with better performance
const { const {
@@ -355,7 +367,8 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
if (!task.id) return; if (!task.id) return;
dispatch(setSelectedTaskId(task.id)); dispatch(setSelectedTaskId(task.id));
dispatch(setShowTaskDrawer(true)); dispatch(setShowTaskDrawer(true));
// Fetch task data // Fetch task data - this is necessary for detailed task drawer information
// that's not available in the list view (comments, attachments, etc.)
dispatch(fetchTask({ taskId: task.id, projectId })); dispatch(fetchTask({ taskId: task.id, projectId }));
}, [task.id, projectId, dispatch]); }, [task.id, projectId, dispatch]);
@@ -898,7 +911,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
rowRef.current = node; rowRef.current = node;
}} }}
style={dragStyle} style={dragStyle}
className={`${styleClasses.container} task-row-optimized ${shouldRenderFull ? 'fully-loaded' : 'initial-load'}`} className={`${styleClasses.container} task-row-optimized ${shouldRenderFull ? 'fully-loaded' : 'initial-load'} ${hasBeenFullyLoadedOnce.current ? 'stable-content' : ''}`}
data-task-id={task.id} data-task-id={task.id}
> >
<div className="flex h-10 max-h-10 overflow-visible relative"> <div className="flex h-10 max-h-10 overflow-visible relative">
@@ -1039,6 +1052,9 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
if (prevProps.isDragOverlay !== nextProps.isDragOverlay) return false; if (prevProps.isDragOverlay !== nextProps.isDragOverlay) return false;
if (prevProps.groupId !== nextProps.groupId) return false; if (prevProps.groupId !== nextProps.groupId) return false;
// REAL-TIME UPDATES: Always re-render if updatedAt changed (indicates real-time update)
if (prevProps.task.updatedAt !== nextProps.task.updatedAt) return false;
// Deep comparison for task properties that commonly change // Deep comparison for task properties that commonly change
const taskProps = ['title', 'progress', 'status', 'priority', 'description', 'startDate', 'dueDate']; const taskProps = ['title', 'progress', 'status', 'priority', 'description', 'startDate', 'dueDate'];
for (const prop of taskProps) { for (const prop of taskProps) {
@@ -1047,9 +1063,36 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
} }
} }
// Compare arrays by length first (fast path) // REAL-TIME UPDATES: Compare assignees and labels content (not just length)
if (prevProps.task.labels?.length !== nextProps.task.labels?.length) return false; if (prevProps.task.assignees?.length !== nextProps.task.assignees?.length) return false;
if (prevProps.task.assignees?.length > 0) {
// Deep compare assignee IDs
const prevAssigneeIds = prevProps.task.assignees.sort();
const nextAssigneeIds = nextProps.task.assignees.sort();
for (let i = 0; i < prevAssigneeIds.length; i++) {
if (prevAssigneeIds[i] !== nextAssigneeIds[i]) return false;
}
}
if (prevProps.task.assignee_names?.length !== nextProps.task.assignee_names?.length) return false; if (prevProps.task.assignee_names?.length !== nextProps.task.assignee_names?.length) return false;
if (prevProps.task.assignee_names && nextProps.task.assignee_names && prevProps.task.assignee_names.length > 0) {
// Deep compare assignee names
for (let i = 0; i < prevProps.task.assignee_names.length; i++) {
if (prevProps.task.assignee_names[i] !== nextProps.task.assignee_names[i]) return false;
}
}
if (prevProps.task.labels?.length !== nextProps.task.labels?.length) return false;
if (prevProps.task.labels?.length > 0) {
// Deep compare label IDs and names
for (let i = 0; i < prevProps.task.labels.length; i++) {
const prevLabel = prevProps.task.labels[i];
const nextLabel = nextProps.task.labels[i];
if (prevLabel.id !== nextLabel.id || prevLabel.name !== nextLabel.name || prevLabel.color !== nextLabel.color) {
return false;
}
}
}
// Compare column configurations // Compare column configurations
if (prevProps.fixedColumns?.length !== nextProps.fixedColumns?.length) return false; if (prevProps.fixedColumns?.length !== nextProps.fixedColumns?.length) return false;

View File

@@ -74,6 +74,19 @@ export const useTaskSocketHandlers = () => {
selected: true, selected: true,
})) || []; })) || [];
// REAL-TIME UPDATES: Update the task-management slice for immediate UI updates
if (data.id) {
dispatch(updateTask({
id: data.id,
changes: {
assignees: data.assignees?.map(a => a.team_member_id) || [],
assignee_names: data.names || [],
updatedAt: new Date().toISOString(),
}
}));
}
// Update the old task slice (for backward compatibility)
const groupId = taskGroups?.find((group: ITaskListGroup) => const groupId = taskGroups?.find((group: ITaskListGroup) =>
group.tasks?.some( group.tasks?.some(
(task: IProjectTask) => (task: IProjectTask) =>
@@ -98,9 +111,10 @@ export const useTaskSocketHandlers = () => {
} as IProjectTask) } as IProjectTask)
); );
if (currentSession?.team_id && !loadingAssignees) { // Remove unnecessary refetch - real-time updates handle this
dispatch(fetchTaskAssignees(currentSession.team_id)); // if (currentSession?.team_id && !loadingAssignees) {
} // dispatch(fetchTaskAssignees(currentSession.team_id));
// }
} }
}, },
[taskGroups, dispatch, currentSession?.team_id, loadingAssignees] [taskGroups, dispatch, currentSession?.team_id, loadingAssignees]
@@ -110,11 +124,31 @@ export const useTaskSocketHandlers = () => {
async (labels: ILabelsChangeResponse) => { async (labels: ILabelsChangeResponse) => {
if (!labels) return; if (!labels) return;
// REAL-TIME UPDATES: Update the task-management slice for immediate UI updates
if (labels.id) {
dispatch(updateTask({
id: labels.id,
changes: {
labels: labels.labels?.map(l => ({
id: l.id || '',
name: l.name || '',
color: l.color_code || '#1890ff',
end: l.end,
names: l.names
})) || [],
updatedAt: new Date().toISOString(),
}
}));
}
// Update the old task slice and other related slices (for backward compatibility)
// Only update existing data, don't refetch from server
await Promise.all([ await Promise.all([
dispatch(updateTaskLabel(labels)), dispatch(updateTaskLabel(labels)),
dispatch(setTaskLabels(labels)), dispatch(setTaskLabels(labels)),
dispatch(fetchLabels()), // Remove unnecessary refetches - real-time updates handle this
projectId && dispatch(fetchLabelsByProject(projectId)), // dispatch(fetchLabels()),
// projectId && dispatch(fetchLabelsByProject(projectId)),
]); ]);
}, },
[dispatch, projectId] [dispatch, projectId]
@@ -189,11 +223,12 @@ export const useTaskSocketHandlers = () => {
} }
})); }));
} else if (!currentGroup || !targetGroup) { } else if (!currentGroup || !targetGroup) {
// Fallback to refetch if groups not found (shouldn't happen normally) // Log the issue but don't refetch - real-time updates should handle this
console.log('🔄 Groups not found, refetching tasks...'); console.log('🔄 Groups not found, but avoiding refetch to prevent data thrashing');
if (projectId) { // Remove unnecessary refetch that causes data thrashing
dispatch(fetchTasksV3(projectId)); // if (projectId) {
} // dispatch(fetchTasksV3(projectId));
// }
} }
} }
}, },
@@ -611,13 +646,27 @@ export const useTaskSocketHandlers = () => {
[dispatch, taskGroups] [dispatch, taskGroups]
); );
// Handler for TASK_ASSIGNEES_CHANGE (fallback event with limited data)
const handleTaskAssigneesChange = useCallback(
(data: { assigneeIds: string[] }) => {
if (!data || !data.assigneeIds) return;
// This event only provides assignee IDs, so we update what we can
// The full assignee data will come from QUICK_ASSIGNEES_UPDATE
console.log('🔄 Task assignees change (limited data):', data);
},
[]
);
// Register socket event listeners // Register socket event listeners
useEffect(() => { useEffect(() => {
if (!socket) return; if (!socket) return;
const eventHandlers = [ const eventHandlers = [
{ event: SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), handler: handleAssigneesUpdate }, { event: SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), handler: handleAssigneesUpdate },
{ event: SocketEvents.TASK_ASSIGNEES_CHANGE.toString(), handler: handleTaskAssigneesChange },
{ event: SocketEvents.TASK_LABELS_CHANGE.toString(), handler: handleLabelsChange }, { event: SocketEvents.TASK_LABELS_CHANGE.toString(), handler: handleLabelsChange },
{ event: SocketEvents.CREATE_LABEL.toString(), handler: handleLabelsChange },
{ event: SocketEvents.TASK_STATUS_CHANGE.toString(), handler: handleTaskStatusChange }, { event: SocketEvents.TASK_STATUS_CHANGE.toString(), handler: handleTaskStatusChange },
{ event: SocketEvents.TASK_PROGRESS_UPDATED.toString(), handler: handleTaskProgress }, { event: SocketEvents.TASK_PROGRESS_UPDATED.toString(), handler: handleTaskProgress },
{ event: SocketEvents.TASK_PRIORITY_CHANGE.toString(), handler: handlePriorityChange }, { event: SocketEvents.TASK_PRIORITY_CHANGE.toString(), handler: handlePriorityChange },
@@ -646,6 +695,7 @@ export const useTaskSocketHandlers = () => {
}, [ }, [
socket, socket,
handleAssigneesUpdate, handleAssigneesUpdate,
handleTaskAssigneesChange,
handleLabelsChange, handleLabelsChange,
handleTaskStatusChange, handleTaskStatusChange,
handleTaskProgress, handleTaskProgress,