Skip to content

Commit a2c062e

Browse files
committed
improvement(preview): added error paths, loop logic
1 parent 7505579 commit a2c062e

File tree

5 files changed

+414
-188
lines changed

5 files changed

+414
-188
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { EdgeDiffStatus } from '@/lib/workflows/diff/types'
66
import { useExecutionStore } from '@/stores/execution'
77
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
88

9+
/** Extended edge props with optional handle identifiers */
910
interface WorkflowEdgeProps extends EdgeProps {
1011
sourceHandle?: string | null
1112
targetHandle?: string | null
@@ -90,15 +91,17 @@ const WorkflowEdgeComponent = ({
9091
if (edgeDiffStatus === 'deleted') {
9192
color = 'var(--text-error)'
9293
opacity = 0.7
93-
} else if (isErrorEdge) {
94-
color = 'var(--text-error)'
9594
} else if (edgeDiffStatus === 'new') {
9695
color = 'var(--brand-tertiary-2)'
9796
} else if (edgeRunStatus === 'success') {
9897
// Use green for preview mode, default for canvas execution
98+
// This also applies to error edges that were taken (error path executed)
9999
color = previewExecutionStatus ? 'var(--brand-tertiary-2)' : 'var(--border-success)'
100100
} else if (edgeRunStatus === 'error') {
101101
color = 'var(--text-error)'
102+
} else if (isErrorEdge) {
103+
// Error edges that weren't taken stay red
104+
color = 'var(--text-error)'
102105
}
103106

104107
if (isSelected) {
@@ -151,4 +154,14 @@ const WorkflowEdgeComponent = ({
151154
)
152155
}
153156

157+
/**
158+
* Workflow edge component with execution status and diff visualization.
159+
*
160+
* @remarks
161+
* Edge coloring priority:
162+
* 1. Diff status (deleted/new) - for version comparison
163+
* 2. Execution status (success/error) - for run visualization
164+
* 3. Error edge default (red) - for untaken error paths
165+
* 4. Default edge color - normal workflow connections
166+
*/
154167
export const WorkflowEdge = memo(WorkflowEdgeComponent)

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

Lines changed: 128 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { memo, useMemo } from 'react'
3+
import { type CSSProperties, memo, useMemo } from 'react'
44
import { Handle, type NodeProps, Position } from 'reactflow'
55
import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
66
import {
@@ -23,6 +23,27 @@ interface SubBlockValueEntry {
2323
value: unknown
2424
}
2525

26+
/**
27+
* Handle style constants for preview blocks.
28+
* Extracted to avoid recreating style objects on each render.
29+
*/
30+
const HANDLE_STYLES = {
31+
horizontal: '!border-none !bg-[var(--surface-7)] !h-5 !w-[7px] !rounded-[2px]',
32+
vertical: '!border-none !bg-[var(--surface-7)] !h-[7px] !w-5 !rounded-[2px]',
33+
right:
34+
'!z-[10] !border-none !bg-[var(--workflow-edge)] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none',
35+
error:
36+
'!z-[10] !border-none !bg-[var(--text-error)] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none',
37+
} as const
38+
39+
/** Reusable style object for error handles positioned at bottom-right */
40+
const ERROR_HANDLE_STYLE: CSSProperties = {
41+
right: '-7px',
42+
top: 'auto',
43+
bottom: `${HANDLE_POSITIONS.ERROR_BOTTOM_OFFSET}px`,
44+
transform: 'translateY(50%)',
45+
}
46+
2647
interface WorkflowPreviewBlockData {
2748
type: string
2849
name: string
@@ -379,9 +400,6 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
379400
? routerRows.length > 0 || shouldShowDefaultHandles
380401
: hasSubBlocks || shouldShowDefaultHandles
381402

382-
const horizontalHandleClass = '!border-none !bg-[var(--surface-7)] !h-5 !w-[7px] !rounded-[2px]'
383-
const verticalHandleClass = '!border-none !bg-[var(--surface-7)] !h-[7px] !w-5 !rounded-[2px]'
384-
385403
const hasError = executionStatus === 'error'
386404
const hasSuccess = executionStatus === 'success'
387405

@@ -406,7 +424,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
406424
type='target'
407425
position={horizontalHandles ? Position.Left : Position.Top}
408426
id='target'
409-
className={horizontalHandles ? horizontalHandleClass : verticalHandleClass}
427+
className={horizontalHandles ? HANDLE_STYLES.horizontal : HANDLE_STYLES.vertical}
410428
style={
411429
horizontalHandles
412430
? { left: '-7px', top: `${HANDLE_POSITIONS.DEFAULT_Y_OFFSET}px` }
@@ -479,27 +497,103 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
479497
</div>
480498
)}
481499

482-
{/* Source handle */}
483-
<Handle
484-
type='source'
485-
position={horizontalHandles ? Position.Right : Position.Bottom}
486-
id='source'
487-
className={horizontalHandles ? horizontalHandleClass : verticalHandleClass}
488-
style={
489-
horizontalHandles
490-
? { right: '-7px', top: `${HANDLE_POSITIONS.DEFAULT_Y_OFFSET}px` }
491-
: { bottom: '-7px', left: '50%', transform: 'translateX(-50%)' }
492-
}
493-
/>
500+
{/* Condition block handles */}
501+
{type === 'condition' && (
502+
<>
503+
{conditionRows.map((cond, condIndex) => {
504+
const topOffset =
505+
HANDLE_POSITIONS.CONDITION_START_Y + condIndex * HANDLE_POSITIONS.CONDITION_ROW_HEIGHT
506+
return (
507+
<Handle
508+
key={`handle-${cond.id}`}
509+
type='source'
510+
position={Position.Right}
511+
id={`condition-${cond.id}`}
512+
className={HANDLE_STYLES.right}
513+
style={{ top: `${topOffset}px`, right: '-7px', transform: 'translateY(-50%)' }}
514+
/>
515+
)
516+
})}
517+
<Handle
518+
type='source'
519+
position={Position.Right}
520+
id='error'
521+
className={HANDLE_STYLES.error}
522+
style={ERROR_HANDLE_STYLE}
523+
/>
524+
</>
525+
)}
526+
527+
{/* Router block handles */}
528+
{type === 'router_v2' && (
529+
<>
530+
{routerRows.map((route, routeIndex) => {
531+
// +1 row offset for context row at the top
532+
const topOffset =
533+
HANDLE_POSITIONS.CONDITION_START_Y +
534+
(routeIndex + 1) * HANDLE_POSITIONS.CONDITION_ROW_HEIGHT
535+
return (
536+
<Handle
537+
key={`handle-${route.id}`}
538+
type='source'
539+
position={Position.Right}
540+
id={`router-${route.id}`}
541+
className={HANDLE_STYLES.right}
542+
style={{ top: `${topOffset}px`, right: '-7px', transform: 'translateY(-50%)' }}
543+
/>
544+
)
545+
})}
546+
<Handle
547+
type='source'
548+
position={Position.Right}
549+
id='error'
550+
className={HANDLE_STYLES.error}
551+
style={ERROR_HANDLE_STYLE}
552+
/>
553+
</>
554+
)}
555+
556+
{/* Source and error handles for non-condition/router blocks */}
557+
{type !== 'condition' && type !== 'router_v2' && type !== 'response' && (
558+
<>
559+
<Handle
560+
type='source'
561+
position={horizontalHandles ? Position.Right : Position.Bottom}
562+
id='source'
563+
className={horizontalHandles ? HANDLE_STYLES.right : HANDLE_STYLES.vertical}
564+
style={
565+
horizontalHandles
566+
? { right: '-7px', top: `${HANDLE_POSITIONS.DEFAULT_Y_OFFSET}px` }
567+
: { bottom: '-7px', left: '50%', transform: 'translateX(-50%)' }
568+
}
569+
/>
570+
{shouldShowDefaultHandles && (
571+
<Handle
572+
type='source'
573+
position={Position.Right}
574+
id='error'
575+
className={HANDLE_STYLES.error}
576+
style={ERROR_HANDLE_STYLE}
577+
/>
578+
)}
579+
</>
580+
)}
494581
</div>
495582
)
496583
}
497584

585+
/**
586+
* Custom comparison function for React.memo optimization.
587+
* Uses fast-path primitive comparison before shallow comparing subBlockValues.
588+
* @param prevProps - Previous render props
589+
* @param nextProps - Next render props
590+
* @returns True if render should be skipped (props are equal)
591+
*/
498592
function shouldSkipPreviewBlockRender(
499593
prevProps: NodeProps<WorkflowPreviewBlockData>,
500594
nextProps: NodeProps<WorkflowPreviewBlockData>
501595
): boolean {
502-
// Check primitive props first (fast path)
596+
// Fast path: check primitive props first
503597
if (
504598
prevProps.id !== nextProps.id ||
505599
prevProps.data.type !== nextProps.data.type ||
@@ -513,33 +607,34 @@ function shouldSkipPreviewBlockRender(
513607
return false
514608
}
515609

516-
// Compare subBlockValues by reference first
517610
const prevValues = prevProps.data.subBlockValues
518611
const nextValues = nextProps.data.subBlockValues
519612

520-
if (prevValues === nextValues) {
521-
return true
522-
}
613+
// Reference equality check
614+
if (prevValues === nextValues) return true
615+
if (!prevValues || !nextValues) return false
523616

524-
if (!prevValues || !nextValues) {
525-
return false
526-
}
527-
528-
// Shallow compare keys and values
617+
// Shallow comparison of subBlockValues
529618
const prevKeys = Object.keys(prevValues)
530619
const nextKeys = Object.keys(nextValues)
531620

532-
if (prevKeys.length !== nextKeys.length) {
533-
return false
534-
}
621+
if (prevKeys.length !== nextKeys.length) return false
535622

536623
for (const key of prevKeys) {
537-
if (prevValues[key] !== nextValues[key]) {
538-
return false
539-
}
624+
if (prevValues[key] !== nextValues[key]) return false
540625
}
541626

542627
return true
543628
}
544629

630+
/**
631+
* Preview block component for workflow visualization in readonly contexts.
632+
* Optimized for rendering without hooks or store subscriptions.
633+
*
634+
* @remarks
635+
* - Renders block header, subblock values, and connection handles
636+
* - Supports condition, router, and standard block types
637+
* - Shows error handles for non-trigger blocks
638+
* - Displays execution status via colored ring overlays
639+
*/
545640
export const WorkflowPreviewBlock = memo(WorkflowPreviewBlockInner, shouldSkipPreviewBlockRender)

0 commit comments

Comments
 (0)