From 5e75863b1fa580d520b2fa2d175848374e38bbaa Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 11 Nov 2025 12:02:16 -0800 Subject: [PATCH 01/11] Notes v1 --- .../components/note-block/note-block.tsx | 238 ++++++++++++++++++ .../components/action-bar/action-bar.tsx | 48 ++-- .../[workspaceId]/w/[workflowId]/workflow.tsx | 29 ++- .../workflow-preview/workflow-preview.tsx | 10 +- apps/sim/blocks/blocks/note.ts | 47 ++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 27 ++ .../lib/workflows/autolayout/containers.ts | 10 +- .../lib/workflows/autolayout/incremental.ts | 15 +- apps/sim/lib/workflows/autolayout/index.ts | 15 +- apps/sim/lib/workflows/autolayout/targeted.ts | 26 +- apps/sim/lib/workflows/autolayout/utils.ts | 8 + 12 files changed, 436 insertions(+), 39 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx create mode 100644 apps/sim/blocks/blocks/note.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx new file mode 100644 index 00000000000..85930493b14 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx @@ -0,0 +1,238 @@ +import { memo, useCallback, useEffect, useMemo, useRef } from 'react' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { useUpdateNodeInternals, type NodeProps } from 'reactflow' +import { cn } from '@/lib/utils' +import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { useCurrentWorkflow } from '../../hooks' +import { ActionBar } from '../workflow-block/components' +import { useBlockState } from '../workflow-block/hooks' +import type { WorkflowBlockProps } from '../workflow-block/types' +import { usePanelEditorStore } from '@/stores/panel-new/editor/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' + +interface NoteBlockNodeData extends WorkflowBlockProps {} + +const NOTE_MIN_WIDTH = 220 +const NOTE_MIN_HEIGHT = 140 + +const MarkdownContent = memo(function MarkdownContent({ content }: { content: string }) { + return ( + ( +

{children}

+ ), + strong: ({ children }) => {children}, + em: ({ children }) => {children}, + h1: ({ children }) => ( +

+ {children} +

+ ), + h2: ({ children }) => ( +

+ {children} +

+ ), + h3: ({ children }) => ( +

+ {children} +

+ ), + a: ({ href, children }) => ( + + {children} + + ), + ul: ({ children }) => ( + + ), + ol: ({ children }) => ( +
    {children}
+ ), + li: ({ children }) =>
  • {children}
  • , + code: ({ inline, children }: any) => + inline ? ( + {children} + ) : ( + {children} + ), + blockquote: ({ children }) => ( +
    + {children} +
    + ), + }} + > + {content} +
    + ) +}) + +export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps) { + const { type, config, name } = data + const containerRef = useRef(null) + const sizeRef = useRef<{ width: number; height: number } | null>(null) + const updateNodeInternals = useUpdateNodeInternals() + const updateBlockLayoutMetrics = useWorkflowStore((state) => state.updateBlockLayoutMetrics) + + const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId) + const currentBlockId = usePanelEditorStore((state) => state.currentBlockId) + const isFocused = currentBlockId === id + + const currentWorkflow = useCurrentWorkflow() + const { isEnabled, isActive, diffStatus, isDeletedBlock } = useBlockState(id, currentWorkflow, data) + + const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) + const storedValues = useSubBlockStore( + useCallback( + (state) => { + if (!activeWorkflowId) return undefined + return state.workflowValues[activeWorkflowId]?.[id] + }, + [activeWorkflowId, id] + ) + ) + + const noteValues = useMemo(() => { + if (data.isPreview && data.subBlockValues) { + const previewFormatState = data.subBlockValues.format + const previewContentState = data.subBlockValues.content + + const extractedPreviewFormat = + typeof previewFormatState === 'object' && previewFormatState !== null + ? (previewFormatState as { value?: unknown }).value + : previewFormatState + const extractedPreviewContent = + typeof previewContentState === 'object' && previewContentState !== null + ? (previewContentState as { value?: unknown }).value + : previewContentState + + return { + format: typeof extractedPreviewFormat === 'string' ? extractedPreviewFormat : 'plain', + content: typeof extractedPreviewContent === 'string' ? extractedPreviewContent : '', + } + } + + const format = + storedValues && typeof storedValues.format === 'string' + ? storedValues.format + : typeof storedValues?.format === 'object' && storedValues?.format !== null + ? (storedValues.format as { value?: unknown }).value + : undefined + const content = + storedValues && typeof storedValues.content === 'string' + ? storedValues.content + : typeof storedValues?.content === 'object' && storedValues?.content !== null + ? (storedValues.content as { value?: unknown }).value + : undefined + + return { + format: typeof format === 'string' ? format : 'plain', + content: typeof content === 'string' ? content : '', + } + }, [data.isPreview, data.subBlockValues, storedValues]) + + const trimmedContent = noteValues.content?.trim() ?? '' + const isEmpty = trimmedContent.length === 0 + const showMarkdown = noteValues.format === 'markdown' && !isEmpty + + const userPermissions = useUserPermissionsContext() + + useEffect(() => { + const element = containerRef.current + if (!element) return + + const observer = new ResizeObserver((entries) => { + const entry = entries[0] + if (!entry) return + + const width = Math.max(Math.round(entry.contentRect.width), NOTE_MIN_WIDTH) + const height = Math.max(Math.round(entry.contentRect.height), NOTE_MIN_HEIGHT) + + const previous = sizeRef.current + if (!previous || previous.width !== width || previous.height !== height) { + sizeRef.current = { width, height } + updateBlockLayoutMetrics(id, { width, height }) + updateNodeInternals(id) + } + }) + + observer.observe(element) + return () => observer.disconnect() + }, [id, updateBlockLayoutMetrics, updateNodeInternals]) + + const hasRing = + isActive || isFocused || diffStatus === 'new' || diffStatus === 'edited' || isDeletedBlock + const ringStyles = cn( + hasRing && 'ring-[1.75px]', + isActive && 'ring-[#8C10FF] animate-pulse-ring', + isFocused && 'ring-[#33B4FF]', + diffStatus === 'new' && 'ring-[#22C55F]', + diffStatus === 'edited' && 'ring-[#FF6600]', + isDeletedBlock && 'ring-[#EF4444]' + ) + + return ( +
    +
    setCurrentBlockId(id)} + > + + +
    { + event.stopPropagation() + }} + > +
    +
    + +
    + + {name} + +
    +
    + +
    +
    + {isEmpty ? ( +

    Add your note...

    + ) : showMarkdown ? ( + + ) : ( +

    {trimmedContent}

    + )} +
    +
    + {hasRing && ( +
    + )} +
    +
    + ) +}) + diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx index 119417ea578..7da58728c7c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx @@ -146,29 +146,31 @@ export const ActionBar = memo( )} - - - - - - {getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')} - - + {blockType !== 'note' && ( + + + + + + {getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')} + + + )} {!isStarterBlock && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index efd9729d45d..8b9d0b26a29 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -19,6 +19,7 @@ import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/ch import { UserAvatarStack } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack' import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index' import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new' +import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block' import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' import { Terminal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal' import { TrainingControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/training-controls/training-controls' @@ -55,6 +56,7 @@ const logger = createLogger('Workflow') // Define custom node and edge types - memoized outside component to prevent re-creation const nodeTypes: NodeTypes = { workflowBlock: WorkflowBlock, + noteBlock: NoteBlock, subflowNode: SubflowNodeComponent, } const edgeTypes: EdgeTypes = { @@ -1298,13 +1300,32 @@ const WorkflowContent = React.memo(() => { const isActive = activeBlockIds.has(block.id) const isPending = isDebugging && pendingBlocks.includes(block.id) + const measuredWidth = + typeof block.layout?.measuredWidth === 'number' ? block.layout.measuredWidth : undefined + const measuredHeight = + typeof block.layout?.measuredHeight === 'number' ? block.layout.measuredHeight : undefined + + const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock' + const dragHandle = + block.type === 'note' ? '.note-drag-handle' : '.workflow-drag-handle' + + const defaultWidth = + block.type === 'note' + ? Math.max(measuredWidth ?? block.data?.width ?? 260, 200) + : 250 + + const defaultHeight = + block.type === 'note' + ? Math.max(measuredHeight ?? block.height ?? 160, 120) + : Math.max(block.height || 100, 100) + // Create stable node object - React Flow will handle shallow comparison nodeArray.push({ id: block.id, - type: 'workflowBlock', + type: nodeType, position, parentId: block.data?.parentId, - dragHandle: '.workflow-drag-handle', + dragHandle, extent: (() => { // Clamp children to subflow body (exclude header) const parentId = block.data?.parentId as string | undefined @@ -1332,8 +1353,8 @@ const WorkflowContent = React.memo(() => { isPending, }, // Include dynamic dimensions for container resizing calculations (must match rendered size) - width: 250, // Standard width - matches w-[250px] in workflow-block.tsx - height: Math.max(block.height || 100, 100), // Use actual height with minimum + width: defaultWidth, + height: defaultHeight, }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx index c6dd703ddd8..e524e8dd715 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx @@ -15,6 +15,7 @@ import 'reactflow/dist/style.css' import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' +import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block' import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' @@ -39,6 +40,7 @@ interface WorkflowPreviewProps { // Define node types - the components now handle preview mode internally const nodeTypes: NodeTypes = { workflowBlock: WorkflowBlock, + noteBlock: NoteBlock, subflowNode: SubflowNodeComponent, } @@ -179,9 +181,11 @@ export function WorkflowPreview({ const subBlocksClone = block.subBlocks ? cloneDeep(block.subBlocks) : {} + const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock' + nodeArray.push({ id: blockId, - type: 'workflowBlock', + type: nodeType, position: absolutePosition, draggable: false, data: { @@ -204,9 +208,11 @@ export function WorkflowPreview({ const childConfig = getBlock(childBlock.type) if (childConfig) { + const childNodeType = childBlock.type === 'note' ? 'noteBlock' : 'workflowBlock' + nodeArray.push({ id: childId, - type: 'workflowBlock', + type: childNodeType, position: { x: block.position.x + 50, y: block.position.y + (childBlock.position?.y || 100), diff --git a/apps/sim/blocks/blocks/note.ts b/apps/sim/blocks/blocks/note.ts new file mode 100644 index 00000000000..2335058e2b4 --- /dev/null +++ b/apps/sim/blocks/blocks/note.ts @@ -0,0 +1,47 @@ +import { NoteIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' + +export const NoteBlock: BlockConfig = { + type: 'note', + name: 'Note', + description: 'Add contextual annotations directly onto the workflow canvas.', + longDescription: + 'Use Note blocks to document decisions, share instructions, or leave context for collaborators directly on the workflow canvas. Notes support both plain text and Markdown rendering.', + category: 'blocks', + bgColor: '#F59E0B', + icon: NoteIcon, + subBlocks: [ + { + id: 'format', + title: 'Display Format', + type: 'dropdown', + options: [ + { label: 'Plain text', id: 'plain' }, + { label: 'Markdown', id: 'markdown' }, + ], + value: () => 'plain', + description: 'Choose how the note should render on the canvas.', + }, + { + id: 'content', + title: 'Content', + type: 'long-input', + rows: 8, + placeholder: 'Add context or instructions for collaborators...', + description: 'Write your note using plain text or Markdown depending on the selected format.', + }, + ], + tools: { access: [] }, + inputs: { + format: { + type: 'string', + description: 'Render mode for the note content.', + }, + content: { + type: 'string', + description: 'Text for the note.', + }, + }, + outputs: {}, +} + diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index d72f6c41218..74eb1cc3b4a 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -48,6 +48,7 @@ import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams' import { MistralParseBlock } from '@/blocks/blocks/mistral_parse' import { MongoDBBlock } from '@/blocks/blocks/mongodb' import { MySQLBlock } from '@/blocks/blocks/mysql' +import { NoteBlock } from '@/blocks/blocks/note' import { NotionBlock } from '@/blocks/blocks/notion' import { OneDriveBlock } from '@/blocks/blocks/onedrive' import { OpenAIBlock } from '@/blocks/blocks/openai' @@ -145,6 +146,7 @@ export const registry: Record = { mistral_parse: MistralParseBlock, mongodb: MongoDBBlock, mysql: MySQLBlock, + note: NoteBlock, notion: NotionBlock, openai: OpenAIBlock, outlook: OutlookBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 71ec0579307..11e0b6ada36 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -165,6 +165,33 @@ export function ConditionalIcon(props: SVGProps) { ) } +export function NoteIcon(props: SVGProps) { + return ( + + + + + + + ) +} + export function AirplaneIcon(props: SVGProps) { return ( = {} + const layoutChildIds: string[] = [] for (const childId of childIds) { - childBlocks[childId] = blocks[childId] + const childBlock = blocks[childId] + if (!childBlock) continue + if (shouldSkipAutoLayout(childBlock)) continue + childBlocks[childId] = childBlock + layoutChildIds.push(childId) } const childEdges = edges.filter( - (edge) => childIds.includes(edge.source) && childIds.includes(edge.target) + (edge) => layoutChildIds.includes(edge.source) && layoutChildIds.includes(edge.target) ) if (Object.keys(childBlocks).length === 0) { diff --git a/apps/sim/lib/workflows/autolayout/incremental.ts b/apps/sim/lib/workflows/autolayout/incremental.ts index c56c4f06637..0add1dfe196 100644 --- a/apps/sim/lib/workflows/autolayout/incremental.ts +++ b/apps/sim/lib/workflows/autolayout/incremental.ts @@ -1,7 +1,7 @@ import { createLogger } from '@/lib/logs/console/logger' import type { BlockState } from '@/stores/workflows/workflow/types' import type { AdjustmentOptions, Edge } from './types' -import { boxesOverlap, createBoundingBox, getBlockMetrics } from './utils' +import { boxesOverlap, createBoundingBox, getBlockMetrics, shouldSkipAutoLayout } from './utils' const logger = createLogger('AutoLayout:Incremental') @@ -19,6 +19,14 @@ export function adjustForNewBlock( return } + if (shouldSkipAutoLayout(newBlock)) { + logger.debug('Skipping incremental layout for block excluded from auto layout', { + newBlockId, + type: newBlock.type, + }) + return + } + const shiftSpacing = options.horizontalSpacing ?? DEFAULT_SHIFT_SPACING const incomingEdges = edges.filter((e) => e.target === newBlockId) @@ -78,6 +86,7 @@ export function adjustForNewBlock( for (const [id, block] of Object.entries(blocks)) { if (id === newBlockId) continue if (block.data?.parentId) continue + if (shouldSkipAutoLayout(block)) continue if (block.position.x >= newBlock.position.x) { const blockMetrics = getBlockMetrics(block) @@ -105,7 +114,9 @@ export function adjustForNewBlock( } export function compactHorizontally(blocks: Record, edges: Edge[]): void { - const blockArray = Object.values(blocks).filter((b) => !b.data?.parentId) + const blockArray = Object.values(blocks).filter( + (b) => !b.data?.parentId && !shouldSkipAutoLayout(b) + ) blockArray.sort((a, b) => a.position.x - b.position.x) diff --git a/apps/sim/lib/workflows/autolayout/index.ts b/apps/sim/lib/workflows/autolayout/index.ts index f48dda601b8..351dce4645d 100644 --- a/apps/sim/lib/workflows/autolayout/index.ts +++ b/apps/sim/lib/workflows/autolayout/index.ts @@ -5,7 +5,7 @@ import { adjustForNewBlock as adjustForNewBlockInternal, compactHorizontally } f import { assignLayers, groupByLayer } from './layering' import { calculatePositions } from './positioning' import type { AdjustmentOptions, Edge, LayoutOptions, LayoutResult, Loop, Parallel } from './types' -import { getBlocksByParent, prepareBlockMetrics } from './utils' +import { getBlocksByParent, prepareBlockMetrics, shouldSkipAutoLayout } from './utils' const logger = createLogger('AutoLayout') @@ -28,13 +28,20 @@ export function applyAutoLayout( const { root: rootBlockIds } = getBlocksByParent(blocksCopy) + const layoutRootIds = rootBlockIds.filter((id) => { + const block = blocksCopy[id] + if (!block) return false + if (shouldSkipAutoLayout(block)) return false + return true + }) + const rootBlocks: Record = {} - for (const id of rootBlockIds) { + for (const id of layoutRootIds) { rootBlocks[id] = blocksCopy[id] } const rootEdges = edges.filter( - (edge) => rootBlockIds.includes(edge.source) && rootBlockIds.includes(edge.target) + (edge) => layoutRootIds.includes(edge.source) && layoutRootIds.includes(edge.target) ) if (Object.keys(rootBlocks).length > 0) { @@ -102,4 +109,4 @@ export function adjustForNewBlock( export type { LayoutOptions, LayoutResult, AdjustmentOptions, Edge, Loop, Parallel } export type { TargetedLayoutOptions } from './targeted' export { applyTargetedLayout, transferBlockHeights } from './targeted' -export { getBlockMetrics, isContainerType } from './utils' +export { getBlockMetrics, isContainerType, shouldSkipAutoLayout } from './utils' diff --git a/apps/sim/lib/workflows/autolayout/targeted.ts b/apps/sim/lib/workflows/autolayout/targeted.ts index 88f7e8a12c0..7831ec79e05 100644 --- a/apps/sim/lib/workflows/autolayout/targeted.ts +++ b/apps/sim/lib/workflows/autolayout/targeted.ts @@ -15,6 +15,7 @@ import { prepareBlockMetrics, ROOT_PADDING_X, ROOT_PADDING_Y, + shouldSkipAutoLayout, } from './utils' const logger = createLogger('AutoLayout:Targeted') @@ -71,17 +72,33 @@ function layoutGroup( const parentBlock = parentId ? blocks[parentId] : undefined + const layoutEligibleChildIds = childIds.filter((id) => { + const block = blocks[id] + if (!block) return false + if (shouldSkipAutoLayout(block)) return false + return true + }) + + if (layoutEligibleChildIds.length === 0) { + if (parentBlock) { + updateContainerDimensions(parentBlock, childIds, blocks) + } + return + } + const requestedLayout = childIds.filter((id) => { const block = blocks[id] if (!block) return false // Never reposition containers, only update their dimensions if (isContainerType(block.type)) return false + if (shouldSkipAutoLayout(block)) return false return changedSet.has(id) }) const missingPositions = childIds.filter((id) => { const block = blocks[id] if (!block) return false // Containers with missing positions should still get positioned + if (shouldSkipAutoLayout(block)) return false return !hasPosition(block) }) const needsLayoutSet = new Set([...requestedLayout, ...missingPositions]) @@ -106,7 +123,7 @@ function layoutGroup( } const layoutPositions = computeLayoutPositions( - childIds, + layoutEligibleChildIds, blocks, edges, parentBlock, @@ -125,7 +142,9 @@ function layoutGroup( let offsetX = 0 let offsetY = 0 - const anchorId = childIds.find((id) => !needsLayout.includes(id) && layoutPositions.has(id)) + const anchorId = layoutEligibleChildIds.find( + (id) => !needsLayout.includes(id) && layoutPositions.has(id) + ) if (anchorId) { const oldPos = oldPositions.get(anchorId) @@ -272,6 +291,9 @@ function updateContainerDimensions( for (const id of childIds) { const child = blocks[id] if (!child) continue + if (shouldSkipAutoLayout(child)) { + continue + } const metrics = getBlockMetrics(child) minX = Math.min(minX, child.position.x) diff --git a/apps/sim/lib/workflows/autolayout/utils.ts b/apps/sim/lib/workflows/autolayout/utils.ts index f1feccaf7ef..5c89e259329 100644 --- a/apps/sim/lib/workflows/autolayout/utils.ts +++ b/apps/sim/lib/workflows/autolayout/utils.ts @@ -22,6 +22,14 @@ export function isContainerType(blockType: string): boolean { return blockType === 'loop' || blockType === 'parallel' } +export function isNoteBlockType(blockType: string): boolean { + return blockType === 'note' +} + +export function shouldSkipAutoLayout(block: BlockState): boolean { + return isNoteBlockType(block.type) +} + function getContainerMetrics(block: BlockState): BlockMetrics { const measuredWidth = block.layout?.measuredWidth const measuredHeight = block.layout?.measuredHeight From 257087adfa3c2211b6143b545594c653940e5822 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 11 Nov 2025 12:39:21 -0800 Subject: [PATCH 02/11] v2 --- .../components/note-block/note-block.tsx | 130 +++++++----------- .../workflow-block/workflow-block.tsx | 2 + .../[workspaceId]/w/[workflowId]/workflow.tsx | 9 +- apps/sim/blocks/blocks/note.ts | 1 - .../lib/workflows/autolayout/containers.ts | 12 +- apps/sim/lib/workflows/autolayout/index.ts | 9 +- apps/sim/lib/workflows/autolayout/targeted.ts | 16 +-- apps/sim/lib/workflows/autolayout/utils.ts | 18 ++- 8 files changed, 82 insertions(+), 115 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx index 85930493b14..5a851c9362f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx @@ -1,75 +1,66 @@ import { memo, useCallback, useEffect, useMemo, useRef } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' -import { useUpdateNodeInternals, type NodeProps } from 'reactflow' +import { type NodeProps, useUpdateNodeInternals } from 'reactflow' import { cn } from '@/lib/utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { usePanelEditorStore } from '@/stores/panel-new/editor/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { useCurrentWorkflow } from '../../hooks' import { ActionBar } from '../workflow-block/components' import { useBlockState } from '../workflow-block/hooks' import type { WorkflowBlockProps } from '../workflow-block/types' -import { usePanelEditorStore } from '@/stores/panel-new/editor/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { useWorkflowStore } from '@/stores/workflows/workflow/store' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' interface NoteBlockNodeData extends WorkflowBlockProps {} const NOTE_MIN_WIDTH = 220 const NOTE_MIN_HEIGHT = 140 -const MarkdownContent = memo(function MarkdownContent({ content }: { content: string }) { +/** + * Extract string value from subblock value object or primitive + */ +function extractFieldValue(rawValue: unknown): string | undefined { + if (typeof rawValue === 'string') return rawValue + if (rawValue && typeof rawValue === 'object' && 'value' in rawValue) { + const candidate = (rawValue as { value?: unknown }).value + return typeof candidate === 'string' ? candidate : undefined + } + return undefined +} + +/** + * Compact markdown renderer for note blocks with tight spacing + */ +const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }) { return ( ( -

    {children}

    - ), - strong: ({ children }) => {children}, - em: ({ children }) => {children}, - h1: ({ children }) => ( -

    - {children} -

    - ), - h2: ({ children }) => ( -

    - {children} -

    - ), - h3: ({ children }) => ( -

    - {children} -

    - ), - a: ({ href, children }) => ( - - {children} - - ), - ul: ({ children }) => ( -
      {children}
    - ), - ol: ({ children }) => ( -
      {children}
    - ), - li: ({ children }) =>
  • {children}
  • , + p: ({ children }) =>

    {children}

    , + h1: ({ children }) =>

    {children}

    , + h2: ({ children }) =>

    {children}

    , + h3: ({ children }) =>

    {children}

    , + h4: ({ children }) =>

    {children}

    , + ul: ({ children }) =>
      {children}
    , + ol: ({ children }) =>
      {children}
    , + li: ({ children }) =>
  • {children}
  • , code: ({ inline, children }: any) => inline ? ( {children} ) : ( {children} ), - blockquote: ({ children }) => ( -
    + a: ({ href, children }) => ( + {children} -
    + + ), + strong: ({ children }) => {children}, + em: ({ children }) => {children}, + blockquote: ({ children }) => ( +
    {children}
    ), }} > @@ -90,7 +81,11 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps state.activeWorkflowId) const storedValues = useSubBlockStore( @@ -105,36 +100,16 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps { if (data.isPreview && data.subBlockValues) { - const previewFormatState = data.subBlockValues.format - const previewContentState = data.subBlockValues.content - - const extractedPreviewFormat = - typeof previewFormatState === 'object' && previewFormatState !== null - ? (previewFormatState as { value?: unknown }).value - : previewFormatState - const extractedPreviewContent = - typeof previewContentState === 'object' && previewContentState !== null - ? (previewContentState as { value?: unknown }).value - : previewContentState - + const extractedPreviewFormat = extractFieldValue(data.subBlockValues.format) + const extractedPreviewContent = extractFieldValue(data.subBlockValues.content) return { format: typeof extractedPreviewFormat === 'string' ? extractedPreviewFormat : 'plain', content: typeof extractedPreviewContent === 'string' ? extractedPreviewContent : '', } } - const format = - storedValues && typeof storedValues.format === 'string' - ? storedValues.format - : typeof storedValues?.format === 'object' && storedValues?.format !== null - ? (storedValues.format as { value?: unknown }).value - : undefined - const content = - storedValues && typeof storedValues.content === 'string' - ? storedValues.content - : typeof storedValues?.content === 'object' && storedValues?.content !== null - ? (storedValues.content as { value?: unknown }).value - : undefined + const format = extractFieldValue(storedValues?.format) + const content = extractFieldValue(storedValues?.content) return { format: typeof format === 'string' ? format : 'plain', @@ -142,8 +117,8 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps
    -
    +
    {isEmpty ? ( -

    Add your note...

    +

    Add a note...

    ) : showMarkdown ? ( - + ) : ( -

    {trimmedContent}

    +

    {content}

    )}
    @@ -235,4 +210,3 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps ) }) - diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index d617b8079ae..6307f70d550 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -1,5 +1,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useParams } from 'next/navigation' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' import { Handle, type NodeProps, Position, useUpdateNodeInternals } from 'reactflow' import { Badge } from '@/components/emcn/components/badge/badge' import { Tooltip } from '@/components/emcn/components/tooltip/tooltip' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 8b9d0b26a29..f8e6fbd9f99 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -18,8 +18,8 @@ import { DiffControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/compo import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat' import { UserAvatarStack } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack' import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index' -import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new' import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block' +import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new' import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' import { Terminal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal' import { TrainingControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/training-controls/training-controls' @@ -1306,13 +1306,10 @@ const WorkflowContent = React.memo(() => { typeof block.layout?.measuredHeight === 'number' ? block.layout.measuredHeight : undefined const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock' - const dragHandle = - block.type === 'note' ? '.note-drag-handle' : '.workflow-drag-handle' + const dragHandle = block.type === 'note' ? '.note-drag-handle' : '.workflow-drag-handle' const defaultWidth = - block.type === 'note' - ? Math.max(measuredWidth ?? block.data?.width ?? 260, 200) - : 250 + block.type === 'note' ? Math.max(measuredWidth ?? block.data?.width ?? 260, 200) : 250 const defaultHeight = block.type === 'note' diff --git a/apps/sim/blocks/blocks/note.ts b/apps/sim/blocks/blocks/note.ts index 2335058e2b4..a030fde10c8 100644 --- a/apps/sim/blocks/blocks/note.ts +++ b/apps/sim/blocks/blocks/note.ts @@ -44,4 +44,3 @@ export const NoteBlock: BlockConfig = { }, outputs: {}, } - diff --git a/apps/sim/lib/workflows/autolayout/containers.ts b/apps/sim/lib/workflows/autolayout/containers.ts index d00ae5a802b..b744dd26676 100644 --- a/apps/sim/lib/workflows/autolayout/containers.ts +++ b/apps/sim/lib/workflows/autolayout/containers.ts @@ -9,9 +9,9 @@ import { CONTAINER_PADDING_Y, DEFAULT_CONTAINER_HEIGHT, DEFAULT_CONTAINER_WIDTH, + filterLayoutEligibleBlockIds, getBlocksByParent, prepareBlockMetrics, - shouldSkipAutoLayout, } from './utils' const logger = createLogger('AutoLayout:Containers') @@ -36,14 +36,10 @@ export function layoutContainers( logger.debug('Processing container', { parentId, childCount: childIds.length }) + const layoutChildIds = filterLayoutEligibleBlockIds(childIds, blocks) const childBlocks: Record = {} - const layoutChildIds: string[] = [] - for (const childId of childIds) { - const childBlock = blocks[childId] - if (!childBlock) continue - if (shouldSkipAutoLayout(childBlock)) continue - childBlocks[childId] = childBlock - layoutChildIds.push(childId) + for (const childId of layoutChildIds) { + childBlocks[childId] = blocks[childId] } const childEdges = edges.filter( diff --git a/apps/sim/lib/workflows/autolayout/index.ts b/apps/sim/lib/workflows/autolayout/index.ts index 351dce4645d..54d883d3364 100644 --- a/apps/sim/lib/workflows/autolayout/index.ts +++ b/apps/sim/lib/workflows/autolayout/index.ts @@ -5,7 +5,7 @@ import { adjustForNewBlock as adjustForNewBlockInternal, compactHorizontally } f import { assignLayers, groupByLayer } from './layering' import { calculatePositions } from './positioning' import type { AdjustmentOptions, Edge, LayoutOptions, LayoutResult, Loop, Parallel } from './types' -import { getBlocksByParent, prepareBlockMetrics, shouldSkipAutoLayout } from './utils' +import { filterLayoutEligibleBlockIds, getBlocksByParent, prepareBlockMetrics } from './utils' const logger = createLogger('AutoLayout') @@ -28,12 +28,7 @@ export function applyAutoLayout( const { root: rootBlockIds } = getBlocksByParent(blocksCopy) - const layoutRootIds = rootBlockIds.filter((id) => { - const block = blocksCopy[id] - if (!block) return false - if (shouldSkipAutoLayout(block)) return false - return true - }) + const layoutRootIds = filterLayoutEligibleBlockIds(rootBlockIds, blocksCopy) const rootBlocks: Record = {} for (const id of layoutRootIds) { diff --git a/apps/sim/lib/workflows/autolayout/targeted.ts b/apps/sim/lib/workflows/autolayout/targeted.ts index 7831ec79e05..0f808f7fdd9 100644 --- a/apps/sim/lib/workflows/autolayout/targeted.ts +++ b/apps/sim/lib/workflows/autolayout/targeted.ts @@ -9,6 +9,7 @@ import { CONTAINER_PADDING_Y, DEFAULT_CONTAINER_HEIGHT, DEFAULT_CONTAINER_WIDTH, + filterLayoutEligibleBlockIds, getBlockMetrics, getBlocksByParent, isContainerType, @@ -72,12 +73,7 @@ function layoutGroup( const parentBlock = parentId ? blocks[parentId] : undefined - const layoutEligibleChildIds = childIds.filter((id) => { - const block = blocks[id] - if (!block) return false - if (shouldSkipAutoLayout(block)) return false - return true - }) + const layoutEligibleChildIds = filterLayoutEligibleBlockIds(childIds, blocks) if (layoutEligibleChildIds.length === 0) { if (parentBlock) { @@ -86,19 +82,17 @@ function layoutGroup( return } - const requestedLayout = childIds.filter((id) => { + const requestedLayout = layoutEligibleChildIds.filter((id) => { const block = blocks[id] if (!block) return false // Never reposition containers, only update their dimensions if (isContainerType(block.type)) return false - if (shouldSkipAutoLayout(block)) return false return changedSet.has(id) }) - const missingPositions = childIds.filter((id) => { + const missingPositions = layoutEligibleChildIds.filter((id) => { const block = blocks[id] if (!block) return false // Containers with missing positions should still get positioned - if (shouldSkipAutoLayout(block)) return false return !hasPosition(block) }) const needsLayoutSet = new Set([...requestedLayout, ...missingPositions]) @@ -116,7 +110,7 @@ function layoutGroup( const oldPositions = new Map() - for (const id of childIds) { + for (const id of layoutEligibleChildIds) { const block = blocks[id] if (!block) continue oldPositions.set(id, { ...block.position }) diff --git a/apps/sim/lib/workflows/autolayout/utils.ts b/apps/sim/lib/workflows/autolayout/utils.ts index 5c89e259329..8d6e83538a2 100644 --- a/apps/sim/lib/workflows/autolayout/utils.ts +++ b/apps/sim/lib/workflows/autolayout/utils.ts @@ -18,16 +18,26 @@ function resolveNumeric(value: number | undefined, fallback: number): number { return typeof value === 'number' && Number.isFinite(value) ? value : fallback } +const AUTO_LAYOUT_EXCLUDED_TYPES = new Set(['note']) + export function isContainerType(blockType: string): boolean { return blockType === 'loop' || blockType === 'parallel' } -export function isNoteBlockType(blockType: string): boolean { - return blockType === 'note' +export function shouldSkipAutoLayout(block?: BlockState): boolean { + if (!block) return true + return AUTO_LAYOUT_EXCLUDED_TYPES.has(block.type) } -export function shouldSkipAutoLayout(block: BlockState): boolean { - return isNoteBlockType(block.type) +export function filterLayoutEligibleBlockIds( + blockIds: string[], + blocks: Record +): string[] { + return blockIds.filter((id) => { + const block = blocks[id] + if (!block) return false + return !shouldSkipAutoLayout(block) + }) } function getContainerMetrics(block: BlockState): BlockMetrics { From 03b9dbb5bc4f893ce90c71b2f240ab4e685af3f9 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 11 Nov 2025 12:56:19 -0800 Subject: [PATCH 03/11] Lint --- .../components/note-block/note-block.tsx | 51 ++++++++++++++----- .../workflow-block/workflow-block.tsx | 2 - 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx index 5a851c9362f..079efae28c8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx @@ -1,7 +1,7 @@ import { memo, useCallback, useEffect, useMemo, useRef } from 'react' import ReactMarkdown from 'react-markdown' -import remarkGfm from 'remark-gfm' import { type NodeProps, useUpdateNodeInternals } from 'reactflow' +import remarkGfm from 'remark-gfm' import { cn } from '@/lib/utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { usePanelEditorStore } from '@/stores/panel-new/editor/store' @@ -38,29 +38,52 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }

    {children}

    , - h1: ({ children }) =>

    {children}

    , - h2: ({ children }) =>

    {children}

    , - h3: ({ children }) =>

    {children}

    , - h4: ({ children }) =>

    {children}

    , - ul: ({ children }) =>
      {children}
    , - ol: ({ children }) =>
      {children}
    , + p: ({ children }) =>

    {children}

    , + h1: ({ children }) => ( +

    {children}

    + ), + h2: ({ children }) => ( +

    {children}

    + ), + h3: ({ children }) => ( +

    {children}

    + ), + h4: ({ children }) => ( +

    {children}

    + ), + ul: ({ children }) => ( +
      {children}
    + ), + ol: ({ children }) => ( +
      {children}
    + ), li: ({ children }) =>
  • {children}
  • , code: ({ inline, children }: any) => inline ? ( - {children} + + {children} + ) : ( - {children} + + {children} + ), a: ({ href, children }) => ( - + {children} ), strong: ({ children }) => {children}, em: ({ children }) => {children}, blockquote: ({ children }) => ( -
    {children}
    +
    + {children} +
    ), }} > @@ -197,7 +220,9 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps ) : ( -

    {content}

    +

    + {content} +

    )}
    diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 6307f70d550..d617b8079ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -1,7 +1,5 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useParams } from 'next/navigation' -import ReactMarkdown from 'react-markdown' -import remarkGfm from 'remark-gfm' import { Handle, type NodeProps, Position, useUpdateNodeInternals } from 'reactflow' import { Badge } from '@/components/emcn/components/badge/badge' import { Tooltip } from '@/components/emcn/components/tooltip/tooltip' From 0429636a070f229352f305450eaee6191c18023b Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 11 Nov 2025 13:04:45 -0800 Subject: [PATCH 04/11] Consolidate into hook --- .../components/note-block/note-block.tsx | 57 +++++------- .../workflow-block/workflow-block.tsx | 93 ++++++++----------- .../w/[workflowId]/hooks/index.ts | 1 + .../hooks/use-block-dimensions.ts | 45 +++++++++ 4 files changed, 107 insertions(+), 89 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx index 079efae28c8..0153576a97a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx @@ -1,23 +1,20 @@ -import { memo, useCallback, useEffect, useMemo, useRef } from 'react' +import { memo, useCallback, useMemo } from 'react' import ReactMarkdown from 'react-markdown' -import { type NodeProps, useUpdateNodeInternals } from 'reactflow' +import type { NodeProps } from 'reactflow' import remarkGfm from 'remark-gfm' import { cn } from '@/lib/utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { usePanelEditorStore } from '@/stores/panel-new/editor/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' -import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { useCurrentWorkflow } from '../../hooks' +import { useBlockDimensions } from '../../hooks/use-block-dimensions' import { ActionBar } from '../workflow-block/components' import { useBlockState } from '../workflow-block/hooks' import type { WorkflowBlockProps } from '../workflow-block/types' interface NoteBlockNodeData extends WorkflowBlockProps {} -const NOTE_MIN_WIDTH = 220 -const NOTE_MIN_HEIGHT = 140 - /** * Extract string value from subblock value object or primitive */ @@ -94,10 +91,6 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string } export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps) { const { type, config, name } = data - const containerRef = useRef(null) - const sizeRef = useRef<{ width: number; height: number } | null>(null) - const updateNodeInternals = useUpdateNodeInternals() - const updateBlockLayoutMetrics = useWorkflowStore((state) => state.updateBlockLayoutMetrics) const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId) const currentBlockId = usePanelEditorStore((state) => state.currentBlockId) @@ -146,28 +139,27 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps { - const element = containerRef.current - if (!element) return - - const observer = new ResizeObserver((entries) => { - const entry = entries[0] - if (!entry) return - - const width = Math.max(Math.round(entry.contentRect.width), NOTE_MIN_WIDTH) - const height = Math.max(Math.round(entry.contentRect.height), NOTE_MIN_HEIGHT) - - const previous = sizeRef.current - if (!previous || previous.width !== width || previous.height !== height) { - sizeRef.current = { width, height } - updateBlockLayoutMetrics(id, { width, height }) - updateNodeInternals(id) - } - }) - - observer.observe(element) - return () => observer.disconnect() - }, [id, updateBlockLayoutMetrics, updateNodeInternals]) + /** + * Calculate deterministic dimensions based on content structure. + * Uses fixed width and computed height to avoid ResizeObserver jitter. + */ + useBlockDimensions({ + blockId: id, + calculateDimensions: () => { + const FIXED_WIDTH = 250 + const HEADER_HEIGHT = 40 + const CONTENT_PADDING = 14 + const MIN_CONTENT_HEIGHT = 20 + const BASE_CONTENT_HEIGHT = 60 + + // Use minimum height for empty notes, base height for content + const contentHeight = isEmpty ? MIN_CONTENT_HEIGHT : BASE_CONTENT_HEIGHT + const calculatedHeight = HEADER_HEIGHT + CONTENT_PADDING + contentHeight + + return { width: FIXED_WIDTH, height: calculatedHeight } + }, + dependencies: [isEmpty], + }) const hasRing = isActive || isFocused || diffStatus === 'new' || diffStatus === 'edited' || isDeletedBlock @@ -183,7 +175,6 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps
    (null) - const updateNodeInternals = useUpdateNodeInternals() const params = useParams() const currentWorkflowId = params.workflowId as string @@ -368,7 +366,6 @@ export const WorkflowBlock = memo(function WorkflowBlock({ } }, [id, collaborativeSetSubblockValue]) - const updateBlockLayoutMetrics = useWorkflowStore((state) => state.updateBlockLayoutMetrics) const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId) const currentBlockId = usePanelEditorStore((state) => state.currentBlockId) @@ -378,14 +375,6 @@ export const WorkflowBlock = memo(function WorkflowBlock({ const isStarterBlock = type === 'starter' const isWebhookTriggerBlock = type === 'webhook' || type === 'generic_webhook' - /** - * Update node internals when handles change to ensure ReactFlow - * correctly calculates connection points. - */ - useEffect(() => { - updateNodeInternals(id) - }, [id, horizontalHandles, updateNodeInternals]) - /** * Subscribe to this block's subblock values to track changes for conditional rendering * of subblocks based on their conditions. @@ -605,51 +594,43 @@ export const WorkflowBlock = memo(function WorkflowBlock({ * * Width is a fixed 250px for workflow blocks. */ - useEffect(() => { - // Only workflow blocks (non-subflow) render here, width is constant - const FIXED_WIDTH = 250 - const HEADER_HEIGHT = 40 - const CONTENT_PADDING = 16 - const ROW_HEIGHT = 29 - - const shouldShowDefaultHandles = - config.category !== 'triggers' && type !== 'starter' && !displayTriggerMode - const hasContentBelowHeader = subBlockRows.length > 0 || shouldShowDefaultHandles - - // Count rows based on block type and whether default handles section is shown - const defaultHandlesRow = shouldShowDefaultHandles ? 1 : 0 - - let rowsCount = 0 - if (type === 'condition') { - rowsCount = conditionRows.length + defaultHandlesRow - } else { - const subblockRowCount = subBlockRows.reduce((acc, row) => acc + row.length, 0) - rowsCount = subblockRowCount + defaultHandlesRow - } - - const contentHeight = hasContentBelowHeader ? CONTENT_PADDING + rowsCount * ROW_HEIGHT : 0 - const calculatedHeight = Math.max(HEADER_HEIGHT + contentHeight, 100) + useBlockDimensions({ + blockId: id, + calculateDimensions: () => { + const FIXED_WIDTH = 250 + const HEADER_HEIGHT = 40 + const CONTENT_PADDING = 16 + const ROW_HEIGHT = 29 + + const shouldShowDefaultHandles = + config.category !== 'triggers' && type !== 'starter' && !displayTriggerMode + const hasContentBelowHeader = subBlockRows.length > 0 || shouldShowDefaultHandles + + // Count rows based on block type and whether default handles section is shown + const defaultHandlesRow = shouldShowDefaultHandles ? 1 : 0 + + let rowsCount = 0 + if (type === 'condition') { + rowsCount = conditionRows.length + defaultHandlesRow + } else { + const subblockRowCount = subBlockRows.reduce((acc, row) => acc + row.length, 0) + rowsCount = subblockRowCount + defaultHandlesRow + } - const prevHeight = - typeof currentStoreBlock?.height === 'number' ? currentStoreBlock.height : undefined - const prevWidth = 250 // fixed across the app for workflow blocks + const contentHeight = hasContentBelowHeader ? CONTENT_PADDING + rowsCount * ROW_HEIGHT : 0 + const calculatedHeight = Math.max(HEADER_HEIGHT + contentHeight, 100) - if (prevHeight !== calculatedHeight || prevWidth !== FIXED_WIDTH) { - updateBlockLayoutMetrics(id, { width: FIXED_WIDTH, height: calculatedHeight }) - updateNodeInternals(id) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - id, - type, - config.category, - displayTriggerMode, - subBlockRows.length, - conditionRows.length, - currentStoreBlock?.height, - updateBlockLayoutMetrics, - updateNodeInternals, - ]) + return { width: FIXED_WIDTH, height: calculatedHeight } + }, + dependencies: [ + type, + config.category, + displayTriggerMode, + subBlockRows.length, + conditionRows.length, + horizontalHandles, + ], + }) const showWebhookIndicator = (isStarterBlock || isWebhookTriggerBlock) && isWebhookConfigured const shouldShowScheduleBadge = diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts index 951cd41d3b3..239253f31fe 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts @@ -1,4 +1,5 @@ export { useAutoLayout } from './use-auto-layout' +export { useBlockDimensions } from './use-block-dimensions' export { type CurrentWorkflow, useCurrentWorkflow } from './use-current-workflow' export { useNodeUtilities } from './use-node-utilities' export { useScrollManagement } from './use-scroll-management' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions.ts new file mode 100644 index 00000000000..aaa20254231 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions.ts @@ -0,0 +1,45 @@ +import { useEffect, useRef } from 'react' +import { useUpdateNodeInternals } from 'reactflow' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' + +interface BlockDimensions { + width: number + height: number +} + +interface UseBlockDimensionsOptions { + blockId: string + calculateDimensions: () => BlockDimensions + dependencies: React.DependencyList +} + +/** + * Hook to manage deterministic block dimensions without ResizeObserver. + * Calculates dimensions based on content structure and updates the store. + * + * @param options - Configuration for dimension calculation + * @param options.blockId - The ID of the block + * @param options.calculateDimensions - Function that returns current dimensions + * @param options.dependencies - Dependencies that trigger recalculation + */ +export function useBlockDimensions({ + blockId, + calculateDimensions, + dependencies, +}: UseBlockDimensionsOptions) { + const updateNodeInternals = useUpdateNodeInternals() + const updateBlockLayoutMetrics = useWorkflowStore((state) => state.updateBlockLayoutMetrics) + const previousDimensions = useRef(null) + + useEffect(() => { + const dimensions = calculateDimensions() + const previous = previousDimensions.current + + if (!previous || previous.width !== dimensions.width || previous.height !== dimensions.height) { + previousDimensions.current = dimensions + updateBlockLayoutMetrics(blockId, dimensions) + updateNodeInternals(blockId) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [blockId, updateBlockLayoutMetrics, updateNodeInternals, ...dependencies]) +} From 30348d3507d749b71cf5a017a4cf861d44d4b3fb Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 11 Nov 2025 13:06:24 -0800 Subject: [PATCH 05/11] Simplify workflow code --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index f8e6fbd9f99..1931b55a46e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -1300,22 +1300,10 @@ const WorkflowContent = React.memo(() => { const isActive = activeBlockIds.has(block.id) const isPending = isDebugging && pendingBlocks.includes(block.id) - const measuredWidth = - typeof block.layout?.measuredWidth === 'number' ? block.layout.measuredWidth : undefined - const measuredHeight = - typeof block.layout?.measuredHeight === 'number' ? block.layout.measuredHeight : undefined - + // Both note blocks and workflow blocks use deterministic dimensions const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock' const dragHandle = block.type === 'note' ? '.note-drag-handle' : '.workflow-drag-handle' - const defaultWidth = - block.type === 'note' ? Math.max(measuredWidth ?? block.data?.width ?? 260, 200) : 250 - - const defaultHeight = - block.type === 'note' - ? Math.max(measuredHeight ?? block.height ?? 160, 120) - : Math.max(block.height || 100, 100) - // Create stable node object - React Flow will handle shallow comparison nodeArray.push({ id: block.id, @@ -1350,8 +1338,9 @@ const WorkflowContent = React.memo(() => { isPending, }, // Include dynamic dimensions for container resizing calculations (must match rendered size) - width: defaultWidth, - height: defaultHeight, + // Both note and workflow blocks calculate dimensions deterministically via useBlockDimensions + width: 250, // Standard width for both block types + height: Math.max(block.height || 100, 100), // Use calculated height with minimum }) }) From 29a5c6035f61242ad30ebd0f63738f9a271c4ba5 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 11 Nov 2025 13:07:59 -0800 Subject: [PATCH 06/11] Fix hitl casing --- apps/sim/components/icons.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 11e0b6ada36..fc102db2438 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3894,9 +3894,9 @@ export function HumanInTheLoopIcon(props: SVGProps) { viewBox='0 0 24 24' fill='none' stroke='currentColor' - stroke-width='2' - stroke-linecap='round' - stroke-linejoin='round' + strokeWidth='2' + strokeLinecap='round' + strokeLinejoin='round' > From f259799068b4b471349c9fa8f18536615b807d1f Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 11 Nov 2025 13:10:37 -0800 Subject: [PATCH 07/11] Don't allow edges in note block and explicitly exclude from executor --- .../workspace/[workspaceId]/w/[workflowId]/workflow.tsx | 5 +++++ apps/sim/executor/consts.ts | 8 +++++++- apps/sim/stores/workflows/workflow/store.ts | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 1931b55a46e..33653fbf478 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -1447,6 +1447,11 @@ const WorkflowContent = React.memo(() => { if (!sourceNode || !targetNode) return + // Prevent connections to/from note blocks (annotation-only, non-executable) + if (sourceNode.data?.type === 'note' || targetNode.data?.type === 'note') { + return + } + // Prevent incoming connections to trigger blocks (webhook, schedule, etc.) if (targetNode.data?.config?.category === 'triggers') { return diff --git a/apps/sim/executor/consts.ts b/apps/sim/executor/consts.ts index 46fd3689e3c..879926969e0 100644 --- a/apps/sim/executor/consts.ts +++ b/apps/sim/executor/consts.ts @@ -21,6 +21,8 @@ export enum BlockType { WAIT = 'wait', + NOTE = 'note', + SENTINEL_START = 'sentinel_start', SENTINEL_END = 'sentinel_end', } @@ -31,7 +33,11 @@ export const TRIGGER_BLOCK_TYPES = [ BlockType.TRIGGER, ] as const -export const METADATA_ONLY_BLOCK_TYPES = [BlockType.LOOP, BlockType.PARALLEL] as const +export const METADATA_ONLY_BLOCK_TYPES = [ + BlockType.LOOP, + BlockType.PARALLEL, + BlockType.NOTE, +] as const export type LoopType = 'for' | 'forEach' | 'while' | 'doWhile' diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 3d4b450b83a..5148222496b 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -422,6 +422,14 @@ export const useWorkflowStore = create()( }, addEdge: (edge: Edge) => { + // Prevent connections to/from note blocks (annotation-only, non-executable) + const sourceBlock = get().blocks[edge.source] + const targetBlock = get().blocks[edge.target] + + if (sourceBlock?.type === 'note' || targetBlock?.type === 'note') { + return + } + // Check for duplicate connections const isDuplicate = get().edges.some( (existingEdge) => From 699800e21636c5ed5f2e529f89ab19732ccbd28e Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 11 Nov 2025 13:15:10 -0800 Subject: [PATCH 08/11] Add hooks --- .../components/note-block/note-block.tsx | 31 ++++++------- .../workflow-block/workflow-block.tsx | 44 +++++++------------ .../w/[workflowId]/hooks/index.ts | 2 + .../w/[workflowId]/hooks/use-block-focus.ts | 17 +++++++ .../hooks/use-block-ring-styles.ts | 44 +++++++++++++++++++ 5 files changed, 92 insertions(+), 46 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-focus.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-ring-styles.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx index 0153576a97a..c116546fbfc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx @@ -4,11 +4,14 @@ import type { NodeProps } from 'reactflow' import remarkGfm from 'remark-gfm' import { cn } from '@/lib/utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' -import { usePanelEditorStore } from '@/stores/panel-new/editor/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' -import { useCurrentWorkflow } from '../../hooks' -import { useBlockDimensions } from '../../hooks/use-block-dimensions' +import { + useBlockDimensions, + useBlockFocus, + useBlockRingStyles, + useCurrentWorkflow, +} from '../../hooks' import { ActionBar } from '../workflow-block/components' import { useBlockState } from '../workflow-block/hooks' import type { WorkflowBlockProps } from '../workflow-block/types' @@ -92,9 +95,7 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string } export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps) { const { type, config, name } = data - const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId) - const currentBlockId = usePanelEditorStore((state) => state.currentBlockId) - const isFocused = currentBlockId === id + const { isFocused, handleClick } = useBlockFocus(id) const currentWorkflow = useCurrentWorkflow() const { isEnabled, isActive, diffStatus, isDeletedBlock } = useBlockState( @@ -161,16 +162,12 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps @@ -178,7 +175,7 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps setCurrentBlockId(id)} + onClick={handleClick} > diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 8672dbe0567..5d20accba52 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -11,10 +11,14 @@ import type { SubBlockConfig } from '@/blocks/types' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useCredentialDisplay } from '@/hooks/use-credential-display' import { useDisplayName } from '@/hooks/use-display-name' -import { usePanelEditorStore } from '@/stores/panel-new/editor/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' -import { useBlockDimensions, useCurrentWorkflow } from '../../hooks' +import { + useBlockDimensions, + useBlockFocus, + useBlockRingStyles, + useCurrentWorkflow, +} from '../../hooks' import { ActionBar, Connections } from './components' import { useBlockProperties, @@ -367,9 +371,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ }, [id, collaborativeSetSubblockValue]) const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) - const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId) - const currentBlockId = usePanelEditorStore((state) => state.currentBlockId) - const isFocused = currentBlockId === id + const { isFocused, handleClick } = useBlockFocus(id) const currentStoreBlock = currentWorkflow.getBlockById(id) const isStarterBlock = type === 'starter' @@ -638,35 +640,19 @@ export const WorkflowBlock = memo(function WorkflowBlock({ const userPermissions = useUserPermissionsContext() const isWorkflowSelector = type === 'workflow' || type === 'workflow_input' - /** - * Determine the ring styling based on block state priority: - * 1. Active (executing) - purple ring with pulse animation - * 2. Pending (next step) - orange ring - * 3. Focused (selected in editor) - blue ring - * 4. Diff status (version comparison) - green/orange/red ring - */ - const hasRing = - isActive || - isPending || - isFocused || - diffStatus === 'new' || - diffStatus === 'edited' || - isDeletedBlock - const ringStyles = cn( - hasRing && 'ring-[1.75px]', - isActive && 'ring-[#8C10FF] animate-pulse-ring', - isPending && 'ring-[#FF6600]', - isFocused && 'ring-[#33B4FF]', - diffStatus === 'new' && 'ring-[#22C55F]', - diffStatus === 'edited' && 'ring-[#FF6600]', - isDeletedBlock && 'ring-[#EF4444]' - ) + const { hasRing, ringStyles } = useBlockRingStyles({ + isActive, + isFocused, + isPending, + diffStatus, + isDeletedBlock, + }) return (
    setCurrentBlockId(id)} + onClick={handleClick} className={cn( 'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] bg-[#232323]' )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts index 239253f31fe..1393f385996 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts @@ -1,5 +1,7 @@ export { useAutoLayout } from './use-auto-layout' export { useBlockDimensions } from './use-block-dimensions' +export { useBlockFocus } from './use-block-focus' +export { useBlockRingStyles } from './use-block-ring-styles' export { type CurrentWorkflow, useCurrentWorkflow } from './use-current-workflow' export { useNodeUtilities } from './use-node-utilities' export { useScrollManagement } from './use-scroll-management' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-focus.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-focus.ts new file mode 100644 index 00000000000..8f71ebfab58 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-focus.ts @@ -0,0 +1,17 @@ +import { useCallback } from 'react' +import { usePanelEditorStore } from '@/stores/panel-new/editor/store' + +/** + * Shared hook for managing block focus state and interactions. + */ +export function useBlockFocus(blockId: string) { + const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId) + const currentBlockId = usePanelEditorStore((state) => state.currentBlockId) + const isFocused = currentBlockId === blockId + + const handleClick = useCallback(() => { + setCurrentBlockId(blockId) + }, [blockId, setCurrentBlockId]) + + return { isFocused, handleClick } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-ring-styles.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-ring-styles.ts new file mode 100644 index 00000000000..88cf391c915 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-ring-styles.ts @@ -0,0 +1,44 @@ +import { useMemo } from 'react' +import { cn } from '@/lib/utils' + +interface UseBlockRingStylesOptions { + isActive: boolean + isFocused: boolean + isPending?: boolean + diffStatus?: 'new' | 'edited' | null + isDeletedBlock: boolean +} + +/** + * Shared hook for computing ring styles across block types. + * Handles visual states: active, pending, focused, diff status, deleted. + */ +export function useBlockRingStyles({ + isActive, + isFocused, + isPending = false, + diffStatus, + isDeletedBlock, +}: UseBlockRingStylesOptions) { + return useMemo(() => { + const hasRing = + isActive || + isPending || + isFocused || + diffStatus === 'new' || + diffStatus === 'edited' || + isDeletedBlock + + const ringStyles = cn( + hasRing && 'ring-[1.75px]', + isActive && 'ring-[#8C10FF] animate-pulse-ring', + isPending && 'ring-[#FF6600]', + isFocused && 'ring-[#33B4FF]', + diffStatus === 'new' && 'ring-[#22C55F]', + diffStatus === 'edited' && 'ring-[#FF6600]', + isDeletedBlock && 'ring-[#EF4444]' + ) + + return { hasRing, ringStyles } + }, [isActive, isPending, isFocused, diffStatus, isDeletedBlock]) +} From 1b20439b1909990dcfc5afc61288cb797a124bcb Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 11 Nov 2025 13:19:19 -0800 Subject: [PATCH 09/11] Consolidate hook --- .../components/note-block/note-block.tsx | 27 +----- .../workflow-block/workflow-block.tsx | 45 ++++------ .../w/[workflowId]/hooks/index.ts | 3 +- .../w/[workflowId]/hooks/use-block-core.ts | 82 +++++++++++++++++++ .../w/[workflowId]/hooks/use-block-focus.ts | 17 ---- .../hooks/use-block-ring-styles.ts | 44 ---------- 6 files changed, 101 insertions(+), 117 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-core.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-focus.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-ring-styles.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx index c116546fbfc..6c5b286b6fd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx @@ -4,16 +4,9 @@ import type { NodeProps } from 'reactflow' import remarkGfm from 'remark-gfm' import { cn } from '@/lib/utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' -import { - useBlockDimensions, - useBlockFocus, - useBlockRingStyles, - useCurrentWorkflow, -} from '../../hooks' +import { useBlockCore, useBlockDimensions } from '../../hooks' import { ActionBar } from '../workflow-block/components' -import { useBlockState } from '../workflow-block/hooks' import type { WorkflowBlockProps } from '../workflow-block/types' interface NoteBlockNodeData extends WorkflowBlockProps {} @@ -95,16 +88,9 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string } export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps) { const { type, config, name } = data - const { isFocused, handleClick } = useBlockFocus(id) - - const currentWorkflow = useCurrentWorkflow() - const { isEnabled, isActive, diffStatus, isDeletedBlock } = useBlockState( - id, - currentWorkflow, - data + const { activeWorkflowId, isEnabled, isFocused, handleClick, hasRing, ringStyles } = useBlockCore( + { blockId: id, data } ) - - const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) const storedValues = useSubBlockStore( useCallback( (state) => { @@ -162,13 +148,6 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps
    state.activeWorkflowId) - const { isFocused, handleClick } = useBlockFocus(id) const currentStoreBlock = currentWorkflow.getBlockById(id) const isStarterBlock = type === 'starter' @@ -640,14 +633,6 @@ export const WorkflowBlock = memo(function WorkflowBlock({ const userPermissions = useUserPermissionsContext() const isWorkflowSelector = type === 'workflow' || type === 'workflow_input' - const { hasRing, ringStyles } = useBlockRingStyles({ - isActive, - isFocused, - isPending, - diffStatus, - isDeletedBlock, - }) - return (
    state.activeWorkflowId) + + // Block state (enabled, active, diff status, deleted) + const { isEnabled, isActive, diffStatus, isDeletedBlock } = useBlockState( + blockId, + currentWorkflow, + data + ) + + // Focus management + const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId) + const currentBlockId = usePanelEditorStore((state) => state.currentBlockId) + const isFocused = currentBlockId === blockId + + const handleClick = useCallback(() => { + setCurrentBlockId(blockId) + }, [blockId, setCurrentBlockId]) + + // Ring styling based on all states + const { hasRing, ringStyles } = useMemo(() => { + const hasRing = + isActive || + isPending || + isFocused || + diffStatus === 'new' || + diffStatus === 'edited' || + isDeletedBlock + + const ringStyles = cn( + hasRing && 'ring-[1.75px]', + isActive && 'ring-[#8C10FF] animate-pulse-ring', + isPending && 'ring-[#FF6600]', + isFocused && 'ring-[#33B4FF]', + diffStatus === 'new' && 'ring-[#22C55F]', + diffStatus === 'edited' && 'ring-[#FF6600]', + isDeletedBlock && 'ring-[#EF4444]' + ) + + return { hasRing, ringStyles } + }, [isActive, isPending, isFocused, diffStatus, isDeletedBlock]) + + return { + // Workflow context + currentWorkflow, + activeWorkflowId, + + // Block state + isEnabled, + isActive, + diffStatus, + isDeletedBlock, + + // Focus + isFocused, + handleClick, + + // Ring styling + hasRing, + ringStyles, + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-focus.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-focus.ts deleted file mode 100644 index 8f71ebfab58..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-focus.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useCallback } from 'react' -import { usePanelEditorStore } from '@/stores/panel-new/editor/store' - -/** - * Shared hook for managing block focus state and interactions. - */ -export function useBlockFocus(blockId: string) { - const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId) - const currentBlockId = usePanelEditorStore((state) => state.currentBlockId) - const isFocused = currentBlockId === blockId - - const handleClick = useCallback(() => { - setCurrentBlockId(blockId) - }, [blockId, setCurrentBlockId]) - - return { isFocused, handleClick } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-ring-styles.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-ring-styles.ts deleted file mode 100644 index 88cf391c915..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-ring-styles.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useMemo } from 'react' -import { cn } from '@/lib/utils' - -interface UseBlockRingStylesOptions { - isActive: boolean - isFocused: boolean - isPending?: boolean - diffStatus?: 'new' | 'edited' | null - isDeletedBlock: boolean -} - -/** - * Shared hook for computing ring styles across block types. - * Handles visual states: active, pending, focused, diff status, deleted. - */ -export function useBlockRingStyles({ - isActive, - isFocused, - isPending = false, - diffStatus, - isDeletedBlock, -}: UseBlockRingStylesOptions) { - return useMemo(() => { - const hasRing = - isActive || - isPending || - isFocused || - diffStatus === 'new' || - diffStatus === 'edited' || - isDeletedBlock - - const ringStyles = cn( - hasRing && 'ring-[1.75px]', - isActive && 'ring-[#8C10FF] animate-pulse-ring', - isPending && 'ring-[#FF6600]', - isFocused && 'ring-[#33B4FF]', - diffStatus === 'new' && 'ring-[#22C55F]', - diffStatus === 'edited' && 'ring-[#FF6600]', - isDeletedBlock && 'ring-[#EF4444]' - ) - - return { hasRing, ringStyles } - }, [isActive, isPending, isFocused, diffStatus, isDeletedBlock]) -} From 253209cd6dc141d13feaeca89dc687f3c1e72b65 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 11 Nov 2025 13:22:18 -0800 Subject: [PATCH 10/11] Consolidate utils checks --- .../workflow-block/components/action-bar/action-bar.tsx | 3 ++- .../workspace/[workspaceId]/w/[workflowId]/workflow.tsx | 8 ++++++-- apps/sim/executor/consts.ts | 8 ++++++++ apps/sim/stores/workflows/workflow/store.ts | 5 +++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx index 7da58728c7c..3c5a71b78cf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx @@ -3,6 +3,7 @@ import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, LogOut } from 'lucide-r import { Button, Duplicate, Tooltip, Trash2 } from '@/components/emcn' import { cn } from '@/lib/utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { supportsHandles } from '@/executor/consts' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -146,7 +147,7 @@ export const ActionBar = memo( )} - {blockType !== 'note' && ( + {supportsHandles(blockType) && (