UNPKG

@tosin2013/kanbn

Version:

A CLI Kanban board with AI-powered task management features

1,012 lines (898 loc) 31.3 kB
const fs = require('fs'); const path = require('path'); const utility = require('../utility'); const fileUtils = require('./file-utils'); const taskUtils = require('./task-utils'); const filterUtils = require('./filter-utils'); /** * Get a list of all tracked task ids * @param {object} index The index object * @param {?string} [columnName=null] The optional column name to filter tasks by * @return {Set} A set of task ids appearing in the index */ function getTrackedTaskIds(index, columnName = null) { if (!index || !index.columns) { return new Set(); } return new Set( columnName ? (index.columns[columnName] || []) : Object.keys(index.columns) .map((columnName) => index.columns[columnName]) .flat() ); } /** * Sort a column in the index * @param {object} index The index object * @param {object[]} tasks The tasks in the index * @param {string} columnName The column to sort * @param {object[]} sorters A list of sorter objects * @return {object} The modified index object */ function sortColumnInIndex(index, tasks, columnName, sorters) { tasks = tasks.map((task) => ({ ...task, ...task.metadata, created: "created" in task.metadata ? task.metadata.created : "", updated: "updated" in task.metadata ? task.metadata.updated : "", started: "started" in task.metadata ? task.metadata.started : "", completed: "completed" in task.metadata ? task.metadata.completed : "", due: "due" in task.metadata ? task.metadata.due : "", assigned: "assigned" in task.metadata ? task.metadata.assigned : "", countSubTasks: task.subTasks.length, subTasks: task.subTasks.map((subTask) => `[${subTask.completed ? "x" : ""}] ${subTask.text}`).join("\n"), countTags: "tags" in task.metadata ? task.metadata.tags.length : 0, tags: "tags" in task.metadata ? task.metadata.tags.join("\n") : "", countRelations: task.relations.length, relations: task.relations.map((relation) => `${relation.type} ${relation.task}`).join("\n"), countComments: task.comments.length, comments: task.comments.map((comment) => `${comment.author} ${comment.text}`).join("\n"), workload: taskWorkload(index, task), progress: taskProgress(index, task), })); tasks = sortTasks(tasks, sorters); index.columns[columnName] = tasks.map((task) => task.id); return index; } /** * Sort a list of tasks * @param {object[]} tasks * @param {object[]} sorters * @return {object[]} The sorted tasks */ function sortTasks(tasks, sorters) { tasks.sort((a, b) => { let compareA, compareB; for (let sorter of sorters) { compareA = a[sorter.field]; compareB = b[sorter.field]; if (sorter.filter) { compareA = sortFilter(compareA, sorter.filter); compareB = sortFilter(compareB, sorter.filter); } if (compareA === compareB) { continue; } return sorter.order === "descending" ? compareValues(compareB, compareA) : compareValues(compareA, compareB); } return 0; }); return tasks; } /** * Transform a value using a sort filter regular expression * @param {string} value * @param {string} filter * @return {string} The transformed value */ function sortFilter(value, filter) { const matches = [...value.matchAll(new RegExp(filter, "gi"))]; const result = matches.map((match) => { if (match.groups) { return Object.values(match.groups).join(""); } if (match[1]) { return match[1]; } return match[0]; }); return result.join(""); } /** * Compare two values (supports string, date and number values) * @param {any} a * @param {any} b * @return {number} A positive value if a > b, negative if a < b, otherwise 0 */ function compareValues(a, b) { if (a === undefined && b === undefined) { return 0; } a = utility.coerceUndefined(a, typeof b); b = utility.coerceUndefined(b, typeof a); if (typeof a === "string" && typeof b === "string") { return a.localeCompare(b, undefined, { sensitivity: "accent" }); } return a - b; } /** * Filter a list of tasks using a filters object containing field names and filter values * @param {object} index * @param {object|object[]} tasks - Can be an array of task objects or an object with task IDs as keys * @param {object} filters * @return {object[]} Array of filtered tasks */ function filterTasks(index, tasks, filters) { // Convert tasks to array if it's an object const taskArray = Array.isArray(tasks) ? tasks : Object.entries(tasks).map(([id, task]) => { // Ensure task has its ID property set return { ...task, id: task.id || id }; }); return taskArray.filter((task) => { // Get task ID, either from task.id or from task.name const taskId = task.id || utility.getTaskId(task.name); const column = taskUtils.findTaskColumn(index, taskId); if (Object.keys(filters).length === 0) { return true; } let result = true; if ("id" in filters && !filterUtils.stringFilter(filters.id, taskId)) { result = false; } if ("name" in filters && !filterUtils.stringFilter(filters.name, task.name)) { result = false; } if ("description" in filters && !filterUtils.stringFilter(filters.description, task.description)) { result = false; } if ("column" in filters && !filterUtils.stringFilter(filters.column, column)) { result = false; } if ( "created" in filters && (!("created" in task.metadata) || !filterUtils.dateFilter(filters.created, task.metadata.created)) ) { result = false; } if ( "updated" in filters && (!("updated" in task.metadata) || !filterUtils.dateFilter(filters.updated, task.metadata.updated)) ) { result = false; } if ( "started" in filters && (!("started" in task.metadata) || !filterUtils.dateFilter(filters.started, task.metadata.started)) ) { result = false; } if ( "completed" in filters && (!("completed" in task.metadata) || !filterUtils.dateFilter(filters.completed, task.metadata.completed)) ) { result = false; } if ("due" in filters && (!("due" in task.metadata) || !filterUtils.dateFilter(filters.due, task.metadata.due))) { result = false; } if ("workload" in filters && !filterUtils.numberFilter(filters.workload, taskWorkload(index, task))) { result = false; } if ("progress" in filters && !filterUtils.numberFilter(filters.progress, taskProgress(index, task))) { result = false; } if ( "assigned" in filters && !filterUtils.stringFilter(filters.assigned, "assigned" in task.metadata ? task.metadata.assigned : "") ) { result = false; } if ( "sub-task" in filters && !filterUtils.stringFilter( filters["sub-task"], task.subTasks.map((subTask) => `[${subTask.completed ? "x" : " "}] ${subTask.text}`).join("\n") ) ) { result = false; } if ("count-sub-tasks" in filters && !filterUtils.numberFilter(filters["count-sub-tasks"], task.subTasks.length)) { result = false; } if ("tag" in filters) { const tags = task.metadata && task.metadata.tags ? task.metadata.tags.join("\n") : ""; if (!filterUtils.stringFilter(filters.tag, tags)) { result = false; } } if ("count-tags" in filters) { const tagsLength = task.metadata && task.metadata.tags ? task.metadata.tags.length : 0; if (!filterUtils.numberFilter(filters["count-tags"], tagsLength)) { result = false; } } if ( "relation" in filters && !filterUtils.stringFilter( filters.relation, task.relations.map((relation) => `${relation.type} ${relation.task}`).join("\n") ) ) { result = false; } if ("count-relations" in filters && !filterUtils.numberFilter(filters["count-relations"], task.relations.length)) { result = false; } if ( "comment" in filters && !filterUtils.stringFilter(filters.comment, task.comments.map((comment) => `${comment.author} ${comment.text}`).join("\n")) ) { result = false; } if ("count-comments" in filters && !filterUtils.numberFilter(filters["count-comments"], task.comments.length)) { result = false; } if ("customFields" in index.options) { for (let customField of index.options.customFields) { if (customField.name in filters) { if (!(customField.name in task.metadata)) { result = false; } else { switch (customField.type) { case "boolean": if (task.metadata[customField.name] !== filters[customField.name]) { result = false; } break; case "number": if (!filterUtils.numberFilter(filters[customField.name], task.metadata[customField.name])) { result = false; } break; case "string": if (!filterUtils.stringFilter(filters[customField.name], task.metadata[customField.name])) { result = false; } break; case "date": if (!filterUtils.dateFilter(filters[customField.name], task.metadata[customField.name])) { result = false; } break; default: break; } } } } } return result; }); } /** * Calculate task workload * @param {object} index The index object * @param {object} task The task object * @return {number} The task workload */ function taskWorkload(index, task) { const DEFAULT_TASK_WORKLOAD = 2; const DEFAULT_TASK_WORKLOAD_TAGS = { Nothing: 0, Tiny: 1, Small: 2, Medium: 3, Large: 5, Huge: 8, }; const defaultTaskWorkload = "defaultTaskWorkload" in index.options ? index.options.defaultTaskWorkload : DEFAULT_TASK_WORKLOAD; const taskWorkloadTags = "taskWorkloadTags" in index.options ? index.options.taskWorkloadTags : DEFAULT_TASK_WORKLOAD_TAGS; let workload = 0; let hasWorkloadTags = false; if ("tags" in task.metadata) { for (let workloadTag of Object.keys(taskWorkloadTags)) { if (task.metadata.tags.indexOf(workloadTag) !== -1) { workload += taskWorkloadTags[workloadTag]; hasWorkloadTags = true; } } } if (!hasWorkloadTags) { workload = defaultTaskWorkload; } return workload; } /** * Get task progress amount * @param {object} index * @param {object} task * @return {number} Task progress */ function taskProgress(index, task) { if (taskUtils.taskCompleted(index, task)) { return 1; } return "progress" in task.metadata ? task.metadata.progress : 0; } /** * Calculate task workload statistics between a start and end date * @param {object[]} tasks * @param {string} metadataProperty * @param {Date} start * @param {Date} end * @return {object} A statistics object */ function taskWorkloadInPeriod(tasks, metadataProperty, start, end) { const filteredTasks = tasks.filter( (task) => metadataProperty in task.metadata && task.metadata[metadataProperty] >= start && task.metadata[metadataProperty] <= end ); return { tasks: filteredTasks.map((task) => ({ id: task.id, column: task.column, workload: task.workload, })), workload: filteredTasks.reduce((a, task) => a + task.workload, 0), }; } /** * Get a list of tasks that were started before and/or completed after a date * @param {object[]} tasks * @param {Date} date * @return {object[]} A filtered list of tasks */ function getActiveTasksAtDate(tasks, date) { return tasks.filter((task) => ( (task.started !== false && task.started <= date) && (task.completed === false || task.completed > date) )); } /** * Calculate the total workload at a specific date * @param {object[]} tasks * @param {Date} date * @return {number} The total workload at the specified date */ function getWorkloadAtDate(tasks, date) { return getActiveTasksAtDate(tasks, date).reduce((a, task) => (a += task.workload), 0); } /** * Get the number of tasks that were active at a specific date * @param {object[]} tasks * @param {Date} date * @return {number} The total number of active tasks at the specified date */ function countActiveTasksAtDate(tasks, date) { return getActiveTasksAtDate(tasks, date).length; } /** * Get a list of tasks that were started or completed on a specific date * @param {object[]} tasks * @param {Date} date * @return {object[]} A list of event objects, with event type and task id */ function getTaskEventsAtDate(tasks, date) { return [ ...tasks .filter((task) => (task.created ? task.created.getTime() : 0) === date.getTime()) .map((task) => ({ eventType: "created", task })), ...tasks .filter((task) => (task.started ? task.started.getTime() : 0) === date.getTime()) .map((task) => ({ eventType: "started", task })), ...tasks .filter((task) => (task.completed ? task.completed.getTime() : 0) === date.getTime()) .map((task) => ({ eventType: "completed", task })), ]; } /** * Quantize a burndown chart date to 1-hour resolution * @param {Date} date * @param {string} resolution One of 'days', 'hours', 'minutes', 'seconds' * @return {Date} The quantized dates */ function normaliseDate(date, resolution = 'minutes') { const result = new Date(date.getTime()); switch (resolution) { case 'days': result.setHours(0); case 'hours': result.setMinutes(0); case 'minutes': result.setSeconds(0); case 'seconds': result.setMilliseconds(0); default: break; } return result; } /** * If a task's column is linked in the index to a custom field with type date, update the custom field's value * in the task data with the current date * @param {object} index * @param {object} taskData * @param {string} columnName * @return {object} The updated task data */ function updateColumnLinkedCustomFields(index, taskData, columnName) { taskData = updateColumnLinkedCustomField(index, taskData, columnName, "completed", "once"); taskData = updateColumnLinkedCustomField(index, taskData, columnName, "started", "once"); if ("customFields" in index.options) { for (let customField of index.options.customFields) { if (customField.type === "date") { taskData = updateColumnLinkedCustomField( index, taskData, columnName, customField.name, customField.updateDate || "none" ); } } } return taskData; } /** * If index options contains a list of columns linked to a custom field name and a task's column matches one * of the columns in this list, set the task's custom field value to the current date depending on criteria: * - if 'once', update the value only if it's not currently set * - if 'always', update the value regardless * - otherwise, don't update the value * @param {object} index * @param {object} taskData * @param {string} columnName * @param {string} fieldName * @param {string} [updateCriteria='none'] */ function updateColumnLinkedCustomField(index, taskData, columnName, fieldName, updateCriteria = "none") { const columnList = `${fieldName}Columns`; if (columnList in index.options && index.options[columnList].indexOf(columnName) !== -1) { switch (updateCriteria) { case "always": taskData = taskUtils.setTaskMetadata(taskData, fieldName, new Date()); break; case "once": if (!(fieldName in taskData.metadata && taskData.metadata[fieldName])) { taskData = taskUtils.setTaskMetadata(taskData, fieldName, new Date()); } break; default: break; } } return taskData; } /** * Save index data to the index file * @param {object} indexData Index data to save * @param {Function} loadAllTrackedTasks Function to load all tracked tasks * @param {Function} configExists Function to check if config exists * @param {Function} saveConfig Function to save config * @param {Function} getIndexPath Function to get index path * @param {boolean} ignoreOptions Whether to ignore options when saving * @return {Promise<void>} */ async function saveIndex(indexData, loadAllTrackedTasks, configExists, saveConfig, getIndexPath, ignoreOptions = false) { const parseIndex = require('../parse-index'); const fs = require('fs'); if ("columnSorting" in indexData.options && Object.keys(indexData.options.columnSorting).length) { for (let columnName in indexData.options.columnSorting) { indexData = sortColumnInIndex( indexData, await loadAllTrackedTasks(indexData, columnName), columnName, indexData.options.columnSorting[columnName] ); } } if (!ignoreOptions && await configExists()) { await saveConfig(indexData.options); ignoreOptions = true; } await fs.promises.writeFile(await getIndexPath(), parseIndex.json2md(indexData, ignoreOptions)); } /** * Load the index file and parse it to an object * @param {Function} getIndexPath Function to get index path * @param {Function} getConfig Function to get config * @return {Promise<object>} The index object */ async function loadIndex(getIndexPath, getConfig) { const parseIndex = require('../parse-index'); const fs = require('fs'); let indexData = ""; try { indexData = await fs.promises.readFile(await getIndexPath(), { encoding: "utf-8" }); } catch (error) { throw new Error(`Couldn't access index file: ${error.message}`); } try { const index = parseIndex.md2json(indexData); const config = await getConfig(); if (config !== null) { index.options = { ...index.options, ...config }; } return index; } catch (error) { throw new Error(`Unable to parse index: ${error.message}`); } } /** * Add an untracked task to the index * @param {object} index The index object * @param {string} taskId The task ID to add * @param {string} columnName The column to add the task to * @param {Function} initialised Function to check if kanbn is initialised * @param {Function} getTaskFolderPath Function to get task folder path * @param {Function} loadTask Function to load a task * @param {Function} saveTask Function to save a task * @param {Function} saveIndex Function to save the index * @return {Promise<string>} The task ID */ async function addUntrackedTaskToIndex( index, taskId, columnName, initialised, getTaskFolderPath, loadTask, saveTask, saveIndex ) { const fs = require('fs'); if (!(await initialised())) { throw new Error("Not initialised in this folder"); } taskId = fileUtils.removeFileExtension(taskId); if (!(await fileUtils.exists(fileUtils.getTaskPath(await getTaskFolderPath(), taskId)))) { throw new Error(`No task file found with id "${taskId}"`); } if (!(columnName in index.columns)) { throw new Error(`Column "${columnName}" doesn't exist`); } if (taskUtils.taskInIndex(index, taskId)) { throw new Error(`Task "${taskId}" is already in the index`); } // Load task data let taskData = await loadTask(taskId); const taskPath = fileUtils.getTaskPath(await getTaskFolderPath(), taskId); taskData = updateColumnLinkedCustomFields(index, taskData, columnName); await saveTask(taskPath, taskData); // Add the task to the column and save the index index = taskUtils.addTaskToIndex(index, taskId, columnName); await saveIndex(index); return taskId; } /** * Find all tracked tasks in the index * @param {object} index The index object * @param {Function} initialised Function to check if kanbn is initialised * @param {string} columnName Optional column name to filter by * @return {Promise<Set>} A set of tracked task IDs */ async function findTrackedTasks(index, initialised, columnName = null) { // Check if this folder has been initialised if (!(await initialised())) { throw new Error("Not initialised in this folder"); } return getTrackedTaskIds(index, columnName); } /** * Find all untracked tasks (markdown files in tasks folder not in the index) * @param {object} index The index object * @param {Function} initialised Function to check if kanbn is initialised * @param {Function} getTaskFolderPath Function to get task folder path * @return {Promise<Set>} A set of untracked task IDs */ async function findUntrackedTasks(index, initialised, getTaskFolderPath) { const fs = require('fs'); const path = require('path'); // Check if this folder has been initialised if (!(await initialised())) { throw new Error("Not initialised in this folder"); } const trackedTasks = getTrackedTaskIds(index); try { // Use fs.readdir instead of glob to avoid dependency issues const taskFolderPath = await getTaskFolderPath(); const files = await fs.promises.readdir(taskFolderPath, { withFileTypes: true }); // Filter for markdown files only const mdFiles = files .filter(file => file.isFile() && file.name.endsWith('.md')) .map(file => path.join(taskFolderPath, file.name)); const untrackedTasks = new Set(mdFiles.map((task) => path.parse(task).name)); return new Set([...untrackedTasks].filter((x) => !trackedTasks.has(x))); } catch (error) { console.error(`Error finding untracked tasks: ${error.message}`); return new Set(); // Return empty set on error } } /** * Update a task with new data * @param {object} index The index object * @param {string} taskId The task ID to update * @param {object} taskData The new task data * @param {string|null} columnName Optional column to move the task to * @param {Function} initialised Function to check if kanbn is initialised * @param {Function} getTaskFolderPath Function to get task folder path * @param {Function} loadTask Function to load a task * @param {Function} saveTask Function to save a task * @param {Function} renameTask Function to rename a task * @param {Function} moveTask Function to move a task * @param {Function} saveIndex Function to save the index * @return {Promise<string>} The task ID (may be updated if renamed) */ async function updateTask( index, taskId, taskData, columnName, initialised, getTaskFolderPath, loadTask, saveTask, renameTask, moveTask, saveIndex ) { const fileUtils = require('./file-utils'); const taskUtils = require('./task-utils'); // Check if this folder has been initialised if (!(await initialised())) { throw new Error("Not initialised in this folder"); } taskId = fileUtils.removeFileExtension(taskId); if (!(await fileUtils.exists(fileUtils.getTaskPath(await getTaskFolderPath(), taskId)))) { throw new Error(`No task file found with id "${taskId}"`); } if (!taskUtils.taskInIndex(index, taskId)) { throw new Error(`Task "${taskId}" is not in the index`); } if (!taskData.name) { throw new Error("Task name cannot be blank"); } const originalTaskData = await loadTask(taskId); if (originalTaskData.name !== taskData.name) { taskId = await renameTask(taskId, taskData.name); index = await loadTask(taskId); } if (columnName && !(columnName in index.columns)) { throw new Error(`Column "${columnName}" doesn't exist`); } taskData = taskUtils.setTaskMetadata(taskData, "updated", new Date()); await saveTask(fileUtils.getTaskPath(await getTaskFolderPath(), taskId), taskData); if (columnName) { await moveTask(taskId, columnName); } else { // Otherwise save the index await saveIndex(index); } return taskId; } /** * Rename a task * @param {object} index The index object * @param {string} taskId The task ID to rename * @param {string} newTaskName The new task name * @param {Function} initialised Function to check if kanbn is initialised * @param {Function} getTaskFolderPath Function to get task folder path * @param {Function} loadTask Function to load a task * @param {Function} saveTask Function to save a task * @param {Function} saveIndex Function to save the index * @return {Promise<string>} The new task ID */ async function renameTask( index, taskId, newTaskName, initialised, getTaskFolderPath, loadTask, saveTask, saveIndex ) { const fs = require('fs'); const utility = require('../utility'); const fileUtils = require('./file-utils'); const taskUtils = require('./task-utils'); // Check if this folder has been initialised if (!(await initialised())) { throw new Error("Not initialised in this folder"); } taskId = fileUtils.removeFileExtension(taskId); if (!(await fileUtils.exists(fileUtils.getTaskPath(await getTaskFolderPath(), taskId)))) { throw new Error(`No task file found with id "${taskId}"`); } if (!taskUtils.taskInIndex(index, taskId)) { throw new Error(`Task "${taskId}" is not in the index`); } const newTaskId = utility.getTaskId(newTaskName); const newTaskPath = fileUtils.getTaskPath(await getTaskFolderPath(), newTaskId); if (await fileUtils.exists(newTaskPath)) { throw new Error(`A task with id "${newTaskId}" already exists`); } if (taskUtils.taskInIndex(index, newTaskId)) { throw new Error(`A task with id "${newTaskId}" is already in the index`); } let taskData = await loadTask(taskId); taskData.name = newTaskName; taskData = taskUtils.setTaskMetadata(taskData, "updated", new Date()); await saveTask(fileUtils.getTaskPath(await getTaskFolderPath(), taskId), taskData); await fs.promises.rename( fileUtils.getTaskPath(await getTaskFolderPath(), taskId), newTaskPath ); // Update the task id in the index index = taskUtils.renameTaskInIndex(index, taskId, newTaskId); await saveIndex(index); return newTaskId; } /** * Move a task from one column to another * @param {object} index The index object * @param {string} taskId The task ID to move * @param {string} columnName The column to move the task to * @param {number|null} position The position to move the task to (null for end of column) * @param {boolean} relative Whether the position is relative to the current position * @param {Function} initialised Function to check if kanbn is initialised * @param {Function} getTaskFolderPath Function to get task folder path * @param {Function} loadTask Function to load a task * @param {Function} saveTask Function to save a task * @param {Function} saveIndex Function to save the index * @return {Promise<string>} The task ID */ async function moveTask( index, taskId, columnName, position = null, relative = false, initialised, getTaskFolderPath, loadTask, saveTask, saveIndex ) { const fileUtils = require('./file-utils'); const taskUtils = require('./task-utils'); // Check if this folder has been initialised if (!(await initialised())) { throw new Error("Not initialised in this folder"); } taskId = fileUtils.removeFileExtension(taskId); if (!(await fileUtils.exists(fileUtils.getTaskPath(await getTaskFolderPath(), taskId)))) { throw new Error(`No task file found with id "${taskId}"`); } if (!taskUtils.taskInIndex(index, taskId)) { throw new Error(`Task "${taskId}" is not in the index`); } if (!(columnName in index.columns)) { throw new Error(`Column "${columnName}" doesn't exist`); } // Update the task's updated date let taskData = await loadTask(taskId); taskData = taskUtils.setTaskMetadata(taskData, "updated", new Date()); // Update task metadata dates taskData = updateColumnLinkedCustomFields(index, taskData, columnName); await saveTask(fileUtils.getTaskPath(await getTaskFolderPath(), taskId), taskData); const currentColumnName = taskUtils.findTaskColumn(index, taskId); const currentPosition = index.columns[currentColumnName].indexOf(taskId); if (position !== null) { if (relative) { const startPosition = (columnName === currentColumnName) ? currentPosition : 0; position = startPosition + position; } position = Math.max(Math.min(position, index.columns[columnName].length), 0); } index = taskUtils.removeTaskFromIndex(index, taskId); index = taskUtils.addTaskToIndex(index, taskId, columnName, position); await saveIndex(index); return taskId; } /** * Delete a task from the index * @param {object} index The index object * @param {string} taskId The task ID to delete * @param {boolean} removeFile Whether to remove the task file * @param {Function} initialised Function to check if kanbn is initialised * @param {Function} getTaskFolderPath Function to get task folder path * @param {Function} saveIndex Function to save the index * @return {Promise<string>} The task ID */ async function deleteTask( index, taskId, removeFile = false, initialised, getTaskFolderPath, saveIndex ) { const fs = require('fs'); const fileUtils = require('./file-utils'); const taskUtils = require('./task-utils'); // Check if this folder has been initialised if (!(await initialised())) { throw new Error("Not initialised in this folder"); } taskId = fileUtils.removeFileExtension(taskId); if (!taskUtils.taskInIndex(index, taskId)) { throw new Error(`Task "${taskId}" is not in the index`); } index = taskUtils.removeTaskFromIndex(index, taskId); if (removeFile && (await fileUtils.exists(fileUtils.getTaskPath(await getTaskFolderPath(), taskId)))) { await fs.promises.unlink(fileUtils.getTaskPath(await getTaskFolderPath(), taskId)); } await saveIndex(index); return taskId; } /** * Search for tasks matching the given filters * @param {object} index The index object * @param {object} filters Filters to apply * @param {boolean} quiet Whether to return only task IDs * @param {Function} initialised Function to check if kanbn is initialised * @param {Function} loadAllTrackedTasks Function to load all tracked tasks * @param {Function} hydrateTask Function to hydrate a task * @return {Promise<Array>} Array of tasks or task IDs */ async function search( index, filters = {}, quiet = false, initialised, loadAllTrackedTasks, hydrateTask ) { const utility = require('../utility'); // Check if this folder has been initialised if (!(await initialised())) { throw new Error("Not initialised in this folder"); } let tasks = filterTasks(index, await loadAllTrackedTasks(index), filters); return tasks.map((task) => { return quiet ? utility.getTaskId(task.name) : hydrateTask(index, task); }); } module.exports = { getTrackedTaskIds, sortColumnInIndex, sortTasks, sortFilter, compareValues, filterTasks, taskWorkload, taskProgress, taskWorkloadInPeriod, getActiveTasksAtDate, getWorkloadAtDate, countActiveTasksAtDate, getTaskEventsAtDate, normaliseDate, updateColumnLinkedCustomFields, updateColumnLinkedCustomField, saveIndex, loadIndex, addUntrackedTaskToIndex, findTrackedTasks, findUntrackedTasks, updateTask, renameTask, moveTask, deleteTask, search };