Skip to content

Instantly share code, notes, and snippets.

@panahi
Last active November 7, 2022 03:08
Show Gist options
  • Save panahi/9c5565db24d9573d35a60b859c6833e5 to your computer and use it in GitHub Desktop.
Save panahi/9c5565db24d9573d35a60b859c6833e5 to your computer and use it in GitHub Desktop.
Scripts loaded via the CustomJS plugin in Obsidian to interact with tasks and build dataviewjs visualizations of tasks
class FileUtilities {
async getFileContents(app, path) {
let fileContents = await app.vault.adapter.read(path);
return fileContents;
}
async writeFile(app, path, newContents) {
return await app.vault.adapter.write(path, newContents);
}
async appendToFile(app, path, textToAppend) {
let fileContents = await this.getFileContents(app, path);
if (!fileContents) {
return new Notice("Failed to load file", 2500);
}
let splitContents = fileContents.split("\n");
let lastLine = splitContents[splitContents.length - 1];
if (lastLine.trim() === '') {
splitContents[splitContents.length - 1] = textToAppend;
fileContents = splitContents.join("\n");
} else {
fileContents = fileContents + "\n" + textToAppend;
}
await this.writeFile(app, path, fileContents);
}
async formatFile(app, path) {
console.log("FileUtilities.formatFile invoked. Path: " + path);
await this.setId(app, path);
let fileContents = await this.getFileContents(app, path);
console.log(`Contents of ${path}\n`, fileContents);
let newFileContents = await this.fixTasksInFile(app, path, fileContents);
console.log(`Updated contents of ${path}\n`, newFileContents);
if (fileContents.normalize() === newFileContents.normalize()) {
console.log("No changes to file");
return;
} else {
console.log("Writing file updates");
await this.writeFile(app, path, newFileContents);
}
}
async findTaskInFile(app, path, taskText) {
const formatter = customJS.TaskFormatter;
const fileContents = await this.getFileContents(app, path);
const splitContents = fileContents.split("\n");
for (let lineNum = 0; lineNum < splitContents.length; lineNum++) {
const line = splitContents[lineNum];
const isTaskLine = line.indexOf(taskText) >= 0;
if (!isTaskLine) {
continue;
}
const taskObj = formatter.convertLineToTask(line);
if (!taskObj.createdDate) {
let createdDate = await this.getTaskCreatedDate(app, path);
taskObj.createdDate = createdDate;
}
return taskObj;
}
return null;
}
async findLineNumberOfTask(splitContents, taskText) {
for (let lineNum = 0; lineNum < splitContents.length; lineNum++) {
const line = splitContents[lineNum];
const isTaskLine = line.indexOf(taskText) >= 0;
if (!isTaskLine) {
continue;
} else {
return lineNum;
}
}
return -1;
}
async replaceTaskInFile(app, path, originalText, taskObj) {
const formatter = customJS.TaskFormatter;
const fileContents = await this.getFileContents(app, path);
const splitContents = fileContents.split("\n");
let taskLineNum = await this.findLineNumberOfTask(splitContents, originalText);
if (taskLineNum >= 0) {
const oldLine = splitContents[taskLineNum];
const newLine = formatter.convertTaskToLine(taskObj);
console.log(`Task Updated:\nOld: ${oldLine}\nNew: ${newLine}`)
splitContents[taskLineNum] = newLine;
let newFileContents = splitContents.join("\n");
await this.writeFile(app, path, newFileContents);
return true;
} else {
return false;
}
}
async deleteTaskInFile(app, path, originalText) {
const fileContents = await this.getFileContents(app, path);
const splitContents = fileContents.split("\n");
let taskLineNum = await this.findLineNumberOfTask(splitContents, originalText);
if (taskLineNum >= 0) {
const oldLine = splitContents[taskLineNum];
console.log(`Removing task ${originalText} from file ${path} after move`);
console.log(`Line number ${taskLineNum}: ${oldLine}`);
let removed = splitContents.splice(taskLineNum, 1);
console.log(`Removed ${removed}`);
let newFileContents = splitContents.join("\n");
await this.writeFile(app, path, newFileContents);
return true;
} else {
return false;
}
}
async fixTasksInFile(app, path, fileContents) {
const regexLib = customJS.TaskRegularExpressions;
const formatter = customJS.TaskFormatter;
let splitContents = fileContents.split("\n");
for (let lineNum = 0; lineNum < splitContents.length; lineNum++) {
let line = splitContents[lineNum];
const regexMatch = line.match(regexLib.taskRegex);
if (regexMatch === null || (regexMatch.length === 0 && regexMatch[0] === "")) {
continue;
}
console.log(`Found a task ${regexMatch[2].trim()}`);
let taskObj = formatter.convertLineToTask(line);
if (!taskObj.createdDate) {
let createdDate = await this.getTaskCreatedDate(app, path);
taskObj.createdDate = createdDate;
}
taskObj.tags.sort();
let newLine = formatter.convertTaskToLine(taskObj);
console.log(`Task Updated:\nOld: ${line}\nNew: ${newLine}`)
splitContents[lineNum] = newLine;
}
return splitContents.join("\n");
}
async setId(app, path) {
const { getPropertyValue, update } = app.plugins.plugins["metaedit"].api;
let currentId = await getPropertyValue('id', path);
console.log(`FileUtilities.setId: document ${path} has id ${currentId}`);
if (!currentId) {
let newId = crypto.randomUUID().replace("-", "");
console.log(`FileUtilities.setId: setting document id to ${newId}`);
update('id', newId, path);
}
}
async getTaskCreatedDate(app, path) {
const { getPropertyValue } = app.plugins.plugins["metaedit"].api;
const moment = window.moment;
const linterFormat = 'YYYY-MM-DD HH:mm';
let fileCreatedString = await getPropertyValue('created', path);
let fileModifiedString = await getPropertyValue('modified', path);
const fileCreatedDate = moment(fileCreatedString, linterFormat);
const fileModifiedDate = moment(fileModifiedString, linterFormat);
const dateDifference = fileModifiedDate.diff(fileCreatedDate, 'days');
console.log(`Created Date: ${fileCreatedDate.format('YYYY-MM-DD')} Modified Date: ${fileModifiedDate.format('YYYY-MM-DD')}`);
if (!(fileCreatedDate.isValid() && fileModifiedDate.isValid())) {
return moment();
} else if (dateDifference > 1) {
return fileModifiedDate;
} else {
return fileCreatedDate;
}
}
/**
* Is one of the top Johnny Decimal categories (e.g. 00-09, 10-19, etc)
*/
getRootJDFolderNames(app) {
const foldersToExclude = ['📥 Inbox 📥', 'Journal'];
const rootDir = app.vault.getRoot();
return rootDir.children.filter((child) => {
let isDirectory = !child.extension || !child.basename;
let isJDFolder = foldersToExclude.indexOf(child.name) < 0;
return isDirectory && isJDFolder;
}).map((child) => child.name);
}
getChildJDFolderNames(app, childRoot) {
const secondLevel = app.vault.getRoot().children.find(child => child.name === childRoot);
if (!secondLevel) {
new Notice("Unable to retrieve directories for " + childRoot);
return false;
}
return secondLevel.children.filter((child) => {
let isDirectory = !child.extension || !child.basename;
return isDirectory;
}).map(child => child.name);
}
}
class ScheduleHelper {
TOP_LEVEL_OPTIONS = {
THIS_WEEK: 'This Week',
UPCOMING_WEEK: 'Upcoming Week',
MONTH: 'Month',
QUARTER: 'Quarter',
YEAR: 'Year',
SOMEDAY: 'Someday'
};
async updateTaskObjectWithSchedule(taskObj) {
const taskUtils = customJS.TaskUtilities;
let currentDate = window.moment();
const targetPeriod = await this.pickNewPeriod();
if (!targetPeriod) {
new Notice("Task scheduling abandoned", 2500);
return false;
}
let generatedTag = null;
let dueDate = null;
switch (targetPeriod) {
case this.TOP_LEVEL_OPTIONS.THIS_WEEK:
dueDate = await this.pickDueDate();
if (dueDate !== null) {
generatedTag = currentDate.format(taskUtils.weekFormat);
if (dueDate === false) {
dueDate = null;
}
}
break;
case this.TOP_LEVEL_OPTIONS.UPCOMING_WEEK:
generatedTag = await this.handleWeek();
break;
case this.TOP_LEVEL_OPTIONS.MONTH:
generatedTag = await this.handleMonth();
break;
case this.TOP_LEVEL_OPTIONS.QUARTER:
generatedTag = await this.handleQuarter();
break;
case this.TOP_LEVEL_OPTIONS.YEAR:
generatedTag = await this.handleYear();
break;
case this.TOP_LEVEL_OPTIONS.SOMEDAY:
generatedTag = taskUtils.somedayTag.replace('#', '');
break;
}
if (generatedTag === null && dueDate === null) {
new Notice("Task scheduling abandoned", 2500);
return false;
}
console.log(`updateTaskWithSchedule: New schedule\nPeriod: ${targetPeriod}\nTag: ${generatedTag}\nDueDate: ${dueDate}`);
const existingTag = taskUtils.hasWorkflowTag(taskObj.tags);
generatedTag = `#${generatedTag}`;
if (!taskObj.createdDate) {
let createdDate = await fileUtils.getTaskCreatedDate(app, path);
taskObj.createdDate = createdDate;
}
taskObj.dueDate = dueDate;
taskObj.tags = taskObj.tags.filter((tag) => tag !== existingTag);
taskObj.tags.push(generatedTag);
taskObj.tags.sort();
return taskObj;
}
async pickNewPeriod() {
let tp = window.tp;
const topLevelChoice = await tp.system.suggester(
Object.values(this.TOP_LEVEL_OPTIONS),
Object.values(this.TOP_LEVEL_OPTIONS)
)
return topLevelChoice;
}
async pickDueDate() {
const NO_DATE = "Don't pick a due date right now";
const tp = window.tp;
const moment = window.moment;
const dayFormat = 'ddd'
const suggesterFormat = 'dddd MM-DD';
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const today = moment().format(dayFormat);
if (today === "Sun") {
return null;
}
const remainingDays = days.slice(days.indexOf(today));
const displayDays = remainingDays.map((day) => moment(day, dayFormat).format(suggesterFormat));
let newDay = await tp.system.suggester(
[NO_DATE, ...displayDays],
[NO_DATE, ...remainingDays]
);
if (!newDay) {
return null;
}
if (newDay === NO_DATE) {
return false;
}
return moment(newDay, dayFormat);
}
async handleWeek() {
const tp = window.tp;
const moment = window.moment;
const taskUtils = customJS.TaskUtilities;
const currentDate = moment();
const choices = [
currentDate.format(taskUtils.weekFormat),
currentDate.add(1, 'w').format(taskUtils.weekFormat),
currentDate.add(1, 'w').format(taskUtils.weekFormat),
currentDate.add(1, 'w').format(taskUtils.weekFormat),
currentDate.add(1, 'w').format(taskUtils.weekFormat),
]
const chosen = await tp.system.suggester(
(week) => moment(week, taskUtils.weekFormat).startOf('week').format('[Week of] YYYY-MM-DD'),
choices
)
if (!chosen) {
return null;
}
return chosen;
}
async handleMonth() {
const moment = window.moment;
const taskUtils = customJS.TaskUtilities;
const currentDate = moment();
const choices = [
currentDate.format(taskUtils.monthFormat),
currentDate.add(1, 'M').format(taskUtils.monthFormat),
currentDate.add(1, 'M').format(taskUtils.monthFormat),
currentDate.add(1, 'M').format(taskUtils.monthFormat),
currentDate.add(1, 'M').format(taskUtils.monthFormat),
]
const chosen = await tp.system.suggester(
(month) => moment(month, taskUtils.monthFormat).format('MMMM'),
choices
)
if (!chosen) {
return null;
}
return chosen;
}
async handleQuarter() {
const tp = window.tp;
const moment = window.moment;
const taskUtils = customJS.TaskUtilities;
const currentDate = moment();
const choices = [
currentDate.format(taskUtils.quarterFormat),
currentDate.add(1, 'Q').format(taskUtils.quarterFormat),
currentDate.add(1, 'Q').format(taskUtils.quarterFormat),
currentDate.add(1, 'Q').format(taskUtils.quarterFormat),
currentDate.add(1, 'Q').format(taskUtils.quarterFormat),
]
const chosen = await tp.system.suggester(
choices,
choices
)
if (!chosen) {
return null;
}
return chosen;
}
async handleYear() {
const tp = window.tp;
const moment = window.moment;
const taskUtils = customJS.TaskUtilities;
const currentDate = moment();
const choices = [
currentDate.format(taskUtils.yearFormat),
currentDate.add(1, 'y').format(taskUtils.yearFormat),
currentDate.add(1, 'y').format(taskUtils.yearFormat),
]
const chosen = await tp.system.suggester(
choices,
choices
)
if (!chosen) {
return null;
}
return chosen;
}
}
class TaskEditor {
EDIT_OPTIONS = {
TAGS: "Tags",
TEXT: "Text",
PRIORITY: "Priority",
STATUS: "Status",
LOCATION: "Location",
SAVE: "Save"
}
async processTaskEdit(taskObj, currentPath) {
const formatter = customJS.TaskFormatter;
if (!taskObj) {
new Notice("Task is not valid", 2500);
}
let isCompleted = false;
let newTags = false;
let newText = false;
let newPriority = false;
let newStatus = false;
let newPath = false;
while (!isCompleted) {
let currentSelection = await this.selectValueToEdit();
if (!currentSelection || currentSelection === this.EDIT_OPTIONS.SAVE) {
isCompleted = true;
} else {
if (currentSelection === this.EDIT_OPTIONS.TAGS) {
newTags = await this.defineNewTags(taskObj);
} else if (currentSelection === this.EDIT_OPTIONS.TEXT) {
newText = await this.defineNewText(taskObj);
} else if (currentSelection === this.EDIT_OPTIONS.PRIORITY) {
newPriority = await this.defineNewPriority(taskObj)
} else if (currentSelection === this.EDIT_OPTIONS.STATUS) {
newStatus = await this.defineNewStatus(taskObj);
} else if (currentSelection === this.EDIT_OPTIONS.LOCATION) {
newPath = await this.pickNewLocation(currentPath);
} else {
new Notice("Unexpected selection - abandoning edit");
isCompleted = true;
newTags = false;
newText = false;
newPriority = false;
}
}
}
let needsUpdate = false;
if (newTags && newTags instanceof Array) {
newTags.sort();
let oldTagString = taskObj.tags.join(" ");
let newTagString = newTags.join(" ");
if (oldTagString !== newTagString) {
console.log(`Tags have been updated:\nOld:${oldTagString}\nNew:${newTagString}`);
taskObj.tags = newTags;
}
}
if (newText && newText !== taskObj.description) {
console.log(`Text has been updated:\nOld:${taskObj.description}\nNew:${newText}`);
taskObj.description = newText;
needsUpdate = true;
}
if (newPriority && newPriority !== taskObj.priority) {
console.log(`Priority has been updated:\nOld:${taskObj.priority}\nNew:${newPriority}`);
taskObj.priority = newPriority;
needsUpdate = true;
}
if (newStatus && newStatus !== taskObj.status) {
if (newStatus === formatter.Status.TODO) {
taskObj.status = newStatus;
taskObj.doneDate = undefined;
needsUpdate = true;
console.log(`Status has been updated:\nOld:${taskObj.status}\nNew:${newStatus}`);
} else if (newStatus === formatter.Status.DONE) {
taskObj.status = newStatus;
taskObj.doneDate = window.moment();
needsUpdate = true;
console.log(`Status has been updated:\nOld:${taskObj.status}\nNew:${newStatus}`);
} else {
new Notice("Unrecognized status " + newStatus);
}
}
if (newPath && newPath.path && newPath !== currentPath) {
newPath = newPath.path;
console.log(`Task filepath has been updated:\nOld:${currentPath}\nNew:${newPath}`);
needsUpdate = true;
}
if (needsUpdate) {
return {
task: taskObj,
path: newPath ? newPath : currentPath
}
} else {
new Notice("Nothing was updated");
return false;
}
}
async selectValueToEdit() {
const tp = window.tp;
return await tp.system.suggester(
Object.values(this.EDIT_OPTIONS),
Object.values(this.EDIT_OPTIONS)
)
}
async defineNewTags(taskObj) {
const tp = window.tp;
let newTags = await tp.system.prompt(
"Edit tags for " + taskObj.description,
taskObj.tags.join("\n"),
false, /* throw_on_cancel */
true /* multiline */
)
if (newTags) {
if (newTags.trim() === '') {
return [];
} else {
newTags = newTags.split("\n").map(tag => tag.trim());
}
}
return newTags;
}
async defineNewText(taskObj) {
const tp = window.tp;
let newText = await tp.system.prompt(
"Edit text for " + taskObj.description,
taskObj.description,
false, /* throw_on_cancel */
false /* multiline */
)
if (newText) {
return newText.trim();
} else {
return false;
}
}
async defineNewPriority(taskObj) {
const tp = window.tp;
const formatter = customJS.TaskFormatter;
const displayItems = [];
const valueItems = [];
Object.keys(formatter.Priority).forEach((key) => {
let value = formatter.Priority[key];
if (value === taskObj.priority) {
displayItems.push(key + " <- current");
} else {
displayItems.push(key);
}
valueItems.push(value);
});
let newPriority = await tp.system.suggester(displayItems, valueItems);
return newPriority;
}
async defineNewStatus(taskObj) {
const tp = window.tp;
const formatter = customJS.TaskFormatter;
const displayItems = [];
const valueItems = [];
Object.keys(formatter.Status).forEach((key) => {
let value = formatter.Status[key];
if (value === taskObj.status) {
displayItems.push(key + " <- current");
} else {
displayItems.push(key);
}
valueItems.push(value);
});
let newStatus = await tp.system.suggester(displayItems, valueItems);
return newStatus;
}
async pickNewLocation(currentPath) {
const tp = window.tp;
const app = window.app;
let fileCandidates = app.vault.getMarkdownFiles().filter((file) => {
return !file.path.startsWith("Journal") && !file.path.startsWith("📥 Inbox 📥");
});
let newPath = await tp.system.suggester(
(item) => {
if (item.path === currentPath) {
return `${item.basename} <-- Current`;
} else {
return item.basename;
}
},
fileCandidates, false, "Select a new path"
)
return newPath;
}
}
class TaskFilters {
isFocusForDay(taskObj, momentDate) {
const taskUtils = customJS.TaskUtilities;
let potentialTags = taskUtils.getFocusTags(taskObj.tags);
let hasTag = false;
for (const tag of potentialTags) {
let splitTag = tag.replace('#', '').split('_');
if (splitTag.length === 2) {
if (momentDate.format(taskUtils.dayFormat) === splitTag[1]) {
hasTag = true;
}
}
}
return hasTag;
}
hasObsoleteWorkflow(dvTask, momentDate) {
return dvTask !== null
&& !dvTask.completed
&& dvTask.tags !== null
&& this.workflowPeriodIsOverdue(dvTask.tags, momentDate)
&& !this.isFocusForDay(dvTask, momentDate);
}
isPastDue(dvTask, momentDate) {
return dvTask !== null
&& !dvTask.completed
&& dvTask.due !== undefined
&& this.convertLuxonDateToMomentDate(dvTask.due).isBefore(momentDate, 'day')
&& !this.isFocusForDay(dvTask, momentDate);
}
convertLuxonDateToMomentDate(luxonDate) {
let moment = window.moment;
return moment(luxonDate.toFormat('yyyy-MM-dd'));
}
isDueOnDate(dvTask, momentDate) {
return dvTask !== null
&& dvTask.due !== undefined
&& !dvTask.completed
&& this.convertLuxonDateToMomentDate(dvTask.due).isSame(momentDate, 'day')
&& !this.isFocusForDay(dvTask, momentDate);
}
wasCreatedDuringPeriod(dvTask, momentDate, periodName) {
return dvTask !== null
&& dvTask.created !== undefined
&& this.convertLuxonDateToMomentDate(dvTask.created).isSame(momentDate, periodName);
}
wasCompletedDuringPeriod(dvTask, momentDate, periodName) {
return dvTask !== null
&& dvTask.completion !== undefined
&& this.convertLuxonDateToMomentDate(dvTask.completion).isSame(momentDate, periodName);
}
isStartedButNotYetDue(dvTask, momentDate) {
if (dvTask === null || !dvTask.started) {
return false;
}
let startedDate = this.convertLuxonDateToMomentDate(dvTask.start);
if (startedDate.isBefore(momentDate, 'day')) {
return !dvTask.due || this.convertLuxonDateToMomentDate(dvTask.due).isAfter(momentDate, 'day');
}
return !this.isFocusForDay;
}
isDueByEndOfWeek(dvTask, momentDate) {
const taskUtils = customJS.TaskUtilities;
if (dvTask === null || dvTask.completion) {
return false;
}
if (dvTask.due) {
let convertedDueDate = this.convertLuxonDateToMomentDate(dvTask.due);
return convertedDueDate.isSame(momentDate, 'week') && convertedDueDate.isSameOrAfter(momentDate, 'day');
}
let workflowTag = taskUtils.hasWorkflowTag(dvTask.tags);
if (!workflowTag) {
return false;
}
let trimmedTag = workflowTag.replace('#', '');
let workflowPeriodName = taskUtils.workflowPeriodName(trimmedTag);
if (workflowPeriodName === 'year') {
return momentDate.format('YYYY') === trimmedTag && momentDate.week() >= 52;
} else if (workflowPeriodName === 'quarter') {
let workflowQuarter = moment(trimmedTag, taskUtils.quarterFormat);
return workflowQuarter.endOf('quarter').week() === momentDate.week();
} else if (workflowPeriodName === 'month') {
let workflowMonth = moment(trimmedTag, taskUtils.monthFormat)
return workflowMonth.endOf('month').week() === momentDate.week();
} else if (workflowPeriodName === 'week') {
let workflowWeek = moment(trimmedTag, taskUtils.weekFormat);
return workflowWeek.week() === momentDate.week();
}
return !this.isFocusForDay;
}
isCreatedOnDate(dvTask, momentDate) {
return this.wasCreatedDuringPeriod(dvTask, momentDate, 'day');
}
wasCreatedDuringWeek(dvTask, momentDate) {
return this.wasCreatedDuringPeriod(dvTask, momentDate, 'week');
}
isCompletedOnDate(dvTask, momentDate) {
return this.wasCompletedDuringPeriod(dvTask, momentDate, 'day');
}
wasCompletedDuringWeek(dvTask, momentDate) {
return this.wasCompletedDuringPeriod(dvTask, momentDate, 'week');
}
needsReview(dvTask) {
const taskUtils = customJS.TaskUtilities;
if (dvTask === null || dvTask.completion) {
return false;
}
let workflowTag = taskUtils.hasWorkflowTag(dvTask.tags);
return !workflowTag || workflowTag === taskUtils.unscheduledTag;
}
workflowPeriodIsAfter(tagList, momentDate) {
let comparisonValue = this._workflowPeriodComparison(tagList, momentDate);
return comparisonValue === 1;
}
workflowPeriodIsSame(tagList, momentDate) {
let comparisonValue = this._workflowPeriodComparison(tagList, momentDate);
return comparisonValue === 0;
}
workflowPeriodIsOverdue(tagList, momentDate) {
let comparisonValue = this._workflowPeriodComparison(tagList, momentDate);
return comparisonValue === -1;
}
/**
* This is intended to be private and accessed via the 3 above methods, largely due to the hacky
* return type. It's a typical comparator response (+ false if uncomparable)
* -1 = workflow period is in the past compared to the given date (using the same time span comparison)
* 0 = workflow period is the same period as the given date (using the same time span comparison)
* 1 = workflow period is in the future compared to the given date (using the same time span comparison)
*/
_workflowPeriodComparison(tagList, momentDate) {
let moment = window.moment;
let taskUtils = customJS.TaskUtilities;
let workflowTag = customJS.TaskUtilities.hasWorkflowTag(tagList);
if (!workflowTag) {
return false;
}
workflowTag = workflowTag.replace('#', '');
let workflowPeriodName = taskUtils.workflowPeriodName(workflowTag);
if (workflowPeriodName === false) {
return false;
}
let period = null;
let periodFormat = null;
switch (workflowPeriodName) {
case 'year':
periodFormat = taskUtils.yearFormat;
break;
case 'quarter':
periodFormat = taskUtils.quarterFormat;
break;
case 'month':
periodFormat = taskUtils.monthFormat;
break;
case 'week':
periodFormat = taskUtils.weekFormat;
break;
}
if (periodFormat === null) {
return false;
}
period = moment(workflowTag, periodFormat);
let comparisonValue = false;
if (period.isBefore(momentDate, workflowPeriodName)) {
comparisonValue = -1;
} else if (period.isSame(momentDate, workflowPeriodName)) {
comparisonValue = 0;
} else if (period.isAfter(momentDate, workflowPeriodName)) {
comparisonValue = 1;
}
return comparisonValue;
}
groupByArea(task) {
return task.path.split("/")[0];
}
groupByTopic(task) {
let splitPath = task.path.split("/");
if (splitPath.length === 1) {
return null;
} else {
return splitPath[1];
}
}
completionSort(task) {
return [task.due, task.completed];
}
}
//Adapted from obsidian-tasks logic
class TaskFormatter {
Status = {
TODO: 'Todo',
DONE: 'Done',
}
/**
* When sorting, make sure low always comes after none. This way any tasks with low will be below any exiting
* tasks that have no priority which would be the default.
*/
Priority = {
High: '1',
Medium: '2',
None: '3',
Low: '4',
}
prioritySymbols = {
High: '⏫',
Medium: '🔼',
Low: '🔽',
None: '',
};
recurrenceSymbol = '🔁';
startDateSymbol = '🛫';
scheduledDateSymbol = '⏳';
dueDateSymbol = '📅';
doneDateSymbol = '✅';
createdDateSymbol = '➕';
convertLineToTask(line) {
const regexLib = customJS.TaskRegularExpressions;
// Check the line to see if it is a markdown task.
const regexMatch = line.match(regexLib.taskRegex);
if (regexMatch === null) {
return null;
}
// match[3] includes the whole body of the task after the brackets.
const body = regexMatch[3].trim();
let description = body;
const indentation = regexMatch[1];
// Get the status of the task, only todo and done supported.
const statusString = regexMatch[2].toLowerCase();
let status;
switch (statusString) {
case ' ':
status = this.Status.TODO;
break;
default:
status = this.Status.DONE;
}
// Match for block link and remove if found. Always expected to be
// at the end of the line.
const blockLinkMatch = description.match(regexLib.blockLinkRegex);
const blockLink = blockLinkMatch !== null ? blockLinkMatch[0] : '';
if (blockLink !== '') {
description = description.replace(regexLib.blockLinkRegex, '').trim();
}
// Keep matching and removing special strings from the end of the
// description in any order. The loop should only run once if the
// strings are in the expected order after the description.
let matched;
let priority = this.Priority.None;
let startDate = null;
let scheduledDate = null;
let dueDate = null;
let doneDate = null;
let recurrenceRule = '';
let recurrence = null;
let createdDate = null;
let tags = [];
let taskObject = {};
// Tags that are removed from the end while parsing, but we want to add them back for being part of the description.
// In the original task description they are possibly mixed with other components
// (e.g. #tag1 <due date> #tag2), they do not have to all trail all task components,
// but eventually we want to paste them back to the task description at the end
let trailingTags = '';
// Add a "max runs" failsafe to never end in an endless loop:
const maxRuns = 20;
let runs = 0;
do {
matched = false;
const priorityMatch = description.match(regexLib.priorityRegex);
if (priorityMatch !== null) {
switch (priorityMatch[1]) {
case this.prioritySymbols.Low:
priority = this.Priority.Low;
break;
case this.prioritySymbols.Medium:
priority = this.Priority.Medium;
break;
case this.prioritySymbols.High:
priority = this.Priority.High;
break;
}
description = description.replace(regexLib.priorityRegex, '').trim();
matched = true;
}
const doneDateMatch = description.match(regexLib.doneDateRegex);
if (doneDateMatch !== null) {
doneDate = window.moment(doneDateMatch[1], regexLib.dateFormat);
description = description.replace(regexLib.doneDateRegex, '').trim();
matched = true;
}
const dueDateMatch = description.match(regexLib.dueDateRegex);
if (dueDateMatch !== null) {
dueDate = window.moment(dueDateMatch[1], regexLib.dateFormat);
description = description.replace(regexLib.dueDateRegex, '').trim();
matched = true;
}
const scheduledDateMatch = description.match(regexLib.scheduledDateRegex);
if (scheduledDateMatch !== null) {
scheduledDate = window.moment(scheduledDateMatch[1], regexLib.dateFormat);
description = description.replace(regexLib.scheduledDateRegex, '').trim();
matched = true;
}
const startDateMatch = description.match(regexLib.startDateRegex);
if (startDateMatch !== null) {
startDate = window.moment(startDateMatch[1], regexLib.dateFormat);
description = description.replace(regexLib.startDateRegex, '').trim();
matched = true;
}
const createdDateMatch = description.match(regexLib.createdDateRegex);
if (createdDateMatch !== null) {
createdDate = window.moment(createdDateMatch[1], regexLib.dateFormat);
description = description.replace(regexLib.createdDateRegex, '').trim();
matched = true;
}
const recurrenceMatch = description.match(regexLib.recurrenceRegex);
if (recurrenceMatch !== null) {
// Save the recurrence rule, but *do not parse it yet*.
// Creating the Recurrence object requires a reference date (e.g. a due date),
// and it might appear in the next (earlier in the line) tokens to parse
recurrenceRule = recurrenceMatch[1].trim();
description = description.replace(regexLib.recurrenceRegex, '').trim();
matched = true;
}
// Match tags from the end to allow users to mix the various task components with
// tags. These tags will be added back to the description below
const tagsMatch = description.match(regexLib.hashTagsFromEnd);
if (tagsMatch != null) {
description = description.replace(regexLib.hashTagsFromEnd, '').trim();
matched = true;
const tagName = tagsMatch[0].trim();
// Adding to the left because the matching is done right-to-left
trailingTags = trailingTags.length > 0 ? [tagName, trailingTags].join(' ') : tagName;
tags.push(tagName);
}
runs++;
} while (matched && runs <= maxRuns);
// Now that we have all the task details, parse the recurrence rule if we found any
if (recurrenceRule.length > 0) {
/**
* See original source for this
*/
recurrence = true;
}
// Add back any trailing tags to the description. We removed them so we can parse the rest of the
// components but now we want them back.
// The goal is for a task of them form 'Do something #tag1 (due) tomorrow #tag2 (start) today'
// to actually have the description 'Do something #tag1 #tag2'
// if (trailingTags.length > 0) description += ' ' + trailingTags;
// Tags are found in the string and pulled out but not removed,
// so when returning the entire task it will match what the user
// entered.
// The global filter will be removed from the collection.
const hashTagMatch = description.match(regexLib.hashTags);
if (hashTagMatch !== null) {
tags = [
...tags,
...hashTagMatch.map((tag) => tag.trim())
]
}
tags = tags.sort();
for (const tag of tags) {
description = description.replace(tag, '').trim();
}
return {
status,
description,
indentation,
originalStatusCharacter: statusString,
priority,
startDate,
scheduledDate,
dueDate,
doneDate,
createdDate,
recurrence,
blockLink,
tags,
};
}
convertTaskToLine(taskObj) {
const regexLib = customJS.TaskRegularExpressions;
let taskString = taskObj.description;
for (const tag of taskObj.tags) {
taskString += ` ${tag}`;
}
let priority = '';
if (taskObj.priority === this.Priority.High) {
priority = ' ' + this.prioritySymbols.High;
} else if (taskObj.priority === this.Priority.Medium) {
priority = ' ' + this.prioritySymbols.Medium;
} else if (taskObj.priority === this.Priority.Low) {
priority = ' ' + this.prioritySymbols.Low;
}
taskString += priority;
if (taskObj.recurrence) {
throw new Error ("Unable to handle tasks with recurrance");
}
// if (!layoutOptions.hideRecurrenceRule && taskObj.recurrence) {
// const recurrenceRule = layoutOptions.shortMode
// ? ' ' + recurrenceSymbol
// : ` ${recurrenceSymbol} ${taskObj.recurrence.toText()}`;
// taskString += recurrenceRule;
// }
if (taskObj.createdDate) {
const createdDate = ` ${this.createdDateSymbol} ${taskObj.createdDate.format(regexLib.dateFormat)}`
taskString += createdDate;
}
if (taskObj.startDate) {
const startDate = ` ${this.startDateSymbol} ${taskObj.startDate.format(regexLib.dateFormat)}`;
taskString += startDate;
}
if (taskObj.scheduledDate) {
const scheduledDate = ` ${this.scheduledDateSymbol} ${taskObj.scheduledDate.format(regexLib.dateFormat)}`;
taskString += scheduledDate;
}
if (taskObj.dueDate) {
const dueDate = ` ${this.dueDateSymbol} ${taskObj.dueDate.format(regexLib.dateFormat)}`;
taskString += dueDate;
}
if (taskObj.doneDate) {
const doneDate = ` ${this.doneDateSymbol} ${taskObj.doneDate.format(regexLib.dateFormat)}`;
taskString += doneDate;
}
const blockLink = taskObj.blockLink ?? '';
taskString += blockLink;
let statusCharacter = ' ';
if (taskObj.status === this.Status.TODO) {
statusCharacter = ' ';
} else if (taskObj.status === this.Status.DONE) {
statusCharacter = 'x';
} else {
new Notice("Error determining status character for " + taskObj.status);
}
return `${taskObj.indentation}- [${statusCharacter}] ${taskString}`;
}
}
/**
* From https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/src/Task.ts
*/
class TaskRegularExpressions {
constructor() {}
dateFormat = 'YYYY-MM-DD';
yearRegex = /(\d{4})/;
monthRegex = /^(\d{2})$/;
quarterRegex = /^Q(\d{1})$/;
weekRegex = /^W(\d{2})$/;
// Matches indentation before a list marker (including > for potentially nested blockquotes or Obsidian callouts)
indentationRegex = /^([\s\t>]*)/;
// Matches (but does not save) - or * list markers.
listMarkerRegex = /[-*]/;
// Matches a checkbox and saves the status character inside
checkboxRegex = /\[(.)\]/u;
// Matches the rest of the task after the checkbox.
afterCheckboxRegex = / *(.*)/u;
// Main regex for parsing a line. It matches the following:
// - Indentation
// - Status character
// - Rest of task after checkbox markdown
taskRegex = new RegExp(
this.indentationRegex.source +
this.listMarkerRegex.source +
' +' +
this.checkboxRegex.source +
this.afterCheckboxRegex.source,
'u',
);
// Used with the "Create or Edit Task" command to parse indentation and status if present
nonTaskRegex = new RegExp(
this.indentationRegex.source +
this.listMarkerRegex.source +
'? *(' +
this.checkboxRegex.source +
')?' +
this.afterCheckboxRegex.source,
'u',
);
// Used with "Toggle Done" command to detect a list item that can get a checkbox added to it.
listItemRegex = new RegExp(
this.indentationRegex.source + '(' + this.listMarkerRegex.source + ')',
);
// Match on block link at end.
blockLinkRegex = / \^[a-zA-Z0-9-]+$/u;
// The following regex's end with `$` because they will be matched and
// removed from the end until none are left.
priorityRegex = /([⏫🔼🔽])$/u;
startDateRegex = /🛫 *(\d{4}-\d{2}-\d{2})$/u;
scheduledDateRegex = /[⏳⌛] *(\d{4}-\d{2}-\d{2})$/u;
dueDateRegex = /[📅📆🗓] *(\d{4}-\d{2}-\d{2})$/u;
createdDateRegex = /[➕] *(\d{4}-\d{2}-\d{2})$/u;
doneDateRegex = /✅ *(\d{4}-\d{2}-\d{2})$/u;
recurrenceRegex = /🔁 ?([a-zA-Z0-9, !]+)$/iu;
// Regex to match all hash tags, basically hash followed by anything but the characters in the negation.
// To ensure URLs are not caught it is looking of beginning of string tag and any
// tag that has a space in front of it. Any # that has a character in front
// of it will be ignored.
// EXAMPLE:
// description: '#dog #car http://www/ddd#ere #house'
// matches: #dog, #car, #house
hashTags = /(^|\s)#[^ !@#$%^&*(),.?":{}|<>]*/g;
hashTagsFromEnd = new RegExp(this.hashTags.source + '$');
}
class TaskUtilities {
dayFormat = 'YYYY-MM-DD';
weekFormat = 'gggg-[W]ww';
monthFormat = 'YYYY-MM';
quarterFormat = 'YYYY-[Q]Q';
yearFormat = 'YYYY'
unscheduledTag = '#unscheduled';
somedayTag = '#someday';
focusTag = '#focus';
isWorkflowTag(tag) {
let confirmedTag = this.hasWorkflowTag([tag]);
return confirmedTag && confirmedTag === tag;
}
/**
* Workflow (defined in [[00.08 Task SOP]]) involves having one of the following tags:
* - Do on a week: #20XX-WNN
* - Do in a month: #20XX-NN
* - Do in a quarter: #20XX-QN
* - Do in a year: #20XX
* - Aspirational: #someday
* - Not processed yet: #unscheduled
*/
hasWorkflowTag(tagList) {
const regexLib = customJS.TaskRegularExpressions;
if (!tagList || tagList.length === 0) {
return false;
}
for (const tag of tagList) {
let trimmed = tag.trim();
if (trimmed === this.somedayTag || trimmed === this.unscheduledTag) {
return tag;
} else {
let split = trimmed.replace('#', '').split('-');
const yearMatch = split[0].match(regexLib.yearRegex);
if (yearMatch !== null) {
if (split.length === 2) {
let periodPart = split[1];
const monthMatch = periodPart.match(regexLib.monthRegex) !== null;
const quarterMatch = periodPart.match(regexLib.quarterRegex) !== null;
const weekMatch = periodPart.match(regexLib.weekRegex) !== null;
if (monthMatch || quarterMatch || weekMatch) {
return tag;
}
} else if (split.length === 1) {
return tag;
}
}
}
}
}
workflowPeriodName(workflowTagTrimmed) {
const regexLib = customJS.TaskRegularExpressions;
let splitTag = workflowTagTrimmed.split("-");
let hasYear = splitTag[0].match(regexLib.yearRegex) !== null;
if (splitTag.length == 1 && hasYear) {
return "year";
} else if (splitTag.length === 2 && hasYear) {
let subperiod = splitTag[1];
if (subperiod.match(regexLib.weekRegex) !== null) {
return "week";
} else if (subperiod.match(regexLib.monthRegex) !== null) {
return "month";
} else if (subperiod.match(regexLib.quarterFormat) !== null) {
return "quarter";
}
}
return false;
}
isFocusTag(tag) {
let confirmedTagList = this.getFocusTags([tag]);
return confirmedTagList.length === 1 && confirmedTagList[0] === tag;
}
/**
* Has two tags:
* #focus -> all will have this
* #focus_2022-08-09 -> all will have one or more specific dates
*/
getFocusTags(tagList) {
if (!tagList || tagList.length === 0) {
return [];
}
return tagList.filter(tag => {
let trimmedTag = tag.replace('#', '');
let splitTag = trimmedTag.split('_');
return tag === this.focusTag ||
(splitTag.length === 2 && splitTag[0] === this.focusTag.replace('#', ''));
})
}
toggleCompletion(taskObj) {
console.log(taskObj);
let formatter = customJS.TaskFormatter;
if (taskObj.status === formatter.Status.TODO) {
taskObj.status = formatter.Status.DONE;
taskObj.doneDate = window.moment();
} else {
taskObj.status = formatter.Status.TODO;
taskObj.doneDate = undefined;
}
return taskObj;
}
async addFocus(taskObj) {
const focusDay = await this.pickFocusDay();
if (!focusDay) {
new Notice("Task update abandoned", 2500);
return false;
}
let newTag = `${this.focusTag}_${focusDay}`;
taskObj.tags = taskObj.tags.filter(tag => tag !== this.focusTag && tag !== newTag);
taskObj.tags.push(this.focusTag);
taskObj.tags.push(newTag);
taskObj.tags.sort();
return taskObj;
}
async removeFocus(taskObj) {
const tp = window.tp;
let potentialTags = this.getFocusTags(taskObj.tags);
if (potentialTags.length <= 1) {
return false;
}
let toRemove = false;
potentialTags = potentialTags.filter(tag => tag.replace('#', '').split('_').length === 2);
if (potentialTags.length === 1) {
toRemove = potentialTags[0];
} else {
toRemove = await tp.system.suggester(potentialTags, potentialTags);
}
if (!toRemove) {
return false;
}
taskObj.tags = taskObj.tags.filter(tag => {
if (tag === this.focusTag) {
return potentialTags.length > 1;
} else {
return tag !== toRemove;
}
});
taskObj.tags.sort();
return taskObj;
}
async pickFocusDay() {
const currentDate = window.moment();
const tp = window.tp;
const day = await tp.system.suggester(
["Today", "Tomorrow"],
[currentDate.format(this.dayFormat), currentDate.add(1, 'd').format(this.dayFormat)]
)
return day;
}
}
class TaskViews {
buildStandardTableView(app, dv, that, tasklist) {
const viewUtils = customJS.TaskViewUtils;
dv.table(["Task", "Schedule", "Edit"], tasklist.map(task => [
viewUtils.formatTableDescription(task),
task.completed ? viewUtils.makeToggleTableButton(app, that, task) : viewUtils.makeScheduleTableButton(app, that, task),
viewUtils.makeEditTableButton(app, that, task)
]));
}
buildListOfTasksToReschedule(args) {
const { app, dv, that } = args;
const filters = customJS.TaskFilters;
const moment = window.moment;
const today = moment();
let tasks = dv.pages("").file.tasks.filter(task => filters.hasObsoleteWorkflow(task, today));
this.buildStandardTableView(app, dv, that, tasks);
}
buildReviewPage(args) {
const { app, dv, that } = args;
const filters = customJS.TaskFilters;
let tasks = dv.pages("").file.tasks.filter(filters.needsReview);
this.buildStandardTableView(app, dv, that, tasks);
}
buildWeeklyOverview(app, dv, that, weekString) {
const filters = customJS.TaskFilters;
const taskUtils = customJS.TaskUtilities;
const momentDate = moment(weekString, taskUtils.weekFormat);
const nextWeek = momentDate.clone().add(1, 'week');
let tasks = dv.pages("").file.tasks;
let overdueWorkflowTasks = tasks.filter(t => filters.hasObsoleteWorkflow(t, momentDate)).sort(filters.completionSort, 'asc');
let dueThisWeek = tasks.filter(t => filters.isDueByEndOfWeek(t, momentDate)).sort(filters.completionSort, 'asc');
let dueNextWeek = tasks.filter(t => filters.isDueByEndOfWeek(t, nextWeek)).sort(filters.completionSort, 'asc');
let createdThisWeek = tasks.filter(t => filters.wasCreatedDuringWeek(t, momentDate)).sort(filters.completionSort, 'asc');
let completedThisWeek = tasks.filter(t => filters.wasCompletedDuringWeek(t, momentDate));
dv.header(2, "Leftovers");
this.buildStandardTableView(app, dv, that, overdueWorkflowTasks);
dv.header(2, "Due this week");
this.buildStandardTableView(app, dv, that, dueThisWeek);
dv.header(2, "Due next week");
this.buildStandardTableView(app, dv, that, dueNextWeek);
dv.header(2, "Created this week");
this.buildStandardTableView(app, dv, that, createdThisWeek);
dv.header(2, "Completed this week");
this.buildStandardTableView(app, dv, that, completedThisWeek);
}
buildDailyOverview(app, dv, that, dateString) {
const filters = customJS.TaskFilters;
const momentDate = moment(dateString);
const viewUtils = customJS.TaskViewUtils;
let tasks = dv.pages("").file.tasks;
let focusForDay = tasks.filter(t => filters.isFocusForDay(t, momentDate)).sort(filters.completionSort, 'asc');
let overdueWorkflowTasks = tasks.filter(t => filters.hasObsoleteWorkflow(t, momentDate)).sort(filters.completionSort, 'asc');
let overdueTasks = tasks.filter(t => filters.isPastDue(t, momentDate)).sort(filters.completionSort, 'asc');
let dueToday = tasks.filter(t => filters.isDueOnDate(t, momentDate)).sort(filters.completionSort, 'asc');
let startedButNotDue = tasks.filter(t => filters.isStartedButNotYetDue(t, momentDate)).sort(filters.completionSort, 'asc');
let dueByEOW = tasks.filter(t => filters.isDueByEndOfWeek(t, momentDate)).sort(filters.completionSort, 'asc');
let createdToday = tasks.filter(t => filters.isCreatedOnDate(t, momentDate)).sort(filters.completionSort, 'asc');
let completedToday = tasks.filter(t => filters.isCompletedOnDate(t, momentDate)).sort(filters.completionSort, 'asc');
if (focusForDay.length > 0) {
dv.header(2, "Daily Focus");
dv.paragraph(
viewUtils.convertTasklistToCalloutString(
"Task to focus on",
viewUtils.calloutLevels.Focus,
false,
focusForDay,
momentDate
)
)
} else {
dv.header(2, "Select a task to focus!")
}
dv.header(2, "Secondary Tasks");
if (overdueWorkflowTasks.length > 0) {
dv.paragraph(
viewUtils.convertTasklistToCalloutString(
"Need to reschedule",
viewUtils.calloutLevels.Overdue,
true,
overdueWorkflowTasks,
momentDate
)
)
}
if (overdueTasks.length > 0) {
dv.paragraph(
viewUtils.convertTasklistToCalloutString(
"Overdue",
viewUtils.calloutLevels.Overdue,
false,
overdueTasks,
momentDate
)
)
}
if (dueToday.length > 0) {
dv.paragraph(
viewUtils.convertTasklistToCalloutString(
"Due Today",
viewUtils.calloutLevels.Today,
false,
dueToday,
momentDate
)
)
}
if (startedButNotDue.length > 0) {
dv.paragraph(
viewUtils.convertTasklistToCalloutString(
"Started",
viewUtils.calloutLevels.Soon,
true,
startedButNotDue,
momentDate
)
)
}
if (dueByEOW.length > 0) {
dv.paragraph(
viewUtils.convertTasklistToCalloutString(
"Due by end of week",
viewUtils.calloutLevels.Soon,
true,
dueByEOW,
momentDate
)
)
}
if (createdToday.length > 0) {
dv.paragraph(
viewUtils.convertTasklistToCalloutString(
"Created Today",
viewUtils.calloutLevels.Created,
true,
createdToday,
momentDate
)
)
}
if (completedToday.length > 0) {
dv.paragraph(
viewUtils.convertTasklistToCalloutString(
"Completed Today",
viewUtils.calloutLevels.Completed,
true,
completedToday,
momentDate
)
)
}
}
allTasksByDecimalGrouping(app, dv, that) {
const filters = customJS.TaskFilters;
let topLevelGroups = dv.pages("").file.tasks.groupBy(filters.groupByArea);
console.log(topLevelGroups);
for (let area of topLevelGroups) {
dv.header(1, area.key);
if (area.key === "📥 Inbox 📥" || area.key === 'Journal') {
this.buildStandardTableView(app, dv, that, area.rows);
} else {
let topicGroups = area.rows.groupBy(filters.groupByTopic);
for (let topic of topicGroups) {
dv.header(2, topic.key);
this.buildStandardTableView(app, dv, that, topic.rows);
}
}
}
}
}
class TaskViewUtils {
calloutLevels = {
Overdue: "DANGER",
Focus: "DANGER",
Today: "ATTENTION",
Soon: "TIP",
Created: "INFO",
Completed: "DONE"
}
async scheduleTableButtonHandler(app, dvTask) {
const fileUtils = customJS.FileUtilities;
const scheduleHelper = customJS.ScheduleHelper;
let statusChar = dvTask.completed ? 'x' : ' ';
let taskText = `- [${statusChar}] ${dvTask.text}`;
const taskObj = await fileUtils.findTaskInFile(app, dvTask.link.path, taskText);
if (!taskObj) {
return new Notice("Unable to locate task", 2500);
}
const updatedObj = await scheduleHelper.updateTaskObjectWithSchedule(taskObj);
if (updatedObj) {
let updated = await fileUtils.replaceTaskInFile(app, dvTask.link.path, taskText, updatedObj);
return new Notice("Task successfully updated: " + updated, 2500);
} else {
return new Notice("Failed to update task", 2500);
}
}
makeScheduleTableButton(app, that, dvTask) {
const { createButton } = app.plugins.plugins["buttons"]
return createButton({
app,
el: that.container,
args: { name: "Schedule", color: 'purple' },
clickOverride: {
click: this.scheduleTableButtonHandler.bind(this),
params: [app, dvTask]
}
})
}
async editTableButtonHandler(app, dvTask) {
const fileUtils = customJS.FileUtilities;
const editor = customJS.TaskEditor;
let statusChar = dvTask.completed ? 'x' : ' ';
let taskText = `- [${statusChar}] ${dvTask.text}`;
const taskObj = await fileUtils.findTaskInFile(app, dvTask.link.path, taskText);
if (!taskObj) {
return new Notice("Unable to locate task", 2500);
}
let currentFile = dvTask.link.path;
const taskAndPath = await editor.processTaskEdit(taskObj, currentFile);
if (taskAndPath && taskAndPath.task && taskAndPath.path) {
if (taskAndPath.path === currentFile) {
let updated = await fileUtils.replaceTaskInFile(app, currentFile, taskText, taskAndPath.task);
return new Notice("Task successfully updated: " + updated, 2500);
} else {
await fileUtils.appendToFile(app, taskAndPath.path, formatter.convertTaskToLine(taskAndPath.task));
await fileUtils.deleteTaskInFile(app, currentFile, taskText);
return new Notice("Task has been updated and moved to " + taskAndPath.path, 2500);
}
} else {
return new Notice("Failed to update task", 2500);
}
}
makeEditTableButton(app, that, dvTask) {
const { createButton } = app.plugins.plugins["buttons"]
return createButton({
app,
el: that.container,
args: { name: "Edit", color: 'purple' },
clickOverride: {
click: this.editTableButtonHandler.bind(this),
params: [app, dvTask]
}
})
}
async toggleTableButtonHandler(app, dvTask) {
const fileUtils = customJS.FileUtilities;
const taskUtils = customJS.TaskUtilities;
let statusChar = dvTask.completed ? 'x' : ' ';
let taskText = `- [${statusChar}] ${dvTask.text}`;
const taskObj = await fileUtils.findTaskInFile(app, dvTask.link.path, taskText);
if (!taskObj) {
return new Notice("Unable to locate task", 2500);
}
const updatedObj = taskUtils.toggleCompletion(taskObj);
if (updatedObj) {
let updated = await fileUtils.replaceTaskInFile(app, dvTask.link.path, taskText, updatedObj);
return new Notice("Task successfully updated: " + updated, 2500);
} else {
return new Notice("Failed to update task", 2500);
}
}
makeToggleTableButton(app, that, dvTask) {
const { createButton } = app.plugins.plugins["buttons"]
let name = dvTask.completed ? "Not Done" : "Done";
let color = dvTask.completed ? "red" : "purple";
return createButton({
app,
el: that.container,
args: { name: name, color: color },
clickOverride: {
click: this.toggleTableButtonHandler.bind(this),
params: [app, dvTask]
}
})
}
convertDVTask(dvTask) {
const formatter = customJS.TaskFormatter;
const markdownTask = this.convertDVTaskToMarkdown(dvTask);
return formatter.convertLineToTask(markdownTask);
}
convertDVTaskToMarkdown(dvTask) {
let taskPrefix = dvTask.completed ? '- [x]' : '- [ ]';
return taskPrefix + ' ' + dvTask.text;
}
formatTableDescription(task) {
const taskUtils = customJS.TaskUtilities;
const taskObj = this.convertDVTask(task);
//`${task.completed ? } ${task.text} (${task.link})`;
let html = "<span class='dataview-table-task'>";
html += task.completed ? '✅' : '⬛';
if (taskObj.dueDate) {
html += "<span class='task-due-date'>[Due " + taskObj.dueDate.format('MM-DD') + "]</span>";
}
html += " " + taskObj.description + " ";
for (const tag of task.tags) {
let tagClass = "";
if (taskUtils.isWorkflowTag(tag)) {
tagClass = " workflow-tag";
}
if (taskUtils.isFocusTag(tag)) {
tagClass = " focus-tag";
}
html += "<span class='" + tagClass + "'>" + tag + "</span>";
}
html += " (" + task.link + ")";
html += "</span>";
return html;
}
/**
* <a href="#someday" class="tag" target="_blank" rel="noopener">#someday</a>
*/
generateTagLink(tag) {
const taskUtils = customJS.TaskUtilities;
let classes = "tag";
if (taskUtils.isWorkflowTag(tag)) {
classes += " workflow-tag";
}
if (taskUtils.isFocusTag(tag)) {
classes += " focus-tag";
}
return `<a href='${tag}' class='${classes}' target='_blank' rel='noopener'>${tag}</a>`
}
/**
*
* <a
* href="app://obsidian.md/00-09%20Meta/00%20SOPs/00.08%20Task%20SOP.md#Remaining work"
* data-href="00-09 Meta/00 SOPs/00.08 Task SOP.md#Remaining work"
* rel="noopener"
* target="_blank"
* class="internal-link"
* >
* 00.08 Task SOP &gt; Remaining work
* </a>
*
*/
generateBacklink(dvTask) {
let fileName = dvTask.link.path;
let fileNameSplit = fileName.split('/');
let subpath = dvTask.link.subpath;
let href = "app://obsidian.md/" + encodeURIComponent(fileName);
let dataHref = fileName;
let displayText = fileNameSplit[fileNameSplit.length - 1];
if (subpath) {
href += "#" + subpath;
dataHref += "#" + subpath;
displayText += " > " + subpath;
}
let tag = `<a class='tasks-backlink internal-link' href='${href}' data-href='${dataHref}' rel='noopener' target='_blank'>`;
tag += displayText
tag += "</a>";
return tag;
}
_generateCommandUrl(commandId, task) {
let base = "obsidian://advanced-uri?vault=brain";
let taskFile = encodeURIComponent(task.link.path);
let taskLine = task.line + 1;
let taskText = encodeURIComponent(this.convertDVTaskToMarkdown(task));
return `${base}&commandid=${commandId}&taskFile=${taskFile}&taskLine=${taskLine}&taskText=${taskText}`
}
generateToggleCompletionButton(task) {
let commandid = "quickadd%253Achoice%253A6c6d56c8-fc75-407e-b320-d2b5362a8e70";
let toggleUrl = this._generateCommandUrl(commandid, task);
let status = task.completed ? '✅' : '⬛';
return `<a class='task-status-toggle' href='${toggleUrl}'>${status}</a>`;
}
generateEditButton(task) {
let commandid = "quickadd%253Achoice%253Ad584413e-6455-4d49-9c9d-3959718c5f31";
let editUrl = this._generateCommandUrl(commandid, task);
return `<a class='edit-button' href='${editUrl}'>[Edit]</a>`;
}
generateScheduleButton(task) {
let commandid = "quickadd%253Achoice%253A9e43784e-5238-4384-9f5e-da17a2970c4c";
let scheduleUrl = this._generateCommandUrl(commandid, task);
return `<a class='schedule-button' href='${scheduleUrl}'>[Schedule]</a>`;
}
generateFocusButton(task) {
let commandid = "quickadd%253Achoice%253A98b02c53-63da-4bfc-a261-16bc7d8544bc"
let focusUrl = this._generateCommandUrl(commandid, task);
return `<a class='focus-button' href='${focusUrl}'>[Focus]</a>`
}
generateUnfocusButton(task) {
let commandid = "quickadd%253Achoice%253Ae42d2c63-2e94-44e9-8cb1-8501eef174dc"
let unfocusUrl = this._generateCommandUrl(commandid, task);
return `<a class='focus-button' href='${unfocusUrl}'>[Unfocus]</a>`
}
convertTaskToString(task, momentDate) {
const filters = customJS.TaskFilters;
const editButton = this.generateEditButton(task);
const scheduleButton = this.generateScheduleButton(task);
let focusButton;
if (filters.isFocusForDay(task, momentDate)) {
focusButton = this.generateUnfocusButton(task);
} else {
focusButton = this.generateFocusButton(task);
}
const taskObj = this.convertDVTask(task);
let statusToggle = this.generateToggleCompletionButton(task);
let html = "<div class='daily-task-container'>";
html += "<span class='task-description'>"
html += statusToggle + " ";
if (taskObj.dueDate) {
html += "<span class='task-due-date'>[Due " + taskObj.dueDate.format('MM-DD') + "]</span>";
}
html += " " + taskObj.description + " ";
for (const tag of task.tags) {
html += this.generateTagLink(tag)
}
html += " (" + this.generateBacklink(task) + ")";
html += "</span>";
html += "<span class='task-actions'>";
html += editButton;
html += scheduleButton;
html += focusButton;
html += "</span>";
html += "</div>";
return html;
}
convertTasklistToCalloutString(
heading,
level,
isCollapsed,
tasklist,
momentDate
) {
let taskStrings = tasklist.map(task => this.convertTaskToString(task, momentDate));
let collapsedPip = isCollapsed ? "-" : "+";
let intro = `> [!${level}]${collapsedPip} ${heading}: ${tasklist.length}`;
return `${intro}\n> ${taskStrings.join('')}`;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment