diff --git a/components/workflow/node-config-panel.tsx b/components/workflow/node-config-panel.tsx index f45dbf67..48d485ec 100644 --- a/components/workflow/node-config-panel.tsx +++ b/components/workflow/node-config-panel.tsx @@ -1,5 +1,6 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { + Check, Copy, Eraser, Eye, @@ -49,7 +50,7 @@ import { showDeleteDialogAtom, updateNodeDataAtom, } from "@/lib/workflow-store"; -import { findActionById } from "@/plugins"; +import { findActionById, getIntegration } from "@/plugins"; import { Panel } from "../ai-elements/panel"; import { IntegrationsDialog } from "../settings/integrations-dialog"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; @@ -71,6 +72,46 @@ const SYSTEM_ACTION_INTEGRATIONS: Record = { "Database Query": "database", }; +// Helper to get a display label for a node +const getNodeDisplayLabel = ( + node: + | { + data: { + label?: string; + type: string; + config?: Record; + }; + id: string; + } + | undefined, + fallbackId: string +): string => { + if (!node) { + return fallbackId; + } + if (node.data.label) { + return node.data.label; + } + + if (node.data.type === "action" && node.data.config?.actionType) { + const actionType = node.data.config.actionType as string; + const action = findActionById(actionType); + if (action) { + const plugin = getIntegration(action.integration); + if (plugin) { + return `${plugin.label}: ${action.label}`; + } + } + return `System: ${actionType}`; + } + + if (node.data.type === "trigger" && node.data.config?.triggerType) { + return `Trigger: ${node.data.config.triggerType as string}`; + } + + return node.id; +}; + // Multi-selection panel component const MultiSelectionPanel = ({ selectedNodes, @@ -173,6 +214,8 @@ export const PanelInner = () => { const [showDeleteRunsAlert, setShowDeleteRunsAlert] = useState(false); const [showIntegrationsDialog, setShowIntegrationsDialog] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); + const [copiedNode, setCopiedNode] = useState(false); + const [copiedWorkflow, setCopiedWorkflow] = useState(false); const [activeTab, setActiveTab] = useAtom(propertiesPanelActiveTabAtom); const refreshRunsRef = useRef<(() => Promise) | null>(null); const autoSelectAbortControllersRef = useRef>( @@ -284,15 +327,28 @@ export const PanelInner = () => { return code; }, [nodes, edges, currentWorkflowName]); - const handleCopyCode = () => { + const handleCopyCode = async () => { if (selectedNode) { - navigator.clipboard.writeText(generateNodeCode(selectedNode)); + try { + await navigator.clipboard.writeText(generateNodeCode(selectedNode)); + setCopiedNode(true); + setTimeout(() => setCopiedNode(false), 2000); + } catch (error) { + console.error("Failed to copy:", error); + toast.error("Failed to copy code to clipboard"); + } } }; - const handleCopyWorkflowCode = () => { - navigator.clipboard.writeText(workflowCode); - toast.success("Code copied to clipboard"); + const handleCopyWorkflowCode = async () => { + try { + await navigator.clipboard.writeText(workflowCode); + setCopiedWorkflow(true); + setTimeout(() => setCopiedWorkflow(false), 2000); + } catch (error) { + console.error("Failed to copy:", error); + toast.error("Failed to copy code to clipboard"); + } }; const handleDelete = () => { @@ -487,43 +543,93 @@ export const PanelInner = () => { } // If an edge is selected, show edge properties + if (selectedEdge) { + const sourceNode = nodes.find((node) => node.id === selectedEdge.source); + const targetNode = nodes.find((node) => node.id === selectedEdge.target); + const sourceLabel = getNodeDisplayLabel(sourceNode, selectedEdge.source); + const targetLabel = getNodeDisplayLabel(targetNode, selectedEdge.target); + return ( <> -
-
-

Properties

-
-
-
- - + + + + Properties + + + Runs + + + +
+
+ + +
+
+ + +
+
+ + +
-
- - +
+
-
- - + + +
+
-
-
- -
-
+
+ +
+
+
{ > - Delete Edge + Delete Connection Are you sure you want to delete this connection? This action cannot be undone. @@ -699,9 +805,14 @@ export const PanelInner = () => {
@@ -830,10 +941,10 @@ export const PanelInner = () => { disabled={isGenerating || !isOwner} id="label" onChange={(e) => handleUpdateLabel(e.target.value)} + placeholder="e.g. Send welcome email" value={selectedNode.data.label} />
-
@@ -989,9 +1100,17 @@ export const PanelInner = () => { />
-
diff --git a/components/workflow/workflow-runs.tsx b/components/workflow/workflow-runs.tsx index 4c4624ad..ea5d4273 100644 --- a/components/workflow/workflow-runs.tsx +++ b/components/workflow/workflow-runs.tsx @@ -58,6 +58,8 @@ type WorkflowRunsProps = { isActive?: boolean; onRefreshRef?: React.MutableRefObject<(() => Promise) | null>; onStartRun?: (executionId: string) => void; + // When provided, shows a badge on runs that executed all these nodes (for connection view) + connectionNodeIds?: string[]; }; // Helper to get the output display config for a node type @@ -488,6 +490,7 @@ export function WorkflowRuns({ isActive = false, onRefreshRef, onStartRun, + connectionNodeIds, }: WorkflowRunsProps) { const [currentWorkflowId] = useAtom(currentWorkflowIdAtom); const [selectedExecutionId, setSelectedExecutionId] = useAtom( @@ -598,6 +601,30 @@ export function WorkflowRuns({ [mapNodeLabels, selectedExecutionId, setExecutionLogs] ); + // Load logs for recent executions when viewing a connection (to show "Used" badge) + useEffect(() => { + if (!connectionNodeIds || connectionNodeIds.length === 0) { + return; + } + + // Limit to recent executions to avoid performance issues + const recentExecutions = executions.slice(0, 10); + + const loadLogsForConnection = async () => { + await Promise.all( + recentExecutions.map(async (execution) => { + // Skip if we already have logs for this execution + if (logs[execution.id]) { + return; + } + await loadExecutionLogs(execution.id); + }) + ); + }; + + loadLogsForConnection(); + }, [connectionNodeIds, executions, logs, loadExecutionLogs]); + // Notify parent when a new execution starts and auto-expand it useEffect(() => { if (executions.length === 0) { @@ -781,6 +808,15 @@ export function WorkflowRuns({ ); }); + // Check if this run used the connection (both nodes were executed) + const usedConnection = + connectionNodeIds && + connectionNodeIds.length > 0 && + executionLogs.length > 0 && + connectionNodeIds.every((nodeId) => + executionLogs.some((log) => log.nodeId === nodeId) + ); + return (
Run #{executions.length - index} + {usedConnection && ( + + Edge Active + + )}
{getRelativeTime(execution.startedAt)} diff --git a/components/workflow/workflow-toolbar.tsx b/components/workflow/workflow-toolbar.tsx index 80f0a7fb..e753c6dd 100644 --- a/components/workflow/workflow-toolbar.tsx +++ b/components/workflow/workflow-toolbar.tsx @@ -1486,6 +1486,13 @@ function WorkflowMenuComponent({ state: ReturnType; actions: ReturnType; }) { + const handleWorkflowClick = (workflow: { id: string; name: string }) => { + if (workflow.id === state.currentWorkflowId) { + return; + } + state.router.push(`/workflows/${workflow.id}`); + }; + return (
@@ -1535,6 +1542,40 @@ function WorkflowMenuComponent({ )) )} +

+ + + + + + New Workflow{" "} + {!workflowId && } + + + + {state.allWorkflows.length === 0 ? ( + No workflows found + ) : ( + state.allWorkflows + .filter((w) => w.name !== "__current__") + .map((workflow) => ( + handleWorkflowClick(workflow)} + > + {workflow.name} + {workflow.id === state.currentWorkflowId && ( + + )} + + )) + )} + +