diff --git a/modules/rtasks/components/folk-tasks-board.ts b/modules/rtasks/components/folk-tasks-board.ts
index 1998658..9e894a5 100644
--- a/modules/rtasks/components/folk-tasks-board.ts
+++ b/modules/rtasks/components/folk-tasks-board.ts
@@ -41,6 +41,10 @@ class FolkTasksBoard extends HTMLElement {
private _boardLabels: string[] = [];
// Inline delete confirmation
private _confirmDeleteId: string | null = null;
+ // Space members for assignee dropdown
+ private _spaceMembers: { did: string; displayName?: string; role: string; isOwner: boolean }[] = [];
+ // Drag guard — prevents column click from firing after drag
+ private _justDragged = false;
// ClickUp integration state
private _cuConnected = false;
private _cuTeamName = '';
@@ -84,6 +88,7 @@ class FolkTasksBoard extends HTMLElement {
this.subscribeOffline();
this.loadTasks();
this.loadClickUpStatus();
+ this.loadSpaceMembers();
this.render();
}
if (!localStorage.getItem("rtasks_tour_done")) {
@@ -206,6 +211,18 @@ class FolkTasksBoard extends HTMLElement {
this.render();
}
+ private async loadSpaceMembers() {
+ if (this.isDemo) return;
+ try {
+ // Members API is at /api/spaces/:slug/members (global, not module-scoped)
+ const res = await fetch(`/api/spaces/${this.space}/members`, { headers: this.authHeaders() });
+ if (res.ok) {
+ const data = await res.json();
+ this._spaceMembers = data.members || [];
+ }
+ } catch { /* not authenticated or no members — fallback to text input */ }
+ }
+
private async loadClickUpStatus() {
if (this.isDemo) return;
try {
@@ -413,11 +430,11 @@ class FolkTasksBoard extends HTMLElement {
return '';
}
- private async submitCreateTask(title: string, priority: string, description: string, dueDate?: string, status?: string) {
+ private async submitCreateTask(title: string, priority: string, description: string, opts?: { dueDate?: string; status?: string; assignee?: string }) {
if (!title.trim()) return;
- const taskStatus = status || this.statuses[0] || "TODO";
+ const taskStatus = opts?.status || this.statuses[0] || "TODO";
if (this.isDemo) {
- this.tasks.push({ id: `d${Date.now()}`, title: title.trim(), status: taskStatus, priority, labels: [], description: description.trim() || undefined, due_date: dueDate || null, sort_order: 0 });
+ this.tasks.push({ id: `d${Date.now()}`, title: title.trim(), status: taskStatus, priority, labels: [], description: description.trim() || undefined, due_date: opts?.dueDate || null, assignee_id: opts?.assignee || null, sort_order: 0 });
this.showCreateForm = false;
this.render();
return;
@@ -427,7 +444,7 @@ class FolkTasksBoard extends HTMLElement {
const res = await fetch(`${base}/api/spaces/${this.workspaceSlug}/tasks`, {
method: "POST",
headers: this.authHeaders({ "Content-Type": "application/json" }),
- body: JSON.stringify({ title: title.trim(), priority, status: taskStatus, description: description.trim() || undefined, due_date: dueDate || undefined }),
+ body: JSON.stringify({ title: title.trim(), priority, status: taskStatus, description: description.trim() || undefined, due_date: opts?.dueDate || undefined, assignee_id: opts?.assignee || undefined }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
@@ -683,6 +700,7 @@ class FolkTasksBoard extends HTMLElement {
.badge-high { background: #3b2611; color: #fb923c; }
.badge-medium { background: #3b3511; color: #facc15; }
.badge-low { background: #112a3b; color: #60a5fa; }
+ .badge-assignee { background: #1a2332; color: #93c5fd; font-style: italic; }
.move-btns { display: flex; gap: 4px; margin-top: 6px; }
.move-btn { font-size: 11px; padding: 6px 10px; min-height: 36px; border-radius: 4px; border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface-sunken); color: var(--rs-text-muted); cursor: pointer; }
@@ -878,6 +896,11 @@ class FolkTasksBoard extends HTMLElement {
+ ${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,