Skip to content
Open
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
'use strict';
const taskbook = require('./lib/taskbook');

if (!global.Intl) {
global.Intl = require('intl');
}

const taskbookCLI = (input, flags) => {
if (flags.archive) {
return taskbook.displayArchive();
Expand Down
64 changes: 61 additions & 3 deletions lib/render.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
'use strict';
const addWeeks = require('date-fns/add_months');
const isBefore = require('date-fns/is_before');
const endOfDay = require('date-fns/end_of_day');
const parseDate = require('date-fns/parse');
const IntlRelativeFormat = require('intl-relativeformat');
const chalk = require('chalk');
const signale = require('signale');
const config = require('./config');

const relativeFormat = new IntlRelativeFormat('en');

signale.config({displayLabel: false});

const {error, log, note, pending, success} = signale;
Expand Down Expand Up @@ -55,6 +62,31 @@ class Render {
return item.isStarred ? yellow('★') : '';
}

_getDueDate(item) {
if (!item.dueDate) {
return '';
}

const now = new Date();
const dueDate = parseDate(item.dueDate);

const humanizedDate = relativeFormat.format(dueDate);
const text = `(Due ${humanizedDate})`;

const isSoon = isBefore(dueDate, addWeeks(now, 1));
const isUrgent = isBefore(dueDate, endOfDay(now));

if (isUrgent) {
return red(underline(text));
}

if (isSoon) {
return yellow(text);
}

return grey(text);
}

_buildTitle(key, items) {
const title = (key === new Date().toDateString()) ? `${underline(key)} ${grey('[Today]')}` : underline(key);
const correlation = this._getCorrelation(items);
Expand All @@ -71,6 +103,28 @@ class Render {
return prefix.join(' ');
}

_buildSuffix(item) {
const suffix = [];

const age = this._getAge(item._timestamp);
const star = this._getStar(item);
const due = this._getDueDate(item);

if (age) {
suffix.push(age);
}

if (star) {
suffix.push(star);
}

if (item._isTask && !item.isComplete && due) {
suffix.push(due);
}

return suffix.join(' ');
}

_buildMessage(item) {
const message = [];

Expand Down Expand Up @@ -99,12 +153,10 @@ class Render {

_displayItemByBoard(item) {
const {_isTask, isComplete} = item;
const age = this._getAge(item._timestamp);
const star = this._getStar(item);

const prefix = this._buildPrefix(item);
const message = this._buildMessage(item);
const suffix = (age.length === 0) ? star : `${age} ${star}`;
const suffix = this._buildSuffix(item);

const msgObj = {prefix, message, suffix};

Expand Down Expand Up @@ -212,6 +264,12 @@ class Render {
error({prefix, message});
}

invalidDueDate() {
const prefix = '\n';
const message = 'Due Date must be a valid date/time descriptor eg. +3d';
error({prefix, message});
}

markComplete(ids) {
const [prefix, suffix] = ['\n', grey(ids.join(', '))];
const message = `Checked ${ids.length > 1 ? 'tasks' : 'task'}:`;
Expand Down
1 change: 1 addition & 0 deletions lib/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Task extends Item {
this.isComplete = options.isComplete || false;
this.isStarred = options.isStarred || false;
this.priority = options.priority || 1;
this.dueDate = options.dueDate || null;
}
}

Expand Down
142 changes: 134 additions & 8 deletions lib/taskbook.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
#!/usr/bin/env node
'use strict';
const addDays = require('date-fns/add_days');
const addMonths = require('date-fns/add_months');
const addYears = require('date-fns/add_years');
const addHours = require('date-fns/add_hours');
const addMinutes = require('date-fns/add_minutes');
const endOfDay = require('date-fns/end_of_day');
const parseDate = require('date-fns/parse');
const isBefore = require('date-fns/is_before');
const isSameDay = require('date-fns/is_same_day');
const Task = require('./task');
const Note = require('./note');
const Storage = require('./storage');
Expand Down Expand Up @@ -62,6 +71,39 @@ class Taskbook {
return ['p:1', 'p:2', 'p:3'].indexOf(x) > -1;
}

_isDueDateOpt(x) {
return x.startsWith('due:');
}

_parseDueDate(date) {
const now = new Date();
const units = {d: addDays, m: addMonths, y: addYears, h: addHours, M: addMinutes};

if (date === 'today') {
return endOfDay(now).toISOString();
}

if (date === 'tomorrow') {
const tomorrow = addDays(now);
return endOfDay(tomorrow).toISOString();
}

const regexRes = date.match(/^\s*(\d+)(d|m|y|h|M)\s*$/);
if (regexRes) {
const offset = parseInt(regexRes[1], 10);
const unitFn = units[regexRes[2]];

return (offset && unitFn) ? unitFn(now, offset) : null;
}

const isoRes = parseDate(date);
if (isoRes) {
return isoRes.toISOString();
}

return null;
}

_getBoards() {
const boards = ['My Board'];

Expand Down Expand Up @@ -93,6 +135,11 @@ class Taskbook {
return opt ? opt[opt.length - 1] : 1;
}

_getDueDate(desc) {
const opt = desc.find(x => this._isDueDateOpt(x));
return opt ? this._parseDueDate(opt.replace(/^due:/, '')) : null;
}

_getOptions(input) {
const [boards, desc] = [[], []];

Expand All @@ -103,20 +150,31 @@ class Taskbook {

const id = this._generateID();
const priority = this._getPriority(input);
const dueDate = this._getDueDate(input);

input.forEach(x => {
if (!this._isPriorityOpt(x)) {
return x.startsWith('@') && x.length > 1 ? boards.push(x) : desc.push(x);
}
});
if (input.find(x => this._isDueDateOpt(x)) && !dueDate) {
render.invalidDueDate();
process.exit(1);
}

input
.filter(x => !this._isPriorityOpt(x))
.filter(x => !this._isDueDateOpt(x))
.forEach(x => x.startsWith('@') && x.length > 1 ? boards.push(x) : desc.push(x));

const description = desc.join(' ');

if (boards.length === 0) {
boards.push('My Board');
}

return {boards, description, id, priority};
return {
boards,
description,
id,
priority,
dueDate
};
}

_getStats() {
Expand Down Expand Up @@ -192,6 +250,62 @@ class Taskbook {
return data;
}

_filterOverdue(data) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could do this date computation natively then why needs an external module.?

Object.entries(data)
.forEach(([id, item]) => {
if (!item.dueDate) {
return delete data[id];
}

const now = new Date();
const dueDate = parseDate(item.dueDate);
const isOverdue = isBefore(dueDate, now);

if (item.isComplete || !isOverdue) {
delete data[id];
}
});

return data;
}

_filterToday(data) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me it's a case of the clarity gained. Under the hood, date-fns is doing very simple date manipulations using the native Date type. I'm not sure why we'd ditch a well tested library in favour of just doing the same implementation within Taskbook?

Object.entries(data).forEach(([id, item]) => {
if (!item.dueDate) {
return delete data[id];
}

const now = new Date();
const dueDate = parseDate(item.dueDate);
const isDueToday = isSameDay(dueDate, now);

if (item.isComplete || !isDueToday) {
delete data[id];
}
});

return data;
}

_filterTomorrow(data) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.entries(data).forEach(([id, item]) => {
if (!item.dueDate) {
return delete data[id];
}

const tomorrow = addDays(new Date(), 1);
const dueDate = parseDate(item.dueDate);

const isDueTomorrow = tomorrow.isSame(dueDate);

if (item.isComplete || !isDueTomorrow) {
delete data[id];
}
});

return data;
}

_filterByAttributes(attr, data = this._data) {
if (Object.keys(data).length === 0) {
return data;
Expand Down Expand Up @@ -227,6 +341,18 @@ class Taskbook {
data = this._filterNote(data);
break;

case 'overdue':
data = this._filterOverdue(data);
break;

case 'today':
data = this._filterToday(data);
break;

case 'tomorrow':
data = this._filterTomorrow(data);
break;

default:
break;
}
Expand Down Expand Up @@ -317,8 +443,8 @@ class Taskbook {
}

createTask(desc) {
const {boards, description, id, priority} = this._getOptions(desc);
const task = new Task({id, description, boards, priority});
const {boards, description, id, priority, dueDate} = this._getOptions(desc);
const task = new Task({id, description, boards, priority, dueDate});
const {_data} = this;
_data[id] = task;
this._save(_data);
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
},
"dependencies": {
"chalk": "^2.4.1",
"date-fns": "^1.29.0",
"intl": "^1.2.5",
"intl-relativeformat": "^2.1.0",
"meow": "^5.0.0",
"signale": "^1.2.1",
"update-notifier": "^2.5.0"
Expand Down