ultisuite-client/components/gmail/settings/automation/workflow-nodes.tsx
R3D347HR4Y 20552a34ff feat(automation): multi-domain rules and webhook scope UI
Extend automations to drive and contacts with context-aware triggers,
conditions, and actions. Webhooks can filter event types and scopes per domain.
2026-06-07 15:51:47 +02:00

291 lines
9.0 KiB
TypeScript

'use client'
import { memo, type ReactNode } from 'react'
import { Handle, Position, type NodeProps } from '@xyflow/react'
import { cn } from '@/lib/utils'
import {
NODE_COLORS,
CONDITION_OPERATORS,
LABEL_CONDITION_OPERATORS,
} from '@/lib/mail-automation/node-definitions'
import { actionTypeLabel, conditionFieldLabel } from '@/lib/mail-automation/domains'
import { formatConditionSummary } from '@/lib/mail-automation/condition-helpers'
import type {
ActionsNodeData,
ConditionNodeData,
LabelCheckNodeData,
LLMCheckNodeData,
SetVarNodeData,
SwitchNodeData,
CallRuleNodeData,
WorkflowNodeType,
} from '@/lib/mail-automation/types'
function fieldLabel(field: string) {
return conditionFieldLabel(field)
}
function opLabel(op: string) {
const fromString = CONDITION_OPERATORS.find((o) => o.value === op)?.label
if (fromString) return fromString
return LABEL_CONDITION_OPERATORS.find((o) => o.value === op)?.label ?? op
}
function actionLabel(type: string) {
return actionTypeLabel(type)
}
function nodeShell(
type: WorkflowNodeType,
selected: boolean | undefined,
className: string,
children: ReactNode
) {
return (
<div
className={cn(
'rounded-lg border-2 px-3 py-2 shadow-sm transition-shadow',
NODE_COLORS[type],
selected && 'border-primary ring-2 ring-primary/40 shadow-md',
className
)}
>
{children}
</div>
)
}
function BranchOutputRow({
id,
label,
labelClassName,
handleClassName,
top,
}: {
id: string
label: string
labelClassName?: string
handleClassName?: string
top: string
}) {
return (
<>
<span
className={cn(
'pointer-events-none absolute right-5 -translate-y-1/2 text-[10px] font-medium',
labelClassName
)}
style={{ top }}
>
{label}
</span>
<Handle
type="source"
position={Position.Right}
id={id}
style={{ top }}
className={cn('!size-2.5', handleClassName)}
/>
</>
)
}
function BranchOutputs({
branches,
}: {
branches: {
id: string
label: string
labelClassName?: string
handleClassName?: string
}[]
}) {
const rowHeight = 24
const blockHeight = branches.length * rowHeight
return (
<div
className="relative mt-3 border-t border-border/40 pt-2"
style={{ height: blockHeight }}
>
{branches.map((b, i) => {
const topPx = rowHeight * i + rowHeight / 2
return <BranchOutputRow key={b.id} {...b} top={`${topPx}px`} />
})}
</div>
)
}
export const StartNode = memo(function StartNode({ selected }: NodeProps) {
return nodeShell('start', selected, 'min-w-[120px]', (
<>
<p className="text-xs font-semibold">Début</p>
<Handle type="source" position={Position.Right} className="!size-2.5 !bg-primary" />
</>
))
})
export const EndNode = memo(function EndNode({ selected }: NodeProps) {
return nodeShell('end', selected, 'min-w-[100px]', (
<>
<Handle type="target" position={Position.Left} className="!size-2.5 !bg-primary" />
<p className="text-xs font-semibold text-muted-foreground">Fin</p>
</>
))
})
export const ConditionNode = memo(function ConditionNode({ data, selected }: NodeProps) {
const d = data as unknown as ConditionNodeData
return nodeShell('condition', selected, 'min-w-[200px]', (
<>
<Handle type="target" position={Position.Left} className="!size-2.5 !bg-primary" />
<p className="text-xs font-semibold">Si</p>
<p className="mt-1 text-[11px] text-muted-foreground">
{formatConditionSummary(d, fieldLabel, opLabel)}
</p>
<BranchOutputs
branches={[
{ id: 'true', label: 'vrai', labelClassName: 'text-emerald-600', handleClassName: '!bg-emerald-500' },
{ id: 'false', label: 'faux', labelClassName: 'text-rose-600', handleClassName: '!bg-rose-500' },
]}
/>
</>
))
})
export const LabelCheckNode = memo(function LabelCheckNode({ data, selected }: NodeProps) {
const d = data as unknown as LabelCheckNodeData
return nodeShell('label_check', selected, 'min-w-[200px]', (
<>
<Handle type="target" position={Position.Left} className="!size-2.5 !bg-primary" />
<p className="text-xs font-semibold">Libellé</p>
<p className="mt-1 text-[11px] text-muted-foreground">
{d.operator === 'not_has' ? 'Sans' : 'A'} « {d.label || '…'} »
</p>
<BranchOutputs
branches={[
{ id: 'true', label: 'vrai', labelClassName: 'text-emerald-600', handleClassName: '!bg-emerald-500' },
{ id: 'false', label: 'faux', labelClassName: 'text-rose-600', handleClassName: '!bg-rose-500' },
]}
/>
</>
))
})
export const SwitchNode = memo(function SwitchNode({ data, selected }: NodeProps) {
const d = data as unknown as SwitchNodeData
const cases = d.cases ?? []
const caseBranches = cases.map((c, i) => ({
id: `case-${i}`,
label: c.label || c.value || `Cas ${i + 1}`,
labelClassName: 'text-violet-600',
handleClassName: '!bg-violet-500',
}))
return nodeShell('switch', selected, 'min-w-[220px]', (
<>
<Handle type="target" position={Position.Left} className="!size-2.5 !bg-primary" />
<p className="text-xs font-semibold">Switch · {fieldLabel(d.field)}</p>
<BranchOutputs
branches={[
...caseBranches,
{
id: 'default',
label: 'défaut',
labelClassName: 'text-muted-foreground',
handleClassName: '!bg-muted-foreground',
},
]}
/>
</>
))
})
export const LLMCheckNode = memo(function LLMCheckNode({ data, selected }: NodeProps) {
const d = data as unknown as LLMCheckNodeData
return nodeShell('llm_check', selected, 'min-w-[220px]', (
<>
<Handle type="target" position={Position.Left} className="!size-2.5 !bg-primary" />
<p className="text-xs font-semibold">LLM</p>
<p className="mt-1 line-clamp-2 text-[11px] text-muted-foreground">{d.prompt || 'Prompt…'}</p>
<BranchOutputs
branches={[
{ id: 'true', label: 'vrai', labelClassName: 'text-emerald-600', handleClassName: '!bg-emerald-500' },
{ id: 'false', label: 'faux', labelClassName: 'text-rose-600', handleClassName: '!bg-rose-500' },
]}
/>
</>
))
})
export const ActionsNode = memo(function ActionsNode({ data, selected }: NodeProps) {
const d = data as unknown as ActionsNodeData
const actions = d.actions ?? []
return nodeShell('actions', selected, 'min-w-[200px]', (
<>
<Handle type="target" position={Position.Left} className="!size-2.5 !bg-primary" />
<p className="text-xs font-semibold">Actions ({actions.length})</p>
<ul className="mt-1 space-y-0.5 text-[11px] text-muted-foreground">
{actions.slice(0, 4).map((a, i) => (
<li key={i}>
{actionLabel(a.type)}
{a.value ? `: ${a.value}` : ''}
</li>
))}
</ul>
<Handle type="source" position={Position.Right} className="!size-2.5 !bg-primary" />
</>
))
})
export const SetVarNode = memo(function SetVarNode({ data, selected }: NodeProps) {
const d = data as unknown as SetVarNodeData
return nodeShell('set_var', selected, 'min-w-[180px]', (
<>
<Handle type="target" position={Position.Left} className="!size-2.5 !bg-primary" />
<p className="text-xs font-semibold">Variable</p>
<p className="mt-1 font-mono text-[11px] text-muted-foreground">
{d.name || 'var'} = {d.value || '…'}
</p>
<Handle type="source" position={Position.Right} className="!size-2.5 !bg-primary" />
</>
))
})
export const CallFunctionNode = memo(function CallFunctionNode({ data, selected }: NodeProps) {
const d = data as unknown as CallRuleNodeData
return nodeShell('call_function', selected, 'min-w-[180px]', (
<>
<Handle type="target" position={Position.Left} className="!size-2.5 !bg-primary" />
<p className="text-xs font-semibold">Fonction</p>
<p className="mt-1 truncate text-[11px] text-muted-foreground">{d.rule_id || 'Choisir…'}</p>
<Handle type="source" position={Position.Right} className="!size-2.5 !bg-primary" />
</>
))
})
export const CallRuleNode = memo(function CallRuleNode({ data, selected }: NodeProps) {
const d = data as unknown as CallRuleNodeData
return nodeShell('call_rule', selected, 'min-w-[180px]', (
<>
<Handle type="target" position={Position.Left} className="!size-2.5 !bg-primary" />
<p className="text-xs font-semibold">Règle cascade</p>
<p className="mt-1 truncate text-[11px] text-muted-foreground">{d.rule_id || 'Choisir…'}</p>
<Handle type="source" position={Position.Right} className="!size-2.5 !bg-primary" />
</>
))
})
export const workflowNodeTypes = {
start: StartNode,
end: EndNode,
condition: ConditionNode,
label_check: LabelCheckNode,
switch: SwitchNode,
llm_check: LLMCheckNode,
actions: ActionsNode,
set_var: SetVarNode,
call_function: CallFunctionNode,
call_rule: CallRuleNode,
}