From 71cdc11192e01edd52e223e23ed51fc54c7744a9 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 8 Jul 2025 00:03:07 -0700 Subject: [PATCH 1/2] fix(dropdown): simplify & fix tag dropdown for parallel & loop blocks --- .../connection-blocks/connection-blocks.tsx | 17 +- .../hooks/use-block-connections.ts | 8 +- apps/sim/components/ui/tag-dropdown.tsx | 396 ++++++++++-------- 3 files changed, 232 insertions(+), 189 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx index 740153f7b4..8077f6f41a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx @@ -1,3 +1,4 @@ +import { RepeatIcon, SplitIcon } from 'lucide-react' import { Card } from '@/components/ui/card' import { cn } from '@/lib/utils' import { @@ -77,8 +78,20 @@ export function ConnectionBlocks({ // Get block configuration for icon and color const blockConfig = getBlock(connection.type) const displayName = connection.name // Use the actual block name instead of transforming it - const Icon = blockConfig?.icon - const bgColor = blockConfig?.bgColor || '#6B7280' // Fallback to gray + + // Handle special blocks that aren't in the registry (loop and parallel) + let Icon = blockConfig?.icon + let bgColor = blockConfig?.bgColor || '#6B7280' // Fallback to gray + + if (!blockConfig) { + if (connection.type === 'loop') { + Icon = RepeatIcon + bgColor = '#2FB3FF' // Blue color for loop blocks + } else if (connection.type === 'parallel') { + Icon = SplitIcon + bgColor = '#FEE12B' // Yellow color for parallel blocks + } + } return ( ({ @@ -140,10 +138,8 @@ export function useBlockConnections(blockId: string) { .getState() .getValue(edge.source, 'responseFormat') - let responseFormat - // Safely parse response format with proper error handling - responseFormat = parseResponseFormatSafely(responseFormatValue, edge.source) + const responseFormat = parseResponseFormatSafely(responseFormatValue, edge.source) // Get the default output type from the block's outputs const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({ diff --git a/apps/sim/components/ui/tag-dropdown.tsx b/apps/sim/components/ui/tag-dropdown.tsx index 323ad831cf..9ae988386f 100644 --- a/apps/sim/components/ui/tag-dropdown.tsx +++ b/apps/sim/components/ui/tag-dropdown.tsx @@ -169,7 +169,39 @@ export const TagDropdown: React.FC = ({ } const blockConfig = getBlock(sourceBlock.type) + + // Handle special blocks that aren't in the registry (loop and parallel) if (!blockConfig) { + if (sourceBlock.type === 'loop' || sourceBlock.type === 'parallel') { + // Create a mock config with results output for loop/parallel blocks + const mockConfig = { + outputs: { + results: 'array', // These blocks have a results array output + }, + } + const blockName = sourceBlock.name || sourceBlock.type + const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() + + // Generate output paths for the mock config + const outputPaths = generateOutputPaths(mockConfig.outputs) + const blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + + const blockTagGroups: BlockTagGroup[] = [ + { + blockName, + blockId: activeSourceBlockId, + blockType: sourceBlock.type, + tags: blockTags, + distance: 0, + }, + ] + + return { + tags: blockTags, + variableInfoMap: {}, + blockTagGroups, + } + } return { tags: [], variableInfoMap: {}, blockTagGroups: [] } } @@ -270,28 +302,65 @@ export const TagDropdown: React.FC = ({ {} as Record ) - // Generate loop tags if current block is in a loop - const loopTags: string[] = [] + // Generate loop contextual block group if current block is in a loop + let loopBlockGroup: BlockTagGroup | null = null const containingLoop = Object.entries(loops).find(([_, loop]) => loop.nodes.includes(blockId)) + let containingLoopBlockId: string | null = null if (containingLoop) { - const [_loopId, loop] = containingLoop + const [loopId, loop] = containingLoop + containingLoopBlockId = loopId const loopType = loop.loopType || 'for' - loopTags.push('loop.index') + const contextualTags: string[] = ['index'] if (loopType === 'forEach') { - loopTags.push('loop.currentItem') - loopTags.push('loop.items') + contextualTags.push('currentItem') + contextualTags.push('items') + } + + // Add the containing loop block's results to the contextual tags + const containingLoopBlock = blocks[loopId] + if (containingLoopBlock) { + const loopBlockName = containingLoopBlock.name || containingLoopBlock.type + const normalizedLoopBlockName = loopBlockName.replace(/\s+/g, '').toLowerCase() + contextualTags.push(`${normalizedLoopBlockName}.results`) + + // Create a block group for the loop contextual tags + loopBlockGroup = { + blockName: loopBlockName, + blockId: loopId, + blockType: 'loop', + tags: contextualTags, + distance: 0, // Contextual tags have highest priority + } } } - // Generate parallel tags if current block is in parallel - const parallelTags: string[] = [] + // Generate parallel contextual block group if current block is in parallel + let parallelBlockGroup: BlockTagGroup | null = null const containingParallel = Object.entries(parallels || {}).find(([_, parallel]) => parallel.nodes.includes(blockId) ) + let containingParallelBlockId: string | null = null if (containingParallel) { - parallelTags.push('parallel.index') - parallelTags.push('parallel.currentItem') - parallelTags.push('parallel.items') + const [parallelId, parallel] = containingParallel + containingParallelBlockId = parallelId + const contextualTags: string[] = ['index', 'currentItem', 'items'] + + // Add the containing parallel block's results to the contextual tags + const containingParallelBlock = blocks[parallelId] + if (containingParallelBlock) { + const parallelBlockName = containingParallelBlock.name || containingParallelBlock.type + const normalizedParallelBlockName = parallelBlockName.replace(/\s+/g, '').toLowerCase() + contextualTags.push(`${normalizedParallelBlockName}.results`) + + // Create a block group for the parallel contextual tags + parallelBlockGroup = { + blockName: parallelBlockName, + blockId: parallelId, + blockType: 'parallel', + tags: contextualTags, + distance: 0, // Contextual tags have highest priority + } + } } // Create block tag groups from accessible blocks @@ -303,7 +372,43 @@ export const TagDropdown: React.FC = ({ if (!accessibleBlock) continue const blockConfig = getBlock(accessibleBlock.type) - if (!blockConfig) continue + + // Handle special blocks that aren't in the registry (loop and parallel) + if (!blockConfig) { + // For loop and parallel blocks, create a mock config with results output + if (accessibleBlock.type === 'loop' || accessibleBlock.type === 'parallel') { + // Skip this block if it's the containing loop/parallel block - we'll handle it with contextual tags + if ( + accessibleBlockId === containingLoopBlockId || + accessibleBlockId === containingParallelBlockId + ) { + continue + } + + const mockConfig = { + outputs: { + results: 'array', // These blocks have a results array output + }, + } + const blockName = accessibleBlock.name || accessibleBlock.type + const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() + + // Generate output paths for the mock config + const outputPaths = generateOutputPaths(mockConfig.outputs) + const blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + + blockTagGroups.push({ + blockName, + blockId: accessibleBlockId, + blockType: accessibleBlock.type, + tags: blockTags, + distance: blockDistances[accessibleBlockId] || 0, + }) + + allBlockTags.push(...blockTags) + } + continue + } const blockName = accessibleBlock.name || accessibleBlock.type const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() @@ -328,13 +433,32 @@ export const TagDropdown: React.FC = ({ allBlockTags.push(...blockTags) } - // Sort block groups by distance (closest first) + // Add contextual block groups at the beginning (they have highest priority) + const finalBlockTagGroups: BlockTagGroup[] = [] + if (loopBlockGroup) { + finalBlockTagGroups.push(loopBlockGroup) + } + if (parallelBlockGroup) { + finalBlockTagGroups.push(parallelBlockGroup) + } + + // Sort regular block groups by distance (closest first) and add them blockTagGroups.sort((a, b) => a.distance - b.distance) + finalBlockTagGroups.push(...blockTagGroups) + + // Collect all tags for the main tags array + const contextualTags: string[] = [] + if (loopBlockGroup) { + contextualTags.push(...loopBlockGroup.tags) + } + if (parallelBlockGroup) { + contextualTags.push(...parallelBlockGroup.tags) + } return { - tags: [...variableTags, ...loopTags, ...parallelTags, ...allBlockTags], + tags: [...variableTags, ...contextualTags, ...allBlockTags], variableInfoMap, - blockTagGroups, + blockTagGroups: finalBlockTagGroups, } }, [blocks, edges, loops, parallels, blockId, activeSourceBlockId, workflowVariables]) @@ -345,18 +469,12 @@ export const TagDropdown: React.FC = ({ }, [tags, searchTerm]) // Group filtered tags by category - const { variableTags, loopTags, parallelTags, filteredBlockTagGroups } = useMemo(() => { + const { variableTags, filteredBlockTagGroups } = useMemo(() => { const varTags: string[] = [] - const loopTags: string[] = [] - const parTags: string[] = [] filteredTags.forEach((tag) => { if (tag.startsWith('variable.')) { varTags.push(tag) - } else if (tag.startsWith('loop.')) { - loopTags.push(tag) - } else if (tag.startsWith('parallel.')) { - parTags.push(tag) } }) @@ -370,8 +488,6 @@ export const TagDropdown: React.FC = ({ return { variableTags: varTags, - loopTags: loopTags, - parallelTags: parTags, filteredBlockTagGroups, } }, [filteredTags, blockTagGroups, searchTerm]) @@ -379,8 +495,8 @@ export const TagDropdown: React.FC = ({ // Create ordered tags for keyboard navigation const orderedTags = useMemo(() => { const allBlockTags = filteredBlockTagGroups.flatMap((group) => group.tags) - return [...variableTags, ...loopTags, ...parallelTags, ...allBlockTags] - }, [variableTags, loopTags, parallelTags, filteredBlockTagGroups]) + return [...variableTags, ...allBlockTags] + }, [variableTags, filteredBlockTagGroups]) // Create efficient tag index lookup map const tagIndexMap = useMemo(() => { @@ -393,7 +509,7 @@ export const TagDropdown: React.FC = ({ // Handle tag selection and text replacement const handleTagSelect = useCallback( - (tag: string) => { + (tag: string, blockGroup?: BlockTagGroup) => { const textBeforeCursor = inputValue.slice(0, cursorPosition) const textAfterCursor = inputValue.slice(cursorPosition) @@ -401,8 +517,10 @@ export const TagDropdown: React.FC = ({ const lastOpenBracket = textBeforeCursor.lastIndexOf('<') if (lastOpenBracket === -1) return - // Process variable tags to maintain compatibility + // Process different types of tags let processedTag = tag + + // Handle variable tags if (tag.startsWith('variable.')) { const variableName = tag.substring('variable.'.length) const variableObj = Object.values(variables).find( @@ -413,6 +531,19 @@ export const TagDropdown: React.FC = ({ processedTag = tag } } + // Handle contextual loop/parallel tags + else if ( + blockGroup && + (blockGroup.blockType === 'loop' || blockGroup.blockType === 'parallel') + ) { + // Check if this is a contextual tag (without dots) that needs a prefix + if (!tag.includes('.') && ['index', 'currentItem', 'items'].includes(tag)) { + processedTag = `${blockGroup.blockType}.${tag}` + } else { + // It's already a properly formatted tag (like blockname.results) + processedTag = tag + } + } // Handle existing closing bracket const nextCloseBracket = textAfterCursor.indexOf('>') @@ -465,7 +596,12 @@ export const TagDropdown: React.FC = ({ e.preventDefault() e.stopPropagation() if (selectedIndex >= 0 && selectedIndex < orderedTags.length) { - handleTagSelect(orderedTags[selectedIndex]) + const selectedTag = orderedTags[selectedIndex] + // Find which block group this tag belongs to + const belongsToGroup = filteredBlockTagGroups.find((group) => + group.tags.includes(selectedTag) + ) + handleTagSelect(selectedTag, belongsToGroup) } break case 'Escape': @@ -479,7 +615,7 @@ export const TagDropdown: React.FC = ({ window.addEventListener('keydown', handleKeyboardEvent, true) return () => window.removeEventListener('keydown', handleKeyboardEvent, true) } - }, [visible, selectedIndex, orderedTags, handleTagSelect, onClose]) + }, [visible, selectedIndex, orderedTags, filteredBlockTagGroups, handleTagSelect, onClose]) // Early return if dropdown should not be visible if (!visible || tags.length === 0 || orderedTags.length === 0) return null @@ -552,152 +688,21 @@ export const TagDropdown: React.FC = ({ )} - {/* Loop section */} - {loopTags.length > 0 && ( - <> - {variableTags.length > 0 &&
} -
- Loop -
-
- {loopTags.map((tag: string) => { - const tagIndex = tagIndexMap.get(tag) ?? -1 - const loopProperty = tag.split('.')[1] - - // Choose appropriate icon and description based on loop property - let tagIcon = 'L' - let tagDescription = '' - const bgColor = '#8857E6' - - if (loopProperty === 'currentItem') { - tagIcon = 'i' - tagDescription = 'Current item' - } else if (loopProperty === 'items') { - tagIcon = 'I' - tagDescription = 'All items' - } else if (loopProperty === 'index') { - tagIcon = '#' - tagDescription = 'Index' - } - - return ( - - ) - })} -
- - )} - - {/* Parallel section */} - {parallelTags.length > 0 && ( - <> - {loopTags.length > 0 &&
} -
- Parallel -
-
- {parallelTags.map((tag: string) => { - const tagIndex = tagIndexMap.get(tag) ?? -1 - const parallelProperty = tag.split('.')[1] - - // Choose appropriate icon and description based on parallel property - let tagIcon = 'P' - let tagDescription = '' - const bgColor = '#FF5757' - - if (parallelProperty === 'currentItem') { - tagIcon = 'i' - tagDescription = 'Current item' - } else if (parallelProperty === 'items') { - tagIcon = 'I' - tagDescription = 'All items' - } else if (parallelProperty === 'index') { - tagIcon = '#' - tagDescription = 'Index' - } - - return ( - - ) - })} -
- - )} - {/* Block sections */} {filteredBlockTagGroups.length > 0 && ( <> - {(variableTags.length > 0 || loopTags.length > 0 || parallelTags.length > 0) && ( -
- )} + {variableTags.length > 0 &&
} {filteredBlockTagGroups.map((group) => { // Get block color from configuration const blockConfig = getBlock(group.blockType) - const blockColor = blockConfig?.bgColor || '#2F55FF' + let blockColor = blockConfig?.bgColor || '#2F55FF' + + // Handle special colors for loop and parallel blocks + if (group.blockType === 'loop') { + blockColor = '#8857E6' // Purple color for loop blocks + } else if (group.blockType === 'parallel') { + blockColor = '#FF5757' // Red color for parallel blocks + } return (
@@ -707,11 +712,37 @@ export const TagDropdown: React.FC = ({
{group.tags.map((tag: string) => { const tagIndex = tagIndexMap.get(tag) ?? -1 - // Extract path after block name (e.g., "field" from "blockname.field") - // For root reference blocks, show the block name instead of empty path - const tagParts = tag.split('.') - const path = tagParts.slice(1).join('.') - const displayText = path || group.blockName + + // Handle display text based on tag type + let displayText: string + let tagDescription = '' + let tagIcon = group.blockName.charAt(0).toUpperCase() + + if ( + (group.blockType === 'loop' || group.blockType === 'parallel') && + !tag.includes('.') + ) { + // Contextual tags like 'index', 'currentItem', 'items' + displayText = tag + if (tag === 'index') { + tagIcon = '#' + tagDescription = 'Index' + } else if (tag === 'currentItem') { + tagIcon = 'i' + tagDescription = 'Current item' + } else if (tag === 'items') { + tagIcon = 'I' + tagDescription = 'All items' + } + } else { + // Regular block output tags like 'blockname.field' or 'blockname.results' + const tagParts = tag.split('.') + const path = tagParts.slice(1).join('.') + displayText = path || group.blockName + if (path === 'results') { + tagDescription = 'Results array' + } + } return ( ) })} From cb2ef7c3ca07709b956d4414eaf92072a79b6b21 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 8 Jul 2025 00:09:18 -0700 Subject: [PATCH 2/2] fixed build --- .../components/connection-blocks/connection-blocks.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx index 8077f6f41a..db09fa80de 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx @@ -85,10 +85,10 @@ export function ConnectionBlocks({ if (!blockConfig) { if (connection.type === 'loop') { - Icon = RepeatIcon + Icon = RepeatIcon as typeof Icon bgColor = '#2FB3FF' // Blue color for loop blocks } else if (connection.type === 'parallel') { - Icon = SplitIcon + Icon = SplitIcon as typeof Icon bgColor = '#FEE12B' // Yellow color for parallel blocks } }