87 |
88 |
89 |
90 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | Draftist
120 | Draftist - a Drafts Action Group to integrate with Todoist.
121 | created by @FlohGro
122 |
127 | This repository contains the underlying functions and the documentation for the Draftist Action Group.
128 | You can install the Draftist Action Group from the Drafts directory: Draftist
129 | If you encounter any issues please open an issue in the repository or reach out to me in the Drafts Forums Post about Draftist or on Twitter ✌🏽
130 | Draftist Instructions
131 | To read through the instructions of Draftist look into the dedicated file in this repository: Draftist Instructions
132 | Support Development
133 | I developed these functions and the Action Group in my free time to help myself and you improve workflows and remove friction from processes. 🚀
134 | Draftist is completely free to use for you. However if this Action Group is useful for you and supports your workflows you can give something back to support development.
135 | I enjoy a good coffee ☕️ (weather at home or in an actual coffee shop) and love pizza 🍕.
136 | You can choose the amount you want to donate on those platforms.
137 | 
138 | 
139 | Changelog
140 | Draftist 1.0 - initial Release
141 | this is the initial version of Draftist
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/Draftist.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Draftist Action Group Functions
3 | * @author FlohGro
4 | * @copyright 2025, FlohGro
5 | * @licensing MIT free to use - but donate coffees to support development http://www.flohgro.com/donate
6 | * @version 2025.1
7 | */
8 |
9 | /**
10 | * Draftist_checkTodoistForError - This function checks the provided Todoist Object for errors.
11 | * If an error was detected it will be returned, otherwise undefined will be returned.
12 | * @param {Todoist_Object} todoist_obj the todoist object to check
13 | * @returns {error | undefined} a present error or undefined
14 | */
15 | function Draftist_getLastTodoistError(todoistObj) {
16 | let error = todoistObj.lastError
17 | if (error) {
18 | return error;
19 | } else {
20 | return undefined;
21 | }
22 | }
23 |
24 | /**
25 | * Draftist_succeedAction - notifies the user about a successful execution of an action
26 | *
27 | * @param {String} actionName the name of the action (might be empty if not displayed)
28 | * @param {Boolean} displayActionName bool if the provided name of the action shall be displayed or not
29 | * @param {String} successMessage the content for the success message
30 | */
31 | function Draftist_succeedAction(actionName, displayActionName, successMessage) {
32 | app.displaySuccessMessage((displayActionName ? actionName + " succeeded: " : "") + successMessage);
33 | }
34 |
35 | /**
36 | * Draftist_cancelAction - This function notifies the user which action was canceled and why.
37 | * @param {String} actionName the name of the actionName
38 | * @param {String} cancelReasonDescription description why the action was cancelled
39 | */
40 | function Draftist_cancelAction(actionName, cancelReasonDescription) {
41 | context.cancel(actionName + " was cancelled: " + cancelReasonDescription);
42 | app.displayWarningMessage(actionName + " was cancelled: " + cancelReasonDescription);
43 | }
44 |
45 | /**
46 | * Draftist_failAction - This function notifies the user when an action failed and why.
47 | * @param {String} actionName the name of the actionName
48 | * @param {String} failedReasonDescription description why the action failed
49 | */
50 | function Draftist_failAction(actionName, failedReasonDescription) {
51 | context.fail(actionName + " failed: " + failedReasonDescription);
52 | alert(actionName + " failed: " + failedReasonDescription);
53 | }
54 |
55 | /**
56 | * Draftist_infoMessage - displays a info message to the user prepended with "Draftist: "
57 | *
58 | * @param {String} actionName the name of the action (might be empty if not displayed)
59 | * @param {String} successMessage the content for the info message
60 | */
61 | function Draftist_infoMessage(actionName, infoMessage) {
62 | app.displayInfoMessage("Draftist: " + infoMessage + (actionName.length > 0 ? "(" + actionName + ")" : ""));
63 | }
64 |
65 | /**
66 | * Draftist_quickAdd - This function adds the provided string to Todoist using the quickAdd API.
67 | * This supports the Todoist natural language which will be processed by Todoist automatically.
68 | *
69 | * @param {Todoist_Object|undefined} todoist_obj - if already created, otherwise the function will create its own.
70 | * @param {string} content the task content as string
71 | * @return {Boolean} true when added successfully, false when adding task failed
72 | */
73 | function Draftist_quickAdd({
74 | todoist = new Todoist(),
75 | content
76 | }) {
77 | if (!todoist.quickAdd(content)) {
78 | let error = Draftist_getLastTodoistError(todoist)
79 | let errorMsg = "adding tasks failed, todoist returned:\n" + error
80 | Draftist_failAction("Quick Add", errorMsg)
81 | return false;
82 | } else {
83 | return true;
84 | }
85 | }
86 |
87 | /**
88 | * Draftist_createTask - creates a Task with the given parameters
89 | *
90 | * @param {Todoist_Object} todoist_obj? - if already created, otherwise the function will create its own.
91 | * @param {String} content: Task content. This value may contain markdown-formatted text and hyperlinks. Details on markdown support can be found in the Text Formatting article in the Todoist Help Center.
92 | * @param {String} description?: A description for the task. This value may contain markdown-formatted text and hyperlinks. Details on markdown support can be found in the Text Formatting article in the Todoist Help Center.
93 | * @param {String} project_id?: Task project ID. If not set, task is put to user's Inbox.
94 | * @param {String} section_id?: ID of section to put task into.
95 | * @param {String} parent_id?: Parent task ID.
96 | * @param {Integer} order?: Non-zero integer value used by clients to sort tasks under the same parent.
97 | * @param {String[]} labels?: names of labels associated with the task.
98 | * @param {Ingeger} priority?: Task priority from 1 (normal) to 4 (urgent).
99 | * @param {String} due_string?: No Human defined task due date (ex.: "next Monday", "Tomorrow"). Value is set using local (not UTC) time.
100 | * @param {String} due_date?: Specific date in YYYY-MM-DD format relative to user’s timezone.
101 | * @param {String} due_datetime?: Specific date and time in RFC3339 format in UTC.
102 | * @param {String} due_lang?: 2-letter code specifying language in case due_string is not written in English.
103 | * @param {Integer} assignee_id?: The responsible user ID (if set, and only for shared tasks).
104 | * @param {Boolean} getTaskResult?: if set to true, the function will return the api response of the created Task
105 | * @return {Boolean} true when added successfully, false when adding task failed
106 | */
107 | function Draftist_createTask({
108 | todoist = new Todoist(),
109 | content,
110 | description = "",
111 | project_id = undefined,
112 | section_id = undefined,
113 | parent_id = undefined,
114 | order = undefined,
115 | labels = [],
116 | priority = undefined,
117 | due_string = undefined,
118 | due_date = undefined,
119 | due_datetime = undefined,
120 | due_lang = undefined,
121 | assignee = undefined
122 | }, getTaskResult = false) {
123 | // check if provided content is not empty
124 | if (content.length == 0) {
125 | Draftist_failAction("create Task", "no task content provided")
126 | return false;
127 | }
128 |
129 | let taskMap = new Map();
130 | taskMap.set("content", content);
131 | taskMap.set("description", description);
132 | if (project_id) {
133 | taskMap.set("project_id", project_id);
134 | }
135 | if (section_id) {
136 | taskMap.set("section_id", section_id);
137 | }
138 | if (parent_id) {
139 | taskMap.set("parent_id", parent_id);
140 | }
141 | if (order) {
142 | taskMap.set("order", order);
143 | }
144 | if (labels.length > 0) {
145 | taskMap.set("labels", labels);
146 | }
147 | if (priority) {
148 | taskMap.set("priority", priority);
149 | }
150 | if (due_string) {
151 | taskMap.set("due_string", due_string);
152 | }
153 | if (due_date) {
154 | taskMap.set("due_date", due_date);
155 | }
156 | if (due_datetime) {
157 | taskMap.set("due_datetime", due_datetime);
158 | }
159 | if (due_lang) {
160 | taskMap.set("due_lang", due_lang);
161 | }
162 | if (assignee) {
163 | taskMap.set("assignee", assignee);
164 | }
165 |
166 | let taskObj = Object.fromEntries(taskMap)
167 |
168 | let taskCreateResult = todoist.createTask(taskObj)
169 | if (taskCreateResult) {
170 | if (getTaskResult) {
171 | return taskCreateResult;
172 | } else {
173 | return true;
174 | }
175 | } else {
176 | Draftist_failAction("create Task", Draftist_getLastTodoistError(todoist))
177 | return false;
178 | }
179 | }
180 |
181 |
182 | /**
183 | * Draftist_quickAddLines - This action creates a new task for each line in the currently open Draft
184 | *
185 | * @param {String} text the text which lines should be added as tasks to Todoist
186 | * @return {Boolean|Number} false when adding faile, task number when adding succeeded
187 | */
188 | function Draftist_quickAddLines(text) {
189 | let todoist = new Todoist()
190 | let lines = text.split("\n");
191 | let createdTasksCounter = 0;
192 | // repeat for each line
193 | for (line of lines) {
194 | if (line.length !== 0) {
195 | if (!Draftist_quickAdd({
196 | todoist: todoist,
197 | content: line
198 | })) {
199 | // if failed directly return, quickadd will display the error
200 | return false;
201 | } else {
202 | createdTasksCounter++;
203 | }
204 | }
205 | }
206 | return createdTasksCounter;
207 | }
208 |
209 |
210 |
211 | // Create Tasks Actions
212 |
213 | /**
214 | * Draftist_quickAddLinesFromCurrentDraft - use todoist quick add for each line of the current draft
215 | *
216 | * @return {Boolean} true when added successfully, false when adding task failed
217 | */
218 | function Draftist_quickAddLinesFromCurrentDraft() {
219 | if (draft.content.length == 0) {
220 | Draftist_cancelAction("Add Tasks from current Draft", "Draft is blank")
221 | return false;
222 | } else {
223 | let taskNumber = Draftist_quickAddLines(draft.content);
224 | if (taskNumber) {
225 | // succeeded
226 | Draftist_succeedAction("", false, "successfully added " + taskNumber + " tasks :)")
227 | return true;
228 | } else {
229 | return false;
230 | }
231 | }
232 | }
233 |
234 |
235 | /**
236 | * Draftist_quickAddLinesFromPrompt - use todoist quick add for each line inserted in the prompt
237 | *
238 | * @return {Boolean} true when added successfully, false when adding task failed
239 | */
240 | function Draftist_quickAddLinesFromPrompt() {
241 | let p = new Prompt();
242 | p.title = "add tasks from lines";
243 | p.addTextView("tasks", "", "", {
244 | wantsFocus: true
245 | });
246 | p.addButton("add tasks");
247 |
248 | if (!p.show()) {
249 | Draftist_cancelAction("Add Tasks from Prompt", "cancelled by user");
250 | return false;
251 | }
252 | // user did select "add tasks"
253 | let input = p.fieldValues["tasks"];
254 | if (input.length == 0) {
255 | Draftist_cancelAction("Add Tasks from Prompt", "No input provided")
256 | return false;
257 | } else {
258 | let taskNumber = Draftist_quickAddLines(input);
259 | if (taskNumber) {
260 | // succeeded
261 | Draftist_succeedAction("", false, "successfully added " + taskNumber + " tasks :)")
262 | return true;
263 | } else {
264 | return false;
265 | }
266 | }
267 | }
268 |
269 | /**
270 | * Draftist_createTaskWithDescription - reate a task with the first line of the input as content and everything else as description
271 | *
272 | * @param {String} text the text which should be used for the task and description
273 | * @return {Boolean} true when added successfully, false when adding task failed
274 | */
275 | function Draftist_createTaskWithDescription(text) {
276 | let lines = text.split("\n");
277 | // first line is the tasks content, remove it from the array and assign it
278 | let content = lines.shift();
279 | let description = lines.join("\n")
280 | return Draftist_createTask({
281 | content: content,
282 | description: description
283 | });
284 | }
285 |
286 | /**
287 | * Draftist_createTaskWithDescriptionFromCurrentDraft - add task with description from current draft. use the first line as content and everything else as description
288 | *
289 | * @return {Boolean} true when added successfully, false when adding task failed
290 | */
291 | function Draftist_createTaskWithDescriptionFromCurrentDraft() {
292 | if (draft.content.length == 0) {
293 | Draftist_cancelAction("Task with description from current Draft", "Draft is blank")
294 | return false;
295 | } else {
296 | let taskCreated = Draftist_createTaskWithDescription(draft.content);
297 | if (taskCreated) {
298 | // succeeded
299 | Draftist_succeedAction("", false, "successfully added task");
300 | return true;
301 | } else {
302 | return false
303 | }
304 | }
305 | }
306 |
307 | /**
308 | * Draftist_createTaskWithDescriptionFromPrompt - add task with description from prompt. use the first line as content and everything else as description
309 | *
310 | * @return {Boolean} true when added successfully, false when adding task failed
311 | */
312 | function Draftist_createTaskWithDescriptionFromPrompt() {
313 | let p = new Prompt();
314 | p.title = "add tasks with description";
315 | p.message = "first line is the tasks content; everything else will be used as description"
316 | p.addTextView("task", "", "", {
317 | wantsFocus: true
318 | });
319 | p.addButton("add task");
320 |
321 | if (!p.show()) {
322 | Draftist_cancelAction("Add Tasks from Prompt", "cancelled by user");
323 | return false;
324 | }
325 | // user did select "add tasks"
326 | let input = p.fieldValues["task"];
327 | if (input.length == 0) {
328 | Draftist_cancelAction("Add Tasks from Prompt", "No input provided")
329 | return false;
330 | } else {
331 | let taskCreated = Draftist_createTaskWithDescription(input);
332 | if (taskCreated) {
333 | // succeeded
334 | Draftist_succeedAction("", false, "successfully added task");
335 | return true;
336 | } else {
337 | return false
338 | }
339 | }
340 | }
341 |
342 | // #############################################################################
343 | // CREATE TASK OBJECT
344 | // #############################################################################
345 |
346 | /**
347 | * Draftist_createTaskObjectWithSettingsFromPrompt - creates a todoist task object with settings from prompts
348 | *
349 | * @param {String} content content of the task (must not be empty)
350 | * @param {String} description? description for the task (can be empty)
351 | * @return {taskObject} taskObject for a todoist task which can be passed to the Todoist.createTask() API of Drafts
352 | */
353 | function Draftist_createTaskObjectWithSettingsFromPrompt(content, description = "") {
354 | // check if any map of the todoist data contains data - if not, load the data into the vars
355 | if (projectsNameToIdMap.size == 0) {
356 | Draftist_getStoredTodoistData();
357 | }
358 |
359 | // due date prompt
360 | let pDate = new Prompt()
361 | pDate.title = "select due date for \"" + content + "\":";
362 | pDate.addButton("today");
363 | pDate.addButton("tomorrow");
364 | pDate.addButton("next week");
365 | pDate.addButton("other");
366 | pDate.addButton("no due date", undefined)
367 | pDate.isCancellable = false;
368 | pDate.show()
369 | // if buttonPressed is undefined no due date was selected
370 | const dateIsSet = (pDate.buttonPressed ? true : false);
371 | let selectedDateString = undefined;
372 | if (dateIsSet) {
373 | if (pDate.buttonPressed == "other") {
374 | var pSelDate = new Prompt();
375 | pSelDate.title = "select custom date for \"" + content + "\":";
376 | var today = new Date();
377 | var tomorrow = new Date(new Date().setDate(new Date().getDate() + 1));
378 | pSelDate.addDatePicker("dueDatePicker", "", tomorrow, {
379 | "mode": "date",
380 | "minimumDate": tomorrow
381 | });
382 |
383 | pSelDate.addButton("set due date");
384 | pSelDate.isCancellable = false;
385 | pSelDate.show();
386 | let pickedDueDate = pSelDate.fieldValues["dueDatePicker"];
387 | let day = pickedDueDate.getDate();
388 | let month = pickedDueDate.getMonth() + 1;
389 | let year = pickedDueDate.getFullYear();
390 | selectedDateString = String(year) + "-" + String(month) + "-" + String(day);
391 | } else {
392 | selectedDateString = pDate.buttonPressed;
393 | }
394 | }
395 |
396 | // priority prompt
397 | let pPrio = new Prompt();
398 | pPrio.title = "select priority for \"" + content + "\":";
399 | pPrio.addButton("p1");
400 | pPrio.addButton("p2");
401 | pPrio.addButton("p3");
402 | pPrio.addButton("p4");
403 | pPrio.isCancellable = false;
404 | pPrio.show();
405 |
406 | // the api of todoist uses different numbering than the user sees. p1 is reflected as value 4, p2 -> 3 and so on -> store this value
407 | let selectedPriority = 0;
408 | switch (pPrio.buttonPressed) {
409 | case "p1":
410 | selectedPriority = 4;
411 | break;
412 | case "p2":
413 | selectedPriority = 3;
414 | break;
415 | case "p3":
416 | selectedPriority = 2;
417 | break;
418 | case "p4":
419 | selectedPriority = 1;
420 | break;
421 | }
422 |
423 |
424 | // project prompt
425 | let pProject = new Prompt();
426 | pProject.title = "select project for \"" + content + "\":";
427 | pProject.message = "select Inbox if you want to sort it later"
428 |
429 | let sortedProjectNameMap = new Map([...projectsNameToIdMap].sort((a, b) => String(a[0]).localeCompare(b[0])))
430 |
431 |
432 |
433 | let inboxProject = sortedProjectNameMap.get("Inbox")
434 | let teamInboxProject = sortedProjectNameMap.get("Team Inbox")
435 |
436 | if (inboxProject) {
437 | pProject.addButton("Inbox", inboxProject);
438 | }
439 |
440 | if (teamInboxProject) {
441 | pProject.addButton("Team Inbox", teamInboxProject);
442 | }
443 |
444 | for (const [pName, pId] of sortedProjectNameMap) {
445 | if (pId != inboxProject && pId != teamInboxProject) {
446 | // selected button will directly contain the projects id as value
447 | pProject.addButton(pName, pId);
448 |
449 | }
450 | }
451 |
452 | pProject.isCancellable = false;
453 | pProject.show();
454 | let selectedProjectId = pProject.buttonPressed;
455 |
456 | // labels prompt
457 | let pLabels = new Prompt();
458 | pLabels.title = "select labels for \"" + content + "\":";
459 |
460 | //TODO
461 |
462 |
463 | let sortedLabelsNameMap = new Map([...labelsNameToIdMap].sort((a, b) => String(a[0]).localeCompare(b[0])))
464 |
465 | pLabels.addSelect("labels", "select labels", Array.from(sortedLabelsNameMap.keys()), [], true);
466 | pLabels.addButton("set labels");
467 | pLabels.isCancellable = false;
468 | pLabels.show();
469 | let selectedLabels = pLabels.fieldValues["labels"];
470 |
471 | let taskObject = {
472 | content: content,
473 | description: description,
474 | project_id: selectedProjectId,
475 | section_id: undefined,
476 | labels: selectedLabels,
477 | priority: selectedPriority,
478 | due_string: (selectedDateString ? selectedDateString : undefined),
479 | }
480 |
481 | return taskObject;
482 |
483 | }
484 |
485 | // #############################################################################
486 | // CREATE TASKS WITH SETTINGS
487 | // #############################################################################
488 |
489 | /**
490 | * Draftist_createTaskWithDescriptionAndSettings - create a task with description and settings (project, labels, due date) from prompts. first line will be used as task content, everything else will be the description
491 | *
492 | * @param {String} text the text which should be used for the task and description
493 | * @return {Boolean} true when added successfully, false when adding task failed
494 | */
495 | function Draftist_createTaskWithDescriptionAndSettings(text) {
496 | let lines = text.split("\n");
497 | // first line is the tasks content, remove it from the array and assign it
498 | let content = lines.shift();
499 | let description = lines.join("\n")
500 | return Draftist_createTask(Draftist_createTaskObjectWithSettingsFromPrompt(content, description));
501 | }
502 |
503 | /**
504 | * Draftist_createTaskWithDescriptionAndSettingsFromCurrentDraft - add task with description and settings from prompt from current draft. use the first line as content and everything else as description
505 | *
506 | * @return {Boolean} true when added successfully, false when adding task failed
507 | */
508 | function Draftist_createTaskWithDescriptionAndSettingsFromCurrentDraft() {
509 | if (draft.content.length == 0) {
510 | Draftist_cancelAction("Task with description from current Draft", "Draft is blank")
511 | return false;
512 | } else {
513 | let taskCreated = Draftist_createTaskWithDescriptionAndSettings(draft.content);
514 | if (taskCreated) {
515 | // succeeded
516 | Draftist_succeedAction("", false, "successfully added task");
517 | return true;
518 | } else {
519 | return false
520 | }
521 | }
522 | }
523 |
524 | /**
525 | * Draftist_createTaskWithDescriptionAndSettingsFromPrompt - add task with description and Settings from prompts. use the first line as content and everything else as description
526 | *
527 | * @return {Boolean} true when added successfully, false when adding task failed
528 | */
529 | function Draftist_createTaskWithDescriptionAndSettingsFromPrompt() {
530 | let p = new Prompt();
531 | p.title = "add task with description & settings";
532 | p.message = "first line is the tasks content; everything else will be used as description"
533 | p.addTextView("task", "", "", {
534 | wantsFocus: true
535 | });
536 | p.addButton("add task");
537 |
538 | if (!p.show()) {
539 | Draftist_cancelAction("Add Tasks from Prompt", "cancelled by user");
540 | return false;
541 | }
542 | // user did select "add tasks"
543 | let input = p.fieldValues["task"];
544 | if (input.length == 0) {
545 | Draftist_cancelAction("Add Tasks from Prompt", "No input provided")
546 | return false;
547 | } else {
548 | let taskCreated = Draftist_createTaskWithDescriptionAndSettings(input);
549 | if (taskCreated) {
550 | // succeeded
551 | Draftist_succeedAction("", false, "successfully added task");
552 | return true;
553 | } else {
554 | return false
555 | }
556 | }
557 | }
558 |
559 | // #############################################################################
560 | // CREATE LINKED TASKS
561 | // #############################################################################
562 |
563 | /**
564 | * Draftist_helperCreateMdLinkToCurrentDraft - creates a markdown link to the current draft in the editor
565 | *
566 | * @return {String} markdown link to the current open draft in the editor
567 | */
568 | function Draftist_helperCreateMdLinkToCurrentDraft() {
569 | return "[" + draft.displayTitle + "]" + "(" + draft.permalink + ")"
570 | }
571 |
572 |
573 | /**
574 | * Draftist_helperCreateOpenTaskUrlFromTaskObject - creates the weblink to the given task object
575 | *
576 | * @param {Todoist_Task} taskObject Todoist Task Object in JSON format
577 | * @return {String} web / mobile link(s) to the Todoist Task depending on the stored settings
578 | */
579 | function Draftist_helperCreateOpenTaskUrlFromTaskObject(taskObject) {
580 | // load settings
581 | Draftist_loadCurrentConfigurationSettings()
582 | const webLink = "[🌐](" + taskObject.url + ")";
583 | const mobileLink = "[📱](todoist://task?id=" + taskObject.id + ")";
584 | if (activeSettings["taskLinkTypes"].includes("web") && activeSettings["taskLinkTypes"].includes("app")) {
585 | return webLink + "\n" + mobileLink;
586 | } else if (activeSettings["taskLinkTypes"].includes("web") && !activeSettings["taskLinkTypes"].includes("app")) {
587 | return webLink;
588 | } else if (!activeSettings["taskLinkTypes"].includes("web") && activeSettings["taskLinkTypes"].includes("app")) {
589 | return mobileLink;
590 | }
591 | }
592 |
593 |
594 | /**
595 | * Draftist_createTaskInInboxWithLinkToDraft - creates a task in the Todoist Inbox containing the title and link to the current draft
596 | *
597 | * @return {Boolean} true if succeeded, otherwise false
598 | */
599 | function Draftist_createTaskInInboxWithLinkToDraft() {
600 | if (Draftist_createTask({
601 | content: Draftist_helperCreateMdLinkToCurrentDraft()
602 | })) {
603 | Draftist_succeedAction("", false, "added linked task");
604 | return true;
605 | } else {
606 | return false;
607 | }
608 | }
609 |
610 | /**
611 | * Draftist_createTaskWithSettingsAndLinkToDraft - creates a task in the Todoist with settings from prompts containing the title and link to the current draft
612 | *
613 | * @return {Boolean} true if succeeded, otherwise false
614 | */
615 | function Draftist_createTaskWithSettingsAndLinkToDraft() {
616 | let taskObject = Draftist_createTaskObjectWithSettingsFromPrompt(Draftist_helperCreateMdLinkToCurrentDraft());
617 | if (Draftist_createTask(taskObject)) {
618 | Draftist_succeedAction("", false, "added linked task with settings");
619 | return true;
620 | } else {
621 | return false;
622 | }
623 | }
624 |
625 |
626 | /**
627 | * Draftist_helperAddTextBetweenTitleAndBodyOfCurrentDraft - adds the provided text between the title and body of the current draft the added text will be surrounded by empty lines
628 | *
629 | * @param {type} textToAdd the text to add between title and body
630 | */
631 | function Draftist_helperAddTextBetweenTitleAndBodyOfCurrentDraft(textToAdd) {
632 | let lines = draft.content.split("\n");
633 | let curIndex = 1
634 | if (lines.length == 1) {
635 | // add two empty lines if draft has only one line
636 | lines.push("")
637 | lines.push("")
638 | }
639 | if (lines[curIndex].length != 0) {
640 | lines.splice(curIndex, 0, "");
641 | }
642 | curIndex++;
643 | lines.splice(curIndex, 0, textToAdd)
644 | curIndex++;
645 | if (lines[curIndex].length != 0) {
646 | lines.splice(curIndex, 0, "")
647 | }
648 | draft.content = lines.join("\n");
649 | draft.update()
650 | }
651 |
652 | /**
653 | * Draftist_createTaskInInboxWithLinkToDraft - creates a task in the Todoist Inbox containing the title and link to the current draft. A link to the created Task in Todoist will be added to the Draft between the title and the body
654 | *
655 | * @return {Boolean} true if succeeded, otherwise false
656 | */
657 | function Draftist_createCrosslinkedTaskInInbox() {
658 | let createdTask = Draftist_createTask({
659 | content: Draftist_helperCreateMdLinkToCurrentDraft()
660 | }, true)
661 | if (createdTask) {
662 | Draftist_helperAddTextBetweenTitleAndBodyOfCurrentDraft(Draftist_helperCreateOpenTaskUrlFromTaskObject(createdTask));
663 | Draftist_succeedAction("", false, "added linked task");
664 | return true;
665 | } else {
666 | return false;
667 | }
668 | }
669 |
670 | /**
671 | * Draftist_createCrosslinkedTaskWithSettings - creates a task in the Todoist with settings from prompts containing the title and link to the current draft. A link to the created Task in Todoist will be added to the Draft between the title and the body
672 | *
673 | * @return {Boolean} true if succeeded, otherwise false
674 | */
675 | function Draftist_createCrosslinkedTaskWithSettings() {
676 | let taskObject = Draftist_createTaskObjectWithSettingsFromPrompt(Draftist_helperCreateMdLinkToCurrentDraft());
677 | let createdTask = Draftist_createTask(taskObject, true)
678 | if (createdTask) {
679 | Draftist_helperAddTextBetweenTitleAndBodyOfCurrentDraft(Draftist_helperCreateOpenTaskUrlFromTaskObject(createdTask));
680 | Draftist_succeedAction("", false, "added linked task");
681 | return true;
682 | } else {
683 | return false;
684 | }
685 | }
686 |
687 | // #############################################################################
688 | // CREATE MULTIPLE TASKS
689 | // #############################################################################
690 |
691 | /**
692 | * Draftist_createTasksFromLinesWithIdenticalSettings - creates a task from each line in the passed text with identical settings from prompts
693 | *
694 | * @param {String} text the string containing the tasks seperated by new lines
695 | * @return {Boolean} true if added successfully; false if adding tasks failed
696 | */
697 | function Draftist_createTasksFromLinesWithIdenticalSettings(text) {
698 | if (text.length == 0) {
699 | return false;
700 | } else {
701 | let taskCount = 0;
702 | let taskBaseObject = Draftist_createTaskObjectWithSettingsFromPrompt("multiple tasks");
703 | let lines = text.split("\n");
704 | for (line of lines) {
705 | if (line.length != 0) {
706 | taskBaseObject.content = line
707 | if (Draftist_createTask(taskBaseObject)) {
708 | // increase task counter
709 | taskCount = taskCount + 1;
710 | } else {
711 | // stop adding tasks and return immideately
712 | return false;
713 | }
714 | }
715 | }
716 | // succeeded
717 | Draftist_succeedAction("", false, "successfully added " + taskCount + " task(s)");
718 | }
719 | }
720 |
721 |
722 | /**
723 | * Draftist_createTasksFromLinesInDraftWithIdenticalSettings - creates tasks for each line in the current draft with identical settings from prompts
724 | *
725 | * @return {Boolean} true if added successfully; false if adding tasks failed
726 | */
727 | function Draftist_createTasksFromLinesInDraftWithIdenticalSettings() {
728 | if (draft.content.length != 0) {
729 | return Draftist_createTasksFromLinesWithIdenticalSettings(draft.content);
730 | } else {
731 | Draftist_cancelAction("Tasks from lines in current Draft with identical settings", "Draft is blank")
732 | return false;
733 | }
734 | }
735 |
736 |
737 | /**
738 | * Draftist_createTasksFromLinesInPromptWithIdenticalSettings - creates tasks for each line in the displayed prompt with identical settings from prompts
739 | *
740 | * @return {Boolean} true when added successfully, false when adding task failed
741 | */
742 | function Draftist_createTasksFromLinesInPromptWithIdenticalSettings() {
743 | let p = new Prompt();
744 | p.title = "add tasks with same settings";
745 | p.message = "each line will be its own task - all use the same settings in the next prompts"
746 | p.addTextView("tasks", "", "", {
747 | wantsFocus: true
748 | });
749 | p.addButton("add tasks");
750 |
751 | if (!p.show()) {
752 | Draftist_cancelAction("Add Tasks from Prompt", "cancelled by user");
753 | return false;
754 | }
755 | // user did select "add tasks"
756 | let input = p.fieldValues["tasks"];
757 | if (input.length == 0) {
758 | Draftist_cancelAction("Add Tasks from Prompt", "No input provided")
759 | return false;
760 | } else {
761 | let taskCreated = Draftist_createTasksFromLinesWithIdenticalSettings(input);
762 | if (taskCreated) {
763 | // succeeded
764 | Draftist_succeedAction("", false, "successfully added task");
765 | return true;
766 | } else {
767 | return false
768 | }
769 | }
770 | }
771 |
772 |
773 | /**
774 | * Draftist_createTasksFromLinesWithIndividualSettings - creates a task from each line in the passed text with individual settings for each line from prompts
775 | *
776 | * @param {String} text the string containing the tasks seperated by new lines
777 | * @return {Boolean} true if added successfully; false if adding tasks failed
778 | */
779 | function Draftist_createTasksFromLinesWithIndividualSettings(text) {
780 | if (text.length == 0) {
781 | return false;
782 | } else {
783 | let taskCount = 0;
784 | let lines = text.split("\n");
785 | for (line of lines) {
786 | if (line.length != 0) {
787 | if (Draftist_createTask(Draftist_createTaskObjectWithSettingsFromPrompt(line))) {
788 | // increase task counter
789 | taskCount = taskCount + 1;
790 | } else {
791 | // stop adding tasks and return immideately
792 | return false;
793 | }
794 | }
795 | }
796 | // succeeded
797 | Draftist_succeedAction("", false, "successfully added " + taskCount + " task(s)");
798 | }
799 | }
800 |
801 | /**
802 | * Draftist_createTasksFromLinesInDraftWithIndividualSettings - creates tasks for each line in the current draft with individual settings from prompts
803 | *
804 | * @return {Boolean} true if added successfully; false if adding tasks failed
805 | */
806 | function Draftist_createTasksFromLinesInDraftWithIndividualSettings() {
807 | if (draft.content.length != 0) {
808 | return Draftist_createTasksFromLinesWithIndividualSettings(draft.content);
809 | } else {
810 | Draftist_cancelAction("Tasks from lines in current Draft with individual settings", "Draft is blank")
811 | return false;
812 | }
813 | }
814 |
815 | /**
816 | * Draftist_createTasksFromLinesInPromptWithIndividualSettings - creates tasks for each line in the displayed prompt with individual settings from prompts
817 | *
818 | * @return {Boolean} true when added successfully, false when adding task failed
819 | */
820 | function Draftist_createTasksFromLinesInPromptWithIndividualSettings() {
821 | let p = new Prompt();
822 | p.title = "add tasks with individual settings";
823 | p.message = "each line will be its own task - each uses different settings from the next prompts"
824 | p.addTextView("tasks", "", "", {
825 | wantsFocus: true
826 | });
827 | p.addButton("add tasks");
828 |
829 | if (!p.show()) {
830 | Draftist_cancelAction("Add Tasks from Prompt", "cancelled by user");
831 | return false;
832 | }
833 | // user did select "add tasks"
834 | let input = p.fieldValues["tasks"];
835 | if (input.length == 0) {
836 | Draftist_cancelAction("Add Tasks from Prompt", "No input provided")
837 | return false;
838 | } else {
839 | let taskCreated = Draftist_createTasksFromLinesWithIndividualSettings(input);
840 | if (taskCreated) {
841 | // succeeded
842 | Draftist_succeedAction("", false, "successfully added task");
843 | return true;
844 | } else {
845 | return false
846 | }
847 | }
848 | }
849 |
850 |
851 | /**
852 | * Draftist_helperGetMdTasksFromCurrentDraft - searches the current draft for markdown tasks "- [ ]" and retruns an array with the "content" of the tasks
853 | *
854 | * @return {String[]} array of task contents
855 | */
856 | function Draftist_helperGetMdTasksFromCurrentDraft() {
857 | let content = draft.content;
858 | // find all lines with a task marker at the beginning which are uncompleted
859 | const regex = /^- \[\s\]\s(.*)$/gm;
860 | const subst = `$1`;
861 | let matches = [...content.matchAll(regex)]
862 | // the first group of each match is the task content (without the md task marker)
863 | let taskContents = matches.map(match => match[1])
864 | return taskContents
865 | }
866 |
867 |
868 | /**
869 | * Draftist_quickAddTasksFromMdTodoLinesInDraft - creates tasks for each md task in the current open draft. This function uses the Todoist quickAdd API which allows adding due dates, labels, projects with the syntax also used in the Todoist task input.
870 | *
871 | * @return {Boolean} true if added successfully (or no task was found), false if adding task failed
872 | */
873 | function Draftist_quickAddTasksFromMdTodoLinesInDraft() {
874 | let tasks = Draftist_helperGetMdTasksFromCurrentDraft()
875 | if (tasks.length > 0) {
876 | // combine task contents by new lines to work with the quickAddLines function
877 | let taskNumber = Draftist_quickAddLines(tasks.join("\n"));
878 | if (taskNumber) {
879 | // succeeded
880 | Draftist_succeedAction("", false, "successfully added " + taskNumber + " tasks :)")
881 | return true;
882 | } else {
883 | return false;
884 | }
885 | } else {
886 | Draftist_infoMessage("", "no (uncompleted) tasks found in draft");
887 | return true;
888 | }
889 | }
890 |
891 |
892 | /**
893 | * Draftist_createTasksWithIdenticalSettingsFromMdTasksInCurrentDraft - creates tasks with same settings for every md task in the current document. The settings can be selected in the displayed prompts.
894 | *
895 | * @return {Boolean} true if added successfully (or no task was found), false if adding task failed
896 | */
897 | function Draftist_createTasksWithIdenticalSettingsFromMdTasksInCurrentDraft() {
898 | let tasks = Draftist_helperGetMdTasksFromCurrentDraft()
899 | if (tasks.length > 0) {
900 | // combine task contents by new lines to input a text which is split in the called function
901 | return Draftist_createTasksFromLinesWithIdenticalSettings(tasks.join("\n"));
902 | } else {
903 | Draftist_infoMessage("", "no (uncompleted) tasks found in draft");
904 | return true;
905 | }
906 | }
907 |
908 | /**
909 | * Draftist_createTasksWithIndividualSettingsFromMdTasksInCurrentDraft - creates tasks with individual settings for every md task in the current document. The settings can be selected in the displayed prompts.
910 | *
911 | * @return {Boolean} true if added successfully (or no task was found), false if adding task failed
912 | */
913 | function Draftist_createTasksWithIndividualSettingsFromMdTasksInCurrentDraft() {
914 | let tasks = Draftist_helperGetMdTasksFromCurrentDraft()
915 | if (tasks.length > 0) {
916 | // combine task contents by new lines to input a text which is split in the called function
917 | return Draftist_createTasksFromLinesWithIndividualSettings(tasks.join("\n"));
918 | } else {
919 | Draftist_infoMessage("", "no (uncompleted) tasks found in draft");
920 | return true;
921 | }
922 | }
923 |
924 | // #############################################################################
925 | // IMPORT TASKS
926 | // #############################################################################
927 |
928 | /**
929 | * Draftist_createStringFromTasks - converts the passed tasks to strings with contents using the active settings for task strings
930 | *
931 | * @param {Tasks[]} tasks - Todoist task objects to convert to strings
932 | * @return {String} string containing the task informations
933 | */
934 | function Draftist_createStringFromTasks({
935 | tasks
936 | }) {
937 | Draftist_loadCurrentConfigurationSettings();
938 | const contentSettings = activeSettings["taskImportContents"];
939 | let tasksString = ""
940 | for (task of tasks) {
941 |
942 | // task content
943 | tasksString = tasksString + "- [ ] " + task.content
944 | // app link
945 | if (contentSettings.includes("appLink")) {
946 | tasksString = tasksString + " [📱](todoist://task?id=" + task.id + ")";
947 | }
948 | // web link
949 | if (contentSettings.includes("webLink")) {
950 | tasksString = tasksString + " [🌐](" + task.url + ")";
951 | }
952 |
953 | if (contentSettings.includes("projectName")) {
954 | if (projectsIdToNameMap.size == 0) {
955 | Draftist_getStoredTodoistData();
956 | }
957 | tasksString = tasksString + " *" + projectsIdToNameMap.get(task.project_id) + "*"
958 | }
959 |
960 | if (contentSettings.includes("priority")) {
961 | tasksString = tasksString + " p" + (5 - task.priority);
962 | }
963 |
964 | if (contentSettings.includes("labels")) {
965 | if (labelsIdToNameMap.size == 0) {
966 | Draftist_getStoredTodoistData();
967 | }
968 | for (label of task.labels) {
969 | tasksString = tasksString + " @" + label
970 | }
971 | }
972 | tasksString = tasksString + "\n"
973 | }
974 | return tasksString;
975 | }
976 |
977 |
978 | /**
979 | * Draftist_getTodoistTasksFromFilter - returns the tasks in todoist for a given filter string
980 | *
981 | * @param {String} filterString a valid todoist filter string
982 | * @return {Task[]} array of Task objects (JSON) for the given filter string
983 | */
984 | function Draftist_getTodoistTasksFromFilter(filterString) {
985 | let todoist = new Todoist()
986 |
987 | // If filter contains comma, split it and query each part separately
988 | if (filterString.includes(",")) {
989 | let filterParts = filterString.split(",").map(part => part.trim()).filter(part => part.length > 0);
990 | let allTasks = [];
991 | let seenTaskIds = new Set();
992 |
993 | for (let singleFilter of filterParts) {
994 | let continueRequest = true;
995 | let next_cursor = null;
996 |
997 | while (continueRequest) {
998 | let options = {};
999 |
1000 | if (next_cursor) {
1001 | options = {
1002 | query: singleFilter,
1003 | limit: 200,
1004 | cursor: next_cursor
1005 | };
1006 | } else {
1007 | options = {
1008 | query: singleFilter,
1009 | limit: 200,
1010 | };
1011 | }
1012 |
1013 | let returnedTasks = todoist.getTasksByFilter(options);
1014 |
1015 | // Add tasks, avoiding duplicates
1016 | for (let task of returnedTasks) {
1017 | if (!seenTaskIds.has(task.id)) {
1018 | seenTaskIds.add(task.id);
1019 | allTasks.push(task);
1020 | }
1021 | }
1022 |
1023 | if (todoist.lastResponse.next_cursor) {
1024 | next_cursor = todoist.lastResponse.next_cursor;
1025 | } else {
1026 | continueRequest = false;
1027 | }
1028 |
1029 | const occuredError = Draftist_getLastTodoistError(todoist);
1030 | if (occuredError) {
1031 | Draftist_failAction("get tasks from filter \"" + singleFilter + "\"", occuredError);
1032 | return false;
1033 | }
1034 | }
1035 | }
1036 | return allTasks;
1037 | }
1038 |
1039 | let continueRequest = true
1040 | let next_cursor = null
1041 |
1042 | let tasks = []
1043 |
1044 | while (continueRequest) {
1045 | let options = {}
1046 |
1047 | if (next_cursor) {
1048 | options = {
1049 | query: filterString,
1050 | limit: 200,
1051 | cursor: next_cursor
1052 | };
1053 | } else {
1054 |
1055 | options = {
1056 | query: filterString,
1057 | limit: 200,
1058 | };
1059 | }
1060 |
1061 | returnedTasks = todoist.getTasksByFilter(options)
1062 |
1063 | tasks.push(...returnedTasks)
1064 |
1065 | if (todoist.lastResponse.next_cursor) {
1066 | next_cursor = todoist.lastResponse.next_cursor
1067 | } else {
1068 | continueRequest = false
1069 | }
1070 |
1071 | const occuredError = Draftist_getLastTodoistError(todoist)
1072 | if (occuredError) {
1073 | Draftist_failAction("get tasks from filter \"" + filterString + "\"", occuredError)
1074 | return false;
1075 | }
1076 | }
1077 |
1078 | return tasks;
1079 | }
1080 |
1081 |
1082 | /**
1083 | * Draftist_importTodaysTasksIntoDraft - appends the tasks due today (or overdue) to the current draft
1084 | *
1085 | */
1086 | function Draftist_importTodaysTasksIntoDraft() {
1087 | const tasks = Draftist_getTodoistTasksFromFilter("overdue | today");
1088 | const stringToInsert = Draftist_createStringFromTasks({
1089 | tasks: tasks
1090 | })
1091 | draft.content = draft.content + "\n **TODAYs TASKs:**\n\n" + stringToInsert;
1092 | draft.update()
1093 | }
1094 |
1095 |
1096 | /**
1097 | * Draftist_importTasksFromProjectName - imports the tasks from the provided project name into the current draft
1098 | *
1099 | * @param {String} projectName the project name for the tasks to import
1100 | */
1101 | function Draftist_importTasksFromProjectName(projectName) {
1102 | if (projectsNameToIdMap.size == 0) {
1103 | Draftist_getStoredTodoistData();
1104 | }
1105 | const projectNames = Array.from(projectsNameToIdMap.keys());
1106 | // check if project name is available, if not fail the action
1107 | if (!projectNames.includes(projectName)) {
1108 | Draftist_failAction("import tasks from project", "project with name \"" + projectName + "\" is not existing in your Todoist Account")
1109 | return
1110 | }
1111 | const tasks = Draftist_getTodoistTasksFromFilter("##" + projectName)
1112 | const stringToInsert = Draftist_createStringFromTasks({
1113 | tasks: tasks
1114 | })
1115 | draft.content = draft.content + "\n **TASKs from " + projectName + ":**\n\n" + stringToInsert;
1116 | draft.update()
1117 | }
1118 |
1119 |
1120 | /**
1121 | * Draftist_importTasksFromSelectedProject - presents a prompt to let the user select a project and then imports the task of the selected project into the current draft
1122 | *
1123 | */
1124 | function Draftist_importTasksFromSelectedProject() {
1125 | if (projectsNameToIdMap.size == 0) {
1126 | Draftist_getStoredTodoistData();
1127 | }
1128 | let pProject = new Prompt();
1129 | pProject.title = "select the project"
1130 | let sortedProjectNameMap = new Map([...projectsNameToIdMap].sort((a, b) => String(a[0]).localeCompare(b[0])))
1131 | for (const [pName, pId] of sortedProjectNameMap) {
1132 | // selected button will directly contain the projects id as value
1133 | pProject.addButton(pName);
1134 | }
1135 | if (pProject.show()) {
1136 | Draftist_importTasksFromProjectName(pProject.buttonPressed);
1137 | } else {
1138 | Draftist_cancelAction("import tasks from project", "user cancelled")
1139 | }
1140 |
1141 | }
1142 |
1143 | /**
1144 | * Draftist_importTasksWithLabels - imports the tasks with the provided label names into the current draft. Depending on the input parameter either all or any given labels must be included in a task.
1145 | *
1146 | * @param {String} labelNames the label names for the tasks to import (separated by a comma)
1147 | * @param {Boolean} requireAllLabels if set to true all given labels must be present in the task to be imported, if set to false only one of the given labels must be present in the task
1148 | */
1149 |
1150 | function Draftist_importTasksWithLabels(labelNames, requireAllLabels) {
1151 | const requestedLabels = labelNames.split(",");
1152 | if (labelsNameToIdMap.size == 0) {
1153 | Draftist_getStoredTodoistData();
1154 | }
1155 | const validLabelNames = Array.from(labelsNameToIdMap.keys());
1156 | let labelStrings = [];
1157 | for (labelName of requestedLabels) {
1158 | // check if all given label names are available, if not fail the action
1159 | if (!validLabelNames.includes(labelName)) {
1160 | Draftist_failAction("import tasks with label", "label with name \"" + labelName + "\" is not existing in your Todoist Account")
1161 | return
1162 | }
1163 | labelStrings.push("@" + labelName);
1164 | }
1165 | const filterString = labelStrings.join((requireAllLabels ? " & " : " | "))
1166 | const tasks = Draftist_getTodoistTasksFromFilter(filterString)
1167 | const stringToInsert = Draftist_createStringFromTasks({
1168 | tasks: tasks
1169 | })
1170 | draft.content = draft.content + "\n **TASKs with label(s) " + filterString + ":**\n\n" + stringToInsert;
1171 | draft.update()
1172 | }
1173 |
1174 |
1175 | /**
1176 | * Draftist_importTasksWithSelectedLabels - imports the tasks from the selected labels in a prompt into the current draft. Depending on the input parameter either all or any given labels must be included in a task.
1177 | *
1178 | * @param {type} requireAllLabels if set to true all given labels must be present in the task to be imported, if set to false only one of the given labels must be present in the task
1179 | */
1180 | function Draftist_importTasksWithSelectedLabels(requireAllLabels) {
1181 | if (labelsNameToIdMap.size == 0) {
1182 | Draftist_getStoredTodoistData();
1183 | }
1184 | let pLabels = new Prompt();
1185 | pLabels.title = "select the labels"
1186 | pLabels.message = (requireAllLabels ? "all selected labels " : "any selected label") + " must be present in the task"
1187 | let sortedLabelNameMap = new Map([...labelsNameToIdMap].sort((a, b) => String(a[0]).localeCompare(b[0])))
1188 | pLabels.addSelect("selectedLabels", "", Array.from(sortedLabelNameMap.keys()), [], true)
1189 | pLabels.addButton("Apply")
1190 | if (pLabels.show()) {
1191 | Draftist_importTasksWithLabels(pLabels.fieldValues["selectedLabels"].join(","), requireAllLabels);
1192 | } else {
1193 | Draftist_cancelAction("import tasks from project", "user cancelled")
1194 | }
1195 | }
1196 |
1197 |
1198 | /**
1199 | * Draftist_importTasksFromFilter - imports the tasks for the given filter string into the current draft
1200 | *
1201 | * @param {type} filterString filter string for Todoist tasks
1202 | */
1203 | function Draftist_importTasksFromFilter(filterString) {
1204 | const tasks = Draftist_getTodoistTasksFromFilter(filterString);
1205 | if (tasks) {
1206 | const stringToInsert = Draftist_createStringFromTasks({
1207 | tasks: tasks
1208 | })
1209 | draft.content = draft.content + "\n **TASKs from filter \"" + filterString + "\":**\n\n" + stringToInsert;
1210 | draft.update()
1211 | }
1212 | }
1213 |
1214 |
1215 | /**
1216 | * Draftist_importTasksFromFilterInPrompt - imports the tasks from the filter typed into the text field in the prompt
1217 | *
1218 | */
1219 | function Draftist_importTasksFromFilterInPrompt() {
1220 | let pFilter = new Prompt()
1221 | pFilter.title = "set the filter query";
1222 | pFilter.message = "you can use any supported filter query by Todoist"
1223 | pFilter.addTextField("filterString", "", "", {
1224 | wantsFocus: true
1225 | });
1226 | pFilter.addButton("Apply");
1227 | if (pFilter.show()) {
1228 | Draftist_importTasksFromFilter(pFilter.fieldValues["filterString"]);
1229 | } else {
1230 | Draftist_cancelAction("import tasks from filter", "user cancelled")
1231 | }
1232 | }
1233 |
1234 | // #############################################################################
1235 | // MODIFY TASKS
1236 | // #############################################################################
1237 |
1238 | /**
1239 | * Draftist_updateTask updates the provided task with the provided options
1240 | *
1241 | * @param {Todoist_Object} todoist_obj? - if already created, otherwise the function will create its own.
1242 | * @param {Todoist_Task} taskToUpdate - the task that should be updated
1243 | * @param {String[]} labelNamesToRemove? - the valid label names which should be removed from the provided task
1244 | * @param {String[]} labelNamesToAdd? - the valid label names which should be added to the provided task
1245 | * @param {String} newDueDateString? - the new due date provided as String (best in the format YYYY-MM-DD; but Human defined dates are possible (https://developer.todoist.com/rest/v1/#update-a-task // https://todoist.com/help/articles/due-dates-and-times)
1246 | * @param {String} newProjectName? - ATTENTION: currently 05/2022 not supported by the todoist API, providing this parameter will fail the action - the new valid project name for the provided task
1247 | * @returns {Boolean} true when updated successfully, false when updating failed or any parameter was not valid (e.g. label name is not existing)
1248 | */
1249 | function Draftist_updateTask({
1250 | todoist = new Todoist(),
1251 | taskToUpdate,
1252 | labelNamesToRemove = [],
1253 | labelNamesToAdd = [],
1254 | newDueDateString = undefined,
1255 | newProjectName = undefined
1256 | }) {
1257 | if (!taskToUpdate) {
1258 | Draftist_failAction("update task", "no task to update was provided")
1259 | return false
1260 | }
1261 | const taskId = taskToUpdate.id;
1262 |
1263 | // update labels
1264 | const currentLabels = taskToUpdate.labels;
1265 | // init projectId variable
1266 |
1267 | // load todoist data if not already loaded and a relevant parameter is present & contains relevant values (e.g. labels, project id)
1268 | if (labelsNameToIdMap.size == 0 && (labelNamesToRemove.length > 0 || labelNamesToAdd.length > 0 || newProjectName)) {
1269 | Draftist_getStoredTodoistData();
1270 | }
1271 |
1272 | // init labelId arrays
1273 | let labelIdsToRemove = [];
1274 | let labelIdsToAdd = [];
1275 |
1276 | for (labelName of labelNamesToRemove) {
1277 | const curLabelId = labelsNameToIdMap.get(labelName);
1278 | if (!curLabelId) {
1279 | Draftist_failAction("update task", "provided label name \"" + labelName + "\" is not existing.");
1280 | return false
1281 | }
1282 | labelIdsToRemove.push(curLabelId);
1283 | }
1284 |
1285 | for (labelName of labelNamesToAdd) {
1286 | const curLabelId = labelsNameToIdMap.get(labelName);
1287 | if (!curLabelId) {
1288 | Draftist_failAction("update task", "provided label name \"" + labelName + "\" is not existing.");
1289 | return false
1290 | }
1291 | labelIdsToAdd.push(curLabelId);
1292 | }
1293 |
1294 |
1295 |
1296 | let updatedLabelIds = labelIdsToAdd;
1297 | let updatedLabels = labelNamesToAdd;
1298 |
1299 | for (curLabel of currentLabels) {
1300 | // add the label to the updated array if its not already included and it is not contained in the labelsToRemove Array
1301 | if (!labelIdsToRemove.includes(curLabel) && !updatedLabelIds.includes(curLabel)) {
1302 | updatedLabelIds.push(curLabel);
1303 | }
1304 | if (!labelNamesToRemove.includes(curLabel) && !updatedLabels.includes(curLabel)) {
1305 | updatedLabels.push(curLabel);
1306 | }
1307 |
1308 | }
1309 |
1310 | // update due date / date time
1311 |
1312 | let dueDateString = taskToUpdate.due_date;
1313 | if (newDueDateString) {
1314 | // new due date was provided
1315 | dueDateString = newDueDateString
1316 | }
1317 |
1318 | // attention the project ID was implemented based on the task property.
1319 | // turned out that project_id is not a parameter for the update task request: https://developer.todoist.com/rest/v1/#update-a-task
1320 | // support request was sent on 2022-05-12 but until implementation this is a point of failure and will fail the function for now.
1321 | let projectId = taskToUpdate.project_id;
1322 | if (newProjectName) {
1323 | // fail the action until project id is supported by Todoist:
1324 | Draftist_failAction("update task", "new project name was provided but is currently not supported by Todoist")
1325 | return false;
1326 | projectId = projectsNameToIdMap.get(newProjectName);
1327 | if (!projectId) {
1328 | Draftist_failAction("update task", "provided project name \"" + newProjectName + "\" is not existing.");
1329 | return false
1330 | }
1331 | }
1332 |
1333 | // create task options
1334 | let options = {
1335 | "content": taskToUpdate.content,
1336 | "description": taskToUpdate.description,
1337 | "project_id": projectId,
1338 | "section_id": taskToUpdate.section_id,
1339 | "parent_id": taskToUpdate.parent_id,
1340 | "order": taskToUpdate.order,
1341 | "labels": updatedLabels,
1342 | "priority": taskToUpdate.priority,
1343 | "due_string": (dueDateString ? dueDateString : undefined),
1344 | "assignee": taskToUpdate.assignee
1345 | };
1346 |
1347 | const updateTaskResult = todoist.updateTask(taskId, options);
1348 | if (!updateTaskResult) {
1349 | const lastError = Draftist_getLastTodoistError(todoist);
1350 | if (lastError) {
1351 | Draftist_failAction("update task", "todoist returned error: " + lastError)
1352 | } else {
1353 | Draftist_failAction("update task", "unknown error occured. please try again and contact @FlohGro with steps to reproduce")
1354 | }
1355 | return false;
1356 | } else {
1357 | return true;
1358 | }
1359 |
1360 | }
1361 |
1362 | /**
1363 | * Draftist_updateLabelsOfTask updates the labels of the provided task with the provided options
1364 | *
1365 | * @param {Todoist_Object} todoist_obj? - if already created, otherwise the function will create its own.
1366 | * @param {Todoist_Task} taskToUpdate - the task that should be updated
1367 | * @param {String[]} labelNamesToRemove? - the valid label names which should be removed from the provided task
1368 | * @param {String[]} labelNamesToAdd? - the valid label names which should be added to the provided task
1369 | * @returns {Boolean} true when updated successfully, false when updating failed or any parameter was not valid (e.g. label name is not existing)
1370 | */
1371 | function Draftist_updateLabelsOfTask({
1372 | todoist = new Todoist(),
1373 | taskToUpdate,
1374 | labelNamesToRemove,
1375 | labelNamesToAdd
1376 | }) {
1377 | return Draftist_updateTask({
1378 | todoist: todoist,
1379 | taskToUpdate: taskToUpdate,
1380 | labelNamesToRemove: labelNamesToRemove,
1381 | labelNamesToAdd: labelNamesToAdd
1382 | })
1383 | }
1384 |
1385 | /**
1386 | * Draftist_updateProjectOfTask updates the project of the provided task to the provided project name (not supported by Todoist right now)
1387 | *
1388 | * @param {Todoist_Object} todoist_obj? - if already created, otherwise the function will create its own.
1389 | * @param {Todoist_Task} taskToUpdate - the task that should be updated
1390 | * @param {String} newProjectName - the valid new project name for the task
1391 | * @returns {Boolean} true when updated successfully, false when updating failed or any parameter was not valid (e.g. project name is not existing)
1392 | */
1393 | function Draftist_updateProjectOfTask({
1394 | todoist = new Todoist(),
1395 | taskToUpdate,
1396 | newProjectName
1397 | }) {
1398 | return Draftist_updateTask({
1399 | todoist: todoist,
1400 | taskToUpdate: taskToUpdate,
1401 | newProjectName: newProjectName
1402 | })
1403 | }
1404 |
1405 |
1406 | /**
1407 | * Draftist_updateDueDateOfTask updates the due date of the provided task to the provided new date
1408 | *
1409 | * @param {Todoist_Object} todoist_obj? - if already created, otherwise the function will create its own.
1410 | * @param {Todoist_Task} taskToUpdate - the task that should be updated
1411 | * @param {String} newDueDateString - the new due date provided as String (best in the format YYYY-MM-DD; but Human defined dates are possible (https://developer.todoist.com/rest/v1/#update-a-task // https://todoist.com/help/articles/due-dates-and-times)
1412 | * @returns {Boolean} true when updated successfully, false when updating failed or any parameter was not valid (e.g. unsupported date format)
1413 | */
1414 | function Draftist_updateDueDateOfTask({
1415 | todoist = new Todoist(),
1416 | taskToUpdate,
1417 | newDueDateString
1418 | }) {
1419 | return Draftist_updateTask({
1420 | todoist: todoist,
1421 | taskToUpdate: taskToUpdate,
1422 | newDueDateString: newDueDateString
1423 | })
1424 | }
1425 |
1426 |
1427 | /**
1428 | * Draftist_selectTasksFromTaskObjects - displays a prompt to let the user select one or multiple tasks and returns the selected ones
1429 | *
1430 | * @param {Todoist_Task[]} taskObjects - array of todoist task objects
1431 | * @param {Boolean} allowSelectMultiple - parameter to define if selecting multiple tasks shall be allowed (true) or not (false)
1432 | * @param {String} promptMessage? - a descriptive message which should be displayed inside the prompt
1433 | * @returns {Todoist_Task[]} - Array of selected Todoist Task (might be empty)
1434 | */
1435 | function Draftist_selectTasksFromTaskObjects(taskObjects, allowSelectMultiple, promptMessage = "") {
1436 | let selectedTasks = [];
1437 | if (taskObjects.length == 0) {
1438 | // return empty array immediately
1439 | return [];
1440 | }
1441 | let pTasks = new Prompt();
1442 | pTasks.title = "select tasks";
1443 | if (promptMessage != "") {
1444 | pTasks.message = promptMessage;
1445 | }
1446 | pTasks.addSelect("selectedTasks", "", taskObjects.map(task => task.content), [], allowSelectMultiple)
1447 | pTasks.addButton("select")
1448 | if (pTasks.show()) {
1449 | const selectedTaskContents = pTasks.fieldValues["selectedTasks"];
1450 | // iterate through the selected task contents
1451 | for (taskContent of selectedTaskContents) {
1452 | // add the task Object with the selected content to the array
1453 | selectedTasks = selectedTasks.concat(taskObjects.filter(task => (task.content == taskContent)))
1454 | }
1455 | } else {
1456 | Draftist_cancelAction("select tasks", "user aborted")
1457 | }
1458 | return selectedTasks;
1459 | }
1460 |
1461 | /**
1462 | * Draftist_helperGetAnyPresentLabelNamesInTasks - gets the names of labels present in at least one of the provided tasks
1463 | * @param {Todoist_Task[]} taskObjects - array of todoist tasks
1464 | * @returns {String[]} Array of present label names in at least one of the provided tasks
1465 | */
1466 | function Draftist_helperGetAnyPresentLabelNamesInTasks(taskObjects) {
1467 | // prevent empty taskObjects and return empty array in that case
1468 | if (taskObjects.length == 0) {
1469 | return [];
1470 | }
1471 | // use a Set to prevent duplicates
1472 | let labelNames = new Set();
1473 |
1474 | // load stored data if not laoded already
1475 | if (labelsIdToNameMap.size == 0) {
1476 | Draftist_getStoredTodoistData();
1477 | }
1478 |
1479 | for (task of taskObjects) {
1480 | // add each label id to the set
1481 | for (labelName of task.labels) {
1482 | labelNames.add(labelName)
1483 | }
1484 | }
1485 | // convert to array to return it
1486 | return Array.from(labelNames.values())
1487 | }
1488 |
1489 | /**
1490 | * Draftist_helperGetCommonPresentLabelNamesInTasks - gets the names of labels present in all of the provided tasks
1491 | * @param {Todoist_Task[]} taskObjects - array of todoist tasks
1492 | * @returns {String[]} Array of present label names present in all of the provided tasks
1493 | */
1494 | function Draftist_helperGetCommonPresentLabelNamesInTasks(taskObjects) {
1495 | // prevent empty taskObjects and return empty array in that case
1496 | if (taskObjects.length == 0) {
1497 | return [];
1498 | }
1499 |
1500 | // load stored data if not laoded already
1501 | if (labelsIdToNameMap.size == 0) {
1502 | Draftist_getStoredTodoistData();
1503 | }
1504 |
1505 | // logic:
1506 | // 1) start with the first task and store its label names
1507 | // 2) if no labels are present, immideately return an empty array
1508 | // 3) repeat with each task: check if all current stored labels are present in it
1509 | // 3.1) if yes, continue
1510 | // 3.2) if not, remove the labels not present from the store and continue
1511 | // 4) get the names from all remaining labels and return as an array
1512 |
1513 | let presentLabelNames = taskObjects[0].labels;
1514 |
1515 | if (presentLabelNames.length == 0) {
1516 | return [];
1517 | }
1518 |
1519 | for (task of taskObjects) {
1520 | presentLabelNames = presentLabelNames.filter(x => task.labels.includes(x))
1521 | }
1522 |
1523 |
1524 | return presentLabelNames
1525 | }
1526 |
1527 | /**
1528 | * Draftist_helperGetLabelNamesNotPresentInAllTasks - gets the names of labels not present in all of the provided tasks
1529 | * @param {Todoist_Task[]} taskObjects - array of todoist tasks
1530 | * @returns {String[]} Array of present label names not present in all of the provided tasks
1531 | */
1532 | function Draftist_helperGetLabelNamesNotPresentInAllTasks(taskObjects) {
1533 | // get all labelNames
1534 | // load stored data if not laoded already
1535 | if (labelsNameToIdMap.size == 0) {
1536 | Draftist_getStoredTodoistData();
1537 | }
1538 | let labelNames = Array.from(labelsNameToIdMap.keys());
1539 | let commonLabels = Draftist_helperGetCommonPresentLabelNamesInTasks(taskObjects)
1540 |
1541 | return labelNames.filter(x => !commonLabels.includes(x))
1542 | }
1543 |
1544 |
1545 | /**
1546 | * Draftist_updateLabelsOfSelectedTasksFromFilter - updates the label(s) of the selected tasks returned for the provided filter
1547 | * @param {String} filterString - a valid todoist filter string
1548 | * @returns true if updated successfully, false if update failed or user cancelled
1549 | */
1550 | function Draftist_updateLabelsOfSelectedTasksFromFilter(filterString) {
1551 | let tasksFromFilter = Draftist_getTodoistTasksFromFilter(filterString)
1552 | // early retrun if no task was retrieved
1553 | if (!tasksFromFilter) {
1554 | return false;
1555 | }
1556 | // let the user select the tasks
1557 | let selectedTasks = Draftist_selectTasksFromTaskObjects(tasksFromFilter, true, "from filter \"" + filterString + "\"");
1558 | let availableLableNames = Draftist_helperGetAnyPresentLabelNamesInTasks(selectedTasks);
1559 | let potentialLabelNamesToAdd = Draftist_helperGetLabelNamesNotPresentInAllTasks(selectedTasks);
1560 |
1561 | if (selectedTasks.length == 0) {
1562 | return false;
1563 | }
1564 | // load stored data if not laoded already
1565 | if (labelsNameToIdMap.size == 0) {
1566 | Draftist_getStoredTodoistData();
1567 | }
1568 |
1569 | // declare before if condition
1570 | let labelNamesToRemove = []
1571 | // only present remove menu if any label is present
1572 | if (availableLableNames.length > 0) {
1573 | let pLabelsToRemove = new Prompt()
1574 | pLabelsToRemove.title = "select labels to remove";
1575 | pLabelsToRemove.message = "all selected labels will be removed from the tasks (if they have the tags assigned). If you don't want to remove labels, just select no label and press \"select\""
1576 | pLabelsToRemove.addSelect("labelsToRemove", "", availableLableNames, [], true)
1577 | pLabelsToRemove.addButton("select")
1578 | if (pLabelsToRemove.show()) {
1579 | labelNamesToRemove = pLabelsToRemove.fieldValues["labelsToRemove"]
1580 | } else {
1581 | Draftist_cancelAction("update labels", "user cancelled")
1582 | return false;
1583 | }
1584 | }
1585 |
1586 | let labelNamesToAdd = []
1587 | let pLabelsToAdd = new Prompt()
1588 | pLabelsToAdd.title = "select labels to add";
1589 | pLabelsToAdd.message = "all selected labels will be added to the selected tasks. If you don't want to add labels, just select no label and press \"select\""
1590 | pLabelsToAdd.addSelect("labelsToAdd", "", potentialLabelNamesToAdd, [], true)
1591 | pLabelsToAdd.addButton("select")
1592 | if (pLabelsToAdd.show()) {
1593 | labelNamesToAdd = pLabelsToAdd.fieldValues["labelsToAdd"]
1594 | } else {
1595 | Draftist_cancelAction("update labels", "user cancelled")
1596 | return false;
1597 | }
1598 |
1599 | // create todoist object to use
1600 | let todoistObj = new Todoist()
1601 | let updatedTasksCount = 0;
1602 | // iterate through all selected tasks and update them
1603 | for (task of selectedTasks) {
1604 | if (!Draftist_updateLabelsOfTask({
1605 | todoist: todoistObj,
1606 | taskToUpdate: task,
1607 | labelNamesToRemove: labelNamesToRemove,
1608 | labelNamesToAdd: labelNamesToAdd
1609 | })) {
1610 | // failed updating failure is already presented, just exit the function here
1611 | return false;
1612 | } else {
1613 | updatedTasksCount = updatedTasksCount + 1;
1614 | }
1615 | }
1616 |
1617 | Draftist_succeedAction("update labels", false, "updated labels of " + updatedTasksCount + " tasks")
1618 | return true;
1619 |
1620 | }
1621 |
1622 |
1623 | /**
1624 | * Draftist_updateProjectOfSelectedTasksFromFilter - ATTENTION: this is currently not supported by Todoists API - updates the project of the selected tasks returned for the provided filter
1625 | * @param {String} filterString - a valid todoist filter string
1626 | * @returns true if updated successfully, false if update failed or user cancelled
1627 | */
1628 | function Draftist_updateProjectOfSelectedTasksFromFilter(filterString) {
1629 | // fail the action until project id is supported by Todoist:
1630 | Draftist_failAction("update project of tasks", "this is currently not supported by Todoist")
1631 | return false;
1632 | let tasksFromFilter = Draftist_getTodoistTasksFromFilter(filterString)
1633 | // early retrun if no task was retrieved
1634 | if (!tasksFromFilter) {
1635 | return false;
1636 | }
1637 | // let the user select the tasks
1638 | let selectedTasks = Draftist_selectTasksFromTaskObjects(tasksFromFilter, true, "from filter \"" + filterString + "\"");
1639 | if (selectedTasks.length == 0) {
1640 | return false;
1641 | }
1642 |
1643 | // load stored data if not laoded already
1644 | if (projectsNameToIdMap.size == 0) {
1645 | Draftist_getStoredTodoistData();
1646 | }
1647 | let pProject = new Prompt();
1648 | pProject.title = "select new project"
1649 | // add a button to the prompt for each available project name
1650 | Array.from(projectsNameToIdMap.keys()).map((x) => pProject.addButton(x));
1651 |
1652 | if (!pProject.show()) {
1653 | // user did not select a project
1654 | }
1655 |
1656 | const selectedProject = pProject.buttonPressed;
1657 |
1658 | // create todoist object to use
1659 | let todoistObj = new Todoist()
1660 | let updatedTasksCount = 0;
1661 | // iterate through all selected tasks and update them
1662 | for (task of selectedTasks) {
1663 | if (!Draftist_updateProjectOfTask({
1664 | todoistObj,
1665 | taskToUpdate: task,
1666 | newProjectName: selectedProject
1667 | })) {
1668 | // failed updating failure is already presented, just exit the function here
1669 | return false;
1670 | } else {
1671 | updatedTasksCount = updatedTasksCount + 1;
1672 | }
1673 | }
1674 |
1675 | Draftist_succeedAction("update project", false, "updated project of " + updatedTasksCount + " tasks")
1676 | return true;
1677 | }
1678 |
1679 | /**
1680 | * Draftist_duplicateSelectedTasksFromLabelWithOtherLabel - duplicates each selected task with a source label. the duplicated tasks will not contain the source label anymore but will contain the destination label
1681 | *
1682 | * @param {Todoist_Object} todoistObj? - the todoist object to use
1683 | * @param {String} sourceLabelName - the name of the source label (must be a valid name of a label in the users todoist account) with or without the @ sign
1684 | * @param {String} destinationLabelName - the name of the destination label (must be a valid name of a label in the users todoist account) with or without the @ sign
1685 | * @returns true if duplicating all selected tasks succeeded, false if it failed (will not proceed if one task fails)
1686 | */
1687 | function Draftist_duplicateSelectedTasksFromLabelWithOtherLabel({
1688 | todoistObj = new Todoist(),
1689 | sourceLabelName,
1690 | destinationLabelName
1691 | }) {
1692 | // remove "@" sign from labelNames if they are present.
1693 | sourceLabelName = sourceLabelName.replace(/@(.*)/gm, `$1`);
1694 | destinationLabelName = destinationLabelName.replace(/@(.*)/gm, `$1`);
1695 |
1696 | // load stored data if not laoded already
1697 | if (labelsNameToIdMap.size == 0) {
1698 | Draftist_getStoredTodoistData();
1699 | }
1700 | //get label Ids for source and destination label
1701 | const sourceLabelId = labelsNameToIdMap.get(sourceLabelName);
1702 | const destinationLabelId = labelsNameToIdMap.get(destinationLabelName);
1703 | if (!sourceLabelId) {
1704 | // source label id is not existing
1705 | Draftist_failAction("duplicate task with different label", "source label \"" + sourceLabelName + "\" not found");
1706 | return false;
1707 | }
1708 | if (!destinationLabelId) {
1709 | // destination label id is not existing
1710 | Draftist_failAction("duplicate task with different label", "destination label \"" + destinationLabelName + "\" not found");
1711 | return false;
1712 | }
1713 |
1714 | // retrieve all tasks with the given source label name and let the user select the tasks to duplicate
1715 | const sourceTasks = Draftist_getTodoistTasksFromFilter("@" + sourceLabelName);
1716 | const selectedTasks = Draftist_selectTasksFromTaskObjects(sourceTasks, true, "duplicate tasks from @" + sourceLabelName + " to @" + destinationLabelName);
1717 | if (selectedTasks.length == 0) {
1718 | Draftist_cancelAction("", "user cancelled / did not select any task")
1719 | return true;
1720 | }
1721 | let createdTasksCount = 0;
1722 | for (task of selectedTasks) {
1723 | // create a new task object, remove the source label and add the destination label
1724 | let curNewTask = task;
1725 |
1726 | if (task.due) {
1727 | curNewTask.due_string = task.due.string;
1728 | }
1729 |
1730 | let labels = new Set(task.labels);
1731 | if (!labels.has(destinationLabelName)) {
1732 | labels.add(destinationLabelName)
1733 | }
1734 | labels.delete(sourceLabelName)
1735 | curNewTask.labels = Array.from(labels.values());
1736 | // delete section id if it is zero (Todoist will otherwise report an error)
1737 | if (curNewTask.section_id == 0) {
1738 | delete curNewTask["section_id"];
1739 | }
1740 | if (!todoistObj.createTask(curNewTask)) {
1741 | let lastError = Draftist_getLastTodoistError(todoistObj);
1742 | Draftist_failAction("duplicate task with different label", "Todoist returned error:\n" + lastError)
1743 | return false;
1744 | }
1745 | createdTasksCount = createdTasksCount + 1;
1746 | }
1747 | Draftist_succeedAction("duplicate task with different label", false, "created " + createdTasksCount + " tasks")
1748 | return true;
1749 | }
1750 |
1751 |
1752 | /**
1753 | * Draftist_changeLabelofSelectedTasksToOtherLabel - changes the given label of each selected task to the new label
1754 | *
1755 | * @param {Todoist_Object} todoistObj? - the todoist object to use
1756 | * @param {String} sourceLabelName - the name of the source label (must be a valid name of a label in the users todoist account) with or without the @ sign
1757 | * @param {String} destinationLabelName - the name of the destination label (must be a valid name of a label in the users todoist account) with or without the @ sign
1758 | * @returns true if updating all selected tasks succeeded, false if it failed (will not proceed if one task fails)
1759 | */
1760 | function Draftist_changeLabelofSelectedTasksToOtherLabel({
1761 | todoistObj = new Todoist(),
1762 | sourceLabelName,
1763 | destinationLabelName
1764 | }) {
1765 | // remove "@" sign from labelNames if they are present.
1766 | sourceLabelName = sourceLabelName.replace(/@(.*)/gm, `$1`);
1767 | destinationLabelName = destinationLabelName.replace(/@(.*)/gm, `$1`);
1768 |
1769 | // load stored data if not laoded already
1770 | if (labelsNameToIdMap.size == 0) {
1771 | Draftist_getStoredTodoistData();
1772 | }
1773 | //get label Ids for source and destination label
1774 | const sourceLabelId = labelsNameToIdMap.get(sourceLabelName);
1775 | const destinationLabelId = labelsNameToIdMap.get(destinationLabelName);
1776 | if (!sourceLabelId) {
1777 | // source label id is not existing
1778 | Draftist_failAction("change label of task", "source label \"" + sourceLabelName + "\" not found");
1779 | return false;
1780 | }
1781 | if (!destinationLabelId) {
1782 | // destination label id is not existing
1783 | Draftist_failAction("change label of task", "destination label \"" + destinationLabelName + "\" not found");
1784 | return false;
1785 | }
1786 |
1787 | // retrieve all tasks with the given source label name and let the user select the tasks to duplicate
1788 | const sourceTasks = Draftist_getTodoistTasksFromFilter("@" + sourceLabelName);
1789 | const selectedTasks = Draftist_selectTasksFromTaskObjects(sourceTasks, true, "duplicate tasks from @" + sourceLabelName + " to @" + destinationLabelName);
1790 | if (selectedTasks.length == 0) {
1791 | Draftist_cancelAction("", "user cancelled / did not select any task")
1792 | return true;
1793 | }
1794 | let updatedTasksCount = 0;
1795 | for (task of selectedTasks) {
1796 |
1797 | Draftist_updateLabelsOfTask({
1798 | todoist: todoistObj,
1799 | taskToUpdate: task,
1800 | labelNamesToRemove: [sourceLabelName],
1801 | labelNamesToAdd: [destinationLabelName]
1802 | })
1803 |
1804 | updatedTasksCount = updatedTasksCount + 1;
1805 | }
1806 | Draftist_succeedAction("change label of task", false, "created " + updatedTasksCount + " tasks")
1807 | return true;
1808 | }
1809 |
1810 | /**
1811 | * Draftist_helperGetNewDueDateFromPrompt - asks the user for a due date and creates an iso date string from the selected date
1812 | * @param {String} taskContent - the content of the task(s) to display in the prompt
1813 | * @returns selected due date as String in the format "YYYY-MM-DD"
1814 | */
1815 | function Draftist_helperGetNewDueDateFromPrompt(taskContent) {
1816 | // due date prompt
1817 | let pDate = new Prompt()
1818 | pDate.title = "select due date for task(s):";
1819 | pDate.message = taskContent;
1820 | pDate.addButton("today");
1821 | pDate.addButton("tomorrow");
1822 | pDate.addButton("other");
1823 | pDate.addButton("remove due date")
1824 | pDate.isCancellable = false;
1825 | pDate.show();
1826 | // if buttonPressed is undefined no due date was selected
1827 | const dateIsSet = (pDate.buttonPressed ? true : false);
1828 | let selectedDateString = undefined;
1829 | if (dateIsSet) {
1830 | if (pDate.buttonPressed == "other") {
1831 | let pSelDate = new Prompt();
1832 | pSelDate.title = "select custom date for \"" + taskContent + "\":";
1833 | let tomorrow = new Date(new Date().setDate(new Date().getDate() + 1));
1834 | pSelDate.addDatePicker("dueDatePicker", "", tomorrow, {
1835 | "mode": "date",
1836 | "minimumDate": tomorrow
1837 | });
1838 |
1839 | pSelDate.addButton("set due date");
1840 | pSelDate.isCancellable = false;
1841 | pSelDate.show();
1842 | let pickedDueDate = pSelDate.fieldValues["dueDatePicker"];
1843 | let day = pickedDueDate.getDate();
1844 | let month = pickedDueDate.getMonth() + 1;
1845 | let year = pickedDueDate.getFullYear();
1846 | selectedDateString = String(year) + "-" + String(month) + "-" + String(day);
1847 | } else {
1848 | let today = new Date()
1849 | switch (pDate.buttonPressed) {
1850 | case "today":
1851 | selectedDateString = today.getFullYear() + "-" + (today.getMonth() + 1) + "-" + today.getDate();
1852 | break;
1853 | //case "today": selectedDateString = today.toISOString(); break;
1854 | case "tomorrow":
1855 | let tomorrow = today.setDate(today.getDate() + 1);
1856 | selectedDateString = today.getFullYear() + "-" + (today.getMonth() + 1) + "-" + today.getDate();
1857 | break;
1858 | case "remove due date":
1859 | selectedDateString = "no date";
1860 | break;
1861 | }
1862 | }
1863 | }
1864 | return selectedDateString;
1865 | }
1866 |
1867 | /**
1868 | * Draftust_updateIndividualDueDateOfSelectedTasksFromFilter - updates the due date of each selected task from a filter to an individual selectable due date
1869 | *
1870 | * @param {String} filterString - a valid todoist filter string
1871 | * @returns true if all selected tasks updated successfully, false if something failed
1872 | */
1873 | function Draftust_updateIndividualDueDateOfSelectedTasksFromFilter(filterString) {
1874 | let tasksFromFilter = Draftist_getTodoistTasksFromFilter(filterString)
1875 | // early retrun if no task was retrieved
1876 | if (!tasksFromFilter) {
1877 | return false;
1878 | }
1879 | // let the user select the tasks
1880 | let selectedTasks = Draftist_selectTasksFromTaskObjects(tasksFromFilter, true, "from filter \"" + filterString + "\"");
1881 | if (selectedTasks.length == 0) {
1882 | return false;
1883 | }
1884 |
1885 | // iterate through all selected tasks
1886 | // ask for new due date
1887 | // update the task
1888 | let updatedTasksCount = 0;
1889 | let todoistObj = new Todoist();
1890 | for (task of selectedTasks) {
1891 | let newDueDateString = Draftist_helperGetNewDueDateFromPrompt(task.content)
1892 |
1893 | if (!Draftist_updateDueDateOfTask({
1894 | todoist: todoistObj,
1895 | taskToUpdate: task,
1896 | newDueDateString: newDueDateString
1897 | })) {
1898 | // not needed, update function will already display the error
1899 | //let lastError = Draftist_getLastTodoistError(todoistObj);
1900 | //Draftist_failAction("update due date", "Todoist returned error:\n" + lastError)
1901 | return false;
1902 | }
1903 | updatedTasksCount = updatedTasksCount + 1;
1904 | }
1905 | Draftist_succeedAction("update due date", false, "updated " + updatedTasksCount + " tasks")
1906 | return true;
1907 | }
1908 |
1909 | /**
1910 | * Draftust_updateDueDateToSameDateOfSelectedTasksFromFilter - updates the due date of each selected task from a filter to one common selectable due date
1911 | *
1912 | * @param {String} filterString - a valid todoist filter string
1913 | * @returns true if all selected tasks updated successfully, false if something failed
1914 | */
1915 | function Draftust_updateDueDateToSameDateOfSelectedTasksFromFilter(filterString) {
1916 | let tasksFromFilter = Draftist_getTodoistTasksFromFilter(filterString)
1917 | // early retrun if no task was retrieved
1918 | if (!tasksFromFilter) {
1919 | return false;
1920 | }
1921 | // let the user select the tasks
1922 | let selectedTasks = Draftist_selectTasksFromTaskObjects(tasksFromFilter, true, "from filter \"" + filterString + "\"");
1923 | if (selectedTasks.length == 0) {
1924 | return false;
1925 | }
1926 |
1927 | // iterate through all selected tasks
1928 | // ask for new due date
1929 | // update the task
1930 | let updatedTasksCount = 0;
1931 | let todoistObj = new Todoist();
1932 | let newDueDateString = Draftist_helperGetNewDueDateFromPrompt(selectedTasks.map((task) => task.content).join("\n"))
1933 | for (task of selectedTasks) {
1934 | if (!Draftist_updateDueDateOfTask({
1935 | todoist: todoistObj,
1936 | taskToUpdate: task,
1937 | newDueDateString: newDueDateString
1938 | })) {
1939 | // not needed, update function will already display the error
1940 | //let lastError = Draftist_getLastTodoistError(todoistObj);
1941 | //Draftist_failAction("update due date", "Todoist returned error:\n" + lastError)
1942 | return false
1943 | }
1944 | updatedTasksCount = updatedTasksCount + 1;
1945 | }
1946 | Draftist_succeedAction("update due date", false, "updated " + updatedTasksCount + " tasks")
1947 | return true;
1948 | }
1949 |
1950 | /**
1951 | * Draftist_resolveSelectedTasksFromFilter - resolves the selected tasks from a filter
1952 | *
1953 | * @param {String} filterString - a valid todoist filter string
1954 | * @returns true if all selected tasks were resolved successfully, false if something failed
1955 | */
1956 | function Draftist_resolveSelectedTasksFromFilter(filterString) {
1957 | let tasksFromFilter = Draftist_getTodoistTasksFromFilter(filterString)
1958 | // early retrun if no task was retrieved
1959 | if (!tasksFromFilter) {
1960 | return false;
1961 | }
1962 | // let the user select the tasks
1963 | let selectedTasks = Draftist_selectTasksFromTaskObjects(tasksFromFilter, true, "from filter \"" + filterString + "\"");
1964 | if (selectedTasks.length == 0) {
1965 | return false;
1966 | }
1967 | let todoist = new Todoist()
1968 | let resolvedTasksCount = 0;
1969 | for (task of selectedTasks) {
1970 | if (!todoist.closeTask(task.id)) {
1971 | const lastError = Draftist_getLastTodoistError(todoist);
1972 | Draftist_failAction("resolve tasks", "Todoist returned error: " + lastError)
1973 | return false;
1974 | }
1975 | resolvedTasksCount = resolvedTasksCount + 1;
1976 | }
1977 | Draftist_succeedAction("resolve tasks", false, "resolved " + resolvedTasksCount + " tasks");
1978 | return true;
1979 | }
1980 |
1981 | /**
1982 | * Draftist_deleteSelectedTasksFromFilter - deletes the selected tasks from a filter
1983 | *
1984 | * @param {String} filterString - a valid todoist filter string
1985 | * @returns true if all selected tasks were resolved successfully, false if something failed
1986 | */
1987 | function Draftist_deleteSelectedTasksFromFilter(filterString) {
1988 | let tasksFromFilter = Draftist_getTodoistTasksFromFilter(filterString)
1989 | // early retrun if no task was retrieved
1990 | if (!tasksFromFilter) {
1991 | return false;
1992 | }
1993 | // let the user select the tasks
1994 | let selectedTasks = Draftist_selectTasksFromTaskObjects(tasksFromFilter, true, "from filter \"" + filterString + "\"");
1995 | if (selectedTasks.length == 0) {
1996 | return false;
1997 | }
1998 | let todoist = new Todoist()
1999 | let deletedTasksCount = 0;
2000 | for (task of selectedTasks) {
2001 | const settings = {
2002 | "method": "DELETE",
2003 | "url": "https://api.todoist.com/rest/v1/tasks/" + task.id
2004 | }
2005 | if (!todoist.request(settings)) {
2006 | const lastError = Draftist_getLastTodoistError(todoist);
2007 | Draftist_failAction("resolve tasks", "Todoist returned error: " + lastError)
2008 | return false;
2009 | }
2010 | deletedTasksCount = deletedTasksCount + 1;
2011 | }
2012 | Draftist_succeedAction("delete tasks", false, "deleted " + deletedTasksCount + " tasks");
2013 | return true;
2014 | }
2015 |
2016 |
2017 | /**
2018 | * @returns Boolean to indicate success of the Action
2019 | */
2020 | function Draftist_createProjectFromDraftsTitleAndAddLinksToDraft(todoist = new Todoist(),) {
2021 | // get title of draft (use safe title)
2022 | let projectTitle = draft.processTemplate("[[safe_title]]").trim()
2023 | if (projectTitle.length == 0) {
2024 | // ask for new title of project
2025 | let tPrompt = new Prompt()
2026 | tPrompt.title = "Set Project Title"
2027 | tPrompt.addTextField("title", "", "", { wantsFocus: true })
2028 | tPrompt.addButton("set title")
2029 | if (tPrompt.show()) {
2030 | projectTitle = tPrompt.fieldValues["title"]
2031 | if (projectTitle.trim().length == 0) {
2032 | // still empty -> fail
2033 | let msg = "project title can't be empty"
2034 | Draftist_failAction("create linked project", msg)
2035 | return false;
2036 | }
2037 | // prepend project title
2038 | draft.prepend("# " + projectTitle);
2039 | }
2040 | }
2041 | let result = todoist.createProject({
2042 | "name": projectTitle
2043 | })
2044 | if (!result) {
2045 | const lastError = Draftist_getLastTodoistError(todoist);
2046 | Draftist_failAction("create project", "Todoist returned error: " + lastError)
2047 | return false;
2048 | }
2049 |
2050 | let projectId = result.id
2051 | // set projectId as template tag to be used later
2052 | draft.setTemplateTag("createdProjectId", projectId)
2053 |
2054 | let text = `
2055 | Todoist Project:
2056 | - [🌐](https://todoist.com/app/project/${projectId})
2057 | - [📱](todoist://project?id=${projectId})`
2058 |
2059 | draft.insert(text, 1)
2060 | draft.update()
2061 |
2062 | let linkedTaskContent = `* Project Draft: [${projectTitle}](${draft.permalink})`
2063 |
2064 | if (!todoist.createTask({
2065 | "content": linkedTaskContent,
2066 | "project_id": projectId
2067 | })) {
2068 | const lastError = Draftist_getLastTodoistError(todoist);
2069 | Draftist_failAction("create task in project", "Todoist returned error: " + lastError)
2070 | return false;
2071 | }
2072 |
2073 | Draftist_succeedAction("create linked project", false, "created linked project")
2074 | return true
2075 | }
2076 |
2077 |
2078 | // #############################################################################
2079 | // SETTINGS AND DATA STORAGE
2080 | // #############################################################################
2081 |
2082 | /**
2083 | * defaultSettingsParams: these are the default settings for the Action Group
2084 | */
2085 | const defaultSettingsParams = {
2086 | "dataStoreUpdateInterval": 24,
2087 | "taskLinkTypes": ["app", "web"],
2088 | "taskImportContents": ["appLink", "webLink", "projectName", "priority", "labels"]
2089 | }
2090 |
2091 |
2092 | /**
2093 | * static definition of the names for the storage Drafts
2094 | */
2095 | const settingsDraftName = "Draftist Action Group Settings";
2096 | const dataStoreDraftName = "Draftist Todoist Data Store";
2097 |
2098 | /**
2099 | * global variables that are used within the various functions to access current settings - stored in variables to quicker access them in different use cases
2100 | */
2101 | let activeSettings = undefined;
2102 | let lastUpdated = undefined;
2103 | let projects = undefined;
2104 | let labels = undefined;
2105 | let sections = undefined;
2106 | let projectsNameToIdMap = new Map();
2107 | let projectsIdToNameMap = new Map();
2108 | let labelsNameToIdMap = new Map();
2109 | let labelsIdToNameMap = new Map();
2110 | const settingsFilePath = "/Library/Scripts/DraftistSettings.json"
2111 | const dataStoreFilePath = "/Library/Scripts/DraftistDataStore.json"
2112 |
2113 |
2114 | /**
2115 | * Draftist_getSettingsFromFile - reads the settings from the stored file, creates file with default settings if not already present
2116 | * @returns {Object} the settings object stored in the settings file
2117 | */
2118 | function Draftist_getSettingsFromFile() {
2119 | // iCloud file manager
2120 | let fmCloud = FileManager.createCloud();
2121 | const readResult = fmCloud.readJSON(settingsFilePath);
2122 | if (!readResult) {
2123 | // file is not existing, write initial Data
2124 | fmCloud.writeJSON(settingsFilePath, defaultSettingsParams)
2125 | return defaultSettingsParams;
2126 | } else {
2127 | // read settings into global variable
2128 | return readResult;
2129 | }
2130 | }
2131 |
2132 | /**
2133 | * Draftist_writeActiveSettingsToFile - writes the current active loaded settings to the settings file
2134 | * @returns {Boolean} true if writing was successfull or no settings are loaded right now; false if writing failed
2135 | */
2136 | function Draftist_writeActiveSettingsToFile() {
2137 | if (!activeSettings) {
2138 | // active Settings are undefined, nothing to write (but nothing failed either)
2139 | return true;
2140 | }
2141 | // iCloud file manager
2142 | let fmCloud = FileManager.createCloud();
2143 | // write active Settings to file
2144 | const writeResult = fmCloud.writeJSON(settingsFilePath, activeSettings);
2145 | return writeResult;
2146 | }
2147 |
2148 | /**
2149 | * Draftist_loadCurrentConfigurationSettings - loads the current settings stored in the settings file into the live variable of Draftist
2150 | */
2151 | function Draftist_loadCurrentConfigurationSettings() {
2152 | activeSettings = Draftist_getSettingsFromFile();
2153 | }
2154 |
2155 |
2156 | /**
2157 | * Draftist_restoreDefaultSettings - funtion to restore the default settings
2158 | *
2159 | */
2160 | function Draftist_restoreDefaultSettings() {
2161 | // iCloud file manager
2162 | let fmCloud = FileManager.createCloud();
2163 | // write active Settings to file
2164 | const writeResult = fmCloud.writeJSON(settingsFilePath, defaultSettingsParams);
2165 | if (!writeResult) {
2166 | Draftist_failAction("restore default settings", "failed writing settings");
2167 | } else {
2168 | Draftist_infoMessage("", "restored default settings")
2169 | }
2170 | }
2171 |
2172 |
2173 | /**
2174 | * Draftist_Settings - open the settings for Draftist - the user can either restore the default settings or change the current active settings
2175 | */
2176 | function Draftist_Settings() {
2177 | // load current settings
2178 | Draftist_loadCurrentConfigurationSettings();
2179 |
2180 | // ask the user if the default settings shall be restored or changes to the settings shall be made.
2181 |
2182 | let pOptions = new Prompt();
2183 | pOptions.title = "Draftist Settings"
2184 | pOptions.addButton("restore default settings");
2185 | pOptions.addButton("change current settings");
2186 | if (pOptions.show()) {
2187 | // user selected an option
2188 | // execute the corresponding functions based on the selection
2189 | switch (pOptions.buttonPressed) {
2190 | case "restore default settings":
2191 | Draftist_restoreDefaultSettings();
2192 | break;
2193 | case "change current settings":
2194 | Draftist_changeConfigurationSettings();
2195 | break;
2196 | }
2197 | }
2198 | }
2199 |
2200 | /**
2201 | * Draftist_changeConfigurationSettings - function to change the current active settings of Draftist and store them in the settings Draft
2202 | *
2203 | */
2204 | function Draftist_changeConfigurationSettings() {
2205 | let proceedSettingsPrompts = true;
2206 | if (proceedSettingsPrompts) {
2207 | // setting for local storage usage
2208 | let pStore = new Prompt();
2209 | pStore.title = "update inteval for todoist data"
2210 | pStore.message = "the action group stores todoist data locally in a Draft, this includes e.g. project/label names, ids which are necessary to quickly add tasks to projects (or add labels to tasks), the local storage speeds up creating tasks a lot. The data will be updated in the time period of your choice (in hours default: every 24h)";
2211 | pStore.addSelect("updateInterval", "update interval [h]", ["1", "2", "5", "10", "24", "36", "48"], [activeSettings["dataStoreUpdateInterval"].toString()], false)
2212 | pStore.addButton("Apply");
2213 | if (pStore.show()) {
2214 | // user selected to apply the settings
2215 | // store the setting in current active settings variable
2216 | activeSettings["dataStoreUpdateInterval"] = parseInt(pStore.fieldValues["updateInterval"])
2217 | } else {
2218 | proceedSettingsPrompts = false;
2219 | }
2220 | }
2221 | if (proceedSettingsPrompts) {
2222 | // settings for crosslinked Task Urls
2223 | let pTaskLinks = new Prompt();
2224 | pTaskLinks.title = "task link settings"
2225 | pTaskLinks.message = "the crosslink task actions can append / prepend links to the created tasks in Todoist to the current draft. App links only work reliably on iOS / iPadOS - If you want to use task links on macOS, too you need to enable web links"
2226 | pTaskLinks.addSelect("linkTypes", "link types", ["app", "web"], activeSettings["taskLinkTypes"], true)
2227 | pTaskLinks.addButton("Apply");
2228 | if (pTaskLinks.show()) {
2229 | activeSettings["taskLinkTypes"] = pTaskLinks.fieldValues["linkTypes"]
2230 | } else {
2231 | proceedSettingsPrompts = false;
2232 | }
2233 | }
2234 |
2235 | if (proceedSettingsPrompts) {
2236 | // settings for import task contents
2237 | let pImportContents = new Prompt();
2238 | pImportContents.title = "task import content settings"
2239 | pImportContents.message = "each imported tasks will contain the information you select in this prompt"
2240 | pImportContents.addSelect("taskImportContents", "task import contents", ["appLink", "webLink", "projectName", "priority", "labels"], activeSettings["taskImportContents"], true)
2241 | pImportContents.addButton("Apply");
2242 | if (pImportContents.show()) {
2243 | activeSettings["taskImportContents"] = pImportContents.fieldValues["taskImportContents"]
2244 | } else {
2245 | proceedSettingsPrompts = false;
2246 | }
2247 | }
2248 |
2249 | if (!Draftist_writeActiveSettingsToFile()) {
2250 | Draftist_failAction("change settings", "unexpected failure, please try again and if the issue persists, contact @FlohGro with a description to reproduce the issue.")
2251 | } else {
2252 | Draftist_infoMessage("", "settings updated")
2253 | }
2254 | }
2255 |
2256 | /**
2257 | * Draftist_getDataStoreFromFile - reads the stored data from the directory. updates the data if the file is not existing
2258 | * @returns true when the file was read successfully
2259 | */
2260 | function Draftist_getDataStoreFromFile() {
2261 | // iCloud file manager
2262 | let fmCloud = FileManager.createCloud();
2263 | const readResult = fmCloud.readJSON(dataStoreFilePath);
2264 | if (!readResult) {
2265 | // file is not existing, update the data which will write them to the file and then run this function again
2266 | Draftist_updateStoredTodoistData();
2267 | return Draftist_getDataStoreFromFile();
2268 | // if (!Draftist_updateStoredTodoistData()) {
2269 | // Draftist_failAction("get data store", "unexpected failure, please try again and if the issue persists, contact @FlohGro with a description to reproduce the issue.")
2270 | // }
2271 | } else {
2272 | // return the read object
2273 | return readResult
2274 | }
2275 | }
2276 |
2277 | function Draftist_writeDataStoreToFile(dataToStore) {
2278 | if (!dataToStore) {
2279 | // lastUpdated, nothing to write (but nothing failed either)
2280 | return true;
2281 | }
2282 | // iCloud file manager
2283 | let fmCloud = FileManager.createCloud();
2284 | // write data to file
2285 | const writeResult = fmCloud.writeJSON(dataStoreFilePath, dataToStore);
2286 | return writeResult;
2287 | }
2288 |
2289 |
2290 | /**
2291 | * Draftist_updateTodoistDataIfUpdateIntervalExceeded - checks if an update of the locally stored Todoist data is needed based on the settings of the dataStoreUpdateInterval
2292 | */
2293 | function Draftist_updateTodoistDataIfUpdateIntervalExceeded() {
2294 | // check if variable is defined (was initialized) otherwise load settings from draft
2295 | if (!lastUpdated) {
2296 | Draftist_getStoredTodoistData()
2297 | }
2298 | const now = Date.now();
2299 | let tDiffLastUpdate = now - lastUpdated
2300 | // check if variable is defined (was initialized) otherwise load settings from draft
2301 | if (!activeSettings) {
2302 | Draftist_loadCurrentConfigurationSettings()
2303 | }
2304 | let updateInterval = activeSettings["dataStoreUpdateInterval"] * 3600000;
2305 |
2306 | if (tDiffLastUpdate > updateInterval) {
2307 | // update is necessary
2308 | Draftist_updateStoredTodoistData();
2309 | }
2310 | }
2311 |
2312 |
2313 | /**
2314 | * Draftist_updateStoredTodoistData - updates the locally stored todoist data in the data store file
2315 | *
2316 | * @param {Todoist} todoist? - the todoist object to use
2317 | */
2318 | function Draftist_updateStoredTodoistData(todoist = new Todoist()) {
2319 | // retrieve projects from todoist with pagination support
2320 | let projects = [];
2321 | let projectsContinue = true;
2322 | let projectsCursor = null;
2323 |
2324 | while (projectsContinue) {
2325 | let projectsOptions = {};
2326 |
2327 | if (projectsCursor) {
2328 | projectsOptions = {
2329 | cursor: projectsCursor,
2330 | limit: 200
2331 | };
2332 | }
2333 |
2334 | let retrievedProjects = todoist.getProjects(projectsOptions);
2335 | projects.push(...retrievedProjects);
2336 | if (todoist.lastResponse.next_cursor) {
2337 | projectsCursor = todoist.lastResponse.next_cursor;
2338 | } else {
2339 | projectsContinue = false;
2340 | }
2341 | }
2342 | // retrieve sections from todoist with pagination support
2343 | let sections = [];
2344 | let sectionsContinue = true;
2345 | let sectionsCursor = null;
2346 |
2347 | while (sectionsContinue) {
2348 | let sectionsOptions = {};
2349 | if (sectionsCursor) {
2350 | sectionsOptions = {
2351 | cursor: sectionsCursor,
2352 | limit: 200
2353 | };
2354 | }
2355 |
2356 | let retrievedSections = todoist.getSections(sectionsOptions);
2357 | sections.push(...retrievedSections);
2358 | if (todoist.lastResponse.next_cursor) {
2359 | sectionsCursor = todoist.lastResponse.next_cursor;
2360 | } else {
2361 | sectionsContinue = false;
2362 | }
2363 | }
2364 | // retrieve labels from todoist with pagination support
2365 | let labels = [];
2366 | let labelsContinue = true;
2367 | let labelsCursor = null;
2368 |
2369 | while (labelsContinue) {
2370 | let labelsOptions = {};
2371 |
2372 | if (labelsCursor) {
2373 | labelsOptions = {
2374 | cursor: labelsCursor
2375 | };
2376 | }
2377 |
2378 | let retrievedLabels = todoist.getLabels(labelsOptions);
2379 | labels.push(...retrievedLabels);
2380 |
2381 | if (todoist.lastResponse.next_cursor) {
2382 | labelsCursor = todoist.lastResponse.next_cursor;
2383 | } else {
2384 | labelsContinue = false;
2385 | }
2386 | }
2387 |
2388 | const updatedTimeUnixMilliseconds = Date.now();
2389 |
2390 | // create object with all todoist data
2391 | const todoistDataToStore = {
2392 | "lastUpdated": updatedTimeUnixMilliseconds,
2393 | "projects": projects,
2394 | "sections": sections,
2395 | "labels": labels
2396 | }
2397 | if (!Draftist_writeDataStoreToFile(todoistDataToStore)) {
2398 | Draftist_failAction("get data store", "unexpected failure, please try again and if the issue persists, contact @FlohGro with a description to reproduce the issue.")
2399 | }
2400 | Draftist_infoMessage("", "updated local Todoist data");
2401 | }
2402 |
2403 |
2404 | /**
2405 | * Draftist_getStoredTodoistData - function to retrieve the stored Todoist Data from the data store file - the stored data will be updated if the dataStoreUpdateInterval was exceeded and the stored data will be loaded into the global variables to be accessible for all other functions
2406 | */
2407 | function Draftist_getStoredTodoistData() {
2408 |
2409 | const storedData = Draftist_getDataStoreFromFile();
2410 |
2411 | lastUpdated = parseInt(storedData["lastUpdated"]);
2412 | //projects = storedData["projects"]
2413 | projects = storedData["projects"].map((project) => {
2414 | return [project["name"], project["id"]];
2415 | })
2416 | labels = storedData["labels"].map((label) => {
2417 | return [label["name"], label["id"]];
2418 | })
2419 | sections = storedData["sections"].map((section) => {
2420 | return [section["name"], section["id"]];
2421 | })
2422 | for (p of projects) {
2423 | projectsNameToIdMap.set(p[0], p[1])
2424 | projectsIdToNameMap.set(p[1], p[0])
2425 | }
2426 | for (l of labels) {
2427 | labelsNameToIdMap.set(l[0], l[1])
2428 | labelsIdToNameMap.set(l[1], l[0])
2429 | }
2430 |
2431 | // update data from Todoist if necessary
2432 | Draftist_updateTodoistDataIfUpdateIntervalExceeded();
2433 | }
2434 |
2435 |
2436 | /**
2437 | * Draftist_helperDraftistActionReplicator - opens the installURL for the selected Action of the Draftist Action Group to easily duplicate an Action of Draftist into another ActionGroup. The user has to manually rename the created Action afterwards.
2438 | *
2439 | * @return {undefined} always returns undefined
2440 | */
2441 | function Draftist_helperDraftistActionReplicator() {
2442 | const replicatorOmitList = ["Draftist Instructions", "Draftist Setup/Update", "Draftist", "Draftist Settings", "Draftist Action Replicator", "update local Todoist data"];
2443 | const actionGroup = ActionGroup.find("Draftist");
2444 | if (!actionGroup) {
2445 | // ActionGroup not found
2446 | Draftist_failAction("replicate Action", "Draftist Action Group name was changed")
2447 | return undefined
2448 | }
2449 | let pAction = new Prompt();
2450 | pAction.title = "select action to replicate";
2451 | for (action of actionGroup.actions) {
2452 | if (action.isSeparator) {
2453 | // nothing to be done
2454 | } else {
2455 | if (replicatorOmitList.indexOf(action.name) == -1) {
2456 | pAction.addButton(action.name, action)
2457 | }
2458 | }
2459 | }
2460 | if (!pAction.show()) {
2461 | Draftist_cancelAction("replicate Draftist Action", "user cancelled")
2462 | return undefined
2463 | }
2464 | const actionToReplicate = pAction.buttonPressed;
2465 | app.openURL(actionToReplicate.installURL)
2466 | return undefined
2467 | }
2468 |
2469 | /**
2470 | * Draftist_updateDraftist - presents a prompt to let the user select to update either the Action Group (by opening the link) or the Draftist.js file. To let the user check the latest version, also a "view" option is included which opens the file in the repository
2471 | */
2472 | function Draftist_updateDraftist() {
2473 | let pConfirmationPrompt = new Prompt()
2474 | pConfirmationPrompt.title = "Update Draftist";
2475 | pConfirmationPrompt.message = "This Action can update the \"Draftist.js\" file to the newest version of the GitHub repository.\nThis is necessary for bug fixes and version updates\n\n\nTo update the Draftist Action Group itself you need to update (reinstall) Draftist from Drafts Action Directory.\nThis will update Draftist to new releases with new Actions in the Action Group\n\n\nTo view the latest version you can open it in the repository and check out the latest changes there\n\nSelect what you want to do:"
2476 | pConfirmationPrompt.addButton("View newest version", "view")
2477 | pConfirmationPrompt.addButton("Update Draftist.js", "updateJs")
2478 | pConfirmationPrompt.addButton("Update Draftist Action Group", "updateAG")
2479 | if (pConfirmationPrompt.show()) {
2480 | switch (pConfirmationPrompt.buttonPressed) {
2481 | case "view":
2482 | app.openURL("https://github.com/FlohGro-dev/Draftist/blob/main/Draftist.js", true);
2483 | break;
2484 | case "updateJs":
2485 | Draftist_setupOrUpdateDraftistJsFilte();
2486 | break;
2487 | case "updateAG":
2488 | app.openURL("https://directory.getdrafts.com/g/1wK", false);
2489 | break;
2490 | }
2491 | }
2492 | }
2493 |
2494 | /**
2495 | * Draftist_setupOrUpdateDraftistJsFilte - this Action updates the Draftist.js file in the iCloud directory of the Drafts/Library folder to the latest version from GitHub
2496 | * @returns true if update successful, false if update was not performed successfully
2497 | */
2498 | function Draftist_setupOrUpdateDraftistJsFilte() {
2499 | const filename = "Draftist.js"
2500 | const subfoldername = "Scripts"
2501 | const filepath = "/Library/" + subfoldername + "/"
2502 |
2503 | // file url "https://github.com/FlohGro-dev/Draftist/blob/main/Draftist.js"
2504 | // need the raw url to get the files content
2505 | const draftistSourceUrl = "https://raw.githubusercontent.com/FlohGro-dev/Draftist/main/Draftist.js"
2506 | let fmCloud = FileManager.createCloud();
2507 | fmCloud.createDirectory(subfoldername, "/Library/");
2508 |
2509 | http = new HTTP();
2510 | // get the file
2511 | let requestResult = http.request({
2512 | "url": draftistSourceUrl,
2513 | "method": "GET"
2514 | });
2515 | // check if the result was successful
2516 | if (requestResult.success) {
2517 | fmCloud.writeString(filepath + filename, requestResult.responseText)
2518 | } else {
2519 | Draftist_failAction("setup or update", "download failed")
2520 | return false;
2521 | }
2522 | Draftist_succeedAction("setup/update Draftist", true, "downloaded latest version")
2523 | return true;
2524 | }
2525 |
2526 |
2527 | // dev part
2528 |
2529 | // create a function that can retrieve all projects with their name and internal link in a markdown format
2530 | function Draftist_TEST_createProjectsMdList() {
2531 | Draftist_updateStoredTodoistData()
2532 | if (projectsNameToIdMap.size == 0) {
2533 | Draftist_getStoredTodoistData();
2534 | }
2535 | let sortedProjectNameMap = new Map([...projectsNameToIdMap].sort((a, b) => String(a[0]).localeCompare(b[0])))
2536 |
2537 | let projectMdLinks = []
2538 |
2539 | for (const [pName, pId] of projectsNameToIdMap) {
2540 | let str = "[" + pName + "](todoist://project?id=" + pId + ")"
2541 | // find related draft
2542 | let foundDrafts = Draft.queryByTitle(pName)
2543 | let dToUse = undefined
2544 | if (foundDrafts.length == 0) {
2545 | // no draft found, thats ok
2546 | } else if (foundDrafts.length == 1) {
2547 | // found just one draft -> perfect!
2548 | dToUse = foundDrafts[0]
2549 | } else {
2550 | // found several drafts, not that good :D
2551 | let p = new Prompt()
2552 | p.title = pName
2553 | p.message = "found several drafts, select one:"
2554 | for (d of foundDrafts) {
2555 | p.addButton(d.displayTitle, d)
2556 | }
2557 | if (p.show) {
2558 | dToUse = p.buttonPressed
2559 | }
2560 | }
2561 | if (dToUse) {
2562 | str = str + " [" + dToUse.displayTitle + "](" + dToUse.permalink + ")"
2563 | }
2564 | projectMdLinks.push(str)
2565 | }
2566 |
2567 | let d = new Draft()
2568 | d.content = "# Todoist Project MD Links List\n\n" + projectMdLinks.join("\n")
2569 | d.update()
2570 | editor.load(d)
2571 |
2572 | }
2573 |
--------------------------------------------------------------------------------