Skip to content

Commit

Permalink
Issue27: Update prerequisites immediately when editing in TaskBan (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidlang42 authored Jan 28, 2024
1 parent 516ed75 commit c811709
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 18 deletions.
8 changes: 7 additions & 1 deletion app/JS_board.html
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,13 @@
function updateTaskSuccess(task,taskId) {
var ele = document.getElementById(taskId);
if(ele) removeClass(ele,"saving");
if(task) updateTaskElement(task);
if (Array.isArray(task)) {
for (const t of task) {
updateTaskElement(t);
}
} else if (task) {
updateTaskElement(task);
}
}

function updateTaskFailure(error,taskId) {
Expand Down
18 changes: 18 additions & 0 deletions app/Locking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const LOCK_PROPERTY_PREFIX = "lock.";

function lock(lockName) {
var p = PropertiesService.getUserProperties();
var key = LOCK_PROPERTY_PREFIX + lockName;
var value = `${Date.now()}.${Math.random() * 1000000}`;
if (p.getProperty(key)) {
return false;
}
p.setProperty(key, value);
return p.getProperty(key) == value;
}

function unlock(lockName) {
var p = PropertiesService.getUserProperties();
var key = LOCK_PROPERTY_PREFIX + lockName;
p.deleteProperty(key);
}
103 changes: 89 additions & 14 deletions app/Prerequisites.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,97 @@ function newTaskState() {
return { name: "", completed: false, due: false, prerequisiteNames: [] };
}

const PREREQUISITES_LOCK_PREFIX = "prerequisiteUpdates.";

//TRIGGER: every 5 minutes (could probably handle every 1 min if needed)
function runPrerequisiteUpdates() {
var now = new Date();
function runAllPrerequisiteUpdates() {
var errors = [];
var errorBoards = [];
for (const board of listBoards()) {
loadBoardProperties(board);
if (board.properties.enable_prerequisites) {
var state = loadPrerequisiteState(board.id);
var changes = updatePrerequisiteState(board.id, state);
if (changes || (!!state.nextDatePrerequisite && state.nextDatePrerequisite < now)) {
processPrerequisiteState(board.id, state);
try {
runPrerequisiteUpdatesForBoard(board.id, true); // throws error if already locked
} catch (err) {
errors.push(err);
errorBoards.push(board.title);
}
storePrerequisiteState(board.id, state);
}
}
if (errors.length) {
throw new AggregateError(errors, "Failed to run prerequisite updates for: " + errorBoards.join(", "));
}
}

function runPrerequisiteUpdatesForBoard(boardId, errorIfLocked) {
if (lock(PREREQUISITES_LOCK_PREFIX + boardId)) {
var state = loadPrerequisiteState(boardId);
var changes = updatePrerequisiteState(boardId, state);
var processed = null;
if (changes || (!!state.nextDatePrerequisite && state.nextDatePrerequisite < new Date())) {
processed = processPrerequisiteState(boardId, state);
}
storePrerequisiteState(boardId, state);
unlock(PREREQUISITES_LOCK_PREFIX + boardId);
return processed;
} else if (errorIfLocked) {
throw Error("Cannot run prerequisite updates for locked board: " + boardId);
}
}

function runPrerequisiteUpdatesForTask(boardId, task) {
// lock not required because we aren't changing the prerequisite state
// instead we just shortcut to check if *this* task is ready
// but not for duplicate tasks or missing tasks, as these could depend on other tasks (if we detect that, we assume not ready)
// technically 'ready' might also be wrong if a task has recently changed been uncompleted, but this seems like a very minor edge case
if (!!task.due) return; // due date already set
/* from: updatePrerequisiteState() */
var prerequisiteNames = task.notes?.match(/(?<=\{).+?(?=\})/g);
if (!prerequisiteNames || !prerequisiteNames.length) return; // no prerequisites to check
var state = loadPrerequisiteState(boardId);
/* from: processPrerequisiteState() */
// create lookups
var completedByName = {};
var duplicateNames = [];
for (const id in state.taskStateById) {
const taskState = state.taskStateById[id];
if (completedByName[taskState.name] != null) {
duplicateNames.push(taskState.name);
}
completedByName[taskState.name] = taskState.completed;
}
// check if tasks are ready to action
var now = new Date();
var ready = true;
var missing = [];
var duplicates = [];
for (const prerequisiteName of prerequisiteNames) {
var completed = completedByName[prerequisiteName];
if (duplicateNames.includes(prerequisiteName)) {
duplicates.push(prerequisiteName);
} else if (completed == null) {
var possible_date = Date.parse(prerequisiteName);
if (!isNaN(possible_date)) {
var d = new Date(possible_date);
if (d > now) {
// waiting on future date
ready = false;
}
} else {
missing.push(prerequisiteName);
}
} else if (!completed) {
ready = false;
}
}
if (ready && duplicates.length == 0 && missing.length == 0) {
// setTaskDueWithMessage() won't call a task read because we already have it
return setTaskDueWithMessage(boardId, task.id, "Prerequisite tasks complete: " + prerequisiteNames.join(", "), task);
}
}

function processPrerequisiteState(boardId, state) {
var processed = [];
// create lookups
var completedByName = {};
var unusedCompletedIdsByName = {};
Expand All @@ -62,7 +136,7 @@ function processPrerequisiteState(boardId, state) {
unusedCompletedIdsByName[taskState.name] = id;
}
}
// check if tasks are ready to action
// check if tasks are ready to action
var now = new Date();
var nextDate = null;
for (const id in state.taskStateById) {
Expand Down Expand Up @@ -98,11 +172,11 @@ function processPrerequisiteState(boardId, state) {
if (!taskState.due) {
// setTaskDueWithMessage() calls a task read, therefore only call if it might actually be useful
if (duplicates.length > 0) {
setTaskDueWithMessage(boardId, id, "Prerequisite tasks have duplicate names: " + duplicates.join(", "));
processed.push(setTaskDueWithMessage(boardId, id, "Prerequisite tasks have duplicate names: " + duplicates.join(", ")));
} else if (missing.length > 0) {
setTaskDueWithMessage(boardId, id, "Could not find prerequisite tasks: " + missing.join(", "));
processed.push(setTaskDueWithMessage(boardId, id, "Could not find prerequisite tasks: " + missing.join(", ")));
} else if (ready) {
setTaskDueWithMessage(boardId, id, "Prerequisite tasks complete: " + taskState.prerequisiteNames.join(", "));
processed.push(setTaskDueWithMessage(boardId, id, "Prerequisite tasks complete: " + taskState.prerequisiteNames.join(", ")));
}
}
}
Expand All @@ -111,19 +185,20 @@ function processPrerequisiteState(boardId, state) {
const id = unusedCompletedIdsByName[name];
delete state.taskStateById[id];
}
return processed;
}

const MAX_NOTES_LENGTH = 8000;

function setTaskDueWithMessage(boardId, taskId, message) {
var t = Tasks.Tasks.get(boardId, taskId);
function setTaskDueWithMessage(boardId, taskId, message, already_up_to_date_task) {
var t = already_up_to_date_task ?? Tasks.Tasks.get(boardId, taskId);
if (!t.due) {
var changes = { due: formatDateTasks(new Date()) };
var new_notes = message + "\n///\n" + t.notes;
if (new_notes.length <= MAX_NOTES_LENGTH) {
changes.notes = new_notes; // only add to notes if it fits within max length
}
Tasks.Tasks.patch(changes, boardId, taskId);
return Tasks.Tasks.patch(changes, boardId, taskId);
}
}

Expand Down
28 changes: 25 additions & 3 deletions app/Tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ const LIST_SUFFIX = ")";

// client call
function getAllTasks(boardId) {
return getAllTasksInternal(boardId, true); // updates prerequisites if enabled
}

function getAllTasksInternal(boardId, updatePrerequisitesIfEnabled) {
var board = {id: boardId};
loadBoardProperties(board);
if (updatePrerequisitesIfEnabled && board.properties.enable_prerequisites) {
runPrerequisiteUpdatesForBoard(boardId, false); // does nothing if already locked
}
var result = {};
var tasks = [];
do {
Expand All @@ -21,7 +28,7 @@ function getAllTasks(boardId) {
// client call
function getManyTasks(boardId, taskIds) {
var tasks = [];
for(const task of getAllTasks(boardId)) {
for(const task of getAllTasksInternal(boardId, false)) { // does not update prerequisites
if (taskIds.includes(task.id))
tasks.push(task);
}
Expand Down Expand Up @@ -154,9 +161,24 @@ function updateTask(boardId,changes,afterTaskId) {
if (changes.id) {
task = Tasks.Tasks.patch(changes, boardId, changes.id);
if (afterTaskId)
task = Tasks.Tasks.move(boardId, changes.id, {previous: afterTaskId}); //FUTURE {parent:,previous:}
task = Tasks.Tasks.move(boardId, changes.id, {previous: afterTaskId}); //FUTURE {parent:}
} else {
task = Tasks.Tasks.insert(changes, boardId, {previous: afterTaskId}); //FUTURE {parent:,previous:}
task = Tasks.Tasks.insert(changes, boardId, {previous: afterTaskId}); //FUTURE {parent:}
}
if (board.properties.enable_prerequisites) {
if (changes.status == "completed") { // might affect any task on this board
var updated_tasks = runPrerequisiteUpdatesForBoard(boardId, false); // does nothing if already locked
if (updated_tasks && updated_tasks.length) {
updated_tasks.push(task);
for (const t of updated_tasks) {
processTask(t, board);
}
return updated_tasks;
}
} else if (!changes.deleted && 'notes' in changes) { // could only affect this task
var updated_task = runPrerequisiteUpdatesForTask(boardId, task);
if (updated_task) task = updated_task;
}
}
processTask(task, board);
return task;
Expand Down

0 comments on commit c811709

Please sign in to comment.