- Fixed logic in moveTaskToNewId function that was incorrectly treating task-to-task moves as subtask creation instead of task replacement - Updated moveTaskToNewId to properly handle replacing existing destination tasks instead of just placeholders - The move command now correctly replaces destination tasks and cleans up properly without leaving duplicates - Task Management: Moved task 93 (Google Vertex AI Provider) to position 88, Moved task 94 (Azure OpenAI Provider) to position 89, Updated task dependencies and regenerated task files, Cleaned up orphaned task files automatically - All important validations remain in place: Prevents moving tasks to themselves, Prevents moving parent tasks to their own subtasks, Prevents circular dependencies - Resolves the issue where moving tasks would leave both source and destination tasks in tasks.json and file system
659 lines
21 KiB
JavaScript
659 lines
21 KiB
JavaScript
import path from "path";
|
|
import { log, readJSON, writeJSON } from "../utils.js";
|
|
import { isTaskDependentOn } from "../task-manager.js";
|
|
import generateTaskFiles from "./generate-task-files.js";
|
|
|
|
/**
|
|
* Move one or more tasks/subtasks to new positions
|
|
* @param {string} tasksPath - Path to tasks.json file
|
|
* @param {string} sourceId - ID(s) of the task/subtask to move (e.g., '5' or '5.2' or '5,6,7')
|
|
* @param {string} destinationId - ID(s) of the destination (e.g., '7' or '7.3' or '7,8,9')
|
|
* @param {boolean} generateFiles - Whether to regenerate task files after moving
|
|
* @returns {Object} Result object with moved task details
|
|
*/
|
|
async function moveTask(
|
|
tasksPath,
|
|
sourceId,
|
|
destinationId,
|
|
generateFiles = true
|
|
) {
|
|
// Check if we have comma-separated IDs (multiple moves)
|
|
const sourceIds = sourceId.split(",").map((id) => id.trim());
|
|
const destinationIds = destinationId.split(",").map((id) => id.trim());
|
|
|
|
// If multiple IDs, validate they match in count
|
|
if (sourceIds.length > 1 || destinationIds.length > 1) {
|
|
if (sourceIds.length !== destinationIds.length) {
|
|
throw new Error(
|
|
`Number of source IDs (${sourceIds.length}) must match number of destination IDs (${destinationIds.length})`
|
|
);
|
|
}
|
|
|
|
// Perform multiple moves
|
|
return await moveMultipleTasks(
|
|
tasksPath,
|
|
sourceIds,
|
|
destinationIds,
|
|
generateFiles
|
|
);
|
|
}
|
|
|
|
// Single move - use existing logic
|
|
return await moveSingleTask(
|
|
tasksPath,
|
|
sourceId,
|
|
destinationId,
|
|
generateFiles
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Move multiple tasks/subtasks to new positions
|
|
* @param {string} tasksPath - Path to tasks.json file
|
|
* @param {string[]} sourceIds - Array of source IDs
|
|
* @param {string[]} destinationIds - Array of destination IDs
|
|
* @param {boolean} generateFiles - Whether to regenerate task files after moving
|
|
* @returns {Object} Result object with moved task details
|
|
*/
|
|
async function moveMultipleTasks(
|
|
tasksPath,
|
|
sourceIds,
|
|
destinationIds,
|
|
generateFiles = true
|
|
) {
|
|
try {
|
|
log(
|
|
"info",
|
|
`Moving multiple tasks/subtasks: ${sourceIds.join(", ")} to ${destinationIds.join(", ")}...`
|
|
);
|
|
|
|
const results = [];
|
|
|
|
// Perform moves one by one, but don't regenerate files until the end
|
|
for (let i = 0; i < sourceIds.length; i++) {
|
|
const result = await moveSingleTask(
|
|
tasksPath,
|
|
sourceIds[i],
|
|
destinationIds[i],
|
|
false
|
|
);
|
|
results.push(result);
|
|
}
|
|
|
|
// Generate task files once at the end if requested
|
|
if (generateFiles) {
|
|
log("info", "Regenerating task files...");
|
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
|
}
|
|
|
|
return {
|
|
message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
|
|
moves: results,
|
|
};
|
|
} catch (error) {
|
|
log("error", `Error moving multiple tasks/subtasks: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move a single task or subtask to a new position
|
|
* @param {string} tasksPath - Path to tasks.json file
|
|
* @param {string} sourceId - ID of the task/subtask to move (e.g., '5' or '5.2')
|
|
* @param {string} destinationId - ID of the destination (e.g., '7' or '7.3')
|
|
* @param {boolean} generateFiles - Whether to regenerate task files after moving
|
|
* @returns {Object} Result object with moved task details
|
|
*/
|
|
async function moveSingleTask(
|
|
tasksPath,
|
|
sourceId,
|
|
destinationId,
|
|
generateFiles = true
|
|
) {
|
|
try {
|
|
log("info", `Moving task/subtask ${sourceId} to ${destinationId}...`);
|
|
|
|
// Read the existing tasks
|
|
const data = readJSON(tasksPath);
|
|
if (!data || !data.tasks) {
|
|
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
|
|
}
|
|
|
|
// Parse source ID to determine if it's a task or subtask
|
|
const isSourceSubtask = sourceId.includes(".");
|
|
let sourceTask,
|
|
sourceParentTask,
|
|
sourceSubtask,
|
|
sourceTaskIndex,
|
|
sourceSubtaskIndex;
|
|
|
|
// Parse destination ID to determine the target
|
|
const isDestinationSubtask = destinationId.includes(".");
|
|
let destTask, destParentTask, destSubtask, destTaskIndex, destSubtaskIndex;
|
|
|
|
// Validate source exists
|
|
if (isSourceSubtask) {
|
|
// Source is a subtask
|
|
const [parentIdStr, subtaskIdStr] = sourceId.split(".");
|
|
const parentIdNum = parseInt(parentIdStr, 10);
|
|
const subtaskIdNum = parseInt(subtaskIdStr, 10);
|
|
|
|
sourceParentTask = data.tasks.find((t) => t.id === parentIdNum);
|
|
if (!sourceParentTask) {
|
|
throw new Error(`Source parent task with ID ${parentIdNum} not found`);
|
|
}
|
|
|
|
if (
|
|
!sourceParentTask.subtasks ||
|
|
sourceParentTask.subtasks.length === 0
|
|
) {
|
|
throw new Error(`Source parent task ${parentIdNum} has no subtasks`);
|
|
}
|
|
|
|
sourceSubtaskIndex = sourceParentTask.subtasks.findIndex(
|
|
(st) => st.id === subtaskIdNum
|
|
);
|
|
if (sourceSubtaskIndex === -1) {
|
|
throw new Error(`Source subtask ${sourceId} not found`);
|
|
}
|
|
|
|
sourceSubtask = { ...sourceParentTask.subtasks[sourceSubtaskIndex] };
|
|
} else {
|
|
// Source is a task
|
|
const sourceIdNum = parseInt(sourceId, 10);
|
|
sourceTaskIndex = data.tasks.findIndex((t) => t.id === sourceIdNum);
|
|
if (sourceTaskIndex === -1) {
|
|
throw new Error(`Source task with ID ${sourceIdNum} not found`);
|
|
}
|
|
|
|
sourceTask = { ...data.tasks[sourceTaskIndex] };
|
|
}
|
|
|
|
// Validate destination exists
|
|
if (isDestinationSubtask) {
|
|
// Destination is a subtask (target will be the parent of this subtask)
|
|
const [parentIdStr, subtaskIdStr] = destinationId.split(".");
|
|
const parentIdNum = parseInt(parentIdStr, 10);
|
|
const subtaskIdNum = parseInt(subtaskIdStr, 10);
|
|
|
|
destParentTask = data.tasks.find((t) => t.id === parentIdNum);
|
|
if (!destParentTask) {
|
|
throw new Error(
|
|
`Destination parent task with ID ${parentIdNum} not found`
|
|
);
|
|
}
|
|
|
|
// Initialize subtasks array if it doesn't exist
|
|
if (!destParentTask.subtasks) {
|
|
destParentTask.subtasks = [];
|
|
}
|
|
|
|
// If there are existing subtasks, try to find the specific destination subtask
|
|
if (destParentTask.subtasks.length > 0) {
|
|
destSubtaskIndex = destParentTask.subtasks.findIndex(
|
|
(st) => st.id === subtaskIdNum
|
|
);
|
|
if (destSubtaskIndex !== -1) {
|
|
destSubtask = destParentTask.subtasks[destSubtaskIndex];
|
|
} else {
|
|
// Subtask doesn't exist, we'll insert at the end
|
|
destSubtaskIndex = destParentTask.subtasks.length - 1;
|
|
}
|
|
} else {
|
|
// No existing subtasks, this will be the first one
|
|
destSubtaskIndex = -1; // Will insert at position 0
|
|
}
|
|
} else {
|
|
// Destination is a task
|
|
const destIdNum = parseInt(destinationId, 10);
|
|
destTaskIndex = data.tasks.findIndex((t) => t.id === destIdNum);
|
|
|
|
if (destTaskIndex === -1) {
|
|
// Create placeholder for destination if it doesn't exist
|
|
log("info", `Creating placeholder for destination task ${destIdNum}`);
|
|
const newTask = {
|
|
id: destIdNum,
|
|
title: `Task ${destIdNum}`,
|
|
description: "",
|
|
status: "pending",
|
|
priority: "medium",
|
|
details: "",
|
|
testStrategy: "",
|
|
};
|
|
|
|
// Find correct position to insert the new task
|
|
let insertIndex = 0;
|
|
while (
|
|
insertIndex < data.tasks.length &&
|
|
data.tasks[insertIndex].id < destIdNum
|
|
) {
|
|
insertIndex++;
|
|
}
|
|
|
|
// Insert the new task at the appropriate position
|
|
data.tasks.splice(insertIndex, 0, newTask);
|
|
destTaskIndex = insertIndex;
|
|
destTask = data.tasks[destTaskIndex];
|
|
} else {
|
|
destTask = data.tasks[destTaskIndex];
|
|
}
|
|
}
|
|
|
|
// Validate that we aren't trying to move a task to itself
|
|
if (sourceId === destinationId) {
|
|
throw new Error("Cannot move a task/subtask to itself");
|
|
}
|
|
|
|
// Prevent moving a parent to its own subtask
|
|
if (!isSourceSubtask && isDestinationSubtask) {
|
|
const destParentId = parseInt(destinationId.split(".")[0], 10);
|
|
if (parseInt(sourceId, 10) === destParentId) {
|
|
throw new Error("Cannot move a parent task to one of its own subtasks");
|
|
}
|
|
}
|
|
|
|
// Check for circular dependency when moving tasks
|
|
if (!isSourceSubtask && !isDestinationSubtask) {
|
|
const sourceIdNum = parseInt(sourceId, 10);
|
|
const destIdNum = parseInt(destinationId, 10);
|
|
|
|
// Check if destination is dependent on source
|
|
if (isTaskDependentOn(data.tasks, destTask, sourceIdNum)) {
|
|
throw new Error(
|
|
`Cannot move task ${sourceId} to task ${destinationId} as it would create a circular dependency`
|
|
);
|
|
}
|
|
}
|
|
|
|
let movedTask;
|
|
|
|
// Handle different move scenarios
|
|
if (!isSourceSubtask && !isDestinationSubtask) {
|
|
// Case: Moving task to task position
|
|
// Always treat this as a task replacement/move to new ID
|
|
// The destination task will be replaced by the source task
|
|
movedTask = moveTaskToNewId(
|
|
data,
|
|
sourceTask,
|
|
sourceTaskIndex,
|
|
destTask,
|
|
destTaskIndex
|
|
);
|
|
} else if (!isSourceSubtask && isDestinationSubtask) {
|
|
// Case 2: Move standalone task to become a subtask at a specific position
|
|
movedTask = moveTaskToSubtaskPosition(
|
|
data,
|
|
sourceTask,
|
|
sourceTaskIndex,
|
|
destParentTask,
|
|
destSubtaskIndex
|
|
);
|
|
} else if (isSourceSubtask && !isDestinationSubtask) {
|
|
// Case 3: Move subtask to become a standalone task
|
|
movedTask = moveSubtaskToTask(
|
|
data,
|
|
sourceSubtask,
|
|
sourceParentTask,
|
|
sourceSubtaskIndex,
|
|
destTask
|
|
);
|
|
} else if (isSourceSubtask && isDestinationSubtask) {
|
|
// Case 4: Move subtask to another parent or position
|
|
// First check if it's the same parent
|
|
const sourceParentId = parseInt(sourceId.split(".")[0], 10);
|
|
const destParentId = parseInt(destinationId.split(".")[0], 10);
|
|
|
|
if (sourceParentId === destParentId) {
|
|
// Case 4a: Move subtask within the same parent (reordering)
|
|
movedTask = reorderSubtask(
|
|
sourceParentTask,
|
|
sourceSubtaskIndex,
|
|
destSubtaskIndex
|
|
);
|
|
} else {
|
|
// Case 4b: Move subtask to a different parent
|
|
movedTask = moveSubtaskToAnotherParent(
|
|
sourceSubtask,
|
|
sourceParentTask,
|
|
sourceSubtaskIndex,
|
|
destParentTask,
|
|
destSubtaskIndex
|
|
);
|
|
}
|
|
}
|
|
|
|
// Write the updated tasks back to the file
|
|
writeJSON(tasksPath, data);
|
|
|
|
// Generate task files if requested
|
|
if (generateFiles) {
|
|
log("info", "Regenerating task files...");
|
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
|
}
|
|
|
|
return movedTask;
|
|
} catch (error) {
|
|
log("error", `Error moving task/subtask: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move a standalone task to become a subtask of another task
|
|
* @param {Object} data - Tasks data object
|
|
* @param {Object} sourceTask - Source task to move
|
|
* @param {number} sourceTaskIndex - Index of source task in data.tasks
|
|
* @param {Object} destTask - Destination task
|
|
* @returns {Object} Moved task object
|
|
*/
|
|
function moveTaskToTask(data, sourceTask, sourceTaskIndex, destTask) {
|
|
// Initialize subtasks array if it doesn't exist
|
|
if (!destTask.subtasks) {
|
|
destTask.subtasks = [];
|
|
}
|
|
|
|
// Find the highest subtask ID to determine the next ID
|
|
const highestSubtaskId =
|
|
destTask.subtasks.length > 0
|
|
? Math.max(...destTask.subtasks.map((st) => st.id))
|
|
: 0;
|
|
const newSubtaskId = highestSubtaskId + 1;
|
|
|
|
// Create the new subtask from the source task
|
|
const newSubtask = {
|
|
...sourceTask,
|
|
id: newSubtaskId,
|
|
parentTaskId: destTask.id,
|
|
};
|
|
|
|
// Add to destination's subtasks
|
|
destTask.subtasks.push(newSubtask);
|
|
|
|
// Remove the original task from the tasks array
|
|
data.tasks.splice(sourceTaskIndex, 1);
|
|
|
|
log(
|
|
"info",
|
|
`Moved task ${sourceTask.id} to become subtask ${destTask.id}.${newSubtaskId}`
|
|
);
|
|
|
|
return newSubtask;
|
|
}
|
|
|
|
/**
|
|
* Move a standalone task to become a subtask at a specific position
|
|
* @param {Object} data - Tasks data object
|
|
* @param {Object} sourceTask - Source task to move
|
|
* @param {number} sourceTaskIndex - Index of source task in data.tasks
|
|
* @param {Object} destParentTask - Destination parent task
|
|
* @param {number} destSubtaskIndex - Index of the subtask before which to insert
|
|
* @returns {Object} Moved task object
|
|
*/
|
|
function moveTaskToSubtaskPosition(
|
|
data,
|
|
sourceTask,
|
|
sourceTaskIndex,
|
|
destParentTask,
|
|
destSubtaskIndex
|
|
) {
|
|
// Initialize subtasks array if it doesn't exist
|
|
if (!destParentTask.subtasks) {
|
|
destParentTask.subtasks = [];
|
|
}
|
|
|
|
// Find the highest subtask ID to determine the next ID
|
|
const highestSubtaskId =
|
|
destParentTask.subtasks.length > 0
|
|
? Math.max(...destParentTask.subtasks.map((st) => st.id))
|
|
: 0;
|
|
const newSubtaskId = highestSubtaskId + 1;
|
|
|
|
// Create the new subtask from the source task
|
|
const newSubtask = {
|
|
...sourceTask,
|
|
id: newSubtaskId,
|
|
parentTaskId: destParentTask.id,
|
|
};
|
|
|
|
// Insert at specific position
|
|
// If destSubtaskIndex is -1, insert at the beginning (position 0)
|
|
// Otherwise, insert after the specified subtask
|
|
const insertPosition = destSubtaskIndex === -1 ? 0 : destSubtaskIndex + 1;
|
|
destParentTask.subtasks.splice(insertPosition, 0, newSubtask);
|
|
|
|
// Remove the original task from the tasks array
|
|
data.tasks.splice(sourceTaskIndex, 1);
|
|
|
|
log(
|
|
"info",
|
|
`Moved task ${sourceTask.id} to become subtask ${destParentTask.id}.${newSubtaskId}`
|
|
);
|
|
|
|
return newSubtask;
|
|
}
|
|
|
|
/**
|
|
* Move a subtask to become a standalone task
|
|
* @param {Object} data - Tasks data object
|
|
* @param {Object} sourceSubtask - Source subtask to move
|
|
* @param {Object} sourceParentTask - Parent task of the source subtask
|
|
* @param {number} sourceSubtaskIndex - Index of source subtask in parent's subtasks
|
|
* @param {Object} destTask - Destination task (for position reference)
|
|
* @returns {Object} Moved task object
|
|
*/
|
|
function moveSubtaskToTask(
|
|
data,
|
|
sourceSubtask,
|
|
sourceParentTask,
|
|
sourceSubtaskIndex,
|
|
destTask
|
|
) {
|
|
// Find the highest task ID to determine the next ID
|
|
const highestId = Math.max(...data.tasks.map((t) => t.id));
|
|
const newTaskId = highestId + 1;
|
|
|
|
// Create the new task from the subtask
|
|
const newTask = {
|
|
...sourceSubtask,
|
|
id: newTaskId,
|
|
priority: sourceParentTask.priority || "medium", // Inherit priority from parent
|
|
};
|
|
delete newTask.parentTaskId;
|
|
|
|
// Add the parent task as a dependency if not already present
|
|
if (!newTask.dependencies) {
|
|
newTask.dependencies = [];
|
|
}
|
|
if (!newTask.dependencies.includes(sourceParentTask.id)) {
|
|
newTask.dependencies.push(sourceParentTask.id);
|
|
}
|
|
|
|
// Find the destination index to insert the new task
|
|
const destTaskIndex = data.tasks.findIndex((t) => t.id === destTask.id);
|
|
|
|
// Insert the new task after the destination task
|
|
data.tasks.splice(destTaskIndex + 1, 0, newTask);
|
|
|
|
// Remove the subtask from the parent
|
|
sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
|
|
|
|
// If parent has no more subtasks, remove the subtasks array
|
|
if (sourceParentTask.subtasks.length === 0) {
|
|
delete sourceParentTask.subtasks;
|
|
}
|
|
|
|
log(
|
|
"info",
|
|
`Moved subtask ${sourceParentTask.id}.${sourceSubtask.id} to become task ${newTaskId}`
|
|
);
|
|
|
|
return newTask;
|
|
}
|
|
|
|
/**
|
|
* Reorder a subtask within the same parent
|
|
* @param {Object} parentTask - Parent task containing the subtask
|
|
* @param {number} sourceIndex - Current index of the subtask
|
|
* @param {number} destIndex - Destination index for the subtask
|
|
* @returns {Object} Moved subtask object
|
|
*/
|
|
function reorderSubtask(parentTask, sourceIndex, destIndex) {
|
|
// Get the subtask to move
|
|
const subtask = parentTask.subtasks[sourceIndex];
|
|
|
|
// Remove the subtask from its current position
|
|
parentTask.subtasks.splice(sourceIndex, 1);
|
|
|
|
// Insert the subtask at the new position
|
|
// If destIndex was after sourceIndex, it's now one less because we removed an item
|
|
const adjustedDestIndex = sourceIndex < destIndex ? destIndex - 1 : destIndex;
|
|
parentTask.subtasks.splice(adjustedDestIndex, 0, subtask);
|
|
|
|
log(
|
|
"info",
|
|
`Reordered subtask ${parentTask.id}.${subtask.id} within parent task ${parentTask.id}`
|
|
);
|
|
|
|
return subtask;
|
|
}
|
|
|
|
/**
|
|
* Move a subtask to a different parent
|
|
* @param {Object} sourceSubtask - Source subtask to move
|
|
* @param {Object} sourceParentTask - Parent task of the source subtask
|
|
* @param {number} sourceSubtaskIndex - Index of source subtask in parent's subtasks
|
|
* @param {Object} destParentTask - Destination parent task
|
|
* @param {number} destSubtaskIndex - Index of the subtask before which to insert
|
|
* @returns {Object} Moved subtask object
|
|
*/
|
|
function moveSubtaskToAnotherParent(
|
|
sourceSubtask,
|
|
sourceParentTask,
|
|
sourceSubtaskIndex,
|
|
destParentTask,
|
|
destSubtaskIndex
|
|
) {
|
|
// Find the highest subtask ID in the destination parent
|
|
const highestSubtaskId =
|
|
destParentTask.subtasks.length > 0
|
|
? Math.max(...destParentTask.subtasks.map((st) => st.id))
|
|
: 0;
|
|
const newSubtaskId = highestSubtaskId + 1;
|
|
|
|
// Create the new subtask with updated parent reference
|
|
const newSubtask = {
|
|
...sourceSubtask,
|
|
id: newSubtaskId,
|
|
parentTaskId: destParentTask.id,
|
|
};
|
|
|
|
// If the subtask depends on its original parent, keep that dependency
|
|
if (!newSubtask.dependencies) {
|
|
newSubtask.dependencies = [];
|
|
}
|
|
if (!newSubtask.dependencies.includes(sourceParentTask.id)) {
|
|
newSubtask.dependencies.push(sourceParentTask.id);
|
|
}
|
|
|
|
// Insert at the destination position
|
|
// If destSubtaskIndex is -1, insert at the beginning (position 0)
|
|
// Otherwise, insert after the specified subtask
|
|
const insertPosition = destSubtaskIndex === -1 ? 0 : destSubtaskIndex + 1;
|
|
destParentTask.subtasks.splice(insertPosition, 0, newSubtask);
|
|
|
|
// Remove the subtask from the original parent
|
|
sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
|
|
|
|
// If original parent has no more subtasks, remove the subtasks array
|
|
if (sourceParentTask.subtasks.length === 0) {
|
|
delete sourceParentTask.subtasks;
|
|
}
|
|
|
|
log(
|
|
"info",
|
|
`Moved subtask ${sourceParentTask.id}.${sourceSubtask.id} to become subtask ${destParentTask.id}.${newSubtaskId}`
|
|
);
|
|
|
|
return newSubtask;
|
|
}
|
|
|
|
/**
|
|
* Move a standalone task to a new ID position
|
|
* @param {Object} data - Tasks data object
|
|
* @param {Object} sourceTask - Source task to move
|
|
* @param {number} sourceTaskIndex - Index of source task in data.tasks
|
|
* @param {Object} destTask - Destination task (will be replaced)
|
|
* @param {number} destTaskIndex - Index of destination task in data.tasks
|
|
* @returns {Object} Moved task object
|
|
*/
|
|
function moveTaskToNewId(
|
|
data,
|
|
sourceTask,
|
|
sourceTaskIndex,
|
|
destTask,
|
|
destTaskIndex
|
|
) {
|
|
// Create a copy of the source task with the new ID
|
|
const movedTask = {
|
|
...sourceTask,
|
|
id: destTask.id,
|
|
};
|
|
|
|
// Get numeric IDs for comparison
|
|
const sourceIdNum = parseInt(sourceTask.id, 10);
|
|
const destIdNum = parseInt(destTask.id, 10);
|
|
|
|
// Handle subtasks if present
|
|
if (sourceTask.subtasks && sourceTask.subtasks.length > 0) {
|
|
// Update subtasks to reference the new parent ID if needed
|
|
movedTask.subtasks = sourceTask.subtasks.map((subtask) => ({
|
|
...subtask,
|
|
parentTaskId: destIdNum,
|
|
}));
|
|
}
|
|
|
|
// Update any dependencies in other tasks that referenced the old source ID
|
|
data.tasks.forEach((task) => {
|
|
if (task.dependencies && task.dependencies.includes(sourceIdNum)) {
|
|
// Replace the old source ID with the new destination ID
|
|
const depIndex = task.dependencies.indexOf(sourceIdNum);
|
|
task.dependencies[depIndex] = destIdNum;
|
|
}
|
|
|
|
// Also check for subtask dependencies that might reference the source task
|
|
if (task.subtasks && task.subtasks.length > 0) {
|
|
task.subtasks.forEach((subtask) => {
|
|
if (
|
|
subtask.dependencies &&
|
|
subtask.dependencies.includes(sourceIdNum)
|
|
) {
|
|
const depIndex = subtask.dependencies.indexOf(sourceIdNum);
|
|
subtask.dependencies[depIndex] = destIdNum;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Remove tasks in the correct order to avoid index shifting issues
|
|
// Always remove the higher index first to avoid shifting the lower index
|
|
if (sourceTaskIndex > destTaskIndex) {
|
|
// Remove source first (higher index), then destination
|
|
data.tasks.splice(sourceTaskIndex, 1);
|
|
data.tasks.splice(destTaskIndex, 1);
|
|
// Insert the moved task at the destination position
|
|
data.tasks.splice(destTaskIndex, 0, movedTask);
|
|
} else {
|
|
// Remove destination first (higher index), then source
|
|
data.tasks.splice(destTaskIndex, 1);
|
|
data.tasks.splice(sourceTaskIndex, 1);
|
|
// Insert the moved task at the original destination position (now shifted down by 1)
|
|
data.tasks.splice(sourceTaskIndex, 0, movedTask);
|
|
}
|
|
|
|
log("info", `Moved task ${sourceIdNum} to replace task ${destIdNum}`);
|
|
|
|
return movedTask;
|
|
}
|
|
|
|
export default moveTask;
|