mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-04 11:45:07 -05:00
fix(editor): block rename applies to correct block when selection changes (#3129)
This commit is contained in:
@@ -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<string | null>(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.
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PanelEditorState>()(
|
||||
(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<PanelEditorState>()(
|
||||
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<PanelEditorState>()(
|
||||
: 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<PanelEditorState>()(
|
||||
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',
|
||||
|
||||
@@ -380,13 +380,18 @@ export const useTerminalConsoleStore = create<ConsoleStore>()(
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user