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' && ( -
- -