|
|
@@ -1,6 +1,6 @@
|
|
|
import { Handle, Position, NodeProps } from 'reactflow'
|
|
|
import { Play, CheckCircle, XCircle, Loader2 } from 'lucide-react'
|
|
|
-import { NodeOutput } from '../../api/workflows'
|
|
|
+import { NodeOutput, DynamicOutputs } from '../../api/workflows'
|
|
|
|
|
|
export interface NodeExecutionState {
|
|
|
status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' | 'disabled'
|
|
|
@@ -26,11 +26,36 @@ function calculateHandlePosition(index: number, total: number): number {
|
|
|
return spacing * (index + 1)
|
|
|
}
|
|
|
|
|
|
+// Ports for a node whose output count comes from its own config. Names are
|
|
|
+// positional so an edge survives someone editing a rule's label or value.
|
|
|
+export function resolveOutputs(
|
|
|
+ staticOutputs: NodeOutput[] | undefined,
|
|
|
+ dynamic: DynamicOutputs | undefined,
|
|
|
+ config: Record<string, any> | undefined
|
|
|
+): NodeOutput[] {
|
|
|
+ const fixed = staticOutputs?.length ? staticOutputs : []
|
|
|
+ if (!dynamic) {
|
|
|
+ return fixed.length ? fixed : [{ name: 'main', displayName: 'Output', type: 'any' }]
|
|
|
+ }
|
|
|
+ const rules = Array.isArray(config?.[dynamic.from]) ? config![dynamic.from] : []
|
|
|
+ const derived: NodeOutput[] = rules.map((rule: any, idx: number) => ({
|
|
|
+ name: `${dynamic.namePrefix}${idx}`,
|
|
|
+ displayName: (dynamic.labelFrom && rule?.[dynamic.labelFrom]) || `${dynamic.namePrefix} ${idx + 1}`,
|
|
|
+ type: 'any',
|
|
|
+ color: dynamic.color,
|
|
|
+ }))
|
|
|
+ const all = [...derived, ...fixed]
|
|
|
+ return all.length ? all : [{ name: 'main', displayName: 'Output', type: 'any' }]
|
|
|
+}
|
|
|
+
|
|
|
export function WorkflowNode({ data, selected }: NodeProps) {
|
|
|
const isTrigger = data.isTrigger
|
|
|
const disabled = data.disabled === true
|
|
|
const execState = data.executionState as NodeExecutionState | undefined
|
|
|
- const outputs: NodeOutput[] = data.outputs || [{ name: 'main', displayName: 'Output', type: 'any' }]
|
|
|
+ const outputs: NodeOutput[] = resolveOutputs(data.outputs, data.dynamicOutputs, data.config)
|
|
|
+ const inputs: NodeOutput[] = data.inputs?.length
|
|
|
+ ? data.inputs
|
|
|
+ : [{ name: 'data', displayName: 'Data', type: 'any' }]
|
|
|
|
|
|
// Get active branch from execution output (for IF conditions, loops, etc.)
|
|
|
const activeBranch = execState?.output?._activeBranch as string | undefined
|
|
|
@@ -93,13 +118,36 @@ export function WorkflowNode({ data, selected }: NodeProps) {
|
|
|
}`}
|
|
|
style={getStatusStyle()}
|
|
|
>
|
|
|
- {/* Input handle at top (not for triggers) */}
|
|
|
- {!isTrigger && (
|
|
|
+ {/* Input handles at top (not for triggers). A node that names more than
|
|
|
+ one input gets a handle each, spread the way outputs are. */}
|
|
|
+ {!isTrigger && inputs.map((input, idx) => (
|
|
|
<Handle
|
|
|
+ key={input.name}
|
|
|
type="target"
|
|
|
position={Position.Top}
|
|
|
+ id={inputs.length > 1 ? input.name : undefined}
|
|
|
className="!w-3 !h-3 !bg-gray-400 dark:!bg-gray-500 !border-2 !border-white dark:!border-slate-800"
|
|
|
+ style={inputs.length > 1 ? { left: `${calculateHandlePosition(idx, inputs.length)}%` } : undefined}
|
|
|
/>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {/* Input labels, only when there is more than one to tell apart */}
|
|
|
+ {!isTrigger && inputs.length > 1 && (
|
|
|
+ <div className="absolute -top-5 left-0 w-full pointer-events-none">
|
|
|
+ {inputs.map((input, idx) => (
|
|
|
+ <div
|
|
|
+ key={input.name}
|
|
|
+ className="text-[10px] text-center whitespace-nowrap text-gray-500 dark:text-gray-400"
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ left: `${calculateHandlePosition(idx, inputs.length)}%`,
|
|
|
+ transform: 'translateX(-50%)',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {input.displayName}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
)}
|
|
|
|
|
|
{/* Node content */}
|