Commit 406537ab authored by ransome1's avatar ransome1
Browse files

Added threshold function, added ambigous dates enhancement

parent 623441bd
{
"name": "sleek",
"productName": "sleek",
"version": "1.0.9-rc.2",
"version": "1.0.9-rc.3",
"description": "Todo app based on todo.txt for Linux, Windows and MacOS, free and open-source",
"synopsis": "Todo app based on todo.txt for Linux, Windows and MacOS, free and open-source",
"category": "ProjectManagement",
......
......@@ -3,7 +3,18 @@
https://codepen.io/fitri/full/oWovYj/ */
"use strict";
import { reorderSortingLevel } from "../render.js";
import { setUserData, startBuilding } from "../render.js";
function reorderSortingLevel() {
let sortBy = new Array;
const children = sortByContainer.children;
for(let i=0; i<children.length; i++) {
if(!children[i].getAttribute("data-id")) continue;
sortBy.push(children[i].getAttribute("data-id"));
}
setUserData("sortBy", sortBy);
startBuilding();
}
function enableDragSort(listClass) {
const sortableLists = document.getElementsByClassName(listClass);
......
......@@ -259,9 +259,6 @@ body.dark #drawerContainer .drawer {
body.dark #drawerContainer .drawer h4.is-4 {
color: white;
}
body.dark #drawerContainer .drawer h4.is-4 i {
color: #CCCDCF !important;
}
body.dark #drawerContainer .drawer button span.tag {
color: #5a5a5a;
}
......
This diff is collapsed.
......@@ -242,7 +242,7 @@ function setFriendlyLanguageNames() {
default:
return;
}
var option = document.createElement("option");
let option = document.createElement("option");
option.text = friendlyLanguageName;
option.value = language;
if(language===userData.language) option.selected = true;
......
......@@ -2,7 +2,7 @@
import { translations, userData } from "../render.js";
import { _paq } from "./matomo.mjs";
import { resizeInput } from "./form.mjs";
import { RecExtension, SugarDueExtension } from "./todotxtExtensions.mjs";
import { RecExtension, SugarDueExtension, ThresholdExtension } from "./todotxtExtensions.mjs";
import "../../node_modules/jstodotxt/jsTodoExtensions.js";
import "../../node_modules/jstodotxt/jsTodoTxt.js";
import Datepicker from "../../node_modules/vanillajs-datepicker/js/Datepicker.js";
......@@ -21,7 +21,7 @@ datePickerInput.addEventListener("changeDate", function (e) {
// we only update the object if there is a date selected. In case of a refresh it would throw an error otherwise
if(e.detail.date) {
// generate the object on what is written into input, so we don't overwrite previous inputs of user
let todo = new TodoTxtItem(document.getElementById("modalFormInput").value, [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
let todo = new TodoTxtItem(document.getElementById("modalFormInput").value, [ new SugarDueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
todo.due = new Date(e.detail.date);
todo.dueString = new Date(e.detail.date.getTime() - (e.detail.date.getTimezoneOffset() * 60000 )).toISOString().split("T")[0];
// if suggestion box was open, it needs to be closed
......@@ -53,7 +53,7 @@ const datePicker = new Datepicker(datePickerInput, {
}
});
document.querySelector(".datepicker .clear-btn").onclick = function() {
let todo = new TodoTxtItem(document.getElementById("modalFormInput").value, [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
let todo = new TodoTxtItem(document.getElementById("modalFormInput").value, [ new SugarDueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
todo.due = undefined;
todo.dueString = undefined;
document.getElementById("modalFormInput").value = todo.toString();
......
This diff is collapsed.
......@@ -50,6 +50,7 @@ NotOp
comparison
= left:priorityComparison { return left; }
/ left:dueComparison { return left; }
/ left:thresholdComparison { return left; }
priorityComparison
= priorityKeyword _ op:compareOp _ right:priorityLiteral { return ["pri", right, op]; }
......@@ -67,8 +68,17 @@ dueComparison
/ "due:" right:dateStr { return ["duestr", right]; }
/ "due" { return ["due"]; }
thresholdComparison
= "t" _ op:compareOp _ right:dateExpr { return ["threshold"].concat(right, [op]); }
/ "t:" right:dateStr { return ["tstr", right]; }
/ "t" { return ["threshold"]; }
dateExpr
= left:dateLiteral _ op:dateOp _ count:number unit:[dbwmy] {
if (count.length == 0) {
/* empty count string means default "1" value */
count = 1;
}
if (op == "-") {
count = count * -1;
}
......@@ -99,19 +109,19 @@ dateStr
dateLiteral
= year:number4 "-" month: number2 "-" day:number2 {
let d = new Date(year, month-1, day);
return d.getTime();
let m = month > 0 ? (month <= 12 ? month-1 : 11) : 0;
let d = day > 0 ? (day <= 31 ? day : 31) : 1; /* ignores lengths of months */
return new Date(year, m, d).getTime();
}
/ year:number4 "-" month: number2 {
let d = new Date(year, month-1, 1);
return d.getTime();
let m = month > 0 ? (month <= 12 ? month-1 : 11) : 0;
return new Date(year, m, 1).getTime();
}
/ year:number4 {
let d = new Date(year, 0, 1);
return d.getTime();
return new Date(year, 0, 1).getTime();
}
/ "today" {
let d = new Date(); // now, w current time of day
let d = new Date(); // now, w current time of day
d = new Date(d.getFullYear(), d.getMonth(), d.getDate());
return d.getTime();
}
......@@ -130,10 +140,10 @@ number4
= [0-9][0-9][0-9][0-9] { return text(); }
number2
= [0-9][0-9] { return text(); }
= [0-9][0-9]? { return text(); }
number
= [0-9]+ { return text(); }
= [0-9]* { return text(); } /* used in date intervals only */
StringLiteral "string"
= '"' chars:DoubleStringCharacter* '"'? {
......@@ -167,9 +177,9 @@ SourceCharacter
= .
name
= '"' nonblank+ '"' { return text(); }
/ nonblankparen+ '"' { return '"' + text(); }
/ nonblankparen+ { return text(); }
= '"' nonblank+ '"' { return text(); }
/ nonblankparen+ '"' { return '"' + text(); }
/ nonblankparen+ { return text(); }
nonblank
= [^ \t\n\r"]
......@@ -178,4 +188,4 @@ nonblankparen
= [^ \t\n\r"()]
_ "whitespace"
= [ \t\n\r]*
= [ \t\n\r]*
......@@ -41,6 +41,29 @@ function runQuery(item, compiledQuery) {
stack.push(false); // no due date
}
break;
case "threshold":
if (item.t) {
// normalize date to have time of midnight in local zone
// we represent dates as millisec from epoch to simplify comparison
let d = item.t;
stack.push(new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime());
} else {
stack.push(undefined); // all comparisons will return false
}
break;
case "tstr":
// match next value (a string) as prefix of ISO date string of threshold date
next = q.shift(); // the string to compare
if (item.t) {
// normalize date to have time of midnight in local zone
// we represent dates as millisec from epoch to simplify comparison
let d = item.t;
d = new Date(d.getFullYear(), d.getMonth(), d.getDate());
stack.push(d.toISOString().slice(0, 10).startsWith(next));
} else {
stack.push(false); // no threshold date
}
break;
case "complete":
stack.push(item.complete);
break;
......
......@@ -2,7 +2,7 @@
import "../../node_modules/jstodotxt/jsTodoExtensions.js";
import { resetModal, handleError, userData, setUserData, translations, getConfirmation } from "../render.js";
import { _paq } from "./matomo.mjs";
import { RecExtension, SugarDueExtension } from "./todotxtExtensions.mjs";
import { RecExtension, SugarDueExtension, ThresholdExtension } from "./todotxtExtensions.mjs";
import { generateFilterData } from "./filters.mjs";
import { items, item, setTodoComplete } from "./todos.mjs";
import { datePickerInput } from "./datePicker.mjs";
......@@ -303,7 +303,7 @@ function show(todo, templated) {
if(todo) {
// replace invisible multiline ascii character with new line
// we need to check if there already is a due date in the object
todo = new TodoTxtItem(todo, [ new DueExtension(), new RecExtension() ]);
todo = new TodoTxtItem(todo, [ new DueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
// set the priority
setPriority(todo.priority);
//
......@@ -393,7 +393,7 @@ function submitForm() {
const index = items.objects.map(function(item) {return item.toString(); }).indexOf(modalForm.getAttribute("data-item"));
// create a todo.txt object
// replace new lines with spaces (https://stackoverflow.com/a/34936253)
let todo = new TodoTxtItem(document.getElementById("modalFormInput").value.replaceAll(/[\r\n]+/g, String.fromCharCode(16)), [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
let todo = new TodoTxtItem(document.getElementById("modalFormInput").value.replaceAll(/[\r\n]+/g, String.fromCharCode(16)), [ new SugarDueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
// check and prevent duplicate todo
if(items.objects.map(function(item) {return item.toString(); }).indexOf(todo.toString())!=-1) {
modalFormAlert.innerHTML = translations.formInfoDuplicate;
......@@ -413,7 +413,7 @@ function submitForm() {
} else if(!modalForm.getAttribute("data-item") && document.getElementById("modalFormInput").value!="") {
// in case there hasn't been a passed data item, we just push the input value as a new item into the array
// replace new lines with spaces (https://stackoverflow.com/a/34936253)
let todo = new TodoTxtItem(document.getElementById("modalFormInput").value.replaceAll(/[\r\n]+/g, String.fromCharCode(16)), [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
let todo = new TodoTxtItem(document.getElementById("modalFormInput").value.replaceAll(/[\r\n]+/g, String.fromCharCode(16)), [ new SugarDueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
// we add the current date to the start date attribute of the todo.txt object
todo.date = new Date();
// check and prevent duplicate todo
......
......@@ -34,13 +34,48 @@ function generateRecurrence(todo) {
try {
// duplicate not reference
let recurringTodo = Object.assign({}, todo);
recurringTodo.date = new Date;
// if the item to be duplicated has been completed before the due date, the recurring item needs to be set incomplete again
recurringTodo.complete = false;
recurringTodo.completed = null;
// if the item to be duplicated has been completed before the due date, the recurring item needs to be set incomplete again
recurringTodo.date = new Date;
// if todo has no due date, the recurrence time frame will be added to the date of completion
recurringTodo.due = getDueDate(todo.due, todo.rec);
recurringTodo.dueString = convertDate(recurringTodo.due);
// adjust due and threshold dates
let recSplit = splitRecurrence(todo.rec);
if (recSplit.plus) {
// strict recurrence is based on previous date value
if (todo.t)
recurringTodo.t = addIntervalToDate(todo.t, recSplit.mul, recSplit.period);
if (todo.due)
recurringTodo.due = addIntervalToDate(todo.due, recSplit.mul, recSplit.period);
} else {
// non-strict recurrence is based on today's date
if (todo.t) {
let threshold_base = new Date();
if (todo.due) {
// preserve interval between threshold and due date
let interval = todo.due - todo.t; // millisec
threshold_base = new Date(threshold_base.getTime() - interval);
}
recurringTodo.t = addIntervalToDate(threshold_base, recSplit.mul, recSplit.period);
console.log("t based on today plus rec: " + recurringTodo + " todo.t is " + recurringTodo.t);
}
if (todo.due)
recurringTodo.due = addIntervalToDate(new Date(), recSplit.mul, recSplit.period);
}
if (!todo.t && !todo.due) {
// This is an ambiguous case: there is a rec: tag but no dates to apply it to.
// Some would prefer to make this a no-op, but past versions of sleek have added
// a due date when there is only a recurrence, so we will continue to do that here.
recurringTodo.due = addIntervalToDate(new Date(), recSplit.mul, recSplit.period);
}
// the following strings are magic from jsTodoTxt and must be changed when dates are changed
// because TodoTxtItem.toString() relies on the fieldString values, not the field values themselves.
if (recurringTodo.t)
recurringTodo.tString = convertDate(recurringTodo.t);
if (recurringTodo.due)
recurringTodo.dueString = convertDate(recurringTodo.due);
// get index of recurring todo
const index = items.objects.map(function(item) {return item.toString().replaceAll(String.fromCharCode(16)," "); }).indexOf(recurringTodo.toString().replaceAll(String.fromCharCode(16)," "));
// only add recurring todo if it is not already in the list
......@@ -57,15 +92,6 @@ function generateRecurrence(todo) {
return Promise.reject(error);
}
}
function getDueDate(due, recurrence) {
let recSplit = splitRecurrence(recurrence);
if (!recSplit.plus || !due) {
// no plus in recurrence expression, so do the default "non-strict" recurrence.
// (Otherwise we will use the previous due date, for strict recurrence.)
due = new Date(); // use today's date as base for recurrence
}
return addIntervalToDate(due, recSplit.mul, recSplit.period);
}
// addIntervalToDate used to compute recurrences, but now also used to add
// or subtract a time interval to/from dates in filter query language.
......
......@@ -6,7 +6,7 @@ import { categories } from "./filters.mjs";
import { generateRecurrence } from "./recurrences.mjs";
import { convertDate, isToday, isTomorrow, isPast } from "./date.mjs";
import { show } from "./form.mjs";
import { SugarDueExtension, RecExtension } from "./todotxtExtensions.mjs";
import { SugarDueExtension, RecExtension, ThresholdExtension } from "./todotxtExtensions.mjs";
const modalForm = document.getElementById("modalForm");
const todoContext = document.getElementById("todoContext");
......@@ -73,6 +73,22 @@ todoContext.addEventListener("keyup", function(event) {
if(event.key==="Escape") this.classList.remove("is-active");
});
function showResultStats() {
try {
// we show some information on filters if any are set
if(items.filtered.length!=items.objects.length) {
resultStats.classList.add("is-active");
resultStats.firstElementChild.innerHTML = translations.visibleTodos + "&nbsp;<strong>" + items.filtered.length + " </strong>&nbsp;" + translations.of + "&nbsp;<strong>" + items.objects.length + "</strong>";
return Promise.resolve("Info: Result box is shown");
} else {
resultStats.classList.remove("is-active");
return Promise.resolve("Info: Result box is hidden");
}
} catch(error) {
error.functionName = showResultStats.name;
return Promise.reject(error);
}
}
function configureTodoTableTemplate(append) {
try {
// setting up for the first cluster
......@@ -99,7 +115,7 @@ function configureTodoTableTemplate(append) {
}
function generateItems(content) {
try {
items = { objects: TodoTxt.parse(content, [ new DueExtension(), new HiddenExtension(), new RecExtension() ]) }
items = { objects: TodoTxt.parse(content, [ new DueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]) }
items.objects = items.objects.filter(function(item) {
if(!item.text) return false;
return true;
......@@ -153,6 +169,8 @@ function generateGroups(items) {
}
async function generateTable(groups, append) {
try {
// configure stats
showResultStats();
// prepare the templates for the table
await configureTodoTableTemplate(append);
// reset cluster count for this run
......@@ -429,7 +447,7 @@ function sortTodoData(group) {
function setTodoComplete(todo) {
try {
// first convert the string to a todo.txt object
todo = new TodoTxtItem(todo, [ new DueExtension(), new HiddenExtension(), new RecExtension() ]);
todo = new TodoTxtItem(todo, [ new DueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
// get index of todo
const index = items.objects.map(function(item) {return item.toString(); }).indexOf(todo.toString());
// mark item as in progress
......@@ -472,7 +490,7 @@ function setTodoDelete(todo) {
// in case edit form is open, text has changed and complete button is pressed, we do not fall back to the initial value of todo but instead choose input value
if(document.getElementById("modalFormInput").value) todo = document.getElementById("modalFormInput").value;
// first convert the string to a todo.txt object
todo = new TodoTxtItem(todo, [ new DueExtension(), new HiddenExtension(), new RecExtension() ]);
todo = new TodoTxtItem(todo, [ new DueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
// get index of todo
const index = items.objects.map(function(item) {return item.toString(); }).indexOf(todo.toString());
// Delete item
......@@ -498,7 +516,7 @@ function setTodoDelete(todo) {
}
function addTodo(todo) {
try {
todo = new TodoTxtItem(todo, [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
todo = new TodoTxtItem(todo, [ new SugarDueExtension(), new HiddenExtension(), new RecExtension(), new ThresholdExtension() ]);
// abort if there is no text
if(!todo.text) return Promise.resolve("Info: Text is missing, no todo is written");
// we add the current date to the start date attribute of the todo.txt object
......
......@@ -33,7 +33,7 @@ SugarDueExtension.prototype.parsingFunction = function (line) {
// Try to parse a valid date until the end of the text
for (var i = Math.max(5, words.length); i > 0; i--) {
match = words.slice(0, i).join(" ");
dueDate = Sugar.Date.create(match);
dueDate = Sugar.Date.create(match, {future: true});
if (Sugar.Date.isValid(dueDate)) {
return [dueDate, line.replace("due:" + match, ''), Sugar.Date.format(dueDate, '%Y-%m-%d')];
}
......@@ -42,4 +42,20 @@ SugarDueExtension.prototype.parsingFunction = function (line) {
return [null, null, null];
};
export { RecExtension, SugarDueExtension };
function ThresholdExtension() {
this.name = "t";
}
ThresholdExtension.prototype = new TodoTxtExtension();
ThresholdExtension.prototype.parsingFunction = function (line) {
var thresholdDate = null;
var thresholdRegex = /t:([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})\s*/;
var matchThreshold = thresholdRegex.exec(line);
if ( matchThreshold !== null ) {
var datePieces = matchThreshold[1].split('-');
thresholdDate = new Date( datePieces[0], datePieces[1] - 1, datePieces[2] );
return [thresholdDate, line.replace(thresholdRegex, ''), matchThreshold[1]];
}
return [null, null, null];
};
export { RecExtension, SugarDueExtension, ThresholdExtension };
......@@ -173,7 +173,9 @@ const createWindow = async function() {
// skip persisted files and go with ENV if set
if(process.env.SLEEK_CUSTOM_FILE && fs.existsSync(process.env.SLEEK_CUSTOM_FILE)) {
file = process.env.SLEEK_CUSTOM_FILE;
}
} /*else if(process.argv.length > 1 && fs.existsSync(process.argv[1])) {
file = process.argv[1];
}*/
// use the loop to check if the new path is already in the user data
let fileFound = false;
if(userData.data.files) {
......
......@@ -107,16 +107,6 @@ async function getConfirmation() {
modalPrompt.classList.remove("is-active");
});
}
function reorderSortingLevel() {
let sortBy = new Array;
const children = sortByContainer.children;
for(let i=0; i<children.length; i++) {
if(!children[i].getAttribute("data-id")) continue;
sortBy.push(children[i].getAttribute("data-id"));
}
setUserData("sortBy", sortBy);
startBuilding();
}
function configureMainView() {
try {
// close filterContext if open
......@@ -715,22 +705,7 @@ function showOnboarding(variable) {
return Promise.reject(error);
}
}
function showResultStats() {
try {
// we show some information on filters if any are set
if(todos.items.filtered.length!=todos.items.objects.length) {
resultStats.classList.add("is-active");
resultStats.firstElementChild.innerHTML = translations.visibleTodos + "&nbsp;<strong>" + todos.items.filtered.length + " </strong>&nbsp;" + translations.of + "&nbsp;<strong>" + todos.items.objects.length + "</strong>";
return Promise.resolve("Info: Result box is shown");
} else {
resultStats.classList.remove("is-active");
return Promise.resolve("Info: Result box is hidden");
}
} catch(error) {
error.functionName = showResultStats.name;
return Promise.reject(error);
}
}
function getBadgeCount() {
let count = 0;
todos.items.objects.forEach((item) => {
......@@ -755,8 +730,6 @@ async function startBuilding(append) {
configureMainView();
showResultStats();
window.api.send("update-badge", getBadgeCount());
console.info("Table build:", performance.now() - t0, "ms");
......@@ -907,4 +880,4 @@ window.api.receive("refresh", async function(content) {
});
});
export { resetFilters, resetModal, setUserData, startBuilding, handleError, userData, appData, translations, modal, setTheme, getConfirmation, reorderSortingLevel };
export { resetFilters, resetModal, setUserData, startBuilding, handleError, userData, appData, translations, modal, setTheme, getConfirmation };
......@@ -289,9 +289,6 @@ body.dark {
background: $even-darker-grey!important;
h4.is-4 {
color: white;
i {
color: $lighter-grey!important;
}
}
button {
span.tag {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment