|
class Todoist { |
|
static API_TOKEN() { |
|
return '<YOUR API TOKEN HERE>'; |
|
} |
|
|
|
kebab(string) { |
|
return string |
|
.replace(/([a-z])([A-Z]+)/g, (_, s1, s2) => `${s1} ${s2}`) |
|
.replace( |
|
/([A-Z])([A-Z]+)([^a-zA-Z0-9]*)$/, |
|
(_, s1, s2, s3) => `${s1} ${s2.toLowerCase()} ${s3}`, |
|
) |
|
.replace( |
|
/([A-Z]+)([A-Z][a-z])/g, |
|
(_, s1, s2) => `${s1.toLowerCase()} ${s2}`, |
|
) |
|
.replace(/\W+/g, ' ') |
|
.replace(/_/g, '-') |
|
.split(/ |\B(?=[A-Z])/) |
|
.map((word) => word.toLowerCase()) |
|
.join('-'); |
|
} |
|
|
|
simpleLocalDate(date) { |
|
const parts = date.slice(0, 10).split('-'); |
|
const localDate = new Date(parts[0], parts[1] - 1, parts[2]); |
|
localDate.setHours(0, 0, 0, 0); |
|
return localDate; |
|
} |
|
|
|
async getProjects() { |
|
const url = 'https://api.todoist.com/rest/v2/projects'; |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
|
|
return data.reduce((acc, project) => { |
|
acc[project.id] = project; |
|
acc[project.id].tasks = {}; |
|
return acc; |
|
}, {}); |
|
} |
|
|
|
async getLabels() { |
|
const url = 'https://api.todoist.com/rest/v2/labels'; |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
|
|
return data.reduce((acc, label) => { |
|
acc[label.name] = label; |
|
return acc; |
|
}, {}); |
|
} |
|
|
|
async getPreviousDueDatesForTask(taskID) { |
|
const url = |
|
'https://api.todoist.com/sync/v9/activity/get?' + |
|
new URLSearchParams({ |
|
object_id: taskID, |
|
object_type: 'item', |
|
event_type: 'updated', |
|
limit: 100, |
|
}); |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
|
|
return data.events.reduce((acc, event) => { |
|
if (event.extra_data.last_due_date !== undefined) { |
|
const date = new Date(event.extra_data.last_due_date); |
|
date.setHours(0, 0, 0, 0); |
|
acc.push(date); |
|
} |
|
return acc; |
|
}, []); |
|
} |
|
|
|
async getItemInfo(taskID) { |
|
const url = |
|
'https://api.todoist.com/sync/v9/items/get?' + |
|
new URLSearchParams({ |
|
item_id: taskID, |
|
all_data: false, |
|
}); |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
return data.item; |
|
} |
|
|
|
async getActiveTasks(date) { |
|
if (date === undefined) { |
|
date = new Date().toJSON().slice(0, 10); |
|
} |
|
|
|
const url = |
|
'https://api.todoist.com/rest/v2/tasks?' + |
|
new URLSearchParams({ |
|
filter: `(created before: ${date} | created: ${date}) & !no date`, |
|
}); |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
const givenDate = this.simpleLocalDate(date); |
|
const givenDateString = givenDate.toUTCString(); |
|
|
|
return await data.reduce(async (acc, task) => { |
|
const dueDates = await this.getPreviousDueDatesForTask(task.id); |
|
const dueDate = this.simpleLocalDate(task.due.date); |
|
dueDates.unshift(dueDate); |
|
|
|
const index = dueDates.findIndex( |
|
(date) => date.toUTCString() === givenDateString, |
|
); |
|
|
|
if (index > -1) { |
|
task.postponed = dueDates.length - index - 1; |
|
(await acc).push(task); |
|
} |
|
return acc; |
|
}, []); |
|
} |
|
|
|
async getCompletedTasks(date) { |
|
if (date === undefined) { |
|
date = new Date().toJSON().slice(0, 10); |
|
} |
|
const givenDate = this.simpleLocalDate(date); |
|
const until = this.simpleLocalDate(date); |
|
until.setHours(23, 59); |
|
|
|
const url = |
|
'https://api.todoist.com/sync/v9/completed/get_all?' + |
|
new URLSearchParams({ |
|
since: givenDate.toJSON(), |
|
until: until.toJSON(), |
|
limit: 200, |
|
}); |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
|
|
return await Promise.all( |
|
data.items.map(async (completedTask) => { |
|
return await this.getItemInfo(completedTask.task_id); |
|
}), |
|
); |
|
} |
|
|
|
async getTasksByProject(date) { |
|
if (date === undefined) { |
|
date = new Date().toJSON().slice(0, 10); |
|
} |
|
|
|
const active_tasks = await this.getActiveTasks(date); |
|
const completed_tasks = await this.getCompletedTasks(date); |
|
const projects = await this.getProjects(); |
|
|
|
active_tasks.forEach((task) => { |
|
projects[task.project_id].tasks[task.id] = task; |
|
}); |
|
|
|
completed_tasks.forEach((task) => { |
|
projects[task.project_id].tasks[task.id] = task; |
|
}); |
|
|
|
return Object.values(projects).sort((a, b) => (a.name > b.name ? 1 : -1)); |
|
} |
|
|
|
async getTaskMarkdown(date) { |
|
if (date === undefined) { |
|
date = new Date().toJSON().slice(0, 10); |
|
} |
|
const labels = await this.getLabels(); |
|
const projects = await this.getTasksByProject(date); |
|
|
|
return projects.reduce((acc, project) => { |
|
const tasks = Object.values(project.tasks).sort((a, b) => { |
|
if (a.content > b.content) { |
|
return 1; |
|
} |
|
if (a.content < b.content) { |
|
return -1; |
|
} |
|
if ( |
|
a.due !== undefined && |
|
a.due !== null && |
|
b.due !== undefined && |
|
b.due !== null |
|
) { |
|
return a.due.date > b.due.date ? 1 : -1; |
|
} |
|
return 0; |
|
}); |
|
|
|
if (tasks.length === 0) { |
|
return acc; |
|
} |
|
|
|
acc += `> [!todoist-${this.kebab(project.color)}-${this.kebab( |
|
project.name, |
|
)}]+ ${project.name}\n`; |
|
|
|
tasks.forEach((task) => { |
|
if (task.is_completed === false) { |
|
acc += `> - [ ]`; |
|
} else { |
|
acc += `> - [x]`; |
|
} |
|
acc += ` ${task.content}\n`; |
|
|
|
let extraParts = []; |
|
|
|
if (task.due !== undefined && task.due !== null) { |
|
let due = ''; |
|
|
|
if (task.due.datetime !== undefined) { |
|
const dueDate = new Date(task.due.datetime); |
|
due = dueDate.toLocaleTimeString([], { |
|
hour: '2-digit', |
|
minute: '2-digit', |
|
}); |
|
} else if (task.due.date !== undefined && task.due.date.length > 10) { |
|
const dueDate = new Date(task.due.date); |
|
due = dueDate.toLocaleTimeString([], { |
|
hour: '2-digit', |
|
minute: '2-digit', |
|
}); |
|
} |
|
|
|
if (due.length > 0 || task.due.is_recurring === true) { |
|
extraParts.push( |
|
`<span class="todoist-task-date${ |
|
task.due.is_recurring === true ? ' todoist-task-recurring' : '' |
|
}">${due}</span>`, |
|
); |
|
} |
|
} |
|
|
|
if (task.labels !== undefined && task.labels.length > 0) { |
|
extraParts = extraParts.concat( |
|
task.labels.map( |
|
(label) => |
|
`<span class="todoist-${this.kebab( |
|
labels[label].color, |
|
)}">${label}</span>`, |
|
), |
|
); |
|
} |
|
|
|
if (task.postponed !== undefined && task.postponed > 0) { |
|
extraParts.push( |
|
`<span class="todoist-postponed">${task.postponed}</span>`, |
|
); |
|
} |
|
|
|
if (extraParts.length > 0) { |
|
acc += `${extraParts |
|
.map((part) => { |
|
return `> - ${part}\n`; |
|
}) |
|
.join('')}`; |
|
} |
|
}); |
|
|
|
acc += '\n'; |
|
|
|
return acc; |
|
}, ''); |
|
} |
|
|
|
async Callout(dv) { |
|
const filename = dv.current().file.name; |
|
const markdown = await this.getTaskMarkdown(filename); |
|
dv.span(markdown); |
|
} |
|
} |
You need to create a folder in your vault and point
CustomJS
at it. Then put the script in that folder. If you have aTemplater
scripts folder, this should not be the same folder asCustomJS
requires a different format thanTemplater
.