From 7977ac88ca96087e19e4591e49ebfa46da2c7fb5 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 3 Feb 2026 20:06:03 -0800 Subject: [PATCH] fix(editor): block rename applies to correct block when selection changes (#3129) --- .../panel/components/editor/editor.tsx | 71 +++++++++++-------- .../hooks/use-workflow-execution.ts | 11 +++ .../[workspaceId]/w/[workflowId]/workflow.tsx | 2 +- apps/sim/executor/execution/engine.ts | 18 +++++ apps/sim/lib/core/utils/formatting.ts | 4 ++ apps/sim/stores/panel/editor/store.ts | 27 ++++--- apps/sim/stores/terminal/console/store.ts | 7 +- 7 files changed, 95 insertions(+), 45 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 33bce6bb6..a7a5d7c38 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -78,21 +78,15 @@ const IconComponent = ({ icon: Icon, className }: { icon: any; className?: strin * @returns Editor panel content */ export function Editor() { - const { - currentBlockId, - connectionsHeight, - toggleConnectionsCollapsed, - shouldFocusRename, - setShouldFocusRename, - } = usePanelEditorStore( - useShallow((state) => ({ - currentBlockId: state.currentBlockId, - connectionsHeight: state.connectionsHeight, - toggleConnectionsCollapsed: state.toggleConnectionsCollapsed, - shouldFocusRename: state.shouldFocusRename, - setShouldFocusRename: state.setShouldFocusRename, - })) - ) + const { currentBlockId, connectionsHeight, toggleConnectionsCollapsed, registerRenameCallback } = + usePanelEditorStore( + useShallow((state) => ({ + currentBlockId: state.currentBlockId, + connectionsHeight: state.connectionsHeight, + toggleConnectionsCollapsed: state.toggleConnectionsCollapsed, + registerRenameCallback: state.registerRenameCallback, + })) + ) const currentWorkflow = useCurrentWorkflow() const currentBlock = currentBlockId ? currentWorkflow.getBlockById(currentBlockId) : null const blockConfig = currentBlock ? getBlock(currentBlock.type) : null @@ -229,6 +223,7 @@ export function Editor() { const [isRenaming, setIsRenaming] = useState(false) const [editedName, setEditedName] = useState('') + const renamingBlockIdRef = useRef(null) /** * Ref callback that auto-selects the input text when mounted. @@ -240,44 +235,62 @@ export function Editor() { }, []) /** - * Handles starting the rename process. + * Starts the rename process for the current block. + * Reads from stores directly to avoid stale closures when called via registered callback. + * Captures the block ID in a ref to ensure the correct block is renamed even if selection changes. */ const handleStartRename = useCallback(() => { - if (!canEditBlock || !currentBlock) return - setEditedName(currentBlock.name || '') + const blockId = usePanelEditorStore.getState().currentBlockId + if (!blockId) return + + const blocks = useWorkflowStore.getState().blocks + const block = blocks[blockId] + if (!block) return + + const parentId = block.data?.parentId as string | undefined + const isParentLocked = parentId ? (blocks[parentId]?.locked ?? false) : false + const isLocked = (block.locked ?? false) || isParentLocked + if (!userPermissions.canEdit || isLocked) return + + renamingBlockIdRef.current = blockId + setEditedName(block.name || '') setIsRenaming(true) - }, [canEditBlock, currentBlock]) + }, [userPermissions.canEdit]) /** - * Handles saving the renamed block. + * Saves the renamed block using the captured block ID from when rename started. */ const handleSaveRename = useCallback(() => { - if (!currentBlockId || !isRenaming) return + const blockIdToRename = renamingBlockIdRef.current + if (!blockIdToRename || !isRenaming) return + + const blocks = useWorkflowStore.getState().blocks + const blockToRename = blocks[blockIdToRename] const trimmedName = editedName.trim() - if (trimmedName && trimmedName !== currentBlock?.name) { - const result = collaborativeUpdateBlockName(currentBlockId, trimmedName) + if (trimmedName && blockToRename && trimmedName !== blockToRename.name) { + const result = collaborativeUpdateBlockName(blockIdToRename, trimmedName) if (!result.success) { return } } + renamingBlockIdRef.current = null setIsRenaming(false) - }, [currentBlockId, isRenaming, editedName, currentBlock?.name, collaborativeUpdateBlockName]) + }, [isRenaming, editedName, collaborativeUpdateBlockName]) /** * Handles canceling the rename process. */ const handleCancelRename = useCallback(() => { + renamingBlockIdRef.current = null setIsRenaming(false) setEditedName('') }, []) useEffect(() => { - if (shouldFocusRename && currentBlock) { - handleStartRename() - setShouldFocusRename(false) - } - }, [shouldFocusRename, currentBlock, handleStartRename, setShouldFocusRename]) + registerRenameCallback(handleStartRename) + return () => registerRenameCallback(null) + }, [registerRenameCallback, handleStartRename]) /** * Handles opening documentation link in a new secure tab. diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 2b021b3c5..a1da75f36 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -1185,6 +1185,12 @@ export function useWorkflowExecution() { }) } }, + + onExecutionCancelled: () => { + if (activeWorkflowId) { + cancelRunningEntries(activeWorkflowId) + } + }, }, }) @@ -1738,6 +1744,10 @@ export function useWorkflowExecution() { }) } }, + + onExecutionCancelled: () => { + cancelRunningEntries(workflowId) + }, }, }) } catch (error) { @@ -1759,6 +1769,7 @@ export function useWorkflowExecution() { setEdgeRunStatus, addNotification, addConsole, + cancelRunningEntries, executionStream, ] ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index bf637c03f..82d05a587 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -1132,7 +1132,7 @@ const WorkflowContent = React.memo(() => { const handleContextRename = useCallback(() => { if (contextMenuBlocks.length === 1) { usePanelEditorStore.getState().setCurrentBlockId(contextMenuBlocks[0].id) - usePanelEditorStore.getState().setShouldFocusRename(true) + usePanelEditorStore.getState().triggerRename() } }, [contextMenuBlocks]) diff --git a/apps/sim/executor/execution/engine.ts b/apps/sim/executor/execution/engine.ts index 61670c1b8..9bff038ee 100644 --- a/apps/sim/executor/execution/engine.ts +++ b/apps/sim/executor/execution/engine.ts @@ -130,6 +130,7 @@ export class ExecutionEngine { this.context.metadata.duration = endTime - startTime if (this.cancelledFlag) { + this.finalizeIncompleteLogs() return { success: false, output: this.finalOutput, @@ -151,6 +152,7 @@ export class ExecutionEngine { this.context.metadata.duration = endTime - startTime if (this.cancelledFlag) { + this.finalizeIncompleteLogs() return { success: false, output: this.finalOutput, @@ -474,4 +476,20 @@ export class ExecutionEngine { pauseCount: responses.length, } } + + /** + * Finalizes any block logs that were still running when execution was cancelled. + * Sets their endedAt to now and calculates the actual elapsed duration. + */ + private finalizeIncompleteLogs(): void { + const now = new Date() + const nowIso = now.toISOString() + + for (const log of this.context.blockLogs) { + if (!log.endedAt) { + log.endedAt = nowIso + log.durationMs = now.getTime() - new Date(log.startedAt).getTime() + } + } + } } diff --git a/apps/sim/lib/core/utils/formatting.ts b/apps/sim/lib/core/utils/formatting.ts index a7051df03..af9bcb321 100644 --- a/apps/sim/lib/core/utils/formatting.ts +++ b/apps/sim/lib/core/utils/formatting.ts @@ -176,6 +176,10 @@ export function formatDuration( } } else { ms = duration + // Handle NaN/Infinity (e.g., cancelled blocks with no end time) + if (!Number.isFinite(ms)) { + return '—' + } } const precision = options?.precision ?? 0 diff --git a/apps/sim/stores/panel/editor/store.ts b/apps/sim/stores/panel/editor/store.ts index c716f6166..f78c35b73 100644 --- a/apps/sim/stores/panel/editor/store.ts +++ b/apps/sim/stores/panel/editor/store.ts @@ -5,6 +5,8 @@ import { persist } from 'zustand/middleware' import { EDITOR_CONNECTIONS_HEIGHT } from '@/stores/constants' import { usePanelStore } from '../store' +let renameCallback: (() => void) | null = null + /** * State for the Editor panel. * Tracks the currently selected block to edit its subblocks/values and connections panel height. @@ -22,10 +24,10 @@ interface PanelEditorState { setConnectionsHeight: (height: number) => void /** Toggle connections between collapsed (min height) and expanded (default height) */ toggleConnectionsCollapsed: () => void - /** Flag to signal the editor to focus the rename input */ - shouldFocusRename: boolean - /** Sets the shouldFocusRename flag */ - setShouldFocusRename: (value: boolean) => void + /** Register the rename callback (called by Editor on mount) */ + registerRenameCallback: (callback: (() => void) | null) => void + /** Trigger rename mode by invoking the registered callback */ + triggerRename: () => void } /** @@ -37,15 +39,16 @@ export const usePanelEditorStore = create()( (set, get) => ({ currentBlockId: null, connectionsHeight: EDITOR_CONNECTIONS_HEIGHT.DEFAULT, - shouldFocusRename: false, - setShouldFocusRename: (value) => set({ shouldFocusRename: value }), + registerRenameCallback: (callback) => { + renameCallback = callback + }, + triggerRename: () => { + renameCallback?.() + }, setCurrentBlockId: (blockId) => { set({ currentBlockId: blockId }) - - // When a block is selected, always switch to the editor tab if (blockId !== null) { - const panelState = usePanelStore.getState() - panelState.setActiveTab('editor') + usePanelStore.getState().setActiveTab('editor') } }, clearCurrentBlock: () => { @@ -57,7 +60,6 @@ export const usePanelEditorStore = create()( Math.min(EDITOR_CONNECTIONS_HEIGHT.MAX, height) ) set({ connectionsHeight: clampedHeight }) - // Update CSS variable for immediate visual feedback if (typeof window !== 'undefined') { document.documentElement.style.setProperty( '--editor-connections-height', @@ -73,8 +75,6 @@ export const usePanelEditorStore = create()( : EDITOR_CONNECTIONS_HEIGHT.MIN set({ connectionsHeight: newHeight }) - - // Update CSS variable if (typeof window !== 'undefined') { document.documentElement.style.setProperty( '--editor-connections-height', @@ -90,7 +90,6 @@ export const usePanelEditorStore = create()( connectionsHeight: state.connectionsHeight, }), onRehydrateStorage: () => (state) => { - // Sync CSS variables with stored state after rehydration if (state && typeof window !== 'undefined') { document.documentElement.style.setProperty( '--editor-connections-height', diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index 9b1386da1..5ede7fd02 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -380,13 +380,18 @@ export const useTerminalConsoleStore = create()( cancelRunningEntries: (workflowId: string) => { set((state) => { + const now = new Date() const updatedEntries = state.entries.map((entry) => { if (entry.workflowId === workflowId && entry.isRunning) { + const durationMs = entry.startedAt + ? now.getTime() - new Date(entry.startedAt).getTime() + : entry.durationMs return { ...entry, isRunning: false, isCanceled: true, - endedAt: new Date().toISOString(), + endedAt: now.toISOString(), + durationMs, } } return entry