1- import { useCallback , useEffect , useMemo , useState } from 'react'
1+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
22import { useReactFlow } from 'reactflow'
33import { Combobox , type ComboboxOption } from '@/components/emcn/components'
44import { cn } from '@/lib/core/utils/cn'
@@ -7,6 +7,9 @@ import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workfl
77import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
88import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
99import type { SubBlockConfig } from '@/blocks/types'
10+ import { getDependsOnFields } from '@/blocks/utils'
11+ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
12+ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1013
1114/**
1215 * Constants for ComboBox component behavior
@@ -48,6 +51,13 @@ interface ComboBoxProps {
4851 placeholder ?: string
4952 /** Configuration for the sub-block */
5053 config : SubBlockConfig
54+ /** Async function to fetch options dynamically */
55+ fetchOptions ?: (
56+ blockId : string ,
57+ subBlockId : string
58+ ) => Promise < Array < { label : string ; id : string } > >
59+ /** Field dependencies that trigger option refetch when changed */
60+ dependsOn ?: SubBlockConfig [ 'dependsOn' ]
5161}
5262
5363export function ComboBox ( {
@@ -61,23 +71,77 @@ export function ComboBox({
6171 disabled,
6272 placeholder = 'Type or select an option...' ,
6373 config,
74+ fetchOptions,
75+ dependsOn,
6476} : ComboBoxProps ) {
6577 // Hooks and context
6678 const [ storeValue , setStoreValue ] = useSubBlockValue < string > ( blockId , subBlockId )
6779 const accessiblePrefixes = useAccessibleReferencePrefixes ( blockId )
6880 const reactFlowInstance = useReactFlow ( )
6981
82+ // Dependency tracking for fetchOptions
83+ const dependsOnFields = useMemo ( ( ) => getDependsOnFields ( dependsOn ) , [ dependsOn ] )
84+ const activeWorkflowId = useWorkflowRegistry ( ( s ) => s . activeWorkflowId )
85+ const dependencyValues = useSubBlockStore (
86+ useCallback (
87+ ( state ) => {
88+ if ( dependsOnFields . length === 0 || ! activeWorkflowId ) return [ ]
89+ const workflowValues = state . workflowValues [ activeWorkflowId ] || { }
90+ const blockValues = workflowValues [ blockId ] || { }
91+ return dependsOnFields . map ( ( depKey ) => blockValues [ depKey ] ?? null )
92+ } ,
93+ [ dependsOnFields , activeWorkflowId , blockId ]
94+ )
95+ )
96+
7097 // State management
7198 const [ storeInitialized , setStoreInitialized ] = useState ( false )
99+ const [ fetchedOptions , setFetchedOptions ] = useState < Array < { label : string ; id : string } > > ( [ ] )
100+ const [ isLoadingOptions , setIsLoadingOptions ] = useState ( false )
101+ const [ fetchError , setFetchError ] = useState < string | null > ( null )
102+ const previousDependencyValuesRef = useRef < string > ( '' )
103+
104+ /**
105+ * Fetches options from the async fetchOptions function if provided
106+ */
107+ const fetchOptionsIfNeeded = useCallback ( async ( ) => {
108+ if ( ! fetchOptions || isPreview || disabled ) return
109+
110+ setIsLoadingOptions ( true )
111+ setFetchError ( null )
112+ try {
113+ const options = await fetchOptions ( blockId , subBlockId )
114+ setFetchedOptions ( options )
115+ } catch ( error ) {
116+ const errorMessage = error instanceof Error ? error . message : 'Failed to fetch options'
117+ setFetchError ( errorMessage )
118+ setFetchedOptions ( [ ] )
119+ } finally {
120+ setIsLoadingOptions ( false )
121+ }
122+ } , [ fetchOptions , blockId , subBlockId , isPreview , disabled ] )
72123
73124 // Determine the active value based on mode (preview vs. controlled vs. store)
74125 const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
75126
76- // Evaluate options if provided as a function
77- const evaluatedOptions = useMemo ( ( ) => {
127+ // Evaluate static options if provided as a function
128+ const staticOptions = useMemo ( ( ) => {
78129 return typeof options === 'function' ? options ( ) : options
79130 } , [ options ] )
80131
132+ // Normalize fetched options to match ComboBoxOption format
133+ const normalizedFetchedOptions = useMemo ( ( ) : ComboBoxOption [ ] => {
134+ return fetchedOptions . map ( ( opt ) => ( { label : opt . label , id : opt . id } ) )
135+ } , [ fetchedOptions ] )
136+
137+ // Merge static and fetched options - fetched options take priority when available
138+ const evaluatedOptions = useMemo ( ( ) : ComboBoxOption [ ] => {
139+ if ( fetchOptions && normalizedFetchedOptions . length > 0 ) {
140+ return normalizedFetchedOptions
141+ }
142+ return staticOptions
143+ } , [ fetchOptions , normalizedFetchedOptions , staticOptions ] )
144+
81145 // Convert options to Combobox format
82146 const comboboxOptions = useMemo ( ( ) : ComboboxOption [ ] => {
83147 return evaluatedOptions . map ( ( option ) => {
@@ -160,6 +224,44 @@ export function ComboBox({
160224 }
161225 } , [ storeInitialized , value , defaultOptionValue , setStoreValue ] )
162226
227+ // Clear fetched options when dependencies change
228+ useEffect ( ( ) => {
229+ if ( fetchOptions && dependsOnFields . length > 0 ) {
230+ const currentDependencyValuesStr = JSON . stringify ( dependencyValues )
231+ const previousDependencyValuesStr = previousDependencyValuesRef . current
232+
233+ if (
234+ previousDependencyValuesStr &&
235+ currentDependencyValuesStr !== previousDependencyValuesStr
236+ ) {
237+ setFetchedOptions ( [ ] )
238+ }
239+
240+ previousDependencyValuesRef . current = currentDependencyValuesStr
241+ }
242+ } , [ dependencyValues , fetchOptions , dependsOnFields . length ] )
243+
244+ // Fetch options when needed (on mount, when enabled, or when dependencies change)
245+ useEffect ( ( ) => {
246+ if (
247+ fetchOptions &&
248+ ! isPreview &&
249+ ! disabled &&
250+ fetchedOptions . length === 0 &&
251+ ! isLoadingOptions
252+ ) {
253+ fetchOptionsIfNeeded ( )
254+ }
255+ } , [
256+ fetchOptions ,
257+ isPreview ,
258+ disabled ,
259+ fetchedOptions . length ,
260+ isLoadingOptions ,
261+ fetchOptionsIfNeeded ,
262+ dependencyValues , // Refetch when dependencies change
263+ ] )
264+
163265 /**
164266 * Handles wheel event for ReactFlow zoom control
165267 * Intercepts Ctrl/Cmd+Wheel to zoom the canvas
@@ -247,11 +349,13 @@ export function ComboBox({
247349 return option . id === newValue
248350 } )
249351
250- if ( ! matchedOption ) {
251- return
252- }
253-
254- const nextValue = typeof matchedOption === 'string' ? matchedOption : matchedOption . id
352+ // If a matching option is found, store its ID; otherwise store the raw value
353+ // (allows expressions like <block.output> to be entered directly)
354+ const nextValue = matchedOption
355+ ? typeof matchedOption === 'string'
356+ ? matchedOption
357+ : matchedOption . id
358+ : newValue
255359 setStoreValue ( nextValue )
256360 } }
257361 isPreview = { isPreview }
@@ -293,6 +397,13 @@ export function ComboBox({
293397 onWheel : handleWheel ,
294398 autoComplete : 'off' ,
295399 } }
400+ isLoading = { isLoadingOptions }
401+ error = { fetchError }
402+ onOpenChange = { ( open ) => {
403+ if ( open ) {
404+ void fetchOptionsIfNeeded ( )
405+ }
406+ } }
296407 />
297408 ) }
298409 </ SubBlockInputController >
0 commit comments