diff --git a/MMM-Todoist.css b/MMM-Todoist.css index 313155e..397ac2e 100644 --- a/MMM-Todoist.css +++ b/MMM-Todoist.css @@ -94,3 +94,26 @@ width: 400px; overflow: hidden; } + +.add-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; +} + +.add-list-item-add { + display: flex; + flex-direction: column; + width: 50px; + height: 50px; + margin: 10px; + font-size: 40px; + font-weight: bold; + color: white; + text-align: center; + padding: 20px 10px 10px 10px; + border: 1px solid black; + border-radius: 10px; + background-color: grey; +} diff --git a/MMM-Todoist.js b/MMM-Todoist.js index fee163a..888bff2 100644 --- a/MMM-Todoist.js +++ b/MMM-Todoist.js @@ -9,6 +9,8 @@ */ /* + * Update by dirtylimerix 24/11/2024 + * - Added support for adding to lists and new tasks * Update by mabahj 24/11/2019 * - Added support for labels in addtion to projects * Update by AgP42 the 18/07/2018 @@ -58,7 +60,7 @@ Module.register("MMM-Todoist", { // "#ffcc00", "#74e8d3", "#3bd5fb", "#dc4fad", "#ac193d", "#d24726", "#82ba00", "#03b3b2", "#008299", // "#5db2ff", "#0072c6", "#000000", "#777777" // ], //These colors come from Todoist and their order matters if you want the colors to match your Todoist project colors. - + //TODOIST Change how they are doing Project Colors, so now I'm changing it. projectColors: { 30:'#b8256f', @@ -83,11 +85,13 @@ Module.register("MMM-Todoist", { 49:'#ccac93' }, - //This has been designed to use the Todoist Sync API. + // list input parameters + inputTasks: [], + + // Non-configurable parameters apiVersion: "v9", apiBase: "https://todoist.com/API", todoistEndpoint: "sync", - todoistResourceType: "[\"items\", \"projects\", \"collaborators\", \"user\", \"labels\"]", debug: false @@ -158,6 +162,12 @@ Module.register("MMM-Todoist", { UserPresence = payload; this.GestionUpdateIntervalToDoIst(); } + + if (notification == "KEYBOARD_INPUT" && payload.key === "MMM-Todoist") { + //this.addItemToList(payload.data, payload.message); + var ndata = {"config" : this.config, "addData" : payload}; + this.sendSocketNotification("ADDITEM_TODOIST", ndata); + } }, GestionUpdateIntervalToDoIst: function () { @@ -237,6 +247,9 @@ Module.register("MMM-Todoist", { Log.log("ToDoIst update OK, project : " + this.config.projects + " at : " + moment.unix(this.lastUpdate).format(this.config.displayLastUpdateFormat)); //AgP } + this.loaded = true; + this.updateDom(1000); + } else if (notification === "ADDITEM") { this.loaded = true; this.updateDom(1000); } else if (notification === "FETCH_ERROR") { @@ -597,33 +610,14 @@ Module.register("MMM-Todoist", { return cell; }, - getDom: function () { - - if (this.config.hideWhenEmpty && this.tasks.items.length===0) { - return null; - } - - //Add a new div to be able to display the update time alone after all the task - var wrapper = document.createElement("div"); - - //display "loading..." if not loaded - if (!this.loaded) { - wrapper.innerHTML = "Loading..."; - wrapper.className = "dimmed light small"; - return wrapper; - } - + buildTaskTable: function () { //New CSS based Table var divTable = document.createElement("div"); divTable.className = "divTable normal small light"; var divBody = document.createElement("div"); divBody.className = "divTableBody"; - - if (this.tasks === undefined) { - return wrapper; - } // create mapping from user id to collaborator index var collaboratorsMap = new Map(); @@ -637,7 +631,6 @@ Module.register("MMM-Todoist", { var divRow = document.createElement("div"); //Add the Row divRow.className = "divTableRow"; - //Columns divRow.appendChild(this.addPriorityIndicatorCell(item)); @@ -653,10 +646,89 @@ Module.register("MMM-Todoist", { } divBody.appendChild(divRow); - }); - + }); divTable.appendChild(divBody); - wrapper.appendChild(divTable); + + return divTable; + }, + + buildInputList: function() { + const addList = document.createElement("div"); + addList.className = "add-list"; + if (this.config.inputTasks.length > 0) { + // For each "inputTask", add a button according to the config parameters + for (var idx = 0; idx < this.config.inputTasks.length; idx++) { + var item = this.config.inputTasks[idx]; + var addListBtn = document.createElement("div"); + var symbol = "plus" + if (item["symbol"]) { + symbol = item["symbol"]; + } + addListBtn.className = "add-list-item-add fas fa-" + symbol; + addListBtn.id = item["project"] + "-" + item["task"]; + if (item["color"]) { + addListBtn.style.color = item["color"]; + } + if (item["bg-color"]) { + addListBtn.style.backgroundColor = item["bg-color"]; + } + addListBtn.addEventListener("click", event => { + this.sendNotification("KEYBOARD", { + key: "MMM-Todoist", + style: "default", + data: {"id" : event.target.id } + }); + }); + addList.appendChild(addListBtn); + } + + // If using input tasks, create a button for new inbox items + if (this.config.inputTasks.length > 0) { + var addNewBtn = document.createElement("div"); + addNewBtn.className = "add-list-item-add fas fa-square-plus"; + //addNewBtn.id = "inbox-NEW"; + addNewBtn.id = "2334830530-NEW"; + addNewBtn.style.color = "white"; + addNewBtn.style.backgroundColor = "darkgrey"; + addNewBtn.addEventListener("click", event => { + this.sendNotification("KEYBOARD", { + key: "MMM-Todoist", + style: "default", + data: {"id" : event.target.id } + }); + }); + addList.appendChild(addNewBtn); + } + } + return addList; + }, + + getDom: function () { + + if (this.config.hideWhenEmpty && this.tasks.items.length===0) { + return null; + } + + //Add a new div to be able to display the update time alone after all the task + var wrapper = document.createElement("div"); + + //display "loading..." if not loaded + if (!this.loaded) { + wrapper.innerHTML = "Loading..."; + wrapper.className = "dimmed light small"; + return wrapper; + } + + if (this.tasks === undefined) { + return wrapper; + } + + // Build the Todoist task table and add it + taskTable = this.buildTaskTable(); + wrapper.appendChild(taskTable); + // Build the input task button list (if enabled) and add it + addList = this.buildInputList(); + wrapper.appendChild(addList); // create the gradient if (this.config.fade && this.config.fadePoint < 1) divTable.querySelectorAll('.divTableRow').forEach((row, i, rows) => row.style.opacity = Math.max(0, Math.min(1 - ((((i + 1) * (1 / (rows.length))) - this.config.fadePoint) / (1 - this.config.fadePoint)) * (1 - this.config.fadeMinimumOpacity), 1))); diff --git a/README.md b/README.md index 054e995..1f3afc2 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,12 @@ modules: [ fade: false, // projects and/or labels is mandatory: projects: [ 166564794 ], - labels: [ "MagicMirror", "Important" ] // Tasks for any projects with these labels will be shown. - } + labels: [ "MagicMirror", "Important" ], // Tasks for any projects with these labels will be shown. + inputTasks: [ + {"project" : 166564794, "task" : "Groceries", "symbol" : "cart-shopping"}, + {"project" : 166564794, "task" : "Hardware Store", "symbol" : "screwdriver-wrench", "color" : "white", "bg-color" : "darkorange"} + ] + } } ] ```` @@ -37,7 +41,6 @@ modules: [ The following properties can be configured: - @@ -222,10 +225,102 @@ The following properties can be configured:
Default value:false - + + + + + +
inputTasksAdd buttons for adding tasks and subtasks to lists below task list. Depends on MMM-Keyboard module being installed.
+
Possible values: An array, see inputTask configuration below. +
Default value: [] +
+ + +## InputTask Configuration options + +InputTasks are buttons that will appear below the task list, enabling user input to add tasks to projects, or add sub-tasks to tasks. If any inputTask is specified, an additional [+] button be automatically added that allows tasks to be added to the inbox. For each specified inputTask, a *project_id* and *task* name is required. The *project_id* specifies the project under which a task will be added. The *task* specifies the parent task under which new tasks will be created (like a list). If the parent task does not exist (as a task under the *project_id* and due "today"), it will be created. + +### Example usage + +For a given config that includes the following: + +````javascript +modules: [ + { + ... + inputTasks: [ + {"project" : 166564794, "task" : "Groceries", "symbol" : "cart-shopping"}, + {"project" : 166564794, "task" : "Hardware Store", "symbol" : "screwdriver-wrench", "color" : "white", "bg-color" : "darkorange"} + ] + } + } +] +```` + +Three buttons will appear (as in the screenshot below), one for a *Groceries* with a shopping cart icon, one for *Hardware Store* with a tools icon, and one for new inbox items with a [+] icon. When you click on any button (i.e. *Groceries*) the visual keyboard (MMM-Keyboard) will show up on the screen. After you input text and hit send, that text will be the name of a new sub-task under *Groceries* in the *project_id* project. If *Groceries* did not already exist, it will be created. Everything is created with a due date of *today* by default. + + +The following properties can be configured for each inputTask: + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDescription
project(required) Project to which this task will belong.
+
Possible values: any project id +
Default value: none +
Example: 166564792 +
+ Getting the Todoist ProjectID:
+ 1) Go to Todoist (Log in if you aren't)
+ 2) Click on a Project in the left menu
+ 3) Your browser URL will change to something like
"https://todoist.com/app?lang=en&v=818#project%2F166564897"

+ Everything after %2F is the Project ID. In this case "166564792"

+
+ Alternatively, if you add debug=true in your config.js the Projects and ProjectsIDs will be displayed on MagicMirror as well as in the Browser console.

+ This value and/or the labels entry must be specified. If both projects and labels are specified, then tasks from both will be shown. +
task(required) Name of the task under which new subtasks will be placed
+
Possible values: string +
Default value: none +
Note: You can use one of three values here. +
symbol(optional) Name of the FontAwesome icon to use for an icon on the button.
+
Possible values: string +
Default value: "plus" +
Note: You can use any of the free Font Awesome icon values found here https://fontawesome.com/v6/search?o=r&m=free. +
color(optional) CSS color to use for the foreground button icon.
+
Possible values: string +
Default value: "grey" +
bg-color(optional) CSS color to use for the background of the button.
+
Possible values: string +
Default value: "white" +
+ ## Dependencies - [request](https://www.npmjs.com/package/request) (installed via `npm install`) @@ -260,6 +355,8 @@ Options enabled: orderBy:dueDateAsc, showProjects: true Options enabled: orderBy:dueDateAsc, showProjects: false ![My image](http://cbrooker.github.io/MMM-Todoist/Screenshots/7.png) +Options enabled: inputTasks +![My image](todoist_btns.png) ## Attribution diff --git a/node_helper.js b/node_helper.js index 4d189d8..58482f1 100644 --- a/node_helper.js +++ b/node_helper.js @@ -1,9 +1,10 @@ "use strict"; +const { unsubscribe } = require("diagnostics_channel"); /* Magic Mirror * Module: MMM-Todoist * - * By Chris Brooker + * By Chris Brooker, James Brock * * MIT Licensed. */ @@ -24,9 +25,14 @@ module.exports = NodeHelper.create({ this.config = payload; this.fetchTodos(); } + if (notification === "ADDITEM_TODOIST") { + this.config = payload.config; + this.addData = payload.addData; + this.fetchTodos(this.addItemToList); + } }, - fetchTodos : function() { + fetchTodos : function(callback) { var self = this; //request.debug = true; var acessCode = self.config.accessToken; @@ -60,12 +66,178 @@ module.exports = NodeHelper.create({ }); taskJson.accessToken = acessCode; - self.sendSocketNotification("TASKS", taskJson); + + if (callback) { + callback(self, taskJson); + } else { + self.sendSocketNotification("TASKS", taskJson); + } } else{ console.log("Todoist api request status="+response.statusCode); } }); + }, + + findItem: function(taskJson, reqProj, reqTask) { + var itemid = null; + taskJson.items.filter(function (item) { + if (item.project_id == reqProj) { + if (item.day_order === -1) { + if (item.due) { + let duedate = item.due["date"]; + let date = new Date(); + let year = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date); + let month = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date); + let day = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date); + let today = `${year}-${month}-${day}`; + if (duedate === today) { + if (item.content === reqTask) { + itemid = item.id; + } + } + } + } + } + }); + return itemid; + }, + + // TBD + addNewSubItemToList: function(self, proj, task, parent) { + var acessCode = self.config.accessToken; + + const crypto = require('crypto'); + // Create self.addData.message as new item + var uuid = crypto.randomBytes(16).toString('hex'); + var tmpid = crypto.randomBytes(16).toString('hex'); + var itemid = null; + request({ + url: self.config.apiBase + "/" + self.config.apiVersion + "/" + self.config.todoistEndpoint + "/", + method: "POST", + headers: { + "content-type": "application/x-www-form-urlencoded", + "cache-control": "no-cache", + "Authorization": "Bearer " + acessCode + }, + form: { + commands:"[{ \ + \"type\": \"item_add\", \ + \"temp_id\": \"" + tmpid + "\", \ + \"uuid\": \"" + uuid + "\", \ + \"args\": { \ + \"content\": \"" + task + "\", \ + \"project_id\": \"" + proj + "\", \ + \"parent_id\": \"" + parent + "\" \ + }}]" + } + }, + function(error, response, body) { + if (error) { + self.sendSocketNotification("ADDNEWSUBITEM_ERROR", { + error: error + }); + return console.error(" ERROR - MMM-Todoist: " + error); + } + if(self.config.debug){ + console.log(body); + } + if (response.statusCode === 200) { + var taskJson = JSON.parse(body); + itemid = taskJson["temp_id_mapping"][JSON.stringify(tmpid)]; + } + }); + }, + + // TBD + addNewItemToList: function(proj, task, callback = null) { + var self = this; + var acessCode = self.config.accessToken; + + const crypto = require('crypto'); + // Create self.addData.message as new item + var uuid = crypto.randomBytes(16).toString('hex'); + var tmpid = crypto.randomBytes(16).toString('hex'); + var itemid = null; + + var proj_str = ""; + if ((proj != "inbox")) { + proj_str = "\"project_id\": \"" + proj + "\","; + } + + request({ + url: self.config.apiBase + "/" + self.config.apiVersion + "/" + self.config.todoistEndpoint + "/", + method: "POST", + headers: { + "content-type": "application/x-www-form-urlencoded", + "cache-control": "no-cache", + "Authorization": "Bearer " + acessCode + }, + form: { + commands:"[{ \ + \"type\": \"item_add\", \ + \"temp_id\": \"" + tmpid + "\", \ + \"uuid\": \"" + uuid + "\", \ + \"args\": { \ + \"content\": \"" + task + "\"," + proj_str + + "\"due\": {\"string\":\"today\"} \ + }}]" + } + }, + function(error, response, body) { + if (error) { + self.sendSocketNotification("ADDNEWITEM_ERROR", { + error: error + }); + return console.error(" ERROR - MMM-Todoist: " + error); + } + if(self.config.debug){ + console.log(body); + } + if (response.statusCode === 200) { + var taskJson = JSON.parse(body); + itemid = taskJson["temp_id_mapping"][tmpid]; + if (callback) { + callback(self, proj, self.addData.message, itemid); + } + } + }); + }, + + addItemToList: function(self, taskJson) { + if (taskJson == undefined) { + return; + } + if (taskJson.accessToken != self.config.accessToken) { + return; + } + if (taskJson.items == undefined) { + return; + } + + const crypto = require('crypto'); + var reqProj = self.addData.data["id"].split("-")[0]; + var reqTask = self.addData.data["id"].split("-")[1]; + + // If we're making a new item, make it + if (reqTask == "NEW") { + self.addNewItemToList(reqProj, self.addData.message); + } else { // add a sub-item to an item + var tmpid = null; + var itemid = null; + itemid = self.findItem(taskJson, reqProj, reqTask); + + // If parent item not found, add it + if (itemid == null) { + // Create self.addData.data["task"] as new item (get itemid) + self.addNewItemToList(reqProj, reqTask, self.addNewSubItemToList); + } else { + // ADD self.addData.message as sub-item to itemid + self.addNewSubItemToList(self, reqProj, self.addData.message, itemid); + } + } + + self.sendSocketNotification("ADDITEM", itemid); } }); \ No newline at end of file diff --git a/todoist_btns.png b/todoist_btns.png new file mode 100644 index 0000000..21a83ec Binary files /dev/null and b/todoist_btns.png differ