/** * — Campaign workflow gallery grid. * * Fetches all campaign workflows and renders them as cards with miniature * SVG previews of the node graph. Click a card to open the workflow editor. * * Attributes: * space — space slug (default "demo") */ import { CAMPAIGN_NODE_CATALOG } from '../schemas'; import type { CampaignWorkflowNodeDef, CampaignWorkflowNodeCategory, CampaignWorkflowNode, CampaignWorkflowEdge, CampaignWorkflow, } from '../schemas'; // ── Constants (match folk-campaign-workflow.ts) ── const NODE_WIDTH = 220; const NODE_HEIGHT = 80; const CATEGORY_COLORS: Record = { trigger: '#3b82f6', delay: '#a855f7', condition: '#f59e0b', action: '#10b981', }; function getNodeDef(type: string): CampaignWorkflowNodeDef | undefined { return CAMPAIGN_NODE_CATALOG.find(n => n.type === type); } function getPortY(node: CampaignWorkflowNode, portName: string, direction: 'input' | 'output'): number { const def = getNodeDef(node.type); if (!def) return node.position.y + NODE_HEIGHT / 2; const ports = direction === 'input' ? def.inputs : def.outputs; const idx = ports.findIndex(p => p.name === portName); if (idx === -1) return node.position.y + NODE_HEIGHT / 2; const spacing = NODE_HEIGHT / (ports.length + 1); return node.position.y + spacing * (idx + 1); } function esc(s: string): string { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } // ── Mini SVG renderer ── function renderMiniSVG(nodes: CampaignWorkflowNode[], edges: CampaignWorkflowEdge[]): string { if (nodes.length === 0) { return ` Empty workflow `; } // Compute bounding box let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const n of nodes) { minX = Math.min(minX, n.position.x); minY = Math.min(minY, n.position.y); maxX = Math.max(maxX, n.position.x + NODE_WIDTH); maxY = Math.max(maxY, n.position.y + NODE_HEIGHT); } const pad = 20; const vx = minX - pad; const vy = minY - pad; const vw = maxX - minX + pad * 2; const vh = maxY - minY + pad * 2; // Render edges as Bezier paths const edgePaths = edges.map(e => { const fromNode = nodes.find(n => n.id === e.fromNode); const toNode = nodes.find(n => n.id === e.toNode); if (!fromNode || !toNode) return ''; const x1 = fromNode.position.x + NODE_WIDTH; const y1 = getPortY(fromNode, e.fromPort, 'output'); const x2 = toNode.position.x; const y2 = getPortY(toNode, e.toPort, 'input'); const dx = Math.abs(x2 - x1) * 0.5; return ``; }).join(''); // Render nodes as rects const nodeRects = nodes.map(n => { const def = getNodeDef(n.type); const cat = def?.category || 'action'; const color = CATEGORY_COLORS[cat] || '#666'; const label = def?.icon ? `${def.icon} ${esc(n.label || def.label)}` : esc(n.label || n.type); // Truncate long labels for mini view const shortLabel = label.length > 18 ? label.slice(0, 16) + '…' : label; return ` ${shortLabel} `; }).join(''); return ` ${edgePaths} ${nodeRects} `; } // ── Component ── class FolkCampaignsDashboard extends HTMLElement { private shadow: ShadowRoot; private space = ''; private workflows: CampaignWorkflow[] = []; private loading = true; private get basePath() { const host = window.location.hostname; if (host.endsWith('.rspace.online')) return '/rsocials/'; return `/${this.space}/rsocials/`; } constructor() { super(); this.shadow = this.attachShadow({ mode: 'open' }); } connectedCallback() { this.space = this.getAttribute('space') || 'demo'; this.render(); this.loadWorkflows(); } private async loadWorkflows() { try { const res = await fetch(`${this.basePath}api/campaign-workflows`); if (res.ok) { const data = await res.json(); this.workflows = data.results || []; } } catch { console.warn('[CampaignsDashboard] Failed to load workflows'); } this.loading = false; this.render(); } private async createWorkflow() { try { const res = await fetch(`${this.basePath}api/campaign-workflows`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'New Campaign Workflow' }), }); if (res.ok) { const wf = await res.json(); this.navigateToWorkflow(wf.id); } } catch { console.error('[CampaignsDashboard] Failed to create workflow'); } } private navigateToWorkflow(id: string) { const url = new URL(window.location.href); url.searchParams.set('workflow', id); window.location.href = url.toString(); } private formatDate(ts: number | null): string { if (!ts) return '—'; const d = new Date(ts); const now = Date.now(); const diff = now - ts; if (diff < 60000) return 'just now'; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } private render() { const cards = this.workflows.map(wf => { const nodeCount = wf.nodes.length; const statusClass = wf.enabled ? 'cd-badge--enabled' : 'cd-badge--disabled'; const statusLabel = wf.enabled ? 'Enabled' : 'Disabled'; const runBadge = wf.lastRunStatus ? `${wf.lastRunStatus}` : ''; return `
${renderMiniSVG(wf.nodes, wf.edges)}
${esc(wf.name)}
${statusLabel} ${nodeCount} node${nodeCount !== 1 ? 's' : ''} ${runBadge} ${wf.runCount > 0 ? `${wf.runCount} run${wf.runCount !== 1 ? 's' : ''}` : ''}
Updated ${this.formatDate(wf.updatedAt)}
`; }).join(''); const emptyState = !this.loading && this.workflows.length === 0 ? `
📋
No campaign workflows yet
Create your first workflow to automate social media campaigns
` : ''; const loadingState = this.loading ? `
Loading workflows…
` : ''; this.shadow.innerHTML = `

Campaign Workflows

${!this.loading && this.workflows.length > 0 ? '' : ''}
${loadingState} ${emptyState} ${!this.loading && this.workflows.length > 0 ? `
${cards}
` : ''}
`; this.attachListeners(); } private attachListeners() { // Card clicks this.shadow.querySelectorAll('.cd-card').forEach(card => { card.addEventListener('click', () => { const id = (card as HTMLElement).dataset.wfId; if (id) this.navigateToWorkflow(id); }); }); // New workflow buttons const btnNew = this.shadow.getElementById('btn-new'); if (btnNew) btnNew.addEventListener('click', () => this.createWorkflow()); const btnNewEmpty = this.shadow.querySelector('.cd-btn--new-empty'); if (btnNewEmpty) btnNewEmpty.addEventListener('click', () => this.createWorkflow()); } } customElements.define('folk-campaigns-dashboard', FolkCampaignsDashboard);