This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/packages/tasks/js/main.js
Jeffrey Smith 221ae94f4f
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 2m47s
CI/CD / build-and-deploy (push) Successful in 1m46s
Feat v0.6.12 css isolation (#47)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-01 11:58:39 +00:00

398 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tasks — Surface Entry Point (v0.1.0)
*
* Task management surface using the v0.2.3 SDK:
* sw.api.ext('tasks') — scoped API client
* sw.ui.* — primitive components
* sw.slots — statusbar widget
* sw.actions — create-task action
*/
(async function () {
'use strict';
var mount = document.getElementById('extension-mount');
if (!mount) return;
var base = window.__BASE__ || '';
var ver = window.__VERSION__ || '0';
// ── Boot SDK ───────────────────────────────
try {
if (!window.preact) {
var { h, render } = await import(base + '/js/sw/vendor/preact.module.js');
var hooksModule = await import(base + '/js/sw/vendor/hooks.module.js');
var htmModule = await import(base + '/js/sw/vendor/htm.module.js');
window.preact = { h, render };
window.hooks = hooksModule;
window.html = htmModule.default.bind(h);
}
var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver);
await sdk.boot();
} catch (e) {
mount.innerHTML = '<p style="color:var(--danger);padding:24px;">SDK boot failed: ' + e.message + '</p>';
return;
}
var { html } = window;
var { useState, useEffect, useCallback, useRef } = hooks;
var { render } = preact;
// ── SDK modules ────────────────────────────
var api = sw.api.ext('tasks');
var { Button, FormField, Dialog, Spinner, Dropdown, Tabs, Banner } = sw.ui;
// ── Constants ──────────────────────────────
var STATUSES = ['todo', 'in_progress', 'done', 'cancelled'];
var PRIORITIES = ['low', 'medium', 'high', 'urgent'];
var STATUS_LABELS = {
todo: 'To Do', in_progress: 'In Progress', done: 'Done', cancelled: 'Cancelled'
};
var PRIORITY_COLORS = {
low: 'var(--text-3)', medium: 'var(--accent)', high: 'var(--warning)', urgent: 'var(--danger)'
};
// ═══════════════════════════════════════════
// TaskForm — create / edit dialog
// ═══════════════════════════════════════════
function TaskForm({ task, onSave, onClose }) {
var [title, setTitle] = useState(task ? task.title : '');
var [desc, setDesc] = useState(task ? task.description : '');
var [status, setStatus] = useState(task ? task.status : STATUSES[0]);
var [priority, setPriority] = useState(task ? task.priority : 'medium');
var [dueDate, setDueDate] = useState(task ? task.due_date : '');
var [tags, setTags] = useState(task ? task.tags : '');
var [saving, setSaving] = useState(false);
var [error, setError] = useState('');
async function handleSubmit(e) {
e.preventDefault();
if (!title.trim()) { setError('Title is required'); return; }
setSaving(true);
setError('');
try {
var data = { title: title.trim(), description: desc, status, priority, due_date: dueDate, tags };
if (task) {
await api.put('/items/' + task.id, data);
} else {
await api.post('/items', data);
}
onSave();
} catch (err) {
setError(err.message || 'Save failed');
} finally {
setSaving(false);
}
}
return html`
<${Dialog} open onClose=${onClose} title=${task ? 'Edit Task' : 'New Task'}>
<form onSubmit=${handleSubmit} class="ext-tasks-form">
${error && html`<${Banner} variant="danger" text=${error} />`}
<${FormField} label="Title" required>
<input type="text" value=${title} onInput=${e => setTitle(e.target.value)}
placeholder="What needs to be done?" autofocus />
<//>
<${FormField} label="Description">
<textarea rows="3" value=${desc} onInput=${e => setDesc(e.target.value)}
placeholder="Details (optional)" />
<//>
<div class="ext-tasks-form__row">
<${FormField} label="Status">
<select value=${status} onChange=${e => setStatus(e.target.value)}>
${STATUSES.map(s => html`<option value=${s}>${STATUS_LABELS[s] || s}</option>`)}
</select>
<//>
<${FormField} label="Priority">
<select value=${priority} onChange=${e => setPriority(e.target.value)}>
${PRIORITIES.map(p => html`<option value=${p}>${p}</option>`)}
</select>
<//>
</div>
<div class="ext-tasks-form__row">
<${FormField} label="Due Date">
<input type="date" value=${dueDate} onInput=${e => setDueDate(e.target.value)} />
<//>
<${FormField} label="Tags">
<input type="text" value=${tags} onInput=${e => setTags(e.target.value)}
placeholder="comma-separated" />
<//>
</div>
<div class="ext-tasks-form__actions">
<${Button} variant="ghost" type="button" onClick=${onClose}>Cancel<//>
<${Button} variant="primary" type="submit" disabled=${saving}>
${saving ? 'Saving…' : (task ? 'Update' : 'Create')}
<//>
</div>
</form>
<//>
`;
}
// ═══════════════════════════════════════════
// TaskList — filterable task table
// ═══════════════════════════════════════════
function TaskList({ items, loading, onEdit, onRefresh }) {
var [filter, setFilter] = useState('all');
var filtered = items;
if (filter !== 'all') {
filtered = items.filter(function(t) { return t.status === filter; });
}
async function handleDelete(id) {
if (!await sw.confirm('Delete this task?', { destructive: true })) return;
await api.del('/items/' + id);
onRefresh();
}
async function handleQuickStatus(id, newStatus) {
await api.put('/items/' + id, { status: newStatus });
onRefresh();
}
return html`
<div class="ext-tasks-list">
<div class="ext-tasks-list__filters">
<select value=${filter} onChange=${e => setFilter(e.target.value)} class="ext-tasks-filter">
<option value="all">All</option>
${STATUSES.map(s => html`<option value=${s}>${STATUS_LABELS[s] || s}</option>`)}
</select>
<span class="ext-tasks-list__count">${filtered.length} task${filtered.length !== 1 ? 's' : ''}</span>
</div>
${loading && html`<div class="ext-tasks-list__loading"><${Spinner} size="md" /></div>`}
${!loading && filtered.length === 0 && html`
<div class="ext-tasks-list__empty">No tasks${filter !== 'all' ? ' matching filter' : ''}. Create one to get started.</div>
`}
${!loading && filtered.map(function(t) {
return html`
<div class="ext-tasks-card" key=${t.id}>
<div class="ext-tasks-card__header">
<span class="ext-tasks-card__priority" style="color:${PRIORITY_COLORS[t.priority] || 'var(--text-3)'}">
</span>
<span class="ext-tasks-card__title" onClick=${() => onEdit(t)}>${t.title}</span>
<span class="ext-tasks-card__status ext-tasks-card__status--${t.status}">
${STATUS_LABELS[t.status] || t.status}
</span>
</div>
${t.description && html`
<div class="ext-tasks-card__desc">${t.description}</div>
`}
<div class="ext-tasks-card__footer">
${t.due_date && html`<span class="ext-tasks-card__due">Due: ${t.due_date}</span>`}
${t.tags && html`<span class="ext-tasks-card__tags">${t.tags}</span>`}
<div class="ext-tasks-card__actions">
${t.status !== 'done' && html`
<button class="ext-tasks-btn ext-tasks-btn--sm" onClick=${() => handleQuickStatus(t.id, 'done')}
title="Mark done">✓</button>
`}
<button class="ext-tasks-btn ext-tasks-btn--sm ext-tasks-btn--danger" onClick=${() => handleDelete(t.id)}
title="Delete">×</button>
</div>
</div>
</div>
`;
})}
</div>
`;
}
// ═══════════════════════════════════════════
// TaskBoard — kanban columns
// ═══════════════════════════════════════════
function TaskBoard({ items, loading, onEdit, onRefresh }) {
async function handleQuickStatus(id, newStatus) {
await api.put('/items/' + id, { status: newStatus });
onRefresh();
}
return html`
<div class="ext-tasks-board">
${loading && html`<div class="ext-tasks-list__loading"><${Spinner} size="md" /></div>`}
${!loading && STATUSES.map(function(status) {
var col = items.filter(function(t) { return t.status === status; });
return html`
<div class="ext-tasks-board__col" key=${status}>
<div class="ext-tasks-board__col-header">
<span class="ext-tasks-board__col-title">${STATUS_LABELS[status] || status}</span>
<span class="ext-tasks-board__col-count">${col.length}</span>
</div>
<div class="ext-tasks-board__col-body">
${col.map(function(t) {
return html`
<div class="ext-tasks-board__card" key=${t.id} onClick=${() => onEdit(t)}>
<div class="ext-tasks-board__card-title">
<span class="ext-tasks-card__priority" style="color:${PRIORITY_COLORS[t.priority] || 'var(--text-3)'}">●</span>
${t.title}
</div>
${t.due_date && html`<div class="ext-tasks-board__card-due">Due: ${t.due_date}</div>`}
</div>
`;
})}
</div>
</div>
`;
})}
</div>
`;
}
// ═══════════════════════════════════════════
// TaskApp — root component
// ═══════════════════════════════════════════
function TaskApp() {
var [items, setItems] = useState([]);
var [loading, setLoading] = useState(true);
var [error, setError] = useState('');
var [view, setView] = useState('list');
var [editTask, setEditTask] = useState(null); // null = closed, {} = new, {id} = edit
var [stats, setStats] = useState(null);
var loadItems = useCallback(async function () {
setLoading(true);
try {
var res = await api.get('/items');
setItems(res.data || res || []);
setError('');
} catch (err) {
setError(err.message || 'Failed to load tasks');
} finally {
setLoading(false);
}
}, []);
var loadStats = useCallback(async function () {
try {
var res = await api.get('/stats');
setStats(res);
} catch (_) {}
}, []);
useEffect(function () {
loadItems();
loadStats();
}, []);
// Live updates via event bus
useEffect(function () {
var off = sw.on('task.*', function () {
loadItems();
loadStats();
});
return off;
}, []);
function handleSave() {
setEditTask(null);
loadItems();
loadStats();
}
var tabs = [
{ id: 'list', label: 'List' },
{ id: 'board', label: 'Board' },
];
var Topbar = sw.shell?.Topbar;
return html`
<div class="ext-tasks-app">
${Topbar ? html`
<${Topbar} title="Tasks">
${stats && html`<span class="ext-tasks-stats">${stats.total || 0} total</span>`}
<div class="ext-tasks-view-tabs">
${tabs.map(function(tab) {
return html`<button key=${tab.id}
class="ext-tasks-view-tab ${view === tab.id ? 'ext-tasks-view-tab--active' : ''}"
onClick=${() => setView(tab.id)}>${tab.label}</button>`;
})}
</div>
<${Button} variant="primary" size="sm" onClick=${() => setEditTask({})}>+ New Task<//>
<//>
` : html`
<div class="ext-tasks-header">
<h1 class="ext-tasks-title">Tasks</h1>
<${Button} variant="primary" size="sm" onClick=${() => setEditTask({})}>+ New Task<//>
</div>
`}
${error && html`<${Banner} variant="danger" text=${error} />`}
${view === 'list' && html`
<${TaskList} items=${items} loading=${loading}
onEdit=${setEditTask} onRefresh=${loadItems} />
`}
${view === 'board' && html`
<${TaskBoard} items=${items} loading=${loading}
onEdit=${setEditTask} onRefresh=${loadItems} />
`}
${editTask !== null && html`
<${TaskForm} task=${editTask.id ? editTask : null}
onSave=${handleSave}
onClose=${() => setEditTask(null)} />
`}
</div>
`;
}
// ═══════════════════════════════════════════
// Actions & Slots registration
// ═══════════════════════════════════════════
// Register create-task action
sw.actions.register('tasks.create', {
label: 'Create Task',
handler: function () {
// Emit event that the app listens to
sw.emit('tasks.open-create', {}, { localOnly: true });
},
});
// Statusbar widget
function TaskStatusWidget() {
var [count, setCount] = useState(null);
useEffect(function () {
async function load() {
try {
var res = await api.get('/stats');
var open = (res.counts || {}).todo || 0;
var inProg = (res.counts || {}).in_progress || 0;
setCount(open + inProg);
} catch (_) {}
}
load();
var off = sw.on('task.*', load);
return off;
}, []);
if (count === null) return null;
return html`<span class="ext-tasks-status-widget" title="Open tasks">${count} open task${count !== 1 ? 's' : ''}</span>`;
}
sw.slots.register('statusbar', {
id: 'tasks-open-count',
component: TaskStatusWidget,
priority: 50,
});
// ── Mount ──────────────────────────────────
render(html`<${TaskApp} />`, mount);
})();