From eeab84e588715db85166bc09a11771f1e39cc21d Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 6 Apr 2026 14:49:58 -0400 Subject: [PATCH] feat(rtasks): add detail panel, column management, search/filter, drag polish - Task detail slide-out panel with inline editing of all fields - Column/status management via gear icon (add, remove, rename, reorder) - Search & filter bar with text search, priority dropdown, label click filter - Enhanced task cards with description preview, due date badge, delete hover - Drag polish with rotation/scale transform on dragging cards - Empty drop zones always visible with green highlight on drag-over - Escape key closes detail panel and column editor - Individual field saves on blur/change (no full re-render flicker) Co-Authored-By: Claude Opus 4.6 --- modules/rtasks/components/folk-tasks-board.ts | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/modules/rtasks/components/folk-tasks-board.ts b/modules/rtasks/components/folk-tasks-board.ts index 40cc37a..8753529 100644 --- a/modules/rtasks/components/folk-tasks-board.ts +++ b/modules/rtasks/components/folk-tasks-board.ts @@ -1276,30 +1276,45 @@ class FolkTasksBoard extends HTMLElement { }); this.shadow.getElementById("detail-overlay")?.addEventListener("click", () => { this.detailTaskId = null; this.render(); }); this.shadow.getElementById("detail-close")?.addEventListener("click", () => { this.detailTaskId = null; this.render(); }); - // Detail field auto-save on blur - const detailSave = () => { + // Detail field auto-save — save individual fields + const detailFieldSave = (field: string, value: any) => { if (!this.detailTaskId) return; - const fields: Record = {}; - const titleEl = this.shadow.getElementById("detail-title") as HTMLInputElement; - const descEl = this.shadow.getElementById("detail-desc") as HTMLTextAreaElement; - const statusEl = this.shadow.getElementById("detail-status") as HTMLSelectElement; - const priorityEl = this.shadow.getElementById("detail-priority") as HTMLSelectElement; - const assigneeEl = this.shadow.getElementById("detail-assignee") as HTMLInputElement; - const dueEl = this.shadow.getElementById("detail-due") as HTMLInputElement; - if (titleEl) fields.title = titleEl.value.trim(); - if (descEl) fields.description = descEl.value; - if (statusEl) fields.status = statusEl.value; - if (priorityEl) fields.priority = priorityEl.value || null; - if (assigneeEl) fields.assignee_id = assigneeEl.value.trim() || null; - if (dueEl) fields.due_date = dueEl.value ? new Date(dueEl.value).toISOString() : null; - this.updateTask(this.detailTaskId!, fields); - this.detailTaskId = this.detailTaskId; // keep panel open + // Update local task data immediately + const task = this.tasks.find(t => t.id === this.detailTaskId); + if (task) { + if (field === 'due_date') task.due_date = value; + else if (field === 'assignee_id') { task.assignee_id = value; task.assignee = value; } + else task[field] = value; + } + // Persist to server (non-blocking, don't re-render) + if (!this.isDemo) { + const base = this.getApiBase(); + fetch(`${base}/api/tasks/${this.detailTaskId}`, { + method: "PATCH", + headers: this.authHeaders({ "Content-Type": "application/json" }), + body: JSON.stringify({ [field]: value }), + }).catch(() => { this.error = "Failed to save"; this.render(); }); + } }; - ['detail-title', 'detail-desc', 'detail-assignee', 'detail-due'].forEach(id => { - this.shadow.getElementById(id)?.addEventListener("blur", detailSave); + this.shadow.getElementById("detail-title")?.addEventListener("blur", (e) => { + const v = (e.target as HTMLInputElement).value.trim(); + if (v) detailFieldSave('title', v); }); - ['detail-status', 'detail-priority'].forEach(id => { - this.shadow.getElementById(id)?.addEventListener("change", detailSave); + this.shadow.getElementById("detail-desc")?.addEventListener("blur", (e) => { + detailFieldSave('description', (e.target as HTMLTextAreaElement).value); + }); + this.shadow.getElementById("detail-status")?.addEventListener("change", (e) => { + detailFieldSave('status', (e.target as HTMLSelectElement).value); + }); + this.shadow.getElementById("detail-priority")?.addEventListener("change", (e) => { + detailFieldSave('priority', (e.target as HTMLSelectElement).value || null); + }); + this.shadow.getElementById("detail-assignee")?.addEventListener("blur", (e) => { + detailFieldSave('assignee_id', (e.target as HTMLInputElement).value.trim() || null); + }); + this.shadow.getElementById("detail-due")?.addEventListener("change", (e) => { + const v = (e.target as HTMLInputElement).value; + detailFieldSave('due_date', v ? new Date(v).toISOString() : null); }); // Detail label add this.shadow.getElementById("detail-add-label")?.addEventListener("keydown", (e) => {