From 1b182463694ab8bb36142b0d905b676ae2267dc7 Mon Sep 17 00:00:00 2001 From: Amos Glenn Date: Fri, 3 Mar 2023 12:56:23 -0500 Subject: [PATCH 1/7] filtering labels by name not id --- MMM-Todoist.js | 1356 ++++++++++++++++++++++++--------------------- package-lock.json | 2 +- 2 files changed, 727 insertions(+), 631 deletions(-) diff --git a/MMM-Todoist.js b/MMM-Todoist.js index e193b75..b1e176d 100644 --- a/MMM-Todoist.js +++ b/MMM-Todoist.js @@ -31,634 +31,730 @@ var UserPresence = true; //true by default, so no impact for user without a PIR sensor Module.register("MMM-Todoist", { - - defaults: { - maximumEntries: 10, - projects: [], - blacklistProjects: false, - labels: [""], - updateInterval: 10 * 60 * 1000, // every 10 minutes, - fade: true, - fadePoint: 0.25, - fadeMinimumOpacity: 0.25, - sortType: "todoist", - - //New config from AgP42 - displayLastUpdate: false, //add or not a line after the tasks with the last server update time - displayLastUpdateFormat: "dd - HH:mm:ss", //format to display the last update. See Moment.js documentation for all display possibilities - maxTitleLength: 25, //10 to 50. Value to cut the line if wrapEvents: true - wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength - displayTasksWithoutDue: true, // Set to false to not print tasks without a due date - displayTasksWithinDays: -1, // If >= 0, do not print tasks with a due date more than this number of days into the future (e.g., 0 prints today and overdue) - // 2019-12-31 by thyed - displaySubtasks: true, // set to false to exclude subtasks - displayAvatar: false, - showProject: true, - // projectColors: ["#95ef63", "#ff8581", "#ffc471", "#f9ec75", "#a8c8e4", "#d2b8a3", "#e2a8e4", "#cccccc", "#fb886e", - // "#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', - 31:'#db4035', - 32:'#ff9933', - 33:'#fad000', - 34:'#afb83b', - 35:'#7ecc49', - 36:'#299438', - 37:'#6accbc', - 38:'#158fad', - 39:'#14aaf5', - 40:'#96c3eb', - 41:'#4073ff', - 42:'#884dff', - 43:'#af38eb', - 44:'#eb96eb', - 45:'#e05194', - 46:'#ff8d85', - 47:'#808080', - 48:'#b8b8b8', - 49:'#ccac93' - }, - - //This has been designed to use the Todoist Sync API. - apiVersion: "v9", - apiBase: "https://todoist.com/API", - todoistEndpoint: "sync", - - todoistResourceType: "[\"items\", \"projects\", \"collaborators\", \"user\", \"labels\"]", - - debug: false - }, - - // Define required scripts. - getStyles: function () { - return ["MMM-Todoist.css"]; - }, - getTranslations: function () { - return { - en: "translations/en.json", - de: "translations/de.json", - nb: "translations/nb.json" - }; - }, - - start: function () { - var self = this; - Log.info("Starting module: " + this.name); - - this.updateIntervalID = 0; // Definition of the IntervalID to be able to stop and start it again - this.ModuleToDoIstHidden = false; // by default it is considered displayed. Note : core function "this.hidden" has strange behaviour, so not used here - - //to display "Loading..." at start-up - this.title = "Loading..."; - this.loaded = false; - - if (this.config.accessToken === "") { - Log.error("MMM-Todoist: AccessToken not set!"); - return; - } - - //Support legacy properties - if (this.config.lists !== undefined) { - if (this.config.lists.length > 0) { - this.config.projects = this.config.lists; - } - } - - // keep track of user's projects list (used to build the "whitelist") - this.userList = typeof this.config.projects !== "undefined" ? - JSON.parse(JSON.stringify(this.config.projects)) : []; - - this.sendSocketNotification("FETCH_TODOIST", this.config); - - //add ID to the setInterval function to be able to stop it later on - this.updateIntervalID = setInterval(function () { - self.sendSocketNotification("FETCH_TODOIST", self.config); - }, this.config.updateInterval); - }, - - suspend: function () { //called by core system when the module is not displayed anymore on the screen - this.ModuleToDoIstHidden = true; - //Log.log("Fct suspend - ModuleHidden = " + ModuleHidden); - this.GestionUpdateIntervalToDoIst(); - }, - - resume: function () { //called by core system when the module is displayed on the screen - this.ModuleToDoIstHidden = false; - //Log.log("Fct resume - ModuleHidden = " + ModuleHidden); - this.GestionUpdateIntervalToDoIst(); - }, - - notificationReceived: function (notification, payload) { - if (notification === "USER_PRESENCE") { // notification sended by module MMM-PIR-Sensor. See its doc - //Log.log("Fct notificationReceived USER_PRESENCE - payload = " + payload); - UserPresence = payload; - this.GestionUpdateIntervalToDoIst(); - } - }, - - GestionUpdateIntervalToDoIst: function () { - if (UserPresence === true && this.ModuleToDoIstHidden === false) { - var self = this; - - // update now - this.sendSocketNotification("FETCH_TODOIST", this.config); - - //if no IntervalID defined, we set one again. This is to avoid several setInterval simultaneously - if (this.updateIntervalID === 0) { - - this.updateIntervalID = setInterval(function () { - self.sendSocketNotification("FETCH_TODOIST", self.config); - }, this.config.updateInterval); - } - - } else { //if (UserPresence = false OR ModuleHidden = true) - Log.log("Personne regarde : on stop l'update " + this.name + " projet : " + this.config.projects); - clearInterval(this.updateIntervalID); // stop the update interval of this module - this.updateIntervalID = 0; //reset the flag to be able to start another one at resume - } - }, - - // Code from MichMich from default module Calendar : to manage task displayed on several lines - /** - * Shortens a string if it's longer than maxLength and add a ellipsis to the end - * - * @param {string} string Text string to shorten - * @param {number} maxLength The max length of the string - * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength - * @returns {string} The shortened string - */ - shorten: function (string, maxLength, wrapEvents) { - if (typeof string !== "string") { - return ""; - } - - if (wrapEvents === true) { - var temp = ""; - var currentLine = ""; - var words = string.split(" "); - - for (var i = 0; i < words.length; i++) { - var word = words[i]; - if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { // max - 1 to account for a space - currentLine += (word + " "); - } else { - if (currentLine.length > 0) { - temp += (currentLine + "
" + word + " "); - } else { - temp += (word + "
"); - } - currentLine = ""; - } - } - - return (temp + currentLine).trim(); - } else { - if (maxLength && typeof maxLength === "number" && string.length > maxLength) { - return string.trim().slice(0, maxLength) + "…"; - } else { - return string.trim(); - } - } - }, - //end modif AgP - - // Override socket notification handler. - // ******** Data sent from the Backend helper. This is the data from the Todoist API ************ - socketNotificationReceived: function (notification, payload) { - if (notification === "TASKS") { - this.filterTodoistData(payload); - - if (this.config.displayLastUpdate) { - this.lastUpdate = Date.now() / 1000; //save the timestamp of the last update to be able to display it - 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 === "FETCH_ERROR") { - Log.error("Todoist Error. Could not fetch todos: " + payload.error); - } - }, - - filterTodoistData: function (tasks) { - var self = this; - var items = []; - var labelIds = []; - - if (tasks == undefined) { - return; - } - if (tasks.accessToken != self.config.accessToken) { - return; - } - if (tasks.items == undefined) { - return; - } - - if (this.config.blacklistProjects) { - // take all projects in payload, and remove the ones specified by user - // i.e., convert user's "whitelist" into a "blacklist" - this.config.projects = []; - tasks.projects.forEach(project => { - if(this.userList.includes(project.id)) { - return; // blacklisted - } - this.config.projects.push(project.id); - }); - if(self.config.debug) { - console.log("MMM-Todoist: original list of projects was blacklisted.\n" + - "Only considering the following projects:"); - console.log(this.config.projects); - } - } - - // Loop through labels fetched from API and find corresponding label IDs for task filtering - // Could be re-used for project names -> project IDs. - if (self.config.labels.length>0 && tasks.labels != undefined) { - for (let apiLabel of tasks.labels) { - for (let configLabelName of self.config.labels) { - if (apiLabel.name == configLabelName) { - labelIds.push(apiLabel.id); - break; - } - } - } - } - - if (self.config.displayTasksWithinDays > -1 || !self.config.displayTasksWithoutDue) { - tasks.items = tasks.items.filter(function (item) { - if (item.due === null) { - return self.config.displayTasksWithoutDue; - } - - var oneDay = 24 * 60 * 60 * 1000; - var dueDateTime = self.parseDueDate(item.due.date); - var dueDate = new Date(dueDateTime.getFullYear(), dueDateTime.getMonth(), dueDateTime.getDate()); - var now = new Date(); - var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - var diffDays = Math.floor((dueDate - today) / (oneDay)); - return diffDays <= self.config.displayTasksWithinDays; - }); - } - - //Filter the Todos by the criteria specified in the Config - tasks.items.forEach(function (item) { - // Ignore sub-tasks - if (item.parent_id!=null && !self.config.displaySubtasks) { return; } - - // Filter using label if a label is configured - if (labelIds.length>0 && item.labels.length > 0) { - // Check all the labels assigned to the task. Add to items if match with configured label - for (let label of item.labels) { - for (let labelNumber of labelIds) { - if (label == labelNumber) { - items.push(item); - return; - } - } - } - } - - // Filter using projets if projects are configured - if (self.config.projects.length>0){ - self.config.projects.forEach(function (project) { - if (item.project_id == project) { - items.push(item); - return; - } - }); - } - }); - - //**** FOR DEBUGGING TO HELP PEOPLE GET THEIR PROJECT IDs */ - if (self.config.debug) { - console.log("%c *** PROJECT -- ID ***", "background: #222; color: #bada55"); - tasks.projects.forEach(project => { - console.log("%c" + project.name + " -- " + project.id, "background: #222; color: #bada55"); - }); - }; - //****** */ - - //Used for ordering by date - items.forEach(function (item) { - if (item.due === null) { - item.due = {}; - item.due["date"] = "2100-12-31"; - item.all_day = true; - } - // Used to sort by date. - item.date = self.parseDueDate(item.due.date); - - // as v8 API does not have 'all_day' field anymore then check due.date for presence of time - // if due.date has a time then set item.all_day to false else all_day is true - if (item.due.date.length > 10) { - item.all_day = false; - } else { - item.all_day = true; - } - }); - - //***** Sorting code if you want to add new methods. */ - switch (self.config.sortType) { - case "todoist": - sorteditems = self.sortByTodoist(items); - break; - case 'priority': - sorteditems = self.sortByPriority(items); - break; - case "dueDateAsc": - sorteditems = self.sortByDueDateAsc(items); - break; - case "dueDateDesc": - sorteditems = self.sortByDueDateDesc(items); - break; - case "dueDateDescPriority": - sorteditems = self.sortByDueDateDescPriority(items); - break; - default: - sorteditems = self.sortByTodoist(items); - break; - } - - //Slice by max Entries - items = items.slice(0, this.config.maximumEntries); - - this.tasks = { - "items": items, - "projects": tasks.projects, - "collaborators": tasks.collaborators - }; - - }, - /* - * The Todoist API returns task due dates as strings in these two formats: YYYY-MM-DD and YYYY-MM-DDThh:mm:ss - * This depends on whether a task only has a due day or a due day and time. You cannot pass this date string into - * "new Date()" - it is inconsistent. In one format, the date string is considered to be in UTC, the other in the - * local timezone. Additionally, if the task's due date has a timezone set, it is given in UTC (zulu format), - * otherwise it is local time. The parseDueDate function keeps Dates consistent by interpreting them all relative - * to the same timezone. - */ - parseDueDate: function (date) { - let [year, month, day, hour = 0, minute = 0, second = 0] = date.split(/\D/).map(Number); - - // If the task's due date has a timezone set (as opposed to the default floating timezone), it's given in UTC time. - if (date[date.length -1] === "Z") { - return new Date(Date.UTC(year, month - 1, day, hour, minute, second)); - } - - return new Date(year, month - 1, day, hour, minute, second); - }, - sortByTodoist: function (itemstoSort) { - itemstoSort.sort(function (a, b) { - // 2019-12-31 bugfix by thyed, property is child_order, not item_order - var itemA = a.child_order, - itemB = b.child_order; - return itemA - itemB; - }); - return itemstoSort; - }, - sortByDueDateAsc: function (itemstoSort) { - itemstoSort.sort(function (a, b) { - return a.date - b.date; - }); - return itemstoSort; - }, - sortByDueDateDesc: function (itemstoSort) { - itemstoSort.sort(function (a, b) { - return b.date - a.date; - }); - return itemstoSort; - }, - sortByPriority: function (itemstoSort) { - itemstoSort.sort(function (a, b) { - return b.priority - a.priority; - }); - return itemstoSort; - }, - sortByDueDateDescPriority: function (itemstoSort) { - itemstoSort.sort(function (a, b) { - if (a.date > b.date) return 1; - if (a.date < b.date) return -1; - - if (a.priority < b.priority) return 1; - if (a.priority > b.priority) return -1; - }); - return itemstoSort; - }, - createCell: function(className, innerHTML) { - var cell = document.createElement("div"); - cell.className = "divTableCell " + className; - cell.innerHTML = innerHTML; - return cell; - }, - addPriorityIndicatorCell: function(item) { - var className = "priority "; - switch (item.priority) { - case 4: - className += "priority1"; - break; - case 3: - className += "priority2"; - break; - case 2: - className += "priority3"; - break; - default: - className = ""; - break; - } - return this.createCell(className, " ");; - }, - addColumnSpacerCell: function() { - return this.createCell("spacerCell", " "); - }, - addTodoTextCell: function(item) { - var temp = document.createElement('div'); - temp.innerHTML = item.contentHtml; - - var para = temp.getElementsByTagName('p'); - - return this.createCell("title bright alignLeft", - this.shorten(para[0].innerHTML, this.config.maxTitleLength, this.config.wrapEvents)); - - // return this.createCell("title bright alignLeft", item.content); - }, - addDueDateCell: function(item) { - var className = "bright align-right dueDate "; - var innerHTML = ""; - - var oneDay = 24 * 60 * 60 * 1000; - var dueDateTime = this.parseDueDate(item.due.date); - var dueDate = new Date(dueDateTime.getFullYear(), dueDateTime.getMonth(), dueDateTime.getDate()); - var now = new Date(); - var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - var diffDays = Math.floor((dueDate - today) / (oneDay)); - var diffMonths = (dueDate.getFullYear() * 12 + dueDate.getMonth()) - (now.getFullYear() * 12 + now.getMonth()); - - if (diffDays < -1) { - innerHTML = dueDate.toLocaleDateString(config.language, { - "month": "short" - }) + " " + dueDate.getDate(); - className += "xsmall overdue"; - } else if (diffDays === -1) { - innerHTML = this.translate("YESTERDAY"); - className += "xsmall overdue"; - } else if (diffDays === 0) { - innerHTML = this.translate("TODAY"); - if (item.all_day || dueDateTime >= now) { - className += "today"; - } else { - className += "overdue"; - } - } else if (diffDays === 1) { - innerHTML = this.translate("TOMORROW"); - className += "xsmall tomorrow"; - } else if (diffDays < 7) { - innerHTML = dueDate.toLocaleDateString(config.language, { - "weekday": "short" - }); - className += "xsmall"; - } else if (diffMonths < 7 || dueDate.getFullYear() == now.getFullYear()) { - innerHTML = dueDate.toLocaleDateString(config.language, { - "month": "short" - }) + " " + dueDate.getDate(); - className += "xsmall"; - } else if (item.due.date === "2100-12-31") { - innerHTML = ""; - className += "xsmall"; - } else { - innerHTML = dueDate.toLocaleDateString(config.language, { - "month": "short" - }) + " " + dueDate.getDate() + " " + dueDate.getFullYear(); - className += "xsmall"; - } - - if (innerHTML !== "" && !item.all_day) { - function formatTime(d) { - function z(n) { - return (n < 10 ? "0" : "") + n; - } - var h = d.getHours(); - var m = z(d.getMinutes()); - if (config.timeFormat == 12) { - return " " + (h % 12 || 12) + ":" + m + (h < 12 ? " AM" : " PM"); - } else { - return " " + h + ":" + m; - } - } - innerHTML += formatTime(dueDateTime); - } - return this.createCell(className, innerHTML); - }, - addProjectCell: function(item) { - var project = this.tasks.projects.find(p => p.id === item.project_id); - var projectcolor = this.config.projectColors[project.color]; - var innerHTML = "" + project.name; - return this.createCell("xsmall", innerHTML); - }, - addAssigneeAvatorCell: function(item, collaboratorsMap) { - var avatarImg = document.createElement("img"); - avatarImg.className = "todoAvatarImg"; - - var colIndex = collaboratorsMap.get(item.responsible_uid); - if (typeof colIndex !== "undefined" && this.tasks.collaborators[colIndex].image_id!=null) { - avatarImg.src = "https://dcff1xvirvpfp.cloudfront.net/" + this.tasks.collaborators[colIndex].image_id + "_big.jpg"; - } else { avatarImg.src = "/modules/MMM-Todoist/1x1px.png"; } - - var cell = this.createCell("", ""); - cell.appendChild(avatarImg); - - 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; - } - - - //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(); - - for (var value=0; value < this.tasks.collaborators.length; value++) { - collaboratorsMap.set(this.tasks.collaborators[value].id, value); - } - - //Iterate through Todos - this.tasks.items.forEach(item => { - var divRow = document.createElement("div"); - //Add the Row - divRow.className = "divTableRow"; - - - //Columns - divRow.appendChild(this.addPriorityIndicatorCell(item)); - divRow.appendChild(this.addColumnSpacerCell()); - divRow.appendChild(this.addTodoTextCell(item)); - divRow.appendChild(this.addDueDateCell(item)); - if (this.config.showProject) { - divRow.appendChild(this.addColumnSpacerCell()); - divRow.appendChild(this.addProjectCell(item)); - } - if (this.config.displayAvatar) { - divRow.appendChild(this.addAssigneeAvatorCell(item, collaboratorsMap)); - } - - divBody.appendChild(divRow); - }); - - divTable.appendChild(divBody); - wrapper.appendChild(divTable); - - // 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))); - - // display the update time at the end, if defined so by the user config - if (this.config.displayLastUpdate) { - var updateinfo = document.createElement("div"); - updateinfo.className = "xsmall light align-left"; - updateinfo.innerHTML = "Update : " + moment.unix(this.lastUpdate).format(this.config.displayLastUpdateFormat); - wrapper.appendChild(updateinfo); - } - - //**** FOR DEBUGGING TO HELP PEOPLE GET THEIR PROJECT IDs - (People who can't see console) */ - if (this.config.debug) { - var projectsids = document.createElement("div"); - projectsids.className = "xsmall light align-left"; - projectsids.innerHTML = "*** PROJECT -- ID ***
"; - this.tasks.projects.forEach(project => { - projectsids.innerHTML += "" + project.name + " -- " + project.id + "
"; - }); - wrapper.appendChild(projectsids); - }; - //****** */ - - return wrapper; - } - + defaults: { + maximumEntries: 10, + projects: [], + blacklistProjects: false, + labels: [""], + updateInterval: 10 * 60 * 1000, // every 10 minutes, + fade: true, + fadePoint: 0.25, + fadeMinimumOpacity: 0.25, + sortType: "todoist", + + //New config from AgP42 + displayLastUpdate: false, //add or not a line after the tasks with the last server update time + displayLastUpdateFormat: "dd - HH:mm:ss", //format to display the last update. See Moment.js documentation for all display possibilities + maxTitleLength: 25, //10 to 50. Value to cut the line if wrapEvents: true + wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength + displayTasksWithoutDue: true, // Set to false to not print tasks without a due date + displayTasksWithinDays: -1, // If >= 0, do not print tasks with a due date more than this number of days into the future (e.g., 0 prints today and overdue) + // 2019-12-31 by thyed + displaySubtasks: true, // set to false to exclude subtasks + displayAvatar: false, + showProject: true, + // projectColors: ["#95ef63", "#ff8581", "#ffc471", "#f9ec75", "#a8c8e4", "#d2b8a3", "#e2a8e4", "#cccccc", "#fb886e", + // "#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", + 31: "#db4035", + 32: "#ff9933", + 33: "#fad000", + 34: "#afb83b", + 35: "#7ecc49", + 36: "#299438", + 37: "#6accbc", + 38: "#158fad", + 39: "#14aaf5", + 40: "#96c3eb", + 41: "#4073ff", + 42: "#884dff", + 43: "#af38eb", + 44: "#eb96eb", + 45: "#e05194", + 46: "#ff8d85", + 47: "#808080", + 48: "#b8b8b8", + 49: "#ccac93" + }, + + //This has been designed to use the Todoist Sync API. + apiVersion: "v9", + apiBase: "https://todoist.com/API", + todoistEndpoint: "sync", + + todoistResourceType: + '["items", "projects", "collaborators", "user", "labels"]', + + debug: false + }, + + // Define required scripts. + getStyles: function () { + return ["MMM-Todoist.css"]; + }, + getTranslations: function () { + return { + en: "translations/en.json", + de: "translations/de.json", + nb: "translations/nb.json" + }; + }, + + start: function () { + var self = this; + Log.info("Starting module: " + this.name); + + this.updateIntervalID = 0; // Definition of the IntervalID to be able to stop and start it again + this.ModuleToDoIstHidden = false; // by default it is considered displayed. Note : core function "this.hidden" has strange behaviour, so not used here + + //to display "Loading..." at start-up + this.title = "Loading..."; + this.loaded = false; + + if (this.config.accessToken === "") { + Log.error("MMM-Todoist: AccessToken not set!"); + return; + } + + //Support legacy properties + if (this.config.lists !== undefined) { + if (this.config.lists.length > 0) { + this.config.projects = this.config.lists; + } + } + + // keep track of user's projects list (used to build the "whitelist") + this.userList = + typeof this.config.projects !== "undefined" + ? JSON.parse(JSON.stringify(this.config.projects)) + : []; + + this.sendSocketNotification("FETCH_TODOIST", this.config); + + //add ID to the setInterval function to be able to stop it later on + this.updateIntervalID = setInterval(function () { + self.sendSocketNotification("FETCH_TODOIST", self.config); + }, this.config.updateInterval); + }, + + suspend: function () { + //called by core system when the module is not displayed anymore on the screen + this.ModuleToDoIstHidden = true; + //Log.log("Fct suspend - ModuleHidden = " + ModuleHidden); + this.GestionUpdateIntervalToDoIst(); + }, + + resume: function () { + //called by core system when the module is displayed on the screen + this.ModuleToDoIstHidden = false; + //Log.log("Fct resume - ModuleHidden = " + ModuleHidden); + this.GestionUpdateIntervalToDoIst(); + }, + + notificationReceived: function (notification, payload) { + if (notification === "USER_PRESENCE") { + // notification sended by module MMM-PIR-Sensor. See its doc + //Log.log("Fct notificationReceived USER_PRESENCE - payload = " + payload); + UserPresence = payload; + this.GestionUpdateIntervalToDoIst(); + } + }, + + GestionUpdateIntervalToDoIst: function () { + if (UserPresence === true && this.ModuleToDoIstHidden === false) { + var self = this; + + // update now + this.sendSocketNotification("FETCH_TODOIST", this.config); + + //if no IntervalID defined, we set one again. This is to avoid several setInterval simultaneously + if (this.updateIntervalID === 0) { + this.updateIntervalID = setInterval(function () { + self.sendSocketNotification("FETCH_TODOIST", self.config); + }, this.config.updateInterval); + } + } else { + //if (UserPresence = false OR ModuleHidden = true) + Log.log( + "Personne regarde : on stop l'update " + + this.name + + " projet : " + + this.config.projects + ); + clearInterval(this.updateIntervalID); // stop the update interval of this module + this.updateIntervalID = 0; //reset the flag to be able to start another one at resume + } + }, + + // Code from MichMich from default module Calendar : to manage task displayed on several lines + /** + * Shortens a string if it's longer than maxLength and add a ellipsis to the end + * + * @param {string} string Text string to shorten + * @param {number} maxLength The max length of the string + * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength + * @returns {string} The shortened string + */ + shorten: function (string, maxLength, wrapEvents) { + if (typeof string !== "string") { + return ""; + } + + if (wrapEvents === true) { + var temp = ""; + var currentLine = ""; + var words = string.split(" "); + + for (var i = 0; i < words.length; i++) { + var word = words[i]; + if ( + currentLine.length + word.length < + (typeof maxLength === "number" ? maxLength : 25) - 1 + ) { + // max - 1 to account for a space + currentLine += word + " "; + } else { + if (currentLine.length > 0) { + temp += currentLine + "
" + word + " "; + } else { + temp += word + "
"; + } + currentLine = ""; + } + } + + return (temp + currentLine).trim(); + } else { + if ( + maxLength && + typeof maxLength === "number" && + string.length > maxLength + ) { + return string.trim().slice(0, maxLength) + "…"; + } else { + return string.trim(); + } + } + }, + //end modif AgP + + // Override socket notification handler. + // ******** Data sent from the Backend helper. This is the data from the Todoist API ************ + socketNotificationReceived: function (notification, payload) { + if (notification === "TASKS") { + this.filterTodoistData(payload); + + if (this.config.displayLastUpdate) { + this.lastUpdate = Date.now() / 1000; //save the timestamp of the last update to be able to display it + 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 === "FETCH_ERROR") { + Log.error("Todoist Error. Could not fetch todos: " + payload.error); + } + }, + + filterTodoistData: function (tasks) { + var self = this; + var items = []; + var labelIds = []; + + if (tasks == undefined) { + return; + } + if (tasks.accessToken != self.config.accessToken) { + return; + } + if (tasks.items == undefined) { + return; + } + + if (this.config.blacklistProjects) { + // take all projects in payload, and remove the ones specified by user + // i.e., convert user's "whitelist" into a "blacklist" + this.config.projects = []; + tasks.projects.forEach((project) => { + if (this.userList.includes(project.id)) { + return; // blacklisted + } + this.config.projects.push(project.id); + }); + if (self.config.debug) { + console.log( + "MMM-Todoist: original list of projects was blacklisted.\n" + + "Only considering the following projects:" + ); + console.log(this.config.projects); + } + } + + // Loop through labels fetched from API and find corresponding label IDs for task filtering + // Could be re-used for project names -> project IDs. + /* if (self.config.labels.length > 0 && tasks.labels != undefined) { + for (let apiLabel of tasks.labels) { + for (let configLabelName of self.config.labels) { + if (apiLabel.name == configLabelName) { + labelIds.push(apiLabel.id); + break; + } + } + } + } */ + + if ( + self.config.displayTasksWithinDays > -1 || + !self.config.displayTasksWithoutDue + ) { + tasks.items = tasks.items.filter(function (item) { + if (item.due === null) { + return self.config.displayTasksWithoutDue; + } + + var oneDay = 24 * 60 * 60 * 1000; + var dueDateTime = self.parseDueDate(item.due.date); + var dueDate = new Date( + dueDateTime.getFullYear(), + dueDateTime.getMonth(), + dueDateTime.getDate() + ); + var now = new Date(); + var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + var diffDays = Math.floor((dueDate - today) / oneDay); + return diffDays <= self.config.displayTasksWithinDays; + }); + } + + //Filter the Todos by the criteria specified in the Config + tasks.items.forEach(function (item) { + // Ignore sub-tasks + if (item.parent_id != null && !self.config.displaySubtasks) { + return; + } + + // Filter using label if a label is configured + if (self.config.labels.length > 0 && item.labels.length > 0) { + // Check all the labels assigned to the task. Add to items if match with configured label + for (let label of item.labels) { + for (let labelName of self.config.labels) { + if (label == labelName) { + items.push(item); + return; + } + } + } + } + + // Filter using projets if projects are configured + if (self.config.projects.length > 0) { + self.config.projects.forEach(function (project) { + if (item.project_id == project) { + items.push(item); + return; + } + }); + } + }); + + //**** FOR DEBUGGING TO HELP PEOPLE GET THEIR PROJECT IDs */ + if (self.config.debug) { + console.log( + "%c *** PROJECT -- ID ***", + "background: #222; color: #bada55" + ); + tasks.projects.forEach((project) => { + console.log( + "%c" + project.name + " -- " + project.id, + "background: #222; color: #bada55" + ); + }); + } + //****** */ + + //Used for ordering by date + items.forEach(function (item) { + if (item.due === null) { + item.due = {}; + item.due["date"] = "2100-12-31"; + item.all_day = true; + } + // Used to sort by date. + item.date = self.parseDueDate(item.due.date); + + // as v8 API does not have 'all_day' field anymore then check due.date for presence of time + // if due.date has a time then set item.all_day to false else all_day is true + if (item.due.date.length > 10) { + item.all_day = false; + } else { + item.all_day = true; + } + }); + + //***** Sorting code if you want to add new methods. */ + switch (self.config.sortType) { + case "todoist": + sorteditems = self.sortByTodoist(items); + break; + case "priority": + sorteditems = self.sortByPriority(items); + break; + case "dueDateAsc": + sorteditems = self.sortByDueDateAsc(items); + break; + case "dueDateDesc": + sorteditems = self.sortByDueDateDesc(items); + break; + case "dueDateDescPriority": + sorteditems = self.sortByDueDateDescPriority(items); + break; + default: + sorteditems = self.sortByTodoist(items); + break; + } + + //Slice by max Entries + items = items.slice(0, this.config.maximumEntries); + + this.tasks = { + items: items, + projects: tasks.projects, + collaborators: tasks.collaborators + }; + }, + /* + * The Todoist API returns task due dates as strings in these two formats: YYYY-MM-DD and YYYY-MM-DDThh:mm:ss + * This depends on whether a task only has a due day or a due day and time. You cannot pass this date string into + * "new Date()" - it is inconsistent. In one format, the date string is considered to be in UTC, the other in the + * local timezone. Additionally, if the task's due date has a timezone set, it is given in UTC (zulu format), + * otherwise it is local time. The parseDueDate function keeps Dates consistent by interpreting them all relative + * to the same timezone. + */ + parseDueDate: function (date) { + let [year, month, day, hour = 0, minute = 0, second = 0] = date + .split(/\D/) + .map(Number); + + // If the task's due date has a timezone set (as opposed to the default floating timezone), it's given in UTC time. + if (date[date.length - 1] === "Z") { + return new Date(Date.UTC(year, month - 1, day, hour, minute, second)); + } + + return new Date(year, month - 1, day, hour, minute, second); + }, + sortByTodoist: function (itemstoSort) { + itemstoSort.sort(function (a, b) { + // 2019-12-31 bugfix by thyed, property is child_order, not item_order + var itemA = a.child_order, + itemB = b.child_order; + return itemA - itemB; + }); + return itemstoSort; + }, + sortByDueDateAsc: function (itemstoSort) { + itemstoSort.sort(function (a, b) { + return a.date - b.date; + }); + return itemstoSort; + }, + sortByDueDateDesc: function (itemstoSort) { + itemstoSort.sort(function (a, b) { + return b.date - a.date; + }); + return itemstoSort; + }, + sortByPriority: function (itemstoSort) { + itemstoSort.sort(function (a, b) { + return b.priority - a.priority; + }); + return itemstoSort; + }, + sortByDueDateDescPriority: function (itemstoSort) { + itemstoSort.sort(function (a, b) { + if (a.date > b.date) return 1; + if (a.date < b.date) return -1; + + if (a.priority < b.priority) return 1; + if (a.priority > b.priority) return -1; + }); + return itemstoSort; + }, + createCell: function (className, innerHTML) { + var cell = document.createElement("div"); + cell.className = "divTableCell " + className; + cell.innerHTML = innerHTML; + return cell; + }, + addPriorityIndicatorCell: function (item) { + var className = "priority "; + switch (item.priority) { + case 4: + className += "priority1"; + break; + case 3: + className += "priority2"; + break; + case 2: + className += "priority3"; + break; + default: + className = ""; + break; + } + return this.createCell(className, " "); + }, + addColumnSpacerCell: function () { + return this.createCell("spacerCell", " "); + }, + addTodoTextCell: function (item) { + var temp = document.createElement("div"); + temp.innerHTML = item.contentHtml; + + var para = temp.getElementsByTagName("p"); + + return this.createCell( + "title bright alignLeft", + this.shorten( + para[0].innerHTML, + this.config.maxTitleLength, + this.config.wrapEvents + ) + ); + + // return this.createCell("title bright alignLeft", item.content); + }, + addDueDateCell: function (item) { + var className = "bright align-right dueDate "; + var innerHTML = ""; + + var oneDay = 24 * 60 * 60 * 1000; + var dueDateTime = this.parseDueDate(item.due.date); + var dueDate = new Date( + dueDateTime.getFullYear(), + dueDateTime.getMonth(), + dueDateTime.getDate() + ); + var now = new Date(); + var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + var diffDays = Math.floor((dueDate - today) / oneDay); + var diffMonths = + dueDate.getFullYear() * 12 + + dueDate.getMonth() - + (now.getFullYear() * 12 + now.getMonth()); + + if (diffDays < -1) { + innerHTML = + dueDate.toLocaleDateString(config.language, { + month: "short" + }) + + " " + + dueDate.getDate(); + className += "xsmall overdue"; + } else if (diffDays === -1) { + innerHTML = this.translate("YESTERDAY"); + className += "xsmall overdue"; + } else if (diffDays === 0) { + innerHTML = this.translate("TODAY"); + if (item.all_day || dueDateTime >= now) { + className += "today"; + } else { + className += "overdue"; + } + } else if (diffDays === 1) { + innerHTML = this.translate("TOMORROW"); + className += "xsmall tomorrow"; + } else if (diffDays < 7) { + innerHTML = dueDate.toLocaleDateString(config.language, { + weekday: "short" + }); + className += "xsmall"; + } else if (diffMonths < 7 || dueDate.getFullYear() == now.getFullYear()) { + innerHTML = + dueDate.toLocaleDateString(config.language, { + month: "short" + }) + + " " + + dueDate.getDate(); + className += "xsmall"; + } else if (item.due.date === "2100-12-31") { + innerHTML = ""; + className += "xsmall"; + } else { + innerHTML = + dueDate.toLocaleDateString(config.language, { + month: "short" + }) + + " " + + dueDate.getDate() + + " " + + dueDate.getFullYear(); + className += "xsmall"; + } + + if (innerHTML !== "" && !item.all_day) { + function formatTime(d) { + function z(n) { + return (n < 10 ? "0" : "") + n; + } + var h = d.getHours(); + var m = z(d.getMinutes()); + if (config.timeFormat == 12) { + return " " + (h % 12 || 12) + ":" + m + (h < 12 ? " AM" : " PM"); + } else { + return " " + h + ":" + m; + } + } + innerHTML += formatTime(dueDateTime); + } + return this.createCell(className, innerHTML); + }, + addProjectCell: function (item) { + var project = this.tasks.projects.find((p) => p.id === item.project_id); + var projectcolor = this.config.projectColors[project.color]; + var innerHTML = + "" + + project.name; + return this.createCell("xsmall", innerHTML); + }, + addAssigneeAvatorCell: function (item, collaboratorsMap) { + var avatarImg = document.createElement("img"); + avatarImg.className = "todoAvatarImg"; + + var colIndex = collaboratorsMap.get(item.responsible_uid); + if ( + typeof colIndex !== "undefined" && + this.tasks.collaborators[colIndex].image_id != null + ) { + avatarImg.src = + "https://dcff1xvirvpfp.cloudfront.net/" + + this.tasks.collaborators[colIndex].image_id + + "_big.jpg"; + } else { + avatarImg.src = "/modules/MMM-Todoist/1x1px.png"; + } + + var cell = this.createCell("", ""); + cell.appendChild(avatarImg); + + 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; + } + + //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(); + + for (var value = 0; value < this.tasks.collaborators.length; value++) { + collaboratorsMap.set(this.tasks.collaborators[value].id, value); + } + + //Iterate through Todos + this.tasks.items.forEach((item) => { + var divRow = document.createElement("div"); + //Add the Row + divRow.className = "divTableRow"; + + //Columns + divRow.appendChild(this.addPriorityIndicatorCell(item)); + divRow.appendChild(this.addColumnSpacerCell()); + divRow.appendChild(this.addTodoTextCell(item)); + divRow.appendChild(this.addDueDateCell(item)); + if (this.config.showProject) { + divRow.appendChild(this.addColumnSpacerCell()); + divRow.appendChild(this.addProjectCell(item)); + } + if (this.config.displayAvatar) { + divRow.appendChild(this.addAssigneeAvatorCell(item, collaboratorsMap)); + } + + divBody.appendChild(divRow); + }); + + divTable.appendChild(divBody); + wrapper.appendChild(divTable); + + // 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 + ) + )) + ); + + // display the update time at the end, if defined so by the user config + if (this.config.displayLastUpdate) { + var updateinfo = document.createElement("div"); + updateinfo.className = "xsmall light align-left"; + updateinfo.innerHTML = + "Update : " + + moment + .unix(this.lastUpdate) + .format(this.config.displayLastUpdateFormat); + wrapper.appendChild(updateinfo); + } + + //**** FOR DEBUGGING TO HELP PEOPLE GET THEIR PROJECT IDs - (People who can't see console) */ + if (this.config.debug) { + var projectsids = document.createElement("div"); + projectsids.className = "xsmall light align-left"; + projectsids.innerHTML = "*** PROJECT -- ID ***
"; + this.tasks.projects.forEach((project) => { + projectsids.innerHTML += + "" + project.name + " -- " + project.id + "
"; + }); + wrapper.appendChild(projectsids); + } + //****** */ + + return wrapper; + } }); diff --git a/package-lock.json b/package-lock.json index 004d6e1..001ff72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "audit": "^0.0.6", - "request": "*", + "request": "latest", "showdown": "^2.0.1" } }, From fb56e74ecdce772180bdffe05a366cdbfe8fa380 Mon Sep 17 00:00:00 2001 From: Amos Glenn Date: Tue, 7 Mar 2023 10:16:45 -0500 Subject: [PATCH 2/7] initial template --- MMM-Todoist.js | 28 +++++++++++++++++++++++++--- baseTemplate.njk | 23 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 baseTemplate.njk diff --git a/MMM-Todoist.js b/MMM-Todoist.js index b1e176d..3f28b69 100644 --- a/MMM-Todoist.js +++ b/MMM-Todoist.js @@ -88,9 +88,23 @@ Module.register("MMM-Todoist", { todoistEndpoint: "sync", todoistResourceType: - '["items", "projects", "collaborators", "user", "labels"]', - - debug: false + '["items", "projects", "collaborators", "user", "labels"]', //TODO add "filters", "reminders", "sections", + + debug: false, + + //Options to display for each task + displayOrder: [ + "content", + "due", + "countdown", + "priority", + "labels", + "assignee", + "avatar", + "checked" + ], + displayPriorityAsIcon: true, + displayDueAsCountdown: false }, // Define required scripts. @@ -428,6 +442,14 @@ Module.register("MMM-Todoist", { //Slice by max Entries items = items.slice(0, this.config.maximumEntries); + //If displayAsCountdown is true, convert all dates to days until due + //TODO: write countdown converter + + //TODO: write converter from responsible_uid to collaborator name + + //if displayPriorityAsIcon is true, convert priority number to icon + //TODO: write priority icon converter + this.tasks = { items: items, projects: tasks.projects, diff --git a/baseTemplate.njk b/baseTemplate.njk new file mode 100644 index 0000000..8e97467 --- /dev/null +++ b/baseTemplate.njk @@ -0,0 +1,23 @@ +{# displayOrder: [ + "content", + "due", + "priority", + "labels", + "assignee", + "checked" + ], + displayPriorityAsIcon: true, + displayDueAsCountdown: false +#} +{% if loaded %} +
+
    + {% for item in self.tasks.items %} + {% for col in self.config.displayOrder %} +
  • {{item[col]}}
  • + {% endfor %} + {% endfor %} +
