Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m55s
Details
CI/CD / deploy (push) Successful in 2m55s
Details
This commit is contained in:
commit
17a17103f4
|
|
@ -1,23 +1,18 @@
|
|||
/**
|
||||
* <folk-tasks-board> — kanban board for workspace task management.
|
||||
* <folk-tasks-board> — kanban board for per-space task management.
|
||||
*
|
||||
* Views: workspace list → board with draggable columns.
|
||||
* Single board per space, loaded directly on page load.
|
||||
* Supports task creation, status changes, and priority labels.
|
||||
*/
|
||||
|
||||
import { boardSchema, type BoardDoc } from "../schemas";
|
||||
import { makeDraggableAll } from "../../../shared/draggable";
|
||||
import type { DocumentId } from "../../../shared/local-first/document";
|
||||
import { TourEngine } from "../../../shared/tour-engine";
|
||||
import { ViewHistory } from "../../../shared/view-history.js";
|
||||
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
|
||||
|
||||
class FolkTasksBoard 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;
|
||||
|
|
@ -32,7 +27,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
private dragSourceStatus: string | null = null;
|
||||
private priorities = ["LOW", "MEDIUM", "HIGH", "URGENT"];
|
||||
private _offlineUnsubs: (() => void)[] = [];
|
||||
private _history = new ViewHistory<"list" | "board">("list");
|
||||
private _backlogTaskId: string | null = null;
|
||||
private _stopPresence: (() => void) | null = null;
|
||||
// Detail panel
|
||||
|
|
@ -45,8 +39,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
private _showColumnEditor = false;
|
||||
// Board labels (from server)
|
||||
private _boardLabels: string[] = [];
|
||||
// Inline workspace creation
|
||||
private _showCreateWs = false;
|
||||
// Inline delete confirmation
|
||||
private _confirmDeleteId: string | null = null;
|
||||
// ClickUp integration state
|
||||
|
|
@ -65,7 +57,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
private _cuImportResult: { boardId: string; taskCount: number } | null = null;
|
||||
private _tour!: TourEngine;
|
||||
private static readonly TOUR_STEPS = [
|
||||
{ target: '.workspace-card', title: "Workspaces", message: "Select a workspace to open its kanban board.", advanceOnClick: true },
|
||||
{ target: '#create-task', title: "New Task", message: "Create a new task with title, priority, and description.", advanceOnClick: false },
|
||||
{ target: '.board', title: "Kanban Board", message: "Drag tasks between columns — TODO, In Progress, Review, Done.", advanceOnClick: false },
|
||||
{ target: '.badge.clickable', title: "Priority", message: "Click the priority badge on a task to cycle through levels.", advanceOnClick: false },
|
||||
|
|
@ -84,13 +75,14 @@ class FolkTasksBoard extends HTMLElement {
|
|||
|
||||
connectedCallback() {
|
||||
this.space = this.getAttribute("space") || "demo";
|
||||
this.workspaceSlug = this.space;
|
||||
// Check for ?backlog= deep-link from email checklist
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
this._backlogTaskId = params.get("backlog");
|
||||
if (this.space === "demo") { this.loadDemoData(); }
|
||||
else {
|
||||
this.subscribeOffline();
|
||||
this.loadWorkspaces();
|
||||
this.loadTasks();
|
||||
this.loadClickUpStatus();
|
||||
this.render();
|
||||
}
|
||||
|
|
@ -137,19 +129,12 @@ class FolkTasksBoard extends HTMLElement {
|
|||
|
||||
try {
|
||||
const docs = await runtime.subscribeModule('tasks', 'boards', boardSchema);
|
||||
// Build workspace list from cached boards
|
||||
if (docs.size > 0 && this.workspaces.length === 0) {
|
||||
const boards: any[] = [];
|
||||
for (const [docId, doc] of docs) {
|
||||
const d = doc as BoardDoc;
|
||||
if (!d?.board) continue;
|
||||
boards.push({ slug: d.board.slug, name: d.board.name, icon: null, task_count: Object.keys(d.tasks || {}).length });
|
||||
// Subscribe to changes for the current board
|
||||
if (docs.size > 0) {
|
||||
for (const [docId] of docs) {
|
||||
this._offlineUnsubs.push(runtime.onChange(docId, () => this.refreshFromDocs()));
|
||||
}
|
||||
if (boards.length > 0) {
|
||||
this.workspaces = boards;
|
||||
this.render();
|
||||
}
|
||||
this.refreshFromDocs();
|
||||
}
|
||||
} catch { /* runtime unavailable */ }
|
||||
}
|
||||
|
|
@ -172,8 +157,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
|
||||
private loadDemoData() {
|
||||
this.isDemo = true;
|
||||
this.workspaces = [{ slug: "rspace-dev", name: "rSpace Development", icon: "\u{1F680}", task_count: 11, member_count: 2 }];
|
||||
this.view = "board";
|
||||
this.workspaceSlug = "rspace-dev";
|
||||
this.tasks = [
|
||||
{ id: "d1", title: "Add dark mode toggle to settings page", status: "TODO", priority: "MEDIUM", labels: ["feature"], assignee: "Alice", sort_order: 0 },
|
||||
|
|
@ -204,19 +187,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
return h;
|
||||
}
|
||||
|
||||
private async loadWorkspaces() {
|
||||
try {
|
||||
const base = this.getApiBase();
|
||||
let res = await fetch(`${base}/api/spaces`, { headers: this.authHeaders() });
|
||||
// If 401 with stale token, retry without auth
|
||||
if (res.status === 401 && localStorage.getItem("encryptid-token")) {
|
||||
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 {
|
||||
|
|
@ -327,7 +297,7 @@ class FolkTasksBoard extends HTMLElement {
|
|||
this.error = 'Import failed';
|
||||
this._cuStep = 'list';
|
||||
}
|
||||
this.loadWorkspaces();
|
||||
this.loadTasks();
|
||||
this.render();
|
||||
}
|
||||
|
||||
|
|
@ -443,33 +413,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
return '';
|
||||
}
|
||||
|
||||
private async createWorkspace(name: string) {
|
||||
if (!name?.trim()) return;
|
||||
if (this.isDemo) {
|
||||
const slug = name.trim().toLowerCase().replace(/\s+/g, "-");
|
||||
this.workspaces.push({ slug, name: name.trim(), icon: "\u{1F4CB}", task_count: 0, member_count: 1 });
|
||||
this._showCreateWs = false;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const base = this.getApiBase();
|
||||
const res = await fetch(`${base}/api/spaces`, {
|
||||
method: "POST",
|
||||
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({ name: name.trim() }),
|
||||
});
|
||||
if (res.ok) {
|
||||
this._showCreateWs = false;
|
||||
this.loadWorkspaces();
|
||||
} else {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
this.error = data.error || `Failed to create workspace (${res.status})`;
|
||||
this.render();
|
||||
}
|
||||
} catch { this.error = "Failed to create workspace"; this.render(); }
|
||||
}
|
||||
|
||||
private async submitCreateTask(title: string, priority: string, description: string, dueDate?: string) {
|
||||
if (!title.trim()) return;
|
||||
if (this.isDemo) {
|
||||
|
|
@ -686,20 +629,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
return { sortOrder: ordinal, rebalanceUpdates };
|
||||
}
|
||||
|
||||
private openBoard(slug: string) {
|
||||
this.workspaceSlug = slug;
|
||||
this.view = "board";
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
private goBack() {
|
||||
const prev = this._history.back();
|
||||
if (!prev) return;
|
||||
this.view = prev.view;
|
||||
if (prev.view === "list") this.loadWorkspaces();
|
||||
else this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.shadow.innerHTML = `
|
||||
<style>
|
||||
|
|
@ -714,15 +643,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
.rapp-nav__btn { padding: 6px 14px; border-radius: 6px; border: none; background: var(--rs-primary); color: #fff; font-weight: 600; cursor: pointer; font-size: 13px; }
|
||||
.rapp-nav__btn:hover { background: var(--rs-primary-hover); }
|
||||
|
||||
.workspace-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
|
||||
.workspace-card {
|
||||
background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong); border-radius: 10px;
|
||||
padding: 16px; cursor: pointer; transition: border-color 0.2s;
|
||||
}
|
||||
.workspace-card:hover { border-color: var(--rs-border-strong); }
|
||||
.ws-name { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
|
||||
.ws-meta { font-size: 12px; color: var(--rs-text-muted); }
|
||||
|
||||
.board { display: flex; gap: 12px; overflow-x: auto; min-height: 400px; padding-bottom: 12px; }
|
||||
.column {
|
||||
min-width: 240px; max-width: 280px; flex-shrink: 0;
|
||||
|
|
@ -893,15 +813,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
.col-editor__add input { flex: 1; padding: 6px 8px; border-radius: 6px; border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface-sunken); color: var(--rs-text-primary); font-size: 12px; outline: none; }
|
||||
.col-editor__add button { padding: 6px 14px; border-radius: 6px; border: none; background: var(--rs-primary); color: #fff; cursor: pointer; font-size: 12px; font-weight: 600; }
|
||||
|
||||
/* Inline workspace create form */
|
||||
.ws-create-form { background: var(--rs-bg-surface); border: 1px solid var(--rs-primary); border-radius: 10px; padding: 14px; margin-bottom: 12px; display: flex; gap: 8px; align-items: center; }
|
||||
.ws-create-form input { flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface-sunken); color: var(--rs-text-primary); font-size: 14px; outline: none; }
|
||||
.ws-create-form input:focus { border-color: var(--rs-primary-hover); }
|
||||
.ws-create-form button { padding: 8px 16px; border-radius: 6px; border: none; font-size: 13px; cursor: pointer; font-weight: 600; }
|
||||
.ws-create-submit { background: var(--rs-primary); color: #fff; }
|
||||
.ws-create-submit:hover { background: var(--rs-primary-hover); }
|
||||
.ws-create-cancel { background: transparent; color: var(--rs-text-muted); border: 1px solid var(--rs-border-strong) !important; }
|
||||
|
||||
/* Inline delete confirmation */
|
||||
.confirm-delete { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #3b1111; border-radius: 6px; margin-top: 6px; }
|
||||
.confirm-delete span { font-size: 12px; color: #f87171; }
|
||||
|
|
@ -916,7 +827,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
@media (max-width: 768px) {
|
||||
.board { flex-direction: column; overflow-x: visible; }
|
||||
.column { min-width: 100%; max-width: 100%; }
|
||||
.workspace-grid { grid-template-columns: 1fr; }
|
||||
.detail-panel { width: 100vw; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
|
|
@ -937,7 +847,7 @@ class FolkTasksBoard extends HTMLElement {
|
|||
<span>📋 Backlog task: <strong>${this.esc(this._backlogTaskId)}</strong></span>
|
||||
<button class="backlog-dismiss" data-dismiss-backlog>✕</button>
|
||||
</div>` : ""}
|
||||
${this.view === "list" ? this.renderList() : this.renderBoard()}
|
||||
${this.renderBoard()}
|
||||
`;
|
||||
this.attachListeners();
|
||||
this._tour.renderOverlay();
|
||||
|
|
@ -947,36 +857,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
this._tour.start();
|
||||
}
|
||||
|
||||
private renderList(): string {
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<span class="rapp-nav__title">Workspaces</span>
|
||||
${!this.isDemo ? `<button class="cu-connect-btn" id="cu-toggle-panel">${this._cuConnected ? 'CU Connected' : 'Connect ClickUp'}</button>` : ''}
|
||||
<button class="rapp-nav__btn" id="create-ws">+ New Workspace</button>
|
||||
<button class="rapp-nav__btn" id="btn-tour" style="font-size:0.78rem;padding:4px 10px;opacity:0.7">Tour</button>
|
||||
</div>
|
||||
${this._showCreateWs ? `
|
||||
<div class="ws-create-form">
|
||||
<input type="text" id="ws-name-input" placeholder="Workspace name..." autofocus>
|
||||
<button class="ws-create-submit" id="ws-create-submit">Create</button>
|
||||
<button class="ws-create-cancel" id="ws-create-cancel">Cancel</button>
|
||||
</div>
|
||||
` : ''}
|
||||
${this.renderClickUpPanel()}
|
||||
${this.workspaces.length > 0 ? `<div class="workspace-grid">
|
||||
${this.workspaces.map(ws => `
|
||||
<div class="workspace-card" data-ws="${ws.slug}">
|
||||
<div class="ws-name">${this.esc(ws.icon || "📋")} ${this.esc(ws.name)}</div>
|
||||
<div class="ws-meta">${ws.task_count || 0} tasks · ${ws.member_count || 0} members</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>` : `<div class="empty">
|
||||
<p style="font-size:16px;margin-bottom:8px">No workspaces yet</p>
|
||||
<p style="font-size:13px">Create a workspace to start managing tasks</p>
|
||||
</div>`}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCreateForm(): string {
|
||||
if (!this.showCreateForm) return "";
|
||||
return `
|
||||
|
|
@ -1006,9 +886,9 @@ class FolkTasksBoard extends HTMLElement {
|
|||
const hasFilters = !!(this._searchQuery || this._filterPriority || this._filterLabel);
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
${this._history.canGoBack ? '<button class="rapp-nav__back" data-back="list">\u2190 Workspaces</button>' : ''}
|
||||
<span class="rapp-nav__title">${this.esc(this.workspaceSlug)}</span>
|
||||
${cuSyncInfo}
|
||||
${!this.isDemo ? `<button class="cu-connect-btn" id="cu-toggle-panel">${this._cuConnected ? 'CU Connected' : 'Connect ClickUp'}</button>` : ''}
|
||||
<button class="gear-btn" id="toggle-col-editor" title="Manage columns">⚙</button>
|
||||
<div class="view-toggle">
|
||||
<button class="view-toggle__btn ${this.boardView === 'board' ? 'active' : ''}" data-set-view="board">Board</button>
|
||||
|
|
@ -1025,6 +905,7 @@ class FolkTasksBoard extends HTMLElement {
|
|||
${hasFilters ? '<button class="filter-bar__clear" id="filter-clear">Clear</button>' : ''}
|
||||
<span class="filter-bar__count">${filtered.length} of ${this.tasks.length} tasks</span>
|
||||
</div>
|
||||
${this.renderClickUpPanel()}
|
||||
${this._showColumnEditor ? this.renderColumnEditor() : ''}
|
||||
${this.boardView === "checklist" ? this.renderChecklist() : this.renderKanban()}
|
||||
${this.detailTaskId ? this.renderDetailPanel() : ''}
|
||||
|
|
@ -1222,26 +1103,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
window.history.replaceState({}, "", url.toString());
|
||||
this.render();
|
||||
});
|
||||
this.shadow.querySelector("#btn-tour")?.addEventListener("click", () => this.startTour());
|
||||
this.shadow.getElementById("create-ws")?.addEventListener("click", () => {
|
||||
this._showCreateWs = !this._showCreateWs;
|
||||
this.render();
|
||||
if (this._showCreateWs) setTimeout(() => this.shadow.getElementById("ws-name-input")?.focus(), 0);
|
||||
});
|
||||
this.shadow.getElementById("ws-create-submit")?.addEventListener("click", () => {
|
||||
const input = this.shadow.getElementById("ws-name-input") as HTMLInputElement;
|
||||
if (input?.value?.trim()) this.createWorkspace(input.value.trim());
|
||||
});
|
||||
this.shadow.getElementById("ws-name-input")?.addEventListener("keydown", (e) => {
|
||||
if ((e as KeyboardEvent).key === "Enter") {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.value?.trim()) this.createWorkspace(input.value.trim());
|
||||
}
|
||||
if ((e as KeyboardEvent).key === "Escape") { this._showCreateWs = false; this.render(); }
|
||||
});
|
||||
this.shadow.getElementById("ws-create-cancel")?.addEventListener("click", () => {
|
||||
this._showCreateWs = false; this.render();
|
||||
});
|
||||
this.shadow.getElementById("create-task")?.addEventListener("click", () => {
|
||||
this.showCreateForm = !this.showCreateForm;
|
||||
this.render();
|
||||
|
|
@ -1303,16 +1164,6 @@ class FolkTasksBoard extends HTMLElement {
|
|||
});
|
||||
});
|
||||
|
||||
this.shadow.querySelectorAll("[data-ws]").forEach(el => {
|
||||
el.addEventListener("click", () => {
|
||||
this._history.push("list");
|
||||
this._history.push("board", { ws: (el as HTMLElement).dataset.ws });
|
||||
this.openBoard((el as HTMLElement).dataset.ws!);
|
||||
});
|
||||
});
|
||||
this.shadow.querySelectorAll("[data-back]").forEach(el => {
|
||||
el.addEventListener("click", () => this.goBack());
|
||||
});
|
||||
this.shadow.querySelectorAll("[data-move]").forEach(el => {
|
||||
el.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -1582,13 +1433,10 @@ class FolkTasksBoard extends HTMLElement {
|
|||
this._cuEnableSync = (e.target as HTMLInputElement).checked;
|
||||
});
|
||||
this.shadow.getElementById("cu-open-board")?.addEventListener("click", () => {
|
||||
const board = (this.shadow.getElementById("cu-open-board") as HTMLElement)?.dataset.cuBoard;
|
||||
if (board) {
|
||||
this._cuShowPanel = false;
|
||||
this._cuStep = 'token';
|
||||
this._cuImportResult = null;
|
||||
this.openBoard(board);
|
||||
}
|
||||
this._cuShowPanel = false;
|
||||
this._cuStep = 'token';
|
||||
this._cuImportResult = null;
|
||||
this.loadTasks();
|
||||
});
|
||||
|
||||
// Pointer events drag-and-drop on task cards (works with touch, pen, mouse)
|
||||
|
|
|
|||
Loading…
Reference in New Issue