What I’m trying to do
I have a projects page where I list where I list down all of my active ongoing projects. Here I’d also like to have a table that lists down all active tasks in all of the related projects and that table should contain a column that allows me to to toggle the status of said tasks from the table without having to go to the notes where those tasks are in.
Things I have tried
I’ve tried using ChatGPT to write some dataviewjs code to try and achieve through the Tasks API, and while I can get the table to render, the toggle itself is useless and is basically there for aesthetics. The code I used is this:
// get all pages that might include tasks
const pages = dv.pages().where(p => p.file && p.file.tasks && p.file.tasks.length > 0);
console.log("Test");
// helper function to determine priority colour
function priorityColor(priority) {
if (!priority) return "black";
const p = priority.toLowerCase();
if (p === "high") return "red";
if (p === "medium") return "orange";
if (p === "low") return "blue";
return "black";
}
// async function to update the task's status using the Tasks API
async function updateTaskCompletion(file, task, newCompleted) {
try {
// Get the Tasks plugin API
const tasksApi = app.plugins.plugins['obsidian-tasks-plugin'].apiV1;
console.log("Tasks API:", tasksApi);
// Read the current content to get the exact task line
const content = await app.vault.read(file);
const lines = content.split("\n");
const taskLine = lines[task.line]; // Get the full line content
console.log("Current task line:", taskLine);
// Toggle the task using the Tasks API
const result = await tasksApi.executeToggleTaskDoneCommand(taskLine, file.path);
console.log("Toggled markdown:", result);
// If the Tasks API didn't work, fall back to manual file modification
if (!result) {
if (newCompleted) {
lines[task.line] = lines[task.line].replace(/- \[ \]/, "- [x]");
} else {
lines[task.line] = lines[task.line].replace(/- \[x\]/, "- [ ]");
}
const newContent = lines.join("\n");
await app.vault.modify(file, newContent);
}
return true;
} catch (error) {
console.error("Failed to update task:", error);
// Fallback to the original method if Tasks API isn't available
try {
// read the current content of the file
let content = await app.vault.read(file);
const lines = content.split("\n");
const index = task.line;
if (index < lines.length) {
if (newCompleted) {
lines[index] = lines[index].replace(/- \[ \]/, "- [x]");
} else {
lines[index] = lines[index].replace(/- \[x\]/, "- [ ]");
}
}
const newContent = lines.join("\n");
await app.vault.modify(file, newContent);
return true;
} catch (e) {
console.error("Fallback update failed:", e);
return false;
}
}
}
// function to render a checkbox as a DOM element that toggles its state and updates the file
function renderCheckbox(task, page, rowElement) {
const wrapper = document.createElement("span");
wrapper.className = "custom-checkbox-wrapper";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = task.completed;
checkbox.className = "custom-checkbox";
checkbox.style.cursor = "pointer";
// Apply styling for completed tasks
if (task.completed) {
wrapper.classList.add("custom-checkbox-complete");
}
checkbox.addEventListener("click", async (e) => {
e.stopPropagation();
const newCompleted = !task.completed;
// Display a small loading indicator
checkbox.disabled = true;
// Update the file with the new task status using Tasks API
const success = await updateTaskCompletion(page.file, task, newCompleted);
if (success) {
// Update the in-memory task state
task.completed = newCompleted;
// Update the status cell text
if (rowElement.cells && rowElement.cells[3]) {
rowElement.cells[3].innerText = task.completed ? "complete" : "incomplete";
}
// Update the checkbox styling
if (task.completed) {
wrapper.classList.add("custom-checkbox-complete");
} else {
wrapper.classList.remove("custom-checkbox-complete");
}
}
checkbox.disabled = false;
});
wrapper.appendChild(checkbox);
return wrapper;
}
// create an array to accumulate rows
let rows = [];
// iterate over pages and their tasks
pages.forEach(page => {
page.file.tasks.forEach(task => {
// Create an object to store row info for later reference
const rowElement = { cells: [] };
// retrieve priority and due date if set in the task object or page frontmatter
const priority = task.priority || (page.priority ? page.priority : "low");
const dueDate = task.due
? moment(task.due).format("YYYY-MM-DD")
: (page.due ? moment(page.due).format("YYYY-MM-DD") : "");
// the icon column: display a note icon linked to the note
const iconLink = `<a href="${page.file.path}">📄</a>`;
// build the row for the table
const row = [
renderCheckbox(task, page, rowElement),
iconLink,
task.text,
task.completed ? "complete" : "incomplete",
`<span style="color: ${priorityColor(priority)}">${priority}</span>`,
dueDate
];
// Store reference to cells for later updates
rowElement.cells = row;
rows.push(row);
});
});
// custom header names for the table columns
const headers = ["Done", "Note", "Task", "Status", "Priority", "Due Date"];
// render the table using the dataview API
dv.table(headers, rows);