/** * — kanban board for workspace task management. * * Views: workspace list → board with draggable columns. * Supports task creation, status changes, and priority labels. */ class FolkWorkBoard extends HTMLElement { private shadow: ShadowRoot; private space = ""; private view: "list" | "board" = "list"; private workspaceSlug = ""; private workspaces: any[] = []; private tasks: any[] = []; private statuses: string[] = ["TODO", "IN_PROGRESS", "REVIEW", "DONE"]; private loading = false; private error = ""; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); } connectedCallback() { this.space = this.getAttribute("space") || "demo"; this.loadWorkspaces(); this.render(); } private getApiBase(): string { const path = window.location.pathname; const match = path.match(/^\/([^/]+)\/work/); return match ? `/${match[1]}/work` : ""; } private async loadWorkspaces() { try { const base = this.getApiBase(); const res = await fetch(`${base}/api/spaces`); if (res.ok) this.workspaces = await res.json(); } catch { this.workspaces = []; } this.render(); } private async loadTasks() { if (!this.workspaceSlug) return; try { const base = this.getApiBase(); const res = await fetch(`${base}/api/spaces/${this.workspaceSlug}/tasks`); if (res.ok) this.tasks = await res.json(); const spaceRes = await fetch(`${base}/api/spaces/${this.workspaceSlug}`); if (spaceRes.ok) { const space = await spaceRes.json(); if (space.statuses?.length) this.statuses = space.statuses; } } catch { this.tasks = []; } this.render(); } private async createWorkspace() { const name = prompt("Workspace name:"); if (!name?.trim()) return; try { const base = this.getApiBase(); const res = await fetch(`${base}/api/spaces`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name.trim() }), }); if (res.ok) this.loadWorkspaces(); } catch { this.error = "Failed to create workspace"; this.render(); } } private async createTask() { const title = prompt("Task title:"); if (!title?.trim()) return; try { const base = this.getApiBase(); await fetch(`${base}/api/spaces/${this.workspaceSlug}/tasks`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: title.trim() }), }); this.loadTasks(); } catch { this.error = "Failed to create task"; this.render(); } } private async moveTask(taskId: string, newStatus: string) { try { const base = this.getApiBase(); await fetch(`${base}/api/tasks/${taskId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: newStatus }), }); this.loadTasks(); } catch { this.error = "Failed to move task"; this.render(); } } private openBoard(slug: string) { this.workspaceSlug = slug; this.view = "board"; this.loadTasks(); } private render() { this.shadow.innerHTML = ` ${this.error ? `
${this.esc(this.error)}
` : ""} ${this.view === "list" ? this.renderList() : this.renderBoard()} `; this.attachListeners(); } private renderList(): string { return `
Workspaces
${this.workspaces.length > 0 ? `
${this.workspaces.map(ws => `
${this.esc(ws.icon || "\u{1F4CB}")} ${this.esc(ws.name)}
${ws.task_count || 0} tasks \u00B7 ${ws.member_count || 0} members
`).join("")}
` : `

No workspaces yet

Create a workspace to start managing tasks

`} `; } private renderBoard(): string { return `
${this.esc(this.workspaceSlug)}
${this.statuses.map(status => { const columnTasks = this.tasks.filter(t => t.status === status); return `
${this.esc(status.replace(/_/g, " "))} ${columnTasks.length}
${columnTasks.map(t => this.renderTaskCard(t, status)).join("")}
`; }).join("")}
`; } private renderTaskCard(task: any, currentStatus: string): string { const otherStatuses = this.statuses.filter(s => s !== currentStatus); return `
${this.esc(task.title)}
${task.priority === "URGENT" ? 'URGENT' : ""} ${task.priority === "HIGH" ? 'HIGH' : ""} ${(task.labels || []).map((l: string) => `${this.esc(l)}`).join("")}
${otherStatuses.map(s => ``).join("")}
`; } private attachListeners() { this.shadow.getElementById("create-ws")?.addEventListener("click", () => this.createWorkspace()); this.shadow.getElementById("create-task")?.addEventListener("click", () => this.createTask()); this.shadow.querySelectorAll("[data-ws]").forEach(el => { el.addEventListener("click", () => this.openBoard((el as HTMLElement).dataset.ws!)); }); this.shadow.querySelectorAll("[data-back]").forEach(el => { el.addEventListener("click", () => { this.view = "list"; this.loadWorkspaces(); }); }); this.shadow.querySelectorAll("[data-move]").forEach(el => { el.addEventListener("click", (e) => { e.stopPropagation(); const btn = el as HTMLElement; this.moveTask(btn.dataset.move!, btn.dataset.to!); }); }); } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; } } customElements.define("folk-work-board", FolkWorkBoard);