perf(renderer): stabilize kanban board handlers
This commit is contained in:
parent
9376416b50
commit
ee5554e59d
1 changed files with 181 additions and 138 deletions
|
|
@ -2541,6 +2541,176 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
[selectReviewFile, taskMap]
|
[selectReviewFile, taskMap]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleRequestReview = useCallback(
|
||||||
|
(taskId: string) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await requestReview(teamName, taskId);
|
||||||
|
} catch {
|
||||||
|
// error via store
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
[requestReview, teamName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleApproveTask = useCallback(
|
||||||
|
(taskId: string) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await updateKanban(teamName, taskId, {
|
||||||
|
op: 'set_column',
|
||||||
|
column: 'approved',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// error via store
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
[teamName, updateKanban]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRequestChanges = useCallback((taskId: string) => {
|
||||||
|
setRequestChangesTaskId(taskId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMoveBackToDone = useCallback(
|
||||||
|
(taskId: string) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await updateKanban(teamName, taskId, { op: 'remove' });
|
||||||
|
await updateTaskStatus(teamName, taskId, 'completed');
|
||||||
|
} catch {
|
||||||
|
// error via store
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
[teamName, updateKanban, updateTaskStatus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStartTask = useCallback(
|
||||||
|
(taskId: string) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const result = await startTaskByUser(teamName, taskId);
|
||||||
|
if (data?.isAlive) {
|
||||||
|
const task = taskMapRef.current.get(taskId);
|
||||||
|
try {
|
||||||
|
if (result.notifiedOwner && task?.owner) {
|
||||||
|
await api.teams.processSend(
|
||||||
|
teamName,
|
||||||
|
`Task ${formatTaskDisplayLabel(task)} "${task.subject}" has started. Please begin working on it.`
|
||||||
|
);
|
||||||
|
} else if (!result.notifiedOwner) {
|
||||||
|
const desc = task?.description?.trim()
|
||||||
|
? `\nDescription: ${task.description.trim()}`
|
||||||
|
: '';
|
||||||
|
await api.teams.processSend(
|
||||||
|
teamName,
|
||||||
|
`Task #${deriveTaskDisplayId(taskId)} "${task?.subject ?? ''}" has been moved to IN PROGRESS but has no assignee.${desc}\nPlease assign it to an available team member, or take it yourself if everyone is busy.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// error via store
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
[data?.isAlive, startTaskByUser, teamName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCompleteTask = useCallback(
|
||||||
|
(taskId: string) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await updateTaskStatus(teamName, taskId, 'completed');
|
||||||
|
} catch {
|
||||||
|
// error via store
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
[teamName, updateTaskStatus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCancelTask = useCallback(
|
||||||
|
(taskId: string) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const task = taskMapRef.current.get(taskId);
|
||||||
|
await updateTaskStatus(teamName, taskId, 'pending');
|
||||||
|
|
||||||
|
// Notify assignee directly via inbox - they'll see it immediately
|
||||||
|
if (task?.owner) {
|
||||||
|
try {
|
||||||
|
await api.teams.sendMessage(teamName, {
|
||||||
|
member: task.owner,
|
||||||
|
text: `Task ${formatTaskDisplayLabel(task)} "${task.subject}" has been CANCELLED by the user and moved back to TODO. Stop working on it immediately.`,
|
||||||
|
summary: `Task ${formatTaskDisplayLabel(task)} cancelled`,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also notify team lead so they can reassign/coordinate
|
||||||
|
if (data?.isAlive) {
|
||||||
|
try {
|
||||||
|
const ownerSuffix = task?.owner ? ` ${task.owner} has been notified to stop.` : '';
|
||||||
|
await api.teams.processSend(
|
||||||
|
teamName,
|
||||||
|
`Task #${deriveTaskDisplayId(taskId)} "${task?.subject ?? ''}" has been cancelled and moved back to TODO.${ownerSuffix}`
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// error via store
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
[data?.isAlive, teamName, updateTaskStatus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleColumnOrderChange = useCallback(
|
||||||
|
(columnId: KanbanColumnId, orderedTaskIds: string[]) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await updateKanbanColumnOrder(teamName, columnId, orderedTaskIds);
|
||||||
|
} catch {
|
||||||
|
// error via store
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
[teamName, updateKanbanColumnOrder]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleScrollToTask = useCallback((taskId: string) => {
|
||||||
|
const el = document.querySelector(`[data-task-id="${taskId}"]`);
|
||||||
|
if (!el) return;
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
el.classList.remove('kanban-card-focus-pulse');
|
||||||
|
void (el as HTMLElement).offsetWidth;
|
||||||
|
el.classList.add('kanban-card-focus-pulse');
|
||||||
|
el.addEventListener('animationend', () => el.classList.remove('kanban-card-focus-pulse'), {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddTask = useCallback(
|
||||||
|
(startImmediately: boolean) => {
|
||||||
|
openCreateTaskDialog('', '', '', startImmediately);
|
||||||
|
},
|
||||||
|
[openCreateTaskDialog]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenTrash = useCallback(() => {
|
||||||
|
setTrashOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDeleteTeam = useCallback((): void => {
|
const handleDeleteTeam = useCallback((): void => {
|
||||||
setDeleteConfirmOpen(true);
|
setDeleteConfirmOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -3163,148 +3333,21 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
members={activeMembers}
|
members={activeMembers}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onRequestReview={(taskId) => {
|
onRequestReview={handleRequestReview}
|
||||||
void (async () => {
|
onApprove={handleApproveTask}
|
||||||
try {
|
onRequestChanges={handleRequestChanges}
|
||||||
await requestReview(teamName, taskId);
|
onMoveBackToDone={handleMoveBackToDone}
|
||||||
} catch {
|
onStartTask={handleStartTask}
|
||||||
// error via store
|
onCompleteTask={handleCompleteTask}
|
||||||
}
|
onCancelTask={handleCancelTask}
|
||||||
})();
|
onColumnOrderChange={handleColumnOrderChange}
|
||||||
}}
|
onScrollToTask={handleScrollToTask}
|
||||||
onApprove={(taskId) => {
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
await updateKanban(teamName, taskId, {
|
|
||||||
op: 'set_column',
|
|
||||||
column: 'approved',
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// error via store
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
onRequestChanges={(taskId) => {
|
|
||||||
setRequestChangesTaskId(taskId);
|
|
||||||
}}
|
|
||||||
onMoveBackToDone={(taskId) => {
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
await updateKanban(teamName, taskId, { op: 'remove' });
|
|
||||||
await updateTaskStatus(teamName, taskId, 'completed');
|
|
||||||
} catch {
|
|
||||||
// error via store
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
onStartTask={(taskId) => {
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const result = await startTaskByUser(teamName, taskId);
|
|
||||||
if (data?.isAlive) {
|
|
||||||
const task = data.tasks.find((t) => t.id === taskId);
|
|
||||||
try {
|
|
||||||
if (result.notifiedOwner && task?.owner) {
|
|
||||||
await api.teams.processSend(
|
|
||||||
teamName,
|
|
||||||
`Task ${formatTaskDisplayLabel(task)} "${task.subject}" has started. Please begin working on it.`
|
|
||||||
);
|
|
||||||
} else if (!result.notifiedOwner) {
|
|
||||||
const desc = task?.description?.trim()
|
|
||||||
? `\nDescription: ${task.description.trim()}`
|
|
||||||
: '';
|
|
||||||
await api.teams.processSend(
|
|
||||||
teamName,
|
|
||||||
`Task #${deriveTaskDisplayId(taskId)} "${task?.subject ?? ''}" has been moved to IN PROGRESS but has no assignee.${desc}\nPlease assign it to an available team member, or take it yourself if everyone is busy.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// error via store
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
onCompleteTask={(taskId) => {
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
await updateTaskStatus(teamName, taskId, 'completed');
|
|
||||||
} catch {
|
|
||||||
// error via store
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
onCancelTask={(taskId) => {
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const task = data?.tasks.find((t) => t.id === taskId);
|
|
||||||
await updateTaskStatus(teamName, taskId, 'pending');
|
|
||||||
|
|
||||||
// Notify assignee directly via inbox — they'll see it immediately
|
|
||||||
if (task?.owner) {
|
|
||||||
try {
|
|
||||||
await api.teams.sendMessage(teamName, {
|
|
||||||
member: task.owner,
|
|
||||||
text: `Task ${formatTaskDisplayLabel(task)} "${task.subject}" has been CANCELLED by the user and moved back to TODO. Stop working on it immediately.`,
|
|
||||||
summary: `Task ${formatTaskDisplayLabel(task)} cancelled`,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also notify team lead so they can reassign/coordinate
|
|
||||||
if (data?.isAlive) {
|
|
||||||
try {
|
|
||||||
const ownerSuffix = task?.owner
|
|
||||||
? ` ${task.owner} has been notified to stop.`
|
|
||||||
: '';
|
|
||||||
await api.teams.processSend(
|
|
||||||
teamName,
|
|
||||||
`Task #${deriveTaskDisplayId(taskId)} "${task?.subject ?? ''}" has been cancelled and moved back to TODO.${ownerSuffix}`
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// error via store
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
onColumnOrderChange={(columnId, orderedTaskIds) => {
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
await updateKanbanColumnOrder(teamName, columnId, orderedTaskIds);
|
|
||||||
} catch {
|
|
||||||
// error via store
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
onScrollToTask={(taskId) => {
|
|
||||||
const el = document.querySelector(`[data-task-id="${taskId}"]`);
|
|
||||||
if (el) {
|
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
el.classList.remove('kanban-card-focus-pulse');
|
|
||||||
void (el as HTMLElement).offsetWidth;
|
|
||||||
el.classList.add('kanban-card-focus-pulse');
|
|
||||||
el.addEventListener(
|
|
||||||
'animationend',
|
|
||||||
() => el.classList.remove('kanban-card-focus-pulse'),
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onTaskClick={openTaskDetailDialog}
|
onTaskClick={openTaskDetailDialog}
|
||||||
onViewChanges={handleViewChanges}
|
onViewChanges={handleViewChanges}
|
||||||
onAddTask={(startImmediately) =>
|
onAddTask={handleAddTask}
|
||||||
openCreateTaskDialog('', '', '', startImmediately)
|
|
||||||
}
|
|
||||||
onDeleteTask={handleDeleteTask}
|
onDeleteTask={handleDeleteTask}
|
||||||
deletedTaskCount={deletedTasks.length}
|
deletedTaskCount={deletedTasks.length}
|
||||||
onOpenTrash={() => setTrashOpen(true)}
|
onOpenTrash={handleOpenTrash}
|
||||||
/>
|
/>
|
||||||
</CollapsibleTeamSection>
|
</CollapsibleTeamSection>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue