Feat v0.3.6 demo workflows + interactive demo
Four example workflow packages proving the engine end-to-end: - Bug Report Triage: public entry, branch routing, SLA timer - Employee Onboarding: Starlark automated stages, signoff gate - Content Approval: multi-party signoff, revision cycle loop - Webhook Notifier: http.post, connections fallback, delivery logging Demo surface at /s/workflow-demo with cards, stage diagrams, Starlark viewer, API examples, active/published status, and copyable public links. Engine fixes: started_by in automated context, sla_seconds in package installer, parseSnapshotStages for wrapped/legacy formats. Platform fixes: extension SDK boot in base.html (Preact globals + boot()), admin teams paginated response extraction, workflow adoption endpoint (POST /teams/:teamId/workflows/:id/adopt), team-admin copyable link. Review pass remains on roadmap for remaining UI polish. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
27
packages/workflow-demo/README.md
Normal file
27
packages/workflow-demo/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Workflow Demo
|
||||
|
||||
Interactive demo surface for the example workflow packages.
|
||||
|
||||
## Features
|
||||
|
||||
- **Workflow cards** — one card per example workflow with description and feature badges
|
||||
- **Stage flow diagrams** — visual representation of each workflow's stage progression
|
||||
- **Starlark viewer** — collapsible code summary for Starlark-tier packages
|
||||
- **API curl examples** — collapsible section with copy-ready curl commands
|
||||
- **"Try It" buttons** — launch workflows directly (public link or team start)
|
||||
- **Install status** — shows which example workflows are currently installed
|
||||
|
||||
## Route
|
||||
|
||||
`/s/workflow-demo` — accessible to any authenticated user.
|
||||
|
||||
## Installation
|
||||
|
||||
Install alongside the example workflow packages. The demo surface detects which
|
||||
workflows are installed and shows their status accordingly.
|
||||
|
||||
```bash
|
||||
curl -X POST $BASE/api/v1/admin/packages/install \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "file=@packages/workflow-demo"
|
||||
```
|
||||
256
packages/workflow-demo/css/main.css
Normal file
256
packages/workflow-demo/css/main.css
Normal file
@@ -0,0 +1,256 @@
|
||||
/* ==========================================
|
||||
Workflow Demo Surface — Styles
|
||||
Uses the app's theme variables (--bg, --bg-surface, --text, etc.)
|
||||
defined in variables.css for automatic dark/light support.
|
||||
========================================== */
|
||||
|
||||
.surface-workflow-demo {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem 2rem;
|
||||
}
|
||||
|
||||
/* Topbar */
|
||||
.demo-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.demo-topbar h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Subtitle */
|
||||
.demo-subtitle {
|
||||
color: var(--text-2);
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.demo-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Title row */
|
||||
.card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.card-title-row h2 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
color: var(--text);
|
||||
}
|
||||
.card-icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.card-tier {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-browser { background: var(--success-dim); color: var(--success-light); }
|
||||
.badge-starlark { background: var(--accent-dim); color: var(--accent-light); }
|
||||
|
||||
/* Description */
|
||||
.card-desc {
|
||||
color: var(--text-2);
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.card-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.demo-card .badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-raised);
|
||||
color: var(--text-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.demo-card .badge-installed { background: var(--success-dim); color: var(--success-light); }
|
||||
.demo-card .badge-not-installed { background: var(--warning-dim); color: var(--warning-light); }
|
||||
.demo-card .badge-needs-publish { background: var(--danger-dim); color: var(--danger-light); }
|
||||
|
||||
/* Stage flow diagram */
|
||||
.stage-flow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-raised);
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.stage-node {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
background: var(--bg-surface);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.stage-node.stage-public {
|
||||
border-color: var(--success);
|
||||
background: var(--success-dim);
|
||||
}
|
||||
.stage-node.stage-system {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
|
||||
.stage-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.stage-meta {
|
||||
color: var(--text-3);
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
.stage-note {
|
||||
color: var(--warning-light);
|
||||
font-size: 0.6rem;
|
||||
font-style: italic;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.stage-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-3);
|
||||
padding: 0 0.15rem;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* Collapsible sections */
|
||||
.card-collapsible {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.card-collapsible summary {
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
color: var(--text);
|
||||
}
|
||||
.card-collapsible summary:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
.collapsible-content {
|
||||
padding: 0 0.75rem 0.75rem;
|
||||
}
|
||||
.collapsible-content pre {
|
||||
background: var(--bg-raised);
|
||||
color: var(--text);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.4;
|
||||
margin: 0.25rem 0;
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: auto;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.demo-card .btn {
|
||||
padding: 0.4rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
.demo-card .btn-primary {
|
||||
background: var(--accent);
|
||||
color: var(--text-on-color);
|
||||
}
|
||||
.demo-card .btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
.demo-card .btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Public link row */
|
||||
.public-link-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.public-link-row input {
|
||||
flex: 1;
|
||||
font-size: 0.7rem;
|
||||
padding: 4px 8px;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
.public-link-row .btn-copy {
|
||||
padding: 4px 10px;
|
||||
font-size: 0.7rem;
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
.public-link-row .btn-copy:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
355
packages/workflow-demo/js/main.js
Normal file
355
packages/workflow-demo/js/main.js
Normal file
@@ -0,0 +1,355 @@
|
||||
// ==========================================
|
||||
// Switchboard Core — Workflow Demo Surface
|
||||
// ==========================================
|
||||
// Interactive walkthrough of the four example workflow packages.
|
||||
// Shows workflow cards with feature badges, stage flow diagrams,
|
||||
// Starlark code viewer, and API curl examples.
|
||||
// ==========================================
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const SURFACE_ID = 'workflow-demo';
|
||||
if (window.__SURFACE__ !== SURFACE_ID) return;
|
||||
|
||||
const base = window.__BASE__ || '';
|
||||
|
||||
// ── Workflow catalog ──────────────────────────
|
||||
|
||||
const WORKFLOWS = [
|
||||
{
|
||||
slug: 'bug-report-triage',
|
||||
title: 'Bug Report Triage',
|
||||
icon: '🐛',
|
||||
description: 'Public bug report submission with severity-based routing, team assignment, and SLA timers.',
|
||||
badges: ['Public Entry', 'Branch Rules', 'SLA Timer', 'Team Assignment'],
|
||||
tier: 'browser',
|
||||
stages: [
|
||||
{ name: 'submit', mode: 'form', audience: 'public' },
|
||||
{ name: 'classify', mode: 'form', audience: 'team', note: 'branch rules' },
|
||||
{ name: 'fix-critical', mode: 'form', audience: 'team', note: 'SLA 1h', branch: true },
|
||||
{ name: 'fix-normal', mode: 'form', audience: 'team', branch: true },
|
||||
{ name: 'verify', mode: 'review', audience: 'team' },
|
||||
],
|
||||
curl: [
|
||||
'# 1. Start public instance (no auth required)\ncurl -X POST $BASE/api/v1/public/workflows/$WF_ID/start \\\n -H "Content-Type: application/json" \\\n -d \'{"data": {"reporter_email": "user@example.com", "summary": "Login broken", "component": "ui", "severity": "critical", "steps": "1. Click login", "expected": "Login page", "actual": "500 error"}}\'',
|
||||
'# 2. Resume (check status)\ncurl $BASE/api/v1/public/workflows/resume/$TOKEN',
|
||||
'# 3. Team member claims classify assignment\ncurl -X POST $BASE/api/v1/teams/$TEAM/assignments/$ASSIGN_ID/claim \\\n -H "Authorization: Bearer $TOKEN"',
|
||||
'# 4. Advance classify (triggers branch rule)\ncurl -X POST $BASE/api/v1/teams/$TEAM/instances/$INST/advance \\\n -H "Authorization: Bearer $TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{"data": {"severity": "critical", "notes": "Confirmed production issue"}}\'',
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'employee-onboarding',
|
||||
title: 'Employee Onboarding',
|
||||
icon: '🏢',
|
||||
description: 'Automated provisioning with Starlark hooks, manager signoff gate, and welcome notification.',
|
||||
badges: ['Starlark', 'Signoff Gate', 'Rejection Reroute', 'db.insert', 'notifications.send'],
|
||||
tier: 'starlark',
|
||||
stages: [
|
||||
{ name: 'new-hire-info', mode: 'form', audience: 'team' },
|
||||
{ name: 'provision-accounts', mode: 'automated', audience: 'system', note: 'Starlark' },
|
||||
{ name: 'manager-signoff', mode: 'review', audience: 'team', note: 'signoff gate' },
|
||||
{ name: 'it-setup', mode: 'form', audience: 'team' },
|
||||
{ name: 'welcome', mode: 'automated', audience: 'system', note: 'Starlark' },
|
||||
],
|
||||
starlark: 'on_provision(ctx): db.insert("provisions", ...), notifications.send(...)\non_welcome(ctx): db.insert("onboarding_log", ...), notifications.send(...)',
|
||||
curl: [
|
||||
'# 1. Start instance (team auth required)\ncurl -X POST $BASE/api/v1/teams/$TEAM/workflows/$WF_ID/start \\\n -H "Authorization: Bearer $TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{"data": {"full_name": "Alice Smith", "department": "engineering", "needs_vpn": "true", "needs_admin": "false", "equipment": "developer workstation"}}\'',
|
||||
'# 2. After auto-provision, submit manager signoff\ncurl -X POST $BASE/api/v1/instances/$INST/signoffs \\\n -H "Authorization: Bearer $MANAGER_TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{"action": "approve"}\'',
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'content-approval',
|
||||
title: 'Content Approval',
|
||||
icon: '📝',
|
||||
description: 'Multi-party review with quorum signoff (2 approvals) and revision cycle loop.',
|
||||
badges: ['Multi-party Signoff', 'Quorum', 'Rejection Reroute', 'Review Cycle'],
|
||||
tier: 'browser',
|
||||
stages: [
|
||||
{ name: 'draft', mode: 'form', audience: 'team' },
|
||||
{ name: 'review', mode: 'review', audience: 'team', note: '2 approvals' },
|
||||
{ name: 'revision', mode: 'form', audience: 'team', note: 'loops to review' },
|
||||
{ name: 'publish', mode: 'form', audience: 'team' },
|
||||
],
|
||||
curl: [
|
||||
'# 1. Submit draft\ncurl -X POST $BASE/api/v1/teams/$TEAM/workflows/$WF_ID/start \\\n -H "Authorization: Bearer $TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{"data": {"title": "Q1 Update", "body": "...", "category": "blog"}}\'',
|
||||
'# 2. Reviewer A approves\ncurl -X POST $BASE/api/v1/instances/$INST/signoffs \\\n -H "Authorization: Bearer $REVIEWER_A" \\\n -d \'{"action": "approve"}\'',
|
||||
'# 3. Reviewer B rejects -> routes to revision\ncurl -X POST $BASE/api/v1/instances/$INST/signoffs \\\n -H "Authorization: Bearer $REVIEWER_B" \\\n -d \'{"action": "reject"}\'',
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'webhook-notifier',
|
||||
title: 'Webhook Notifier',
|
||||
icon: '🔔',
|
||||
description: 'Outbound HTTP delivery via Starlark with connection credentials and delivery logging.',
|
||||
badges: ['Starlark', 'http.post', 'connections.get', 'db.insert'],
|
||||
tier: 'starlark',
|
||||
stages: [
|
||||
{ name: 'configure', mode: 'form', audience: 'team' },
|
||||
{ name: 'fire-webhook', mode: 'automated', audience: 'system', note: 'http.post' },
|
||||
{ name: 'result', mode: 'review', audience: 'team' },
|
||||
],
|
||||
starlark: 'on_fire(ctx): http.post(url, body=payload), db.insert("webhook_log", ...)',
|
||||
curl: [
|
||||
'# 1. Configure and start\ncurl -X POST $BASE/api/v1/teams/$TEAM/workflows/$WF_ID/start \\\n -H "Authorization: Bearer $TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{"data": {"webhook_url": "https://webhook.site/test", "event_name": "deploy.complete", "payload": "{}"}}\'',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Init ─────────────────────────────────────
|
||||
|
||||
async function _init() {
|
||||
const mount = document.getElementById('extension-mount');
|
||||
if (!mount) return;
|
||||
|
||||
const surface = document.createElement('div');
|
||||
surface.className = 'surface-workflow-demo';
|
||||
mount.appendChild(surface);
|
||||
|
||||
// Topbar with user menu
|
||||
const topbar = document.createElement('div');
|
||||
topbar.className = 'demo-topbar';
|
||||
topbar.innerHTML = '<h1>Workflow Demo</h1>';
|
||||
surface.appendChild(topbar);
|
||||
sw.userMenu(topbar, { flyout: 'down' });
|
||||
|
||||
// Subtitle
|
||||
const subtitle = document.createElement('p');
|
||||
subtitle.className = 'demo-subtitle';
|
||||
subtitle.textContent = 'Four example workflows that prove the engine works end-to-end. Install any package via Admin > Packages.';
|
||||
surface.appendChild(subtitle);
|
||||
|
||||
// Workflow cards
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'demo-grid';
|
||||
surface.appendChild(grid);
|
||||
|
||||
// Fetch installed workflows to check status
|
||||
// The API returns { data: [...], total, page } — _unwrap keeps the
|
||||
// full object when siblings exist, so extract .data ourselves.
|
||||
let installed = {};
|
||||
try {
|
||||
const resp = await sw.api.get('/api/v1/workflows');
|
||||
const list = Array.isArray(resp) ? resp : (resp && resp.data ? resp.data : []);
|
||||
for (const wf of list) {
|
||||
if (wf.slug) installed[wf.slug] = wf;
|
||||
}
|
||||
} catch (e) { /* ignore — show all as not-installed */ }
|
||||
|
||||
for (const wf of WORKFLOWS) {
|
||||
grid.appendChild(_buildCard(wf, installed[wf.slug]));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Card builder ─────────────────────────────
|
||||
|
||||
function _buildCard(wf, installedWf) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'demo-card';
|
||||
|
||||
// Title row
|
||||
const titleRow = document.createElement('div');
|
||||
titleRow.className = 'card-title-row';
|
||||
titleRow.innerHTML = '<span class="card-icon">' + wf.icon + '</span>'
|
||||
+ '<h2>' + _esc(wf.title) + '</h2>'
|
||||
+ '<span class="card-tier badge-' + wf.tier + '">' + wf.tier + '</span>';
|
||||
card.appendChild(titleRow);
|
||||
|
||||
// Description
|
||||
const desc = document.createElement('p');
|
||||
desc.className = 'card-desc';
|
||||
desc.textContent = wf.description;
|
||||
card.appendChild(desc);
|
||||
|
||||
// Badges
|
||||
const badges = document.createElement('div');
|
||||
badges.className = 'card-badges';
|
||||
for (const b of wf.badges) {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'badge';
|
||||
span.textContent = b;
|
||||
badges.appendChild(span);
|
||||
}
|
||||
card.appendChild(badges);
|
||||
|
||||
// Stage flow diagram
|
||||
card.appendChild(_buildStageDiagram(wf));
|
||||
|
||||
// Starlark viewer (if applicable)
|
||||
if (wf.starlark) {
|
||||
card.appendChild(_buildCollapsible('Starlark Hooks', '<pre><code>' + _esc(wf.starlark) + '</code></pre>'));
|
||||
}
|
||||
|
||||
// API examples
|
||||
if (wf.curl && wf.curl.length) {
|
||||
const curlHtml = wf.curl.map(function (c) { return '<pre><code>' + _esc(c) + '</code></pre>'; }).join('');
|
||||
card.appendChild(_buildCollapsible('API Examples', curlHtml));
|
||||
}
|
||||
|
||||
// Status + action
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'card-actions';
|
||||
|
||||
if (installedWf) {
|
||||
const isActive = installedWf.is_active;
|
||||
const version = installedWf.version || 0;
|
||||
// version > 1 means published at least once (version increments on edit, publish snapshots it)
|
||||
const isPublished = version >= 2;
|
||||
const isReady = isActive && isPublished;
|
||||
|
||||
// Installed badge
|
||||
const status = document.createElement('span');
|
||||
status.className = 'badge badge-installed';
|
||||
status.textContent = 'Installed';
|
||||
actions.appendChild(status);
|
||||
|
||||
if (!isActive) {
|
||||
const hint = document.createElement('span');
|
||||
hint.className = 'badge badge-needs-publish';
|
||||
hint.textContent = 'Not Active';
|
||||
actions.appendChild(hint);
|
||||
} else if (!isPublished) {
|
||||
const hint = document.createElement('span');
|
||||
hint.className = 'badge badge-needs-publish';
|
||||
hint.textContent = 'Not Published';
|
||||
actions.appendChild(hint);
|
||||
}
|
||||
|
||||
const tryBtn = document.createElement('button');
|
||||
tryBtn.className = 'btn btn-primary';
|
||||
tryBtn.textContent = 'Try It';
|
||||
tryBtn.disabled = !isReady;
|
||||
if (!isReady) {
|
||||
tryBtn.title = 'Activate and publish this workflow in Team Admin first';
|
||||
}
|
||||
tryBtn.onclick = function () {
|
||||
if (!isReady) return;
|
||||
if (wf.slug === 'bug-report-triage') {
|
||||
window.open(base + '/api/v1/public/workflows/' + installedWf.id + '/form', '_blank');
|
||||
} else {
|
||||
sw.toast('Navigate to your team admin to start a ' + wf.title + ' instance.', 'info');
|
||||
}
|
||||
};
|
||||
actions.appendChild(tryBtn);
|
||||
|
||||
// Public link for public_link workflows
|
||||
if (installedWf.entry_mode === 'public_link' && isReady) {
|
||||
const linkRow = document.createElement('div');
|
||||
linkRow.className = 'public-link-row';
|
||||
const linkInput = document.createElement('input');
|
||||
linkInput.type = 'text';
|
||||
linkInput.readOnly = true;
|
||||
linkInput.value = window.location.origin + base + '/api/v1/public/workflows/' + installedWf.id + '/start';
|
||||
linkRow.appendChild(linkInput);
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'btn-copy';
|
||||
copyBtn.textContent = 'Copy';
|
||||
copyBtn.onclick = function () {
|
||||
navigator.clipboard.writeText(linkInput.value).then(function () {
|
||||
copyBtn.textContent = 'Copied!';
|
||||
setTimeout(function () { copyBtn.textContent = 'Copy'; }, 1500);
|
||||
});
|
||||
};
|
||||
linkRow.appendChild(copyBtn);
|
||||
card.appendChild(linkRow);
|
||||
}
|
||||
} else {
|
||||
const status = document.createElement('span');
|
||||
status.className = 'badge badge-not-installed';
|
||||
status.textContent = 'Not Installed';
|
||||
actions.appendChild(status);
|
||||
}
|
||||
|
||||
card.appendChild(actions);
|
||||
return card;
|
||||
}
|
||||
|
||||
// ── Stage flow diagram ───────────────────────
|
||||
|
||||
function _buildStageDiagram(wf) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'stage-flow';
|
||||
|
||||
const stages = wf.stages;
|
||||
let hasBranch = false;
|
||||
|
||||
for (let i = 0; i < stages.length; i++) {
|
||||
const s = stages[i];
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.className = 'stage-node stage-' + s.audience;
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'stage-label';
|
||||
label.textContent = s.name;
|
||||
node.appendChild(label);
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'stage-meta';
|
||||
meta.textContent = s.mode + (s.audience !== 'team' ? ' (' + s.audience + ')' : '');
|
||||
node.appendChild(meta);
|
||||
|
||||
if (s.note) {
|
||||
const note = document.createElement('div');
|
||||
note.className = 'stage-note';
|
||||
note.textContent = s.note;
|
||||
node.appendChild(note);
|
||||
}
|
||||
|
||||
container.appendChild(node);
|
||||
|
||||
// Arrow (except after last stage)
|
||||
if (i < stages.length - 1 && !stages[i + 1].branch) {
|
||||
const arrow = document.createElement('div');
|
||||
arrow.className = 'stage-arrow';
|
||||
arrow.textContent = '\u2192';
|
||||
container.appendChild(arrow);
|
||||
} else if (stages[i + 1] && stages[i + 1].branch) {
|
||||
if (!hasBranch) {
|
||||
const split = document.createElement('div');
|
||||
split.className = 'stage-arrow stage-branch';
|
||||
split.textContent = '\u2193\u2197';
|
||||
container.appendChild(split);
|
||||
hasBranch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// ── Collapsible section ──────────────────────
|
||||
|
||||
function _buildCollapsible(title, innerHtml) {
|
||||
const details = document.createElement('details');
|
||||
details.className = 'card-collapsible';
|
||||
|
||||
const summary = document.createElement('summary');
|
||||
summary.textContent = title;
|
||||
details.appendChild(summary);
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'collapsible-content';
|
||||
content.innerHTML = innerHtml;
|
||||
details.appendChild(content);
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────
|
||||
|
||||
function _esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// ── Boot ──────────────────────────────────────
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', _init);
|
||||
} else {
|
||||
_init();
|
||||
}
|
||||
|
||||
})();
|
||||
15
packages/workflow-demo/manifest.json
Normal file
15
packages/workflow-demo/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "workflow-demo",
|
||||
"title": "Workflow Demo",
|
||||
"type": "full",
|
||||
"version": "0.1.0",
|
||||
"tier": "browser",
|
||||
"icon": "🎓",
|
||||
"author": "Switchboard Core",
|
||||
"description": "Interactive demo surface for example workflow packages. Cards, stage diagrams, and API walkthroughs.",
|
||||
"route": "/s/workflow-demo",
|
||||
"auth": "authenticated",
|
||||
"layout": "single",
|
||||
"permissions": [],
|
||||
"hooks": ["surface"]
|
||||
}
|
||||
Reference in New Issue
Block a user