Commit 4cabc995 authored by ransome1's avatar ransome1
Browse files

Added test cases, added confirmation modal, cleaned up the code a bit

parent 22b20958
......@@ -82,7 +82,7 @@ A prioritized backlog of new features and known issues can be found <a href="htt
* <a href="https://github.com/ransome1/sleek/wiki/Keyboard-shortcuts">Keyboard shortcuts following todotxt.net</a>
* Tabindex available
* <a href="https://github.com/ransome1/sleek/wiki/Hidden-todos">A todo can be hidden but its attributes will be available in the filter drawer and autocomplete function</a>
* Alarms will be shown when a todo is due tomorrow or today
* Due dates trigger alarms and appear as badges in sleeks icon
* Dark and light mode can be toggled
* A compact view is available
* Completed todos can be bulk archived to a separate done.txt ([name of todo file]_done.txt) file
......
{
"name": "sleek",
"productName": "sleek",
"version": "1.0.6",
"version": "1.0.6-1",
"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",
......@@ -104,8 +104,8 @@
"build:appimage": "electron-builder -l AppImage --publish never",
"pack": "electron-builder --dir",
"lint": "eslint --ext .js, src --ext .mjs, src",
"test": "mocha --timeout 5000",
"test1": "mocha ./test/settings.js --timeout 5000",
"test": "mocha --timeout 10000",
"test1": "mocha ./test/createTodos.js --timeout 10000",
"sass": "sass -w src/scss/:src/css/",
"start": "yarn sass & electron ."
},
......
......@@ -372,7 +372,7 @@ nav ul:nth-child(2) {
cursor: not-allowed;
}
#drawerContainer .drawer h4.is-4 {
font-size: 1.35em;
font-size: 1.25em;
color: #4a4a4a;
font-family: FreeSansBold;
margin-bottom: 1.5em;
......@@ -769,6 +769,13 @@ nav ul:nth-child(2) {
display: block;
}
#modalPrompt {
z-index: 60;
}
#modalPrompt .modal-content {
width: 20em;
}
.contentContainer {
width: 100%;
height: 90%;
......@@ -847,7 +854,7 @@ nav ul:nth-child(2) {
}
.priority .button, .priority .button:hover {
font-size: 1.35em;
font-size: 1.25em;
font-family: FreeSansBold;
background: #ccc;
color: #666666;
......@@ -1153,13 +1160,18 @@ body.compact nav ul li.logo {
line-height: 3.5em;
font-family: "FreeSansBold";
}
body.compact .drawer {
body.compact #drawerContainer .drawer {
padding: 1.5em !important;
}
body.compact .drawer .button {
body.compact #drawerContainer .drawer .is-4 {
font-size: 1.1em;
}
body.compact #drawerContainer .drawer .button {
font-size: 0.9em;
margin: 0 0.3em 0.3em 0;
padding: 0.25em 0.6em;
}
body.compact .drawer .button .tag {
body.compact #drawerContainer .drawer .button .tag {
padding: 0 0.5em;
width: auto;
height: auto;
......
This diff is collapsed.
......@@ -26,8 +26,8 @@
<ul>
<li class="logo">sleek</li>
<li id="navBtnAddTodo"><a href="#" tabindex="0"><i class="fas fa-plus"></i></a></li>
<li id="navBtnFilter"><a href="#" tabindex="0"><i class="fas fa-filter"></i></a></li>
<li id="navBtnView"><a href="#" tabindex="0"><i class="fas fa-sliders-h"></i></a></li>
<li id="navBtnFilter" class="drawer" data-drawer="filterDrawer"><a href="#" tabindex="0"><i class="fas fa-filter"></i></a></li>
<li id="navBtnView" class="drawer" data-drawer="viewDrawer"><a href="#" tabindex="0"><i class="fas fa-sliders-h"></i></a></li>
<li id="btnOpenTodoFile"><a href="#" tabindex="-1"><i class="fas fa-folder-open"></i></a></li>
<li id="btnTheme"><a href="#" tabindex="-1"><i class="fas fa-adjust"></i></a></li>
</ul>
......@@ -304,11 +304,7 @@
</div>
<div id="recurrencePicker" class="field">
<div class="control has-icons-left">
<<<<<<< HEAD
<input id="recurrencePickerInput" class="input" tabindex="0" readonly>
=======
<input id="recurrencePickerInput" class="input" readonly tabindex="-1">
>>>>>>> develop
<a href="#" class="icon is-left" tabindex="-1">
<i class="fas fa-redo"></i>
</a>
......@@ -691,11 +687,11 @@
</div>
</div>
<div id="todoContext" class="dropdown-menu" role="menu">
<div id="todoContext" class="dropdown-menu" role="menu" tabindex="0">
<div class="dropdown-content">
<a id="todoContextUseAsTemplate" class="dropdown-item"></a>
<a id="todoContextEdit" href="#" class="dropdown-item"></a>
<a id="todoContextDelete" class="dropdown-item"></a>
<a id="todoContextUseAsTemplate" class="dropdown-item" tabindex="0"></a>
<a id="todoContextEdit" href="#" class="dropdown-item" tabindex="0"></a>
<a id="todoContextDelete" class="dropdown-item" tabindex="0"></a>
</div>
</div>
......@@ -730,7 +726,21 @@
</div>
</div>
<div id="modalPrompt" class="modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="card">
<div id="modalPromptContent" class="card-content"></div>
<footer class="card-footer">
<a href="#" id="modalPromptConfirm" class="card-footer-item" tabindex="0">Confirm</a>
<a href="#" id="modalPromptCancel" class="card-footer-item" tabindex="0">Cancel</a>
</footer>
</div>
</div>
</div>
<section id="messages">
<article class="message fixed" data="logging">
<div class="message-header">
<p><span id="messageLoggingTitle"></span></p>
......@@ -741,6 +751,7 @@
<p><button id="btnMessageLogging" class="button" tabindex="0"><i class="fas fa-cog"></i>&nbsp;<span id="messageLoggingButton"></span></button></p>
</div>
</article>
<article class="message fixed" data="share">
<div class="message-header">
<p><span id="messageShareTitle"></span></p>
......@@ -755,6 +766,7 @@
</p>
</div>
</article>
<article id="errorContainer" class="notification is-danger" data="error" style="position: relative">
<button id="errorContainerClose" class="delete" aria-label="delete" data-message="error"></button>
<p id="errorMessage"></p>
......
"use strict";
import { modal, userData, appData, setUserData, translations, handleError, setTheme } from "../render.js";
import { modal, userData, appData, setUserData, translations, handleError, setTheme, getConfirmation } from "../render.js";
import { _paq } from "./matomo.mjs";
import { createModalJail } from "../configs/modal.config.mjs";
......@@ -20,7 +20,6 @@ const helpTabContextsProjectsBody = document.getElementById("helpTabContextsProj
const helpTabContextsProjectsTitle = document.getElementById("helpTabContextsProjectsTitle");
const helpTabDatesBody1 = document.getElementById("helpTabDatesBody1");
const helpTabDatesBody2 = document.getElementById("helpTabDatesBody2");
const helpTabRecurrencesBody1 = document.getElementById("helpTabRecurrencesBody1");
const helpTabDatesTitle1 = document.getElementById("helpTabDatesTitle1");
const helpTabDatesTitle2 = document.getElementById("helpTabDatesTitle2");
......@@ -86,19 +85,14 @@ helpTab2Title.innerHTML = translations.priorities;
helpTab3Title.innerHTML = translations.helpTab3Title;
helpTab4Title.innerHTML = translations.helpTab4Title;
helpTab5Title.innerHTML = translations.helpTab5Title;
helpTabContextsProjectsBody.innerHTML = translations.helpTabContextsProjectsBody;
helpTabContextsProjectsTitle.innerHTML = translations.helpTabContextsProjectsTitle;
helpTabDatesBody1.innerHTML = translations.helpTabDatesBody1;
helpTabDatesBody2.innerHTML = translations.helpTabDatesBody2;
helpTabRecurrencesBody1.innerHTML = translations.helpTabRecurrencesBody1;
helpTabDatesTitle1.innerHTML = translations.helpTabDatesTitle1;
helpTabDatesTitle2.innerHTML = translations.helpTabDatesTitle2;
helpTabRecurrencesTitle1.innerHTML = translations.helpTabRecurrencesTitle1;
helpTabKeyboardTitle.innerHTML = translations.shortcuts;
helpTabKeyboardTR10TD1.innerHTML = translations.helpTabKeyboardTR10TD1;
helpTabKeyboardTR1TD1.innerHTML = translations.addTodo;
......@@ -154,16 +148,12 @@ contentTabs.forEach(tab => tab.addEventListener("click", function() {
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "Content", "Click on " + this.firstElementChild.innerHTML, this.classList[0]]);
}));
settingsLanguage.onchange = function() {
userData.language = this.value;
window.api.send("userData", ["language", userData.language]);
window.api.send("changeLanguage", this.value);
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "Settings", "Language changed to: " + this.value]);
settingsLanguage.onchange = function(event) {
getConfirmation(setLanguage, translations.restartPrompt, this.value);
}
toggleNotifications.onclick = function() {
//notifications = this.checked;
setUserData('notifications', this.checked);
setUserData("notifications", this.checked);
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "Setting", "Click on Notifications", this.checked])
}
......@@ -172,17 +162,28 @@ toggleDarkmode.onclick = function() {
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "Setting", "Click on Dark mode", this.checked])
}
toggleTray.onclick = function() {
setUserData("tray", this.checked);
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "Setting", "Click on Tray", this.checked])
// restart
window.api.send("restart");
toggleTray.onclick = function(event) {
event.preventDefault();
getConfirmation(setTray, translations.restartPrompt, this.checked);
}
toggleNotifications.checked = userData.notifications;
function setTray(setting) {
setUserData("tray", setting);
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "Settings", "Tray changed to: " + setting]);
window.api.send("restart");
return Promise.resolve("Info: Tray changed to: " + setting);
}
function setLanguage(language) {
if(appData.environment==="testing") return false;
userData.language = language;
window.api.send("userData", ["language", userData.language]);
window.api.send("changeLanguage", language);
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "Settings", "Language changed to: " + language]);
}
function showTab(tab) {
contentTabsCards.forEach(function(el) {
el.classList.remove("is-active");
......
......@@ -3,17 +3,15 @@ import { setUserData, userData, handleError } from "../render.js";
import { _paq } from "./matomo.mjs";
import { getHandleElement, startDragging } from "./drawer_handle.mjs";
const viewDrawer = document.getElementById("viewDrawer");
const filterDrawer = document.getElementById("filterDrawer");
const drawerContainer = document.getElementById("drawerContainer");
const todoTable = document.getElementById("todoTable");
const todoTableSearchContainer = document.getElementById("todoTableSearchContainer");
const navBtnFilter = document.getElementById("navBtnFilter");
const navBtnView = document.getElementById("navBtnView");
const drawers = document.querySelectorAll(".drawer");
const drawers = document.querySelectorAll("nav ul li.drawer");
document.getElementById("drawerClose").onclick = function() {
showDrawer(false).then(function(result) {
showDrawer(null, null, true).then(function(result) {
console.log(result);
}).catch(function(error) {
handleError(error);
......@@ -41,9 +39,7 @@ document.getElementById("viewDrawer").addEventListener ("keydown", function () {
});
getHandleElement.addEventListener("mousedown", startDragging);
navBtnFilter.onclick = function() {
let toggle = true;
if(document.getElementById("drawerContainer").classList.contains("is-active")) toggle = false;
showDrawer(toggle, "navBtnFilter", "filterDrawer").then(function(result) {
showDrawer(this, this.getAttribute("data-drawer")).then(function(result) {
console.log(result);
}).catch(function(error) {
handleError(error);
......@@ -52,9 +48,7 @@ navBtnFilter.onclick = function() {
if(userData.matomoEvents) _paq.push(["trackEvent", "Menu", "Click on filter"]);
}
navBtnView.onclick = function() {
let toggle = true;
if(document.getElementById("drawerContainer").classList.contains("is-active")) toggle = false;
showDrawer(toggle, this.id, viewDrawer.id).then(function(result) {
showDrawer(this, this.getAttribute("data-drawer")).then(function(result) {
console.log(result);
}).catch(function(error) {
handleError(error);
......@@ -62,63 +56,38 @@ navBtnView.onclick = function() {
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "Menu", "Click on view"]);
}
// open filter drawer if it has been persisted
showDrawer(userData.filterDrawer, "navBtnFilter", "filterDrawer").then(function(result) {
console.log(result);
}).catch(function(error) {
handleError(error);
});
export function showDrawer(variable, buttonId, drawerId) {
try {
if(!variable && !buttonId && !drawerId) {
export function showDrawer(button, drawer, close) {
try {
// close drawers and the container and persist it
if(close) {
drawerContainer.classList.remove("is-active");
filterDrawer.classList.remove("is-active");
viewDrawer.classList.remove("is-active");
navBtnFilter.classList.remove("is-highlighted");
navBtnView.classList.remove("is-highlighted");
setUserData("filterDrawer", false);
drawers.forEach((item) => {
item.classList.remove("is-highlighted");
document.getElementById(item.getAttribute("data-drawer")).classList.remove("is-active");
setUserData(item.getAttribute("data-drawer"), false);
});
return Promise.resolve("Success: Drawer closed");
}
switch (drawerId) {
case "viewDrawer":
// highlight persisted selection in dropdown
Array.from(document.getElementById("viewSelectSortBy").options).forEach(function(item) {
if(item.value===userData.sortBy) item.selected = true
});
break;
}
buttonId = document.getElementById(buttonId);
drawerId = document.getElementById(drawerId);
// always hide the drawer container first
drawerContainer.classList.remove("is-active");
// next show or hide the single drawers
switch(variable) {
case true:
buttonId.classList.add("is-highlighted");
drawerId.classList.add("is-active");
drawerContainer.classList.add("is-active");
break;
case false:
drawers.forEach(function(drawer) {
drawer.classList.remove("is-active");
});
buttonId.classList.remove("is-highlighted");
drawerContainer.classList.remove("is-active");
break;
}
setUserData(drawerId.id, variable);
// persist filter drawer state
if(drawerId && drawerId.classList.contains("is-active")) {
// if the drawer is open the table needs a fixed width to overlap the viewport
todoTable.style.minWidth = "45em";
todoTableSearchContainer.style.minWidth = "45em";
// close open drawers, open the new one and persist it
drawer = document.getElementById(drawer);
if(drawer.classList.contains("is-active")) {
drawerContainer.classList.remove("is-active");
drawer.classList.remove("is-active");
button.classList.remove("is-highlighted");
setUserData(drawer.id, false);
return Promise.resolve("Success: Drawer closed");
} else {
// undo what has been done
todoTable.style.minWidth = "auto";
todoTableSearchContainer.style.minWidth = "auto";
drawerContainer.classList.add("is-active");
drawers.forEach((item) => {
item.classList.remove("is-highlighted");
document.getElementById(item.getAttribute("data-drawer")).classList.remove("is-active");
});
drawer.classList.add("is-active");
button.classList.add("is-highlighted");
setUserData(drawer.id, true);
return Promise.resolve("Success: Drawer opened");
}
return Promise.resolve("Success: Drawer toggled");
} catch(error) {
error.functionName = showDrawer.name;
return Promise.reject(error);
......
......@@ -22,6 +22,10 @@ let categories,
filterContextSave.innerHTML = translations.save;
filterContextDelete.innerHTML = translations.delete;
filterContextInput.addEventListener("keydown", (event) => {
if(event.code==="Space") event.preventDefault();
})
function saveFilter(newFilter, oldFilter, category) {
try {
items.objects.forEach((item) => {
......
......@@ -247,8 +247,6 @@ function show(todo, templated) {
// 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 SugarDueExtension(), new RecExtension(), new HiddenExtension() ]);
// pass todo string to form data item
modalForm.setAttribute("data-item", todo.toString());
// set the priority
setPriority(todo.priority);
//
......@@ -265,6 +263,8 @@ function show(todo, templated) {
document.getElementById("modalFormInput").setSelectionRange(selectStart, selectEnd);
btnItemStatus.classList.remove("is-active");
} else {
// pass todo string to form data item
modalForm.setAttribute("data-item", todo.toString());
// this is an existing todo task to be edited
// put the initially passed todo to the modal data field
//modalForm.setAttribute("data-item", todo.toString());
......
......@@ -8,6 +8,10 @@ let _paq;
function configureMatomo() {
try {
_paq = window._paq = window._paq || [];
// only continue if app is connected to the internet
if(!navigator.onLine) return Promise.resolve("Info: App is offline, Matomo will not be loaded");
// only continue if machine is no dvelopment machine
if(appData.environment) return Promise.resolve("Info: No tracking in development and testing environment");
if(!userData.uid) {
// generate random number/string combination as user id and persist it
......@@ -29,10 +33,6 @@ function configureMatomo() {
return ">301"
}
}
// only continue if app is connected to the internet
if(!navigator.onLine) return Promise.resolve("Info: App is offline, Matomo will not be loaded");
_paq = window._paq = window._paq || [];
//if(appData.development) return Promise.resolve("Info: Machine is development machine, logging will be skipped")
if(userData.uid)_paq.push(['setUserId', userData.uid]);
if(userData.theme)_paq.push(['setCustomDimension', 1, userData.theme]);
if(userData.language)_paq.push(['setCustomDimension', 2, userData.language]);
......@@ -82,7 +82,6 @@ function configureMatomo() {
}
toggleMatomoEvents.onclick = function() {
//matomoEvents = this.checked;
setUserData('matomoEvents', this.checked);
configureMatomo(this.checked).then(response => {
console.info(response);
......
import { userData, translations } from "../render.js";
import { showContent } from "./content.mjs";
import { show } from "./form.mjs";
import { _paq } from "./matomo.mjs";
const navBtnAddTodo = document.getElementById("navBtnAddTodo");
const navBtnHelp = document.getElementById("navBtnHelp");
const navBtnSettings = document.getElementById("navBtnSettings");
const navBtnView = document.getElementById("navBtnView");
......@@ -12,6 +14,11 @@ navBtnSettings.firstElementChild.setAttribute("title", translations.settings);
navBtnView.firstElementChild.setAttribute("title", translations.view);
btnTheme.setAttribute("title", translations.toggleDarkMode);
navBtnAddTodo.onclick = function () {
show();
// trigger matomo event
if(userData.matomoEvents) matomo._paq.push(["trackEvent", "Menu", "Click on add todo"]);
}
navBtnHelp.onclick = function () {
showContent("modalHelp");
// trigger matomo event
......
......@@ -71,6 +71,9 @@ todoTableWrapper.addEventListener("scroll", function(event) {
startBuilding(true);
}
});
todoContext.addEventListener("keyup", function(event) {
if(event.key==="Escape") this.classList.remove("is-active");
});
function configureTodoTableTemplate(append) {
try {
......@@ -347,6 +350,7 @@ function generateTableRow(todo) {
todoTableBodyRow.appendChild(todoTableBodyCellText);
todoTableBodyRow.addEventListener("contextmenu", event => {
todoContext.focus();
todoContext.style.left = event.x + "px";
todoContext.style.top = event.y + "px";
todoContext.classList.toggle("is-active");
......@@ -537,6 +541,9 @@ async function archiveTodos() {
window.api.send("writeToFile", [contentForDoneFile.join("\n").toString() + "\n", doneFile()]);
// write incompleted items to todo file
window.api.send("writeToFile", [items.incomplete.join("\n").toString() + "\n", userData.file]);
// send notifcation on success
generateNotification(null, null, translations.archivingCompletedTitle, translations.archivingCompletedBody + doneFile());
return Promise.resolve("Success: Completed todos appended to: " + doneFile())
} catch(error) {
error.functionName = archiveTodos.name;
......@@ -548,7 +555,7 @@ function checkIsTodoVisible(todo) {
if(!todo.text) return false
return true;
}
function generateNotification(todo, offset) {
function generateNotification(todo, offset, customTitle, customBody) {
try {
// abort if user didn't permit notifications within sleek
if(!userData.notifications) return Promise.resolve("Info: Notification surpressed (turned off in sleek's settings)");
......@@ -556,29 +563,45 @@ function generateNotification(todo, offset) {
return navigator.permissions.query({name: "notifications"}).then(function(result) {
// abort if user didn't permit notifications
if(result.state!="granted") return Promise.resolve("Info: Notification surpressed (not permitted by OS)");
// add the offset so a notification shown today with "due tomorrow", will be shown again tomorrow but with "due today"
//const hash = generateHash(todo.due.toISOString().slice(0, 10) + todo.text) + offset;
const hash = generateHash(todo.toString()) + offset;
let title;
switch (offset) {
case 0:
title = translations.dueToday;
break;
case 1:
title = translations.dueTomorrow;
break;
}
// if notification already has been triggered once it will be discarded
if(userData.dismissedNotifications.includes(hash)) return Promise.resolve("Info: Notification skipped (has already been sent)");
// set options for notifcation
let notification = {
title: title,
body: todo.text,
string: todo.toString(),
let notification;
if(todo) {
// add the offset so a notification shown today with "due tomorrow", will be shown again tomorrow but with "due today"
//const hash = generateHash(todo.due.toISOString().slice(0, 10) + todo.text) + offset;
const hash = generateHash(todo.toString()) + offset;
let title;
switch (offset) {
case 0:
title = translations.dueToday;
break;
case 1:
title = translations.dueTomorrow;
break;
}
// if notification already has been triggered once it will be discarded
if(userData.dismissedNotifications.includes(hash)) return Promise.resolve("Info: Notification skipped (has already been sent)");
// set options for notifcation
notification = {
title: title,
body: todo.text,
string: todo.toString(),
timeoutType: "never",
silent: false,
actions: [{
type: "button",
text: "Show Button"
}]
}
// once shown, it will be persisted as hash to it won't be shown a second time
userData.dismissedNotifications.push(hash);
setUserData("dismissedNotifications", userData.dismissedNotifications);
} else {
notification = {
title: customTitle,
body: customBody,
timeoutType: "default",
silent: true
}
}
// once shown, it will be persisted as hash to it won't be shown a second time
userData.dismissedNotifications.push(hash);
setUserData("dismissedNotifications", userData.dismissedNotifications);
// send notification object to main process for execution
window.api.send("showNotification", notification);
// trigger matomo event
......
......@@ -15,8 +15,6 @@ const sortByDueDate = document.getElementById("sortByDueDate");
const sortByPriority = document.getElementById("sortByPriority");
const sortByProjects = document.getElementById("sortByProjects");
const sortCompletedLast = document.getElementById("sortCompletedLast");
//const toggleCompactView = document.getElementById("toggleCompactView");
const toggleTray = document.getElementById("toggleTray");
const viewHeadlineAppView = document.getElementById("viewHeadlineAppView");
const viewHeadlineTodoList = document.getElementById("viewHeadlineTodoList");
......@@ -35,10 +33,8 @@ const zoomRangePicker = document.getElementById("zoomRangePicker");
const zoomUndo = document.getElementById("zoomUndo");
const showEmptyFilters = document.getElementById("showEmptyFilters");
const viewToggleShowEmptyFilters = document.getElementById("viewToggleShowEmptyFilters");
const compactView = document.getElementById("compactView");
sortBy.innerHTML = translations.sortBy;
sortByContexts.innerHTML = translations.contexts;
sortByDueDate.innerHTML = translations.dueDate;
......@@ -72,7 +68,8 @@ viewSelectSortBy.onchange = async function() {
}
}
zoomRangePicker.onchange = function() {
zoom(this.value).then(response => {
const value = this.value;
zoom(value).then(response => {
console.log(response);
// trigger matomo event
if(userData.matomoEvents) _paq.push(["trackEvent", "View-Drawer", "Zoom ranger dragged"]);
......@@ -114,9 +111,8 @@ showEmptyFilters.checked = userData.showEmptyFilters;
function zoom(zoom) {