import * as React from "react" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Card, CardContent } from "@/components/ui/card" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Separator } from "@/components/ui/separator" import { UserPlus, UserMinus, UserCheck, UserCog, Rss, Ban, UserX, Mail, MailX, MailCheck, MessageSquare, Package, PackagePlus, DollarSign, Star, ShoppingCart, ChevronDown, GripVertical, MoreVertical, LogOut, ListChecks, PenTool, Settings, Search, Clock, Filter, Target, Zap, Phone, MessageCircle, Edit, Copy, Trash2 } from "lucide-react" interface TriggerItemProps { icon: React.ReactNode title: string category: string isDraggable?: boolean onDragStart?: (e: React.DragEvent, trigger: any) => void onDragEnd?: () => void onClick?: () => void } function TriggerItem({ icon, title, category, isDraggable = true, onDragStart, onDragEnd, onClick }: TriggerItemProps) { const triggerData = { icon, title, category } return (
onDragStart?.(e, triggerData)} onDragEnd={onDragEnd} onClick={onClick} >
{icon}
{title}
{isDraggable && (
)}
) } interface WorkflowNodeProps { id?: string title: string icon: React.ReactNode backgroundColor: string details?: Array<{ label: string; value: string }> children?: React.ReactNode onEdit?: (node: { id: string; type: string; title: string; fields: any }) => void onDelete?: (id: string) => void fields?: any type?: string } function WorkflowNode({ id, title, icon, backgroundColor, details, children, onEdit, onDelete, fields, type }: WorkflowNodeProps) { return (
{icon}

{title}

{type === "trigger" && (
)} {(type === "rule" || type === "action") && (
)} {type === "exit" && ( )}
{details && details.length > 0 && (
{details.map((detail, index) => (
{detail.label && {detail.label} } {detail.value}
))}
)} {children}
) } interface TriggerSectionProps { title: string isExpanded?: boolean onToggle?: () => void children: React.ReactNode } function TriggerSection({ title, isExpanded = true, onToggle, children }: TriggerSectionProps) { return (
{isExpanded && (
{children}
)}
) } export function WorkflowBuilder({ className }: { className?: string }) { const [searchTerm, setSearchTerm] = React.useState("") const [activeTab, setActiveTab] = React.useState<"triggers" | "rules" | "actions">("triggers") const [workflowNodes, setWorkflowNodes] = React.useState; fields?: any }>>([]) const [isDragging, setIsDragging] = React.useState(false) const [draggedItem, setDraggedItem] = React.useState(null) const [editingNode, setEditingNode] = React.useState<{ id: string; type: string; title: string; fields: any } | null>(null) const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false) const [zoomLevel, setZoomLevel] = React.useState(100) const triggerCategories = [ { title: "User", triggers: [ { icon: , title: "User Registered", defaultFields: { userType: "Any User", registrationSource: "Website" } }, { icon: , title: "User Deleted", defaultFields: { deletionReason: "User Request", notifyAdmin: true } }, { icon: , title: "User Updated", defaultFields: { updatedField: "Profile Information", trackChanges: true } }, { icon: , title: "User Role Changed", defaultFields: { previousRole: "Subscriber", newRole: "Administrator" } }, ] }, { title: "Subscriber", triggers: [ { icon: , title: "User Subscribed", defaultFields: { subscriptionList: "Main List", subscriptionSource: "Website Form" } }, { icon: , title: "User Unconfirmed", defaultFields: { confirmationTimeout: "24 hours", sendReminder: true } }, { icon: , title: "User Unsubscribed", defaultFields: { unsubscribeReason: "User Request", fromList: "All Lists" } }, ] }, { title: "Admin", triggers: [ { icon: , title: "Campaign sent", defaultFields: { campaignType: "Newsletter", recipientCount: "100+" } }, { icon: , title: "Campaign failed", defaultFields: { failureReason: "Server Error", retryAttempts: 3 } }, ] }, { title: "Comment", triggers: [ { icon: , title: "Comment Added", defaultFields: { postType: "Blog Post", moderationStatus: "Pending" } }, ] }, { title: "Order", triggers: [ { icon: , title: "WooCommerce Order Completed", defaultFields: { orderValue: "$50+", paymentMethod: "Any" } }, { icon: , title: "WooCommerce Order Created", defaultFields: { orderStatus: "Processing", productCategory: "Any" } }, { icon: , title: "WooCommerce Order Refunded", defaultFields: { refundAmount: "Full Amount", refundReason: "Customer Request" } }, ] }, { title: "Review", triggers: [ { icon: , title: "New Product Review Posted", defaultFields: { minimumRating: "3 stars", productType: "Any Product" } }, ] }, { title: "Carts", triggers: [ { icon: , title: "Cart Abandoned", defaultFields: { abandonmentTime: "1 hour", cartValue: "$10+" } }, { icon: , title: "Cart Abandoned - Registered User Only", defaultFields: { abandonmentTime: "1 hour", cartValue: "$10+", userType: "Registered" } }, { icon: , title: "Cart Abandoned - Guests Only", defaultFields: { abandonmentTime: "1 hour", cartValue: "$10+", userType: "Guest" } }, ] } ] const rulesCategories = [ { title: "Subscriber", rules: [ { icon: , title: "Subscriber - List", defaultFields: { operator: "Matches", values: ["Main List", "Test List", "Some other list"] } }, { icon: , title: "Subscriber - Tag", defaultFields: { operator: "Matches any", values: ["VIP", "New Customer", "Premium"] } }, { icon: , title: "Subscriber - Field", defaultFields: { operator: "Equals", values: ["Active Status", "Location", "Age"] } }, ] }, { title: "Time", rules: [ { icon: , title: "Time Delay", defaultFields: { operator: "Wait for", values: ["1 hour", "1 day", "1 week"] } }, { icon: , title: "Wait Until", defaultFields: { operator: "Wait until", values: ["9 AM", "12 PM", "6 PM"] } }, ] }, { title: "Conditions", rules: [ { icon: , title: "If/Else Condition", defaultFields: { operator: "If", values: ["True", "False"] } }, { icon: , title: "Split Test", defaultFields: { operator: "Split", values: ["50%", "30%", "20%"] } }, ] } ] const actionsCategories = [ { title: "Email", actions: [ { icon: , title: "Send email", defaultFields: { subject: "Welcome to the fam", recipients: "80 Contacts selected", content: "Welcome to LuminaTech – we're truly excited to have you with us! 🎉\n\nI'm Amit Pathania, your dedicated Success Manager, and I'll be your main point of contact as you get started with us. Whether you're here to streamline your workflows, gain deeper insights from your data, or scale your team's productivity, we're committed to helping you achieve your goals." } }, { icon: , title: "Send broadcast", defaultFields: { subject: "Important Update", recipients: "All subscribers", content: "We have an important update to share with you..." } }, { icon: , title: "Stop email", defaultFields: { reason: "User unsubscribed", action: "Stop all emails" } }, ] }, { title: "SMS", actions: [ { icon: , title: "Send SMS", defaultFields: { message: "Hello! Important update for you.", recipients: "Phone contacts" } }, { icon: , title: "Send WhatsApp", defaultFields: { message: "Hi there! Check out our latest update.", recipients: "WhatsApp contacts" } }, ] }, { title: "Subscriber", actions: [ { icon: , title: "Add to list", defaultFields: { list: "Main List", action: "Add subscriber" } }, { icon: , title: "Remove from list", defaultFields: { list: "Main List", action: "Remove subscriber" } }, { icon: , title: "Update field", defaultFields: { field: "Status", value: "Active" } }, ] } ] const handleZoomIn = () => { setZoomLevel(prev => Math.min(prev + 25, 200)) } const handleZoomOut = () => { setZoomLevel(prev => Math.max(prev - 25, 50)) } // Calculate actual height of an action node based on its content const calculateActionHeight = (action: any) => { let baseHeight = 54 // Base height for the action header (padding + icon + text) if (action.details && action.details.length > 0) { // Each detail line calculation: action.details.forEach((detail: any) => { // Short labels/values: ~28px, longer content might wrap: ~40-60px const isLongContent = detail.value && detail.value.length > 50 if (isLongContent) { // Content might wrap - calculate based on estimated lines const estimatedLines = Math.ceil(detail.value.length / 50) baseHeight += Math.max(28, estimatedLines * 20 + 16) // 20px per line + padding } else { baseHeight += 28 // Standard single line height } }) baseHeight += 8 // Additional padding for details container } // Add margins and rounded corners compensation baseHeight += 16 // Container padding return baseHeight } // Calculate cumulative positions for all actions const getActionPositions = () => { const actions = workflowNodes.filter(n => n.type === 'action') const rules = workflowNodes.filter(n => n.type === 'rule') let basePosition = 149 // After trigger with 35px connector (64px + 35px + 50px margin) if (rules.length === 1) { basePosition = 294 // After single rule (149px + 110px rule height + 35px connector) } else if (rules.length > 1) { basePosition = 364 // After multiple rules (184px + 110px rule height + 35px + 35px connectors) } const positions: { [key: string]: number } = {} let currentPosition = basePosition actions.forEach((action) => { positions[action.id] = currentPosition // Calculate height for current action and add spacing for next const actionHeight = calculateActionHeight(action) currentPosition += actionHeight + 32 // 32px gap between actions (reduced from 40px) }) return positions } const handleDragStart = (e: React.DragEvent, item: any, type: "trigger" | "rule" | "action") => { setIsDragging(true) setDraggedItem({ ...item, type }) e.dataTransfer.effectAllowed = 'copy' } const handleDragEnd = () => { setIsDragging(false) setDraggedItem(null) } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() e.dataTransfer.dropEffect = 'copy' } const handleDrop = (e: React.DragEvent) => { e.preventDefault() if (draggedItem) { handleAddItem(draggedItem, draggedItem.type) } setIsDragging(false) setDraggedItem(null) } const handleAddTrigger = (trigger: any) => { const newNode = { id: `node-${Date.now()}`, type: 'trigger' as const, title: trigger.title, icon: trigger.icon, backgroundColor: "bg-red-100", details: [], // Don't show details by default fields: trigger.defaultFields || {} } // Replace existing trigger if one exists, otherwise add new one const existingTriggerIndex = workflowNodes.findIndex(node => node.type === 'trigger') if (existingTriggerIndex !== -1) { const updatedNodes = [...workflowNodes] updatedNodes[existingTriggerIndex] = newNode setWorkflowNodes(updatedNodes) } else { setWorkflowNodes([...workflowNodes, newNode]) } } const handleAddRule = (rule: any) => { const newNode = { id: `node-${Date.now()}`, type: 'rule' as const, title: rule.title, icon: rule.icon, backgroundColor: "bg-blue-100", details: rule.defaultFields ? [ { label: "", value: rule.defaultFields.operator }, { label: "", value: rule.defaultFields.values.join(", ") } ] : [], fields: rule.defaultFields || {} } setWorkflowNodes([...workflowNodes, newNode]) } const handleAddAction = (action: any) => { const newNode = { id: `node-${Date.now()}`, type: 'action' as const, title: action.title, icon: action.icon, backgroundColor: "bg-green-100", details: action.defaultFields ? Object.entries(action.defaultFields).map(([key, value]) => ({ label: key === 'subject' ? 'Subject:' : key === 'recipients' ? 'To:' : key === 'content' ? 'Content:' : `${key}:`, value: typeof value === 'string' ? value : String(value) })) : [], fields: action.defaultFields || {} } setWorkflowNodes([...workflowNodes, newNode]) } const handleAddItem = (item: any, type: "trigger" | "rule" | "action") => { switch (type) { case 'trigger': handleAddTrigger(item) break case 'rule': handleAddRule(item) break case 'action': handleAddAction(item) break } } const handleDeleteNode = (nodeId: string) => { setWorkflowNodes(workflowNodes.filter(node => node.id !== nodeId)) } const handleEditNode = (node: { id: string; type: string; title: string; fields: any }) => { setEditingNode(node) setIsEditDialogOpen(true) } const handleSaveEdit = (updatedFields: any) => { if (!editingNode) return setWorkflowNodes(prevNodes => prevNodes.map(node => { if (node.id === editingNode.id) { let updatedDetails if (editingNode.type === 'rule') { updatedDetails = [ { label: "", value: updatedFields.operator || editingNode.fields.operator }, { label: "", value: (updatedFields.values || editingNode.fields.values).join(", ") } ] } else if (editingNode.type === 'trigger') { updatedDetails = Object.entries(updatedFields).map(([key, value]) => ({ label: key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1') + ':', value: typeof value === 'string' ? value : String(value) })) } else { updatedDetails = Object.entries(updatedFields).map(([key, value]) => ({ label: key === 'subject' ? 'Subject:' : key === 'recipients' ? 'To:' : key === 'content' ? 'Content:' : `${key}:`, value: typeof value === 'string' ? value : String(value) })) } return { ...node, fields: { ...node.fields, ...updatedFields }, details: updatedDetails } } return node }) ) setIsEditDialogOpen(false) setEditingNode(null) } return (
{/* Top Alert Bar */}

