From e0b55ad6ab98a7c3def8af3be8ccaa798beda01c Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Sun, 30 Nov 2025 17:55:10 +1100 Subject: [PATCH 1/4] Improve properties panel UX and workflow navigation feedback Node Config Panel (node-config-panel.tsx): - Add success toast when copying the node code - Improve edge/connection panel: - Rename header from 'Properties' to 'Connection' - Show human-readable node labels instead of raw IDs - Display format: 'Plugin: Action' (e.g., 'Resend: Send Email') - Show 'System: {action}' for non-plugin actions - Show 'Trigger: {type}' for trigger nodes - Rename fields from 'Source/Target' to 'From/To' - Remove Edge ID field (not user-friendly) - Update delete dialog title to 'Delete Connection' - Add tooltips to all icon-only buttons for discoverability - Unify button styling: make all action buttons icon-only with tooltips - Add 'Node Details' section with heading and description for label/description fields - Add placeholder examples: 'e.g. Send welcome email' Workflow Toolbar (workflow-toolbar.tsx): - Add loading toast when opening a workflow from the dropdown menu - Toast shows 'Opening {workflow name}...' while navigating - Toast auto-dismisses when navigation completes --- components/workflow/node-config-panel.tsx | 151 +++++++++++++++------- components/workflow/workflow-toolbar.tsx | 17 ++- 2 files changed, 121 insertions(+), 47 deletions(-) diff --git a/components/workflow/node-config-panel.tsx b/components/workflow/node-config-panel.tsx index 6442eeb0..df8e7cea 100644 --- a/components/workflow/node-config-panel.tsx +++ b/components/workflow/node-config-panel.tsx @@ -46,7 +46,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"; @@ -68,6 +68,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, @@ -202,6 +242,7 @@ export const PanelInner = () => { const handleCopyCode = () => { if (selectedNode) { navigator.clipboard.writeText(generateNodeCode(selectedNode)); + toast.success("Code copied to clipboard"); } }; @@ -402,37 +443,38 @@ 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

+

Connection

