From f5063af161f9aed8d0d3ff56bbac8c0942a7e395 Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Thu, 20 Mar 2025 23:24:17 -0700 Subject: [PATCH] fix[ux]: referenced block renaming (#132) * fix[ui]: referenced block renaming #120 * improvement[block-renaming]: added support for condition/function blocks --------- Co-authored-by: Braulio Fernandes --- sim/app/stores/workflows/workflow/store.ts | 64 ++++++++++- .../sub-block/components/condition-input.tsx | 104 +++++++++++------- 2 files changed, 127 insertions(+), 41 deletions(-) diff --git a/sim/app/stores/workflows/workflow/store.ts b/sim/app/stores/workflows/workflow/store.ts index 61edd705cc..d51e15451f 100644 --- a/sim/app/stores/workflows/workflow/store.ts +++ b/sim/app/stores/workflows/workflow/store.ts @@ -402,11 +402,15 @@ export const useWorkflowStore = create()( }, updateBlockName: (id: string, name: string) => { + const oldBlock = get().blocks[id] + if (!oldBlock) return + + // Create a new state with the updated block name const newState = { blocks: { ...get().blocks, [id]: { - ...get().blocks[id], + ...oldBlock, name, }, }, @@ -414,6 +418,64 @@ export const useWorkflowStore = create()( loops: { ...get().loops }, } + // Update references in subblock store + const subBlockStore = useSubBlockStore.getState() + const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId + if (activeWorkflowId) { + // Get the workflow values for the active workflow + // workflowValues: {[block_id]:{[subblock_id]:[subblock_value]}} + const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {} + const updatedWorkflowValues = { ...workflowValues } + + // Loop through blocks + Object.entries(workflowValues).forEach(([blockId, blockValues]) => { + if (blockId === id) return // Skip the block being renamed + + // Loop through subblocks and update references + Object.entries(blockValues).forEach(([subBlockId, value]) => { + const oldBlockName = oldBlock.name.replace(/\s+/g, '').toLowerCase() + const newBlockName = name.replace(/\s+/g, '').toLowerCase() + const regex = new RegExp(`<${oldBlockName}\\.`, 'g') + + // Use a recursive function to handle all object types + updatedWorkflowValues[blockId][subBlockId] = updateReferences(value, regex, `<${newBlockName}.`) + + // Helper function to recursively update references in any data structure + function updateReferences(value: any, regex: RegExp, replacement: string): any { + // Handle string values + if (typeof value === 'string') { + return regex.test(value) ? value.replace(regex, replacement) : value + } + + // Handle arrays + if (Array.isArray(value)) { + return value.map(item => updateReferences(item, regex, replacement)) + } + + // Handle objects + if (value !== null && typeof value === 'object') { + const result = { ...value } + for (const key in result) { + result[key] = updateReferences(result[key], regex, replacement) + } + return result + } + + // Return unchanged for other types + return value + } + }) + }) + + // Update the subblock store with the new values + useSubBlockStore.setState({ + workflowValues: { + ...subBlockStore.workflowValues, + [activeWorkflowId]: updatedWorkflowValues, + }, + }) + } + set(newState) pushHistory(set, get, newState, `${name} block name updated`) get().updateLastSaved() diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx index fdebdc5883..fbf582e5f1 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx @@ -36,12 +36,6 @@ interface ConditionInputProps { export function ConditionInput({ blockId, subBlockId, isConnecting }: ConditionInputProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) - const [lineCount, setLineCount] = useState(1) - const [showTags, setShowTags] = useState(false) - const [showEnvVars, setShowEnvVars] = useState(false) - const [searchTerm, setSearchTerm] = useState('') - const [cursorPosition, setCursorPosition] = useState(0) - const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) const editorRef = useRef(null) const [visualLineHeights, setVisualLineHeights] = useState<{ [key: string]: number[] @@ -50,6 +44,11 @@ export function ConditionInput({ blockId, subBlockId, isConnecting }: ConditionI const removeEdge = useWorkflowStore((state) => state.removeEdge) const edges = useWorkflowStore((state) => state.edges) + // Use a ref to track the previous store value for comparison + const prevStoreValueRef = useRef(null) + // Use a ref to track if we're currently syncing from store to prevent loops + const isSyncingFromStoreRef = useRef(false) + // Initialize conditional blocks with if and else blocks const [conditionalBlocks, setConditionalBlocks] = useState([ { @@ -74,46 +73,71 @@ export function ConditionInput({ blockId, subBlockId, isConnecting }: ConditionI }, ]) - // Sync store value with conditional blocks on initial load + // Sync store value with conditional blocks when storeValue changes useEffect(() => { - if (storeValue !== null) { - try { - const parsedValue = JSON.parse(storeValue.toString()) - if (Array.isArray(parsedValue)) { - setConditionalBlocks(parsedValue) - } - } catch { - // If the store value isn't valid JSON, initialize with default blocks - setConditionalBlocks([ - { - id: crypto.randomUUID(), - title: 'if', - value: '', - showTags: false, - showEnvVars: false, - searchTerm: '', - cursorPosition: 0, - activeSourceBlockId: null, - }, - { - id: crypto.randomUUID(), - title: 'else', - value: '', - showTags: false, - showEnvVars: false, - searchTerm: '', - cursorPosition: 0, - activeSourceBlockId: null, - }, - ]) + // Skip if we don't have a store value + if (storeValue === null) return + + // Skip if the store value hasn't changed + const storeValueStr = storeValue.toString() + if (storeValueStr === prevStoreValueRef.current) return + + // Update the previous store value ref + prevStoreValueRef.current = storeValueStr + + // Set that we're syncing from store to prevent loops + isSyncingFromStoreRef.current = true + + try { + const parsedValue = JSON.parse(storeValueStr) + if (Array.isArray(parsedValue)) { + setConditionalBlocks(parsedValue) } + } catch (error) { + // If parsing fails, set default blocks + setConditionalBlocks([ + { + id: crypto.randomUUID(), + title: 'if', + value: '', + showTags: false, + showEnvVars: false, + searchTerm: '', + cursorPosition: 0, + activeSourceBlockId: null, + }, + { + id: crypto.randomUUID(), + title: 'else', + value: '', + showTags: false, + showEnvVars: false, + searchTerm: '', + cursorPosition: 0, + activeSourceBlockId: null, + }, + ]) + } finally { + // Reset the syncing flag + setTimeout(() => { + isSyncingFromStoreRef.current = false + }, 0) } - }, []) + }, [storeValue]) // Update store whenever conditional blocks change useEffect(() => { - setStoreValue(JSON.stringify(conditionalBlocks)) - updateNodeInternals(`${blockId}-${subBlockId}`) + // Skip if we're currently syncing from store to prevent loops + if (isSyncingFromStoreRef.current) return + + const newValue = JSON.stringify(conditionalBlocks) + + // Only update if the value has actually changed + if (newValue !== prevStoreValueRef.current) { + prevStoreValueRef.current = newValue + setStoreValue(newValue) + updateNodeInternals(`${blockId}-${subBlockId}`) + } }, [conditionalBlocks, blockId, subBlockId]) // Update block value with trigger checks - handle both tag and env var triggers consistently