UNPKG

@tosin2013/kanbn

Version:

A CLI Kanban board with AI-powered task management features

526 lines (495 loc) 13.8 kB
const Kanbn = require('../main'); const utility = require('../utility'); const inquirer = require('inquirer'); const fuzzy = require('fuzzy'); const chrono = require('chrono-node'); const getGitUsername = require('git-user-name'); inquirer.registerPrompt('datepicker', require('inquirer-datepicker')); inquirer.registerPrompt('recursive', require('inquirer-recursive')); inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); /** * Create a task interactively * @param {object} taskData * @param {string[]} taskIds * @param {string} columnName * @param {string[]} columnNames * @return {Promise<any>} */ async function interactiveCreateTask(taskData, taskIds, columnName, columnNames) { const dueDateExists = ( 'metadata' in taskData && 'due' in taskData.metadata && taskData.metadata.due != null ); const assignedExists = ( 'metadata' in taskData && 'assigned' in taskData.metadata && taskData.metadata.assigned != null ); return await inquirer.prompt([ { type: 'input', name: 'name', message: 'Task name:', default: taskData.name || '', validate: async value => { if (!value) { return 'Task name cannot be empty'; } return true; } }, { type: 'confirm', name: 'setDescription', message: 'Add a description?', default: false }, { type: 'editor', name: 'description', message: 'Task description:', default: taskData.description, when: answers => answers.setDescription }, { type: 'list', name: 'column', message: 'Column:', default: columnName, choices: columnNames }, { type: 'confirm', name: 'setDue', message: 'Set a due date?', default: false, when: answers => !dueDateExists }, { type: 'datepicker', name: 'due', message: 'Due date:', default: dueDateExists ? taskData.metadata.due : new Date(), format: ['Y', '/', 'MM', '/', 'DD'], when: answers => answers.setDue, }, { type: 'confirm', name: 'setAssigned', message: 'Assign this task?', default: false, when: answers => !assignedExists }, { type: 'input', name: 'assigned', message: 'Assigned to:', default: assignedExists ? taskData.metadata.assigned : getGitUsername(), when: answers => answers.setAssigned || assignedExists }, { type: 'recursive', name: 'subTasks', initialMessage: 'Add a sub-task?', message: 'Add another sub-task?', default: false, prompts: [ { type: 'input', name: 'text', message: 'Sub-task text:', validate: value => { if (!value) { return 'Sub-task text cannot be empty'; } return true; } }, { type: 'confirm', name: 'completed', message: 'Sub-task completed?', default: false } ] }, { type: 'recursive', name: 'tags', initialMessage: 'Add a tag?', message: 'Add another tag?', default: false, prompts: [ { type: 'input', name: 'name', message: 'Tag name:', validate: value => { if (!value) { return 'Tag name cannot be empty'; } return true; } } ] }, { type: 'recursive', name: 'references', initialMessage: 'Add a reference?', message: 'Add another reference?', default: false, prompts: [ { type: 'input', name: 'url', message: 'Reference URL:', validate: value => { if (!value) { return 'Reference URL cannot be empty'; } return true; } } ] }, { type: 'recursive', name: 'relations', initialMessage: 'Add a relation?', message: 'Add another relation?', default: false, when: answers => taskIds.length > 0, prompts: [ { type: 'autocomplete', name: 'task', message: 'Related task id:', source: (answers, input) => { input = input || ''; const result = fuzzy.filter(input, taskIds); return new Promise(resolve => { resolve(result.map(result => result.string)); }); } }, { type: 'input', name: 'type', message: 'Relation type:' } ] } ]); } /** * Add untracked tasks interactively * @param {string[]} untrackedTasks * @param {string} columnName * @param {string[]} columnNames */ async function interactiveAddUntrackedTasks(untrackedTasks, columnName, columnNames) { return await inquirer.prompt([ { type: 'checkbox', name: 'untrackedTasks', message: 'Choose which tasks to add:', choices: untrackedTasks }, { type: 'list', name: 'column', message: 'Column:', default: columnName, choices: columnNames } ]); } /** * Create a task * @param {object} taskData * @param {string} columnName */ function createTask(taskData, columnName) { // Create a Kanbn instance const kanbn = Kanbn(); kanbn .createTask(taskData, columnName) .then(taskId => { console.log(`Created task "${taskId}" in column "${columnName}"`); }) .catch(error => { utility.error(error); }); } /** * Add untracked tasks to the index * @param {string[]} untrackedTasks * @param {string} columnName */ async function addUntrackedTasks(untrackedTasks, columnName) { // Create a Kanbn instance const kanbn = Kanbn(); for (let untrackedTask of untrackedTasks) { try { await kanbn.addUntrackedTaskToIndex(untrackedTask, columnName); } catch (error) { utility.error(error); return; } } console.log( `Added ${untrackedTasks.length} task${untrackedTasks.length !== 1 ? 's' : ''} to column "${columnName}"` ); } module.exports = async args => { // Create a Kanbn instance const kanbn = Kanbn(); // Make sure kanbn has been initialised try { if (!await kanbn.initialised()) { utility.warning('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}'); return; } } catch (error) { utility.warning('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}'); return; } // Get the index and make sure it has some columns let index; try { index = await kanbn.getIndex(); } catch (error) { utility.error(error); return; } const columnNames = Object.keys(index.columns); if (!columnNames.length) { utility.error('No columns defined in the index\nTry running {b}kanbn init -c "column name"{b}'); return; } // Get column name if specified, otherwise default to the first available column let columnName = columnNames[0]; if (args.column) { columnName = utility.strArg(args.column); if (columnNames.indexOf(columnName) === -1) { utility.error(`Column "${columnName}" doesn't exist`); return; } } // Add untracked file(s) if (args.untracked) { const untrackedTasks = []; if (Array.isArray(args.untracked)) { untrackedTasks.push(...args.untracked); } else if (typeof args.untracked === 'string') { untrackedTasks.push(args.untracked); } else if (args.untracked === true) { try { untrackedTasks.push(...await kanbn.findUntrackedTasks()); } catch (error) { utility.error(error); return; } } // Make sure there are some untracked tasks to add if (untrackedTasks.length === 0) { utility.error('No untracked tasks to add'); return; } // Add untracked files interactively if (args.interactive) { interactiveAddUntrackedTasks(untrackedTasks, columnName, columnNames) .then(async answers => { await addUntrackedTasks(answers.untrackedTasks, answers.column); }) .catch(error => { utility.error(error); }); return; } else { await addUntrackedTasks(untrackedTasks, columnName); return; } } // Otherwise, create a task from arguments or interactively const taskData = { metadata: {} }; // Get a list of existing task ids const taskIds = [...await kanbn.findTrackedTasks()]; // Get task settings from arguments // Name if (args.name) { taskData.name = utility.strArg(args.name); } // Description if (args.description) { taskData.description = utility.strArg(args.description); } // Due date if (args.due) { taskData.metadata.due = chrono.parseDate(utility.strArg(args.due)); if (taskData.metadata.due === null) { utility.error('Unable to parse due date'); return; } } // Progress if (args.progress) { const progressValue = parseFloat(utility.strArg(args.progress)); if (isNaN(progressValue)) { utility.error('Progress value is not a number'); return; } taskData.metadata.progress = progressValue; } // Assigned if (args.assigned) { const gitUsername = getGitUsername(); if (args.assigned === true) { if (gitUsername) { taskData.metadata.assigned = gitUsername; } } else { taskData.metadata.assigned = utility.strArg(args.assigned); } } // Sub-tasks if (args['sub-task']) { const subTasks = utility.arrayArg(args['sub-task']); taskData.subTasks = subTasks.map(subTask => { const match = subTask.match(/^\[([x ])\] (.*)/); if (match !== null) { return { completed: match[1] === 'x', text: match[2] }; } return { completed: false, text: subTask }; }); } // Tags if (args.tag) { taskData.metadata.tags = utility.arrayArg(args.tag); } // References if (args.refs) { taskData.metadata.references = utility.arrayArg(args.refs); } // Relations if (args.relation) { const relations = utility.arrayArg(args.relation).map(relation => { const parts = relation.split(':'); return parts.length === 1 ? { type: '', task: parts[0].trim() } : { type: parts[0].trim(), task: parts[1].trim() }; }); // Make sure each relation is an existing task for (let relation of relations) { if (taskIds.indexOf(relation.task) === -1) { utility.error(`Related task ${relation.task} doesn't exist`); return; } } taskData.relations = relations; } // Check metadata field types if ('customFields' in index.options) { for (let arg of Object.keys(args)) { const customField = index.options.customFields.find(p => p.name === arg); if (customField !== undefined) { // Check value type switch (customField.type) { case 'boolean': if (typeof args[arg] === 'boolean') { taskData.metadata[arg] = args[arg]; } else { utility.error(`Custom field "${arg}" value is not a boolean`); return; } break; case 'number': const numberValue = parseFloat(args[arg]); if (!isNaN(numberValue)) { taskData.metadata[arg] = numberValue; } else { utility.error(`Custom field "${arg}" value is not a number`); return; } break; case 'string': if (typeof args[arg] === 'string') { taskData.metadata[arg] = args[arg]; } else { utility.error(`Custom field "${arg}" value is not a string`); return; } break; case 'date': const dateValue = chrono.parseDate(args[arg]); if (dateValue instanceof Date) { taskData.metadata[arg] = dateValue; } else { utility.error(`Unable to parse date for custom field "${arg}"`); return; } break; default: break; } } } } // Create task interactively if (args.interactive) { interactiveCreateTask(taskData, taskIds, columnName, columnNames) .then(answers => { taskData.name = answers.name; if ('description' in answers) { taskData.description = answers.description; } if ('due' in answers) { taskData.metadata.due = answers.due.toISOString(); } if ('assigned' in answers) { taskData.metadata.assigned = answers.assigned; } if ('subTasks' in answers) { taskData.subTasks = answers.subTasks.map(subTask => ({ text: subTask.text, completed: subTask.completed })); } if ('tags' in answers && answers.tags.length > 0) { taskData.metadata.tags = answers.tags.map(tag => tag.name); } if ('references' in answers && answers.references.length > 0) { taskData.metadata.references = answers.references.map(ref => ref.url); } if ('relations' in answers) { taskData.relations = answers.relations.map(relation => ({ task: relation.task, type: relation.type })); } columnName = answers.column; createTask(taskData, columnName); }) .catch(error => { utility.error(error); }); // Otherwise create task non-interactively } else { createTask(taskData, columnName); } };