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.
183 lines
6.1 KiB
JavaScript
183 lines
6.1 KiB
JavaScript
const { Task, Tag, Project } = require('../../../models');
|
|
const taskRepository = require('../repository');
|
|
const permissionsService = require('../../../services/permissionsService');
|
|
const { logError } = require('../../../services/logService');
|
|
const { serializeTask } = require('../core/serializers');
|
|
const { parsePriority, parseStatus } = require('../core/parsers');
|
|
|
|
async function getSubtasks(parentTaskId, userId, timezone) {
|
|
const parent = await taskRepository.findById(parentTaskId);
|
|
if (!parent) {
|
|
return { error: 'Not found', subtasks: [] };
|
|
}
|
|
|
|
const pAccess = await permissionsService.getAccess(
|
|
userId,
|
|
'task',
|
|
parent.uid
|
|
);
|
|
if (pAccess === 'none') {
|
|
return { error: 'Forbidden', subtasks: null };
|
|
}
|
|
|
|
const subtasks = await taskRepository.findAll(
|
|
{ parent_task_id: parentTaskId },
|
|
{
|
|
include: [
|
|
{
|
|
model: Tag,
|
|
attributes: ['id', 'name', 'uid'],
|
|
through: { attributes: [] },
|
|
},
|
|
{
|
|
model: Project,
|
|
attributes: ['id', 'name', 'uid'],
|
|
required: false,
|
|
},
|
|
],
|
|
order: [
|
|
['order', 'ASC'],
|
|
['created_at', 'ASC'],
|
|
], // Order by order field, fallback to created_at
|
|
}
|
|
);
|
|
|
|
const serializedSubtasks = await Promise.all(
|
|
subtasks.map((subtask) => serializeTask(subtask, timezone))
|
|
);
|
|
|
|
return { error: null, subtasks: serializedSubtasks };
|
|
}
|
|
|
|
async function createSubtasks(parentTaskId, subtasks, userId) {
|
|
if (!subtasks || !Array.isArray(subtasks)) return;
|
|
|
|
// Get the highest order value for existing subtasks
|
|
const existingSubtasks = await taskRepository.findAll(
|
|
{ parent_task_id: parentTaskId },
|
|
{ attributes: ['order'], order: [['order', 'DESC']], limit: 1 }
|
|
);
|
|
const maxOrder = existingSubtasks[0]?.order ?? 0;
|
|
|
|
const subtasksData = subtasks
|
|
.filter((subtask) => subtask.name && subtask.name.trim())
|
|
.map((subtask, index) => ({
|
|
name: subtask.name.trim(),
|
|
parent_task_id: parentTaskId,
|
|
user_id: userId,
|
|
priority: parsePriority(subtask.priority) || Task.PRIORITY.LOW,
|
|
status: parseStatus(subtask.status),
|
|
completed_at:
|
|
subtask.status === 'done' || subtask.status === Task.STATUS.DONE
|
|
? subtask.completed_at
|
|
? new Date(subtask.completed_at)
|
|
: new Date()
|
|
: null,
|
|
recurrence_type: 'none',
|
|
completion_based: false,
|
|
order: maxOrder + index + 1, // Assign sequential order values
|
|
}));
|
|
|
|
await taskRepository.createMany(subtasksData);
|
|
}
|
|
|
|
async function updateSubtasks(taskId, subtasks, userId) {
|
|
if (!subtasks || !Array.isArray(subtasks)) return;
|
|
|
|
const existingSubtasks = await taskRepository.findChildren(taskId, userId);
|
|
|
|
const subtasksToKeep = subtasks.filter((s) => s.id && !s.isNew);
|
|
const subtasksToDelete = existingSubtasks.filter(
|
|
(existing) => !subtasksToKeep.find((keep) => keep.id === existing.id)
|
|
);
|
|
|
|
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({
|
|
where: {
|
|
id: subtasksToDelete.map((s) => s.id),
|
|
user_id: userId,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Update order for all subtasks to reflect their position in the array
|
|
const allSubtasksToUpdate = subtasks.filter((s) => s.id);
|
|
|
|
const subtasksToUpdate = subtasks.filter(
|
|
(s) =>
|
|
s.id &&
|
|
((s.isEdited && s.name && s.name.trim()) || s._statusChanged)
|
|
);
|
|
|
|
if (subtasksToUpdate.length > 0 || allSubtasksToUpdate.length > 0) {
|
|
const updatePromises = allSubtasksToUpdate.map((subtask, index) => {
|
|
const updateData = {
|
|
order: index + 1, // Update order based on position in array
|
|
};
|
|
|
|
if (subtasksToUpdate.includes(subtask)) {
|
|
if (subtask.isEdited && subtask.name && subtask.name.trim()) {
|
|
updateData.name = subtask.name.trim();
|
|
}
|
|
|
|
if (subtask._statusChanged || subtask.status !== undefined) {
|
|
updateData.status = parseStatus(subtask.status);
|
|
|
|
if (
|
|
updateData.status === Task.STATUS.DONE &&
|
|
!subtask.completed_at
|
|
) {
|
|
updateData.completed_at = new Date();
|
|
} else if (updateData.status !== Task.STATUS.DONE) {
|
|
updateData.completed_at = null;
|
|
}
|
|
}
|
|
|
|
if (subtask.priority !== undefined) {
|
|
updateData.priority =
|
|
parsePriority(subtask.priority) || Task.PRIORITY.LOW;
|
|
}
|
|
}
|
|
|
|
return taskRepository.bulkUpdate(updateData, {
|
|
where: { id: subtask.id, user_id: userId },
|
|
});
|
|
});
|
|
|
|
await Promise.all(updatePromises);
|
|
}
|
|
|
|
const newSubtasks = subtasks.filter(
|
|
(s) => s.isNew && s.name && s.name.trim()
|
|
);
|
|
|
|
if (newSubtasks.length > 0) {
|
|
await createSubtasks(taskId, newSubtasks, userId);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getSubtasks,
|
|
createSubtasks,
|
|
updateSubtasks,
|
|
};
|