From d02a6c5bca2211e8f5720a0e778d449353ff98e2 Mon Sep 17 00:00:00 2001 From: mohiit1502 Date: Fri, 21 Aug 2026 02:07:16 +0530 Subject: [PATCH] =?UTF-8?q?feat(workflows):=20Phase=202a+2c=20=E2=80=94=20?= =?UTF-8?q?extended=20node=20types=20+=20trigger=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2a (frontend node types): - Extend WfNodeType from 9 to 17 types: add switch, loop, merge, delay, http_request, code, human_approval, notification - Extend WfNodeFamily from 4 to 7: add logic, tool, gate - Full inspector for each new type: Switch (cases editor), Loop (concurrency, iteration config), Merge (wait mode), Delay (duration), HTTP Request (method, URL, headers, body, auth), Code (language, editor, packages), Approval Gate (title, timeout, notify channels), Notification (template, channels) - Node palette: 6 categories (Input, AI/Engine, Control Flow, Integration, Governance, Output) - Graph validation rules for all new types - Diamond shape for logic nodes, new colors/icons Phase 2c (trigger UI): - Trigger selector: 5 types (Manual, Webhook, Schedule, Event, Chat) - Schedule trigger: cron expression + timezone + agent ID - Event trigger: multi-line event types - Chat trigger: /run command input Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../workflows/components/WorkflowNode.tsx | 6 +- .../workflows/components/graph-validation.ts | 51 +- .../components/inspector/NodeInspector.tsx | 582 +++++++++++++++++- .../inspector/WorkflowInspector.tsx | 90 ++- .../workflows/components/node-registry.ts | 144 ++++- .../components/workflow-node-constants.ts | 33 +- src/pages/workflows/types.ts | 134 +++- 7 files changed, 1000 insertions(+), 40 deletions(-) diff --git a/src/pages/workflows/components/WorkflowNode.tsx b/src/pages/workflows/components/WorkflowNode.tsx index 8b9c408..c304ab8 100644 --- a/src/pages/workflows/components/WorkflowNode.tsx +++ b/src/pages/workflows/components/WorkflowNode.tsx @@ -22,10 +22,12 @@ export function WorkflowNode({ id, data }: NodeProps) { const Icon = NODE_ICON[d.type] ?? Zap const isTerminal = d.type === 'start' || d.type === 'end' const shape = NODE_SHAPE_BY_FAMILY[family] - const borderRadius = shape === 'pill' ? 999 : (shape === 'hex' ? 22 : 14) + const borderRadius = shape === 'pill' ? 999 : (shape === 'hex' || shape === 'diamond' ? 22 : 14) const clipPath = shape === 'hex' ? 'polygon(8% 0%, 92% 0%, 100% 50%, 92% 100%, 8% 100%, 0% 50%)' - : undefined + : shape === 'diamond' + ? 'polygon(50% 0%, 96% 50%, 50% 100%, 4% 50%)' + : undefined return (
0 && outgoing !== 2) { + errors.push({ nodeId: node.id, severity: 'warning', message: 'Condition node should have exactly 2 outgoing edges (true/false)' }) + } + } + + if (node.type === 'switch') { + if (!node.config?.expression?.trim()) { + errors.push({ nodeId: node.id, severity: 'error', message: 'Switch node requires an expression' }) + } + if (!node.config?.cases?.length) { + errors.push({ nodeId: node.id, severity: 'error', message: 'Switch node requires at least one case' }) + } + } + + if (node.type === 'loop') { + if (!node.config?.source_expression?.trim()) { + errors.push({ nodeId: node.id, severity: 'error', message: 'Loop node requires a source expression (array)' }) + } + } + + if (node.type === 'merge') { + if ((incomingCount.get(node.id) ?? 0) < 2) { + errors.push({ nodeId: node.id, severity: 'warning', message: 'Merge node should have 2+ incoming edges' }) + } + } + + if (node.type === 'http_request') { + if (!node.config?.http_url?.trim()) { + errors.push({ nodeId: node.id, severity: 'error', message: 'HTTP Request node requires a URL' }) + } + } + + if (node.type === 'code') { + if (!node.config?.code_source?.trim()) { + errors.push({ nodeId: node.id, severity: 'error', message: 'Code node requires source code' }) + } + } + + if (node.type === 'human_approval') { + if (!node.config?.approval_title?.trim()) { + errors.push({ nodeId: node.id, severity: 'error', message: 'Approval gate requires a title' }) } } diff --git a/src/pages/workflows/components/inspector/NodeInspector.tsx b/src/pages/workflows/components/inspector/NodeInspector.tsx index 5314f4d..9efa273 100644 --- a/src/pages/workflows/components/inspector/NodeInspector.tsx +++ b/src/pages/workflows/components/inspector/NodeInspector.tsx @@ -16,6 +16,7 @@ import type { WfRuntimeParam, WfOutputFormat, WfOutputRouteDestination, + WfNotifyChannel, } from '../../types' import { INPUT_NODE_TYPES, @@ -53,6 +54,70 @@ function serializeRuntimeParamOptions(param: WfRuntimeParam) { return (param.options ?? []).map(option => `${option.value}:${option.label}`).join('\n') } +/* ── Notify channels sub-editor ───────────────────────────────────────── */ + +function NotifyChannelsEditor({ + channels, + onChange, +}: { + channels: WfNotifyChannel[] + onChange: (channels: WfNotifyChannel[]) => void +}) { + return ( +
+
Notification channels
+
+ {channels.map((ch, i) => ( +
+
+ + { + const updated = [...channels] + updated[i] = { ...ch, type: v as WfNotifyChannel['type'] } + onChange(updated) + }} + options={[ + { value: 'email', label: 'Email' }, + { value: 'slack', label: 'Slack' }, + { value: 'agos_notification', label: 'In-app' }, + ]} + /> +
+
+ + { + const updated = [...channels] + updated[i] = { ...ch, target: e.target.value } + onChange(updated) + }} + /> +
+ +
+ ))} + +
+
+ ) +} + interface NodeInspectorProps { node: WfNode onUpdateNode: (patch: Partial) => void @@ -277,17 +342,514 @@ export function NodeInspector({ {/* Condition config */} {node.type === 'condition' && ( -
- -