@taazkareem/clickup-mcp-server 0.4.60 → 0.4.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/build/config.js +10 -0
- package/build/index.js +9 -1356
- package/build/server.js +120 -0
- package/build/services/clickup/base.js +253 -0
- package/build/services/clickup/bulk.js +116 -0
- package/build/services/clickup/folder.js +133 -0
- package/build/services/clickup/index.js +43 -0
- package/build/services/clickup/initialization.js +28 -0
- package/build/services/clickup/list.js +188 -0
- package/build/services/clickup/task.js +492 -0
- package/build/services/clickup/types.js +4 -0
- package/build/services/clickup/workspace.js +314 -0
- package/build/services/shared.js +15 -0
- package/build/tools/folder.js +356 -0
- package/build/tools/index.js +11 -0
- package/build/tools/list.js +452 -0
- package/build/tools/task.js +1519 -0
- package/build/tools/utils.js +150 -0
- package/build/tools/workspace.js +132 -0
- package/package.json +3 -1
- package/build/services/clickup.js +0 -765
- package/build/types/clickup.js +0 -1
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickUp Task Service
|
|
3
|
+
*
|
|
4
|
+
* Handles all operations related to tasks in ClickUp, including:
|
|
5
|
+
* - Creating tasks (single and bulk)
|
|
6
|
+
* - Retrieving tasks (single or multiple)
|
|
7
|
+
* - Updating tasks
|
|
8
|
+
* - Deleting tasks
|
|
9
|
+
* - Finding tasks by name
|
|
10
|
+
*/
|
|
11
|
+
import { BaseClickUpService, ErrorCode, ClickUpServiceError } from './base.js';
|
|
12
|
+
import { BulkProcessor } from './bulk.js';
|
|
13
|
+
import { ListService } from './list.js';
|
|
14
|
+
export class TaskService extends BaseClickUpService {
|
|
15
|
+
constructor(apiKey, teamId, baseUrl, workspaceService) {
|
|
16
|
+
super(apiKey, teamId, baseUrl);
|
|
17
|
+
this.workspaceService = null;
|
|
18
|
+
this.bulkProcessor = new BulkProcessor();
|
|
19
|
+
this.listService = new ListService(apiKey, teamId);
|
|
20
|
+
this.workspaceService = workspaceService || null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Helper method to handle errors consistently
|
|
24
|
+
* @param error The error that occurred
|
|
25
|
+
* @param message Optional custom error message
|
|
26
|
+
* @returns A ClickUpServiceError
|
|
27
|
+
*/
|
|
28
|
+
handleError(error, message) {
|
|
29
|
+
if (error instanceof ClickUpServiceError) {
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
return new ClickUpServiceError(message || `Task service error: ${error.message}`, ErrorCode.UNKNOWN, error);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create a new task in the specified list
|
|
36
|
+
* @param listId The ID of the list to create the task in
|
|
37
|
+
* @param taskData The data for the new task
|
|
38
|
+
* @returns The created task
|
|
39
|
+
*/
|
|
40
|
+
async createTask(listId, taskData) {
|
|
41
|
+
this.logOperation('createTask', { listId, ...taskData });
|
|
42
|
+
try {
|
|
43
|
+
return await this.makeRequest(async () => {
|
|
44
|
+
const response = await this.client.post(`/list/${listId}/task`, taskData);
|
|
45
|
+
return response.data;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw this.handleError(error, `Failed to create task in list ${listId}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get all tasks in a list with optional filtering
|
|
54
|
+
* @param listId The ID of the list to get tasks from
|
|
55
|
+
* @param filters Optional filters to apply
|
|
56
|
+
* @returns List of tasks matching the filters
|
|
57
|
+
*/
|
|
58
|
+
async getTasks(listId, filters = {}) {
|
|
59
|
+
this.logOperation('getTasks', { listId, filters });
|
|
60
|
+
try {
|
|
61
|
+
return await this.makeRequest(async () => {
|
|
62
|
+
const params = new URLSearchParams();
|
|
63
|
+
// Add all filters to the query parameters
|
|
64
|
+
if (filters.include_closed)
|
|
65
|
+
params.append('include_closed', String(filters.include_closed));
|
|
66
|
+
if (filters.subtasks)
|
|
67
|
+
params.append('subtasks', String(filters.subtasks));
|
|
68
|
+
if (filters.page)
|
|
69
|
+
params.append('page', String(filters.page));
|
|
70
|
+
if (filters.order_by)
|
|
71
|
+
params.append('order_by', filters.order_by);
|
|
72
|
+
if (filters.reverse)
|
|
73
|
+
params.append('reverse', String(filters.reverse));
|
|
74
|
+
if (filters.statuses && filters.statuses.length > 0) {
|
|
75
|
+
filters.statuses.forEach(status => params.append('statuses[]', status));
|
|
76
|
+
}
|
|
77
|
+
if (filters.assignees && filters.assignees.length > 0) {
|
|
78
|
+
filters.assignees.forEach(assignee => params.append('assignees[]', assignee));
|
|
79
|
+
}
|
|
80
|
+
if (filters.due_date_gt)
|
|
81
|
+
params.append('due_date_gt', String(filters.due_date_gt));
|
|
82
|
+
if (filters.due_date_lt)
|
|
83
|
+
params.append('due_date_lt', String(filters.due_date_lt));
|
|
84
|
+
if (filters.date_created_gt)
|
|
85
|
+
params.append('date_created_gt', String(filters.date_created_gt));
|
|
86
|
+
if (filters.date_created_lt)
|
|
87
|
+
params.append('date_created_lt', String(filters.date_created_lt));
|
|
88
|
+
if (filters.date_updated_gt)
|
|
89
|
+
params.append('date_updated_gt', String(filters.date_updated_gt));
|
|
90
|
+
if (filters.date_updated_lt)
|
|
91
|
+
params.append('date_updated_lt', String(filters.date_updated_lt));
|
|
92
|
+
// Handle custom fields if present
|
|
93
|
+
if (filters.custom_fields) {
|
|
94
|
+
Object.entries(filters.custom_fields).forEach(([key, value]) => {
|
|
95
|
+
params.append(`custom_fields[${key}]`, String(value));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const response = await this.client.get(`/list/${listId}/task?${params.toString()}`);
|
|
99
|
+
return response.data.tasks;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw this.handleError(error, `Failed to get tasks from list ${listId}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get a specific task by ID
|
|
108
|
+
* @param taskId The ID of the task to retrieve
|
|
109
|
+
* @returns The task details
|
|
110
|
+
*/
|
|
111
|
+
async getTask(taskId) {
|
|
112
|
+
this.logOperation('getTask', { taskId });
|
|
113
|
+
try {
|
|
114
|
+
return await this.makeRequest(async () => {
|
|
115
|
+
const response = await this.client.get(`/task/${taskId}`);
|
|
116
|
+
return response.data;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
throw this.handleError(error, `Failed to get task ${taskId}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Update an existing task
|
|
125
|
+
* @param taskId ID of the task to update
|
|
126
|
+
* @param updateData Data to update on the task
|
|
127
|
+
* @returns The updated task
|
|
128
|
+
*/
|
|
129
|
+
async updateTask(taskId, updateData) {
|
|
130
|
+
this.logOperation('updateTask', { taskId, ...updateData });
|
|
131
|
+
try {
|
|
132
|
+
return await this.makeRequest(async () => {
|
|
133
|
+
const response = await this.client.put(`/task/${taskId}`, updateData);
|
|
134
|
+
return response.data;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
throw this.handleError(error, `Failed to update task ${taskId}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Delete a task
|
|
143
|
+
* @param taskId The ID of the task to delete
|
|
144
|
+
* @returns Success indicator
|
|
145
|
+
*/
|
|
146
|
+
async deleteTask(taskId) {
|
|
147
|
+
this.logOperation('deleteTask', { taskId });
|
|
148
|
+
try {
|
|
149
|
+
await this.makeRequest(async () => {
|
|
150
|
+
await this.client.delete(`/task/${taskId}`);
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
success: true
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw this.handleError(error, `Failed to delete task ${taskId}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Find a task by its name in a specific list
|
|
162
|
+
* @param listId The list ID to search within
|
|
163
|
+
* @param taskName The name of the task to find
|
|
164
|
+
* @returns The task if found, otherwise null
|
|
165
|
+
*/
|
|
166
|
+
async findTaskByName(listId, taskName) {
|
|
167
|
+
this.logOperation('findTaskByName', { listId, taskName });
|
|
168
|
+
try {
|
|
169
|
+
const tasks = await this.getTasks(listId);
|
|
170
|
+
const matchingTask = tasks.find(task => task.name.toLowerCase() === taskName.toLowerCase());
|
|
171
|
+
return matchingTask || null;
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
throw this.handleError(error, `Failed to find task by name: ${error instanceof Error ? error.message : String(error)}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Move a task to a different list
|
|
179
|
+
* @param taskId The ID of the task to move
|
|
180
|
+
* @param destinationListId The ID of the list to move the task to
|
|
181
|
+
* @returns The updated task
|
|
182
|
+
*/
|
|
183
|
+
async moveTask(taskId, destinationListId) {
|
|
184
|
+
this.logOperation('moveTask', { taskId, destinationListId });
|
|
185
|
+
try {
|
|
186
|
+
// First, get both the task and list info in parallel to save time
|
|
187
|
+
const [originalTask, destinationList] = await Promise.all([
|
|
188
|
+
this.getTask(taskId),
|
|
189
|
+
this.listService.getList(destinationListId)
|
|
190
|
+
]);
|
|
191
|
+
const currentStatus = originalTask.status?.status;
|
|
192
|
+
const availableStatuses = destinationList.statuses?.map(s => s.status) || [];
|
|
193
|
+
// Determine the appropriate status for the destination list
|
|
194
|
+
let newStatus = availableStatuses.includes(currentStatus || '')
|
|
195
|
+
? currentStatus // Keep the same status if available in destination list
|
|
196
|
+
: destinationList.statuses?.[0]?.status; // Otherwise use the default (first) status
|
|
197
|
+
// Priority mapping: convert string priority to numeric value if needed
|
|
198
|
+
let priorityValue = null;
|
|
199
|
+
if (originalTask.priority) {
|
|
200
|
+
// If priority.id exists and is numeric, use that
|
|
201
|
+
if (originalTask.priority.id) {
|
|
202
|
+
priorityValue = parseInt(originalTask.priority.id);
|
|
203
|
+
// Ensure it's in the valid range 1-4
|
|
204
|
+
if (isNaN(priorityValue) || priorityValue < 1 || priorityValue > 4) {
|
|
205
|
+
priorityValue = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Prepare the task data for the new list
|
|
210
|
+
const taskData = {
|
|
211
|
+
name: originalTask.name,
|
|
212
|
+
description: originalTask.description,
|
|
213
|
+
status: newStatus,
|
|
214
|
+
priority: priorityValue,
|
|
215
|
+
due_date: originalTask.due_date ? Number(originalTask.due_date) : undefined,
|
|
216
|
+
assignees: originalTask.assignees?.map(a => a.id) || [],
|
|
217
|
+
// Add any other relevant fields from the original task
|
|
218
|
+
};
|
|
219
|
+
// Create new task and delete old one in a single makeRequest call
|
|
220
|
+
return await this.makeRequest(async () => {
|
|
221
|
+
// First create the new task
|
|
222
|
+
const response = await this.client.post(`/list/${destinationListId}/task`, taskData);
|
|
223
|
+
// Then delete the original task
|
|
224
|
+
await this.client.delete(`/task/${taskId}`);
|
|
225
|
+
// Add a property to indicate the task was moved
|
|
226
|
+
const newTask = {
|
|
227
|
+
...response.data,
|
|
228
|
+
moved: true,
|
|
229
|
+
originalId: taskId
|
|
230
|
+
};
|
|
231
|
+
return newTask;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
throw this.handleError(error, 'Failed to move task');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Create a duplicate of an existing task
|
|
240
|
+
* @param taskId The ID of the task to duplicate
|
|
241
|
+
* @param listId Optional destination list ID (defaults to the same list)
|
|
242
|
+
* @returns The newly created duplicate task
|
|
243
|
+
*/
|
|
244
|
+
async duplicateTask(taskId, listId) {
|
|
245
|
+
this.logOperation('duplicateTask', { taskId, listId });
|
|
246
|
+
try {
|
|
247
|
+
// First, get the original task
|
|
248
|
+
const originalTask = await this.getTask(taskId);
|
|
249
|
+
// Priority mapping: convert string priority to numeric value if needed
|
|
250
|
+
let priorityValue = null;
|
|
251
|
+
if (originalTask.priority) {
|
|
252
|
+
// If priority.id exists and is numeric, use that
|
|
253
|
+
if (originalTask.priority.id) {
|
|
254
|
+
priorityValue = parseInt(originalTask.priority.id);
|
|
255
|
+
// Ensure it's in the valid range 1-4
|
|
256
|
+
if (isNaN(priorityValue) || priorityValue < 1 || priorityValue > 4) {
|
|
257
|
+
priorityValue = null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Prepare data for the new task
|
|
262
|
+
const newTaskData = {
|
|
263
|
+
name: `${originalTask.name} (Copy)`,
|
|
264
|
+
description: originalTask.description,
|
|
265
|
+
status: originalTask.status?.status,
|
|
266
|
+
priority: priorityValue,
|
|
267
|
+
due_date: originalTask.due_date ? new Date(originalTask.due_date).getTime() : undefined,
|
|
268
|
+
assignees: originalTask.assignees?.map(a => a.id) || []
|
|
269
|
+
};
|
|
270
|
+
// Create the new task in the specified list or original list
|
|
271
|
+
const targetListId = listId || originalTask.list.id;
|
|
272
|
+
return await this.createTask(targetListId, newTaskData);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
throw this.handleError(error, 'Failed to duplicate task');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Create multiple tasks in a list with advanced batching options
|
|
280
|
+
*
|
|
281
|
+
* @param listId The ID of the list to create tasks in
|
|
282
|
+
* @param data Object containing array of tasks to create
|
|
283
|
+
* @param options Configuration options for the bulk operation
|
|
284
|
+
* @param progressCallback Optional callback for tracking progress
|
|
285
|
+
* @returns Result containing both successful and failed operations
|
|
286
|
+
*/
|
|
287
|
+
async createBulkTasks(listId, data, options, progressCallback) {
|
|
288
|
+
this.logOperation('createBulkTasks', {
|
|
289
|
+
listId,
|
|
290
|
+
taskCount: data.tasks.length,
|
|
291
|
+
batchSize: options?.batchSize,
|
|
292
|
+
concurrency: options?.concurrency
|
|
293
|
+
});
|
|
294
|
+
try {
|
|
295
|
+
// Validate list exists before proceeding
|
|
296
|
+
const list = await this.listService.getList(listId).catch(() => null);
|
|
297
|
+
if (!list) {
|
|
298
|
+
throw new ClickUpServiceError(`List not found with ID: ${listId}`, ErrorCode.NOT_FOUND);
|
|
299
|
+
}
|
|
300
|
+
// Set default options for better performance
|
|
301
|
+
const bulkOptions = {
|
|
302
|
+
batchSize: options?.batchSize ?? 5, // Smaller batch size for better rate limit handling
|
|
303
|
+
concurrency: options?.concurrency ?? 2, // Lower concurrency to avoid rate limits
|
|
304
|
+
continueOnError: options?.continueOnError ?? true, // Continue on individual task failures
|
|
305
|
+
retryCount: options?.retryCount ?? 3, // Retry failed operations
|
|
306
|
+
...options
|
|
307
|
+
};
|
|
308
|
+
// Add list validation to progress tracking
|
|
309
|
+
const wrappedCallback = progressCallback ?
|
|
310
|
+
(progress) => {
|
|
311
|
+
progress.context = { listId, listName: list.name };
|
|
312
|
+
progressCallback(progress);
|
|
313
|
+
} : undefined;
|
|
314
|
+
return await this.bulkProcessor.processBulk(data.tasks, async (taskData) => {
|
|
315
|
+
try {
|
|
316
|
+
// Ensure task data is properly formatted
|
|
317
|
+
const sanitizedData = {
|
|
318
|
+
...taskData,
|
|
319
|
+
// Remove any accidentally included list IDs in task data
|
|
320
|
+
list: undefined,
|
|
321
|
+
// Ensure name has emoji if missing
|
|
322
|
+
name: taskData.name.match(/^\p{Emoji}/u) ?
|
|
323
|
+
taskData.name :
|
|
324
|
+
'📋 ' + taskData.name
|
|
325
|
+
};
|
|
326
|
+
return await this.createTask(listId, sanitizedData);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
// Enhance error context for better debugging
|
|
330
|
+
if (error instanceof ClickUpServiceError) {
|
|
331
|
+
error.context = {
|
|
332
|
+
...error.context,
|
|
333
|
+
taskName: taskData.name,
|
|
334
|
+
listId,
|
|
335
|
+
listName: list.name
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}, bulkOptions);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
const errorMessage = error instanceof ClickUpServiceError ?
|
|
344
|
+
error.message :
|
|
345
|
+
`Failed to create tasks in bulk: ${error.message}`;
|
|
346
|
+
throw new ClickUpServiceError(errorMessage, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, {
|
|
347
|
+
listId,
|
|
348
|
+
taskCount: data.tasks.length,
|
|
349
|
+
error: error instanceof Error ? error.message : String(error)
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Update multiple tasks in bulk with advanced batching options
|
|
355
|
+
*
|
|
356
|
+
* @param tasks Array of task IDs and update data
|
|
357
|
+
* @param options Configuration options for the bulk operation
|
|
358
|
+
* @param progressCallback Optional callback for tracking progress
|
|
359
|
+
* @returns Result containing both successful and failed operations
|
|
360
|
+
*/
|
|
361
|
+
async updateBulkTasks(tasks, options, progressCallback) {
|
|
362
|
+
this.logOperation('updateBulkTasks', {
|
|
363
|
+
taskCount: tasks.length,
|
|
364
|
+
batchSize: options?.batchSize,
|
|
365
|
+
concurrency: options?.concurrency
|
|
366
|
+
});
|
|
367
|
+
try {
|
|
368
|
+
return await this.bulkProcessor.processBulk(tasks, ({ id, data }) => this.updateTask(id, data), options);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
if (error instanceof ClickUpServiceError) {
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
throw new ClickUpServiceError(`Failed to update tasks in bulk: ${error.message}`, ErrorCode.UNKNOWN, error);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Move multiple tasks to a different list in bulk
|
|
379
|
+
*
|
|
380
|
+
* @param tasks Array of task IDs to move
|
|
381
|
+
* @param targetListId ID of the list to move tasks to
|
|
382
|
+
* @param options Configuration options for the bulk operation
|
|
383
|
+
* @param progressCallback Optional callback for tracking progress
|
|
384
|
+
* @returns Result containing both successful and failed operations
|
|
385
|
+
*/
|
|
386
|
+
async moveBulkTasks(tasks, targetListId, options, progressCallback) {
|
|
387
|
+
this.logOperation('moveBulkTasks', {
|
|
388
|
+
taskCount: tasks.length,
|
|
389
|
+
targetListId,
|
|
390
|
+
batchSize: options?.batchSize,
|
|
391
|
+
concurrency: options?.concurrency
|
|
392
|
+
});
|
|
393
|
+
try {
|
|
394
|
+
// First verify destination list exists
|
|
395
|
+
const destinationList = await this.listService.getList(targetListId);
|
|
396
|
+
if (!destinationList) {
|
|
397
|
+
throw new ClickUpServiceError(`Destination list not found with ID: ${targetListId}`, ErrorCode.NOT_FOUND);
|
|
398
|
+
}
|
|
399
|
+
// Set default options for better performance
|
|
400
|
+
const bulkOptions = {
|
|
401
|
+
batchSize: options?.batchSize ?? 5, // Smaller batch size for better rate limit handling
|
|
402
|
+
concurrency: options?.concurrency ?? 2, // Lower concurrency to avoid rate limits
|
|
403
|
+
continueOnError: options?.continueOnError ?? true, // Continue on individual task failures
|
|
404
|
+
retryCount: options?.retryCount ?? 3, // Retry failed operations
|
|
405
|
+
...options
|
|
406
|
+
};
|
|
407
|
+
return await this.bulkProcessor.processBulk(tasks, async (taskId) => {
|
|
408
|
+
try {
|
|
409
|
+
// Get the original task
|
|
410
|
+
const originalTask = await this.getTask(taskId);
|
|
411
|
+
const currentStatus = originalTask.status?.status;
|
|
412
|
+
const availableStatuses = destinationList.statuses?.map(s => s.status) || [];
|
|
413
|
+
// Determine the appropriate status for the destination list
|
|
414
|
+
let newStatus = availableStatuses.includes(currentStatus || '')
|
|
415
|
+
? currentStatus // Keep the same status if available in destination list
|
|
416
|
+
: destinationList.statuses?.[0]?.status; // Otherwise use the default (first) status
|
|
417
|
+
// Prepare the task data for the new list
|
|
418
|
+
const taskData = {
|
|
419
|
+
name: originalTask.name,
|
|
420
|
+
description: originalTask.description,
|
|
421
|
+
status: newStatus,
|
|
422
|
+
priority: originalTask.priority?.priority,
|
|
423
|
+
due_date: originalTask.due_date ? Number(originalTask.due_date) : undefined,
|
|
424
|
+
assignees: originalTask.assignees?.map(a => a.id) || []
|
|
425
|
+
};
|
|
426
|
+
// Create new task and delete old one in a single makeRequest call
|
|
427
|
+
return await this.makeRequest(async () => {
|
|
428
|
+
// First create the new task
|
|
429
|
+
const response = await this.client.post(`/list/${targetListId}/task`, taskData);
|
|
430
|
+
// Then delete the original task
|
|
431
|
+
await this.client.delete(`/task/${taskId}`);
|
|
432
|
+
// Add a property to indicate the task was moved
|
|
433
|
+
const newTask = {
|
|
434
|
+
...response.data,
|
|
435
|
+
moved: true,
|
|
436
|
+
originalId: taskId
|
|
437
|
+
};
|
|
438
|
+
return newTask;
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
// Enhance error context for better debugging
|
|
443
|
+
if (error instanceof ClickUpServiceError) {
|
|
444
|
+
error.context = {
|
|
445
|
+
...error.context,
|
|
446
|
+
taskId,
|
|
447
|
+
targetListId,
|
|
448
|
+
targetListName: destinationList.name
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}, bulkOptions);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
const errorMessage = error instanceof ClickUpServiceError ?
|
|
457
|
+
error.message :
|
|
458
|
+
`Failed to move tasks in bulk: ${error.message}`;
|
|
459
|
+
throw new ClickUpServiceError(errorMessage, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, {
|
|
460
|
+
targetListId,
|
|
461
|
+
taskCount: tasks.length,
|
|
462
|
+
error: error instanceof Error ? error.message : String(error)
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Delete multiple tasks in bulk with advanced batching options
|
|
468
|
+
*
|
|
469
|
+
* @param taskIds Array of task IDs to delete
|
|
470
|
+
* @param options Configuration options for the bulk operation
|
|
471
|
+
* @returns Result containing both successful and failed operations
|
|
472
|
+
*/
|
|
473
|
+
async deleteBulkTasks(taskIds, options) {
|
|
474
|
+
this.logOperation('deleteBulkTasks', {
|
|
475
|
+
taskCount: taskIds.length,
|
|
476
|
+
batchSize: options?.batchSize,
|
|
477
|
+
concurrency: options?.concurrency
|
|
478
|
+
});
|
|
479
|
+
try {
|
|
480
|
+
return await this.bulkProcessor.processBulk(taskIds, async (taskId) => {
|
|
481
|
+
await this.deleteTask(taskId);
|
|
482
|
+
return taskId;
|
|
483
|
+
}, options);
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (error instanceof ClickUpServiceError) {
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
throw new ClickUpServiceError(`Failed to delete tasks in bulk: ${error.message}`, ErrorCode.UNKNOWN, error);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|