-
- - -
- +
- +
@@ -875,21 +934,21 @@ export const PanelInner = () => {
diff --git a/components/workflow/workflow-toolbar.tsx b/components/workflow/workflow-toolbar.tsx index a4220f2e..b32911d6 100644 --- a/components/workflow/workflow-toolbar.tsx +++ b/components/workflow/workflow-toolbar.tsx @@ -1303,6 +1303,21 @@ function WorkflowMenuComponent({ state: ReturnType; actions: ReturnType; }) { + + useEffect(() => { + if (workflowId !== undefined) { + toast.dismiss("workflow-navigation"); + } + }, [workflowId]); + + const handleWorkflowClick = (workflow: { id: string; name: string }) => { + if (workflow.id === state.currentWorkflowId) { + return; + } + toast.loading(`Opening ${workflow.name}...`, { id: "workflow-navigation" }); + state.router.push(`/workflows/${workflow.id}`); + }; + return (
open && actions.loadWorkflows()}> @@ -1340,7 +1355,7 @@ function WorkflowMenuComponent({ state.router.push(`/workflows/${workflow.id}`)} + onClick={() => handleWorkflowClick(workflow)} > {workflow.name} {workflow.id === state.currentWorkflowId && ( From ca88e06517c61591a24ca70ff90753e6d91bb9b6 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Sun, 30 Nov 2025 18:06:04 +1100 Subject: [PATCH 2/4] fix: pr check formatting --- components/workflow/workflow-toolbar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/workflow/workflow-toolbar.tsx b/components/workflow/workflow-toolbar.tsx index b32911d6..2245a6cd 100644 --- a/components/workflow/workflow-toolbar.tsx +++ b/components/workflow/workflow-toolbar.tsx @@ -1303,7 +1303,6 @@ function WorkflowMenuComponent({ state: ReturnType; actions: ReturnType; }) { - useEffect(() => { if (workflowId !== undefined) { toast.dismiss("workflow-navigation"); From 07cdceab6fc539fb277c107df78903bd7d250407 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Tue, 2 Dec 2025 01:06:22 +1100 Subject: [PATCH 3/4] misc updates based on pr feedback --- components/workflow/node-config-panel.tsx | 182 ++++++++++++++-------- components/workflow/workflow-runs.tsx | 41 +++++ components/workflow/workflow-toolbar.tsx | 7 - 3 files changed, 158 insertions(+), 72 deletions(-) diff --git a/components/workflow/node-config-panel.tsx b/components/workflow/node-config-panel.tsx index df8e7cea..165ab586 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, @@ -206,6 +207,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>( @@ -242,13 +245,15 @@ export const PanelInner = () => { const handleCopyCode = () => { if (selectedNode) { navigator.clipboard.writeText(generateNodeCode(selectedNode)); - toast.success("Code copied to clipboard"); + setCopiedNode(true); + setTimeout(() => setCopiedNode(false), 2000); } }; const handleCopyWorkflowCode = () => { navigator.clipboard.writeText(workflowCode); - toast.success("Code copied to clipboard"); + setCopiedWorkflow(true); + setTimeout(() => setCopiedWorkflow(false), 2000); }; const handleDelete = () => { @@ -452,35 +457,84 @@ export const PanelInner = () => { return ( <> -
-
-

Connection

-
-
-
- - + + + + Properties + + + Runs + + + +
+
+ + +
+
+ + +
+
+ + +
-
- - +
+
-
-
- -
-
+ + +
+ +
+
+ +
+
+ { title="Copy code" variant="ghost" > - + {copiedWorkflow ? ( + + ) : ( + + )}
@@ -741,42 +799,32 @@ export const PanelInner = () => { {selectedNode.data.type !== "action" || selectedNode.data.config?.actionType ? ( -
-
-

- Node Details -

-

- Customize how this step appears in your workflow -

+ <> +
+ + handleUpdateLabel(e.target.value)} + placeholder="e.g. Send welcome email" + value={selectedNode.data.label} + />
-
-
- - handleUpdateLabel(e.target.value)} - placeholder="e.g. Send welcome email" - value={selectedNode.data.label} - /> -
-
- - handleUpdateDescription(e.target.value)} - placeholder="e.g. Sends a welcome email to new users" - value={selectedNode.data.description || ""} - /> -
+
+ + handleUpdateDescription(e.target.value)} + placeholder="e.g. Sends a welcome email to new users" + value={selectedNode.data.description || ""} + />
-
+ ) : null}
{selectedNode.data.type === "action" && ( @@ -916,7 +964,11 @@ export const PanelInner = () => { title="Copy code" variant="ghost" > - + {copiedNode ? ( + + ) : ( + + )}
diff --git a/components/workflow/workflow-runs.tsx b/components/workflow/workflow-runs.tsx index 7ee78326..3bbc69df 100644 --- a/components/workflow/workflow-runs.tsx +++ b/components/workflow/workflow-runs.tsx @@ -53,6 +53,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 detect if output is a base64 image from generateImage step @@ -278,6 +280,7 @@ export function WorkflowRuns({ isActive = false, onRefreshRef, onStartRun, + connectionNodeIds, }: WorkflowRunsProps) { const [currentWorkflowId] = useAtom(currentWorkflowIdAtom); const [selectedExecutionId, setSelectedExecutionId] = useAtom( @@ -388,6 +391,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) { @@ -571,6 +598,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 2245a6cd..03f6dcfd 100644 --- a/components/workflow/workflow-toolbar.tsx +++ b/components/workflow/workflow-toolbar.tsx @@ -1303,17 +1303,10 @@ function WorkflowMenuComponent({ state: ReturnType; actions: ReturnType; }) { - useEffect(() => { - if (workflowId !== undefined) { - toast.dismiss("workflow-navigation"); - } - }, [workflowId]); - const handleWorkflowClick = (workflow: { id: string; name: string }) => { if (workflow.id === state.currentWorkflowId) { return; } - toast.loading(`Opening ${workflow.name}...`, { id: "workflow-navigation" }); state.router.push(`/workflows/${workflow.id}`); }; From 9861495bd8219e42a80931f71df3c9cbdd971690 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Tue, 2 Dec 2025 01:17:17 +1100 Subject: [PATCH 4/4] fix: handle clipboard copy errors with proper async/await --- components/workflow/node-config-panel.tsx | 26 ++++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/components/workflow/node-config-panel.tsx b/components/workflow/node-config-panel.tsx index 165ab586..16698c53 100644 --- a/components/workflow/node-config-panel.tsx +++ b/components/workflow/node-config-panel.tsx @@ -242,18 +242,28 @@ export const PanelInner = () => { return code; }, [nodes, edges, currentWorkflowName]); - const handleCopyCode = () => { + const handleCopyCode = async () => { if (selectedNode) { - navigator.clipboard.writeText(generateNodeCode(selectedNode)); - setCopiedNode(true); - setTimeout(() => setCopiedNode(false), 2000); + 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); - setCopiedWorkflow(true); - setTimeout(() => setCopiedWorkflow(false), 2000); + 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 = () => {