Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/test-utils/test-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const DEFAULT_TASK: Task = {
noteCount: DEFAULT_NOTE_COUNT,
dayOrder: DEFAULT_ORDER,
isCollapsed: DEFAULT_IS_COLLAPSED,
isUncompletable: false,
url: DEFAULT_TASK_URL,
}

Expand Down Expand Up @@ -127,6 +128,7 @@ export const TASK_WITH_OPTIONALS_AS_NULL: Task = {
description: DEFAULT_TASK_DESCRIPTION,
dayOrder: DEFAULT_ORDER,
isCollapsed: DEFAULT_IS_COLLAPSED,
isUncompletable: false,
noteCount: DEFAULT_NOTE_COUNT,
url: DEFAULT_TASK_URL,
}
Expand Down
174 changes: 174 additions & 0 deletions src/todoist-api.tasks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,81 @@

expect(task).toEqual(DEFAULT_TASK)
})

test('adds uncompletable prefix when isUncompletable is true', async () => {
const expectedTask = {
...DEFAULT_TASK,
content: '* This is an uncompletable task',
isUncompletable: true,
}

server.use(
http.post(`${getSyncBaseUri()}${ENDPOINT_REST_TASKS}`, async ({ request }) => {
const body = (await request.json()) as any

Check failure on line 68 in src/todoist-api.tasks.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
expect(body.content).toBe('* This is an uncompletable task')

Check failure on line 69 in src/todoist-api.tasks.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe member access .content on an `any` value
return HttpResponse.json(expectedTask, { status: 200 })
}),
)
const api = getTarget()

const task = await api.addTask({
content: 'This is an uncompletable task',
isUncompletable: true,
})

expect(task.content).toBe('* This is an uncompletable task')
expect(task.isUncompletable).toBe(true)
})

test('preserves existing prefix when isUncompletable is false', async () => {
const expectedTask = {
...DEFAULT_TASK,
content: '* Already has prefix',
isUncompletable: true,
}

server.use(
http.post(`${getSyncBaseUri()}${ENDPOINT_REST_TASKS}`, async ({ request }) => {
const body = (await request.json()) as any

Check failure on line 93 in src/todoist-api.tasks.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
expect(body.content).toBe('* Already has prefix')

Check failure on line 94 in src/todoist-api.tasks.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe member access .content on an `any` value
return HttpResponse.json(expectedTask, { status: 200 })
}),
)
const api = getTarget()

const task = await api.addTask({
content: '* Already has prefix',
isUncompletable: false,
})

expect(task.content).toBe('* Already has prefix')
expect(task.isUncompletable).toBe(true)
})

test('does not add prefix when isUncompletable is false', async () => {
const expectedTask = {
...DEFAULT_TASK,
content: 'Regular completable task',
isUncompletable: false,
}

server.use(
http.post(`${getSyncBaseUri()}${ENDPOINT_REST_TASKS}`, async ({ request }) => {
const body = (await request.json()) as any

Check failure on line 118 in src/todoist-api.tasks.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
expect(body.content).toBe('Regular completable task')

Check failure on line 119 in src/todoist-api.tasks.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe member access .content on an `any` value
return HttpResponse.json(expectedTask, { status: 200 })
}),
)
const api = getTarget()

const task = await api.addTask({
content: 'Regular completable task',
isUncompletable: false,
})

expect(task.content).toBe('Regular completable task')
expect(task.isUncompletable).toBe(false)
})
})

describe('updateTask', () => {
Expand All @@ -81,6 +156,55 @@

expect(response).toEqual(returnedTask)
})

test('processes content with isUncompletable when both are provided', async () => {
const returnedTask = {
...DEFAULT_TASK,
content: '* Updated uncompletable task',
isUncompletable: true,
url: getTaskUrl(DEFAULT_TASK_ID, '* Updated uncompletable task'),
}

server.use(
http.post(`${getSyncBaseUri()}${ENDPOINT_REST_TASKS}/123`, async ({ request }) => {
const body = (await request.json()) as any

Check failure on line 170 in src/todoist-api.tasks.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
expect(body.content).toBe('* Updated uncompletable task')

Check failure on line 171 in src/todoist-api.tasks.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe member access .content on an `any` value
return HttpResponse.json(returnedTask, { status: 200 })
}),
)
const api = getTarget()

const response = await api.updateTask('123', {
content: 'Updated uncompletable task',
isUncompletable: true,
})

expect(response.content).toBe('* Updated uncompletable task')
expect(response.isUncompletable).toBe(true)
})

test('does not process content when only isUncompletable is provided', async () => {
const returnedTask = {
...DEFAULT_TASK,
isUncompletable: false,
}

server.use(
http.post(`${getSyncBaseUri()}${ENDPOINT_REST_TASKS}/123`, async ({ request }) => {
const body = (await request.json()) as any

Check failure on line 194 in src/todoist-api.tasks.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
expect(body.content).toBeUndefined()

Check failure on line 195 in src/todoist-api.tasks.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe member access .content on an `any` value
expect(body.is_uncompletable).toBe(false) // Note: snake_case conversion
return HttpResponse.json(returnedTask, { status: 200 })
}),
)
const api = getTarget()

const response = await api.updateTask('123', {
isUncompletable: false,
})

expect(response.isUncompletable).toBe(false)
})
})

describe('closeTask', () => {
Expand Down Expand Up @@ -152,6 +276,56 @@
const task = await api.quickAddTask(DEFAULT_QUICK_ADD_ARGS)
expect(task).toEqual(DEFAULT_TASK)
})

