Skip to content

Commit 20bb7cd

Browse files
emir-karabegcursoragentwaleedlatif1
authored
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 <emir@simstudio.ai> * 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 <cursoragent@cursor.com> Co-authored-by: waleed <walif6@gmail.com>
1 parent 1469e9c commit 20bb7cd

File tree

3 files changed

+127
-28
lines changed

3 files changed

+127
-28
lines changed

apps/sim/app/api/workflows/[id]/route.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,19 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
133133
const finalWorkflowData = {
134134
...workflowData,
135135
state: {
136-
// Default values for expected properties
137136
deploymentStatuses: {},
138-
// Data from normalized tables
139137
blocks: normalizedData.blocks,
140138
edges: normalizedData.edges,
141139
loops: normalizedData.loops,
142140
parallels: normalizedData.parallels,
143141
lastSaved: Date.now(),
144142
isDeployed: workflowData.isDeployed || false,
145143
deployedAt: workflowData.deployedAt,
144+
metadata: {
145+
name: workflowData.name,
146+
description: workflowData.description,
147+
},
146148
},
147-
// Include workflow variables
148149
variables: workflowData.variables || {},
149150
}
150151

@@ -166,6 +167,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
166167
lastSaved: Date.now(),
167168
isDeployed: workflowData.isDeployed || false,
168169
deployedAt: workflowData.deployedAt,
170+
metadata: {
171+
name: workflowData.name,
172+
description: workflowData.description,
173+
},
169174
},
170175
variables: workflowData.variables || {},
171176
}

apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import {
55
ArrowDown,
66
ArrowUp,
7+
Check,
78
ChevronDown as ChevronDownIcon,
89
ChevronUp,
10+
Clipboard,
911
ExternalLink,
1012
Maximize2,
1113
RepeatIcon,
14+
Search,
1215
SplitIcon,
1316
X,
1417
} from 'lucide-react'
@@ -813,6 +816,13 @@ function PreviewEditorContent({
813816
} = useContextMenu()
814817

815818
const [contextMenuData, setContextMenuData] = useState({ content: '', copyOnly: false })
819+
const [copiedSection, setCopiedSection] = useState<'input' | 'output' | null>(null)
820+
821+
const handleCopySection = useCallback((content: string, section: 'input' | 'output') => {
822+
navigator.clipboard.writeText(content)
823+
setCopiedSection(section)
824+
setTimeout(() => setCopiedSection(null), 1500)
825+
}, [])
816826

817827
const openContextMenu = useCallback(
818828
(e: React.MouseEvent, content: string, copyOnly: boolean) => {
@@ -862,9 +872,6 @@ function PreviewEditorContent({
862872
}
863873
}, [contextMenuData.content])
864874

865-
/**
866-
* Handles mouse down event on the resize handle to initiate resizing
867-
*/
868875
const handleConnectionsResizeMouseDown = useCallback(
869876
(e: React.MouseEvent) => {
870877
setIsResizing(true)
@@ -874,18 +881,12 @@ function PreviewEditorContent({
874881
[connectionsHeight]
875882
)
876883

877-
/**
878-
* Toggle connections collapsed state
879-
*/
880884
const toggleConnectionsCollapsed = useCallback(() => {
881885
setConnectionsHeight((prev) =>
882886
prev <= MIN_CONNECTIONS_HEIGHT ? DEFAULT_CONNECTIONS_HEIGHT : MIN_CONNECTIONS_HEIGHT
883887
)
884888
}, [])
885889

886-
/**
887-
* Sets up resize event listeners during resize operations
888-
*/
889890
useEffect(() => {
890891
if (!isResizing) return
891892

@@ -1205,7 +1206,11 @@ function PreviewEditorContent({
12051206
}
12061207
emptyMessage='No input data'
12071208
>
1208-
<div onContextMenu={handleExecutionContextMenu} ref={contentRef}>
1209+
<div
1210+
onContextMenu={handleExecutionContextMenu}
1211+
ref={contentRef}
1212+
className='relative'
1213+
>
12091214
<Code.Viewer
12101215
code={formatValueAsJson(executionData.input)}
12111216
language='json'
@@ -1215,6 +1220,49 @@ function PreviewEditorContent({
12151220
currentMatchIndex={currentMatchIndex}
12161221
onMatchCountChange={handleMatchCountChange}
12171222
/>
1223+
{/* Action buttons overlay */}
1224+
{!isSearchActive && (
1225+
<div className='absolute top-[7px] right-[6px] z-10 flex gap-[4px]'>
1226+
<Tooltip.Root>
1227+
<Tooltip.Trigger asChild>
1228+
<Button
1229+
type='button'
1230+
variant='ghost'
1231+
onClick={(e) => {
1232+
e.stopPropagation()
1233+
handleCopySection(formatValueAsJson(executionData.input), 'input')
1234+
}}
1235+
className='h-[20px] w-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-sm hover:bg-[var(--surface-4)]'
1236+
>
1237+
{copiedSection === 'input' ? (
1238+
<Check className='h-[10px] w-[10px] text-[var(--text-success)]' />
1239+
) : (
1240+
<Clipboard className='h-[10px] w-[10px]' />
1241+
)}
1242+
</Button>
1243+
</Tooltip.Trigger>
1244+
<Tooltip.Content side='top'>
1245+
{copiedSection === 'input' ? 'Copied' : 'Copy'}
1246+
</Tooltip.Content>
1247+
</Tooltip.Root>
1248+
<Tooltip.Root>
1249+
<Tooltip.Trigger asChild>
1250+
<Button
1251+
type='button'
1252+
variant='ghost'
1253+
onClick={(e) => {
1254+
e.stopPropagation()
1255+
activateSearch()
1256+
}}
1257+
className='h-[20px] w-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-sm hover:bg-[var(--surface-4)]'
1258+
>
1259+
<Search className='h-[10px] w-[10px]' />
1260+
</Button>
1261+
</Tooltip.Trigger>
1262+
<Tooltip.Content side='top'>Search</Tooltip.Content>
1263+
</Tooltip.Root>
1264+
</div>
1265+
)}
12181266
</div>
12191267
</CollapsibleSection>
12201268
)}
@@ -1231,7 +1279,7 @@ function PreviewEditorContent({
12311279
emptyMessage='No output data'
12321280
isError={executionData.status === 'error'}
12331281
>
1234-
<div onContextMenu={handleExecutionContextMenu}>
1282+
<div onContextMenu={handleExecutionContextMenu} className='relative'>
12351283
<Code.Viewer
12361284
code={formatValueAsJson(executionData.output)}
12371285
language='json'
@@ -1244,6 +1292,49 @@ function PreviewEditorContent({
12441292
currentMatchIndex={currentMatchIndex}
12451293
onMatchCountChange={handleMatchCountChange}
12461294
/>
1295+
{/* Action buttons overlay */}
1296+
{!isSearchActive && (
1297+
<div className='absolute top-[7px] right-[6px] z-10 flex gap-[4px]'>
1298+
<Tooltip.Root>
1299+
<Tooltip.Trigger asChild>
1300+
<Button
1301+
type='button'
1302+
variant='ghost'
1303+
onClick={(e) => {
1304+
e.stopPropagation()
1305+
handleCopySection(formatValueAsJson(executionData.output), 'output')
1306+
}}
1307+
className='h-[20px] w-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-sm hover:bg-[var(--surface-4)]'
1308+
>
1309+
{copiedSection === 'output' ? (
1310+
<Check className='h-[10px] w-[10px] text-[var(--text-success)]' />
1311+
) : (
1312+
<Clipboard className='h-[10px] w-[10px]' />
1313+
)}
1314+
</Button>
1315+
</Tooltip.Trigger>
1316+
<Tooltip.Content side='top'>
1317+
{copiedSection === 'output' ? 'Copied' : 'Copy'}
1318+
</Tooltip.Content>
1319+
</Tooltip.Root>
1320+
<Tooltip.Root>
1321+
<Tooltip.Trigger asChild>
1322+
<Button
1323+
type='button'
1324+
variant='ghost'
1325+
onClick={(e) => {
1326+
e.stopPropagation()
1327+
activateSearch()
1328+
}}
1329+
className='h-[20px] w-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-sm hover:bg-[var(--surface-4)]'
1330+
>
1331+
<Search className='h-[10px] w-[10px]' />
1332+
</Button>
1333+
</Tooltip.Trigger>
1334+
<Tooltip.Content side='top'>Search</Tooltip.Content>
1335+
</Tooltip.Root>
1336+
</div>
1337+
)}
12471338
</div>
12481339
</CollapsibleSection>
12491340
)}

apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface WorkflowStackEntry {
3535
workflowState: WorkflowState
3636
traceSpans: TraceSpan[]
3737
blockExecutions: Record<string, BlockExecutionData>
38+
workflowName: string
3839
}
3940

4041
/**
@@ -144,7 +145,6 @@ export function Preview({
144145
initialSelectedBlockId,
145146
autoSelectLeftmost = true,
146147
}: PreviewProps) {
147-
/** Initialize pinnedBlockId synchronously to ensure sidebar is present from first render */
148148
const [pinnedBlockId, setPinnedBlockId] = useState<string | null>(() => {
149149
if (initialSelectedBlockId) return initialSelectedBlockId
150150
if (autoSelectLeftmost) {
@@ -153,67 +153,61 @@ export function Preview({
153153
return null
154154
})
155155

156-
/** Stack for nested workflow navigation. Empty means we're at the root level. */
157156
const [workflowStack, setWorkflowStack] = useState<WorkflowStackEntry[]>([])
158157

159-
/** Block executions for the root level */
160158
const rootBlockExecutions = useMemo(() => {
161159
if (providedBlockExecutions) return providedBlockExecutions
162160
if (!rootTraceSpans || !Array.isArray(rootTraceSpans)) return {}
163161
return buildBlockExecutions(rootTraceSpans)
164162
}, [providedBlockExecutions, rootTraceSpans])
165163

166-
/** Current block executions - either from stack or root */
167164
const blockExecutions = useMemo(() => {
168165
if (workflowStack.length > 0) {
169166
return workflowStack[workflowStack.length - 1].blockExecutions
170167
}
171168
return rootBlockExecutions
172169
}, [workflowStack, rootBlockExecutions])
173170

174-
/** Current workflow state - either from stack or root */
175171
const workflowState = useMemo(() => {
176172
if (workflowStack.length > 0) {
177173
return workflowStack[workflowStack.length - 1].workflowState
178174
}
179175
return rootWorkflowState
180176
}, [workflowStack, rootWorkflowState])
181177

182-
/** Whether we're in execution mode (have trace spans/block executions) */
183178
const isExecutionMode = useMemo(() => {
184179
return Object.keys(blockExecutions).length > 0
185180
}, [blockExecutions])
186181

187-
/** Handler to drill down into a nested workflow block */
188182
const handleDrillDown = useCallback(
189183
(blockId: string, childWorkflowState: WorkflowState) => {
190184
const blockExecution = blockExecutions[blockId]
191185
const childTraceSpans = extractChildTraceSpans(blockExecution)
192186
const childBlockExecutions = buildBlockExecutions(childTraceSpans)
193187

188+
const workflowName = childWorkflowState.metadata?.name || 'Nested Workflow'
189+
194190
setWorkflowStack((prev) => [
195191
...prev,
196192
{
197193
workflowState: childWorkflowState,
198194
traceSpans: childTraceSpans,
199195
blockExecutions: childBlockExecutions,
196+
workflowName,
200197
},
201198
])
202199

203-
/** Set pinned block synchronously to avoid double fitView from sidebar resize */
204200
const leftmostId = getLeftmostBlockId(childWorkflowState)
205201
setPinnedBlockId(leftmostId)
206202
},
207203
[blockExecutions]
208204
)
209205

210-
/** Handler to go back up the stack */
211206
const handleGoBack = useCallback(() => {
212207
setWorkflowStack((prev) => prev.slice(0, -1))
213208
setPinnedBlockId(null)
214209
}, [])
215210

216-
/** Handlers for node interactions - memoized to prevent unnecessary re-renders */
217211
const handleNodeClick = useCallback((blockId: string) => {
218212
setPinnedBlockId(blockId)
219213
}, [])
@@ -232,6 +226,8 @@ export function Preview({
232226

233227
const isNested = workflowStack.length > 0
234228

229+
const currentWorkflowName = isNested ? workflowStack[workflowStack.length - 1].workflowName : null
230+
235231
return (
236232
<div
237233
style={{ height, width }}
@@ -242,20 +238,27 @@ export function Preview({
242238
)}
243239
>
244240
{isNested && (
245-
<div className='absolute top-[12px] left-[12px] z-20'>
241+
<div className='absolute top-[12px] left-[12px] z-20 flex items-center gap-[6px]'>
246242
<Tooltip.Root>
247243
<Tooltip.Trigger asChild>
248244
<Button
249245
variant='ghost'
250246
onClick={handleGoBack}
251-
className='flex h-[30px] items-center gap-[5px] border border-[var(--border)] bg-[var(--surface-2)] px-[10px] hover:bg-[var(--surface-4)]'
247+
className='flex h-[28px] items-center gap-[5px] rounded-[6px] border border-[var(--border)] bg-[var(--surface-2)] px-[10px] text-[var(--text-secondary)] shadow-sm hover:bg-[var(--surface-4)] hover:text-[var(--text-primary)]'
252248
>
253-
<ArrowLeft className='h-[13px] w-[13px]' />
254-
<span className='font-medium text-[13px]'>Back</span>
249+
<ArrowLeft className='h-[12px] w-[12px]' />
250+
<span className='font-medium text-[12px]'>Back</span>
255251
</Button>
256252
</Tooltip.Trigger>
257253
<Tooltip.Content side='bottom'>Go back to parent workflow</Tooltip.Content>
258254
</Tooltip.Root>
255+
{currentWorkflowName && (
256+
<div className='flex h-[28px] max-w-[200px] items-center rounded-[6px] border border-[var(--border)] bg-[var(--surface-2)] px-[10px] shadow-sm'>
257+
<span className='truncate font-medium text-[12px] text-[var(--text-secondary)]'>
258+
{currentWorkflowName}
259+
</span>
260+
</div>
261+
)}
259262
</div>
260263
)}
261264

0 commit comments

Comments
 (0)