From 883f4b5f2c5173144578ed5b1c224ad0e6be49aa Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 7 Apr 2026 22:32:20 -0400 Subject: [PATCH] feat(rtasks): assignee dropdown from space members + drag guard - Load space members via /api/spaces/:slug/members for assignee dropdown - Detail panel shows + ${this._spaceMembers.length > 0 ? ` + ` : ''}
@@ -1002,6 +1025,12 @@ class FolkTasksBoard extends HTMLElement { })(); const labelFilter = this._filterLabel; const confirmingDelete = this._confirmDeleteId === task.id; + const assigneeName = (() => { + const aid = task.assignee_id || task.assignee; + if (!aid) return ''; + const member = this._spaceMembers.find(m => m.did === aid); + return member?.displayName || aid.slice(0, 10); + })(); return `
@@ -1012,6 +1041,7 @@ class FolkTasksBoard extends HTMLElement {
${priorityBadge(task.priority || "")} ${dueDateBadge} + ${assigneeName ? `${this.esc(assigneeName)}` : ''} ${(task.labels || []).map((l: string) => `${this.esc(l)}`).join("")}
${confirmingDelete ? `
Delete?
` : ''} @@ -1062,7 +1092,17 @@ class FolkTasksBoard extends HTMLElement {
- + ${this._spaceMembers.length > 0 ? ` + + ` : ``}
@@ -1125,6 +1165,8 @@ class FolkTasksBoard extends HTMLElement { // Column click to create task in that column this.shadow.querySelectorAll("[data-col-click]").forEach(el => { el.addEventListener("click", (e) => { + // Don't trigger if a drag just ended + if (this._justDragged) return; // Don't trigger if clicking on a card, button, or input const target = e.target as HTMLElement; if (target.closest('.task-card, button, input, select, textarea, .create-form')) return; @@ -1137,23 +1179,24 @@ class FolkTasksBoard extends HTMLElement { }); // Create form handlers + const getCreateFormValues = () => ({ + title: (this.shadow.getElementById("cf-title") as HTMLInputElement)?.value || "", + priority: (this.shadow.getElementById("cf-priority") as HTMLSelectElement)?.value || "MEDIUM", + desc: (this.shadow.getElementById("cf-desc") as HTMLTextAreaElement)?.value || "", + status: (this.shadow.getElementById("cf-status") as HTMLInputElement)?.value || "", + assignee: (this.shadow.getElementById("cf-assignee") as HTMLSelectElement)?.value || "", + }); this.shadow.getElementById("cf-submit")?.addEventListener("click", () => { - const title = (this.shadow.getElementById("cf-title") as HTMLInputElement)?.value || ""; - const priority = (this.shadow.getElementById("cf-priority") as HTMLSelectElement)?.value || "MEDIUM"; - const desc = (this.shadow.getElementById("cf-desc") as HTMLTextAreaElement)?.value || ""; - const status = (this.shadow.getElementById("cf-status") as HTMLInputElement)?.value || ""; - this.submitCreateTask(title, priority, desc, undefined, status); + const v = getCreateFormValues(); + this.submitCreateTask(v.title, v.priority, v.desc, { status: v.status, assignee: v.assignee }); }); this.shadow.getElementById("cf-cancel")?.addEventListener("click", () => { this.showCreateForm = false; this.render(); }); this.shadow.getElementById("cf-title")?.addEventListener("keydown", (e) => { if ((e as KeyboardEvent).key === "Enter") { - const title = (this.shadow.getElementById("cf-title") as HTMLInputElement)?.value || ""; - const priority = (this.shadow.getElementById("cf-priority") as HTMLSelectElement)?.value || "MEDIUM"; - const desc = (this.shadow.getElementById("cf-desc") as HTMLTextAreaElement)?.value || ""; - const status = (this.shadow.getElementById("cf-status") as HTMLInputElement)?.value || ""; - this.submitCreateTask(title, priority, desc, undefined, status); + const v = getCreateFormValues(); + this.submitCreateTask(v.title, v.priority, v.desc, { status: v.status, assignee: v.assignee }); } }); @@ -1259,9 +1302,13 @@ class FolkTasksBoard extends HTMLElement { 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); - }); + const assigneeEl = this.shadow.getElementById("detail-assignee"); + if (assigneeEl) { + const event = assigneeEl.tagName === 'SELECT' ? 'change' : 'blur'; + assigneeEl.addEventListener(event, (e) => { + detailFieldSave('assignee_id', (e.target as HTMLInputElement | HTMLSelectElement).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); @@ -1535,6 +1582,9 @@ class FolkTasksBoard extends HTMLElement { }); el.addEventListener("pointerup", (e) => { if (!this.dragTaskId) return; + // Prevent column click handler from firing after drag + this._justDragged = true; + setTimeout(() => { this._justDragged = false; }, 100); el.classList.remove("dragging"); el.style.touchAction = ""; this.shadow.querySelectorAll(".column[data-status]").forEach(c => { @@ -1567,6 +1617,8 @@ class FolkTasksBoard extends HTMLElement { this.dragOverIndex = -1; }); el.addEventListener("pointercancel", () => { + this._justDragged = true; + setTimeout(() => { this._justDragged = false; }, 100); el.classList.remove("dragging"); el.style.touchAction = ""; this.shadow.querySelectorAll(".column[data-status]").forEach(c => { diff --git a/modules/rtasks/mod.ts b/modules/rtasks/mod.ts index cfa4560..fde270d 100644 --- a/modules/rtasks/mod.ts +++ b/modules/rtasks/mod.ts @@ -370,7 +370,7 @@ routes.post("/api/spaces/:slug/tasks", async (c) => { const slug = c.req.param("slug"); const body = await c.req.json(); - const { title, description, status, priority, labels, due_date } = body; + const { title, description, status, priority, labels, due_date, assignee_id } = body; if (!title?.trim()) return c.json({ error: "Title required" }, 400); const doc = ensureDoc(slug); @@ -380,7 +380,7 @@ routes.post("/api/spaces/:slug/tasks", async (c) => { const docId = boardDocId(slug, slug); _syncServer!.changeDoc(docId, `Create task ${taskId}`, (d) => { - d.tasks[taskId] = createTaskItem(taskId, slug, title.trim(), { + const task = createTaskItem(taskId, slug, title.trim(), { description: description || '', status: taskStatus, priority: priority || 'MEDIUM', @@ -388,6 +388,8 @@ routes.post("/api/spaces/:slug/tasks", async (c) => { dueDate: due_date ? new Date(due_date).getTime() : null, createdBy, }); + if (assignee_id) task.assigneeId = assignee_id; + d.tasks[taskId] = task; }); // Notify space members about the new task @@ -405,7 +407,7 @@ routes.post("/api/spaces/:slug/tasks", async (c) => { status: taskStatus, priority: priority || "MEDIUM", labels: labels || [], - assignee_id: null, + assignee_id: assignee_id || null, created_by: createdBy, sort_order: 0, due_date: due_date ? new Date(due_date).toISOString() : null,