+
+ + From 0ad8be970184ddba8d02e309a16639d066bef114 Mon Sep 17 00:00:00 2001 From: Amos Glenn Date: Fri, 10 Mar 2023 08:54:08 -0500 Subject: [PATCH 3/7] starting templating efforts --- MMM-Todoist.js | 31 ++++++++++++++++++++++--------- baseTemplate.njk | 4 +++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/MMM-Todoist.js b/MMM-Todoist.js index 3f28b69..42bf5b5 100644 --- a/MMM-Todoist.js +++ b/MMM-Todoist.js @@ -108,6 +108,14 @@ Module.register("MMM-Todoist", { }, // Define required scripts. + getTemplate: function () { + return "baseTemplate.njk"; + }, + getTemplateData: function () { + Log.info("SENDING TEMPLATE DATA"); + return this.config; + }, + getStyles: function () { return ["MMM-Todoist.css"]; }, @@ -148,6 +156,7 @@ Module.register("MMM-Todoist", { ? JSON.parse(JSON.stringify(this.config.projects)) : []; + Log.info("FETCHING TODOIST DATA"); this.sendSocketNotification("FETCH_TODOIST", this.config); //add ID to the setInterval function to be able to stop it later on @@ -261,7 +270,8 @@ Module.register("MMM-Todoist", { // ******** Data sent from the Backend helper. This is the data from the Todoist API ************ socketNotificationReceived: function (notification, payload) { if (notification === "TASKS") { - this.filterTodoistData(payload); + Log.info("FILTERING PAYLOAD"); + this.config.tasks = this.filterTodoistData(payload); if (this.config.displayLastUpdate) { this.lastUpdate = Date.now() / 1000; //save the timestamp of the last update to be able to display it @@ -276,6 +286,7 @@ Module.register("MMM-Todoist", { } this.loaded = true; + Log.info("UPDATING DOM"); this.updateDom(1000); } else if (notification === "FETCH_ERROR") { Log.error("Todoist Error. Could not fetch todos: " + payload.error); @@ -287,6 +298,8 @@ Module.register("MMM-Todoist", { var items = []; var labelIds = []; + Log.info("DOING FILTERING NOW"); + if (tasks == undefined) { return; } @@ -383,7 +396,7 @@ Module.register("MMM-Todoist", { } }); - //**** FOR DEBUGGING TO HELP PEOPLE GET THEIR PROJECT IDs */ + // FOR DEBUGGING TO HELP PEOPLE GET THEIR PROJECT IDs // if (self.config.debug) { console.log( "%c *** PROJECT -- ID ***", @@ -396,7 +409,6 @@ Module.register("MMM-Todoist", { ); }); } - //****** */ //Used for ordering by date items.forEach(function (item) { @@ -417,7 +429,7 @@ Module.register("MMM-Todoist", { } }); - //***** Sorting code if you want to add new methods. */ + // Sorting code if you want to add new methods. // switch (self.config.sortType) { case "todoist": sorteditems = self.sortByTodoist(items); @@ -455,6 +467,8 @@ Module.register("MMM-Todoist", { projects: tasks.projects, collaborators: tasks.collaborators }; + Log.info("FILTERING COMPLETE--UPDATING CONFIG"); + return this.tasks; }, /* * The Todoist API returns task due dates as strings in these two formats: YYYY-MM-DD and YYYY-MM-DDThh:mm:ss @@ -675,8 +689,8 @@ Module.register("MMM-Todoist", { cell.appendChild(avatarImg); return cell; - }, - getDom: function () { + } + /* getDom: function () { if (this.config.hideWhenEmpty && this.tasks.items.length === 0) { return null; } @@ -764,7 +778,7 @@ Module.register("MMM-Todoist", { wrapper.appendChild(updateinfo); } - //**** FOR DEBUGGING TO HELP PEOPLE GET THEIR PROJECT IDs - (People who can't see console) */ + // FOR DEBUGGING TO HELP PEOPLE GET THEIR PROJECT IDs - (People who can't see console) // if (this.config.debug) { var projectsids = document.createElement("div"); projectsids.className = "xsmall light align-left"; @@ -775,8 +789,7 @@ Module.register("MMM-Todoist", { }); wrapper.appendChild(projectsids); } - //****** */ return wrapper; - } + } */ }); diff --git a/baseTemplate.njk b/baseTemplate.njk index 8e97467..f58f295 100644 --- a/baseTemplate.njk +++ b/baseTemplate.njk @@ -9,8 +9,9 @@ displayPriorityAsIcon: true, displayDueAsCountdown: false #} -{% if loaded %} +
+ This template is displaying this text.
    {% for item in self.tasks.items %} {% for col in self.config.displayOrder %} @@ -21,3 +22,4 @@
+ From 50e8c8fb8d335647e38f36cbcdc129389b8d0cd8 Mon Sep 17 00:00:00 2001 From: Amos Glenn Date: Mon, 20 Mar 2023 16:42:29 -0400 Subject: [PATCH 4/7] completed basic templating with data --- MMM-Todoist.css | 107 +++++++++++++++++++++++++++-------------------- MMM-Todoist.js | 67 +++++++++++++++++++++++------ baseTemplate.njk | 33 +++++++++++---- 3 files changed, 143 insertions(+), 64 deletions(-) diff --git a/MMM-Todoist.css b/MMM-Todoist.css index 313155e..7fd2158 100644 --- a/MMM-Todoist.css +++ b/MMM-Todoist.css @@ -1,96 +1,113 @@ .priority { - width: 3px; - border-right: 3px; + width: 3px; + border-right: 3px; } .priority1 { - background-color: #D1453B; + background-color: #d1453b; } .priority2 { - background-color: #EB8909; + background-color: #eb8909; } .priority3 { - background-color: #246FE0; + background-color: #246fe0; } .alignLeft { - text-align: left; + text-align: left; } .spacerCell { - width: 3px; + width: 3px; } .overdue { - text-decoration: underline #ac0000; + text-decoration: underline #ac0000; } .today { - text-decoration: underline #03a05c; + text-decoration: underline #03a05c; } .tomorrow { - text-decoration: underline #166cec; + text-decoration: underline #166cec; } .projectcolor { - padding: 0 !important; - margin-right: 4px !important; - display: inline-block; - width: 12px; - height: 12px; - border-radius: 12px; - vertical-align: top; - margin-top: 3px; + padding: 0 !important; + margin-right: 4px !important; + display: inline-block; + width: 12px; + height: 12px; + border-radius: 12px; + vertical-align: top; + margin-top: 3px; } - /* .dueDate { min-width: 100px; } */ .todoAvatarImg { - border-radius: 50%; - /* filter: grayscale(100%); */ - height: 27px; - display: block; - margin-left: 10px; - margin-right: auto; + border-radius: 50%; + /* filter: grayscale(100%); */ + height: 27px; + display: block; + margin-left: 10px; + margin-right: auto; } -.divTable{ - display: table; - width: 100%; +.divTable { + display: table; + width: 100%; } .divTableRow { - display: table-row; + display: table-row; } .divTableHeading { - background-color: #EEE; - display: table-header-group; + background-color: #eee; + display: table-header-group; } -.divTableCell, .divTableHead { - /* border: 1px solid #999999; */ - display: table-cell; - /* padding: 3px 10px; */ - vertical-align: middle; +.divTableCell, +.divTableHead { + /* border: 1px solid #999999; */ + display: table-cell; + /* padding: 3px 10px; */ + vertical-align: middle; } .divTableHeading { - background-color: #EEE; - display: table-header-group; - font-weight: bold; + background-color: #eee; + display: table-header-group; + font-weight: bold; } .divTableFoot { - background-color: #EEE; - display: table-footer-group; - font-weight: bold; + background-color: #eee; + display: table-footer-group; + font-weight: bold; } .divTableBody { - display: table-row-group; + display: table-row-group; } .todoTextCell { - width: 400px; - overflow: hidden; + width: 400px; + overflow: hidden; +} +.tasks-wrapper { + width: 100%; + border-collapse: collapse; + text-align: left; +} +.tasks-wrapper th, +.tasks-wrapper td { + padding: 20px; +} +.tasks-wrapper tbody tr { + border-bottom: 2px solid #dddddd; +} +.task-label-bubble { + background-color: #333333; + border-radius: 15px; + padding: 2px 6px 2px 6px; } diff --git a/MMM-Todoist.js b/MMM-Todoist.js index 42bf5b5..479a20c 100644 --- a/MMM-Todoist.js +++ b/MMM-Todoist.js @@ -95,7 +95,7 @@ Module.register("MMM-Todoist", { //Options to display for each task displayOrder: [ "content", - "due", + "duedate", "countdown", "priority", "labels", @@ -104,7 +104,8 @@ Module.register("MMM-Todoist", { "checked" ], displayPriorityAsIcon: true, - displayDueAsCountdown: false + displayDueAsCountdown: false, + tasks: false }, // Define required scripts. @@ -113,6 +114,7 @@ Module.register("MMM-Todoist", { }, getTemplateData: function () { Log.info("SENDING TEMPLATE DATA"); + Log.info(this.config); return this.config; }, @@ -410,11 +412,12 @@ Module.register("MMM-Todoist", { }); } - //Used for ordering by date + //convert item information to display formats items.forEach(function (item) { + //Used for ordering by date if (item.due === null) { item.due = {}; - item.due["date"] = "2100-12-31"; + item.due["date"] = "2100-12-31"; //FAKE DUE DATE WHEN NONE SUPPLIED item.all_day = true; } // Used to sort by date. @@ -427,6 +430,54 @@ Module.register("MMM-Todoist", { } else { item.all_day = true; } + + //TEMP converting due date object to string + //TODO: add config option for date string to show + if (item.due["date"] == "2100-12-31") { + //check for fake due date + item.duedate = "---"; + } else { + item.duedate = moment(self.parseDueDate(item.due.date)).format( + "ddd, MMM Do" + ); + } + + //If displayAsCountdown is true, convert all dates to days until due + //TODO: write countdown converter + if (self.config.displayOrder.includes("countdown")) { + var oneDay = 24 * 60 * 60 * 1000; + var dueDateTime = self.parseDueDate(item.due.date); + var dueDate = new Date( + dueDateTime.getFullYear(), + dueDateTime.getMonth(), + dueDateTime.getDate() + ); + var now = new Date(); + var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + var diffDays = Math.floor((dueDate - today) / oneDay); + if (item.due["date"] == "2100-12-31") { + //check for fake due date + item.countdown = "---"; + } else { + item.countdown = diffDays; + } + } + //TODO: write converter from responsible_uid to collaborator name + let collaborator = tasks.collaborators.find( + ({ id }) => id === item.responsible_uid + ); + if (collaborator === undefined) { + item.assignee = "---"; + } else { + item.assignee = collaborator.full_name; + item.avatarURL = + "https://dcff1xvirvpfp.cloudfront.net/" + + collaborator.image_id + + "_small.jpg"; + } + + //if displayPriorityAsIcon is true, convert priority number to icon + //TODO: write priority icon converter }); // Sorting code if you want to add new methods. // @@ -454,14 +505,6 @@ Module.register("MMM-Todoist", { //Slice by max Entries items = items.slice(0, this.config.maximumEntries); - //If displayAsCountdown is true, convert all dates to days until due - //TODO: write countdown converter - - //TODO: write converter from responsible_uid to collaborator name - - //if displayPriorityAsIcon is true, convert priority number to icon - //TODO: write priority icon converter - this.tasks = { items: items, projects: tasks.projects, diff --git a/baseTemplate.njk b/baseTemplate.njk index f58f295..31b8e74 100644 --- a/baseTemplate.njk +++ b/baseTemplate.njk @@ -10,15 +10,34 @@ displayDueAsCountdown: false #} -
- This template is displaying this text. -
    - {% for item in self.tasks.items %} - {% for col in self.config.displayOrder %} -
  • {{item[col]}}
  • +
    + + + {% for header in displayOrder %} + {% endfor %} + + + {% for task in tasks.items %} + + {% for col in displayOrder %} + + {% endfor %} + {% endfor %} - + +
    {{header}}
    + {% if col == "labels" %} + {%for label in task.labels %} + {# break label string into individual labels #} + {{label}} + {% endfor %} + {% elseif col == "avatar" %} + + {% else %} + {{task[col]}} + {% endif %} +
    From 8e5942ee5ccc4bde8512a689778f6bfa9cdf3d13 Mon Sep 17 00:00:00 2001 From: Amos Glenn Date: Wed, 22 Mar 2023 09:16:50 -0400 Subject: [PATCH 5/7] completed template --- MMM-Todoist.css | 134 +++++++++++----------------- MMM-Todoist.js | 206 ++++++++++++++++---------------------------- baseTemplate.njk | 69 +++++++++++---- icons/countdown.svg | 15 ++++ 4 files changed, 191 insertions(+), 233 deletions(-) create mode 100644 icons/countdown.svg diff --git a/MMM-Todoist.css b/MMM-Todoist.css index 7fd2158..e6d9520 100644 --- a/MMM-Todoist.css +++ b/MMM-Todoist.css @@ -1,113 +1,77 @@ -.priority { - width: 3px; - border-right: 3px; +.task-table { + width: 100%; + text-align: center; + border-collapse: collapse; } -.priority1 { - background-color: #d1453b; +.task-table thead th { + text-align: center; } -.priority2 { - background-color: #eb8909; +.col-content { + text-align: left; } -.priority3 { - background-color: #246fe0; +.task-table td { + padding: 0 5px 0 5px; } -.alignLeft { - text-align: left; +.header-icon-countdown::before { + font: var(--fa-font-solid); + content: "\f254"; } -.spacerCell { - width: 3px; +.header-icon-content::before { + font: var(--fa-font-solid); + content: "\f04b"; } -.overdue { - text-decoration: underline #ac0000; +.header-icon-duedate::before { + font: var(--fa-font-solid); + content: "\f274"; } -.today { - text-decoration: underline #03a05c; +.header-icon-priority::before { + font: var(--fa-font-solid); + content: "\f06a"; } -.tomorrow { - text-decoration: underline #166cec; +.header-icon-assignee::before { + font: var(--fa-font-solid); + content: "\f007"; } -.projectcolor { - padding: 0 !important; - margin-right: 4px !important; - display: inline-block; - width: 12px; - height: 12px; - border-radius: 12px; - vertical-align: top; - margin-top: 3px; +.header-icon-avatar::before { + font: var(--fa-font-solid); + content: "\f007"; } -/* .dueDate { - min-width: 100px; -} */ - -.todoAvatarImg { - border-radius: 50%; - /* filter: grayscale(100%); */ - height: 27px; - display: block; - margin-left: 10px; - margin-right: auto; +.header-icon-labels::before { + font: var(--fa-font-solid); + content: "\f02b"; } -.divTable { - display: table; - width: 100%; -} -.divTableRow { - display: table-row; -} -.divTableHeading { - background-color: #eee; - display: table-header-group; -} -.divTableCell, -.divTableHead { - /* border: 1px solid #999999; */ - display: table-cell; - /* padding: 3px 10px; */ - vertical-align: middle; -} -.divTableHeading { - background-color: #eee; - display: table-header-group; - font-weight: bold; -} -.divTableFoot { - background-color: #eee; - display: table-footer-group; - font-weight: bold; -} -.divTableBody { - display: table-row-group; -} -.todoTextCell { - width: 400px; - overflow: hidden; -} -.tasks-wrapper { - width: 100%; - border-collapse: collapse; - text-align: left; -} -.tasks-wrapper th, -.tasks-wrapper td { - padding: 20px; -} -.tasks-wrapper tbody tr { - border-bottom: 2px solid #dddddd; +.header-icon-project::before { + font: var(--fa-font-solid); + content: "\f03a"; } + .task-label-bubble { background-color: #333333; border-radius: 15px; padding: 2px 6px 2px 6px; } +.task-project-bubble { + text-align: center; + border-style: solid; + border-width: 1px; + border-radius: 10px; + padding: 0 6px 0 6px; +} + +/*font awesome requires this to make icons render reliably */ +.icon::before { + display: inline-block; + text-rendering: auto; + -webkit-font-smoothing: antialiased; +} diff --git a/MMM-Todoist.js b/MMM-Todoist.js index 479a20c..f6f97c0 100644 --- a/MMM-Todoist.js +++ b/MMM-Todoist.js @@ -82,6 +82,30 @@ Module.register("MMM-Todoist", { 49: "#ccac93" }, + //colorMap taken from https://developer.todoist.com/guides/#colors + colorMap: [ + { id: 30, name: "berry_red", hex: "#b8256f" }, + { id: 31, name: "red", hex: "#db4035" }, + { id: 31, name: "orange", hex: "#ff9933" }, + { id: 33, name: "yellow", hex: "#fad000" }, + { id: 34, name: "olive_green", hex: "#afb83b" }, + { id: 35, name: "lime_green", hex: "#7ecc49" }, + { id: 36, name: "green", hex: "#299438" }, + { id: 37, name: "mint_green", hex: "#6accbc" }, + { id: 38, name: "teal", hex: "#158fad" }, + { id: 39, name: "sky_blue", hex: "#14aaf5" }, + { id: 40, name: "light_blue", hex: "#96c3eb" }, + { id: 41, name: "blue", hex: "#4073ff" }, + { id: 42, name: "grape", hex: "#884dff" }, + { id: 43, name: "violet", hex: "#af38eb" }, + { id: 44, name: "lavender", hex: "#eb96eb" }, + { id: 45, name: "magenta", hex: "#e05194" }, + { id: 46, name: "salmon", hex: "#ff8d85" }, + { id: 47, name: "charcoal", hex: "#808080" }, + { id: 48, name: "grey", hex: "#b8b8b8" }, + { id: 49, name: "taupe", hex: "#ccac93" } + ], + //This has been designed to use the Todoist Sync API. apiVersion: "v9", apiBase: "https://todoist.com/API", @@ -101,10 +125,18 @@ Module.register("MMM-Todoist", { "labels", "assignee", "avatar", - "checked" + "project" ], - displayPriorityAsIcon: true, - displayDueAsCountdown: false, + priorityColors: { + 1: "#333333", + 2: "#246fe0", + 3: "#eb8909", + 4: "#d1453b" + }, + displayProjectAs: "both", //"name" excludes color dot icon, "color" excludes the project name (anything else = "both") + duedateFormat: "ddd, MMM Do", //see moment.js strong formatting + displayColumnHeadings: "icons", //"text", "icons", "none" --hiding column text saves a little space in some columns + displayTaskInProjectColor: false, ///content column text is displayed in project color tasks: false }, @@ -272,7 +304,6 @@ Module.register("MMM-Todoist", { // ******** Data sent from the Backend helper. This is the data from the Todoist API ************ socketNotificationReceived: function (notification, payload) { if (notification === "TASKS") { - Log.info("FILTERING PAYLOAD"); this.config.tasks = this.filterTodoistData(payload); if (this.config.displayLastUpdate) { @@ -288,7 +319,6 @@ Module.register("MMM-Todoist", { } this.loaded = true; - Log.info("UPDATING DOM"); this.updateDom(1000); } else if (notification === "FETCH_ERROR") { Log.error("Todoist Error. Could not fetch todos: " + payload.error); @@ -300,8 +330,6 @@ Module.register("MMM-Todoist", { var items = []; var labelIds = []; - Log.info("DOING FILTERING NOW"); - if (tasks == undefined) { return; } @@ -387,7 +415,7 @@ Module.register("MMM-Todoist", { } } - // Filter using projets if projects are configured + // Filter using projects if projects are configured if (self.config.projects.length > 0) { self.config.projects.forEach(function (project) { if (item.project_id == project) { @@ -431,19 +459,36 @@ Module.register("MMM-Todoist", { item.all_day = true; } - //TEMP converting due date object to string - //TODO: add config option for date string to show + //converting due date object to string if (item.due["date"] == "2100-12-31") { //check for fake due date item.duedate = "---"; } else { item.duedate = moment(self.parseDueDate(item.due.date)).format( - "ddd, MMM Do" + self.config.duedateFormat ); } - //If displayAsCountdown is true, convert all dates to days until due - //TODO: write countdown converter + //inserting project info into task item + if (self.config.displayOrder.includes("project")) { + let proj = tasks.projects.find(({ id }) => id === item.project_id); + if (proj === undefined) { + item.project = { + name: "---", + color: "#808080" + }; + } else { + let projColor = self.config.colorMap.find( + ({ name }) => name === proj.color + ); + item.project = { + name: proj.name, + color: projColor.hex + }; + } + } + + //convert all dates to days until due for countdown if (self.config.displayOrder.includes("countdown")) { var oneDay = 24 * 60 * 60 * 1000; var dueDateTime = self.parseDueDate(item.due.date); @@ -462,22 +507,22 @@ Module.register("MMM-Todoist", { item.countdown = diffDays; } } - //TODO: write converter from responsible_uid to collaborator name - let collaborator = tasks.collaborators.find( - ({ id }) => id === item.responsible_uid - ); - if (collaborator === undefined) { - item.assignee = "---"; - } else { - item.assignee = collaborator.full_name; - item.avatarURL = - "https://dcff1xvirvpfp.cloudfront.net/" + - collaborator.image_id + - "_small.jpg"; - } - //if displayPriorityAsIcon is true, convert priority number to icon - //TODO: write priority icon converter + //convert assignee to avatar url + if (self.config.displayOrder.includes("avatar")) { + let collaborator = tasks.collaborators.find( + ({ id }) => id === item.responsible_uid + ); + if (collaborator === undefined) { + item.assignee = "---"; + } else { + item.assignee = collaborator.full_name; + item.avatarURL = + "https://dcff1xvirvpfp.cloudfront.net/" + + collaborator.image_id + + "_small.jpg"; + } + } }); // Sorting code if you want to add new methods. // @@ -510,7 +555,6 @@ Module.register("MMM-Todoist", { projects: tasks.projects, collaborators: tasks.collaborators }; - Log.info("FILTERING COMPLETE--UPDATING CONFIG"); return this.tasks; }, /* @@ -570,6 +614,8 @@ Module.register("MMM-Todoist", { }); return itemstoSort; }, + + //BELOW IS KEPT ONLY TEMPORARY----------------------------------------------------------------------------------- createCell: function (className, innerHTML) { var cell = document.createElement("div"); cell.className = "divTableCell " + className; @@ -733,106 +779,4 @@ 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; - } - - //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(); - - for (var value = 0; value < this.tasks.collaborators.length; value++) { - collaboratorsMap.set(this.tasks.collaborators[value].id, value); - } - - //Iterate through Todos - this.tasks.items.forEach((item) => { - var divRow = document.createElement("div"); - //Add the Row - divRow.className = "divTableRow"; - - //Columns - divRow.appendChild(this.addPriorityIndicatorCell(item)); - divRow.appendChild(this.addColumnSpacerCell()); - divRow.appendChild(this.addTodoTextCell(item)); - divRow.appendChild(this.addDueDateCell(item)); - if (this.config.showProject) { - divRow.appendChild(this.addColumnSpacerCell()); - divRow.appendChild(this.addProjectCell(item)); - } - if (this.config.displayAvatar) { - divRow.appendChild(this.addAssigneeAvatorCell(item, collaboratorsMap)); - } - - divBody.appendChild(divRow); - }); - - divTable.appendChild(divBody); - wrapper.appendChild(divTable); - - // 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 - ) - )) - ); - - // display the update time at the end, if defined so by the user config - if (this.config.displayLastUpdate) { - var updateinfo = document.createElement("div"); - updateinfo.className = "xsmall light align-left"; - updateinfo.innerHTML = - "Update : " + - moment - .unix(this.lastUpdate) - .format(this.config.displayLastUpdateFormat); - wrapper.appendChild(updateinfo); - } - - // FOR DEBUGGING TO HELP PEOPLE GET THEIR PROJECT IDs - (People who can't see console) // - if (this.config.debug) { - var projectsids = document.createElement("div"); - projectsids.className = "xsmall light align-left"; - projectsids.innerHTML = "*** PROJECT -- ID ***
    "; - this.tasks.projects.forEach((project) => { - projectsids.innerHTML += - "" + project.name + " -- " + project.id + "
    "; - }); - wrapper.appendChild(projectsids); - } - - return wrapper; - } */ }); diff --git a/baseTemplate.njk b/baseTemplate.njk index 31b8e74..e547228 100644 --- a/baseTemplate.njk +++ b/baseTemplate.njk @@ -1,20 +1,14 @@ -{# displayOrder: [ - "content", - "due", - "priority", - "labels", - "assignee", - "checked" - ], - displayPriorityAsIcon: true, - displayDueAsCountdown: false -#} -
    - +
    {% for header in displayOrder %} - + {% endfor %} @@ -22,13 +16,39 @@ {% for col in displayOrder %} {% for col in displayOrder %} -
    {{header}} + {% if displayColumnHeadings == "text" %} + {{header}} + {% elseif displayColumnHeadings == "icons" %} + + {% endif %} +
    + {% if col == "labels" %} {%for label in task.labels %} - {# break label string into individual labels #} {{label}} - {% endfor %} + {% endfor %} + {% elseif col == "avatar" %} - + + + {% elseif col == "priority" %} + + + + + {% elseif col == "project" %} + {% if displayProjectAs !== "name" %} + + {% endif %} + {% if displayProjectAs !== "color" %} + {{task.project.name}} + {% else %} +   + {% endif %} + {% if displayProjectAs !== "name" %} + + {% endif %} + + {% elseif col == "content" and displayTaskInProjectColor %} + {{task.content}} + + {% elseif col == "countdown" %} + {{task.countdown}} + {% else %} {{task[col]}} {% endif %} @@ -42,3 +62,18 @@ +{# UNUSED CODE FOR DISPLAYING CHECKED AND UNCHECKED BOXES FOR TASKS +SYNCAPI DOES NOT RETURN COMPLETED TASKS, BUT WE MIGHT FIND A WAY TO SHOW THEM LATER +{% elseif col == "checked" %} + {% if task.checked %} + + + + + + {% else %} + + + + {% endif %} +#} diff --git a/icons/countdown.svg b/icons/countdown.svg new file mode 100644 index 0000000..39aa50a --- /dev/null +++ b/icons/countdown.svg @@ -0,0 +1,15 @@ + + + + + + + + + From 65c1c9cfdadf5ed9bebe2eb65e3163c2c21b46aa Mon Sep 17 00:00:00 2001 From: Amos Glenn Date: Thu, 23 Mar 2023 17:23:49 -0400 Subject: [PATCH 6/7] replaced dom construction with nkj template --- MMM-Todoist.js | 7 +++++++ icons/countdown.svg | 15 --------------- 2 files changed, 7 insertions(+), 15 deletions(-) delete mode 100644 icons/countdown.svg diff --git a/MMM-Todoist.js b/MMM-Todoist.js index f6f97c0..d334e97 100644 --- a/MMM-Todoist.js +++ b/MMM-Todoist.js @@ -4,11 +4,18 @@ * Module: MMM-Todoist * * By Chris Brooker + * Forked by Amos Glenn * * MIT Licensed. */ /* + * FORKED BY AMOS GLENN March 20, 2023 + * -replaced dom construction in javascript with njk template + * -added display column for due date countdown + * -added config option for column display order + * -added template data to config + * * Update by mabahj 24/11/2019 * - Added support for labels in addtion to projects * Update by AgP42 the 18/07/2018 diff --git a/icons/countdown.svg b/icons/countdown.svg deleted file mode 100644 index 39aa50a..0000000 --- a/icons/countdown.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - From 9430b23a7bd531ca275296965e144d7db547a003 Mon Sep 17 00:00:00 2001 From: Amos Glenn Date: Sun, 26 Mar 2023 17:53:16 -0400 Subject: [PATCH 7/7] added formatting and time internationalization --- MMM-Todoist.css | 28 ++++- MMM-Todoist.js | 294 +++++++++-------------------------------------- baseTemplate.njk | 32 ++---- node_helper.js | 109 ++++++++++-------- 4 files changed, 148 insertions(+), 315 deletions(-) diff --git a/MMM-Todoist.css b/MMM-Todoist.css index e6d9520..6fd3159 100644 --- a/MMM-Todoist.css +++ b/MMM-Todoist.css @@ -13,7 +13,7 @@ } .task-table td { - padding: 0 5px 0 5px; + padding: 2px 5px 2px 5px; } .header-icon-countdown::before { @@ -56,8 +56,14 @@ content: "\f03a"; } +.nowrapping { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + .task-label-bubble { - background-color: #333333; + background-color: rgba(255, 255, 255, 0.2); /* 80% transparent */ border-radius: 15px; padding: 2px 6px 2px 6px; } @@ -69,6 +75,24 @@ padding: 0 6px 0 6px; } +.overdue { + color: firebrick; + font-size: smaller; +} +.today { + color: chartreuse; +} +.tomorrow { + color: green; + font-size: smaller; +} +.soon { + color: forestgreen; +} +.later { + color: gray; +} + /*font awesome requires this to make icons render reliably */ .icon::before { display: inline-block; diff --git a/MMM-Todoist.js b/MMM-Todoist.js index d334e97..fe348cd 100644 --- a/MMM-Todoist.js +++ b/MMM-Todoist.js @@ -4,18 +4,12 @@ * Module: MMM-Todoist * * By Chris Brooker - * Forked by Amos Glenn * * MIT Licensed. */ /* - * FORKED BY AMOS GLENN March 20, 2023 - * -replaced dom construction in javascript with njk template - * -added display column for due date countdown - * -added config option for column display order - * -added template data to config - * + * Update by mabahj 24/11/2019 * - Added support for labels in addtion to projects * Update by AgP42 the 18/07/2018 @@ -40,9 +34,9 @@ var UserPresence = true; //true by default, so no impact for user without a PIR Module.register("MMM-Todoist", { defaults: { maximumEntries: 10, - projects: [], + projects: [], //include all task from these projects regardless of label blacklistProjects: false, - labels: [""], + labels: [""], //tasks with these labels will be displayed regardless of project updateInterval: 10 * 60 * 1000, // every 10 minutes, fade: true, fadePoint: 0.25, @@ -52,44 +46,15 @@ Module.register("MMM-Todoist", { //New config from AgP42 displayLastUpdate: false, //add or not a line after the tasks with the last server update time displayLastUpdateFormat: "dd - HH:mm:ss", //format to display the last update. See Moment.js documentation for all display possibilities - maxTitleLength: 25, //10 to 50. Value to cut the line if wrapEvents: true + maxTitleLength: 50, //10 to 50. Value to cut the line if wrapEvents: true wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength displayTasksWithoutDue: true, // Set to false to not print tasks without a due date displayTasksWithinDays: -1, // If >= 0, do not print tasks with a due date more than this number of days into the future (e.g., 0 prints today and overdue) // 2019-12-31 by thyed displaySubtasks: true, // set to false to exclude subtasks - displayAvatar: false, - showProject: true, - // projectColors: ["#95ef63", "#ff8581", "#ffc471", "#f9ec75", "#a8c8e4", "#d2b8a3", "#e2a8e4", "#cccccc", "#fb886e", - // "#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", - 31: "#db4035", - 32: "#ff9933", - 33: "#fad000", - 34: "#afb83b", - 35: "#7ecc49", - 36: "#299438", - 37: "#6accbc", - 38: "#158fad", - 39: "#14aaf5", - 40: "#96c3eb", - 41: "#4073ff", - 42: "#884dff", - 43: "#af38eb", - 44: "#eb96eb", - 45: "#e05194", - 46: "#ff8d85", - 47: "#808080", - 48: "#b8b8b8", - 49: "#ccac93" - }, //colorMap taken from https://developer.todoist.com/guides/#colors + //names needed for project colors colorMap: [ { id: 30, name: "berry_red", hex: "#b8256f" }, { id: 31, name: "red", hex: "#db4035" }, @@ -119,11 +84,11 @@ Module.register("MMM-Todoist", { todoistEndpoint: "sync", todoistResourceType: - '["items", "projects", "collaborators", "user", "labels"]', //TODO add "filters", "reminders", "sections", + '["items", "projects", "collaborators", "user", "labels"]', - debug: false, + debug: true, - //Options to display for each task + //display these columns in this order; all are optional displayOrder: [ "content", "duedate", @@ -134,17 +99,16 @@ Module.register("MMM-Todoist", { "avatar", "project" ], + //taken from Todoist priorityColors: { 1: "#333333", 2: "#246fe0", 3: "#eb8909", 4: "#d1453b" }, - displayProjectAs: "both", //"name" excludes color dot icon, "color" excludes the project name (anything else = "both") - duedateFormat: "ddd, MMM Do", //see moment.js strong formatting - displayColumnHeadings: "icons", //"text", "icons", "none" --hiding column text saves a little space in some columns - displayTaskInProjectColor: false, ///content column text is displayed in project color - tasks: false + displayProjectAs: "both", //"name" excludes color border surrounding project name, "color" excludes the project name (anything else = "both" project name and project color border around name) + displayColumnHeadings: "icons", //"text", "icons", "none" --using column text makes table significantly wider + tasks: false //not user adjustable, this is where the template data is stored }, // Define required scripts. @@ -152,8 +116,6 @@ Module.register("MMM-Todoist", { return "baseTemplate.njk"; }, getTemplateData: function () { - Log.info("SENDING TEMPLATE DATA"); - Log.info(this.config); return this.config; }, @@ -197,7 +159,6 @@ Module.register("MMM-Todoist", { ? JSON.parse(JSON.stringify(this.config.projects)) : []; - Log.info("FETCHING TODOIST DATA"); this.sendSocketNotification("FETCH_TODOIST", this.config); //add ID to the setInterval function to be able to stop it later on @@ -255,62 +216,13 @@ Module.register("MMM-Todoist", { } }, - // Code from MichMich from default module Calendar : to manage task displayed on several lines - /** - * Shortens a string if it's longer than maxLength and add a ellipsis to the end - * - * @param {string} string Text string to shorten - * @param {number} maxLength The max length of the string - * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength - * @returns {string} The shortened string - */ - shorten: function (string, maxLength, wrapEvents) { - if (typeof string !== "string") { - return ""; - } - - if (wrapEvents === true) { - var temp = ""; - var currentLine = ""; - var words = string.split(" "); - - for (var i = 0; i < words.length; i++) { - var word = words[i]; - if ( - currentLine.length + word.length < - (typeof maxLength === "number" ? maxLength : 25) - 1 - ) { - // max - 1 to account for a space - currentLine += word + " "; - } else { - if (currentLine.length > 0) { - temp += currentLine + "
    " + word + " "; - } else { - temp += word + "
    "; - } - currentLine = ""; - } - } - - return (temp + currentLine).trim(); - } else { - if ( - maxLength && - typeof maxLength === "number" && - string.length > maxLength - ) { - return string.trim().slice(0, maxLength) + "…"; - } else { - return string.trim(); - } - } - }, - //end modif AgP - // Override socket notification handler. // ******** Data sent from the Backend helper. This is the data from the Todoist API ************ socketNotificationReceived: function (notification, payload) { if (notification === "TASKS") { + if (this.config.debug) { + Log.info(payload); + } this.config.tasks = this.filterTodoistData(payload); if (this.config.displayLastUpdate) { @@ -366,19 +278,7 @@ Module.register("MMM-Todoist", { } } - // Loop through labels fetched from API and find corresponding label IDs for task filtering - // Could be re-used for project names -> project IDs. - /* if (self.config.labels.length > 0 && tasks.labels != undefined) { - for (let apiLabel of tasks.labels) { - for (let configLabelName of self.config.labels) { - if (apiLabel.name == configLabelName) { - labelIds.push(apiLabel.id); - break; - } - } - } - } */ - + //include all tasks with due dates (if set in config) OR with due dates within config number of days if ( self.config.displayTasksWithinDays > -1 || !self.config.displayTasksWithoutDue @@ -402,14 +302,14 @@ Module.register("MMM-Todoist", { }); } - //Filter the Todos by the criteria specified in the Config + //filter tasks to include in template data by the criteria specified in the Config tasks.items.forEach(function (item) { - // Ignore sub-tasks + // do not include any subtasks if (item.parent_id != null && !self.config.displaySubtasks) { return; } - // Filter using label if a label is configured + // Filter to include all tasks with labels listed in config (if any labels are listed) if (self.config.labels.length > 0 && item.labels.length > 0) { // Check all the labels assigned to the task. Add to items if match with configured label for (let label of item.labels) { @@ -422,16 +322,15 @@ Module.register("MMM-Todoist", { } } - // Filter using projects if projects are configured + // Filter to include tasks with projects listed in config (if any projects are listed) if (self.config.projects.length > 0) { self.config.projects.forEach(function (project) { if (item.project_id == project) { items.push(item); - return; } }); } - }); + }); //end of filters // FOR DEBUGGING TO HELP PEOPLE GET THEIR PROJECT IDs // if (self.config.debug) { @@ -471,14 +370,12 @@ Module.register("MMM-Todoist", { //check for fake due date item.duedate = "---"; } else { - item.duedate = moment(self.parseDueDate(item.due.date)).format( - self.config.duedateFormat - ); + item.duedate = self.addDueDate(item); } - //inserting project info into task item + //inserting project info into task item for template if (self.config.displayOrder.includes("project")) { - let proj = tasks.projects.find(({ id }) => id === item.project_id); + let proj = tasks.projects.find(({ pid }) => pid === item.project_id); if (proj === undefined) { item.project = { name: "---", @@ -495,39 +392,41 @@ Module.register("MMM-Todoist", { } } - //convert all dates to days until due for countdown + //convert all due dates into days-until-due for countdown if (self.config.displayOrder.includes("countdown")) { - var oneDay = 24 * 60 * 60 * 1000; - var dueDateTime = self.parseDueDate(item.due.date); - var dueDate = new Date( - dueDateTime.getFullYear(), - dueDateTime.getMonth(), - dueDateTime.getDate() - ); - var now = new Date(); - var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - var diffDays = Math.floor((dueDate - today) / oneDay); if (item.due["date"] == "2100-12-31") { //check for fake due date item.countdown = "---"; } else { - item.countdown = diffDays; + let itemDueDate = moment(self.parseDueDate(item.due.date)); + item.countdown = itemDueDate.diff(moment(), "days") + 1; //adding to include the day something is due } } - //convert assignee to avatar url - if (self.config.displayOrder.includes("avatar")) { + //insert assignee and avatar url + if ( + self.config.displayOrder.includes("assignee") || + self.config.displayOrder.includes("avatar") + ) { let collaborator = tasks.collaborators.find( ({ id }) => id === item.responsible_uid ); if (collaborator === undefined) { item.assignee = "---"; + item.avatarURL = + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 15'%3E%3Ccircle cx='7.5' cy='7.5' r='7.1' style='fill: none; stroke: %23828282; stroke-width: .8px;'/%3E%3Ccircle cx='7.5' cy='5.63' r='3.08' style='fill: none; stroke: %23828282; stroke-width: .8px;'/%3E%3Cpath d='M2.33,12.36c1.02-2.86,4.16-4.35,7.01-3.34,1.56,.55,2.78,1.78,3.34,3.34' style='fill: none; stroke: %23828282; stroke-width: .8px;'/%3E%3C/svg%3E"; } else { item.assignee = collaborator.full_name; - item.avatarURL = - "https://dcff1xvirvpfp.cloudfront.net/" + - collaborator.image_id + - "_small.jpg"; + if (collaborator.image_id) { + /* Todoist provides a url for each user's avatar */ + item.avatarURL = + "https://dcff1xvirvpfp.cloudfront.net/" + + collaborator.image_id + + "_small.jpg"; + } else { + item.avatarURL = + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 15'%3E%3Ccircle cx='7.5' cy='7.5' r='7.1' style='fill: none; stroke: %23828282; stroke-width: .8px;'/%3E%3Ccircle cx='7.5' cy='5.63' r='3.08' style='fill: none; stroke: %23828282; stroke-width: .8px;'/%3E%3Cpath d='M2.33,12.36c1.02-2.86,4.16-4.35,7.01-3.34,1.56,.55,2.78,1.78,3.34,3.34' style='fill: none; stroke: %23828282; stroke-width: .8px;'/%3E%3C/svg%3E"; + } } } }); @@ -557,6 +456,7 @@ Module.register("MMM-Todoist", { //Slice by max Entries items = items.slice(0, this.config.maximumEntries); + //collate filtered and converted task information this.tasks = { items: items, projects: tasks.projects, @@ -622,53 +522,8 @@ Module.register("MMM-Todoist", { return itemstoSort; }, - //BELOW IS KEPT ONLY TEMPORARY----------------------------------------------------------------------------------- - createCell: function (className, innerHTML) { - var cell = document.createElement("div"); - cell.className = "divTableCell " + className; - cell.innerHTML = innerHTML; - return cell; - }, - addPriorityIndicatorCell: function (item) { - var className = "priority "; - switch (item.priority) { - case 4: - className += "priority1"; - break; - case 3: - className += "priority2"; - break; - case 2: - className += "priority3"; - break; - default: - className = ""; - break; - } - return this.createCell(className, " "); - }, - addColumnSpacerCell: function () { - return this.createCell("spacerCell", " "); - }, - addTodoTextCell: function (item) { - var temp = document.createElement("div"); - temp.innerHTML = item.contentHtml; - - var para = temp.getElementsByTagName("p"); - - return this.createCell( - "title bright alignLeft", - this.shorten( - para[0].innerHTML, - this.config.maxTitleLength, - this.config.wrapEvents - ) - ); - - // return this.createCell("title bright alignLeft", item.content); - }, - addDueDateCell: function (item) { - var className = "bright align-right dueDate "; + addDueDate: function (item) { + item.isDue = "later"; //var className = "bright align-right dueDate "; var innerHTML = ""; var oneDay = 24 * 60 * 60 * 1000; @@ -693,25 +548,25 @@ Module.register("MMM-Todoist", { }) + " " + dueDate.getDate(); - className += "xsmall overdue"; + item.isDue = "overdue"; //className += "xsmall overdue"; } else if (diffDays === -1) { innerHTML = this.translate("YESTERDAY"); - className += "xsmall overdue"; + item.isDue = "overdue"; //className += "xsmall overdue"; } else if (diffDays === 0) { innerHTML = this.translate("TODAY"); if (item.all_day || dueDateTime >= now) { - className += "today"; + item.isDue = "today"; //className += "today"; } else { - className += "overdue"; + item.isDue = "overdue"; //className += "overdue"; } } else if (diffDays === 1) { innerHTML = this.translate("TOMORROW"); - className += "xsmall tomorrow"; + item.isDue = "tomorrow"; //className += "xsmall tomorrow"; } else if (diffDays < 7) { innerHTML = dueDate.toLocaleDateString(config.language, { weekday: "short" }); - className += "xsmall"; + item.isDue = "soon"; //className += "xsmall"; } else if (diffMonths < 7 || dueDate.getFullYear() == now.getFullYear()) { innerHTML = dueDate.toLocaleDateString(config.language, { @@ -719,10 +574,10 @@ Module.register("MMM-Todoist", { }) + " " + dueDate.getDate(); - className += "xsmall"; + //className += "xsmall"; } else if (item.due.date === "2100-12-31") { innerHTML = ""; - className += "xsmall"; + //className += "xsmall"; } else { innerHTML = dueDate.toLocaleDateString(config.language, { @@ -732,9 +587,8 @@ Module.register("MMM-Todoist", { dueDate.getDate() + " " + dueDate.getFullYear(); - className += "xsmall"; + //className += "xsmall"; } - if (innerHTML !== "" && !item.all_day) { function formatTime(d) { function z(n) { @@ -750,40 +604,6 @@ Module.register("MMM-Todoist", { } innerHTML += formatTime(dueDateTime); } - return this.createCell(className, innerHTML); - }, - addProjectCell: function (item) { - var project = this.tasks.projects.find((p) => p.id === item.project_id); - var projectcolor = this.config.projectColors[project.color]; - var innerHTML = - "" + - project.name; - return this.createCell("xsmall", innerHTML); - }, - addAssigneeAvatorCell: function (item, collaboratorsMap) { - var avatarImg = document.createElement("img"); - avatarImg.className = "todoAvatarImg"; - - var colIndex = collaboratorsMap.get(item.responsible_uid); - if ( - typeof colIndex !== "undefined" && - this.tasks.collaborators[colIndex].image_id != null - ) { - avatarImg.src = - "https://dcff1xvirvpfp.cloudfront.net/" + - this.tasks.collaborators[colIndex].image_id + - "_big.jpg"; - } else { - avatarImg.src = "/modules/MMM-Todoist/1x1px.png"; - } - - var cell = this.createCell("", ""); - cell.appendChild(avatarImg); - - return cell; + return innerHTML; //this.createCell(className, innerHTML); } }); diff --git a/baseTemplate.njk b/baseTemplate.njk index e547228..e598b3e 100644 --- a/baseTemplate.njk +++ b/baseTemplate.njk @@ -15,7 +15,8 @@ {% for task in tasks.items %}
    + + {% if col == "labels" %} {%for label in task.labels %} @@ -43,12 +44,11 @@ {% endif %} - {% elseif col == "content" and displayTaskInProjectColor %} - {{task.content}} + {% elseif col == "content" and not wrapEvents %} +
    {{task.content}}
    - {% elseif col == "countdown" %} - {{task.countdown}} - + {% elseif col == "duedate" %} + {{task.duedate}} {% else %} {{task[col]}} {% endif %} @@ -58,22 +58,4 @@ {% endfor %}
    -
    - - - -{# UNUSED CODE FOR DISPLAYING CHECKED AND UNCHECKED BOXES FOR TASKS -SYNCAPI DOES NOT RETURN COMPLETED TASKS, BUT WE MIGHT FIND A WAY TO SHOW THEM LATER -{% elseif col == "checked" %} - {% if task.checked %} - - - - - - {% else %} - - - - {% endif %} -#} +
\ No newline at end of file diff --git a/node_helper.js b/node_helper.js index 4d189d8..4f10169 100644 --- a/node_helper.js +++ b/node_helper.js @@ -15,57 +15,64 @@ const showdown = require("showdown"); const markdown = new showdown.Converter(); module.exports = NodeHelper.create({ - start: function() { - console.log("Starting node helper for: " + this.name); - }, + start: function () { + console.log("Starting node helper for: " + this.name); + }, - socketNotificationReceived: function(notification, payload) { - if (notification === "FETCH_TODOIST") { - this.config = payload; - this.fetchTodos(); - } - }, + socketNotificationReceived: function (notification, payload) { + if (notification === "FETCH_TODOIST") { + this.config = payload; + this.fetchTodos(); + } + }, - fetchTodos : function() { - var self = this; - //request.debug = true; - var acessCode = self.config.accessToken; - 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: { - sync_token: "*", - resource_types: self.config.todoistResourceType - } - }, - function(error, response, body) { - if (error) { - self.sendSocketNotification("FETCH_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); - taskJson.items.forEach((item)=>{ - item.contentHtml = markdown.makeHtml(item.content); - }); + fetchTodos: function () { + var self = this; + //request.debug = true; + var acessCode = self.config.accessToken; + 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: { + sync_token: "*", + resource_types: self.config.todoistResourceType + } + }, + function (error, response, body) { + console.log(body); + if (error) { + self.sendSocketNotification("FETCH_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); + taskJson.items.forEach((item) => { + item.contentHtml = markdown.makeHtml(item.content); + }); - taskJson.accessToken = acessCode; - self.sendSocketNotification("TASKS", taskJson); - } - else{ - console.log("Todoist api request status="+response.statusCode); - } - - }); - } -}); \ No newline at end of file + taskJson.accessToken = acessCode; + self.sendSocketNotification("TASKS", taskJson); + } else { + console.log("Todoist api request status=" + response.statusCode); + } + } + ); + } +});