@md2do/todoist 0.2.0
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +597 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/client.ts.html +694 -0
- package/coverage/coverage-final.json +4 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +146 -0
- package/coverage/index.ts.html +118 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/client.ts.html +694 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/index.ts.html +118 -0
- package/coverage/lcov-report/mapper.ts.html +760 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +523 -0
- package/coverage/mapper.ts.html +760 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/index.d.mts +121 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +318 -0
- package/dist/index.mjs +285 -0
- package/package.json +50 -0
- package/src/client.ts +203 -0
- package/src/index.ts +11 -0
- package/src/mapper.ts +225 -0
- package/tests/mapper.test.ts +283 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +25 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { TodoistApi } from '@doist/todoist-api-typescript';
|
|
2
|
+
import type { Task, Project, Label } from '@doist/todoist-api-typescript';
|
|
3
|
+
import type { TodoistTaskParams } from './mapper.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for Todoist client
|
|
7
|
+
*/
|
|
8
|
+
export interface TodoistClientConfig {
|
|
9
|
+
apiToken: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wrapper around Todoist API with error handling and convenience methods
|
|
14
|
+
*/
|
|
15
|
+
export class TodoistClient {
|
|
16
|
+
private api: TodoistApi;
|
|
17
|
+
|
|
18
|
+
constructor(config: TodoistClientConfig) {
|
|
19
|
+
this.api = new TodoistApi(config.apiToken);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get all active tasks
|
|
24
|
+
*/
|
|
25
|
+
async getTasks(options?: {
|
|
26
|
+
projectId?: string;
|
|
27
|
+
labelId?: string;
|
|
28
|
+
}): Promise<Task[]> {
|
|
29
|
+
try {
|
|
30
|
+
return await this.api.getTasks(options);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Failed to get tasks: ${error instanceof Error ? error.message : String(error)}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get a specific task by ID
|
|
40
|
+
*/
|
|
41
|
+
async getTask(taskId: string): Promise<Task> {
|
|
42
|
+
try {
|
|
43
|
+
return await this.api.getTask(taskId);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Failed to get task ${taskId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a new task
|
|
53
|
+
*/
|
|
54
|
+
async createTask(
|
|
55
|
+
params: TodoistTaskParams | Record<string, unknown>,
|
|
56
|
+
): Promise<Task> {
|
|
57
|
+
try {
|
|
58
|
+
return await this.api.addTask(params as TodoistTaskParams);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Failed to create task: ${error instanceof Error ? error.message : String(error)}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Update an existing task
|
|
68
|
+
*/
|
|
69
|
+
async updateTask(
|
|
70
|
+
taskId: string,
|
|
71
|
+
params: Partial<TodoistTaskParams>,
|
|
72
|
+
): Promise<Task> {
|
|
73
|
+
try {
|
|
74
|
+
return await this.api.updateTask(taskId, params);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Failed to update task ${taskId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Complete a task
|
|
84
|
+
*/
|
|
85
|
+
async completeTask(taskId: string): Promise<boolean> {
|
|
86
|
+
try {
|
|
87
|
+
return await this.api.closeTask(taskId);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Failed to complete task ${taskId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reopen a completed task
|
|
97
|
+
*/
|
|
98
|
+
async reopenTask(taskId: string): Promise<boolean> {
|
|
99
|
+
try {
|
|
100
|
+
return await this.api.reopenTask(taskId);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Failed to reopen task ${taskId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Delete a task
|
|
110
|
+
*/
|
|
111
|
+
async deleteTask(taskId: string): Promise<boolean> {
|
|
112
|
+
try {
|
|
113
|
+
return await this.api.deleteTask(taskId);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Failed to delete task ${taskId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get all projects
|
|
123
|
+
*/
|
|
124
|
+
async getProjects(): Promise<Project[]> {
|
|
125
|
+
try {
|
|
126
|
+
return await this.api.getProjects();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Failed to get projects: ${error instanceof Error ? error.message : String(error)}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get a specific project by ID
|
|
136
|
+
*/
|
|
137
|
+
async getProject(projectId: string): Promise<Project> {
|
|
138
|
+
try {
|
|
139
|
+
return await this.api.getProject(projectId);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Failed to get project ${projectId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Find project by name
|
|
149
|
+
*/
|
|
150
|
+
async findProjectByName(name: string): Promise<Project | null> {
|
|
151
|
+
const projects = await this.getProjects();
|
|
152
|
+
return (
|
|
153
|
+
projects.find((p) => p.name.toLowerCase() === name.toLowerCase()) ?? null
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get all labels
|
|
159
|
+
*/
|
|
160
|
+
async getLabels(): Promise<Label[]> {
|
|
161
|
+
try {
|
|
162
|
+
return await this.api.getLabels();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Failed to get labels: ${error instanceof Error ? error.message : String(error)}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a label if it doesn't exist
|
|
172
|
+
*/
|
|
173
|
+
async ensureLabel(name: string): Promise<Label> {
|
|
174
|
+
const labels = await this.getLabels();
|
|
175
|
+
const existing = labels.find(
|
|
176
|
+
(l) => l.name.toLowerCase() === name.toLowerCase(),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (existing) {
|
|
180
|
+
return existing;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
return await this.api.addLabel({ name });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Failed to create label ${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Test the API connection
|
|
194
|
+
*/
|
|
195
|
+
async test(): Promise<boolean> {
|
|
196
|
+
try {
|
|
197
|
+
await this.getProjects();
|
|
198
|
+
return true;
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { TodoistClient } from './client.js';
|
|
2
|
+
export type { TodoistClientConfig } from './client.js';
|
|
3
|
+
export {
|
|
4
|
+
md2doToTodoistPriority,
|
|
5
|
+
todoistToMd2doPriority,
|
|
6
|
+
extractTaskContent,
|
|
7
|
+
formatTaskContent,
|
|
8
|
+
md2doToTodoist,
|
|
9
|
+
todoistToMd2do,
|
|
10
|
+
} from './mapper.js';
|
|
11
|
+
export type { TodoistTaskParams, Md2doTaskUpdate } from './mapper.js';
|
package/src/mapper.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Task } from '@md2do/core';
|
|
2
|
+
import type { Task as TodoistTask } from '@doist/todoist-api-typescript';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Priority mapping between md2do and Todoist
|
|
6
|
+
* md2do: urgent (!!!) / high (!!) / normal (!) / low (none)
|
|
7
|
+
* Todoist: 4 / 3 / 2 / 1
|
|
8
|
+
*/
|
|
9
|
+
export function md2doToTodoistPriority(priority?: string): number {
|
|
10
|
+
switch (priority) {
|
|
11
|
+
case 'urgent':
|
|
12
|
+
return 4;
|
|
13
|
+
case 'high':
|
|
14
|
+
return 3;
|
|
15
|
+
case 'normal':
|
|
16
|
+
return 2;
|
|
17
|
+
case 'low':
|
|
18
|
+
default:
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function todoistToMd2doPriority(priority: number): string | undefined {
|
|
24
|
+
switch (priority) {
|
|
25
|
+
case 4:
|
|
26
|
+
return 'urgent';
|
|
27
|
+
case 3:
|
|
28
|
+
return 'high';
|
|
29
|
+
case 2:
|
|
30
|
+
return 'normal';
|
|
31
|
+
case 1:
|
|
32
|
+
return 'low';
|
|
33
|
+
default:
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract task content without metadata for Todoist
|
|
40
|
+
* Removes: assignee, priority markers, tags, dates, todoist ID
|
|
41
|
+
*/
|
|
42
|
+
export function extractTaskContent(text: string): string {
|
|
43
|
+
return (
|
|
44
|
+
text
|
|
45
|
+
// Remove assignee @username
|
|
46
|
+
.replace(/@\w+/g, '')
|
|
47
|
+
// Remove priority markers
|
|
48
|
+
.replace(/!+/g, '')
|
|
49
|
+
// Remove tags
|
|
50
|
+
.replace(/#\w+/g, '')
|
|
51
|
+
// Remove dates in parentheses
|
|
52
|
+
.replace(/\(\d{4}-\d{2}-\d{2}\)/g, '')
|
|
53
|
+
// Remove Todoist ID
|
|
54
|
+
.replace(/\[todoist:\s*\d+\]/gi, '')
|
|
55
|
+
// Clean up extra whitespace
|
|
56
|
+
.replace(/\s+/g, ' ')
|
|
57
|
+
.trim()
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format Todoist task content for markdown
|
|
63
|
+
* Adds back assignee, priority, tags, and due date
|
|
64
|
+
*/
|
|
65
|
+
export function formatTaskContent(
|
|
66
|
+
content: string,
|
|
67
|
+
options: {
|
|
68
|
+
assignee?: string;
|
|
69
|
+
priority?: string;
|
|
70
|
+
tags?: string[];
|
|
71
|
+
due?: Date;
|
|
72
|
+
todoistId?: string;
|
|
73
|
+
},
|
|
74
|
+
): string {
|
|
75
|
+
let result = content;
|
|
76
|
+
|
|
77
|
+
// Add assignee
|
|
78
|
+
if (options.assignee) {
|
|
79
|
+
result += ` @${options.assignee}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Add priority markers
|
|
83
|
+
if (options.priority === 'urgent') {
|
|
84
|
+
result += ' !!!';
|
|
85
|
+
} else if (options.priority === 'high') {
|
|
86
|
+
result += ' !!';
|
|
87
|
+
} else if (options.priority === 'normal') {
|
|
88
|
+
result += ' !';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Add tags
|
|
92
|
+
if (options.tags && options.tags.length > 0) {
|
|
93
|
+
result += ' ' + options.tags.map((tag) => `#${tag}`).join(' ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add due date
|
|
97
|
+
if (options.due) {
|
|
98
|
+
// Format date in UTC to avoid timezone issues
|
|
99
|
+
const year = options.due.getUTCFullYear();
|
|
100
|
+
const month = String(options.due.getUTCMonth() + 1).padStart(2, '0');
|
|
101
|
+
const day = String(options.due.getUTCDate()).padStart(2, '0');
|
|
102
|
+
result += ` (${year}-${month}-${day})`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add Todoist ID
|
|
106
|
+
if (options.todoistId) {
|
|
107
|
+
result += ` [todoist:${options.todoistId}]`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Convert md2do task to Todoist task creation parameters
|
|
115
|
+
*/
|
|
116
|
+
export interface TodoistTaskParams {
|
|
117
|
+
content: string;
|
|
118
|
+
priority: number;
|
|
119
|
+
labels?: string[];
|
|
120
|
+
due_date?: string;
|
|
121
|
+
due_string?: string;
|
|
122
|
+
project_id?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function md2doToTodoist(
|
|
126
|
+
task: Task,
|
|
127
|
+
projectId?: string,
|
|
128
|
+
): TodoistTaskParams {
|
|
129
|
+
const params: TodoistTaskParams = {
|
|
130
|
+
content: extractTaskContent(task.text),
|
|
131
|
+
priority: md2doToTodoistPriority(task.priority),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Add labels from tags
|
|
135
|
+
if (task.tags && task.tags.length > 0) {
|
|
136
|
+
params.labels = task.tags;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add due date
|
|
140
|
+
if (task.dueDate) {
|
|
141
|
+
// Format date in UTC to avoid timezone issues
|
|
142
|
+
const year = task.dueDate.getUTCFullYear();
|
|
143
|
+
const month = String(task.dueDate.getUTCMonth() + 1).padStart(2, '0');
|
|
144
|
+
const day = String(task.dueDate.getUTCDate()).padStart(2, '0');
|
|
145
|
+
params.due_date = `${year}-${month}-${day}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add project ID
|
|
149
|
+
if (projectId) {
|
|
150
|
+
params.project_id = projectId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return params;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Convert Todoist task to md2do task update data
|
|
158
|
+
*/
|
|
159
|
+
export interface Md2doTaskUpdate {
|
|
160
|
+
text: string;
|
|
161
|
+
completed: boolean;
|
|
162
|
+
todoistId: string;
|
|
163
|
+
priority?: string;
|
|
164
|
+
tags?: string[];
|
|
165
|
+
due?: Date;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function todoistToMd2do(
|
|
169
|
+
todoistTask: TodoistTask,
|
|
170
|
+
assignee?: string,
|
|
171
|
+
): Md2doTaskUpdate {
|
|
172
|
+
const priority = todoistToMd2doPriority(todoistTask.priority);
|
|
173
|
+
// Parse date string as UTC to avoid timezone issues
|
|
174
|
+
const due = todoistTask.due?.date
|
|
175
|
+
? new Date(`${todoistTask.due.date}T00:00:00.000Z`)
|
|
176
|
+
: undefined;
|
|
177
|
+
|
|
178
|
+
// Build format options with only defined values
|
|
179
|
+
const formatOptions: {
|
|
180
|
+
assignee?: string;
|
|
181
|
+
priority?: string;
|
|
182
|
+
tags?: string[];
|
|
183
|
+
due?: Date;
|
|
184
|
+
todoistId?: string;
|
|
185
|
+
} = {
|
|
186
|
+
todoistId: todoistTask.id,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (assignee !== undefined) {
|
|
190
|
+
formatOptions.assignee = assignee;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (priority !== undefined) {
|
|
194
|
+
formatOptions.priority = priority;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (todoistTask.labels.length > 0) {
|
|
198
|
+
formatOptions.tags = todoistTask.labels;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (due !== undefined) {
|
|
202
|
+
formatOptions.due = due;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const update: Md2doTaskUpdate = {
|
|
206
|
+
text: formatTaskContent(todoistTask.content, formatOptions),
|
|
207
|
+
completed: todoistTask.isCompleted ?? false,
|
|
208
|
+
todoistId: todoistTask.id,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Add optional properties only if they have values
|
|
212
|
+
if (priority !== undefined) {
|
|
213
|
+
update.priority = priority;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (todoistTask.labels.length > 0) {
|
|
217
|
+
update.tags = todoistTask.labels;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (due !== undefined) {
|
|
221
|
+
update.due = due;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return update;
|
|
225
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
md2doToTodoistPriority,
|
|
4
|
+
todoistToMd2doPriority,
|
|
5
|
+
extractTaskContent,
|
|
6
|
+
formatTaskContent,
|
|
7
|
+
md2doToTodoist,
|
|
8
|
+
todoistToMd2do,
|
|
9
|
+
} from '../src/mapper.js';
|
|
10
|
+
import type { Task } from '@md2do/core';
|
|
11
|
+
import type { Task as TodoistTask } from '@doist/todoist-api-typescript';
|
|
12
|
+
|
|
13
|
+
describe('md2doToTodoistPriority', () => {
|
|
14
|
+
it('should map urgent to 4', () => {
|
|
15
|
+
expect(md2doToTodoistPriority('urgent')).toBe(4);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should map high to 3', () => {
|
|
19
|
+
expect(md2doToTodoistPriority('high')).toBe(3);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should map normal to 2', () => {
|
|
23
|
+
expect(md2doToTodoistPriority('normal')).toBe(2);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should map low to 1', () => {
|
|
27
|
+
expect(md2doToTodoistPriority('low')).toBe(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should default to 1 for undefined', () => {
|
|
31
|
+
expect(md2doToTodoistPriority(undefined)).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('todoistToMd2doPriority', () => {
|
|
36
|
+
it('should map 4 to urgent', () => {
|
|
37
|
+
expect(todoistToMd2doPriority(4)).toBe('urgent');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should map 3 to high', () => {
|
|
41
|
+
expect(todoistToMd2doPriority(3)).toBe('high');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should map 2 to normal', () => {
|
|
45
|
+
expect(todoistToMd2doPriority(2)).toBe('normal');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should map 1 to low', () => {
|
|
49
|
+
expect(todoistToMd2doPriority(1)).toBe('low');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return undefined for invalid priority', () => {
|
|
53
|
+
expect(todoistToMd2doPriority(0)).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('extractTaskContent', () => {
|
|
58
|
+
it('should remove assignee', () => {
|
|
59
|
+
const text = 'Fix bug @nick';
|
|
60
|
+
expect(extractTaskContent(text)).toBe('Fix bug');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should remove priority markers', () => {
|
|
64
|
+
const text = 'Fix bug !!!';
|
|
65
|
+
expect(extractTaskContent(text)).toBe('Fix bug');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should remove tags', () => {
|
|
69
|
+
const text = 'Fix bug #backend #urgent';
|
|
70
|
+
expect(extractTaskContent(text)).toBe('Fix bug');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should remove dates', () => {
|
|
74
|
+
const text = 'Fix bug (2026-01-20)';
|
|
75
|
+
expect(extractTaskContent(text)).toBe('Fix bug');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should remove Todoist ID', () => {
|
|
79
|
+
const text = 'Fix bug [todoist:123456]';
|
|
80
|
+
expect(extractTaskContent(text)).toBe('Fix bug');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should remove all metadata', () => {
|
|
84
|
+
const text = 'Fix bug @nick !!! #backend (2026-01-20) [todoist:123456]';
|
|
85
|
+
expect(extractTaskContent(text)).toBe('Fix bug');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle multiple spaces', () => {
|
|
89
|
+
const text = 'Fix bug @nick !!! #backend';
|
|
90
|
+
expect(extractTaskContent(text)).toBe('Fix bug');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('formatTaskContent', () => {
|
|
95
|
+
it('should format content with assignee', () => {
|
|
96
|
+
const result = formatTaskContent('Fix bug', { assignee: 'nick' });
|
|
97
|
+
expect(result).toBe('Fix bug @nick');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should format content with urgent priority', () => {
|
|
101
|
+
const result = formatTaskContent('Fix bug', { priority: 'urgent' });
|
|
102
|
+
expect(result).toBe('Fix bug !!!');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should format content with high priority', () => {
|
|
106
|
+
const result = formatTaskContent('Fix bug', { priority: 'high' });
|
|
107
|
+
expect(result).toBe('Fix bug !!');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should format content with normal priority', () => {
|
|
111
|
+
const result = formatTaskContent('Fix bug', { priority: 'normal' });
|
|
112
|
+
expect(result).toBe('Fix bug !');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should format content with tags', () => {
|
|
116
|
+
const result = formatTaskContent('Fix bug', {
|
|
117
|
+
tags: ['backend', 'urgent'],
|
|
118
|
+
});
|
|
119
|
+
expect(result).toBe('Fix bug #backend #urgent');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should format content with due date', () => {
|
|
123
|
+
const result = formatTaskContent('Fix bug', {
|
|
124
|
+
due: new Date('2026-01-20T00:00:00.000Z'),
|
|
125
|
+
});
|
|
126
|
+
expect(result).toBe('Fix bug (2026-01-20)');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should format content with Todoist ID', () => {
|
|
130
|
+
const result = formatTaskContent('Fix bug', { todoistId: '123456' });
|
|
131
|
+
expect(result).toBe('Fix bug [todoist:123456]');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should format content with all metadata', () => {
|
|
135
|
+
const result = formatTaskContent('Fix bug', {
|
|
136
|
+
assignee: 'nick',
|
|
137
|
+
priority: 'urgent',
|
|
138
|
+
tags: ['backend'],
|
|
139
|
+
due: new Date('2026-01-20T00:00:00.000Z'),
|
|
140
|
+
todoistId: '123456',
|
|
141
|
+
});
|
|
142
|
+
expect(result).toBe(
|
|
143
|
+
'Fix bug @nick !!! #backend (2026-01-20) [todoist:123456]',
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('md2doToTodoist', () => {
|
|
149
|
+
it('should convert md2do task to Todoist params', () => {
|
|
150
|
+
const task: Task = {
|
|
151
|
+
id: 'test-id',
|
|
152
|
+
text: 'Fix bug @nick !!! #backend (2026-01-20)',
|
|
153
|
+
completed: false,
|
|
154
|
+
file: 'test.md',
|
|
155
|
+
line: 1,
|
|
156
|
+
tags: ['backend'],
|
|
157
|
+
assignee: 'nick',
|
|
158
|
+
priority: 'urgent',
|
|
159
|
+
dueDate: new Date('2026-01-20T00:00:00.000Z'),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const params = md2doToTodoist(task);
|
|
163
|
+
|
|
164
|
+
expect(params).toEqual({
|
|
165
|
+
content: 'Fix bug',
|
|
166
|
+
priority: 4,
|
|
167
|
+
labels: ['backend'],
|
|
168
|
+
due_date: '2026-01-20',
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should include project ID if provided', () => {
|
|
173
|
+
const task: Task = {
|
|
174
|
+
id: 'test-id',
|
|
175
|
+
text: 'Fix bug',
|
|
176
|
+
completed: false,
|
|
177
|
+
file: 'test.md',
|
|
178
|
+
line: 1,
|
|
179
|
+
tags: [],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const params = md2doToTodoist(task, 'project-123');
|
|
183
|
+
|
|
184
|
+
expect(params.project_id).toBe('project-123');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle task without optional fields', () => {
|
|
188
|
+
const task: Task = {
|
|
189
|
+
id: 'test-id',
|
|
190
|
+
text: 'Fix bug',
|
|
191
|
+
completed: false,
|
|
192
|
+
file: 'test.md',
|
|
193
|
+
line: 1,
|
|
194
|
+
tags: [],
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const params = md2doToTodoist(task);
|
|
198
|
+
|
|
199
|
+
expect(params).toEqual({
|
|
200
|
+
content: 'Fix bug',
|
|
201
|
+
priority: 1,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('todoistToMd2do', () => {
|
|
207
|
+
it('should convert Todoist task to md2do update', () => {
|
|
208
|
+
const todoistTask: TodoistTask = {
|
|
209
|
+
id: '123456',
|
|
210
|
+
order: 1,
|
|
211
|
+
content: 'Fix bug',
|
|
212
|
+
description: '',
|
|
213
|
+
priority: 4,
|
|
214
|
+
labels: ['backend'],
|
|
215
|
+
due: { date: '2026-01-20', isRecurring: false, string: '2026-01-20' },
|
|
216
|
+
isCompleted: false,
|
|
217
|
+
createdAt: '2026-01-18T10:00:00Z',
|
|
218
|
+
creatorId: 'user-id',
|
|
219
|
+
projectId: 'project-id',
|
|
220
|
+
commentCount: 0,
|
|
221
|
+
url: 'https://todoist.com/app/task/123456',
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const update = todoistToMd2do(todoistTask, 'nick');
|
|
225
|
+
|
|
226
|
+
expect(update).toEqual({
|
|
227
|
+
text: 'Fix bug @nick !!! #backend (2026-01-20) [todoist:123456]',
|
|
228
|
+
completed: false,
|
|
229
|
+
priority: 'urgent',
|
|
230
|
+
tags: ['backend'],
|
|
231
|
+
due: new Date('2026-01-20T00:00:00.000Z'),
|
|
232
|
+
todoistId: '123456',
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle completed task', () => {
|
|
237
|
+
const todoistTask: TodoistTask = {
|
|
238
|
+
id: '123456',
|
|
239
|
+
order: 1,
|
|
240
|
+
content: 'Fix bug',
|
|
241
|
+
description: '',
|
|
242
|
+
priority: 1,
|
|
243
|
+
labels: [],
|
|
244
|
+
isCompleted: true,
|
|
245
|
+
createdAt: '2026-01-18T10:00:00Z',
|
|
246
|
+
creatorId: 'user-id',
|
|
247
|
+
projectId: 'project-id',
|
|
248
|
+
commentCount: 0,
|
|
249
|
+
url: 'https://todoist.com/app/task/123456',
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const update = todoistToMd2do(todoistTask);
|
|
253
|
+
|
|
254
|
+
expect(update.completed).toBe(true);
|
|
255
|
+
expect(update.text).toBe('Fix bug [todoist:123456]');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should handle task without optional fields', () => {
|
|
259
|
+
const todoistTask: TodoistTask = {
|
|
260
|
+
id: '123456',
|
|
261
|
+
order: 1,
|
|
262
|
+
content: 'Fix bug',
|
|
263
|
+
description: '',
|
|
264
|
+
priority: 1,
|
|
265
|
+
labels: [],
|
|
266
|
+
isCompleted: false,
|
|
267
|
+
createdAt: '2026-01-18T10:00:00Z',
|
|
268
|
+
creatorId: 'user-id',
|
|
269
|
+
projectId: 'project-id',
|
|
270
|
+
commentCount: 0,
|
|
271
|
+
url: 'https://todoist.com/app/task/123456',
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const update = todoistToMd2do(todoistTask);
|
|
275
|
+
|
|
276
|
+
expect(update).toEqual({
|
|
277
|
+
text: 'Fix bug [todoist:123456]',
|
|
278
|
+
completed: false,
|
|
279
|
+
priority: 'low',
|
|
280
|
+
todoistId: '123456',
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|