Changeset 0.37.15 (#227)

This commit is contained in:
2026-03-23 19:58:17 +00:00
parent b7746c3004
commit d005e8a30f
23 changed files with 1764 additions and 170 deletions

View File

@@ -45,7 +45,7 @@ const NAV_ITEMS = [
{ key: 'personas', label: 'Personas', gate: 'personas' },
{ key: 'profile', label: 'Profile' },
{ key: 'teams', label: 'Teams' },
{ key: 'workflows', label: 'Workflows' },
{ key: 'workflows', label: 'Assignments' },
{ key: 'tasks', label: 'Tasks' },
{ key: 'gitkeys', label: 'Git Keys' },
{ key: 'knowledge', label: 'Knowledge Bases' },
@@ -64,7 +64,7 @@ const BYOK_ITEMS = [
const SECTION_TITLES = {
general: 'General', appearance: 'Appearance', models: 'Models',
personas: 'Personas', profile: 'Profile', teams: 'Teams',
workflows: 'Workflows', tasks: 'Tasks', gitkeys: 'Git Keys',
workflows: 'Assignments', tasks: 'Tasks', gitkeys: 'Git Keys',
data: 'Data & Privacy', providers: 'My Providers', roles: 'Model Roles',
usage: 'My Usage', knowledge: 'Knowledge Bases', memory: 'Memory',
notifications: 'Notifications',

View File

@@ -1,87 +1,137 @@
/**
* Settings > Workflows — read-only view of user's visible workflows
* Settings > Workflows — v0.37.15 rewrite
*
* Replaces read-only workflow list with MyAssignments (E4).
* Shows the user's personal assignment queue across all teams.
*/
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
function _timeAgo(ts) {
if (!ts) return '';
const diff = Date.now() - new Date(ts).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
return `${Math.floor(hrs / 24)}d ago`;
}
export default function WorkflowsSection() {
const [workflows, setWorkflows] = useState([]);
const [assignments, setAssignments] = useState([]);
const [loading, setLoading] = useState(true);
const [detail, setDetail] = useState(null);
const [stages, setStages] = useState([]);
const load = useCallback(async () => {
try {
const data = await sw.api.workflows.list();
setWorkflows(data || []);
const data = await sw.api.workflowAssignments.mine();
setAssignments(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, []);
async function openDetail(wf) {
setDetail(wf);
const userId = sw.auth?.user?.id;
const mine = assignments.filter(a => a.status === 'claimed' && a.assigned_to === userId);
const available = assignments.filter(a => a.status === 'unassigned');
async function claim(id) {
try {
const d = await sw.api.workflows.get(wf.id);
setStages(d.stages || []);
} catch (e) { sw.toast(e.message, 'error'); setStages([]); }
await sw.api.workflowAssignments.claim(id);
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
async function unclaim(id) {
try {
await sw.api.workflowAssignments.unclaim(id);
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
async function complete(id) {
try {
await sw.api.workflowAssignments.complete(id);
sw.toast('Assignment completed', 'success');
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
if (detail) return html`
<div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<button class="btn-small" onClick=${() => { setDetail(null); setStages([]); }}>\u2190 Back</button>
<h4 style="margin:0;">${detail.name}</h4>
<span class="badge ${detail.is_active ? 'badge-active' : 'badge-inactive'}">
${detail.is_active ? 'active' : 'inactive'}
</span>
</div>
${detail.description && html`
<p class="text-muted" style="font-size:13px;margin:0 0 12px;">${detail.description}</p>
`}
<div style="display:flex;gap:12px;margin-bottom:16px;font-size:12px;color:var(--text-3);">
<span>Slug: <strong>/${detail.slug}</strong></span>
<span>Entry: <strong>${detail.entry_mode}</strong></span>
<span>Version: <strong>${detail.version || 0}</strong></span>
</div>
<h5 style="margin:0 0 8px;">Stages (${stages.length})</h5>
<div class="admin-list">
${stages.length === 0 && html`<div class="empty-hint">No stages defined</div>`}
${stages.map((s, i) => html`
<div class="admin-surface-row" key=${s.id}>
<span class="text-muted" style="font-size:11px;width:24px;">#${i + 1}</span>
<strong style="flex:1;">${s.name || `Stage ${i + 1}`}</strong>
<span class="badge">${s.stage_mode}</span>
${s.persona_id && html`<span class="badge badge-active">persona</span>`}
</div>
`)}
</div>
</div>
`;
return html`
<div>
<div class="admin-list">
${workflows.length === 0 && html`<div class="empty-hint">No workflows available.</div>`}
${workflows.map(w => html`
<div class="admin-surface-row" key=${w.id} style="cursor:pointer;" onClick=${() => openDetail(w)}>
<div style="flex:1;min-width:0;">
<strong>${w.name}</strong>
<span class="text-muted" style="margin-left:8px;font-size:11px;">/${w.slug}</span>
</div>
<span class="badge">${w.entry_mode}</span>
<span class="badge ${w.is_active ? 'badge-active' : 'badge-inactive'}">
${w.is_active ? 'active' : 'inactive'}
</span>
<span class="text-muted" style="font-size:11px;">v${w.version || 0}</span>
</div>
`)}
${mine.length > 0 && html`
<h4 style="margin:0 0 8px;">Claimed (${mine.length})</h4>
<div class="admin-list">
${mine.map(a => html`
<${AssignmentRow} key=${a.id} assignment=${a} onAction=${load} showTeam />
`)}
</div>
`}
${available.length > 0 && html`
<h4 style="margin:${mine.length > 0 ? '16px' : '0'} 0 8px;">Available (${available.length})</h4>
<div class="admin-list">
${available.map(a => html`
<${AssignmentRow} key=${a.id} assignment=${a} onAction=${load} showTeam />
`)}
</div>
`}
${mine.length === 0 && available.length === 0 && html`
<div class="empty-hint">No assignments</div>
`}
</div>
`;
}
function AssignmentRow({ assignment: a, onAction, showTeam }) {
const userId = sw.auth?.user?.id;
const isMine = a.assigned_to === userId;
async function claim() {
try {
await sw.api.workflowAssignments.claim(a.id);
onAction();
} catch (e) { sw.toast(e.message, 'error'); }
}
async function unclaim() {
try {
await sw.api.workflowAssignments.unclaim(a.id);
onAction();
} catch (e) { sw.toast(e.message, 'error'); }
}
async function complete() {
try {
await sw.api.workflowAssignments.complete(a.id);
sw.toast('Assignment completed', 'success');
onAction();
} catch (e) { sw.toast(e.message, 'error'); }
}
return html`
<div class="admin-surface-row" style="${isMine ? 'border-left:3px solid var(--accent-1);' : ''}">
<div style="flex:1;min-width:0;">
<strong>${a.workflow_name || 'Workflow'}</strong>
${showTeam && a.team_name && html`<span class="badge">${a.team_name}</span>`}
<span class="badge">${a.stage_name || `Stage ${a.stage}`}</span>
${a.sla_breached && html`<span class="badge badge-danger">SLA</span>`}
<span class="text-muted" style="font-size:11px;">${_timeAgo(a.created_at)}</span>
</div>
<div style="display:flex;gap:4px;">
${a.status === 'unassigned' && html`
<button class="btn-small btn-primary" onClick=${claim}>Claim</button>
`}
${isMine && html`
<button class="btn-small" onClick=${unclaim}>Release</button>
<button class="btn-small btn-primary"
onClick=${() => { location.href = sw.base + '/w/' + a.channel_id; }}>Open</button>
<button class="btn-small btn-success" onClick=${complete}>Complete</button>
`}
</div>
</div>
`;