Compare commits
4 Commits
task-maste
...
task-maste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b3dbabed1 | ||
|
|
af53525cbc | ||
|
|
b7f32eac5a | ||
|
|
044a7bfc98 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
|||||||
# task-master-ai
|
# task-master-ai
|
||||||
|
|
||||||
|
## 0.27.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1254](https://github.com/eyaltoledano/claude-task-master/pull/1254) [`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fixed issue where `tm show` command could not find subtasks using dotted notation IDs (e.g., '8.1').
|
||||||
|
- The command now properly searches within parent task subtasks and returns the correct subtask information.
|
||||||
|
|
||||||
|
## 0.27.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1248](https://github.com/eyaltoledano/claude-task-master/pull/1248) [`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix set-status for subtasks:
|
||||||
|
- Parent tasks are now set as `done` when subtasks are all `done`
|
||||||
|
- Parent tasks are now set as `in-progress` when at least one subtask is `in-progress` or `done`
|
||||||
|
|
||||||
## 0.27.1
|
## 0.27.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 0.25.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d)]:
|
||||||
|
- task-master-ai@0.27.3
|
||||||
|
|
||||||
|
## 0.25.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011)]:
|
||||||
|
- task-master-ai@0.27.2
|
||||||
|
|
||||||
## 0.25.2
|
## 0.25.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "TaskMaster",
|
"displayName": "TaskMaster",
|
||||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||||
"version": "0.25.2",
|
"version": "0.25.4",
|
||||||
"publisher": "Hamster",
|
"publisher": "Hamster",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "0.27.1"
|
"task-master-ai": "0.27.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.1",
|
"version": "0.27.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.1",
|
"version": "0.27.3",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -357,9 +357,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension": {
|
"apps/extension": {
|
||||||
"version": "0.25.2",
|
"version": "0.25.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "0.27.1"
|
"task-master-ai": "0.27.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.1",
|
"version": "0.27.3",
|
||||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,7 +3,17 @@
|
|||||||
* This file defines the contract for all storage implementations
|
* This file defines the contract for all storage implementations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Task, TaskMetadata } from '../types/index.js';
|
import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result type for updateTaskStatus operations
|
||||||
|
*/
|
||||||
|
export interface UpdateStatusResult {
|
||||||
|
success: boolean;
|
||||||
|
oldStatus: TaskStatus;
|
||||||
|
newStatus: TaskStatus;
|
||||||
|
taskId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for storage operations on tasks
|
* Interface for storage operations on tasks
|
||||||
@@ -54,6 +64,19 @@ export interface IStorage {
|
|||||||
tag?: string
|
tag?: string
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update task or subtask status by ID
|
||||||
|
* @param taskId - ID of the task or subtask (e.g., "1" or "1.2")
|
||||||
|
* @param newStatus - New status to set
|
||||||
|
* @param tag - Optional tag context for the task
|
||||||
|
* @returns Promise that resolves to update result with old and new status
|
||||||
|
*/
|
||||||
|
updateTaskStatus(
|
||||||
|
taskId: string,
|
||||||
|
newStatus: TaskStatus,
|
||||||
|
tag?: string
|
||||||
|
): Promise<UpdateStatusResult>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a task by ID
|
* Delete a task by ID
|
||||||
* @param taskId - ID of the task to delete
|
* @param taskId - ID of the task to delete
|
||||||
@@ -191,6 +214,11 @@ export abstract class BaseStorage implements IStorage {
|
|||||||
updates: Partial<Task>,
|
updates: Partial<Task>,
|
||||||
tag?: string
|
tag?: string
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
abstract updateTaskStatus(
|
||||||
|
taskId: string,
|
||||||
|
newStatus: TaskStatus,
|
||||||
|
tag?: string
|
||||||
|
): Promise<UpdateStatusResult>;
|
||||||
abstract deleteTask(taskId: string, tag?: string): Promise<void>;
|
abstract deleteTask(taskId: string, tag?: string): Promise<void>;
|
||||||
abstract exists(tag?: string): Promise<boolean>;
|
abstract exists(tag?: string): Promise<boolean>;
|
||||||
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
||||||
|
|||||||
@@ -135,15 +135,28 @@ export class TaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single task by ID
|
* Get a single task by ID - delegates to storage layer
|
||||||
*/
|
*/
|
||||||
async getTask(taskId: string, tag?: string): Promise<Task | null> {
|
async getTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||||
const result = await this.getTaskList({
|
// Use provided tag or get active tag
|
||||||
tag,
|
const activeTag = tag || this.getActiveTag();
|
||||||
includeSubtasks: true
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.tasks.find((t) => t.id === taskId) || null;
|
try {
|
||||||
|
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
|
||||||
|
return await this.storage.loadTask(String(taskId), activeTag);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TaskMasterError(
|
||||||
|
`Failed to get task ${taskId}`,
|
||||||
|
ERROR_CODES.STORAGE_ERROR,
|
||||||
|
{
|
||||||
|
operation: 'getTask',
|
||||||
|
resource: 'task',
|
||||||
|
taskId: String(taskId),
|
||||||
|
tag: activeTag
|
||||||
|
},
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -446,7 +459,7 @@ export class TaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update task status
|
* Update task status - delegates to storage layer which handles storage-specific logic
|
||||||
*/
|
*/
|
||||||
async updateTaskStatus(
|
async updateTaskStatus(
|
||||||
taskId: string | number,
|
taskId: string | number,
|
||||||
@@ -468,49 +481,28 @@ export class TaskService {
|
|||||||
|
|
||||||
// Use provided tag or get active tag
|
// Use provided tag or get active tag
|
||||||
const activeTag = tag || this.getActiveTag();
|
const activeTag = tag || this.getActiveTag();
|
||||||
|
|
||||||
const taskIdStr = String(taskId);
|
const taskIdStr = String(taskId);
|
||||||
|
|
||||||
// TODO: For now, assume it's a regular task and just try to update directly
|
|
||||||
// In the future, we can add subtask support if needed
|
|
||||||
if (taskIdStr.includes('.')) {
|
|
||||||
throw new TaskMasterError(
|
|
||||||
'Subtask status updates not yet supported in API storage',
|
|
||||||
ERROR_CODES.NOT_IMPLEMENTED
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current task to get old status (simple, direct approach)
|
|
||||||
let currentTask: Task | null;
|
|
||||||
try {
|
try {
|
||||||
// Try to get the task directly
|
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
|
||||||
currentTask = await this.storage.loadTask(taskIdStr, activeTag);
|
return await this.storage.updateTaskStatus(
|
||||||
|
taskIdStr,
|
||||||
|
newStatus,
|
||||||
|
activeTag
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
`Failed to load task ${taskIdStr}`,
|
`Failed to update task status for ${taskIdStr}`,
|
||||||
ERROR_CODES.TASK_NOT_FOUND,
|
ERROR_CODES.STORAGE_ERROR,
|
||||||
{ taskId: taskIdStr },
|
{
|
||||||
|
operation: 'updateTaskStatus',
|
||||||
|
resource: 'task',
|
||||||
|
taskId: taskIdStr,
|
||||||
|
newStatus,
|
||||||
|
tag: activeTag
|
||||||
|
},
|
||||||
error as Error
|
error as Error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentTask) {
|
|
||||||
throw new TaskMasterError(
|
|
||||||
`Task ${taskIdStr} not found`,
|
|
||||||
ERROR_CODES.TASK_NOT_FOUND
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldStatus = currentTask.status;
|
|
||||||
|
|
||||||
// Simple, direct update - just change the status
|
|
||||||
await this.storage.updateTask(taskIdStr, { status: newStatus }, activeTag);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
oldStatus,
|
|
||||||
newStatus,
|
|
||||||
taskId: taskIdStr
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,15 @@
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
IStorage,
|
IStorage,
|
||||||
StorageStats
|
StorageStats,
|
||||||
|
UpdateStatusResult
|
||||||
} from '../interfaces/storage.interface.js';
|
} from '../interfaces/storage.interface.js';
|
||||||
import type { Task, TaskMetadata, TaskTag } from '../types/index.js';
|
import type {
|
||||||
|
Task,
|
||||||
|
TaskMetadata,
|
||||||
|
TaskTag,
|
||||||
|
TaskStatus
|
||||||
|
} from '../types/index.js';
|
||||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||||
import { TaskRepository } from '../repositories/task-repository.interface.js';
|
import { TaskRepository } from '../repositories/task-repository.interface.js';
|
||||||
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
|
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
|
||||||
@@ -485,6 +491,62 @@ export class ApiStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update task or subtask status by ID - for API storage
|
||||||
|
*/
|
||||||
|
async updateTaskStatus(
|
||||||
|
taskId: string,
|
||||||
|
newStatus: TaskStatus,
|
||||||
|
tag?: string
|
||||||
|
): Promise<UpdateStatusResult> {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingTask = await this.retryOperation(() =>
|
||||||
|
this.repository.getTask(this.projectId, taskId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingTask) {
|
||||||
|
throw new Error(`Task ${taskId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldStatus = existingTask.status;
|
||||||
|
if (oldStatus === newStatus) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
oldStatus,
|
||||||
|
newStatus,
|
||||||
|
taskId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the task/subtask status
|
||||||
|
await this.retryOperation(() =>
|
||||||
|
this.repository.updateTask(this.projectId, taskId, {
|
||||||
|
status: newStatus,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Note: Parent status auto-adjustment is handled by the backend API service
|
||||||
|
// which has its own business logic for managing task relationships
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
oldStatus,
|
||||||
|
newStatus,
|
||||||
|
taskId
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new TaskMasterError(
|
||||||
|
'Failed to update task status via API',
|
||||||
|
ERROR_CODES.STORAGE_ERROR,
|
||||||
|
{ operation: 'updateTaskStatus', taskId, newStatus, tag },
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all available tags
|
* Get all available tags
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
* @fileoverview Refactored file-based storage implementation for Task Master
|
* @fileoverview Refactored file-based storage implementation for Task Master
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Task, TaskMetadata } from '../../types/index.js';
|
import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js';
|
||||||
import type {
|
import type {
|
||||||
IStorage,
|
IStorage,
|
||||||
StorageStats
|
StorageStats,
|
||||||
|
UpdateStatusResult
|
||||||
} from '../../interfaces/storage.interface.js';
|
} from '../../interfaces/storage.interface.js';
|
||||||
import { FormatHandler } from './format-handler.js';
|
import { FormatHandler } from './format-handler.js';
|
||||||
import { FileOperations } from './file-operations.js';
|
import { FileOperations } from './file-operations.js';
|
||||||
@@ -104,9 +105,65 @@ export class FileStorage implements IStorage {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a single task by ID from the tasks.json file
|
* Load a single task by ID from the tasks.json file
|
||||||
|
* Handles both regular tasks and subtasks (with dotted notation like "1.2")
|
||||||
*/
|
*/
|
||||||
async loadTask(taskId: string, tag?: string): Promise<Task | null> {
|
async loadTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||||
const tasks = await this.loadTasks(tag);
|
const tasks = await this.loadTasks(tag);
|
||||||
|
|
||||||
|
// Check if this is a subtask (contains a dot)
|
||||||
|
if (taskId.includes('.')) {
|
||||||
|
const [parentId, subtaskId] = taskId.split('.');
|
||||||
|
const parentTask = tasks.find((t) => String(t.id) === parentId);
|
||||||
|
|
||||||
|
if (!parentTask || !parentTask.subtasks) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtask = parentTask.subtasks.find(
|
||||||
|
(st) => String(st.id) === subtaskId
|
||||||
|
);
|
||||||
|
if (!subtask) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toFullSubId = (maybeDotId: string | number): string => {
|
||||||
|
const depId = String(maybeDotId);
|
||||||
|
return depId.includes('.') ? depId : `${parentTask.id}.${depId}`;
|
||||||
|
};
|
||||||
|
const resolvedDependencies =
|
||||||
|
subtask.dependencies?.map((dep) => toFullSubId(dep)) ?? [];
|
||||||
|
|
||||||
|
// Return a Task-like object for the subtask with the full dotted ID
|
||||||
|
// Following the same pattern as findTaskById in utils.js
|
||||||
|
const subtaskResult = {
|
||||||
|
...subtask,
|
||||||
|
id: taskId, // Use the full dotted ID
|
||||||
|
title: subtask.title || `Subtask ${subtaskId}`,
|
||||||
|
description: subtask.description || '',
|
||||||
|
status: subtask.status || 'pending',
|
||||||
|
priority: subtask.priority || parentTask.priority || 'medium',
|
||||||
|
dependencies: resolvedDependencies,
|
||||||
|
details: subtask.details || '',
|
||||||
|
testStrategy: subtask.testStrategy || '',
|
||||||
|
subtasks: [],
|
||||||
|
tags: parentTask.tags || [],
|
||||||
|
assignee: subtask.assignee || parentTask.assignee,
|
||||||
|
complexity: subtask.complexity || parentTask.complexity,
|
||||||
|
createdAt: subtask.createdAt || parentTask.createdAt,
|
||||||
|
updatedAt: subtask.updatedAt || parentTask.updatedAt,
|
||||||
|
// Add reference to parent task for context (like utils.js does)
|
||||||
|
parentTask: {
|
||||||
|
id: parentTask.id,
|
||||||
|
title: parentTask.title,
|
||||||
|
status: parentTask.status
|
||||||
|
},
|
||||||
|
isSubtask: true
|
||||||
|
};
|
||||||
|
|
||||||
|
return subtaskResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular task lookup
|
||||||
return tasks.find((task) => String(task.id) === String(taskId)) || null;
|
return tasks.find((task) => String(task.id) === String(taskId)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +338,156 @@ export class FileStorage implements IStorage {
|
|||||||
await this.saveTasks(tasks, tag);
|
await this.saveTasks(tasks, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update task or subtask status by ID - handles file storage logic with parent/subtask relationships
|
||||||
|
*/
|
||||||
|
async updateTaskStatus(
|
||||||
|
taskId: string,
|
||||||
|
newStatus: TaskStatus,
|
||||||
|
tag?: string
|
||||||
|
): Promise<UpdateStatusResult> {
|
||||||
|
const tasks = await this.loadTasks(tag);
|
||||||
|
|
||||||
|
// Check if this is a subtask (contains a dot)
|
||||||
|
if (taskId.includes('.')) {
|
||||||
|
return this.updateSubtaskStatusInFile(tasks, taskId, newStatus, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular task update
|
||||||
|
const taskIndex = tasks.findIndex((t) => String(t.id) === String(taskId));
|
||||||
|
|
||||||
|
if (taskIndex === -1) {
|
||||||
|
throw new Error(`Task ${taskId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldStatus = tasks[taskIndex].status;
|
||||||
|
if (oldStatus === newStatus) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
oldStatus,
|
||||||
|
newStatus,
|
||||||
|
taskId: String(taskId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks[taskIndex] = {
|
||||||
|
...tasks[taskIndex],
|
||||||
|
status: newStatus,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.saveTasks(tasks, tag);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
oldStatus,
|
||||||
|
newStatus,
|
||||||
|
taskId: String(taskId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update subtask status within file storage - handles parent status auto-adjustment
|
||||||
|
*/
|
||||||
|
private async updateSubtaskStatusInFile(
|
||||||
|
tasks: Task[],
|
||||||
|
subtaskId: string,
|
||||||
|
newStatus: TaskStatus,
|
||||||
|
tag?: string
|
||||||
|
): Promise<UpdateStatusResult> {
|
||||||
|
// Parse the subtask ID to get parent ID and subtask ID
|
||||||
|
const parts = subtaskId.split('.');
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid subtask ID format: ${subtaskId}. Expected format: parentId.subtaskId`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [parentId, subIdRaw] = parts;
|
||||||
|
const subId = subIdRaw.trim();
|
||||||
|
if (!/^\d+$/.test(subId)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid subtask ID: ${subId}. Subtask ID must be a positive integer.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const subtaskNumericId = Number(subId);
|
||||||
|
|
||||||
|
// Find the parent task
|
||||||
|
const parentTaskIndex = tasks.findIndex(
|
||||||
|
(t) => String(t.id) === String(parentId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parentTaskIndex === -1) {
|
||||||
|
throw new Error(`Parent task ${parentId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentTask = tasks[parentTaskIndex];
|
||||||
|
|
||||||
|
// Find the subtask within the parent task
|
||||||
|
const subtaskIndex = parentTask.subtasks.findIndex(
|
||||||
|
(st) => st.id === subtaskNumericId || String(st.id) === subId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subtaskIndex === -1) {
|
||||||
|
throw new Error(
|
||||||
|
`Subtask ${subtaskId} not found in parent task ${parentId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldStatus = parentTask.subtasks[subtaskIndex].status || 'pending';
|
||||||
|
if (oldStatus === newStatus) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
oldStatus,
|
||||||
|
newStatus,
|
||||||
|
taskId: subtaskId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// Update the subtask status
|
||||||
|
parentTask.subtasks[subtaskIndex] = {
|
||||||
|
...parentTask.subtasks[subtaskIndex],
|
||||||
|
status: newStatus,
|
||||||
|
updatedAt: now
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-adjust parent status based on subtask statuses
|
||||||
|
const subs = parentTask.subtasks;
|
||||||
|
let parentNewStatus = parentTask.status;
|
||||||
|
if (subs.length > 0) {
|
||||||
|
const norm = (s: any) => s.status || 'pending';
|
||||||
|
const isDoneLike = (s: any) => {
|
||||||
|
const st = norm(s);
|
||||||
|
return st === 'done' || st === 'completed';
|
||||||
|
};
|
||||||
|
const allDone = subs.every(isDoneLike);
|
||||||
|
const anyInProgress = subs.some((s) => norm(s) === 'in-progress');
|
||||||
|
const anyDone = subs.some(isDoneLike);
|
||||||
|
if (allDone) parentNewStatus = 'done';
|
||||||
|
else if (anyInProgress || anyDone) parentNewStatus = 'in-progress';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always bump updatedAt; update status only if changed
|
||||||
|
tasks[parentTaskIndex] = {
|
||||||
|
...parentTask,
|
||||||
|
...(parentNewStatus !== parentTask.status
|
||||||
|
? { status: parentNewStatus }
|
||||||
|
: {}),
|
||||||
|
updatedAt: now
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.saveTasks(tasks, tag);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
oldStatus,
|
||||||
|
newStatus,
|
||||||
|
taskId: subtaskId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a task
|
* Delete a task
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user