Last active
November 7, 2022 03:08
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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}`; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 + '$'); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 > 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