diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index 7575d08aa0..29d8ee05a9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -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(() => { @@ -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) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/google-drive-picker.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/google-drive-picker.tsx index e8ce75e172..9d0c6384fd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/google-drive-picker.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/google-drive-picker.tsx @@ -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 { @@ -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 }) + } } } }, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx index efc7efc0f6..cbd6b053a6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx @@ -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, @@ -157,7 +158,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`. diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index e3fc10454e..741c82f842 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -12,7 +12,7 @@ import { BLOCK_DIMENSIONS, useBlockDimensions, } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' -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' @@ -237,7 +237,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 (
diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 75d4f4b690..b785a8f068 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -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 extends ToolResponse ? T['output'] : never export type ToolOutputToValueType = T extends Record diff --git a/apps/sim/hooks/use-display-name.ts b/apps/sim/hooks/use-display-name.ts index b0d9ed9dad..fbe1eb0774 100644 --- a/apps/sim/hooks/use-display-name.ts +++ b/apps/sim/hooks/use-display-name.ts @@ -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) } diff --git a/apps/sim/stores/display-names/store.ts b/apps/sim/stores/display-names/store.ts index a792e012d4..9b889c2bbb 100644 --- a/apps/sim/stores/display-names/store.ts +++ b/apps/sim/stores/display-names/store.ts @@ -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 */ @@ -103,6 +108,22 @@ export const useDisplayNamesStore = create((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] } diff --git a/apps/sim/triggers/gmail/poller.ts b/apps/sim/triggers/gmail/poller.ts index 0ed5788e29..deb87baf8b 100644 --- a/apps/sim/triggers/gmail/poller.ts +++ b/apps/sim/triggers/gmail/poller.ts @@ -38,7 +38,8 @@ export const gmailPollingTrigger: TriggerConfig = { | 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}`) @@ -55,7 +56,7 @@ export const gmailPollingTrigger: TriggerConfig = { return [] } catch (error) { logger.error('Error fetching Gmail labels:', error) - return [] + throw error } }, dependsOn: ['triggerCredentials'], diff --git a/apps/sim/triggers/outlook/poller.ts b/apps/sim/triggers/outlook/poller.ts index 0ffc7f65bd..43d096215c 100644 --- a/apps/sim/triggers/outlook/poller.ts +++ b/apps/sim/triggers/outlook/poller.ts @@ -38,7 +38,7 @@ export const outlookPollingTrigger: TriggerConfig = { | string | null if (!credentialId) { - return [] + throw new Error('No Outlook credential selected') } try { const response = await fetch(`/api/tools/outlook/folders?credentialId=${credentialId}`) @@ -55,7 +55,7 @@ export const outlookPollingTrigger: TriggerConfig = { return [] } catch (error) { logger.error('Error fetching Outlook folders:', error) - return [] + throw error } }, dependsOn: ['triggerCredentials'],