From 20bb7cdec61a389fe274b38af0df4a6b67231a59 Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:33:19 -0800 Subject: [PATCH] improvement(preview): include current workflow badge in breadcrumb in workflow snapshot (#3062) * feat(preview): add workflow context badge for nested navigation Adds a badge next to the Back button when viewing nested workflows to help users identify which workflow they are currently viewing. This is especially helpful when navigating deeply into nested workflow blocks. Changes: - Added workflowName field to WorkflowStackEntry interface - Capture workflow name from metadata when drilling down - Display workflow name badge next to Back button Co-authored-by: emir * added workflow name and desc to metadata for workflow preview * added copy and search icon in code in preview editor --------- Co-authored-by: Cursor Agent Co-authored-by: waleed --- apps/sim/app/api/workflows/[id]/route.ts | 11 +- .../preview-editor/preview-editor.tsx | 113 ++++++++++++++++-- .../w/components/preview/preview.tsx | 31 ++--- 3 files changed, 127 insertions(+), 28 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index 0172f6d68..802e4e447 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -133,9 +133,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const finalWorkflowData = { ...workflowData, state: { - // Default values for expected properties deploymentStatuses: {}, - // Data from normalized tables blocks: normalizedData.blocks, edges: normalizedData.edges, loops: normalizedData.loops, @@ -143,8 +141,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ lastSaved: Date.now(), isDeployed: workflowData.isDeployed || false, deployedAt: workflowData.deployedAt, + metadata: { + name: workflowData.name, + description: workflowData.description, + }, }, - // Include workflow variables variables: workflowData.variables || {}, } @@ -166,6 +167,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ lastSaved: Date.now(), isDeployed: workflowData.isDeployed || false, deployedAt: workflowData.deployedAt, + metadata: { + name: workflowData.name, + description: workflowData.description, + }, }, variables: workflowData.variables || {}, } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx index 8e4839ef9..d5e5aba12 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx @@ -4,11 +4,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ArrowDown, ArrowUp, + Check, ChevronDown as ChevronDownIcon, ChevronUp, + Clipboard, ExternalLink, Maximize2, RepeatIcon, + Search, SplitIcon, X, } from 'lucide-react' @@ -813,6 +816,13 @@ function PreviewEditorContent({ } = useContextMenu() const [contextMenuData, setContextMenuData] = useState({ content: '', copyOnly: false }) + const [copiedSection, setCopiedSection] = useState<'input' | 'output' | null>(null) + + const handleCopySection = useCallback((content: string, section: 'input' | 'output') => { + navigator.clipboard.writeText(content) + setCopiedSection(section) + setTimeout(() => setCopiedSection(null), 1500) + }, []) const openContextMenu = useCallback( (e: React.MouseEvent, content: string, copyOnly: boolean) => { @@ -862,9 +872,6 @@ function PreviewEditorContent({ } }, [contextMenuData.content]) - /** - * Handles mouse down event on the resize handle to initiate resizing - */ const handleConnectionsResizeMouseDown = useCallback( (e: React.MouseEvent) => { setIsResizing(true) @@ -874,18 +881,12 @@ function PreviewEditorContent({ [connectionsHeight] ) - /** - * Toggle connections collapsed state - */ const toggleConnectionsCollapsed = useCallback(() => { setConnectionsHeight((prev) => prev <= MIN_CONNECTIONS_HEIGHT ? DEFAULT_CONNECTIONS_HEIGHT : MIN_CONNECTIONS_HEIGHT ) }, []) - /** - * Sets up resize event listeners during resize operations - */ useEffect(() => { if (!isResizing) return @@ -1205,7 +1206,11 @@ function PreviewEditorContent({ } emptyMessage='No input data' > -
+
+ {/* Action buttons overlay */} + {!isSearchActive && ( +
+ + + + + + {copiedSection === 'input' ? 'Copied' : 'Copy'} + + + + + + + Search + +
+ )}
)} @@ -1231,7 +1279,7 @@ function PreviewEditorContent({ emptyMessage='No output data' isError={executionData.status === 'error'} > -
+
+ {/* Action buttons overlay */} + {!isSearchActive && ( +
+ + + + + + {copiedSection === 'output' ? 'Copied' : 'Copy'} + + + + + + + Search + +
+ )}
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx index da6bafe03..47df78e53 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx @@ -35,6 +35,7 @@ interface WorkflowStackEntry { workflowState: WorkflowState traceSpans: TraceSpan[] blockExecutions: Record + workflowName: string } /** @@ -144,7 +145,6 @@ export function Preview({ initialSelectedBlockId, autoSelectLeftmost = true, }: PreviewProps) { - /** Initialize pinnedBlockId synchronously to ensure sidebar is present from first render */ const [pinnedBlockId, setPinnedBlockId] = useState(() => { if (initialSelectedBlockId) return initialSelectedBlockId if (autoSelectLeftmost) { @@ -153,17 +153,14 @@ export function Preview({ return null }) - /** Stack for nested workflow navigation. Empty means we're at the root level. */ const [workflowStack, setWorkflowStack] = useState([]) - /** Block executions for the root level */ const rootBlockExecutions = useMemo(() => { if (providedBlockExecutions) return providedBlockExecutions if (!rootTraceSpans || !Array.isArray(rootTraceSpans)) return {} return buildBlockExecutions(rootTraceSpans) }, [providedBlockExecutions, rootTraceSpans]) - /** Current block executions - either from stack or root */ const blockExecutions = useMemo(() => { if (workflowStack.length > 0) { return workflowStack[workflowStack.length - 1].blockExecutions @@ -171,7 +168,6 @@ export function Preview({ return rootBlockExecutions }, [workflowStack, rootBlockExecutions]) - /** Current workflow state - either from stack or root */ const workflowState = useMemo(() => { if (workflowStack.length > 0) { return workflowStack[workflowStack.length - 1].workflowState @@ -179,41 +175,39 @@ export function Preview({ return rootWorkflowState }, [workflowStack, rootWorkflowState]) - /** Whether we're in execution mode (have trace spans/block executions) */ const isExecutionMode = useMemo(() => { return Object.keys(blockExecutions).length > 0 }, [blockExecutions]) - /** Handler to drill down into a nested workflow block */ const handleDrillDown = useCallback( (blockId: string, childWorkflowState: WorkflowState) => { const blockExecution = blockExecutions[blockId] const childTraceSpans = extractChildTraceSpans(blockExecution) const childBlockExecutions = buildBlockExecutions(childTraceSpans) + const workflowName = childWorkflowState.metadata?.name || 'Nested Workflow' + setWorkflowStack((prev) => [ ...prev, { workflowState: childWorkflowState, traceSpans: childTraceSpans, blockExecutions: childBlockExecutions, + workflowName, }, ]) - /** Set pinned block synchronously to avoid double fitView from sidebar resize */ const leftmostId = getLeftmostBlockId(childWorkflowState) setPinnedBlockId(leftmostId) }, [blockExecutions] ) - /** Handler to go back up the stack */ const handleGoBack = useCallback(() => { setWorkflowStack((prev) => prev.slice(0, -1)) setPinnedBlockId(null) }, []) - /** Handlers for node interactions - memoized to prevent unnecessary re-renders */ const handleNodeClick = useCallback((blockId: string) => { setPinnedBlockId(blockId) }, []) @@ -232,6 +226,8 @@ export function Preview({ const isNested = workflowStack.length > 0 + const currentWorkflowName = isNested ? workflowStack[workflowStack.length - 1].workflowName : null + return (
{isNested && ( -
+
Go back to parent workflow + {currentWorkflowName && ( +
+ + {currentWorkflowName} + +
+ )}
)}