@tosin2013/kanbn
Version:
A CLI Kanban board with AI-powered task management features
199 lines (181 loc) • 6.5 kB
JavaScript
const Kanbn = require('./main');
const { findTaskColumn } = require('./main');
const term = require('terminal-kit').terminal;
const formatDate = require('dateformat');
const utility = require('./utility');
module.exports = (() => {
const TASK_SEPARATOR = '\n\n';
/**
* Show only the selected fields for a task, as specified in the index options
* @param {object} index
* @param {object} task
* @return {string} The selected task fields
*/
function getTaskString(index, task) {
const kanbn = Kanbn();
const taskTemplate = kanbn.getTaskTemplate(index);
const dateFormat = kanbn.getDateFormat(index);
// Make sure the task has the correct column
if (typeof task.id === 'string') {
task.column = findTaskColumn(index, task.id);
} else {
console.error(`Invalid task id: expected string but got ${typeof task.id}`);
task.column = null;
}
// Get an object containing custom fields from the task
let customFields = {};
if ('customFields' in index.options) {
const customFieldNames = index.options.customFields.map(customField => customField.name);
customFields = Object.fromEntries(utility.zip(
customFieldNames,
customFieldNames.map(customFieldName => task.metadata[customFieldName] || '')
));
}
// Prepare task data for interpolation
const taskData = {
name: task.name,
description: task.name,
created: 'created' in task.metadata ? formatDate(task.metadata.created, dateFormat) : '',
updated: 'updated' in task.metadata ? formatDate(task.metadata.updated, dateFormat) : '',
started: 'started' in task.metadata ? formatDate(task.metadata.started, dateFormat) : '',
completed: 'completed' in task.metadata ? formatDate(task.metadata.completed, dateFormat) : '',
due: 'due' in task.metadata ? formatDate(task.metadata.due, dateFormat) : '',
tags: 'tags' in task.metadata ? task.metadata.tags : [],
subTasks: task.subTasks,
relations: task.relations,
overdue: 'dueData' in task && 'overdue' in task.dueData ? task.dueData.overdue : null,
dueDelta: 'dueData' in task && 'dueDelta' in task.dueData ? task.dueData.dueDelta : 0,
dueMessage: 'dueData' in task && 'dueMessage' in task.dueData ? task.dueData.dueMessage : '',
column: task.column,
workload: task.workload,
progress: task.progress,
...customFields
};
try {
return new Function(...Object.keys(taskData), 'return `^:' + taskTemplate + '`;')(...Object.values(taskData));
} catch (e) {
utility.error(`Unable to build task template: ${e.message}`);
return;
}
}
/**
* Get a column heading with icons
* @param {object} index
* @param {string} columnName
* @return {string} The column heading
*/
function getColumnHeading(index, columnName) {
let heading = '^:';
if (
'completedColumns' in index.options &&
index.options.completedColumns.indexOf(columnName) !== -1
) {
heading += '^g\u2713^: ';
}
if (
'startedColumns' in index.options &&
index.options.startedColumns.indexOf(columnName) !== -1
) {
heading += '^c\u00bb^: ';
}
return heading + `^+${columnName}^:`;
}
return {
/**
* Show the kanbn board
* @param {object} index The index object
* @param {object[]} tasks An array of task objects
* @param {?string} [view=null] The view to show, or null to show the default view
* @param {boolean} [json=false] Show JSON output
*/
async show(index, tasks, view = null, json = false) {
const board = {};
let viewSettings = {};
if (view !== null) {
// Make sure the view exists
if (
!('views' in index.options) ||
(viewSettings = index.options.views.find(v => v.name === view)) === undefined
) {
throw new Error(`No view found with name "${view}"`);
}
}
// Check if there is a list of columns in the view settings
if (!('columns' in viewSettings)) {
viewSettings.columns = Object.keys(index.columns)
.filter(columnName => (
!('hiddenColumns' in index.options) ||
index.options.hiddenColumns.indexOf(columnName) === -1
))
.map(columnName => ({
name: columnName,
filters: {
column: columnName
}
}));
}
// Check if there is a list of lanes in the view settings
if (!('lanes' in viewSettings) || viewSettings.lanes.length === 0) {
viewSettings.lanes = [{
name: 'All tasks'
}];
}
// If a root-level filter is defined, apply it to the tasks
if ('filters' in viewSettings) {
const kanbn = Kanbn();
tasks = kanbn.filterAndSortTasks(index, tasks, viewSettings.filters, []);
}
// Add columns
board.headings = viewSettings.columns.map(column => ({
name: column.name,
heading: getColumnHeading(index, column.name)
}));
// Add lanes and column contents
board.lanes = [];
for (let lane of viewSettings.lanes) {
const columns = [];
for (let column of viewSettings.columns) {
const kanbn = Kanbn();
const cellTasks = kanbn.filterAndSortTasks(
index,
tasks,
{
...('filters' in column ? column.filters : {}),
...('filters' in lane ? lane.filters : {})
},
'sorters' in column ? column.sorters : []
);
columns.push(cellTasks);
}
board.lanes.push({
name: lane.name,
columns
});
}
if (json) {
console.log(JSON.stringify(board, null, 2));
return;
}
// Prepare table
const table = [];
table.push(board.headings.map(heading => heading.heading));
board.lanes.forEach(lane => table.push(
[lane.name],
lane.columns.map(column => column.map(task => getTaskString(index, task)).join(TASK_SEPARATOR))
));
// Display as a table
term.table(
table,
{
hasBorder: true,
contentHasMarkup: true,
borderChars: 'lightRounded',
borderAttr: { color: 'grey' },
textAttr: { color: 'white', bgColor: 'default' },
width: term.width,
fit: true
}
);
}
}
})();