Alert Title

Save and Exit

Activate Workflow

{/* Main Canvas Area */}
{/* Zoom Controls */}

{zoomLevel}%

{/* Scrollable Canvas Container */}
{/* Empty State */} {workflowNodes.length === 0 && (
Start building your workflow
Add triggers, rules, and actions from the sidebar to create your automation workflow. You can drag and drop items or click to add them.
)} {/* Workflow Connectors */} {workflowNodes.length > 0 && (() => { const rules = workflowNodes.filter(n => n.type === 'rule') const actions = workflowNodes.filter(n => n.type === 'action') const hasRules = rules.length > 0 const hasActions = actions.length > 0 return ( <> {/* Connector from trigger to rules/actions */} {(hasRules || hasActions) && ( <> {hasRules && rules.length === 1 ? ( // Single rule - direct connection to center
) : hasRules && rules.length > 1 ? ( // Multiple rules - split connections dynamically <> {/* Main vertical line from trigger */}
{/* Horizontal split line - spans across all rule positions */}
{/* Branches to each rule */} {rules.map((_, index) => { const totalRules = rules.length const spacing = 100 / (totalRules + 1) const leftPercentage = spacing * (index + 1) return (
) })} ) : hasActions && !hasRules ? ( // Direct to actions (no rules)
) : null} )} {/* Connector from rules to actions */} {hasRules && hasActions && ( <> {rules.length === 1 ? ( // Single rule to actions
) : ( // Multiple rules to actions - merge connections dynamically <> {/* Branches from each rule */} {rules.map((_, index) => { const totalRules = rules.length const spacing = 100 / (totalRules + 1) const leftPercentage = spacing * (index + 1) return (
) })} {/* Horizontal merge line - spans across all rule positions */}
{/* Final connector to actions */}
)} )} {/* Dynamic connectors between actions */} {actions.map((action, index) => { if (index < actions.length - 1) { // Calculate dynamic connector position based on actual action heights const actionPositions = getActionPositions() const currentActionPosition = actionPositions[action.id] const currentActionHeight = calculateActionHeight(action) const connectorPosition = currentActionPosition + currentActionHeight + 8 // +8px gap from bottom of action return (
) } return null })} {/* Connector to exit */} {(() => { let connectorPosition = 114 // Default after trigger (adjusted to match trigger bottom) if (hasActions) { // Connect from last action to exit using dynamic positioning const actionPositions = getActionPositions() const lastAction = actions[actions.length - 1] const lastActionPosition = actionPositions[lastAction.id] const lastActionHeight = calculateActionHeight(lastAction) connectorPosition = lastActionPosition + lastActionHeight + 8 // +8px gap from bottom of last action } else if (hasRules) { // Connect from rules to exit (no actions) if (rules.length === 1) { connectorPosition = 259 } else { // Multiple rules - merge to center then to exit return ( <> {/* Branches from each rule */} {rules.map((_, index) => { const totalRules = rules.length const spacing = 100 / (totalRules + 1) const leftPercentage = spacing * (index + 1) return (
) })} {/* Horizontal merge line */}
{/* Final connector to exit */}
) } } return (
) })()} ) })()} {/* Dynamic Workflow Nodes */} {workflowNodes.map((node) => { // Calculate position based on node type and workflow structure let position = "top-16 left-1/2 transform -translate-x-1/2" // Multiple nodes - arrange by type const rules = workflowNodes.filter(n => n.type === 'rule') const ruleIndex = rules.findIndex(n => n.id === node.id) if (node.type === 'trigger') { position = "top-16 left-1/2 transform -translate-x-1/2" } else if (node.type === 'rule') { if (rules.length === 1) { position = "top-[149px] left-1/2 transform -translate-x-1/2" } else { // Multiple rules - distribute evenly across available space // Calculate percentage position based on rule index and total count const totalRules = rules.length const spacing = 100 / (totalRules + 1) // Add 1 to create equal spacing on both sides const leftPercentage = spacing * (ruleIndex + 1) // +1 because index starts at 0 position = `top-[184px] transform -translate-x-1/2` return (
) } } else if (node.type === 'action') { // Calculate dynamic position for actions using actual heights const actionPositions = getActionPositions() const actionPosition = actionPositions[node.id] position = `left-1/2 transform -translate-x-1/2` return (
) } return (
) })} {/* Exit Node - only show when there are workflow nodes */} {workflowNodes.length > 0 && (() => { const rules = workflowNodes.filter(n => n.type === 'rule') const actions = workflowNodes.filter(n => n.type === 'action') // Calculate position based on the last node in the workflow using dynamic heights let exitPosition = 200 // Minimum position to ensure it's always below trigger if (actions.length > 0) { // If there are actions, place exit after the last action using dynamic positioning const actionPositions = getActionPositions() const lastAction = actions[actions.length - 1] const lastActionPosition = actionPositions[lastAction.id] const lastActionHeight = calculateActionHeight(lastAction) exitPosition = lastActionPosition + lastActionHeight + 67 // +32px gap + 35px connector } else if (rules.length === 1) { // Single rule but no actions exitPosition = 294 // After single rule with 35px connector (149px + 110px rule height + 35px) } else if (rules.length > 1) { // Multiple rules but no actions exitPosition = 364 // After multiple rules with merge and 35px connector (184px + 110px + 35px + 35px) } return (

Exit

) })()} {/* Progress Indicator - only show when there are workflow nodes */} {workflowNodes.length > 0 && (
{zoomLevel}%
)}
{/* Right Sidebar - Triggers Panel */} {/* Search */}
) => setSearchTerm(e.target.value)} className="pl-10" />
{/* Tabs */}
{activeTab === "triggers" && ( <>
Workflow require a trigger to start. Choose a trigger below, and drag it to the canvas.
{triggerCategories.map((category) => (
{category.triggers.map((trigger, index) => (
handleAddTrigger(trigger)} className="cursor-pointer" draggable onDragStart={(e) => handleDragStart(e, trigger, "trigger")} onDragEnd={handleDragEnd} >
))}
))}
)} {activeTab === "rules" && ( <>
Add rules to control when your workflow runs. Rules are optional but help target the right audience.
{rulesCategories.map((category) => (
{category.rules.map((rule, index) => (
handleAddRule(rule)} className="cursor-pointer" draggable onDragStart={(e) => handleDragStart(e, rule, "rule")} onDragEnd={handleDragEnd} >
))}
))}
)} {activeTab === "actions" && ( <>
Add actions to define what happens in your workflow. Actions are the actual steps performed.
{actionsCategories.map((category) => (
{category.actions.map((action, index) => (
handleAddAction(action)} className="cursor-pointer" draggable onDragStart={(e) => handleDragStart(e, action, "action")} onDragEnd={handleDragEnd} >
))}
))}
)}
{/* Right Sidebar - Editor/Settings */}
Editor
Settings
{/* Active indicator for Editor */}
{/* Edit Dialog */} Edit {editingNode?.type === 'trigger' ? 'Trigger' : editingNode?.type === 'rule' ? 'Rule' : 'Action'} Configure the settings for "{editingNode?.title}". {editingNode && ( setIsEditDialogOpen(false)} /> )}
) } // Edit Node Form Component interface EditNodeFormProps { node: { id: string; type: string; title: string; fields: any } onSave: (fields: any) => void onCancel: () => void } function EditNodeForm({ node, onSave, onCancel }: EditNodeFormProps) { const [formFields, setFormFields] = React.useState>(node.fields || {}) const handleSave = () => { onSave(formFields) } const updateField = (key: string, value: any) => { setFormFields((prev: Record) => ({ ...prev, [key]: value })) } if (node.type === 'trigger') { return (
{/* User Role Changed trigger fields */} {formFields.previousRole !== undefined && (
)} {formFields.newRole !== undefined && (
)} {/* User Registration trigger fields */} {formFields.userType !== undefined && (
)} {formFields.registrationSource !== undefined && (
)} {/* Subscription trigger fields */} {formFields.subscriptionList !== undefined && (
)} {formFields.subscriptionSource !== undefined && (
) => updateField('subscriptionSource', e.target.value)} placeholder="e.g., Website Form, Pop-up, Import" />
)} {/* Campaign trigger fields */} {formFields.campaignType !== undefined && (
)} {formFields.recipientCount !== undefined && (
) => updateField('recipientCount', e.target.value)} placeholder="e.g., 100+, 50-100, <50" />
)} {/* Order trigger fields */} {formFields.orderValue !== undefined && (
) => updateField('orderValue', e.target.value)} placeholder="e.g., $50+, $100-200" />
)} {formFields.paymentMethod !== undefined && (
)} {/* Cart abandonment trigger fields */} {formFields.abandonmentTime !== undefined && (
)} {formFields.cartValue !== undefined && (
) => updateField('cartValue', e.target.value)} placeholder="e.g., $10+, $50-100" />
)} {/* Other common fields */} {formFields.deletionReason !== undefined && (
) => updateField('deletionReason', e.target.value)} placeholder="Reason for user deletion" />
)} {formFields.updatedField !== undefined && (
) => updateField('updatedField', e.target.value)} placeholder="Which field was updated" />
)} {formFields.minimumRating !== undefined && (
)}
) } if (node.type === 'rule') { return (