Skip to content

Commit 4fb7f2b

Browse files
feat: Add moveTask method for single task moves (#354)
Co-authored-by: Claude <[email protected]>
1 parent 888741f commit 4fb7f2b

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed

src/TodoistApi.moveTask.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { TodoistApi } from '.'
2+
import { DEFAULT_AUTH_TOKEN, DEFAULT_REQUEST_ID, DEFAULT_TASK } from './testUtils/testDefaults'
3+
import { getSyncBaseUri, ENDPOINT_REST_TASKS, ENDPOINT_REST_TASK_MOVE } from './consts/endpoints'
4+
import { setupRestClientMock } from './testUtils/mocks'
5+
import { getTaskUrl } from './utils/urlHelpers'
6+
7+
function getTarget(baseUrl = 'https://api.todoist.com') {
8+
return new TodoistApi(DEFAULT_AUTH_TOKEN, baseUrl)
9+
}
10+
11+
describe('TodoistApi moveTask', () => {
12+
const TASK_ID = '123'
13+
const PROJECT_ID = '999'
14+
const SECTION_ID = '888'
15+
const PARENT_ID = '777'
16+
17+
describe.each([
18+
{
19+
description: 'project',
20+
args: { projectId: PROJECT_ID },
21+
expectedApiArgs: { project_id: PROJECT_ID },
22+
expectedTaskProps: { projectId: PROJECT_ID },
23+
},
24+
{
25+
description: 'section',
26+
args: { sectionId: SECTION_ID },
27+
expectedApiArgs: { section_id: SECTION_ID },
28+
expectedTaskProps: { sectionId: SECTION_ID },
29+
},
30+
{
31+
description: 'parent',
32+
args: { parentId: PARENT_ID },
33+
expectedApiArgs: { parent_id: PARENT_ID },
34+
expectedTaskProps: { parentId: PARENT_ID },
35+
},
36+
])('moving task to $description', ({ args, expectedApiArgs, expectedTaskProps }) => {
37+
test('calls post on restClient with expected parameters', async () => {
38+
const movedTask = {
39+
...DEFAULT_TASK,
40+
id: TASK_ID,
41+
...expectedTaskProps,
42+
url: getTaskUrl(TASK_ID, DEFAULT_TASK.content),
43+
}
44+
const requestMock = setupRestClientMock(movedTask)
45+
const api = getTarget()
46+
47+
await api.moveTask(TASK_ID, args, DEFAULT_REQUEST_ID)
48+
49+
expect(requestMock).toHaveBeenCalledTimes(1)
50+
expect(requestMock).toHaveBeenCalledWith(
51+
'POST',
52+
getSyncBaseUri(),
53+
`${ENDPOINT_REST_TASKS}/${TASK_ID}/${ENDPOINT_REST_TASK_MOVE}`,
54+
DEFAULT_AUTH_TOKEN,
55+
expectedApiArgs,
56+
DEFAULT_REQUEST_ID,
57+
)
58+
})
59+
60+
test('returns moved task', async () => {
61+
const movedTask = {
62+
...DEFAULT_TASK,
63+
id: TASK_ID,
64+
...expectedTaskProps,
65+
url: getTaskUrl(TASK_ID, DEFAULT_TASK.content),
66+
}
67+
setupRestClientMock(movedTask)
68+
const api = getTarget()
69+
70+
const result = await api.moveTask(TASK_ID, args)
71+
72+
expect(result).toEqual(movedTask)
73+
})
74+
})
75+
76+
test('calls post on restClient with expected parameters against staging', async () => {
77+
const movedTask = {
78+
...DEFAULT_TASK,
79+
id: TASK_ID,
80+
projectId: PROJECT_ID,
81+
url: getTaskUrl(TASK_ID, DEFAULT_TASK.content),
82+
}
83+
const requestMock = setupRestClientMock(movedTask)
84+
const api = getTarget('https://staging.todoist.com')
85+
86+
await api.moveTask(TASK_ID, { projectId: PROJECT_ID }, DEFAULT_REQUEST_ID)
87+
88+
expect(requestMock).toHaveBeenCalledTimes(1)
89+
expect(requestMock).toHaveBeenCalledWith(
90+
'POST',
91+
getSyncBaseUri('https://staging.todoist.com'),
92+
`${ENDPOINT_REST_TASKS}/${TASK_ID}/${ENDPOINT_REST_TASK_MOVE}`,
93+
DEFAULT_AUTH_TOKEN,
94+
{ project_id: PROJECT_ID },
95+
DEFAULT_REQUEST_ID,
96+
)
97+
})
98+
99+
test('works without requestId', async () => {
100+
const movedTask = {
101+
...DEFAULT_TASK,
102+
id: TASK_ID,
103+
projectId: PROJECT_ID,
104+
url: getTaskUrl(TASK_ID, DEFAULT_TASK.content),
105+
}
106+
const requestMock = setupRestClientMock(movedTask)
107+
const api = getTarget()
108+
109+
await api.moveTask(TASK_ID, { projectId: PROJECT_ID })
110+
111+
expect(requestMock).toHaveBeenCalledTimes(1)
112+
expect(requestMock).toHaveBeenCalledWith(
113+
'POST',
114+
getSyncBaseUri(),
115+
`${ENDPOINT_REST_TASKS}/${TASK_ID}/${ENDPOINT_REST_TASK_MOVE}`,
116+
DEFAULT_AUTH_TOKEN,
117+
{ project_id: PROJECT_ID },
118+
undefined,
119+
)
120+
})
121+
})

src/TodoistApi.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
ENDPOINT_SYNC_QUICK_ADD,
6161
ENDPOINT_REST_TASK_CLOSE,
6262
ENDPOINT_REST_TASK_REOPEN,
63+
ENDPOINT_REST_TASK_MOVE,
6364
ENDPOINT_REST_LABELS,
6465
ENDPOINT_REST_PROJECT_COLLABORATORS,
6566
ENDPOINT_REST_SECTIONS,
@@ -369,6 +370,7 @@ export class TodoistApi {
369370
* @param args - The paramets that should contain only one of projectId, sectionId, or parentId
370371
* @param requestId - Optional custom identifier for the request.
371372
* @returns - A promise that resolves to an array of the updated tasks.
373+
* @deprecated Use `moveTask` for single task operations. This method uses the Sync API and may be removed in a future version.
372374
*/
373375
async moveTasks(ids: string[], args: MoveTaskArgs, requestId?: string): Promise<Task[]> {
374376
if (ids.length > MAX_COMMAND_COUNT) {
@@ -420,6 +422,32 @@ export class TodoistApi {
420422
return validateTaskArray(syncTasks)
421423
}
422424

425+
/**
426+
* Moves a task by its ID to either a different parent/section/project.
427+
*
428+
* @param id - The unique identifier of the task to be moved.
429+
* @param args - The parameters that should contain exactly one of projectId, sectionId, or parentId
430+
* @param requestId - Optional custom identifier for the request.
431+
* @returns A promise that resolves to the updated task.
432+
*/
433+
async moveTask(id: string, args: MoveTaskArgs, requestId?: string): Promise<Task> {
434+
z.string().parse(id)
435+
const response = await request<Task>(
436+
'POST',
437+
this.syncApiBase,
438+
generatePath(ENDPOINT_REST_TASKS, id, ENDPOINT_REST_TASK_MOVE),
439+
this.authToken,
440+
{
441+
...(args.projectId && { project_id: args.projectId }),
442+
...(args.sectionId && { section_id: args.sectionId }),
443+
...(args.parentId && { parent_id: args.parentId }),
444+
},
445+
requestId,
446+
)
447+
448+
return validateTask(response.data)
449+
}
450+
423451
/**
424452
* Closes (completes) a task by its ID.
425453
*

src/consts/endpoints.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const ENDPOINT_REST_LABELS_SHARED_REMOVE = ENDPOINT_REST_LABELS_SHARED +
3232
export const ENDPOINT_REST_COMMENTS = 'comments'
3333
export const ENDPOINT_REST_TASK_CLOSE = 'close'
3434
export const ENDPOINT_REST_TASK_REOPEN = 'reopen'
35+
export const ENDPOINT_REST_TASK_MOVE = 'move'
3536
export const ENDPOINT_REST_PROJECTS = 'projects'
3637
export const ENDPOINT_REST_PROJECTS_ARCHIVED = ENDPOINT_REST_PROJECTS + '/archived'
3738
export const ENDPOINT_REST_PROJECT_COLLABORATORS = 'collaborators'

0 commit comments

Comments
 (0)