From f3a2f31d000955a23fe6cbae49be9fc8f0c92195 Mon Sep 17 00:00:00 2001 From: Ramon Felciano <407700+felciano@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:45:12 +0000 Subject: [PATCH 01/11] Add date filtering option for Todoist project syncs - Add projectDateFilter setting with choices: all, today, overdue|today, 7 days - Default to 'overdue | today' to focus on actionable tasks - Fix URL encoding in filter construction - Update README with documentation Co-Authored-By: Claude Opus 4.5 --- dbludeau.TodoistNoteplanSync/README.md | 20 +++++++++- dbludeau.TodoistNoteplanSync/plugin.json | 19 ++++++++++ .../src/NPPluginMain.js | 37 +++++++++++++++++-- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/dbludeau.TodoistNoteplanSync/README.md b/dbludeau.TodoistNoteplanSync/README.md index 9c2360832..281a11545 100644 --- a/dbludeau.TodoistNoteplanSync/README.md +++ b/dbludeau.TodoistNoteplanSync/README.md @@ -29,8 +29,26 @@ NOTE: All sync actions (other then content and status) can be turned on and off ## Configuration - This plug in requires an API token from Todoist. These are available on both the free and paid plans. To get the token follow the instructions [here](https://todoist.com/help/articles/find-your-api-token) - You can configure a folder to use for syncing everything, headings that tasks will fall under and what details are synced. -- Sections in Todoist will become headers in Noteplan. See [here](https://todoist.com/help/articles/introduction-to-sections) to learn about sections in Todoist. +- Sections in Todoist will become headers in Noteplan. See [here](https://todoist.com/help/articles/introduction-to-sections) to learn about sections in Todoist. - Currently the API token is required, everything else is optional. + +### Project Date Filter +By default, project sync commands only fetch tasks that are **overdue or due today**. This keeps your notes focused on actionable items. You can change this behavior in settings: + +| Filter Option | Description | +|---------------|-------------| +| `all` | Sync all tasks regardless of due date | +| `today` | Only tasks due today | +| `overdue \| today` | Tasks that are overdue or due today (default) | +| `7 days` | Tasks due within the next 7 days | + +This setting affects the following commands: +- `/todoist sync project` +- `/todoist sync all linked projects` +- `/todoist sync all linked projects and today` (project portion only) +- `/todoist sync everything` + +Note: The `/todoist sync today` command always filters by today regardless of this setting. - To link a Todoist list to a Noteplan note, you need the list ID from Todoist. To get the ID, open www.todoist.com in a web browser and sign in so you can see your lists. Open the list you want to link to a Noteplan note. The list ID is at the end of the URL. For example, if the end of the Todoist.com URL is /app/project/2317353827, then you want the list ID of 2317353827. You would add frontmatter to the top of your note that would look like (see https://help.noteplan.co/article/136-templates for more information on frontmatter): ``` --- diff --git a/dbludeau.TodoistNoteplanSync/plugin.json b/dbludeau.TodoistNoteplanSync/plugin.json index f2c251953..fc30048f7 100644 --- a/dbludeau.TodoistNoteplanSync/plugin.json +++ b/dbludeau.TodoistNoteplanSync/plugin.json @@ -205,6 +205,25 @@ "description": "By default the sync will pull only tasks assigned to you. If you want to sync all unassigned tasks as well, check this box.", "default": false }, + { + "note": "================== PROJECT SYNC SETTINGS ========================" + }, + { + "type": "heading", + "title": "Project Sync Settings" + }, + { + "type": "separator" + }, + { + "type": "string", + "key": "projectDateFilter", + "title": "Date filter for project syncs", + "description": "Filter which tasks are synced based on due date. Choose 'all' to sync everything.", + "choices": ["all", "today", "overdue | today", "7 days"], + "default": "overdue | today", + "required": false + }, { "note": "================== DEBUGGING SETTINGS ========================" }, diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index 2b0c03724..01981364d 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -46,6 +46,7 @@ const setup: { teamAccount: boolean, addUnassigned: boolean, header: string, + projectDateFilter: string, newFolder: any, newToken: any, useTeamAccount: any, @@ -54,6 +55,7 @@ const setup: { syncTags: any, syncUnassigned: any, newHeader: any, + newProjectDateFilter: any, } = { token: '', folder: 'Todoist', @@ -63,6 +65,7 @@ const setup: { teamAccount: false, addUnassigned: false, header: '', + projectDateFilter: 'overdue | today', /** * @param {string} passedToken @@ -115,6 +118,12 @@ const setup: { set newHeader(passedHeader: string) { setup.header = passedHeader }, + /** + * @param {string} passedProjectDateFilter + */ + set newProjectDateFilter(passedProjectDateFilter: string) { + setup.projectDateFilter = passedProjectDateFilter + }, } const closed: Array = [] @@ -387,15 +396,31 @@ async function projectSync(note: TNote, id: string): Promise { */ async function pullTodoistTasksByProject(project_id: string): Promise { if (project_id !== '') { - let filter = '' + const filterParts: Array = [] + + // Add date filter based on setting (skip if 'all') + if (setup.projectDateFilter && setup.projectDateFilter !== 'all') { + filterParts.push(setup.projectDateFilter) + } + + // Add team account filter if applicable if (setup.useTeamAccount) { if (setup.addUnassigned) { - filter = '& filter=!assigned to: others' + filterParts.push('!assigned to: others') } else { - filter = '& filter=assigned to: me' + filterParts.push('assigned to: me') } } - const result = await fetch(`${todo_api}/tasks?project_id=${project_id}${filter}`, getRequestObject()) + + // Build the URL with proper encoding + let url = `${todo_api}/tasks?project_id=${project_id}` + if (filterParts.length > 0) { + const filterString = filterParts.join(' & ') + url = `${url}&filter=${encodeURIComponent(filterString)}` + } + + logDebug(pluginJson, `Fetching tasks from URL: ${url}`) + const result = await fetch(url, getRequestObject()) return result } return null @@ -556,6 +581,10 @@ function setSettings() { if ('headerToUse' in settings && settings.headerToUse !== '') { setup.newHeader = settings.headerToUse } + + if ('projectDateFilter' in settings && settings.projectDateFilter !== '') { + setup.newProjectDateFilter = settings.projectDateFilter + } } } From b2b070d481c4787adae86652b3af96b9041baed3 Mon Sep 17 00:00:00 2001 From: Ramon Felciano <407700+felciano@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:57:25 +0000 Subject: [PATCH 02/11] Add command-line date filter arguments to sync project - Support /todoist sync project today - Support /todoist sync project overdue - Support /todoist sync project current (today+overdue) Co-Authored-By: Claude Opus 4.5 --- dbludeau.TodoistNoteplanSync/README.md | 5 +- dbludeau.TodoistNoteplanSync/plugin.json | 4 +- .../src/NPPluginMain.js | 55 +++++++++++++++---- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/dbludeau.TodoistNoteplanSync/README.md b/dbludeau.TodoistNoteplanSync/README.md index 281a11545..a50115ed3 100644 --- a/dbludeau.TodoistNoteplanSync/README.md +++ b/dbludeau.TodoistNoteplanSync/README.md @@ -22,7 +22,10 @@ NOTE: All sync actions (other then content and status) can be turned on and off ## Available Commands - **/todoist sync everything** (alias **/tosa**): sync everything in Todoist to a folder in Noteplan. Every list in todoist will become a note in Noteplan. Use this if you want to use Todoist just as a conduit to get tasks into Noteplan. The folder used in Noteplan can be configured in settings. - **/todoist sync today** (alias **/tost**): sync tasks due today from Todoist to your daily note in Noteplan. A header can be configured in settings. -- **/todoist sync project** (alias **/tosp**): link a single list from Todoist to a note in Note plan using frontmatter. This command will sync the current project you have open. +- **/todoist sync project** (alias **/tosp**): link a single list from Todoist to a note in Note plan using frontmatter. This command will sync the current project you have open. You can optionally add a date filter argument: + - `/todoist sync project today` - only tasks due today + - `/todoist sync project overdue` - only overdue tasks + - `/todoist sync project current` - overdue + today (same as default setting) - **/todoist sync all projects** (alias **/tosa**): this will sync all projects that have been linked using frontmatter. - **/todoist sync all projects and today** (alias **/tosat** **/toast**): this will sync all projects and the today note. Running it as one comand instead of individually will check for duplicates. This command will sync all tasks from projects to their linked note, including tasks due today. It will sync all tasks from all projects in Todoist that are due today except for those already in the project notes to avoid duplication. diff --git a/dbludeau.TodoistNoteplanSync/plugin.json b/dbludeau.TodoistNoteplanSync/plugin.json index fc30048f7..6cee76e2d 100644 --- a/dbludeau.TodoistNoteplanSync/plugin.json +++ b/dbludeau.TodoistNoteplanSync/plugin.json @@ -47,10 +47,10 @@ "alias": [ "tosp" ], - "description": "Sync Todoist project (list) linked to the current Noteplan note using frontmatter", + "description": "Sync Todoist project. Optional: add 'today', 'overdue', or 'current' (today+overdue) to override date filter", "jsFunction": "syncProject", "arguments": [ - "" + "Date filter override (optional): today, overdue, or current" ] }, { diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index 01981364d..61b0bb340 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -183,7 +183,7 @@ export async function syncEverything() { // grab the tasks and write them out with sections const id: string = projects[i].project_id - await projectSync(note, id) + await projectSync(note, id, null) } } @@ -197,14 +197,42 @@ export async function syncEverything() { logDebug(pluginJson, 'Plugin completed without errors') } +/** + * Parse the date filter argument from command line + * + * @param {string} arg - the argument passed to the command + * @returns {string | null} - the filter string or null if no override + */ +function parseDateFilterArg(arg: ?string): ?string { + if (!arg || arg.trim() === '') { + return null + } + const trimmed = arg.trim().toLowerCase() + if (trimmed === 'today') { + return 'today' + } else if (trimmed === 'overdue') { + return 'overdue' + } else if (trimmed === 'current') { + return 'overdue | today' + } + logWarn(pluginJson, `Unknown date filter argument: ${arg}. Using setting value.`) + return null +} + /** * Synchronize the current linked project. * + * @param {string} filterArg - optional date filter override (today, overdue, current) * @returns {Promise} A promise that resolves once synchronization is complete */ // eslint-disable-next-line require-await -export async function syncProject() { +export async function syncProject(filterArg: ?string) { setSettings() + const filterOverride = parseDateFilterArg(filterArg) + if (filterOverride) { + logInfo(pluginJson, `Using date filter override: ${filterOverride}`) + } + const note: ?TNote = Editor.note if (note) { // check to see if this has any frontmatter @@ -222,7 +250,7 @@ export async function syncProject() { }) } - await projectSync(note, frontmatter.todoist_id) + await projectSync(note, frontmatter.todoist_id, filterOverride) //close the tasks in Todoist if they are complete in Noteplan` closed.forEach(async (t) => { @@ -297,7 +325,7 @@ async function syncThemAll() { id = id.trim() logInfo(pluginJson, `Matches up to Todoist project id: ${id}`) - await projectSync(note, id) + await projectSync(note, id, null) //close the tasks in Todoist if they are complete in Noteplan` closed.forEach(async (t) => { @@ -377,13 +405,14 @@ async function syncTodayTasks() { * * @param {TNote} note - note that will be written to * @param {string} id - Todoist project ID + * @param {string} filterOverride - optional date filter override * @returns {Promise} */ -async function projectSync(note: TNote, id: string): Promise { - const task_result = await pullTodoistTasksByProject(id) +async function projectSync(note: TNote, id: string, filterOverride: ?string): Promise { + const task_result = await pullTodoistTasksByProject(id, filterOverride) const tasks: Array = JSON.parse(task_result) - - tasks.results.forEach(async (t) => { + + tasks.results.forEach(async (t) => { await writeOutTask(note, t) }) } @@ -392,15 +421,17 @@ async function projectSync(note: TNote, id: string): Promise { * Pull todoist tasks from list matching the ID provided * * @param {string} project_id - the id of the Todoist project + * @param {string} filterOverride - optional date filter override (bypasses setting) * @returns {Promise} - promise that resolves into array of task objects or null */ -async function pullTodoistTasksByProject(project_id: string): Promise { +async function pullTodoistTasksByProject(project_id: string, filterOverride: ?string): Promise { if (project_id !== '') { const filterParts: Array = [] - // Add date filter based on setting (skip if 'all') - if (setup.projectDateFilter && setup.projectDateFilter !== 'all') { - filterParts.push(setup.projectDateFilter) + // Add date filter: use override if provided, otherwise use setting + const dateFilter = filterOverride ?? setup.projectDateFilter + if (dateFilter && dateFilter !== 'all') { + filterParts.push(dateFilter) } // Add team account filter if applicable From ed1062d8b25218c4b4baa00223e3f671dfb64cd3 Mon Sep 17 00:00:00 2001 From: Ramon Felciano <407700+felciano@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:01:27 +0000 Subject: [PATCH 03/11] Add separate commands for date filter options - /todoist sync project today (alias: tospt) - /todoist sync project overdue (alias: tospo) - /todoist sync project current (alias: tospc) Co-Authored-By: Claude Opus 4.5 --- dbludeau.TodoistNoteplanSync/plugin.json | 33 ++++++++++++++++--- .../src/NPPluginMain.js | 24 ++++++++++++++ dbludeau.TodoistNoteplanSync/src/index.js | 2 +- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/dbludeau.TodoistNoteplanSync/plugin.json b/dbludeau.TodoistNoteplanSync/plugin.json index 6cee76e2d..b052b69d6 100644 --- a/dbludeau.TodoistNoteplanSync/plugin.json +++ b/dbludeau.TodoistNoteplanSync/plugin.json @@ -47,11 +47,36 @@ "alias": [ "tosp" ], - "description": "Sync Todoist project. Optional: add 'today', 'overdue', or 'current' (today+overdue) to override date filter", + "description": "Sync Todoist project (uses date filter from settings)", "jsFunction": "syncProject", - "arguments": [ - "Date filter override (optional): today, overdue, or current" - ] + "arguments": [] + }, + { + "name": "todoist sync project today", + "alias": [ + "tospt" + ], + "description": "Sync Todoist project - only tasks due today", + "jsFunction": "syncProjectToday", + "arguments": [] + }, + { + "name": "todoist sync project overdue", + "alias": [ + "tospo" + ], + "description": "Sync Todoist project - only overdue tasks", + "jsFunction": "syncProjectOverdue", + "arguments": [] + }, + { + "name": "todoist sync project current", + "alias": [ + "tospc" + ], + "description": "Sync Todoist project - overdue + today tasks", + "jsFunction": "syncProjectCurrent", + "arguments": [] }, { "name": "todoist sync all linked projects", diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index 61b0bb340..5f6209a75 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -268,6 +268,30 @@ export async function syncProject(filterArg: ?string) { } } +/** + * Sync project with 'today' filter + * @returns {Promise} + */ +export async function syncProjectToday(): Promise { + await syncProject('today') +} + +/** + * Sync project with 'overdue' filter + * @returns {Promise} + */ +export async function syncProjectOverdue(): Promise { + await syncProject('overdue') +} + +/** + * Sync project with 'current' (overdue | today) filter + * @returns {Promise} + */ +export async function syncProjectCurrent(): Promise { + await syncProject('current') +} + /** * Syncronize all linked projects. * diff --git a/dbludeau.TodoistNoteplanSync/src/index.js b/dbludeau.TodoistNoteplanSync/src/index.js index 461e45b94..a22b02479 100644 --- a/dbludeau.TodoistNoteplanSync/src/index.js +++ b/dbludeau.TodoistNoteplanSync/src/index.js @@ -15,7 +15,7 @@ // So you need to add a line below for each function that you want NP to have access to. // Typically, listed below are only the top-level plug-in functions listed in plugin.json -export { syncToday, syncEverything, syncProject, syncAllProjects, syncAllProjectsAndToday } from './NPPluginMain' +export { syncToday, syncEverything, syncProject, syncProjectToday, syncProjectOverdue, syncProjectCurrent, syncAllProjects, syncAllProjectsAndToday } from './NPPluginMain' // FETCH mocking for offline testing // If you want to use external server calls in your plugin, it can be useful to mock the server responses From 256f53e6dc4876234a610e640621a61dc21d6fa0 Mon Sep 17 00:00:00 2001 From: Ramon Felciano <407700+felciano@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:09:14 +0000 Subject: [PATCH 04/11] Auto-create headings if they don't exist - Add ensureHeadingExists helper function - Create section headings from Todoist automatically - Create default header from settings automatically Co-Authored-By: Claude Opus 4.5 --- .../src/NPPluginMain.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index 5f6209a75..59fc9176d 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -31,6 +31,7 @@ import { getFrontmatterAttributes } from '../../helpers/NPFrontMatter' import { getTodaysDateAsArrowDate, getTodaysDateUnhyphenated } from '../../helpers/dateTime' +import { findHeading } from '../../helpers/paragraph' import pluginJson from '../plugin.json' import { log, logInfo, logDebug, logError, logWarn, clo, JSP } from '@helpers/dev' @@ -643,6 +644,20 @@ function setSettings() { } } +/** + * Ensure a heading exists in the note, creating it if necessary + * + * @param {TNote} note - the note to check/modify + * @param {string} headingName - the heading to ensure exists + */ +function ensureHeadingExists(note: TNote, headingName: string): void { + const existingHeading = findHeading(note, headingName) + if (!existingHeading) { + logInfo(pluginJson, `Creating heading: ${headingName}`) + note.appendParagraph(`### ${headingName}`, 'text') + } +} + /** * Format and write task to correct noteplan note * @@ -659,6 +674,7 @@ async function writeOutTask(note: TNote, task: Object) { section = JSON.parse(section) if (section) { if (!existing.includes(task.id) && !just_written.includes(task.id)) { + ensureHeadingExists(note, section.name) logInfo(pluginJson, `1. Task will be added to ${note.title} below ${section.name} (${formatted})`) note.addTodoBelowHeadingTitle(formatted, section.name, true, true) @@ -686,6 +702,7 @@ async function writeOutTask(note: TNote, task: Object) { // if there is a predefined header in settings if (setup.header !== '') { if (!existing.includes(task.id) && !just_written.includes(task.id)) { + ensureHeadingExists(note, setup.header) logInfo(pluginJson, `3. Task will be added to ${note.title} below ${setup.header} (${formatted})`) note.addTodoBelowHeadingTitle(formatted, setup.header, true, true) From a5daa470a8e466080f69048456257dfbf1a8f250 Mon Sep 17 00:00:00 2001 From: Ramon Felciano <407700+felciano@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:12:03 +0000 Subject: [PATCH 05/11] Fix async issue and add client-side date filtering - Use for...of instead of forEach for proper async/await - Add filterTasksByDate() for client-side filtering - Todoist API ignores filter param when project_id is specified Co-Authored-By: Claude Opus 4.5 --- .../src/NPPluginMain.js | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index 59fc9176d..c2f7553d0 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -425,6 +425,49 @@ async function syncTodayTasks() { } } +/** + * Filter tasks by date based on the filter setting + * Note: Todoist API ignores filter param when project_id is specified, so we filter client-side + * + * @param {Array} tasks - array of task objects from Todoist + * @param {string} dateFilter - the date filter to apply (today, overdue, overdue | today, 7 days, all) + * @returns {Array} - filtered tasks + */ +function filterTasksByDate(tasks: Array, dateFilter: ?string): Array { + if (!dateFilter || dateFilter === 'all') { + return tasks + } + + const today = new Date() + today.setHours(0, 0, 0, 0) + + const sevenDaysFromNow = new Date(today) + sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7) + + return tasks.filter((task) => { + if (!task.due || !task.due.date) { + // Tasks without due dates: only include if filter is 'all' + return false + } + + const dueDate = new Date(task.due.date) + dueDate.setHours(0, 0, 0, 0) + + switch (dateFilter) { + case 'today': + return dueDate.getTime() === today.getTime() + case 'overdue': + return dueDate.getTime() < today.getTime() + case 'overdue | today': + return dueDate.getTime() <= today.getTime() + case '7 days': + return dueDate.getTime() <= sevenDaysFromNow.getTime() + default: + return true + } + }) +} + /** * Get Todoist project tasks and write them out one by one * @@ -437,9 +480,17 @@ async function projectSync(note: TNote, id: string, filterOverride: ?string): Pr const task_result = await pullTodoistTasksByProject(id, filterOverride) const tasks: Array = JSON.parse(task_result) - tasks.results.forEach(async (t) => { + // Determine which filter to use + const dateFilter = filterOverride ?? setup.projectDateFilter + + // Filter tasks client-side (Todoist API ignores filter when project_id is specified) + const filteredTasks = filterTasksByDate(tasks.results || [], dateFilter) + logInfo(pluginJson, `Filtered ${tasks.results?.length || 0} tasks to ${filteredTasks.length} based on filter: ${dateFilter}`) + + // Use for...of to properly await each task write + for (const t of filteredTasks) { await writeOutTask(note, t) - }) + } } /** From 10cb3b35ae0d46b3d3d8a8bcbdd791c823cf7c01 Mon Sep 17 00:00:00 2001 From: Ramon Felciano <407700+felciano@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:15:13 +0000 Subject: [PATCH 06/11] Fix heading creation - append task directly after new heading - Replace ensureHeadingExists with addTaskBelowHeading - When heading doesn't exist, append both heading and task - Use appendTodo after creating heading instead of addTodoBelowHeadingTitle Co-Authored-By: Claude Opus 4.5 --- .../src/NPPluginMain.js | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index c2f7553d0..6f7de24c5 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -696,16 +696,22 @@ function setSettings() { } /** - * Ensure a heading exists in the note, creating it if necessary + * Add a task below a heading, creating the heading if it doesn't exist * - * @param {TNote} note - the note to check/modify - * @param {string} headingName - the heading to ensure exists + * @param {TNote} note - the note to modify + * @param {string} headingName - the heading to add the task below + * @param {string} taskContent - the formatted task content */ -function ensureHeadingExists(note: TNote, headingName: string): void { +function addTaskBelowHeading(note: TNote, headingName: string, taskContent: string): void { const existingHeading = findHeading(note, headingName) - if (!existingHeading) { + if (existingHeading) { + // Heading exists, use the standard method + note.addTodoBelowHeadingTitle(taskContent, headingName, true, true) + } else { + // Heading doesn't exist - append heading and task directly logInfo(pluginJson, `Creating heading: ${headingName}`) note.appendParagraph(`### ${headingName}`, 'text') + note.appendTodo(taskContent) } } @@ -717,7 +723,6 @@ function ensureHeadingExists(note: TNote, headingName: string): void { */ async function writeOutTask(note: TNote, task: Object) { if (note) { - //console.log(note.content) logDebug(pluginJson, task) const formatted = formatTaskDetails(task) if (task.section_id !== null) { @@ -725,24 +730,18 @@ async function writeOutTask(note: TNote, task: Object) { section = JSON.parse(section) if (section) { if (!existing.includes(task.id) && !just_written.includes(task.id)) { - ensureHeadingExists(note, section.name) logInfo(pluginJson, `1. Task will be added to ${note.title} below ${section.name} (${formatted})`) - note.addTodoBelowHeadingTitle(formatted, section.name, true, true) - - // add to just_written so they do not get duplicated in the Today note when updating all projects and today + addTaskBelowHeading(note, section.name, formatted) just_written.push(task.id) } else { logInfo(pluginJson, `Task is already in Noteplan ${task.id}`) } } else { // this one has a section ID but Todoist will not return a name - // Put it in with no heading logWarn(pluginJson, `Section ID ${task.section_id} did not return a section name`) if (!existing.includes(task.id) && !just_written.includes(task.id)) { logInfo(pluginJson, `2. Task will be added to ${note.title} (${formatted})`) note.appendTodo(formatted) - - // add to just_written so they do not get duplicated in the Today note when updating all projects and today just_written.push(task.id) } else { logInfo(pluginJson, `Task is already in Noteplan (${formatted})`) @@ -750,22 +749,16 @@ async function writeOutTask(note: TNote, task: Object) { } } else { // check for a default heading - // if there is a predefined header in settings if (setup.header !== '') { if (!existing.includes(task.id) && !just_written.includes(task.id)) { - ensureHeadingExists(note, setup.header) logInfo(pluginJson, `3. Task will be added to ${note.title} below ${setup.header} (${formatted})`) - note.addTodoBelowHeadingTitle(formatted, setup.header, true, true) - - // add to just_written so they do not get duplicated in the Today note when updating all projects and today + addTaskBelowHeading(note, setup.header, formatted) just_written.push(task.id) } } else { if (!existing.includes(task.id) && !just_written.includes(task.id)) { logInfo(pluginJson, `4. Task will be added to ${note.title} (${formatted})`) note.appendTodo(formatted) - - // add to just_written so they do not get duplicated in the Today note when updating all projects and today just_written.push(task.id) } } From 6b5f4a3d3e2b00ef45dc999de8167272d05a7663 Mon Sep 17 00:00:00 2001 From: Ramon Felciano <407700+felciano@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:18:35 +0000 Subject: [PATCH 07/11] Use Editor methods for the currently open note - Add isEditorNote flag throughout the call chain - Use Editor.appendParagraph instead of note methods for current note - Fixes tasks not appearing when syncing the open note Co-Authored-By: Claude Opus 4.5 --- .../src/NPPluginMain.js | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index 6f7de24c5..a3ba7363c 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -251,7 +251,7 @@ export async function syncProject(filterArg: ?string) { }) } - await projectSync(note, frontmatter.todoist_id, filterOverride) + await projectSync(note, frontmatter.todoist_id, filterOverride, true) //close the tasks in Todoist if they are complete in Noteplan` closed.forEach(async (t) => { @@ -474,9 +474,10 @@ function filterTasksByDate(tasks: Array, dateFilter: ?string): Array} */ -async function projectSync(note: TNote, id: string, filterOverride: ?string): Promise { +async function projectSync(note: TNote, id: string, filterOverride: ?string, isEditorNote: boolean = false): Promise { const task_result = await pullTodoistTasksByProject(id, filterOverride) const tasks: Array = JSON.parse(task_result) @@ -489,7 +490,7 @@ async function projectSync(note: TNote, id: string, filterOverride: ?string): Pr // Use for...of to properly await each task write for (const t of filteredTasks) { - await writeOutTask(note, t) + await writeOutTask(note, t, isEditorNote) } } @@ -697,21 +698,32 @@ function setSettings() { /** * Add a task below a heading, creating the heading if it doesn't exist + * Uses Editor methods for the currently open note for reliable updates * * @param {TNote} note - the note to modify * @param {string} headingName - the heading to add the task below * @param {string} taskContent - the formatted task content + * @param {boolean} isEditorNote - whether this is the currently open note in Editor */ -function addTaskBelowHeading(note: TNote, headingName: string, taskContent: string): void { +function addTaskBelowHeading(note: TNote, headingName: string, taskContent: string, isEditorNote: boolean = false): void { const existingHeading = findHeading(note, headingName) if (existingHeading) { // Heading exists, use the standard method - note.addTodoBelowHeadingTitle(taskContent, headingName, true, true) + if (isEditorNote) { + Editor.addTodoBelowHeadingTitle(taskContent, headingName, true, true) + } else { + note.addTodoBelowHeadingTitle(taskContent, headingName, true, true) + } } else { // Heading doesn't exist - append heading and task directly logInfo(pluginJson, `Creating heading: ${headingName}`) - note.appendParagraph(`### ${headingName}`, 'text') - note.appendTodo(taskContent) + if (isEditorNote) { + Editor.appendParagraph(`### ${headingName}`, 'text') + Editor.appendParagraph(`- [ ] ${taskContent}`, 'text') + } else { + note.appendParagraph(`### ${headingName}`, 'text') + note.appendTodo(taskContent) + } } } @@ -720,8 +732,9 @@ function addTaskBelowHeading(note: TNote, headingName: string, taskContent: stri * * @param {TNote} note - the note object that will get the task * @param {Object} task - the task object that will be written + * @param {boolean} isEditorNote - whether this is the currently open note in Editor */ -async function writeOutTask(note: TNote, task: Object) { +async function writeOutTask(note: TNote, task: Object, isEditorNote: boolean = false) { if (note) { logDebug(pluginJson, task) const formatted = formatTaskDetails(task) @@ -731,7 +744,7 @@ async function writeOutTask(note: TNote, task: Object) { if (section) { if (!existing.includes(task.id) && !just_written.includes(task.id)) { logInfo(pluginJson, `1. Task will be added to ${note.title} below ${section.name} (${formatted})`) - addTaskBelowHeading(note, section.name, formatted) + addTaskBelowHeading(note, section.name, formatted, isEditorNote) just_written.push(task.id) } else { logInfo(pluginJson, `Task is already in Noteplan ${task.id}`) @@ -741,7 +754,11 @@ async function writeOutTask(note: TNote, task: Object) { logWarn(pluginJson, `Section ID ${task.section_id} did not return a section name`) if (!existing.includes(task.id) && !just_written.includes(task.id)) { logInfo(pluginJson, `2. Task will be added to ${note.title} (${formatted})`) - note.appendTodo(formatted) + if (isEditorNote) { + Editor.appendParagraph(`- [ ] ${formatted}`, 'text') + } else { + note.appendTodo(formatted) + } just_written.push(task.id) } else { logInfo(pluginJson, `Task is already in Noteplan (${formatted})`) @@ -752,13 +769,17 @@ async function writeOutTask(note: TNote, task: Object) { if (setup.header !== '') { if (!existing.includes(task.id) && !just_written.includes(task.id)) { logInfo(pluginJson, `3. Task will be added to ${note.title} below ${setup.header} (${formatted})`) - addTaskBelowHeading(note, setup.header, formatted) + addTaskBelowHeading(note, setup.header, formatted, isEditorNote) just_written.push(task.id) } } else { if (!existing.includes(task.id) && !just_written.includes(task.id)) { logInfo(pluginJson, `4. Task will be added to ${note.title} (${formatted})`) - note.appendTodo(formatted) + if (isEditorNote) { + Editor.appendParagraph(`- [ ] ${formatted}`, 'text') + } else { + note.appendTodo(formatted) + } just_written.push(task.id) } } From bc660674d162065537a6000c3c382e12e7f4f7d7 Mon Sep 17 00:00:00 2001 From: Ramon Felciano <407700+felciano@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:33:02 +0000 Subject: [PATCH 08/11] Add todoist_filter frontmatter for per-note date filtering - Read todoist_filter from note frontmatter - Priority: command-line > frontmatter > settings - Valid values: all, today, overdue, current, 7 days - Updated README with documentation Co-Authored-By: Claude Opus 4.5 --- dbludeau.TodoistNoteplanSync/README.md | 22 ++++++++++++++++++- .../src/NPPluginMain.js | 15 ++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/dbludeau.TodoistNoteplanSync/README.md b/dbludeau.TodoistNoteplanSync/README.md index a50115ed3..2a89df388 100644 --- a/dbludeau.TodoistNoteplanSync/README.md +++ b/dbludeau.TodoistNoteplanSync/README.md @@ -52,13 +52,33 @@ This setting affects the following commands: - `/todoist sync everything` Note: The `/todoist sync today` command always filters by today regardless of this setting. -- To link a Todoist list to a Noteplan note, you need the list ID from Todoist. To get the ID, open www.todoist.com in a web browser and sign in so you can see your lists. Open the list you want to link to a Noteplan note. The list ID is at the end of the URL. For example, if the end of the Todoist.com URL is /app/project/2317353827, then you want the list ID of 2317353827. You would add frontmatter to the top of your note that would look like (see https://help.noteplan.co/article/136-templates for more information on frontmatter): + +### Linking a Todoist Project +To link a Todoist list to a Noteplan note, you need the list ID from Todoist. To get the ID, open www.todoist.com in a web browser and sign in so you can see your lists. Open the list you want to link to a Noteplan note. The list ID is at the end of the URL. For example, if the end of the Todoist.com URL is /app/project/2317353827, then you want the list ID of 2317353827. + +Add frontmatter to the top of your note (see https://help.noteplan.co/article/136-templates for more information on frontmatter): +``` +--- +todoist_id: 2317353827 +--- +``` + +### Per-Note Date Filter +You can override the default date filter for a specific note by adding `todoist_filter` to the frontmatter: ``` --- todoist_id: 2317353827 +todoist_filter: current --- ``` +Valid values for `todoist_filter`: `all`, `today`, `overdue`, `current` (same as overdue | today), `7 days` + +**Filter Priority:** +1. Command-line argument (e.g., `/todoist sync project today`) - highest +2. Frontmatter `todoist_filter` - second +3. Plugin settings "Date filter for project syncs" - default + ## Caveats, Warnings and Notes - All synced tasks in Noteplan rely on the Todoist ID being present and associated with the task. This is stored at the end of a synced task in the form of a link to www.todoist.com. - These links can be used to view the Todoist task on the web. diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index a3ba7363c..0d888e510 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -229,9 +229,9 @@ function parseDateFilterArg(arg: ?string): ?string { // eslint-disable-next-line require-await export async function syncProject(filterArg: ?string) { setSettings() - const filterOverride = parseDateFilterArg(filterArg) - if (filterOverride) { - logInfo(pluginJson, `Using date filter override: ${filterOverride}`) + const commandLineFilter = parseDateFilterArg(filterArg) + if (commandLineFilter) { + logInfo(pluginJson, `Using command-line filter override: ${commandLineFilter}`) } const note: ?TNote = Editor.note @@ -244,6 +244,15 @@ export async function syncProject(filterArg: ?string) { if ('todoist_id' in frontmatter) { logDebug(pluginJson, `Frontmatter has link to Todoist project -> ${frontmatter.todoist_id}`) + // Determine filter priority: command-line > frontmatter > settings + let filterOverride = commandLineFilter + if (!filterOverride && 'todoist_filter' in frontmatter && frontmatter.todoist_filter) { + filterOverride = parseDateFilterArg(frontmatter.todoist_filter) + if (filterOverride) { + logInfo(pluginJson, `Using frontmatter filter: ${filterOverride}`) + } + } + const paragraphs: ?$ReadOnlyArray = note.paragraphs if (paragraphs) { paragraphs.forEach((paragraph) => { From 040d6018938a17f4469d28c05f674c8d6d6e2fae Mon Sep 17 00:00:00 2001 From: Ramon Felciano <407700+felciano@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:33:58 +0000 Subject: [PATCH 09/11] Add '3 days' filter option Co-Authored-By: Claude Opus 4.5 --- dbludeau.TodoistNoteplanSync/README.md | 3 ++- dbludeau.TodoistNoteplanSync/plugin.json | 2 +- dbludeau.TodoistNoteplanSync/src/NPPluginMain.js | 7 ++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/dbludeau.TodoistNoteplanSync/README.md b/dbludeau.TodoistNoteplanSync/README.md index 2a89df388..2c43656eb 100644 --- a/dbludeau.TodoistNoteplanSync/README.md +++ b/dbludeau.TodoistNoteplanSync/README.md @@ -43,6 +43,7 @@ By default, project sync commands only fetch tasks that are **overdue or due tod | `all` | Sync all tasks regardless of due date | | `today` | Only tasks due today | | `overdue \| today` | Tasks that are overdue or due today (default) | +| `3 days` | Tasks due within the next 3 days | | `7 days` | Tasks due within the next 7 days | This setting affects the following commands: @@ -72,7 +73,7 @@ todoist_filter: current --- ``` -Valid values for `todoist_filter`: `all`, `today`, `overdue`, `current` (same as overdue | today), `7 days` +Valid values for `todoist_filter`: `all`, `today`, `overdue`, `current` (same as overdue | today), `3 days`, `7 days` **Filter Priority:** 1. Command-line argument (e.g., `/todoist sync project today`) - highest diff --git a/dbludeau.TodoistNoteplanSync/plugin.json b/dbludeau.TodoistNoteplanSync/plugin.json index b052b69d6..1914eb33e 100644 --- a/dbludeau.TodoistNoteplanSync/plugin.json +++ b/dbludeau.TodoistNoteplanSync/plugin.json @@ -245,7 +245,7 @@ "key": "projectDateFilter", "title": "Date filter for project syncs", "description": "Filter which tasks are synced based on due date. Choose 'all' to sync everything.", - "choices": ["all", "today", "overdue | today", "7 days"], + "choices": ["all", "today", "overdue | today", "3 days", "7 days"], "default": "overdue | today", "required": false }, diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index 0d888e510..166a4e136 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -439,7 +439,7 @@ async function syncTodayTasks() { * Note: Todoist API ignores filter param when project_id is specified, so we filter client-side * * @param {Array} tasks - array of task objects from Todoist - * @param {string} dateFilter - the date filter to apply (today, overdue, overdue | today, 7 days, all) + * @param {string} dateFilter - the date filter to apply (today, overdue, overdue | today, 3 days, 7 days, all) * @returns {Array} - filtered tasks */ function filterTasksByDate(tasks: Array, dateFilter: ?string): Array { @@ -450,6 +450,9 @@ function filterTasksByDate(tasks: Array, dateFilter: ?string): Array, dateFilter: ?string): Array Date: Sun, 25 Jan 2026 22:09:38 +0000 Subject: [PATCH 10/11] Remove heading creation code (moved to multi-project branch) Reverts heading-related commits that were incorrectly added to this branch: - Auto-create headings if they don't exist - Fix heading creation - Use Editor methods for the currently open note These features belong in feature/todoist-multi-project branch. Co-Authored-By: Claude Opus 4.5 --- .../src/NPPluginMain.js | 69 +++++-------------- 1 file changed, 19 insertions(+), 50 deletions(-) diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index 166a4e136..a916280ab 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -31,7 +31,6 @@ import { getFrontmatterAttributes } from '../../helpers/NPFrontMatter' import { getTodaysDateAsArrowDate, getTodaysDateUnhyphenated } from '../../helpers/dateTime' -import { findHeading } from '../../helpers/paragraph' import pluginJson from '../plugin.json' import { log, logInfo, logDebug, logError, logWarn, clo, JSP } from '@helpers/dev' @@ -260,7 +259,7 @@ export async function syncProject(filterArg: ?string) { }) } - await projectSync(note, frontmatter.todoist_id, filterOverride, true) + await projectSync(note, frontmatter.todoist_id, filterOverride) //close the tasks in Todoist if they are complete in Noteplan` closed.forEach(async (t) => { @@ -488,10 +487,9 @@ function filterTasksByDate(tasks: Array, dateFilter: ?string): Array} */ -async function projectSync(note: TNote, id: string, filterOverride: ?string, isEditorNote: boolean = false): Promise { +async function projectSync(note: TNote, id: string, filterOverride: ?string): Promise { const task_result = await pullTodoistTasksByProject(id, filterOverride) const tasks: Array = JSON.parse(task_result) @@ -504,7 +502,7 @@ async function projectSync(note: TNote, id: string, filterOverride: ?string, isE // Use for...of to properly await each task write for (const t of filteredTasks) { - await writeOutTask(note, t, isEditorNote) + await writeOutTask(note, t) } } @@ -710,46 +708,15 @@ function setSettings() { } } -/** - * Add a task below a heading, creating the heading if it doesn't exist - * Uses Editor methods for the currently open note for reliable updates - * - * @param {TNote} note - the note to modify - * @param {string} headingName - the heading to add the task below - * @param {string} taskContent - the formatted task content - * @param {boolean} isEditorNote - whether this is the currently open note in Editor - */ -function addTaskBelowHeading(note: TNote, headingName: string, taskContent: string, isEditorNote: boolean = false): void { - const existingHeading = findHeading(note, headingName) - if (existingHeading) { - // Heading exists, use the standard method - if (isEditorNote) { - Editor.addTodoBelowHeadingTitle(taskContent, headingName, true, true) - } else { - note.addTodoBelowHeadingTitle(taskContent, headingName, true, true) - } - } else { - // Heading doesn't exist - append heading and task directly - logInfo(pluginJson, `Creating heading: ${headingName}`) - if (isEditorNote) { - Editor.appendParagraph(`### ${headingName}`, 'text') - Editor.appendParagraph(`- [ ] ${taskContent}`, 'text') - } else { - note.appendParagraph(`### ${headingName}`, 'text') - note.appendTodo(taskContent) - } - } -} - /** * Format and write task to correct noteplan note * * @param {TNote} note - the note object that will get the task * @param {Object} task - the task object that will be written - * @param {boolean} isEditorNote - whether this is the currently open note in Editor */ -async function writeOutTask(note: TNote, task: Object, isEditorNote: boolean = false) { +async function writeOutTask(note: TNote, task: Object) { if (note) { + //console.log(note.content) logDebug(pluginJson, task) const formatted = formatTaskDetails(task) if (task.section_id !== null) { @@ -758,21 +725,22 @@ async function writeOutTask(note: TNote, task: Object, isEditorNote: boolean = f if (section) { if (!existing.includes(task.id) && !just_written.includes(task.id)) { logInfo(pluginJson, `1. Task will be added to ${note.title} below ${section.name} (${formatted})`) - addTaskBelowHeading(note, section.name, formatted, isEditorNote) + note.addTodoBelowHeadingTitle(formatted, section.name, true, true) + + // add to just_written so they do not get duplicated in the Today note when updating all projects and today just_written.push(task.id) } else { logInfo(pluginJson, `Task is already in Noteplan ${task.id}`) } } else { // this one has a section ID but Todoist will not return a name + // Put it in with no heading logWarn(pluginJson, `Section ID ${task.section_id} did not return a section name`) if (!existing.includes(task.id) && !just_written.includes(task.id)) { logInfo(pluginJson, `2. Task will be added to ${note.title} (${formatted})`) - if (isEditorNote) { - Editor.appendParagraph(`- [ ] ${formatted}`, 'text') - } else { - note.appendTodo(formatted) - } + note.appendTodo(formatted) + + // add to just_written so they do not get duplicated in the Today note when updating all projects and today just_written.push(task.id) } else { logInfo(pluginJson, `Task is already in Noteplan (${formatted})`) @@ -780,20 +748,21 @@ async function writeOutTask(note: TNote, task: Object, isEditorNote: boolean = f } } else { // check for a default heading + // if there is a predefined header in settings if (setup.header !== '') { if (!existing.includes(task.id) && !just_written.includes(task.id)) { logInfo(pluginJson, `3. Task will be added to ${note.title} below ${setup.header} (${formatted})`) - addTaskBelowHeading(note, setup.header, formatted, isEditorNote) + note.addTodoBelowHeadingTitle(formatted, setup.header, true, true) + + // add to just_written so they do not get duplicated in the Today note when updating all projects and today just_written.push(task.id) } } else { if (!existing.includes(task.id) && !just_written.includes(task.id)) { logInfo(pluginJson, `4. Task will be added to ${note.title} (${formatted})`) - if (isEditorNote) { - Editor.appendParagraph(`- [ ] ${formatted}`, 'text') - } else { - note.appendTodo(formatted) - } + note.appendTodo(formatted) + + // add to just_written so they do not get duplicated in the Today note when updating all projects and today just_written.push(task.id) } } From fe9f2b984421c44ec31ba5c173eae055be5e0e0f Mon Sep 17 00:00:00 2001 From: Ramon Felciano <407700+felciano@users.noreply.github.com> Date: Sun, 25 Jan 2026 22:16:02 +0000 Subject: [PATCH 11/11] Fix timezone issue in date filtering When parsing Todoist due dates, new Date('YYYY-MM-DD') interprets the date as UTC midnight, causing incorrect filtering for users in non-UTC timezones (e.g., Mountain Standard Time). Added parseLocalDate() helper that splits the ISO date string and creates a Date using local timezone (new Date(year, month, day)). Fixes issue where 'today' filter was returning tomorrow's tasks. Co-Authored-By: Claude Opus 4.5 --- .../src/NPPluginMain.js | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js index a916280ab..d6bcccd27 100644 --- a/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js +++ b/dbludeau.TodoistNoteplanSync/src/NPPluginMain.js @@ -433,6 +433,22 @@ async function syncTodayTasks() { } } +/** + * Parse an ISO date string (YYYY-MM-DD) into a local Date object at midnight. + * This avoids timezone issues that occur when using new Date('YYYY-MM-DD'), + * which interprets the date as UTC midnight rather than local midnight. + * + * @param {string} isoDateString - date string in YYYY-MM-DD format + * @returns {Date} - Date object at local midnight + */ +function parseLocalDate(isoDateString: string): Date { + const parts = isoDateString.split('-') + const year = parseInt(parts[0], 10) + const month = parseInt(parts[1], 10) - 1 // JavaScript months are 0-indexed + const day = parseInt(parts[2], 10) + return new Date(year, month, day, 0, 0, 0, 0) +} + /** * Filter tasks by date based on the filter setting * Note: Todoist API ignores filter param when project_id is specified, so we filter client-side @@ -461,8 +477,9 @@ function filterTasksByDate(tasks: Array, dateFilter: ?string): Array