mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(var-resolution): resolve variables with block name check and consolidate code (#1469)
* improvement(var-resolution): resolve variables with block name check and consolidate code * fix tests * fix type error * fix var highlighting in kb tags * fix kb tags
This commit is contained in:
+6
-1
@@ -10,6 +10,7 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useTagSelection } from '@/hooks/use-tag-selection'
|
||||
|
||||
@@ -60,6 +61,7 @@ export function ComboBox({
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
||||
|
||||
const emitTagSelection = useTagSelection(blockId, subBlockId)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
@@ -432,7 +434,10 @@ export function ComboBox({
|
||||
style={{ right: '42px' }}
|
||||
>
|
||||
<div className='w-full truncate text-foreground' style={{ scrollbarWidth: 'none' }}>
|
||||
{formatDisplayText(displayValue)}
|
||||
{formatDisplayText(displayValue, {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/* Chevron button */}
|
||||
|
||||
+10
-1
@@ -5,6 +5,7 @@ import { Label } from '@/components/ui/label'
|
||||
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
interface InputFormatField {
|
||||
@@ -152,6 +153,8 @@ export function InputMapping({
|
||||
setMapping(updated)
|
||||
}
|
||||
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
if (!selectedWorkflowId) {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center rounded-lg border border-border/50 bg-muted/30 p-8 text-center'>
|
||||
@@ -213,6 +216,7 @@ export function InputMapping({
|
||||
blockId={blockId}
|
||||
subBlockId={subBlockId}
|
||||
disabled={isPreview || disabled}
|
||||
accessiblePrefixes={accessiblePrefixes}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -229,6 +233,7 @@ function InputMappingField({
|
||||
blockId,
|
||||
subBlockId,
|
||||
disabled,
|
||||
accessiblePrefixes,
|
||||
}: {
|
||||
fieldName: string
|
||||
fieldType?: string
|
||||
@@ -237,6 +242,7 @@ function InputMappingField({
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
disabled: boolean
|
||||
accessiblePrefixes: Set<string> | undefined
|
||||
}) {
|
||||
const [showTags, setShowTags] = useState(false)
|
||||
const [cursorPosition, setCursorPosition] = useState(0)
|
||||
@@ -318,7 +324,10 @@ function InputMappingField({
|
||||
className='w-full whitespace-pre'
|
||||
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
|
||||
>
|
||||
{formatDisplayText(value)}
|
||||
{formatDisplayText(value, {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+10
-1
@@ -7,6 +7,7 @@ import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions'
|
||||
import { useTagSelection } from '@/hooks/use-tag-selection'
|
||||
@@ -55,6 +56,9 @@ export function KnowledgeTagFilters({
|
||||
// Use KB tag definitions hook to get available tags
|
||||
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
|
||||
|
||||
// Get accessible prefixes for variable highlighting
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
// State for managing tag dropdown
|
||||
const [activeTagDropdown, setActiveTagDropdown] = useState<{
|
||||
rowIndex: number
|
||||
@@ -314,7 +318,12 @@ export function KnowledgeTagFilters({
|
||||
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
|
||||
<div className='whitespace-pre'>{formatDisplayText(cellValue)}</div>
|
||||
<div className='whitespace-pre'>
|
||||
{formatDisplayText(cellValue, {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
+6
-1
@@ -11,6 +11,7 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useTagSelection } from '@/hooks/use-tag-selection'
|
||||
@@ -92,6 +93,7 @@ export function LongInput({
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value or prop value
|
||||
const baseValue = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
|
||||
@@ -405,7 +407,10 @@ export function LongInput({
|
||||
height: `${height}px`,
|
||||
}}
|
||||
>
|
||||
{formatDisplayText(value?.toString() ?? '')}
|
||||
{formatDisplayText(value?.toString() ?? '', {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Wand Button */}
|
||||
|
||||
+7
-1
@@ -11,6 +11,7 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useTagSelection } from '@/hooks/use-tag-selection'
|
||||
@@ -345,6 +346,8 @@ export function ShortInput({
|
||||
}
|
||||
}
|
||||
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
return (
|
||||
<>
|
||||
<WandPromptBar
|
||||
@@ -417,7 +420,10 @@ export function ShortInput({
|
||||
>
|
||||
{password && !isFocused
|
||||
? '•'.repeat(value?.toString().length ?? 0)
|
||||
: formatDisplayText(value?.toString() ?? '')}
|
||||
: formatDisplayText(value?.toString() ?? '', {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+6
-1
@@ -22,6 +22,7 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
|
||||
interface Field {
|
||||
id: string
|
||||
@@ -80,6 +81,7 @@ export function FieldFormat({
|
||||
const [cursorPosition, setCursorPosition] = useState(0)
|
||||
const [activeFieldId, setActiveFieldId] = useState<string | null>(null)
|
||||
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
@@ -471,7 +473,10 @@ export function FieldFormat({
|
||||
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
|
||||
>
|
||||
{formatDisplayText(
|
||||
(localValues[field.id] ?? field.value ?? '')?.toString()
|
||||
(localValues[field.id] ?? field.value ?? '')?.toString(),
|
||||
accessiblePrefixes
|
||||
? { accessiblePrefixes }
|
||||
: { highlightAll: true }
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+22
-4
@@ -24,6 +24,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { McpTransport } from '@/lib/mcp/types'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { useMcpServerTest } from '@/hooks/use-mcp-server-test'
|
||||
import { useMcpServersStore } from '@/stores/mcp-servers/store'
|
||||
|
||||
@@ -33,6 +34,7 @@ interface McpServerModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onServerCreated?: () => void
|
||||
blockId: string
|
||||
}
|
||||
|
||||
interface McpServerFormData {
|
||||
@@ -42,7 +44,12 @@ interface McpServerFormData {
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export function McpServerModal({ open, onOpenChange, onServerCreated }: McpServerModalProps) {
|
||||
export function McpServerModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onServerCreated,
|
||||
blockId,
|
||||
}: McpServerModalProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const [formData, setFormData] = useState<McpServerFormData>({
|
||||
@@ -262,6 +269,8 @@ export function McpServerModal({ open, onOpenChange, onServerCreated }: McpServe
|
||||
workspaceId,
|
||||
])
|
||||
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className='sm:max-w-[600px]'>
|
||||
@@ -337,7 +346,10 @@ export function McpServerModal({ open, onOpenChange, onServerCreated }: McpServe
|
||||
className='whitespace-nowrap'
|
||||
style={{ transform: `translateX(-${urlScrollLeft}px)` }}
|
||||
>
|
||||
{formatDisplayText(formData.url || '')}
|
||||
{formatDisplayText(formData.url || '', {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -389,7 +401,10 @@ export function McpServerModal({ open, onOpenChange, onServerCreated }: McpServe
|
||||
transform: `translateX(-${headerScrollLeft[`key-${index}`] || 0}px)`,
|
||||
}}
|
||||
>
|
||||
{formatDisplayText(key || '')}
|
||||
{formatDisplayText(key || '', {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -417,7 +432,10 @@ export function McpServerModal({ open, onOpenChange, onServerCreated }: McpServe
|
||||
transform: `translateX(-${headerScrollLeft[`value-${index}`] || 0}px)`,
|
||||
}}
|
||||
>
|
||||
{formatDisplayText(value || '')}
|
||||
{formatDisplayText(value || '', {
|
||||
accessiblePrefixes,
|
||||
highlightAll: !accessiblePrefixes,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
@@ -1977,6 +1977,7 @@ export function ToolInput({
|
||||
// Refresh MCP tools when a new server is created
|
||||
refreshTools(true)
|
||||
}}
|
||||
blockId={blockId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { useMemo } from 'react'
|
||||
import { shallow } from 'zustand/shallow'
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/references'
|
||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
|
||||
export function useAccessibleReferencePrefixes(blockId?: string | null): Set<string> | undefined {
|
||||
const { blocks, edges, loops, parallels } = useWorkflowStore(
|
||||
(state) => ({
|
||||
blocks: state.blocks,
|
||||
edges: state.edges,
|
||||
loops: state.loops || {},
|
||||
parallels: state.parallels || {},
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!blockId) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const graphEdges = edges.map((edge) => ({ source: edge.source, target: edge.target }))
|
||||
const ancestorIds = BlockPathCalculator.findAllPathNodes(graphEdges, blockId)
|
||||
const accessibleIds = new Set<string>(ancestorIds)
|
||||
accessibleIds.add(blockId)
|
||||
|
||||
const starterBlock = Object.values(blocks).find((block) => block.type === 'starter')
|
||||
if (starterBlock) {
|
||||
accessibleIds.add(starterBlock.id)
|
||||
}
|
||||
|
||||
const loopValues = Object.values(loops as Record<string, Loop>)
|
||||
loopValues.forEach((loop) => {
|
||||
if (!loop?.nodes) return
|
||||
if (loop.nodes.includes(blockId)) {
|
||||
loop.nodes.forEach((nodeId) => accessibleIds.add(nodeId))
|
||||
}
|
||||
})
|
||||
|
||||
const parallelValues = Object.values(parallels as Record<string, Parallel>)
|
||||
parallelValues.forEach((parallel) => {
|
||||
if (!parallel?.nodes) return
|
||||
if (parallel.nodes.includes(blockId)) {
|
||||
parallel.nodes.forEach((nodeId) => accessibleIds.add(nodeId))
|
||||
}
|
||||
})
|
||||
|
||||
const prefixes = new Set<string>()
|
||||
accessibleIds.forEach((id) => {
|
||||
prefixes.add(normalizeBlockName(id))
|
||||
const block = blocks[id]
|
||||
if (block?.name) {
|
||||
prefixes.add(normalizeBlockName(block.name))
|
||||
}
|
||||
})
|
||||
|
||||
SYSTEM_REFERENCE_PREFIXES.forEach((prefix) => prefixes.add(prefix))
|
||||
|
||||
return prefixes
|
||||
}, [blockId, blocks, edges, loops, parallels])
|
||||
}
|
||||
@@ -1,28 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
||||
|
||||
export interface HighlightContext {
|
||||
accessiblePrefixes?: Set<string>
|
||||
highlightAll?: boolean
|
||||
}
|
||||
|
||||
const SYSTEM_PREFIXES = new Set(['start', 'loop', 'parallel', 'variable'])
|
||||
|
||||
/**
|
||||
* Formats text by highlighting block references (<...>) and environment variables ({{...}})
|
||||
* Used in code editor, long inputs, and short inputs for consistent syntax highlighting
|
||||
*
|
||||
* @param text The text to format
|
||||
*/
|
||||
export function formatDisplayText(text: string): ReactNode[] {
|
||||
export function formatDisplayText(text: string, context?: HighlightContext): ReactNode[] {
|
||||
if (!text) return []
|
||||
|
||||
const shouldHighlightPart = (part: string): boolean => {
|
||||
if (!part.startsWith('<') || !part.endsWith('>')) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (context?.highlightAll) {
|
||||
return true
|
||||
}
|
||||
|
||||
const inner = part.slice(1, -1)
|
||||
const [prefix] = inner.split('.')
|
||||
const normalizedPrefix = normalizeBlockName(prefix)
|
||||
|
||||
if (SYSTEM_PREFIXES.has(normalizedPrefix)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (context?.accessiblePrefixes?.has(normalizedPrefix)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const parts = text.split(/(<[^>]+>|\{\{[^}]+\}\})/g)
|
||||
|
||||
return parts.map((part, index) => {
|
||||
if (part.startsWith('<') && part.endsWith('>')) {
|
||||
return (
|
||||
<span key={index} className='text-blue-500'>
|
||||
{part}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (part.match(/^\{\{[^}]+\}\}$/)) {
|
||||
if (shouldHighlightPart(part) || part.match(/^\{\{[^}]+\}\}$/)) {
|
||||
return (
|
||||
<span key={index} className='text-blue-500'>
|
||||
{part}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { shallow } from 'zustand/shallow'
|
||||
import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getBlockOutputPaths, getBlockOutputType } from '@/lib/workflows/block-outputs'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import type { Variable } from '@/stores/panel/variables/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
@@ -25,6 +24,15 @@ interface BlockTagGroup {
|
||||
distance: number
|
||||
}
|
||||
|
||||
interface NestedBlockTagGroup extends BlockTagGroup {
|
||||
nestedTags: Array<{
|
||||
key: string
|
||||
display: string
|
||||
fullTag?: string
|
||||
children?: Array<{ key: string; display: string; fullTag: string }>
|
||||
}>
|
||||
}
|
||||
|
||||
interface TagDropdownProps {
|
||||
visible: boolean
|
||||
onSelect: (newValue: string) => void
|
||||
@@ -70,6 +78,18 @@ const normalizeVariableName = (variableName: string): string => {
|
||||
return variableName.replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
const ensureRootTag = (tags: string[], rootTag: string): string[] => {
|
||||
if (!rootTag) {
|
||||
return tags
|
||||
}
|
||||
|
||||
if (tags.includes(rootTag)) {
|
||||
return tags
|
||||
}
|
||||
|
||||
return [rootTag, ...tags]
|
||||
}
|
||||
|
||||
const getSubBlockValue = (blockId: string, property: string): any => {
|
||||
return useSubBlockStore.getState().getValue(blockId, property)
|
||||
}
|
||||
@@ -300,12 +320,27 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const [parentHovered, setParentHovered] = useState<string | null>(null)
|
||||
const [submenuHovered, setSubmenuHovered] = useState(false)
|
||||
|
||||
const blocks = useWorkflowStore((state) => state.blocks)
|
||||
const loops = useWorkflowStore((state) => state.loops)
|
||||
const parallels = useWorkflowStore((state) => state.parallels)
|
||||
const edges = useWorkflowStore((state) => state.edges)
|
||||
const { blocks, edges, loops, parallels } = useWorkflowStore(
|
||||
(state) => ({
|
||||
blocks: state.blocks,
|
||||
edges: state.edges,
|
||||
loops: state.loops || {},
|
||||
parallels: state.parallels || {},
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
|
||||
const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
|
||||
const rawAccessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
|
||||
const combinedAccessiblePrefixes = useMemo(() => {
|
||||
if (!rawAccessiblePrefixes) return new Set<string>()
|
||||
const normalized = new Set<string>(rawAccessiblePrefixes)
|
||||
normalized.add(normalizeBlockName(blockId))
|
||||
return normalized
|
||||
}, [rawAccessiblePrefixes, blockId])
|
||||
|
||||
// Subscribe to live subblock values for the active workflow to react to input format changes
|
||||
const workflowSubBlockValues = useSubBlockStore((state) =>
|
||||
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
|
||||
@@ -325,7 +360,6 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
)
|
||||
|
||||
const getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId)
|
||||
const variables = useVariablesStore((state) => state.variables)
|
||||
const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : []
|
||||
|
||||
const searchTerm = useMemo(() => {
|
||||
@@ -336,8 +370,12 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
|
||||
const {
|
||||
tags,
|
||||
variableInfoMap = {},
|
||||
blockTagGroups = [],
|
||||
variableInfoMap,
|
||||
blockTagGroups: computedBlockTagGroups,
|
||||
}: {
|
||||
tags: string[]
|
||||
variableInfoMap: Record<string, { type: string; id: string }>
|
||||
blockTagGroups: BlockTagGroup[]
|
||||
} = useMemo(() => {
|
||||
if (activeSourceBlockId) {
|
||||
const sourceBlock = blocks[activeSourceBlockId]
|
||||
@@ -481,6 +519,12 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
blockTags = ensureRootTag(blockTags, normalizedBlockName)
|
||||
const shouldShowRootTag = sourceBlock.type === 'generic_webhook'
|
||||
if (!shouldShowRootTag) {
|
||||
blockTags = blockTags.filter((tag) => tag !== normalizedBlockName)
|
||||
}
|
||||
|
||||
const blockTagGroups: BlockTagGroup[] = [
|
||||
{
|
||||
blockName,
|
||||
@@ -507,18 +551,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const serializer = new Serializer()
|
||||
const serializedWorkflow = serializer.serializeWorkflow(blocks, edges, loops, parallels)
|
||||
|
||||
const accessibleBlockIds = BlockPathCalculator.findAllPathNodes(
|
||||
serializedWorkflow.connections,
|
||||
blockId
|
||||
)
|
||||
|
||||
const starterBlock = Object.values(blocks).find((block) => block.type === 'starter')
|
||||
if (starterBlock && !accessibleBlockIds.includes(starterBlock.id)) {
|
||||
accessibleBlockIds.push(starterBlock.id)
|
||||
}
|
||||
|
||||
const blockDistances: Record<string, number> = {}
|
||||
if (starterBlock) {
|
||||
@@ -623,6 +656,10 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const blockTagGroups: BlockTagGroup[] = []
|
||||
const allBlockTags: string[] = []
|
||||
|
||||
// Use the combinedAccessiblePrefixes to iterate through accessible blocks
|
||||
const accessibleBlockIds = combinedAccessiblePrefixes
|
||||
? Array.from(combinedAccessiblePrefixes)
|
||||
: []
|
||||
for (const accessibleBlockId of accessibleBlockIds) {
|
||||
const accessibleBlock = blocks[accessibleBlockId]
|
||||
if (!accessibleBlock) continue
|
||||
@@ -648,7 +685,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const normalizedBlockName = normalizeBlockName(blockName)
|
||||
|
||||
const outputPaths = generateOutputPaths(mockConfig.outputs)
|
||||
const blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
let blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
blockTags = ensureRootTag(blockTags, normalizedBlockName)
|
||||
|
||||
blockTagGroups.push({
|
||||
blockName,
|
||||
@@ -750,6 +788,12 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
blockTags = ensureRootTag(blockTags, normalizedBlockName)
|
||||
const shouldShowRootTag = accessibleBlock.type === 'generic_webhook'
|
||||
if (!shouldShowRootTag) {
|
||||
blockTags = blockTags.filter((tag) => tag !== normalizedBlockName)
|
||||
}
|
||||
|
||||
blockTagGroups.push({
|
||||
blockName,
|
||||
blockId: accessibleBlockId,
|
||||
@@ -781,51 +825,54 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
|
||||
return {
|
||||
tags: [...variableTags, ...contextualTags, ...allBlockTags],
|
||||
tags: [...allBlockTags, ...variableTags, ...contextualTags],
|
||||
variableInfoMap,
|
||||
blockTagGroups: finalBlockTagGroups,
|
||||
}
|
||||
}, [
|
||||
activeSourceBlockId,
|
||||
combinedAccessiblePrefixes,
|
||||
blockId,
|
||||
blocks,
|
||||
edges,
|
||||
getMergedSubBlocks,
|
||||
loops,
|
||||
parallels,
|
||||
blockId,
|
||||
activeSourceBlockId,
|
||||
workflowVariables,
|
||||
workflowSubBlockValues,
|
||||
getMergedSubBlocks,
|
||||
workflowId,
|
||||
])
|
||||
|
||||
const filteredTags = useMemo(() => {
|
||||
if (!searchTerm) return tags
|
||||
return tags.filter((tag: string) => tag.toLowerCase().includes(searchTerm))
|
||||
return tags.filter((tag) => tag.toLowerCase().includes(searchTerm))
|
||||
}, [tags, searchTerm])
|
||||
|
||||
const { variableTags, filteredBlockTagGroups } = useMemo(() => {
|
||||
const varTags: string[] = []
|
||||
|
||||
filteredTags.forEach((tag) => {
|
||||
filteredTags.forEach((tag: string) => {
|
||||
if (tag.startsWith(TAG_PREFIXES.VARIABLE)) {
|
||||
varTags.push(tag)
|
||||
}
|
||||
})
|
||||
|
||||
const filteredBlockTagGroups = blockTagGroups
|
||||
.map((group) => ({
|
||||
const filteredBlockTagGroups = computedBlockTagGroups
|
||||
.map((group: BlockTagGroup) => ({
|
||||
...group,
|
||||
tags: group.tags.filter((tag) => !searchTerm || tag.toLowerCase().includes(searchTerm)),
|
||||
tags: group.tags.filter(
|
||||
(tag: string) => !searchTerm || tag.toLowerCase().includes(searchTerm)
|
||||
),
|
||||
}))
|
||||
.filter((group) => group.tags.length > 0)
|
||||
.filter((group: BlockTagGroup) => group.tags.length > 0)
|
||||
|
||||
return {
|
||||
variableTags: varTags,
|
||||
filteredBlockTagGroups,
|
||||
}
|
||||
}, [filteredTags, blockTagGroups, searchTerm])
|
||||
}, [filteredTags, computedBlockTagGroups, searchTerm])
|
||||
|
||||
const nestedBlockTagGroups = useMemo(() => {
|
||||
return filteredBlockTagGroups.map((group) => {
|
||||
const nestedBlockTagGroups: NestedBlockTagGroup[] = useMemo(() => {
|
||||
return filteredBlockTagGroups.map((group: BlockTagGroup) => {
|
||||
const nestedTags: Array<{
|
||||
key: string
|
||||
display: string
|
||||
@@ -839,7 +886,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
> = {}
|
||||
const directTags: Array<{ key: string; display: string; fullTag: string }> = []
|
||||
|
||||
group.tags.forEach((tag) => {
|
||||
group.tags.forEach((tag: string) => {
|
||||
const tagParts = tag.split('.')
|
||||
if (tagParts.length >= 3) {
|
||||
const parent = tagParts[1]
|
||||
@@ -899,8 +946,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
|
||||
visualTags.push(...variableTags)
|
||||
|
||||
nestedBlockTagGroups.forEach((group) => {
|
||||
group.nestedTags.forEach((nestedTag) => {
|
||||
nestedBlockTagGroups.forEach((group: NestedBlockTagGroup) => {
|
||||
group.nestedTags.forEach((nestedTag: any) => {
|
||||
if (nestedTag.children && nestedTag.children.length > 0) {
|
||||
const firstChild = nestedTag.children[0]
|
||||
if (firstChild.fullTag) {
|
||||
@@ -952,8 +999,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
|
||||
if (tag.startsWith(TAG_PREFIXES.VARIABLE)) {
|
||||
const variableName = tag.substring(TAG_PREFIXES.VARIABLE.length)
|
||||
const variableObj = Object.values(variables).find(
|
||||
(v) => v.name.replace(/\s+/g, '') === variableName
|
||||
const variableObj = workflowVariables.find(
|
||||
(v: Variable) => v.name.replace(/\s+/g, '') === variableName
|
||||
)
|
||||
|
||||
if (variableObj) {
|
||||
@@ -985,7 +1032,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
onSelect(newValue)
|
||||
onClose?.()
|
||||
},
|
||||
[inputValue, cursorPosition, variables, onSelect, onClose]
|
||||
[inputValue, cursorPosition, workflowVariables, onSelect, onClose]
|
||||
)
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [searchTerm])
|
||||
@@ -1030,7 +1077,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
if (selectedIndex < 0 || selectedIndex >= orderedTags.length) return null
|
||||
const selectedTag = orderedTags[selectedIndex]
|
||||
for (let gi = 0; gi < nestedBlockTagGroups.length; gi++) {
|
||||
const group = nestedBlockTagGroups[gi]
|
||||
const group = nestedBlockTagGroups[gi]!
|
||||
for (let ni = 0; ni < group.nestedTags.length; ni++) {
|
||||
const nestedTag = group.nestedTags[ni]
|
||||
if (nestedTag.children && nestedTag.children.length > 0) {
|
||||
@@ -1051,16 +1098,16 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
const currentGroup = nestedBlockTagGroups.find((group) => {
|
||||
const currentGroup = nestedBlockTagGroups.find((group: NestedBlockTagGroup) => {
|
||||
return group.nestedTags.some(
|
||||
(tag, index) =>
|
||||
(tag: any, index: number) =>
|
||||
`${group.blockId}-${tag.key}` === currentHovered.tag &&
|
||||
index === currentHovered.index
|
||||
)
|
||||
})
|
||||
|
||||
const currentNestedTag = currentGroup?.nestedTags.find(
|
||||
(tag, index) =>
|
||||
(tag: any, index: number) =>
|
||||
`${currentGroup.blockId}-${tag.key}` === currentHovered.tag &&
|
||||
index === currentHovered.index
|
||||
)
|
||||
@@ -1089,8 +1136,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (submenuIndex >= 0 && submenuIndex < children.length) {
|
||||
const selectedChild = children[submenuIndex]
|
||||
handleTagSelect(selectedChild.fullTag, currentGroup)
|
||||
const selectedChild = children[submenuIndex] as any
|
||||
handleTagSelect(selectedChild.fullTag, currentGroup as BlockTagGroup | undefined)
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
@@ -1324,7 +1371,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
{nestedBlockTagGroups.length > 0 && (
|
||||
<>
|
||||
{variableTags.length > 0 && <div className='my-0' />}
|
||||
{nestedBlockTagGroups.map((group) => {
|
||||
{nestedBlockTagGroups.map((group: NestedBlockTagGroup) => {
|
||||
const blockConfig = getBlock(group.blockType)
|
||||
let blockColor = blockConfig?.bgColor || BLOCK_COLORS.DEFAULT
|
||||
|
||||
@@ -1340,7 +1387,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
{group.blockName}
|
||||
</div>
|
||||
<div>
|
||||
{group.nestedTags.map((nestedTag, index) => {
|
||||
{group.nestedTags.map((nestedTag: any, index: number) => {
|
||||
const tagIndex = nestedTag.fullTag
|
||||
? (tagIndexMap.get(nestedTag.fullTag) ?? -1)
|
||||
: -1
|
||||
@@ -1505,7 +1552,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}}
|
||||
>
|
||||
<div className='py-1'>
|
||||
{nestedTag.children!.map((child, childIndex) => {
|
||||
{nestedTag.children!.map((child: any, childIndex: number) => {
|
||||
const isKeyboardSelected =
|
||||
inSubmenu && submenuIndex === childIndex
|
||||
const isSelected = isKeyboardSelected
|
||||
|
||||
@@ -1356,7 +1356,7 @@ describe('InputResolver', () => {
|
||||
expect(result.code).toBe('return "Agent response"')
|
||||
})
|
||||
|
||||
it('should reject references to unconnected blocks', () => {
|
||||
it('should leave references to unconnected blocks as strings', () => {
|
||||
// Create a new block that is added to the workflow but not connected to isolated-block
|
||||
workflowWithConnections.blocks.push({
|
||||
id: 'test-block',
|
||||
@@ -1402,9 +1402,9 @@ describe('InputResolver', () => {
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
expect(() => connectionResolver.resolveInputs(testBlock, contextWithConnections)).toThrow(
|
||||
/Block "isolated-block" is not connected to this block/
|
||||
)
|
||||
// Should not throw - inaccessible references remain as strings
|
||||
const result = connectionResolver.resolveInputs(testBlock, contextWithConnections)
|
||||
expect(result.code).toBe('return <isolated-block.content>') // Reference remains as-is
|
||||
})
|
||||
|
||||
it('should always allow references to starter block', () => {
|
||||
@@ -1546,7 +1546,7 @@ describe('InputResolver', () => {
|
||||
expect(otherResult).toBe('content: Hello World')
|
||||
})
|
||||
|
||||
it('should provide helpful error messages for unconnected blocks', () => {
|
||||
it('should not throw for unconnected blocks and leave references as strings', () => {
|
||||
// Create a test block in the workflow first
|
||||
workflowWithConnections.blocks.push({
|
||||
id: 'test-block-2',
|
||||
@@ -1592,9 +1592,9 @@ describe('InputResolver', () => {
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
expect(() => connectionResolver.resolveInputs(testBlock, contextWithConnections)).toThrow(
|
||||
/Available connected blocks:.*Agent Block.*Start/
|
||||
)
|
||||
// Should not throw - references to nonexistent blocks remain as strings
|
||||
const result = connectionResolver.resolveInputs(testBlock, contextWithConnections)
|
||||
expect(result.code).toBe('return <nonexistent.value>') // Reference remains as-is
|
||||
})
|
||||
|
||||
it('should work with block names and normalized names', () => {
|
||||
@@ -1725,7 +1725,7 @@ describe('InputResolver', () => {
|
||||
extendedResolver.resolveInputs(block1, extendedContext)
|
||||
}).not.toThrow()
|
||||
|
||||
// Should fail for indirect connection
|
||||
// Should not fail for indirect connection - reference remains as string
|
||||
expect(() => {
|
||||
// Add the response block to the workflow so it can be validated properly
|
||||
extendedWorkflow.blocks.push({
|
||||
@@ -1748,8 +1748,9 @@ describe('InputResolver', () => {
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
}
|
||||
extendedResolver.resolveInputs(block2, extendedContext)
|
||||
}).toThrow(/Block "agent-1" is not connected to this block/)
|
||||
const result = extendedResolver.resolveInputs(block2, extendedContext)
|
||||
expect(result.test).toBe('<agent-1.content>') // Reference remains as-is since agent-1 is not accessible
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle blocks in same loop referencing each other', () => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { VariableManager } from '@/lib/variables/variable-manager'
|
||||
import { extractReferencePrefixes, SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/references'
|
||||
import { TRIGGER_REFERENCE_ALIAS_MAP } from '@/lib/workflows/triggers'
|
||||
import { getBlock } from '@/blocks/index'
|
||||
import type { LoopManager } from '@/executor/loops/loops'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
||||
|
||||
const logger = createLogger('InputResolver')
|
||||
|
||||
@@ -461,64 +463,40 @@ export class InputResolver {
|
||||
return value
|
||||
}
|
||||
|
||||
const blockMatches = value.match(/<([^>]+)>/g)
|
||||
if (!blockMatches) return value
|
||||
const blockMatches = extractReferencePrefixes(value)
|
||||
if (blockMatches.length === 0) return value
|
||||
|
||||
// Filter out patterns that are clearly not variable references (e.g., comparison operators)
|
||||
const validBlockMatches = blockMatches.filter((match) => this.isValidVariableReference(match))
|
||||
|
||||
// If no valid matches found after filtering, return original value
|
||||
if (validBlockMatches.length === 0) {
|
||||
return value
|
||||
}
|
||||
|
||||
// If we're in an API block body, check each valid match to see if it looks like XML rather than a reference
|
||||
if (
|
||||
currentBlock.metadata?.id === 'api' &&
|
||||
validBlockMatches.some((match) => {
|
||||
const innerContent = match.slice(1, -1)
|
||||
// Patterns that suggest this is XML, not a block reference:
|
||||
return (
|
||||
innerContent.includes(':') || // namespaces like soap:Envelope
|
||||
innerContent.includes('=') || // attributes like xmlns="http://..."
|
||||
innerContent.includes(' ') || // any space indicates attributes
|
||||
innerContent.includes('/') || // self-closing tags
|
||||
!innerContent.includes('.')
|
||||
) // block refs always have dots
|
||||
})
|
||||
) {
|
||||
return value // Likely XML content, return unchanged
|
||||
}
|
||||
const accessiblePrefixes = this.getAccessiblePrefixes(currentBlock)
|
||||
|
||||
let resolvedValue = value
|
||||
|
||||
// Check if we're in a template literal for function blocks
|
||||
const isInTemplateLiteral =
|
||||
currentBlock.metadata?.id === 'function' &&
|
||||
value.includes('${') &&
|
||||
value.includes('}') &&
|
||||
value.includes('`')
|
||||
|
||||
for (const match of validBlockMatches) {
|
||||
// Skip variables - they've already been processed
|
||||
if (match.startsWith('<variable.')) {
|
||||
for (const match of blockMatches) {
|
||||
const { raw, prefix } = match
|
||||
if (!accessiblePrefixes.has(prefix)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const path = match.slice(1, -1)
|
||||
const [blockRef, ...pathParts] = path.split('.')
|
||||
if (raw.startsWith('<variable.')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const path = raw.slice(1, -1)
|
||||
const [blockRefToken, ...pathParts] = path.split('.')
|
||||
const blockRef = blockRefToken.trim()
|
||||
|
||||
// Skip XML-like tags (but allow block names with spaces)
|
||||
if (blockRef.includes(':')) {
|
||||
continue
|
||||
}
|
||||
|
||||
// System references (start, loop, parallel, variable) are handled as special cases
|
||||
const isSystemReference = ['start', 'loop', 'parallel', 'variable'].includes(
|
||||
blockRef.toLowerCase()
|
||||
)
|
||||
// Check if we're in a template literal context
|
||||
const isInTemplateLiteral =
|
||||
currentBlock.metadata?.id === 'function' &&
|
||||
value.includes('${') &&
|
||||
value.includes('}') &&
|
||||
value.includes('`')
|
||||
|
||||
// System references and regular block references are both processed
|
||||
// System references (start, loop, parallel, variable) and regular block references are both processed
|
||||
// Accessibility validation happens later in validateBlockReference
|
||||
|
||||
// Special case for trigger block references (start, api, chat, manual)
|
||||
@@ -657,7 +635,7 @@ export class InputResolver {
|
||||
}
|
||||
}
|
||||
|
||||
resolvedValue = resolvedValue.replace(match, formattedValue)
|
||||
resolvedValue = resolvedValue.replace(raw, formattedValue)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -678,7 +656,7 @@ export class InputResolver {
|
||||
)
|
||||
|
||||
if (formattedValue !== null) {
|
||||
resolvedValue = resolvedValue.replace(match, formattedValue)
|
||||
resolvedValue = resolvedValue.replace(raw, formattedValue)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -699,7 +677,7 @@ export class InputResolver {
|
||||
)
|
||||
|
||||
if (formattedValue !== null) {
|
||||
resolvedValue = resolvedValue.replace(match, formattedValue)
|
||||
resolvedValue = resolvedValue.replace(raw, formattedValue)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -723,7 +701,7 @@ export class InputResolver {
|
||||
const isInActivePath = context.activeExecutionPath.has(sourceBlock.id)
|
||||
|
||||
if (!isInActivePath) {
|
||||
resolvedValue = resolvedValue.replace(match, '')
|
||||
resolvedValue = resolvedValue.replace(raw, '')
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -753,14 +731,14 @@ export class InputResolver {
|
||||
const isInLoop = this.loopsByBlockId.has(sourceBlock.id)
|
||||
|
||||
if (isInLoop) {
|
||||
resolvedValue = resolvedValue.replace(match, '')
|
||||
resolvedValue = resolvedValue.replace(raw, '')
|
||||
continue
|
||||
}
|
||||
|
||||
// If the block hasn't been executed and isn't in the active path,
|
||||
// it means it's in an inactive branch - return empty string
|
||||
if (!context.activeExecutionPath.has(sourceBlock.id)) {
|
||||
resolvedValue = resolvedValue.replace(match, '')
|
||||
resolvedValue = resolvedValue.replace(raw, '')
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -861,7 +839,7 @@ export class InputResolver {
|
||||
: String(replacementValue)
|
||||
}
|
||||
|
||||
resolvedValue = resolvedValue.replace(match, formattedValue)
|
||||
resolvedValue = resolvedValue.replace(raw, formattedValue)
|
||||
}
|
||||
|
||||
return resolvedValue
|
||||
@@ -1362,6 +1340,18 @@ export class InputResolver {
|
||||
if (!sourceBlock) {
|
||||
const normalizedRef = this.normalizeBlockName(blockRef)
|
||||
sourceBlock = this.blockByNormalizedName.get(normalizedRef)
|
||||
|
||||
if (!sourceBlock) {
|
||||
for (const candidate of this.workflow.blocks) {
|
||||
const candidateName = candidate.metadata?.name
|
||||
if (!candidateName) continue
|
||||
const normalizedName = this.normalizeBlockName(candidateName)
|
||||
if (normalizedName === normalizedRef) {
|
||||
sourceBlock = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourceBlock) {
|
||||
@@ -2035,4 +2025,21 @@ export class InputResolver {
|
||||
getContainingParallelId(blockId: string): string | undefined {
|
||||
return this.parallelsByBlockId.get(blockId)
|
||||
}
|
||||
|
||||
private getAccessiblePrefixes(block: SerializedBlock): Set<string> {
|
||||
const prefixes = new Set<string>()
|
||||
|
||||
const accessibleBlocks = this.getAccessibleBlocks(block.id)
|
||||
accessibleBlocks.forEach((blockId) => {
|
||||
prefixes.add(normalizeBlockName(blockId))
|
||||
const sourceBlock = this.blockById.get(blockId)
|
||||
if (sourceBlock?.metadata?.name) {
|
||||
prefixes.add(normalizeBlockName(sourceBlock.metadata.name))
|
||||
}
|
||||
})
|
||||
|
||||
SYSTEM_REFERENCE_PREFIXES.forEach((prefix) => prefixes.add(prefix))
|
||||
|
||||
return prefixes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
||||
|
||||
export const SYSTEM_REFERENCE_PREFIXES = new Set(['start', 'loop', 'parallel', 'variable'])
|
||||
|
||||
const INVALID_REFERENCE_CHARS = /[+*/=<>!]/
|
||||
|
||||
export function isLikelyReferenceSegment(segment: string): boolean {
|
||||
if (!segment.startsWith('<') || !segment.endsWith('>')) {
|
||||
return false
|
||||
}
|
||||
|
||||
const inner = segment.slice(1, -1)
|
||||
|
||||
if (inner.startsWith(' ')) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (inner.match(/^\s*[<>=!]+\s*$/) || inner.match(/\s[<>=!]+\s/)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (inner.match(/^[<>=!]+\s/)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (inner.includes('.')) {
|
||||
const dotIndex = inner.indexOf('.')
|
||||
const beforeDot = inner.substring(0, dotIndex)
|
||||
const afterDot = inner.substring(dotIndex + 1)
|
||||
|
||||
if (afterDot.includes(' ')) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (INVALID_REFERENCE_CHARS.test(beforeDot) || INVALID_REFERENCE_CHARS.test(afterDot)) {
|
||||
return false
|
||||
}
|
||||
} else if (INVALID_REFERENCE_CHARS.test(inner) || inner.match(/^\d/) || inner.match(/\s\d/)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function extractReferencePrefixes(value: string): Array<{ raw: string; prefix: string }> {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return []
|
||||
}
|
||||
|
||||
const matches = value.match(/<[^>]+>/g)
|
||||
if (!matches) {
|
||||
return []
|
||||
}
|
||||
|
||||
const references: Array<{ raw: string; prefix: string }> = []
|
||||
|
||||
for (const match of matches) {
|
||||
if (!isLikelyReferenceSegment(match)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const inner = match.slice(1, -1)
|
||||
const [rawPrefix] = inner.split('.')
|
||||
if (!rawPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
const normalized = normalizeBlockName(rawPrefix)
|
||||
references.push({ raw: match, prefix: normalized })
|
||||
}
|
||||
|
||||
return references
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Edge } from 'reactflow'
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
@@ -44,22 +45,36 @@ export class Serializer {
|
||||
parallels?: Record<string, Parallel>,
|
||||
validateRequired = false
|
||||
): SerializedWorkflow {
|
||||
// Validate subflow requirements (loops/parallels) before serialization if requested
|
||||
const safeLoops = loops || {}
|
||||
const safeParallels = parallels || {}
|
||||
const accessibleBlocksMap = this.computeAccessibleBlockIds(
|
||||
blocks,
|
||||
edges,
|
||||
safeLoops,
|
||||
safeParallels
|
||||
)
|
||||
|
||||
if (validateRequired) {
|
||||
this.validateSubflowsBeforeExecution(blocks, loops || {}, parallels || {})
|
||||
this.validateSubflowsBeforeExecution(blocks, safeLoops, safeParallels)
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1.0',
|
||||
blocks: Object.values(blocks).map((block) => this.serializeBlock(block, validateRequired)),
|
||||
blocks: Object.values(blocks).map((block) =>
|
||||
this.serializeBlock(block, {
|
||||
validateRequired,
|
||||
allBlocks: blocks,
|
||||
accessibleBlocksMap,
|
||||
})
|
||||
),
|
||||
connections: edges.map((edge) => ({
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
sourceHandle: edge.sourceHandle || undefined,
|
||||
targetHandle: edge.targetHandle || undefined,
|
||||
})),
|
||||
loops,
|
||||
parallels,
|
||||
loops: safeLoops,
|
||||
parallels: safeParallels,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +171,14 @@ export class Serializer {
|
||||
})
|
||||
}
|
||||
|
||||
private serializeBlock(block: BlockState, validateRequired = false): SerializedBlock {
|
||||
private serializeBlock(
|
||||
block: BlockState,
|
||||
options: {
|
||||
validateRequired: boolean
|
||||
allBlocks: Record<string, BlockState>
|
||||
accessibleBlocksMap: Map<string, Set<string>>
|
||||
}
|
||||
): SerializedBlock {
|
||||
// Special handling for subflow blocks (loops, parallels, etc.)
|
||||
if (block.type === 'loop' || block.type === 'parallel') {
|
||||
return {
|
||||
@@ -197,7 +219,7 @@ export class Serializer {
|
||||
}
|
||||
|
||||
// Validate required fields that only users can provide (before execution starts)
|
||||
if (validateRequired) {
|
||||
if (options.validateRequired) {
|
||||
this.validateRequiredFieldsBeforeExecution(block, blockConfig, params)
|
||||
}
|
||||
|
||||
@@ -541,6 +563,46 @@ export class Serializer {
|
||||
}
|
||||
}
|
||||
|
||||
private computeAccessibleBlockIds(
|
||||
blocks: Record<string, BlockState>,
|
||||
edges: Edge[],
|
||||
loops: Record<string, Loop>,
|
||||
parallels: Record<string, Parallel>
|
||||
): Map<string, Set<string>> {
|
||||
const accessibleMap = new Map<string, Set<string>>()
|
||||
const simplifiedEdges = edges.map((edge) => ({ source: edge.source, target: edge.target }))
|
||||
|
||||
const starterBlock = Object.values(blocks).find((block) => block.type === 'starter')
|
||||
|
||||
Object.keys(blocks).forEach((blockId) => {
|
||||
const ancestorIds = BlockPathCalculator.findAllPathNodes(simplifiedEdges, blockId)
|
||||
const accessibleIds = new Set<string>(ancestorIds)
|
||||
accessibleIds.add(blockId)
|
||||
|
||||
if (starterBlock) {
|
||||
accessibleIds.add(starterBlock.id)
|
||||
}
|
||||
|
||||
Object.values(loops).forEach((loop) => {
|
||||
if (!loop?.nodes) return
|
||||
if (loop.nodes.includes(blockId)) {
|
||||
loop.nodes.forEach((nodeId) => accessibleIds.add(nodeId))
|
||||
}
|
||||
})
|
||||
|
||||
Object.values(parallels).forEach((parallel) => {
|
||||
if (!parallel?.nodes) return
|
||||
if (parallel.nodes.includes(blockId)) {
|
||||
parallel.nodes.forEach((nodeId) => accessibleIds.add(nodeId))
|
||||
}
|
||||
})
|
||||
|
||||
accessibleMap.set(blockId, accessibleIds)
|
||||
})
|
||||
|
||||
return accessibleMap
|
||||
}
|
||||
|
||||
deserializeWorkflow(workflow: SerializedWorkflow): {
|
||||
blocks: Record<string, BlockState>
|
||||
edges: Edge[]
|
||||
|
||||
Reference in New Issue
Block a user