Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cursor/worktrees.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"setup-worktree": ["npm install"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,36 @@ export function CredentialSelector({
.setDisplayNames('credentials', effectiveProviderId, credentialMap)
}

// Do not auto-select or reset. We only show what's persisted.
// Check if the currently selected credential still exists
const selectedCredentialStillExists = (creds || []).some(
(cred: Credential) => cred.id === selectedId
)
const shouldClearPersistedSelection =
!isPreview && selectedId && !selectedCredentialStillExists && !foreignMetaFound

if (shouldClearPersistedSelection) {
logger.info('Clearing invalid credential selection - credential was disconnected', {
selectedId,
provider: effectiveProviderId,
})

// Clear via setStoreValue to trigger cascade
setStoreValue('')
setSelectedId('')

if (effectiveProviderId) {
useDisplayNamesStore
.getState()
.removeDisplayName('credentials', effectiveProviderId, selectedId)
}
}
}
} catch (error) {
logger.error('Error fetching credentials:', { error })
} finally {
setIsLoading(false)
}
}, [effectiveProviderId, selectedId, activeWorkflowId])
}, [effectiveProviderId, selectedId, activeWorkflowId, isPreview, setStoreValue])

// Fetch credentials on initial mount and whenever the subblock value changes externally
useEffect(() => {
Expand Down Expand Up @@ -204,6 +226,24 @@ export function CredentialSelector({
}
}, [fetchCredentials])

// Listen for credential disconnection events from settings modal
useEffect(() => {
const handleCredentialDisconnected = (event: Event) => {
const customEvent = event as CustomEvent
const { providerId } = customEvent.detail
// Re-fetch if this disconnection affects our provider
if (providerId && (providerId === effectiveProviderId || providerId.startsWith(provider))) {
fetchCredentials()
}
}

window.addEventListener('credential-disconnected', handleCredentialDisconnected)

return () => {
window.removeEventListener('credential-disconnected', handleCredentialDisconnected)
}
}, [fetchCredentials, effectiveProviderId, provider])

// Handle popover open to fetch fresh credentials
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ export function GoogleDrivePicker({
if (data.file) {
setSelectedFile(data.file)
onFileInfoChange?.(data.file)

// Cache the file name
if (selectedCredentialId && data.file.id && data.file.name) {
useDisplayNamesStore.getState().setDisplayNames('files', selectedCredentialId, {
[data.file.id]: data.file.name,
})
}

return data.file
}
} else {
Expand Down Expand Up @@ -335,6 +343,13 @@ export function GoogleDrivePicker({
setSelectedFile(fileInfo)
onChange(file.id, fileInfo)
onFileInfoChange?.(fileInfo)

// Cache the selected file name
if (selectedCredentialId) {
useDisplayNamesStore
.getState()
.setDisplayNames('files', selectedCredentialId, { [file.id]: file.name })
}
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AlertTriangle } from 'lucide-react'
import { Label, Tooltip } from '@/components/emcn/components'
import { cn } from '@/lib/utils'
import type { FieldDiffStatus } from '@/lib/workflows/diff/types'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-depends-on-gate'
import type { SubBlockConfig } from '@/blocks/types'
import {
ChannelSelectorInput,
Expand Down Expand Up @@ -158,7 +159,15 @@ function SubBlockComponent({
| string[]
| null
| undefined
const isDisabled = disabled || isPreview

// Use dependsOn gating to compute final disabled state
const { finalDisabled: gatedDisabled } = useDependsOnGate(blockId, config, {
disabled,
isPreview,
previewContextValues: subBlockValues,
})

const isDisabled = gatedDisabled

/**
* Selects and renders the appropriate input component for the current sub-block `config.type`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getEnv, isTruthy } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import type { SubBlockConfig } from '@/blocks/types'
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useCredentialDisplay } from '@/hooks/use-credential-display'
import { useDisplayName } from '@/hooks/use-display-name'
Expand Down Expand Up @@ -241,7 +241,10 @@ const SubBlockRow = ({

const isPasswordField = subBlock?.password === true
const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null
const displayValue = maskedValue || credentialName || dropdownLabel || genericDisplayName || value

const isSelectorType = subBlock?.type && SELECTOR_TYPES_HYDRATION_REQUIRED.includes(subBlock.type)
const hydratedName = credentialName || dropdownLabel || genericDisplayName
const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value)

return (
<div className='flex items-center gap-[8px]'>
Expand Down
14 changes: 14 additions & 0 deletions apps/sim/blocks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ export type SubBlockType =
| 'variables-input' // Variable assignments for updating workflow variables
| 'text' // Read-only text display

/**
* Selector types that require display name hydration
* These show IDs/keys that need to be resolved to human-readable names
*/
export const SELECTOR_TYPES_HYDRATION_REQUIRED: SubBlockType[] = [
'oauth-input',
'channel-selector',
'file-selector',
'folder-selector',
'project-selector',
'knowledge-base-selector',
'document-selector',
] as const

export type ExtractToolOutput<T> = T extends ToolResponse ? T['output'] : never

export type ToolOutputToValueType<T> = T extends Record<string, any>
Expand Down
22 changes: 22 additions & 0 deletions apps/sim/hooks/use-display-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,28 @@ export function useDisplayName(
})
.catch(() => {})
.finally(() => setIsFetching(false))
}
// Google Drive files/folders (fetch by ID since no list endpoint via Picker API)
else if (
(provider === 'google-drive' || subBlock.serviceId === 'google-drive') &&
typeof value === 'string' &&
value
) {
const queryParams = new URLSearchParams({
credentialId: context.credentialId,
fileId: value,
})
fetch(`/api/tools/drive/file?${queryParams.toString()}`)
.then((res) => res.json())
.then((data) => {
if (data.file?.id && data.file.name) {
useDisplayNamesStore
.getState()
.setDisplayNames('files', context.credentialId!, { [data.file.id]: data.file.name })
}
})
.catch(() => {})
.finally(() => setIsFetching(false))
} else {
setIsFetching(false)
}
Expand Down
21 changes: 21 additions & 0 deletions apps/sim/stores/display-names/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ interface DisplayNamesStore {
*/
getDisplayName: (type: keyof DisplayNamesCache, context: string, id: string) => string | null

/**
* Remove a single display name
*/
removeDisplayName: (type: keyof DisplayNamesCache, context: string, id: string) => void

/**
* Clear all cached display names for a type/context
*/
Expand Down Expand Up @@ -103,6 +108,22 @@ export const useDisplayNamesStore = create<DisplayNamesStore>((set, get) => ({
return contextCache?.[id] || null
},

removeDisplayName: (type, context, id) => {
set((state) => {
const contextCache = { ...state.cache[type][context] }
delete contextCache[id]
return {
cache: {
...state.cache,
[type]: {
...state.cache[type],
[context]: contextCache,
},
},
}
})
},

clearContext: (type, context) => {
set((state) => {
const newTypeCache = { ...state.cache[type] }
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/triggers/gmail/poller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ export const gmailPollingTrigger: TriggerConfig = {
placeholder: 'Select Gmail labels to monitor for new emails',
description: 'Choose which Gmail labels to monitor. Leave empty to monitor all emails.',
required: false,
dependsOn: ['triggerCredentials'],
options: [], // Will be populated dynamically from user's Gmail labels
fetchOptions: async (blockId: string, subBlockId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
if (!credentialId) {
return []
// Return a sentinel to prevent infinite retry loops when credential is missing
throw new Error('No Gmail credential selected')
}
try {
const response = await fetch(`/api/tools/gmail/labels?credentialId=${credentialId}`)
Expand All @@ -55,7 +57,7 @@ export const gmailPollingTrigger: TriggerConfig = {
return []
} catch (error) {
logger.error('Error fetching Gmail labels:', error)
return []
throw error
}
},
mode: 'trigger',
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/triggers/outlook/poller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ export const outlookPollingTrigger: TriggerConfig = {
placeholder: 'Select Outlook folders to monitor for new emails',
description: 'Choose which Outlook folders to monitor. Leave empty to monitor all emails.',
required: false,
dependsOn: ['triggerCredentials'],
options: [], // Will be populated dynamically
fetchOptions: async (blockId: string, subBlockId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
if (!credentialId) {
return []
throw new Error('No Outlook credential selected')
}
try {
const response = await fetch(`/api/tools/outlook/folders?credentialId=${credentialId}`)
Expand All @@ -55,7 +56,7 @@ export const outlookPollingTrigger: TriggerConfig = {
return []
} catch (error) {
logger.error('Error fetching Outlook folders:', error)
return []
throw error
}
},
mode: 'trigger',
Expand Down
Loading