April 2023 Web App Development Update

Time Block Tasks

I added a couple features to the Time Block Tasks app this month. You can now reorder tasks, quickly add new tasks, and and update special repeatable tasks.

Reordering Tasks

Figuring out how to reorder tasks was tricky. I had no idea how I’d end up doing it. Giving database records a custom order has been one my long time muses. There’s lots of ways to reorder, but none of those other ones really matter. I created a new field called order_ms. The ms stands for milliseconds. When a user creates a new task, I get the current timestamp in milliseconds and put it in the order_ms field. Let’s say we have tasks A, B, and C with C being the oldest. When a user wants to move C up one, all I have to do is get the order_ms of tasks A and B, get the difference between the two, divide it by 2, and add it to the order_ms of B. That number will be C’s new order_ms, and if we’re ordering the tasks by order_ms, it’ll put C right in between A and B. It’s a similar process for moving a task down.

It’s not perfect, but I’ll test it out to see if it’s good enough. The problem is that if you keep dividing the order_ms, eventually they’ll be the same, but that might not be something that an actual user would do, and to break the task out of that, you’d just have to move the task to the bottom or top.

The great thing about this way of reordering is that you only have to update the single task. If you were using consecutive numbers instead of milliseconds, you’d have to update the order of every single task that comes before the task that you’re reordering.

Here are the two JavaScript functions for moving the task up and down the list. I didn’t have to do anything on the back end.

/**
* In the client, move the tasks around using the array index of the task in the store;
* In the server, re-order the task using the order_ms of the two tasks above (after) this task.
* Get the difference of those task's order_ms; divide by 2; and add that to the sibling task to get the new order_ms for the task that is being re-ordered
*/
const reorderTaskUp = () => {
const taskIndexValue = taskIndex.value; // save index to variable since computed value changes
let orderMs = Date.now(); // we can use this default value if the user is moving the task to the top
// first get the order_ms for the previous two tasks; find the difference, and split it in half for the moving task's new order_ms
const siblingTask = piniaStore.tasks[taskIndexValue - 1];
let dif, difHalf;
// if there is no other task, then just use now for the order_ms (move it to the top).
if (taskIndexValue - 2 >= 0) {
const siblingNextTask = piniaStore.tasks[taskIndexValue - 2];
dif = siblingNextTask.task.order_ms - siblingTask.task.order_ms;
difHalf = dif / 2;
orderMs = siblingTask.task.order_ms + difHalf;
}
// Remove the task from the array.
const taskToReorder = piniaStore.tasks.splice(taskIndexValue, 1);
// Subtract 1 from the index and add the task back into the array using the new index.
piniaStore.tasks.splice(taskIndexValue - 1, 0, taskToReorder[0])
// update the order_ms
piniaStore.tasks[taskIndex.value].task.order_ms = Math.round(orderMs); // nearest whole number
saveTask();
piniaStore.tasks.forEach((task) => {
console.log('order_ms', task.task.order_ms);
})
}

/**
* Move the task down the array.
*/
const reorderTaskDown = () => {
const taskIndexValue = taskIndex.value; // save index to variable since computed value changes
let orderMs = piniaStore.tasks[piniaStore.tasks.length - 1].task.order_ms - 10000 // we can use this default value (the last task order_ms - 10 seconds) if the user is moving the task to the bottom
// first get the order_ms for the next two tasks; find the difference, and split it in half for the moving task's new order_ms
const siblingTask = piniaStore.tasks[taskIndexValue + 1];
let dif, difHalf;
const lastTaskIndex = piniaStore.tasks.length - 1;
// only calculate if there are two ahead
if (taskIndexValue + 2 <= lastTaskIndex) {
const siblingNextTask = piniaStore.tasks[taskIndexValue + 2];
dif = siblingNextTask.task.order_ms - siblingTask.task.order_ms;
difHalf = dif / 2;
orderMs = siblingTask.task.order_ms + difHalf;
}
// Remove the task from the array.
const taskToReorder = piniaStore.tasks.splice(taskIndexValue, 1);
// Add 1 from the index, and add the task back into the array using the new index.
piniaStore.tasks.splice(taskIndexValue + 1, 0, taskToReorder[0])
// update the order_ms
piniaStore.tasks[taskIndex.value].task.order_ms = Math.round(orderMs); // nearest whole number
saveTask();
piniaStore.tasks.forEach((task) => {
console.log('order_ms', task.task.order_ms);
})
}

Update Repeatable Tasks

Repeatable tasks are kind of difficult to define. Programatically, I keep repeatable tasks in the books table, and I keep normal tasks in the tasks table. Tasks can belong to a repeatable task. Repeatable tasks basically help you categorize tasks and quickly add tasks to a todo list. Let’s say you had a task like Mow Lawn. That’s something that you’re going to do multiple times in your life time. You could create Mow Lawn as a repeatable task. You could create another repeatable task called Yard Work and put Mow Lawn under that repeatable task. Yard Work would be the parent repeatable task, and Mow Lawn would be the child repeatable task. Then, Mow Lawn would show up as Yard Work > Mow Lawn. Now, you could quickly create a todo task that belongs to that repeatable task.

Quickly Add Repeatable Tasks (Search Drop Down)

This feature was the logical next step after creating repeatable tasks. This feature lets you quickly create a task using the repeatable tasks. When you click into the search box, you’ll see a drop down of all your repeatable tasks (Yard Work and Yard Work > Mow Lawn). As you type, it looks through your repeatable tasks. If searched for “mow”, it would show you “Yard Work > Mow Lawn”. Clicking it would create a new task that belongs to the “Yard Work > Mow Lawn” repeatable task. Not as cool as reordering, but it’ll probably be a useful feature. I also ordered the repeatable tasks by recently updated. Pretty much every day, I create a new task called Planning so, Planning would always appear at the top, and I could immediately click it to add it to today’s todo list.

Here’s an important back end function that builds out the full titles of a repeatable task. Repeatable tasks can be hierarchical having multiple parent tasks. Instead of just seeing Mow Lawn, this shows you “Yard Work > Mow Lawn” in the drop down. Yard Work is the parent of Mow Law.

/**
* @param Book $book
* @param string $title
* @return string Example 'Work > Yard Work > Mowing Lawn'
*/
public function buildTitle(Book $book, string $title = ''): string
{
// if both the parent_id and title are empty, this is just the parent
if (empty($book->parent_id) && empty($title)) {
return $book->title;
// else if this is the parent, and the title has something, append the children titles
} else if (empty($book->parent_id)) {
return "$book->title > $title";
}
// if the book has a parent
$parent = $book->parent;
// if this is the first iteration, the title will be empty
if (empty($title)) {
$title = $book->title;
} else {
// otherwise, append what we've created so far
$title = "$book->title > $title";
}
// and send the title to be built further
return $this->buildTitle($parent, $title);
}