Skip to content

Commit b01884c

Browse files
committed
feat(combobox): added expression support to combobox
1 parent d248557 commit b01884c

File tree

3 files changed

+123
-9
lines changed

3 files changed

+123
-9
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx

Lines changed: 119 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useMemo, useState } from 'react'
1+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { useReactFlow } from 'reactflow'
33
import { Combobox, type ComboboxOption } from '@/components/emcn/components'
44
import { cn } from '@/lib/core/utils/cn'
@@ -7,6 +7,9 @@ import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workfl
77
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
88
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
99
import 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

5363
export 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>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,8 @@ function SubBlockComponent({
479479
previewValue={previewValue as any}
480480
disabled={isDisabled}
481481
config={config}
482+
fetchOptions={config.fetchOptions}
483+
dependsOn={config.dependsOn}
482484
/>
483485
</div>
484486
)

apps/sim/blocks/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,8 @@ export interface SubBlockConfig {
288288
useWebhookUrl?: boolean
289289
// Trigger-save specific: The trigger ID for validation and saving
290290
triggerId?: string
291-
// Dropdown specific: Function to fetch options dynamically (for multi-select or single-select)
291+
// Dropdown/Combobox: Function to fetch options dynamically
292+
// Works with both 'dropdown' (select-only) and 'combobox' (editable with expression support)
292293
fetchOptions?: (
293294
blockId: string,
294295
subBlockId: string

0 commit comments

Comments
 (0)