From c58cdf5638d0c3ba099bdac48d8a5384cf620a4a Mon Sep 17 00:00:00 2001 From: Art Malanok Date: Thu, 16 Apr 2026 18:38:53 -0700 Subject: [PATCH] v0.1.2: address ObsidianReviewBot findings Why: - Fix unhandled promises (void-prefixed calls in event listeners, setTimeout callbacks, and sync handlers). - Replace async-without-await signatures (loadTasks, onunload, onClose, countTasksInBucket) with sync equivalents. - Narrow nullish-coalesce stringify of keyof TaskItem to exclude TFile / objects from "[object Object]" stringification. - Replace innerHTML with DOM API in the empty-state renderer. - Replace createEl("h2", ...) with setHeading() via Setting API for settings sections. - Move inline element.style.* assignments into styles.css classes. - Use FileManager.trashFile() instead of Vault.delete() for stub cleanup. - UI strings use sentence case ("Open tasks", "New bucket", "Secondary grouping", "Getting started"). - Remove unused imports/bindings (BucketGroup, TaskRowCallbacks, BucketConfig, originalOnComplete). Co-authored-by: Isaac --- manifest.json | 2 +- package.json | 2 +- src/data/inlineTaskWatcher.ts | 6 +- src/data/taskStore.ts | 10 ++- src/interactions/checkboxHandler.ts | 11 +-- src/interactions/inlineCreator.ts | 13 +-- src/main.ts | 49 ++++------- src/settings.ts | 121 +++++++++++++--------------- src/views/focusRenderer.ts | 2 +- src/views/taskView.ts | 46 +++++++---- styles.css | 18 +++++ versions.json | 3 +- 12 files changed, 146 insertions(+), 137 deletions(-) diff --git a/manifest.json b/manifest.json index deef345..e206226 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "minimal-task-board", "name": "Minimal Task Board", - "version": "0.1.1", + "version": "0.1.2", "minAppVersion": "1.10.0", "description": "A clean, minimal task board with customizable buckets, secondary grouping, and inline task tracking.", "author": "Art Malanok", diff --git a/package.json b/package.json index c68ced3..94d1157 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minimal-task-board", - "version": "0.1.1", + "version": "0.1.2", "private": true, "scripts": { "build": "node esbuild.config.mjs", diff --git a/src/data/inlineTaskWatcher.ts b/src/data/inlineTaskWatcher.ts index 1379795..b8b7f1d 100644 --- a/src/data/inlineTaskWatcher.ts +++ b/src/data/inlineTaskWatcher.ts @@ -29,8 +29,8 @@ export class InlineTaskWatcher { } start(): void { - const ref = this.app.metadataCache.on("changed", (file, data, cache) => { - this.onFileChanged(file, cache); + const ref = this.app.metadataCache.on("changed", (file, _data, cache) => { + void this.onFileChanged(file, cache); }); this.eventRefs.push(ref); } @@ -168,7 +168,7 @@ export class InlineTaskWatcher { // Delete stubs that no longer have a matching #task line for (const stub of existingStubs) { if (!matchedStubs.has(stub.path)) { - await this.app.vault.delete(stub); + await this.app.fileManager.trashFile(stub); } } } diff --git a/src/data/taskStore.ts b/src/data/taskStore.ts index b8f3ca2..b83ffe5 100644 --- a/src/data/taskStore.ts +++ b/src/data/taskStore.ts @@ -83,7 +83,7 @@ export class TaskStore { this.completingPaths.delete(path); } - async loadTasks(): Promise { + loadTasks(): TaskItem[] { const settings = this.getSettings(); const folder = settings.taskFolder; const bucketProp = settings.bucketProperty.toLowerCase(); @@ -210,7 +210,13 @@ export class TaskStore { const groupMap = new Map(); for (const task of tasks) { - const key = String(task[prop] ?? ""); + const rawValue = task[prop]; + const key = + typeof rawValue === "string" + ? rawValue + : typeof rawValue === "number" || typeof rawValue === "boolean" + ? String(rawValue) + : ""; if (!groupMap.has(key)) { groupMap.set(key, []); } diff --git a/src/interactions/checkboxHandler.ts b/src/interactions/checkboxHandler.ts index bb32387..0e4c87c 100644 --- a/src/interactions/checkboxHandler.ts +++ b/src/interactions/checkboxHandler.ts @@ -62,11 +62,12 @@ export class CheckboxHandler { // Step 4: write frontmatter after 1700ms timeouts.push( - window.setTimeout(async () => { + window.setTimeout(() => { this.pendingTimeouts.delete(key); - await this.callbacks.onToggleDone(task.file); - // Signal animation end so the view can resume re-renders - this.callbacks.onAnimationEnd?.(); + void this.callbacks.onToggleDone(task.file).then(() => { + // Signal animation end so the view can resume re-renders + this.callbacks.onAnimationEnd?.(); + }); }, 1700) ); @@ -79,7 +80,7 @@ export class CheckboxHandler { rowElement.classList.remove("done", "completing"); checkbox?.classList.remove("checked"); - this.callbacks.onToggleDone(task.file); + void this.callbacks.onToggleDone(task.file); } private cancelPending(key: string): void { diff --git a/src/interactions/inlineCreator.ts b/src/interactions/inlineCreator.ts index 2234630..eface9d 100644 --- a/src/interactions/inlineCreator.ts +++ b/src/interactions/inlineCreator.ts @@ -61,16 +61,17 @@ export class InlineCreator { wrapper.appendChild(datalist); // Keyboard handlers - const handleKeydown = async (e: KeyboardEvent) => { + const handleKeydown = (e: KeyboardEvent) => { if (e.key === "Enter" && actionInput.value.trim()) { - await this.createTask( + void this.createTask( actionInput.value.trim(), bucketName, groupInput.value.trim() - ); - actionInput.value = ""; - groupInput.value = ""; - actionInput.focus(); + ).then(() => { + actionInput.value = ""; + groupInput.value = ""; + actionInput.focus(); + }); } else if (e.key === "Escape") { wrapper.remove(); } diff --git a/src/main.ts b/src/main.ts index ae04efc..2f3f857 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,106 +13,91 @@ export default class TasksPlugin extends Plugin { async onload(): Promise { await this.loadSettings(); - // Initialize the task store this.store = new TaskStore(this.app, () => this.settings); - // Register the custom view this.registerView(TASKS_VIEW_TYPE, (leaf: WorkspaceLeaf) => { return new TaskView(leaf, () => this.settings, this.store!); }); - // Add ribbon icon to open the task view - this.addRibbonIcon("square-check-big", "Open Tasks", () => { - this.activateView(); + this.addRibbonIcon("square-check-big", "Open tasks", () => { + void this.activateView(); }); - // Add command to open task view this.addCommand({ id: "open-tasks-view", name: "Open task board", callback: () => { - this.activateView(); + void this.activateView(); }, }); - // Add command to toggle between focus and board mode this.addCommand({ id: "toggle-tasks-mode", name: "Toggle focus/board mode", callback: () => { - // Toggle is handled within the view - this.activateView(); + void this.activateView(); }, }); - // Register settings tab this.addSettingTab(new TasksSettingTab(this.app, this)); - // Start inline task watcher this.watcher = new InlineTaskWatcher(this.app, () => this.settings); this.watcher.start(); - // Clean up orphaned stubs when source files are deleted this.registerEvent( - this.app.vault.on("delete", async (file: TAbstractFile) => { + this.app.vault.on("delete", (file: TAbstractFile) => { if (!(file instanceof TFile)) return; const taskFolder = this.settings.taskFolder; - // Only act on non-stub files (files outside the task folder) if (file.path.startsWith(taskFolder + "/")) return; const stubs = this.store?.getStubsForSource(file.path) ?? []; for (const stub of stubs) { - await this.app.vault.delete(stub); + void this.app.fileManager.trashFile(stub); } }) ); - // Update source_file in stubs when source files are renamed this.registerEvent( - this.app.vault.on("rename", async (file: TAbstractFile, oldPath: string) => { + this.app.vault.on("rename", (file: TAbstractFile, oldPath: string) => { if (!(file instanceof TFile)) return; const taskFolder = this.settings.taskFolder; - // Only act on non-stub files (files outside the task folder) if (file.path.startsWith(taskFolder + "/")) return; const stubs = this.store?.getStubsForSource(oldPath) ?? []; for (const stub of stubs) { - await this.app.fileManager.processFrontMatter(stub, (fm) => { + void this.app.fileManager.processFrontMatter(stub, (fm) => { fm.source_file = file.path; }); } }) ); - // After vault is ready: ensure folder exists, then load tasks - this.app.workspace.onLayoutReady(async () => { - await this.ensureTaskFolder(); - await this.store?.loadTasks(); + this.app.workspace.onLayoutReady(() => { + void (async () => { + await this.ensureTaskFolder(); + this.store?.loadTasks(); + })(); }); } - async onunload(): Promise { + onunload(): void { this.watcher?.destroy(); this.store?.destroy(); } private async ensureTaskFolder(): Promise { - let folder = this.settings.taskFolder; + const folder = this.settings.taskFolder; - // Check if the configured folder already exists if (this.app.vault.getAbstractFileByPath(folder)) { return; } - // Try the default name at vault root const defaultName = "_Tasks"; if (folder === defaultName || !this.app.vault.getAbstractFileByPath(defaultName)) { - // Use the default name await this.app.vault.createFolder(defaultName); this.settings.taskFolder = defaultName; await this.saveSettings(); return; } - // Default name is taken — append timestamp const now = new Date(); const stamp = [ now.getFullYear(), @@ -131,12 +116,10 @@ export default class TasksPlugin extends Plugin { private async activateView(): Promise { const existing = this.app.workspace.getLeavesOfType(TASKS_VIEW_TYPE); if (existing.length > 0) { - // Focus existing view this.app.workspace.revealLeaf(existing[0]); return; } - // Open in a new leaf const leaf = this.app.workspace.getLeaf("tab"); await leaf.setViewState({ type: TASKS_VIEW_TYPE, @@ -148,7 +131,6 @@ export default class TasksPlugin extends Plugin { async loadSettings(): Promise { const loaded = await this.loadData(); this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded); - // Ensure nested objects are merged properly if (loaded?.display) { this.settings.display = Object.assign( {}, @@ -163,7 +145,6 @@ export default class TasksPlugin extends Plugin { async saveSettings(): Promise { await this.saveData(this.settings); - // Reload tasks to pick up any config changes this.store?.loadTasks(); } } diff --git a/src/settings.ts b/src/settings.ts index e09065f..1974e6b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -18,8 +18,7 @@ export class TasksSettingTab extends PluginSettingTab { const { containerEl } = this; containerEl.empty(); - // ── General ────────────────────────────────────────────── - containerEl.createEl("h2", { text: "General" }); + new Setting(containerEl).setName("General").setHeading(); new Setting(containerEl) .setName("Task folder") @@ -61,8 +60,7 @@ export class TasksSettingTab extends PluginSettingTab { }) ); - // ── Buckets ────────────────────────────────────────────── - containerEl.createEl("h2", { text: "Buckets" }); + new Setting(containerEl).setName("Buckets").setHeading(); const bucketPropertySetting = new Setting(containerEl) .setName("Group by property") @@ -87,7 +85,6 @@ export class TasksSettingTab extends PluginSettingTab { text: "Property names are case-sensitive in frontmatter. Use lowercase.", }); - // Bucket list (custom DOM) const bucketSection = containerEl.createDiv({ cls: "tasks-bucket-list" }); bucketSection.createEl("div", { text: "Buckets", @@ -102,58 +99,54 @@ export class TasksSettingTab extends PluginSettingTab { this.plugin.settings.buckets.forEach((bucket, index) => { const row = listEl.createDiv({ cls: "tasks-bucket-row" }); - row.style.display = "flex"; - row.style.alignItems = "center"; - row.style.gap = "6px"; - row.style.marginBottom = "4px"; - // Up arrow const upBtn = row.createEl("button", { text: "\u2191" }); upBtn.setAttribute("aria-label", "Move up"); upBtn.disabled = index === 0; - upBtn.addEventListener("click", async () => { - this.swapBuckets(index, index - 1); - await this.plugin.saveSettings(); - this.display(); + upBtn.addEventListener("click", () => { + void (async () => { + this.swapBuckets(index, index - 1); + await this.plugin.saveSettings(); + this.display(); + })(); }); - // Down arrow const downBtn = row.createEl("button", { text: "\u2193" }); downBtn.setAttribute("aria-label", "Move down"); downBtn.disabled = index === this.plugin.settings.buckets.length - 1; - downBtn.addEventListener("click", async () => { - this.swapBuckets(index, index + 1); - await this.plugin.saveSettings(); - this.display(); + downBtn.addEventListener("click", () => { + void (async () => { + this.swapBuckets(index, index + 1); + await this.plugin.saveSettings(); + this.display(); + })(); }); - // Name input const input = row.createEl("input", { type: "text" }); input.value = bucket.name; - input.style.flex = "1"; - input.addEventListener("change", async () => { - const newName = input.value.trim(); - const duplicate = this.plugin.settings.buckets.some( - (b, i) => i !== index && b.name === newName - ); - if (duplicate) { - new Notice("A bucket with that name already exists."); - input.value = bucket.name; - return; - } - bucket.name = newName; - await this.plugin.saveSettings(); + input.addEventListener("change", () => { + void (async () => { + const newName = input.value.trim(); + const duplicate = this.plugin.settings.buckets.some( + (b, i) => i !== index && b.name === newName + ); + if (duplicate) { + new Notice("A bucket with that name already exists."); + input.value = bucket.name; + return; + } + bucket.name = newName; + await this.plugin.saveSettings(); + })(); }); - // Delete button with confirmation const delBtn = row.createEl("button", { text: "\u00d7" }); delBtn.setAttribute("aria-label", "Delete bucket"); let confirmPending = false; let confirmTimeout: ReturnType | null = null; - delBtn.addEventListener("click", async () => { + delBtn.addEventListener("click", () => { if (!confirmPending) { - // First click: count tasks and show confirmation state - const taskCount = await this.countTasksInBucket(bucket); + const taskCount = this.countTasksInBucket(bucket); delBtn.textContent = `Confirm? (${taskCount} task${taskCount !== 1 ? "s" : ""})`; confirmPending = true; confirmTimeout = setTimeout(() => { @@ -162,7 +155,6 @@ export class TasksSettingTab extends PluginSettingTab { }, 3000); return; } - // Second click: actually delete if (confirmTimeout) clearTimeout(confirmTimeout); this.plugin.settings.buckets.splice(index, 1); this.plugin.settings.buckets.forEach((b, i) => (b.sortOrder = i)); @@ -172,26 +164,30 @@ export class TasksSettingTab extends PluginSettingTab { ? this.plugin.settings.buckets[0].id : ""; } + void (async () => { + await this.plugin.saveSettings(); + this.display(); + })(); + }); + }); + + const addBtn = bucketSection.createEl("button", { + text: "+ Add bucket", + cls: "tasks-bucket-add-btn", + }); + addBtn.addEventListener("click", () => { + void (async () => { + const id = "bucket_" + Date.now().toString(36); + this.plugin.settings.buckets.push({ + id, + name: "New bucket", + sortOrder: this.plugin.settings.buckets.length, + }); await this.plugin.saveSettings(); this.display(); - }); + })(); }); - // Add bucket button - const addBtn = bucketSection.createEl("button", { text: "+ Add bucket" }); - addBtn.style.marginTop = "6px"; - addBtn.addEventListener("click", async () => { - const id = "bucket_" + Date.now().toString(36); - this.plugin.settings.buckets.push({ - id, - name: "New Bucket", - sortOrder: this.plugin.settings.buckets.length, - }); - await this.plugin.saveSettings(); - this.display(); - }); - - // Default bucket dropdown new Setting(containerEl) .setName("Default bucket for new tasks") .setDesc("Bucket assigned to tasks that don't match any other.") @@ -218,8 +214,7 @@ export class TasksSettingTab extends PluginSettingTab { }) ); - // ── Secondary Grouping ─────────────────────────────────── - containerEl.createEl("h2", { text: "Secondary Grouping" }); + new Setting(containerEl).setName("Secondary grouping").setHeading(); new Setting(containerEl) .setName("Enable sub-groups") @@ -265,8 +260,7 @@ export class TasksSettingTab extends PluginSettingTab { ); } - // ── Display ────────────────────────────────────────────── - containerEl.createEl("h2", { text: "Display" }); + new Setting(containerEl).setName("Display").setHeading(); new Setting(containerEl) .setName("Show due date") @@ -334,11 +328,10 @@ export class TasksSettingTab extends PluginSettingTab { }) ); - // ── Getting Started ────────────────────────────────────── - containerEl.createEl("h2", { text: "Getting Started" }); + new Setting(containerEl).setName("Getting started").setHeading(); containerEl.createEl("p", { - text: 'Add #task to any checkbox in your notes: - [ ] Do something #task', + text: "Add #task to any checkbox in your notes: - [ ] Do something #task", cls: "setting-item-description", }); containerEl.createEl("p", { @@ -361,11 +354,7 @@ export class TasksSettingTab extends PluginSettingTab { buckets.forEach((b, i) => (b.sortOrder = i)); } - /** - * Count how many task files in the _Tasks/ folder have a bucket property - * matching the given bucket's name. - */ - private async countTasksInBucket(bucket: BucketConfig): Promise { + private countTasksInBucket(bucket: BucketConfig): number { const folder = this.plugin.settings.taskFolder; const property = this.plugin.settings.bucketProperty; const files = this.app.vault.getFiles().filter( diff --git a/src/views/focusRenderer.ts b/src/views/focusRenderer.ts index 1c95e08..d85b415 100644 --- a/src/views/focusRenderer.ts +++ b/src/views/focusRenderer.ts @@ -1,4 +1,4 @@ -import { BucketGroup, DisplayConfig, BucketConfig, CLS } from "../types"; +import { BucketGroup, DisplayConfig, CLS } from "../types"; import { createBucket, BucketCallbacks } from "./bucketComponent"; export interface FocusRendererConfig { diff --git a/src/views/taskView.ts b/src/views/taskView.ts index 1828b48..48b4f50 100644 --- a/src/views/taskView.ts +++ b/src/views/taskView.ts @@ -4,14 +4,12 @@ import { CLS, TaskItem, TasksPluginSettings, - BucketGroup, } from "../types"; import { TaskStore } from "../data/taskStore"; import { FocusRenderer } from "./focusRenderer"; import { BoardRenderer } from "./boardRenderer"; import { DragManager } from "../interactions/dragManager"; import { CheckboxHandler } from "../interactions/checkboxHandler"; -import { TaskRowCallbacks } from "./taskRow"; import { BucketCallbacks } from "./bucketComponent"; import { InlineCreator } from "../interactions/inlineCreator"; @@ -38,8 +36,12 @@ export class TaskView extends ItemView { this.currentMode = settings().defaultView; this.dragManager = new DragManager({ - onMoveToBucket: (file, newBucket) => this.store.moveToBucket(file, newBucket), - onReorder: (file, order) => this.store.reorder(file, order), + onMoveToBucket: (file, newBucket) => { + void this.store.moveToBucket(file, newBucket); + }, + onReorder: (file, order) => { + void this.store.reorder(file, order); + }, }); this.checkboxHandler = new CheckboxHandler({ @@ -90,7 +92,7 @@ export class TaskView extends ItemView { const contentArea = this.rootEl.createDiv({ cls: `${CLS}-content` }); // Load tasks first, then render - await this.store.loadTasks(); + this.store.loadTasks(); this.renderView(contentArea); // React to data changes (debounced to avoid re-render storms) @@ -135,12 +137,22 @@ export class TaskView extends ItemView { } const emptyState = document.createElement("div"); emptyState.className = `${CLS}-empty-state`; - emptyState.innerHTML = [ - `
\u2610
`, - `
No tasks yet
`, - `
Add #task to any checkbox in your notes to see it here.
`, - `
Example: - [ ] Send report [[Project Alpha]] #task
`, - ].join(""); + const iconEl = document.createElement("div"); + iconEl.className = `${CLS}-empty-icon`; + iconEl.textContent = "\u2610"; + emptyState.appendChild(iconEl); + const titleEl = document.createElement("div"); + titleEl.className = `${CLS}-empty-title`; + titleEl.textContent = "No tasks yet"; + emptyState.appendChild(titleEl); + const hint1 = document.createElement("div"); + hint1.className = `${CLS}-empty-hint`; + hint1.textContent = "Add #task to any checkbox in your notes to see it here."; + emptyState.appendChild(hint1); + const hint2 = document.createElement("div"); + hint2.className = `${CLS}-empty-hint`; + hint2.textContent = "Example: - [ ] Send report [[Project Alpha]] #task"; + emptyState.appendChild(hint2); contentArea.appendChild(emptyState); return; } @@ -211,7 +223,6 @@ export class TaskView extends ItemView { if (row) { // P0: Block re-renders during the completion animation this.animating = true; - const originalOnComplete = this.checkboxHandler.handleToggle.bind(this.checkboxHandler); this.checkboxHandler.handleToggle(task, row); // The checkbox handler uses a 1700ms delay; set a timeout to clear the flag setTimeout(() => { @@ -223,11 +234,9 @@ export class TaskView extends ItemView { this.dragManager.handleDragStart(task, event); }, onClick: (task: TaskItem) => { - // Open the source note or the task file in a new leaf - const targetPath = task.sourceNote || task.file.path; const file = this.app.vault.getAbstractFileByPath(task.file.path); if (file instanceof TFile) { - this.app.workspace.getLeaf("tab").openFile(file); + void this.app.workspace.getLeaf("tab").openFile(file); } }, onAddTask: (bucketName: string) => { @@ -243,7 +252,9 @@ export class TaskView extends ItemView { const menu = new Menu(); for (const bucket of this.settings().buckets) { menu.addItem((item) => - item.setTitle(bucket.name).onClick(() => this.store.moveToBucket(task.file, bucket.name)) + item.setTitle(bucket.name).onClick(() => { + void this.store.moveToBucket(task.file, bucket.name); + }) ); } menu.showAtMouseEvent(event); @@ -265,9 +276,10 @@ export class TaskView extends ItemView { this.inlineCreator.showInput(contentArea, bucketName, existingGroups); } - async onClose(): Promise { + onClose(): Promise { this.focusRenderer?.destroy(); this.boardRenderer?.destroy(); this.dragManager.destroy(); + return Promise.resolve(); } } diff --git a/styles.css b/styles.css index 6c6c5e2..a8dbe4f 100644 --- a/styles.css +++ b/styles.css @@ -541,6 +541,24 @@ } } +/* ============================================================ + SETTINGS — BUCKET LIST + ============================================================ */ +.tasks-bucket-row { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; +} + +.tasks-bucket-row input[type="text"] { + flex: 1; +} + +.tasks-bucket-add-btn { + margin-top: 6px; +} + /* ============================================================ INLINE CREATION — SECONDARY GROUP INPUT ============================================================ */ diff --git a/versions.json b/versions.json index d032452..15e59fe 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,5 @@ { "0.1.0": "1.10.0", - "0.1.1": "1.10.0" + "0.1.1": "1.10.0", + "0.1.2": "1.10.0" }