fix: prevent subtasks from disappearing when updating parent task (#984)

This commit addresses a critical bug where subtasks would disappear when
updating the parent task (e.g., assigning tags). The issue had multiple
potential causes:

1. Backend vulnerability: The updateSubtasks() function would delete all
   subtasks if an empty array was sent, treating it as "delete everything
   not in this list"

2. Frontend state management: After reloading a task, subtasks weren't
   being preserved if the backend response didn't include them

3. Unclear error messages: "Invalid parent task" errors didn't provide
   enough context for debugging

Changes:
- Added defensive logging in updateSubtasks() to warn when all subtasks
  are being deleted with an empty array
- Enhanced validateParentTaskAccess() error messages to provide detailed
  diagnostics (task not found vs. permission issues vs. wrong user)
- Updated handleTagsUpdate() in TaskDetails to explicitly preserve
  subtasks when reloading task after tag updates

This fix is defensive in nature and adds better observability for
diagnosing similar issues in the future.

Fixes issue reported by user where subtasks disappeared after assigning
tags to parent task, and "Invalid parent task" errors occurred when
trying to update the orphaned subtasks.
This commit is contained in:
Chris 2026-04-01 17:37:20 +03:00 committed by GitHub
parent 11c3fe5e43
commit e8c7eed226
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 37 additions and 3 deletions

View file

@ -92,6 +92,26 @@ async function updateSubtasks(taskId, subtasks, userId) {
); );
if (subtasksToDelete.length > 0) { if (subtasksToDelete.length > 0) {
if (
subtasksToDelete.length === existingSubtasks.length &&
subtasks.length === 0
) {
logError(
'WARNING: Attempting to delete all subtasks with empty array:',
{
taskId,
userId,
existingCount: existingSubtasks.length,
providedCount: subtasks.length,
subtasksToDelete: subtasksToDelete.map((s) => ({
id: s.id,
name: s.name,
parent_task_id: s.parent_task_id,
})),
}
);
}
await taskRepository.destroyMany({ await taskRepository.destroyMany({
where: { where: {
id: subtasksToDelete.map((s) => s.id), id: subtasksToDelete.map((s) => s.id),

View file

@ -36,7 +36,18 @@ async function validateParentTaskAccess(parentTaskId, userId) {
where: { id: parentTaskId, user_id: userId }, where: { id: parentTaskId, user_id: userId },
}); });
if (!parentTask) { if (!parentTask) {
throw new Error('Invalid parent task.'); const anyTask = await Task.findOne({
where: { id: parentTaskId },
});
if (anyTask) {
throw new Error(
`Invalid parent task. Parent task exists but belongs to a different user (parent user_id: ${anyTask.user_id}, current user_id: ${userId}).`
);
} else {
throw new Error(
`Invalid parent task. Parent task with id ${parentTaskId} not found.`
);
}
} }
const parentAccess = await permissionsService.getAccess( const parentAccess = await permissionsService.getAccess(
@ -49,7 +60,7 @@ async function validateParentTaskAccess(parentTaskId, userId) {
isOwner || parentAccess === 'rw' || parentAccess === 'admin'; isOwner || parentAccess === 'rw' || parentAccess === 'admin';
if (!canWrite) { if (!canWrite) {
throw new Error('Invalid parent task.'); throw new Error('Invalid parent task. Insufficient permissions.');
} }
return parentTaskId; return parentTaskId;

View file

@ -1062,7 +1062,10 @@ const TaskDetails: React.FC = () => {
); );
if (existingIndex >= 0) { if (existingIndex >= 0) {
const updatedTasks = [...tasksStore.tasks]; const updatedTasks = [...tasksStore.tasks];
updatedTasks[existingIndex] = updatedTask; updatedTasks[existingIndex] = {
...updatedTask,
subtasks: updatedTask.subtasks || task.subtasks || [],
};
tasksStore.setTasks(updatedTasks); tasksStore.setTasks(updatedTasks);
} }
} }