test('adds uncompletable prefix when isUncompletable is true', async () => {
const expectedTask = {
...DEFAULT_TASK,
content: '* Quick uncompletable task',
isUncompletable: true,
}

server.use(
http.post(`${getSyncBaseUri()}${ENDPOINT_SYNC_QUICK_ADD}`, async ({ request }) => {
const body = (await request.json()) as any
expect(body.text).toBe('* Quick uncompletable task')
return HttpResponse.json(expectedTask, { status: 200 })
}),
)
const api = getTarget()

const task = await api.quickAddTask({
text: 'Quick uncompletable task',
isUncompletable: true,
})

expect(task.content).toBe('* Quick uncompletable task')
expect(task.isUncompletable).toBe(true)
})

test('preserves existing prefix even when isUncompletable is false', async () => {
const expectedTask = {
...DEFAULT_TASK,
content: '* Already prefixed quick task',
isUncompletable: true,
}

server.use(
http.post(`${getSyncBaseUri()}${ENDPOINT_SYNC_QUICK_ADD}`, async ({ request }) => {
const body = (await request.json()) as any
expect(body.text).toBe('* Already prefixed quick task')
return HttpResponse.json(expectedTask, { status: 200 })
}),
)
const api = getTarget()

const task = await api.quickAddTask({
text: '* Already prefixed quick task',
isUncompletable: false,
})

expect(task.content).toBe('* Already prefixed quick task')
expect(task.isUncompletable).toBe(true)
})
})

describe('getTask', () => {
Expand Down
26 changes: 23 additions & 3 deletions src/todoist-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ import {
import { formatDateToYYYYMMDD } from './utils/url-helpers'
import { uploadMultipartFile } from './utils/multipart-upload'
import { normalizeObjectTypeForApi, denormalizeObjectTypeFromApi } from './utils/activity-helpers'
import { processTaskContent } from './utils/uncompletable-helpers'
import { z } from 'zod'

import { v4 as uuidv4 } from 'uuid'
Expand Down Expand Up @@ -395,13 +396,19 @@ export class TodoistApi {
* @returns A promise that resolves to the created task.
*/
async addTask(args: AddTaskArgs, requestId?: string): Promise<Task> {
// Process content based on isUncompletable flag
const processedArgs = {
...args,
content: processTaskContent(args.content, args.isUncompletable),
}

const response = await request<Task>({
httpMethod: 'POST',
baseUri: this.syncApiBase,
relativePath: ENDPOINT_REST_TASKS,
apiToken: this.authToken,
customFetch: this.customFetch,
payload: args,
payload: processedArgs,
requestId: requestId,
})

Expand All @@ -415,13 +422,19 @@ export class TodoistApi {
* @returns A promise that resolves to the created task.
*/
async quickAddTask(args: QuickAddTaskArgs): Promise<Task> {
// Process text based on isUncompletable flag
const processedArgs = {
...args,
text: processTaskContent(args.text, args.isUncompletable),
}

const response = await request<Task>({
httpMethod: 'POST',
baseUri: this.syncApiBase,
relativePath: ENDPOINT_SYNC_QUICK_ADD,
apiToken: this.authToken,
customFetch: this.customFetch,
payload: args,
payload: processedArgs,
})

return validateTask(response.data)
Expand All @@ -437,13 +450,20 @@ export class TodoistApi {
*/
async updateTask(id: string, args: UpdateTaskArgs, requestId?: string): Promise<Task> {
z.string().parse(id)

// Process content if both content and isUncompletable are provided
const processedArgs =
args.content && args.isUncompletable !== undefined
? { ...args, content: processTaskContent(args.content, args.isUncompletable) }
: args

const response = await request<Task>({
httpMethod: 'POST',
baseUri: this.syncApiBase,
relativePath: generatePath(ENDPOINT_REST_TASKS, id),
apiToken: this.authToken,
customFetch: this.customFetch,
payload: args,
payload: processedArgs,
requestId: requestId,
})

Expand Down
5 changes: 5 additions & 0 deletions src/types/entities.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { getProjectUrl, getTaskUrl, getSectionUrl } from '../utils/url-helpers'
import { hasUncompletablePrefix } from '../utils/uncompletable-helpers'

export const DueDateSchema = z
.object({
Expand Down Expand Up @@ -63,10 +64,14 @@ export const TaskSchema = z
noteCount: z.number().int(),
dayOrder: z.number().int(),
isCollapsed: z.boolean(),
isUncompletable: z.boolean().default(false),
})
.transform((data) => {
// Auto-detect uncompletable status from content prefix
const isUncompletable = hasUncompletablePrefix(data.content)
return {
...data,
isUncompletable,
url: getTaskUrl(data.id, data.content),
}
})
Expand Down
3 changes: 3 additions & 0 deletions src/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type AddTaskArgs = {
dueLang?: string
deadlineLang?: string
deadlineDate?: string
isUncompletable?: boolean
} & RequireOneOrNone<{
dueDate?: string
dueDatetime?: string
Expand Down Expand Up @@ -141,6 +142,7 @@ export type UpdateTaskArgs = {
assigneeId?: string | null
deadlineDate?: string | null
deadlineLang?: string | null
isUncompletable?: boolean
} & RequireOneOrNone<{
dueDate?: string
dueDatetime?: string
Expand All @@ -160,6 +162,7 @@ export type QuickAddTaskArgs = {
reminder?: string
autoReminder?: boolean
meta?: boolean
isUncompletable?: boolean
}

/**
Expand Down
Loading