perf(renderer): compare kanban task dependencies

This commit is contained in:
777genius 2026-05-31 05:59:02 +03:00
parent ad2f602cba
commit ad6a7b1998
2 changed files with 88 additions and 1 deletions

View file

@ -242,6 +242,55 @@ describe('KanbanTaskCard comment badge pulse', () => {
});
});
it('skips rerender when an unrelated taskMap entry changes', async () => {
const memberColorMap = new Map([['alice', 'blue']]);
const { root } = await renderTaskCard({
task: { ...baseTask, blockedBy: [], blocks: [], comments: [] },
taskMap: new Map([['other-task', { ...baseTask, id: 'other-task', subject: 'Other task' }]]),
memberColorMap,
});
unreadCommentCountMock.calls = 0;
await rerenderTaskCard(root, {
task: { ...baseTask, blockedBy: [], blocks: [], comments: [] },
taskMap: new Map([
['other-task', { ...baseTask, id: 'other-task', subject: 'Updated unrelated task' }],
]),
memberColorMap,
});
expect(unreadCommentCountMock.calls).toBe(0);
await act(async () => {
root.unmount();
await flushReact();
});
});
it('rerenders when a displayed dependency task changes', async () => {
const memberColorMap = new Map([['alice', 'blue']]);
const blockedTask = { ...baseTask, id: 'dep-1', displayId: 'dep1', subject: 'Dependency A' };
const { root } = await renderTaskCard({
task: { ...baseTask, blockedBy: ['dep-1'], blocks: [], comments: [] },
taskMap: new Map([['dep-1', blockedTask]]),
memberColorMap,
});
unreadCommentCountMock.calls = 0;
await rerenderTaskCard(root, {
task: { ...baseTask, blockedBy: ['dep-1'], blocks: [], comments: [] },
taskMap: new Map([['dep-1', { ...blockedTask, subject: 'Dependency B', status: 'done' }]]),
memberColorMap,
});
expect(unreadCommentCountMock.calls).toBeGreaterThan(0);
await act(async () => {
root.unmount();
await flushReact();
});
});
it('rerenders when a hidden task field changes so click handlers stay current', async () => {
const taskMap = new Map();
const memberColorMap = new Map([['alice', 'blue']]);

View file

@ -108,6 +108,44 @@ function areKanbanTaskStatesEqual(
);
}
function getTaskDependencyIds(task: TeamTaskWithKanban): string[] {
return [...(task.blockedBy ?? []), ...(task.blocks ?? [])].filter((id) => id.length > 0);
}
function getDependencyTaskSignature(task: TeamTask | undefined): string {
if (!task) return '';
const kanbanTask = task as Partial<TeamTaskWithKanban>;
return [
task.id,
task.displayId ?? '',
task.subject,
task.status,
task.reviewState ?? '',
kanbanTask.kanbanColumn ?? '',
].join('\u001f');
}
function areTaskMapDependenciesEqual(
prevTask: TeamTaskWithKanban,
nextTask: TeamTaskWithKanban,
prevTaskMap: Map<string, TeamTask>,
nextTaskMap: Map<string, TeamTask>
): boolean {
const dependencyIds = new Set([
...getTaskDependencyIds(prevTask),
...getTaskDependencyIds(nextTask),
]);
for (const taskId of dependencyIds) {
if (
getDependencyTaskSignature(prevTaskMap.get(taskId)) !==
getDependencyTaskSignature(nextTaskMap.get(taskId))
) {
return false;
}
}
return true;
}
function createCommentPulseState(
taskKey: string,
comments: readonly TaskComment[],
@ -682,7 +720,7 @@ export const KanbanTaskCard = memo(
areKanbanTaskStatesEqual(prev.kanbanTaskState, next.kanbanTaskState) &&
prev.hasReviewers === next.hasReviewers &&
prev.compact === next.compact &&
prev.taskMap === next.taskMap &&
areTaskMapDependenciesEqual(prev.task, next.task, prev.taskMap, next.taskMap) &&
prev.memberColorMap === next.memberColorMap &&
prev.hasLiveTaskLogs === next.hasLiveTaskLogs &&
prev.onRequestReview === next.onRequestReview &&