mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(agent): tool credential dropdown (#1884)
* Checkpoint * Fix dropdown
This commit is contained in:
committed by
GitHub
parent
82731850b2
commit
37fd21e0aa
@@ -51,6 +51,7 @@ export function ChannelSelectorInput({
|
||||
const { finalDisabled, dependsOn, dependencyValues } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
// Choose credential strictly based on auth method - use effective values
|
||||
|
||||
@@ -42,7 +42,11 @@ export function FileSelectorInput({
|
||||
const params = useParams()
|
||||
const workflowIdFromUrl = (params?.workflowId as string) || activeWorkflowId || ''
|
||||
// Central dependsOn gating for this selector instance
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
||||
const { finalDisabled, dependsOn } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
// Helper to coerce various preview value shapes into a string ID
|
||||
const coerceToIdString = (val: unknown): string => {
|
||||
@@ -63,12 +67,20 @@ export function FileSelectorInput({
|
||||
|
||||
// Use the proper hook to get the current value and setter
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
||||
const [domainValue] = useSubBlockValue(blockId, 'domain')
|
||||
const [projectIdValue] = useSubBlockValue(blockId, 'projectId')
|
||||
const [planIdValue] = useSubBlockValue(blockId, 'planId')
|
||||
const [teamIdValue] = useSubBlockValue(blockId, 'teamId')
|
||||
const [operationValue] = useSubBlockValue(blockId, 'operation')
|
||||
const [connectedCredentialFromStore] = useSubBlockValue(blockId, 'credential')
|
||||
const [domainValueFromStore] = useSubBlockValue(blockId, 'domain')
|
||||
const [projectIdValueFromStore] = useSubBlockValue(blockId, 'projectId')
|
||||
const [planIdValueFromStore] = useSubBlockValue(blockId, 'planId')
|
||||
const [teamIdValueFromStore] = useSubBlockValue(blockId, 'teamId')
|
||||
const [operationValueFromStore] = useSubBlockValue(blockId, 'operation')
|
||||
|
||||
// Use previewContextValues if provided (for tools inside agent blocks), otherwise use store values
|
||||
const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore
|
||||
const domainValue = previewContextValues?.domain ?? domainValueFromStore
|
||||
const projectIdValue = previewContextValues?.projectId ?? projectIdValueFromStore
|
||||
const planIdValue = previewContextValues?.planId ?? planIdValueFromStore
|
||||
const teamIdValue = previewContextValues?.teamId ?? teamIdValueFromStore
|
||||
const operationValue = previewContextValues?.operation ?? operationValueFromStore
|
||||
|
||||
// Determine if the persisted credential belongs to the current viewer
|
||||
// Use service providerId where available (e.g., onedrive/sharepoint) instead of base provider ("microsoft")
|
||||
@@ -76,7 +88,7 @@ export function FileSelectorInput({
|
||||
? getProviderIdFromServiceId(subBlock.serviceId)
|
||||
: (subBlock.provider as string) || ''
|
||||
const { isForeignCredential } = useForeignCredential(
|
||||
foreignCheckProvider,
|
||||
subBlock.provider || subBlock.serviceId || 'outlook',
|
||||
(connectedCredential as string) || ''
|
||||
)
|
||||
|
||||
@@ -109,6 +121,20 @@ export function FileSelectorInput({
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
|
||||
const credentialDependencySatisfied = (() => {
|
||||
if (!dependsOn.includes('credential')) return true
|
||||
const normalizedCredential = coerceToIdString(connectedCredential)
|
||||
if (!normalizedCredential || normalizedCredential.trim().length === 0) {
|
||||
return false
|
||||
}
|
||||
if (isForeignCredential) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})()
|
||||
|
||||
const shouldForceDisable = !credentialDependencySatisfied
|
||||
|
||||
// For Google Drive
|
||||
const clientId = getEnv('NEXT_PUBLIC_GOOGLE_CLIENT_ID') || ''
|
||||
const apiKey = getEnv('NEXT_PUBLIC_GOOGLE_API_KEY') || ''
|
||||
@@ -132,7 +158,7 @@ export function FileSelectorInput({
|
||||
collaborativeSetSubblockValue(blockId, subBlock.id, val)
|
||||
}}
|
||||
label={subBlock.placeholder || 'Select Google Calendar'}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
showPreview={true}
|
||||
credentialId={credential}
|
||||
workflowId={workflowIdFromUrl}
|
||||
@@ -166,7 +192,7 @@ export function FileSelectorInput({
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
serviceId={subBlock.serviceId}
|
||||
label={subBlock.placeholder || 'Select Confluence page'}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
showPreview={true}
|
||||
credentialId={credential}
|
||||
workflowId={workflowIdFromUrl}
|
||||
@@ -200,7 +226,7 @@ export function FileSelectorInput({
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
serviceId={subBlock.serviceId}
|
||||
label={subBlock.placeholder || 'Select Jira issue'}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
showPreview={true}
|
||||
credentialId={credential}
|
||||
projectId={(projectIdValue as string) || ''}
|
||||
@@ -230,7 +256,7 @@ export function FileSelectorInput({
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
serviceId={subBlock.serviceId}
|
||||
label={subBlock.placeholder || 'Select Microsoft Excel file'}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
showPreview={true}
|
||||
workflowId={activeWorkflowId || ''}
|
||||
credentialId={credential}
|
||||
@@ -260,7 +286,7 @@ export function FileSelectorInput({
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
serviceId={subBlock.serviceId}
|
||||
label={subBlock.placeholder || 'Select Microsoft Word document'}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
showPreview={true}
|
||||
/>
|
||||
</div>
|
||||
@@ -288,7 +314,7 @@ export function FileSelectorInput({
|
||||
serviceId={subBlock.serviceId}
|
||||
mimeType={subBlock.mimeType}
|
||||
label={subBlock.placeholder || 'Select OneDrive folder'}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
showPreview={true}
|
||||
workflowId={activeWorkflowId || ''}
|
||||
credentialId={credential}
|
||||
@@ -318,7 +344,7 @@ export function FileSelectorInput({
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
serviceId={subBlock.serviceId}
|
||||
label={subBlock.placeholder || 'Select SharePoint site'}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
showPreview={true}
|
||||
workflowId={activeWorkflowId || ''}
|
||||
credentialId={credential}
|
||||
@@ -354,7 +380,7 @@ export function FileSelectorInput({
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
serviceId='microsoft-planner'
|
||||
label={subBlock.placeholder || 'Select task'}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
showPreview={true}
|
||||
planId={planId}
|
||||
workflowId={activeWorkflowId || ''}
|
||||
@@ -412,7 +438,7 @@ export function FileSelectorInput({
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
serviceId={subBlock.serviceId}
|
||||
label={subBlock.placeholder || 'Select Teams message location'}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
showPreview={true}
|
||||
credential={credential}
|
||||
selectionType={selectionType}
|
||||
@@ -455,7 +481,7 @@ export function FileSelectorInput({
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
serviceId={subBlock.serviceId}
|
||||
label={subBlock.placeholder || `Select ${itemType}`}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
showPreview={true}
|
||||
credentialId={credential}
|
||||
itemType={itemType}
|
||||
@@ -496,7 +522,7 @@ export function FileSelectorInput({
|
||||
provider={provider}
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
label={subBlock.placeholder || 'Select file'}
|
||||
disabled={finalDisabled}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
serviceId={subBlock.serviceId}
|
||||
mimeTypeFilter={subBlock.mimeType}
|
||||
showPreview={true}
|
||||
|
||||
@@ -28,6 +28,7 @@ interface ProjectSelectorInputProps {
|
||||
onProjectSelect?: (projectId: string) => void
|
||||
isPreview?: boolean
|
||||
previewValue?: any | null
|
||||
previewContextValues?: Record<string, any>
|
||||
}
|
||||
|
||||
export function ProjectSelectorInput({
|
||||
@@ -37,30 +38,40 @@ export function ProjectSelectorInput({
|
||||
onProjectSelect,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: ProjectSelectorInputProps) {
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string>('')
|
||||
const [_projectInfo, setProjectInfo] = useState<any | null>(null)
|
||||
// Use the proper hook to get the current value and setter
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
||||
const [connectedCredentialFromStore] = useSubBlockValue(blockId, 'credential')
|
||||
const [linearTeamIdFromStore] = useSubBlockValue(blockId, 'teamId')
|
||||
const [jiraDomainFromStore] = useSubBlockValue(blockId, 'domain')
|
||||
|
||||
// Use previewContextValues if provided (for tools inside agent blocks), otherwise use store values
|
||||
const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore
|
||||
const linearCredential = previewContextValues?.credential ?? connectedCredentialFromStore
|
||||
const linearTeamId = previewContextValues?.teamId ?? linearTeamIdFromStore
|
||||
const jiraDomain = previewContextValues?.domain ?? jiraDomainFromStore
|
||||
|
||||
const { isForeignCredential } = useForeignCredential(
|
||||
subBlock.provider || subBlock.serviceId || 'jira',
|
||||
(connectedCredential as string) || ''
|
||||
)
|
||||
// Reactive dependencies from store for Linear
|
||||
const [linearCredential] = useSubBlockValue(blockId, 'credential')
|
||||
const [linearTeamId] = useSubBlockValue(blockId, 'teamId')
|
||||
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) as string | null
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
// Get provider-specific values
|
||||
const provider = subBlock.provider || 'jira'
|
||||
const isLinear = provider === 'linear'
|
||||
|
||||
// Jira/Discord upstream fields
|
||||
const [jiraDomain] = useSubBlockValue(blockId, 'domain')
|
||||
const [jiraCredential] = useSubBlockValue(blockId, 'credential')
|
||||
// Jira/Discord upstream fields - use values from previewContextValues or store
|
||||
const jiraCredential = connectedCredential
|
||||
const domain = (jiraDomain as string) || ''
|
||||
|
||||
// Verify Jira credential belongs to current user; if not, treat as absent
|
||||
|
||||
@@ -168,6 +168,7 @@ function FileSelectorSyncWrapper({
|
||||
mimeType: uiComponent.mimeType,
|
||||
requiredScopes: uiComponent.requiredScopes || [],
|
||||
placeholder: uiComponent.placeholder,
|
||||
dependsOn: uiComponent.dependsOn,
|
||||
}}
|
||||
disabled={disabled}
|
||||
previewContextValues={previewContextValues}
|
||||
@@ -433,6 +434,7 @@ function ChannelSelectorSyncWrapper({
|
||||
title: paramId,
|
||||
provider: uiComponent.provider || 'slack',
|
||||
placeholder: uiComponent.placeholder,
|
||||
dependsOn: uiComponent.dependsOn,
|
||||
}}
|
||||
onChannelSelect={onChange}
|
||||
disabled={disabled}
|
||||
@@ -1174,9 +1176,11 @@ export function ToolInput({
|
||||
serviceId: uiComponent.serviceId,
|
||||
placeholder: uiComponent.placeholder,
|
||||
requiredScopes: uiComponent.requiredScopes,
|
||||
dependsOn: uiComponent.dependsOn,
|
||||
}}
|
||||
onProjectSelect={onChange}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -13,29 +13,62 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
export function useDependsOnGate(
|
||||
blockId: string,
|
||||
subBlock: SubBlockConfig,
|
||||
opts?: { disabled?: boolean; isPreview?: boolean }
|
||||
opts?: { disabled?: boolean; isPreview?: boolean; previewContextValues?: Record<string, any> }
|
||||
) {
|
||||
const disabledProp = opts?.disabled ?? false
|
||||
const isPreview = opts?.isPreview ?? false
|
||||
const previewContextValues = opts?.previewContextValues
|
||||
|
||||
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
||||
|
||||
// Use only explicit dependsOn from block config. No inference.
|
||||
const dependsOn: string[] = (subBlock.dependsOn as string[] | undefined) || []
|
||||
|
||||
const normalizeDependencyValue = (rawValue: unknown): unknown => {
|
||||
if (rawValue === null || rawValue === undefined) return null
|
||||
|
||||
if (typeof rawValue === 'object') {
|
||||
if (Array.isArray(rawValue)) {
|
||||
if (rawValue.length === 0) return null
|
||||
return rawValue.map((item) => normalizeDependencyValue(item))
|
||||
}
|
||||
|
||||
const record = rawValue as Record<string, any>
|
||||
if ('value' in record) {
|
||||
return normalizeDependencyValue(record.value)
|
||||
}
|
||||
if ('id' in record) {
|
||||
return record.id
|
||||
}
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
return rawValue
|
||||
}
|
||||
|
||||
const dependencyValues = useSubBlockStore((state) => {
|
||||
if (dependsOn.length === 0) return [] as any[]
|
||||
|
||||
// If previewContextValues are provided (e.g., tool parameters), use those first
|
||||
if (previewContextValues) {
|
||||
return dependsOn.map((depKey) => normalizeDependencyValue(previewContextValues[depKey]))
|
||||
}
|
||||
|
||||
if (!activeWorkflowId) return dependsOn.map(() => null)
|
||||
const workflowValues = state.workflowValues[activeWorkflowId] || {}
|
||||
const blockValues = (workflowValues as any)[blockId] || {}
|
||||
return dependsOn.map((depKey) => (blockValues as any)[depKey] ?? null)
|
||||
return dependsOn.map((depKey) => normalizeDependencyValue((blockValues as any)[depKey]))
|
||||
}) as any[]
|
||||
|
||||
const depsSatisfied = useMemo(() => {
|
||||
if (dependsOn.length === 0) return true
|
||||
return dependencyValues.every((v) =>
|
||||
typeof v === 'string' ? v.trim().length > 0 : v !== null && v !== undefined && v !== ''
|
||||
)
|
||||
return dependencyValues.every((value) => {
|
||||
if (value === null || value === undefined) return false
|
||||
if (typeof value === 'string') return value.trim().length > 0
|
||||
if (Array.isArray(value)) return value.length > 0
|
||||
return value !== ''
|
||||
})
|
||||
}, [dependencyValues, dependsOn])
|
||||
|
||||
// Block everything except the credential field itself until dependencies are set
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface UIComponentConfig {
|
||||
acceptedTypes?: string[]
|
||||
multiple?: boolean
|
||||
maxSize?: number
|
||||
dependsOn?: string[]
|
||||
}
|
||||
|
||||
export interface SubBlockConfig {
|
||||
@@ -61,6 +62,7 @@ export interface SubBlockConfig {
|
||||
acceptedTypes?: string[]
|
||||
multiple?: boolean
|
||||
maxSize?: number
|
||||
dependsOn?: string[]
|
||||
}
|
||||
|
||||
export interface BlockConfig {
|
||||
@@ -236,6 +238,7 @@ export function getToolParametersConfig(
|
||||
acceptedTypes: subBlock.acceptedTypes,
|
||||
multiple: subBlock.multiple,
|
||||
maxSize: subBlock.maxSize,
|
||||
dependsOn: subBlock.dependsOn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user