/** * — Flat backlog list widget for rTasks canvas. * * Shows all tasks as a unified, filterable list (cross-status), sorted by * priority then due date. Complements (column view) by * offering a single sorted stream. * * Attribute: space — space slug */ interface TaskRow { id: string; title: string; description: string; status: string; priority: string | null; labels: string[]; due_date: string | null; created_at: string; updated_at: string; } class FolkTasksBacklog extends HTMLElement { private shadow: ShadowRoot; private space = ''; private tasks: TaskRow[] = []; private loading = true; private error = ''; private query = ''; private priorityFilter: '' | 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT' = ''; constructor() { super(); this.shadow = this.attachShadow({ mode: 'open' }); } connectedCallback() { this.space = this.getAttribute('space') || 'demo'; this.render(); this.load(); } static get observedAttributes() { return ['space']; } attributeChangedCallback(name: string, _old: string, val: string) { if (name === 'space' && val !== this.space) { this.space = val; this.load(); } } private getApiBase(): string { const path = window.location.pathname; const match = path.match(/^(\/[^/]+)?\/rtasks/); return match ? match[0] : '/rtasks'; } private async load() { this.loading = true; this.error = ''; this.render(); try { const res = await fetch(`${this.getApiBase()}/api/spaces/${encodeURIComponent(this.space)}/tasks`); if (!res.ok) throw new Error(`HTTP ${res.status}`); this.tasks = await res.json(); } catch (e) { this.error = e instanceof Error ? e.message : String(e); } this.loading = false; this.render(); } private filtered(): TaskRow[] { const q = this.query.trim().toLowerCase(); const prio = this.priorityFilter; const priOrder: Record = { URGENT: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }; return this.tasks .filter(t => !q || t.title.toLowerCase().includes(q) || (t.description || '').toLowerCase().includes(q)) .filter(t => !prio || t.priority === prio) .sort((a, b) => { const pa = priOrder[a.priority || 'LOW'] ?? 4; const pb = priOrder[b.priority || 'LOW'] ?? 4; if (pa !== pb) return pa - pb; const da = a.due_date ? Date.parse(a.due_date) : Number.MAX_SAFE_INTEGER; const db = b.due_date ? Date.parse(b.due_date) : Number.MAX_SAFE_INTEGER; return da - db; }); } private render() { const items = this.filtered(); this.shadow.innerHTML = `
${items.length} / ${this.tasks.length}
${this.loading ? '
Loading…
' : this.error ? `
${this.esc(this.error)}
` : items.length === 0 ? '
No matching tasks.
' : items.map(t => this.renderRow(t)).join('')}
`; this.bindEvents(); } private renderRow(t: TaskRow): string { const due = t.due_date ? new Date(t.due_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : ''; const prioClass = `prio-${(t.priority || 'LOW').toLowerCase()}`; const statusCls = `status-${t.status.toLowerCase().replace(/_/g, '-')}`; return `
${this.esc(t.title)}
${this.esc(t.status)} ${due ? `${this.esc(due)}` : ''} ${t.labels.slice(0, 3).map(l => `${this.esc(l)}`).join('')}
`; } private bindEvents() { this.shadow.querySelector('.search')?.addEventListener('input', (e) => { this.query = (e.target as HTMLInputElement).value; this.render(); }); this.shadow.querySelector('.prio')?.addEventListener('change', (e) => { this.priorityFilter = (e.target as HTMLSelectElement).value as any; this.render(); }); this.shadow.querySelectorAll('.row').forEach(el => { el.addEventListener('click', () => { const id = (el as HTMLElement).dataset.taskId; if (!id) return; this.dispatchEvent(new CustomEvent('task-selected', { bubbles: true, composed: true, detail: { taskId: id } })); }); }); } private getStyles(): string { return ` :host { display: block; height: 100%; } .backlog { display: flex; flex-direction: column; height: 100%; min-height: 0; } .controls { display: flex; gap: 0.5rem; align-items: center; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--rs-border); flex-shrink: 0; } .search, .prio { padding: 0.375rem 0.5rem; border: 1px solid var(--rs-border); border-radius: 6px; background: var(--rs-input-bg, var(--rs-bg-surface)); color: var(--rs-text-primary); font-size: 0.8125rem; } .search { flex: 1; min-width: 0; } .count { font-size: 0.75rem; color: var(--rs-text-secondary); flex-shrink: 0; } .list { flex: 1; overflow-y: auto; padding: 0.5rem 0; min-height: 0; } .state { text-align: center; padding: 1.5rem 1rem; color: var(--rs-text-secondary); font-size: 0.875rem; } .state.err { color: #f87171; } .row { display: flex; align-items: flex-start; gap: 0.625rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--rs-border); cursor: pointer; transition: background 0.1s; } .row:hover { background: var(--rs-bg-hover); } .prio-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 0.375rem; flex-shrink: 0; background: var(--rs-border); } .prio-urgent { background: #ef4444; } .prio-high { background: #f97316; } .prio-medium { background: #eab308; } .prio-low { background: #60a5fa; } .row-main { flex: 1; min-width: 0; } .row-title { font-size: 0.875rem; color: var(--rs-text-primary); font-weight: 500; overflow: hidden; text-overflow: ellipsis; } .row-meta { display: flex; flex-wrap: wrap; gap: 0.375rem; margin-top: 0.25rem; align-items: center; } .status { font-size: 0.6875rem; font-weight: 600; padding: 0.0625rem 0.375rem; border-radius: 4px; background: var(--rs-bg-hover); color: var(--rs-text-secondary); text-transform: uppercase; letter-spacing: 0.03em; } .status-done { background: rgba(34,197,94,0.15); color: #16a34a; } .status-in-progress { background: rgba(59,130,246,0.15); color: #2563eb; } .status-review { background: rgba(168,85,247,0.15); color: #9333ea; } .due { font-size: 0.75rem; color: var(--rs-text-muted); } .label { font-size: 0.6875rem; color: var(--rs-text-secondary); background: var(--rs-bg-hover); padding: 0.0625rem 0.375rem; border-radius: 4px; } `; } private esc(s: string): string { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } } if (!customElements.get('folk-tasks-backlog')) customElements.define('folk-tasks-backlog', FolkTasksBacklog); export {};