mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
feat(combobox): added expression support to combobox (#2697)
* feat(combobox): added expression support to combobox * fix chat messages styling in light mode * last sec stuff * ack comments
This commit is contained in:
@@ -157,7 +157,7 @@ export function ChatMessage({ message }: ChatMessageProps) {
|
|||||||
|
|
||||||
{formattedContent && !formattedContent.startsWith('Uploaded') && (
|
{formattedContent && !formattedContent.startsWith('Uploaded') && (
|
||||||
<div className='rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] transition-all duration-200'>
|
<div className='rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] transition-all duration-200'>
|
||||||
<div className='whitespace-pre-wrap break-words font-medium font-sans text-gray-100 text-sm leading-[1.25rem]'>
|
<div className='whitespace-pre-wrap break-words font-medium font-sans text-[var(--text-primary)] text-sm leading-[1.25rem]'>
|
||||||
<WordWrap text={formattedContent} />
|
<WordWrap text={formattedContent} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +168,7 @@ export function ChatMessage({ message }: ChatMessageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full max-w-full overflow-hidden pl-[2px] opacity-100 transition-opacity duration-200'>
|
<div className='w-full max-w-full overflow-hidden pl-[2px] opacity-100 transition-opacity duration-200'>
|
||||||
<div className='whitespace-pre-wrap break-words font-[470] font-season text-[#E8E8E8] text-sm leading-[1.25rem]'>
|
<div className='whitespace-pre-wrap break-words font-[470] font-season text-[var(--text-primary)] text-sm leading-[1.25rem]'>
|
||||||
<WordWrap text={formattedContent} />
|
<WordWrap text={formattedContent} />
|
||||||
{message.isStreaming && <StreamingIndicator />}
|
{message.isStreaming && <StreamingIndicator />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useReactFlow } from 'reactflow'
|
import { useReactFlow } from 'reactflow'
|
||||||
import { Combobox, type ComboboxOption } from '@/components/emcn/components'
|
import { Combobox, type ComboboxOption } from '@/components/emcn/components'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
@@ -7,6 +7,9 @@ import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workfl
|
|||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
|
import { getDependsOnFields } from '@/blocks/utils'
|
||||||
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants for ComboBox component behavior
|
* Constants for ComboBox component behavior
|
||||||
@@ -48,6 +51,19 @@ interface ComboBoxProps {
|
|||||||
placeholder?: string
|
placeholder?: string
|
||||||
/** Configuration for the sub-block */
|
/** Configuration for the sub-block */
|
||||||
config: SubBlockConfig
|
config: SubBlockConfig
|
||||||
|
/** Async function to fetch options dynamically */
|
||||||
|
fetchOptions?: (
|
||||||
|
blockId: string,
|
||||||
|
subBlockId: string
|
||||||
|
) => Promise<Array<{ label: string; id: string }>>
|
||||||
|
/** Async function to fetch a single option's label by ID (for hydration) */
|
||||||
|
fetchOptionById?: (
|
||||||
|
blockId: string,
|
||||||
|
subBlockId: string,
|
||||||
|
optionId: string
|
||||||
|
) => Promise<{ label: string; id: string } | null>
|
||||||
|
/** Field dependencies that trigger option refetch when changed */
|
||||||
|
dependsOn?: SubBlockConfig['dependsOn']
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComboBox({
|
export function ComboBox({
|
||||||
@@ -61,23 +77,89 @@ export function ComboBox({
|
|||||||
disabled,
|
disabled,
|
||||||
placeholder = 'Type or select an option...',
|
placeholder = 'Type or select an option...',
|
||||||
config,
|
config,
|
||||||
|
fetchOptions,
|
||||||
|
fetchOptionById,
|
||||||
|
dependsOn,
|
||||||
}: ComboBoxProps) {
|
}: ComboBoxProps) {
|
||||||
// Hooks and context
|
// Hooks and context
|
||||||
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
|
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
|
||||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||||
const reactFlowInstance = useReactFlow()
|
const reactFlowInstance = useReactFlow()
|
||||||
|
|
||||||
|
// Dependency tracking for fetchOptions
|
||||||
|
const dependsOnFields = useMemo(() => getDependsOnFields(dependsOn), [dependsOn])
|
||||||
|
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
||||||
|
const dependencyValues = useSubBlockStore(
|
||||||
|
useCallback(
|
||||||
|
(state) => {
|
||||||
|
if (dependsOnFields.length === 0 || !activeWorkflowId) return []
|
||||||
|
const workflowValues = state.workflowValues[activeWorkflowId] || {}
|
||||||
|
const blockValues = workflowValues[blockId] || {}
|
||||||
|
return dependsOnFields.map((depKey) => blockValues[depKey] ?? null)
|
||||||
|
},
|
||||||
|
[dependsOnFields, activeWorkflowId, blockId]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
const [storeInitialized, setStoreInitialized] = useState(false)
|
const [storeInitialized, setStoreInitialized] = useState(false)
|
||||||
|
const [fetchedOptions, setFetchedOptions] = useState<Array<{ label: string; id: string }>>([])
|
||||||
|
const [isLoadingOptions, setIsLoadingOptions] = useState(false)
|
||||||
|
const [fetchError, setFetchError] = useState<string | null>(null)
|
||||||
|
const [hydratedOption, setHydratedOption] = useState<{ label: string; id: string } | null>(null)
|
||||||
|
const previousDependencyValuesRef = useRef<string>('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches options from the async fetchOptions function if provided
|
||||||
|
*/
|
||||||
|
const fetchOptionsIfNeeded = useCallback(async () => {
|
||||||
|
if (!fetchOptions || isPreview || disabled) return
|
||||||
|
|
||||||
|
setIsLoadingOptions(true)
|
||||||
|
setFetchError(null)
|
||||||
|
try {
|
||||||
|
const options = await fetchOptions(blockId, subBlockId)
|
||||||
|
setFetchedOptions(options)
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch options'
|
||||||
|
setFetchError(errorMessage)
|
||||||
|
setFetchedOptions([])
|
||||||
|
} finally {
|
||||||
|
setIsLoadingOptions(false)
|
||||||
|
}
|
||||||
|
}, [fetchOptions, blockId, subBlockId, isPreview, disabled])
|
||||||
|
|
||||||
// Determine the active value based on mode (preview vs. controlled vs. store)
|
// Determine the active value based on mode (preview vs. controlled vs. store)
|
||||||
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
|
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
|
||||||
|
|
||||||
// Evaluate options if provided as a function
|
// Evaluate static options if provided as a function
|
||||||
const evaluatedOptions = useMemo(() => {
|
const staticOptions = useMemo(() => {
|
||||||
return typeof options === 'function' ? options() : options
|
return typeof options === 'function' ? options() : options
|
||||||
}, [options])
|
}, [options])
|
||||||
|
|
||||||
|
// Normalize fetched options to match ComboBoxOption format
|
||||||
|
const normalizedFetchedOptions = useMemo((): ComboBoxOption[] => {
|
||||||
|
return fetchedOptions.map((opt) => ({ label: opt.label, id: opt.id }))
|
||||||
|
}, [fetchedOptions])
|
||||||
|
|
||||||
|
// Merge static and fetched options - fetched options take priority when available
|
||||||
|
const evaluatedOptions = useMemo((): ComboBoxOption[] => {
|
||||||
|
let opts: ComboBoxOption[] =
|
||||||
|
fetchOptions && normalizedFetchedOptions.length > 0 ? normalizedFetchedOptions : staticOptions
|
||||||
|
|
||||||
|
// Merge hydrated option if not already present
|
||||||
|
if (hydratedOption) {
|
||||||
|
const alreadyPresent = opts.some((o) =>
|
||||||
|
typeof o === 'string' ? o === hydratedOption.id : o.id === hydratedOption.id
|
||||||
|
)
|
||||||
|
if (!alreadyPresent) {
|
||||||
|
opts = [hydratedOption, ...opts]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}, [fetchOptions, normalizedFetchedOptions, staticOptions, hydratedOption])
|
||||||
|
|
||||||
// Convert options to Combobox format
|
// Convert options to Combobox format
|
||||||
const comboboxOptions = useMemo((): ComboboxOption[] => {
|
const comboboxOptions = useMemo((): ComboboxOption[] => {
|
||||||
return evaluatedOptions.map((option) => {
|
return evaluatedOptions.map((option) => {
|
||||||
@@ -160,6 +242,94 @@ export function ComboBox({
|
|||||||
}
|
}
|
||||||
}, [storeInitialized, value, defaultOptionValue, setStoreValue])
|
}, [storeInitialized, value, defaultOptionValue, setStoreValue])
|
||||||
|
|
||||||
|
// Clear fetched options and hydrated option when dependencies change
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetchOptions && dependsOnFields.length > 0) {
|
||||||
|
const currentDependencyValuesStr = JSON.stringify(dependencyValues)
|
||||||
|
const previousDependencyValuesStr = previousDependencyValuesRef.current
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousDependencyValuesStr &&
|
||||||
|
currentDependencyValuesStr !== previousDependencyValuesStr
|
||||||
|
) {
|
||||||
|
setFetchedOptions([])
|
||||||
|
setHydratedOption(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
previousDependencyValuesRef.current = currentDependencyValuesStr
|
||||||
|
}
|
||||||
|
}, [dependencyValues, fetchOptions, dependsOnFields.length])
|
||||||
|
|
||||||
|
// Fetch options when needed (on mount, when enabled, or when dependencies change)
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
fetchOptions &&
|
||||||
|
!isPreview &&
|
||||||
|
!disabled &&
|
||||||
|
fetchedOptions.length === 0 &&
|
||||||
|
!isLoadingOptions &&
|
||||||
|
!fetchError
|
||||||
|
) {
|
||||||
|
fetchOptionsIfNeeded()
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- fetchOptionsIfNeeded deps already covered above
|
||||||
|
}, [
|
||||||
|
fetchOptions,
|
||||||
|
isPreview,
|
||||||
|
disabled,
|
||||||
|
fetchedOptions.length,
|
||||||
|
isLoadingOptions,
|
||||||
|
fetchError,
|
||||||
|
dependencyValues,
|
||||||
|
])
|
||||||
|
|
||||||
|
// Hydrate the stored value's label by fetching it individually
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fetchOptionById || isPreview || disabled) return
|
||||||
|
|
||||||
|
const valueToHydrate = value as string | null | undefined
|
||||||
|
if (!valueToHydrate) return
|
||||||
|
|
||||||
|
// Skip if value is an expression (not a real ID)
|
||||||
|
if (valueToHydrate.startsWith('<') || valueToHydrate.includes('{{')) return
|
||||||
|
|
||||||
|
// Skip if already hydrated with the same value
|
||||||
|
if (hydratedOption?.id === valueToHydrate) return
|
||||||
|
|
||||||
|
// Skip if value is already in fetched options or static options
|
||||||
|
const alreadyInFetchedOptions = fetchedOptions.some((opt) => opt.id === valueToHydrate)
|
||||||
|
const alreadyInStaticOptions = staticOptions.some((opt) =>
|
||||||
|
typeof opt === 'string' ? opt === valueToHydrate : opt.id === valueToHydrate
|
||||||
|
)
|
||||||
|
if (alreadyInFetchedOptions || alreadyInStaticOptions) return
|
||||||
|
|
||||||
|
// Track if effect is still active (cleanup on unmount or value change)
|
||||||
|
let isActive = true
|
||||||
|
|
||||||
|
// Fetch the hydrated option
|
||||||
|
fetchOptionById(blockId, subBlockId, valueToHydrate)
|
||||||
|
.then((option) => {
|
||||||
|
if (isActive) setHydratedOption(option)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (isActive) setHydratedOption(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isActive = false
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
fetchOptionById,
|
||||||
|
value,
|
||||||
|
blockId,
|
||||||
|
subBlockId,
|
||||||
|
isPreview,
|
||||||
|
disabled,
|
||||||
|
fetchedOptions,
|
||||||
|
staticOptions,
|
||||||
|
hydratedOption?.id,
|
||||||
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles wheel event for ReactFlow zoom control
|
* Handles wheel event for ReactFlow zoom control
|
||||||
* Intercepts Ctrl/Cmd+Wheel to zoom the canvas
|
* Intercepts Ctrl/Cmd+Wheel to zoom the canvas
|
||||||
@@ -247,11 +417,13 @@ export function ComboBox({
|
|||||||
return option.id === newValue
|
return option.id === newValue
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!matchedOption) {
|
// If a matching option is found, store its ID; otherwise store the raw value
|
||||||
return
|
// (allows expressions like <block.output> to be entered directly)
|
||||||
}
|
const nextValue = matchedOption
|
||||||
|
? typeof matchedOption === 'string'
|
||||||
const nextValue = typeof matchedOption === 'string' ? matchedOption : matchedOption.id
|
? matchedOption
|
||||||
|
: matchedOption.id
|
||||||
|
: newValue
|
||||||
setStoreValue(nextValue)
|
setStoreValue(nextValue)
|
||||||
}}
|
}}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
@@ -293,6 +465,13 @@ export function ComboBox({
|
|||||||
onWheel: handleWheel,
|
onWheel: handleWheel,
|
||||||
autoComplete: 'off',
|
autoComplete: 'off',
|
||||||
}}
|
}}
|
||||||
|
isLoading={isLoadingOptions}
|
||||||
|
error={fetchError}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (open) {
|
||||||
|
void fetchOptionsIfNeeded()
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SubBlockInputController>
|
</SubBlockInputController>
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ interface DropdownProps {
|
|||||||
blockId: string,
|
blockId: string,
|
||||||
subBlockId: string
|
subBlockId: string
|
||||||
) => Promise<Array<{ label: string; id: string }>>
|
) => Promise<Array<{ label: string; id: string }>>
|
||||||
|
/** Async function to fetch a single option's label by ID (for hydration) */
|
||||||
|
fetchOptionById?: (
|
||||||
|
blockId: string,
|
||||||
|
subBlockId: string,
|
||||||
|
optionId: string
|
||||||
|
) => Promise<{ label: string; id: string } | null>
|
||||||
/** Field dependencies that trigger option refetch when changed */
|
/** Field dependencies that trigger option refetch when changed */
|
||||||
dependsOn?: SubBlockConfig['dependsOn']
|
dependsOn?: SubBlockConfig['dependsOn']
|
||||||
/** Enable search input in dropdown */
|
/** Enable search input in dropdown */
|
||||||
@@ -71,6 +77,7 @@ export function Dropdown({
|
|||||||
placeholder = 'Select an option...',
|
placeholder = 'Select an option...',
|
||||||
multiSelect = false,
|
multiSelect = false,
|
||||||
fetchOptions,
|
fetchOptions,
|
||||||
|
fetchOptionById,
|
||||||
dependsOn,
|
dependsOn,
|
||||||
searchable = false,
|
searchable = false,
|
||||||
}: DropdownProps) {
|
}: DropdownProps) {
|
||||||
@@ -98,6 +105,7 @@ export function Dropdown({
|
|||||||
const [fetchedOptions, setFetchedOptions] = useState<Array<{ label: string; id: string }>>([])
|
const [fetchedOptions, setFetchedOptions] = useState<Array<{ label: string; id: string }>>([])
|
||||||
const [isLoadingOptions, setIsLoadingOptions] = useState(false)
|
const [isLoadingOptions, setIsLoadingOptions] = useState(false)
|
||||||
const [fetchError, setFetchError] = useState<string | null>(null)
|
const [fetchError, setFetchError] = useState<string | null>(null)
|
||||||
|
const [hydratedOption, setHydratedOption] = useState<{ label: string; id: string } | null>(null)
|
||||||
|
|
||||||
const previousModeRef = useRef<string | null>(null)
|
const previousModeRef = useRef<string | null>(null)
|
||||||
const previousDependencyValuesRef = useRef<string>('')
|
const previousDependencyValuesRef = useRef<string>('')
|
||||||
@@ -150,11 +158,23 @@ export function Dropdown({
|
|||||||
}, [fetchedOptions])
|
}, [fetchedOptions])
|
||||||
|
|
||||||
const availableOptions = useMemo(() => {
|
const availableOptions = useMemo(() => {
|
||||||
if (fetchOptions && normalizedFetchedOptions.length > 0) {
|
let opts: DropdownOption[] =
|
||||||
return normalizedFetchedOptions
|
fetchOptions && normalizedFetchedOptions.length > 0
|
||||||
|
? normalizedFetchedOptions
|
||||||
|
: evaluatedOptions
|
||||||
|
|
||||||
|
// Merge hydrated option if not already present
|
||||||
|
if (hydratedOption) {
|
||||||
|
const alreadyPresent = opts.some((o) =>
|
||||||
|
typeof o === 'string' ? o === hydratedOption.id : o.id === hydratedOption.id
|
||||||
|
)
|
||||||
|
if (!alreadyPresent) {
|
||||||
|
opts = [hydratedOption, ...opts]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return evaluatedOptions
|
|
||||||
}, [fetchOptions, normalizedFetchedOptions, evaluatedOptions])
|
return opts
|
||||||
|
}, [fetchOptions, normalizedFetchedOptions, evaluatedOptions, hydratedOption])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert dropdown options to Combobox format
|
* Convert dropdown options to Combobox format
|
||||||
@@ -310,7 +330,7 @@ export function Dropdown({
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect to clear fetched options when dependencies actually change
|
* Effect to clear fetched options and hydrated option when dependencies actually change
|
||||||
* This ensures options are refetched with new dependency values (e.g., new credentials)
|
* This ensures options are refetched with new dependency values (e.g., new credentials)
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -323,6 +343,7 @@ export function Dropdown({
|
|||||||
currentDependencyValuesStr !== previousDependencyValuesStr
|
currentDependencyValuesStr !== previousDependencyValuesStr
|
||||||
) {
|
) {
|
||||||
setFetchedOptions([])
|
setFetchedOptions([])
|
||||||
|
setHydratedOption(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
previousDependencyValuesRef.current = currentDependencyValuesStr
|
previousDependencyValuesRef.current = currentDependencyValuesStr
|
||||||
@@ -338,18 +359,72 @@ export function Dropdown({
|
|||||||
!isPreview &&
|
!isPreview &&
|
||||||
!disabled &&
|
!disabled &&
|
||||||
fetchedOptions.length === 0 &&
|
fetchedOptions.length === 0 &&
|
||||||
!isLoadingOptions
|
!isLoadingOptions &&
|
||||||
|
!fetchError
|
||||||
) {
|
) {
|
||||||
fetchOptionsIfNeeded()
|
fetchOptionsIfNeeded()
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- fetchOptionsIfNeeded deps already covered above
|
||||||
}, [
|
}, [
|
||||||
fetchOptions,
|
fetchOptions,
|
||||||
isPreview,
|
isPreview,
|
||||||
disabled,
|
disabled,
|
||||||
fetchedOptions.length,
|
fetchedOptions.length,
|
||||||
isLoadingOptions,
|
isLoadingOptions,
|
||||||
fetchOptionsIfNeeded,
|
fetchError,
|
||||||
dependencyValues, // Refetch when dependencies change
|
dependencyValues,
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effect to hydrate the stored value's label by fetching it individually
|
||||||
|
* This ensures the correct label is shown before the full options list loads
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fetchOptionById || isPreview || disabled) return
|
||||||
|
|
||||||
|
// Get the value to hydrate (single value only, not multi-select)
|
||||||
|
const valueToHydrate = multiSelect ? null : (singleValue as string | null | undefined)
|
||||||
|
if (!valueToHydrate) return
|
||||||
|
|
||||||
|
// Skip if value is an expression (not a real ID)
|
||||||
|
if (valueToHydrate.startsWith('<') || valueToHydrate.includes('{{')) return
|
||||||
|
|
||||||
|
// Skip if already hydrated with the same value
|
||||||
|
if (hydratedOption?.id === valueToHydrate) return
|
||||||
|
|
||||||
|
// Skip if value is already in fetched options or static options
|
||||||
|
const alreadyInFetchedOptions = fetchedOptions.some((opt) => opt.id === valueToHydrate)
|
||||||
|
const alreadyInStaticOptions = evaluatedOptions.some((opt) =>
|
||||||
|
typeof opt === 'string' ? opt === valueToHydrate : opt.id === valueToHydrate
|
||||||
|
)
|
||||||
|
if (alreadyInFetchedOptions || alreadyInStaticOptions) return
|
||||||
|
|
||||||
|
// Track if effect is still active (cleanup on unmount or value change)
|
||||||
|
let isActive = true
|
||||||
|
|
||||||
|
// Fetch the hydrated option
|
||||||
|
fetchOptionById(blockId, subBlockId, valueToHydrate)
|
||||||
|
.then((option) => {
|
||||||
|
if (isActive) setHydratedOption(option)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (isActive) setHydratedOption(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isActive = false
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
fetchOptionById,
|
||||||
|
singleValue,
|
||||||
|
multiSelect,
|
||||||
|
blockId,
|
||||||
|
subBlockId,
|
||||||
|
isPreview,
|
||||||
|
disabled,
|
||||||
|
fetchedOptions,
|
||||||
|
evaluatedOptions,
|
||||||
|
hydratedOption?.id,
|
||||||
])
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -460,6 +460,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
multiSelect={config.multiSelect}
|
multiSelect={config.multiSelect}
|
||||||
fetchOptions={config.fetchOptions}
|
fetchOptions={config.fetchOptions}
|
||||||
|
fetchOptionById={config.fetchOptionById}
|
||||||
dependsOn={config.dependsOn}
|
dependsOn={config.dependsOn}
|
||||||
searchable={config.searchable}
|
searchable={config.searchable}
|
||||||
/>
|
/>
|
||||||
@@ -479,6 +480,9 @@ function SubBlockComponent({
|
|||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
config={config}
|
config={config}
|
||||||
|
fetchOptions={config.fetchOptions}
|
||||||
|
fetchOptionById={config.fetchOptionById}
|
||||||
|
dependsOn={config.dependsOn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -289,11 +289,19 @@ export interface SubBlockConfig {
|
|||||||
useWebhookUrl?: boolean
|
useWebhookUrl?: boolean
|
||||||
// Trigger-save specific: The trigger ID for validation and saving
|
// Trigger-save specific: The trigger ID for validation and saving
|
||||||
triggerId?: string
|
triggerId?: string
|
||||||
// Dropdown specific: Function to fetch options dynamically (for multi-select or single-select)
|
// Dropdown/Combobox: Function to fetch options dynamically
|
||||||
|
// Works with both 'dropdown' (select-only) and 'combobox' (editable with expression support)
|
||||||
fetchOptions?: (
|
fetchOptions?: (
|
||||||
blockId: string,
|
blockId: string,
|
||||||
subBlockId: string
|
subBlockId: string
|
||||||
) => Promise<Array<{ label: string; id: string }>>
|
) => Promise<Array<{ label: string; id: string }>>
|
||||||
|
// Dropdown/Combobox: Function to fetch a single option's label by ID (for hydration)
|
||||||
|
// Called when component mounts with a stored value to display the correct label before options load
|
||||||
|
fetchOptionById?: (
|
||||||
|
blockId: string,
|
||||||
|
subBlockId: string,
|
||||||
|
optionId: string
|
||||||
|
) => Promise<{ label: string; id: string } | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockConfig<T extends ToolResponse = ToolResponse> {
|
export interface BlockConfig<T extends ToolResponse = ToolResponse> {
|
||||||
|
|||||||
Reference in New Issue
Block a user