Skip to content

Commit 054865a

Browse files
icecrasher321waleedlatif1
authored andcommitted
fix(google-drive-picker): hydration issues with drive picker + dependsOn for trigger subblocks (#1901)
* fix(google-drive-picker): hydration issues with drive picker * respect depends on gating * add depends on for outlook polling * remove useless file * fix lint
1 parent 6b4270a commit 054865a

File tree

9 files changed

+134
-9
lines changed

9 files changed

+134
-9
lines changed

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,36 @@ export function CredentialSelector({
128128
.setDisplayNames('credentials', effectiveProviderId, credentialMap)
129129
}
130130

131-
// Do not auto-select or reset. We only show what's persisted.
131+
// Check if the currently selected credential still exists
132+
const selectedCredentialStillExists = (creds || []).some(
133+
(cred: Credential) => cred.id === selectedId
134+
)
135+
const shouldClearPersistedSelection =
136+
!isPreview && selectedId && !selectedCredentialStillExists && !foreignMetaFound
137+
138+
if (shouldClearPersistedSelection) {
139+
logger.info('Clearing invalid credential selection - credential was disconnected', {
140+
selectedId,
141+
provider: effectiveProviderId,
142+
})
143+
144+
// Clear via setStoreValue to trigger cascade
145+
setStoreValue('')
146+
setSelectedId('')
147+
148+
if (effectiveProviderId) {
149+
useDisplayNamesStore
150+
.getState()
151+
.removeDisplayName('credentials', effectiveProviderId, selectedId)
152+
}
153+
}
132154
}
133155
} catch (error) {
134156
logger.error('Error fetching credentials:', { error })
135157
} finally {
136158
setIsLoading(false)
137159
}
138-
}, [effectiveProviderId, selectedId, activeWorkflowId])
160+
}, [effectiveProviderId, selectedId, activeWorkflowId, isPreview, setStoreValue])
139161

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

229+
// Listen for credential disconnection events from settings modal
230+
useEffect(() => {
231+
const handleCredentialDisconnected = (event: Event) => {
232+
const customEvent = event as CustomEvent
233+
const { providerId } = customEvent.detail
234+
// Re-fetch if this disconnection affects our provider
235+
if (providerId && (providerId === effectiveProviderId || providerId.startsWith(provider))) {
236+
fetchCredentials()
237+
}
238+
}
239+
240+
window.addEventListener('credential-disconnected', handleCredentialDisconnected)
241+
242+
return () => {
243+
window.removeEventListener('credential-disconnected', handleCredentialDisconnected)
244+
}
245+
}, [fetchCredentials, effectiveProviderId, provider])
246+
207247
// Handle popover open to fetch fresh credentials
208248
const handleOpenChange = (isOpen: boolean) => {
209249
setOpen(isOpen)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/google-drive-picker.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ export function GoogleDrivePicker({
150150
if (data.file) {
151151
setSelectedFile(data.file)
152152
onFileInfoChange?.(data.file)
153+
154+
// Cache the file name
155+
if (selectedCredentialId && data.file.id && data.file.name) {
156+
useDisplayNamesStore.getState().setDisplayNames('files', selectedCredentialId, {
157+
[data.file.id]: data.file.name,
158+
})
159+
}
160+
153161
return data.file
154162
}
155163
} else {
@@ -335,6 +343,13 @@ export function GoogleDrivePicker({
335343
setSelectedFile(fileInfo)
336344
onChange(file.id, fileInfo)
337345
onFileInfoChange?.(fileInfo)
346+
347+
// Cache the selected file name
348+
if (selectedCredentialId) {
349+
useDisplayNamesStore
350+
.getState()
351+
.setDisplayNames('files', selectedCredentialId, { [file.id]: file.name })
352+
}
338353
}
339354
}
340355
},

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AlertTriangle } from 'lucide-react'
33
import { Label, Tooltip } from '@/components/emcn/components'
44
import { cn } from '@/lib/utils'
55
import type { FieldDiffStatus } from '@/lib/workflows/diff/types'
6+
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-depends-on-gate'
67
import type { SubBlockConfig } from '@/blocks/types'
78
import {
89
ChannelSelectorInput,
@@ -157,7 +158,15 @@ function SubBlockComponent({
157158
| string[]
158159
| null
159160
| undefined
160-
const isDisabled = disabled || isPreview
161+
162+
// Use dependsOn gating to compute final disabled state
163+
const { finalDisabled: gatedDisabled } = useDependsOnGate(blockId, config, {
164+
disabled,
165+
isPreview,
166+
previewContextValues: subBlockValues,
167+
})
168+
169+
const isDisabled = gatedDisabled
161170

162171
/**
163172
* Selects and renders the appropriate input component for the current sub-block `config.type`.

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
BLOCK_DIMENSIONS,
1313
useBlockDimensions,
1414
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
15-
import type { SubBlockConfig } from '@/blocks/types'
15+
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
1616
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
1717
import { useCredentialDisplay } from '@/hooks/use-credential-display'
1818
import { useDisplayName } from '@/hooks/use-display-name'
@@ -237,7 +237,10 @@ const SubBlockRow = ({
237237

238238
const isPasswordField = subBlock?.password === true
239239
const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null
240-
const displayValue = maskedValue || credentialName || dropdownLabel || genericDisplayName || value
240+
241+
const isSelectorType = subBlock?.type && SELECTOR_TYPES_HYDRATION_REQUIRED.includes(subBlock.type)
242+
const hydratedName = credentialName || dropdownLabel || genericDisplayName
243+
const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value)
241244

242245
return (
243246
<div className='flex items-center gap-[8px]'>

apps/sim/blocks/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ export type SubBlockType =
7373
| 'variables-input' // Variable assignments for updating workflow variables
7474
| 'text' // Read-only text display
7575

76+
/**
77+
* Selector types that require display name hydration
78+
* These show IDs/keys that need to be resolved to human-readable names
79+
*/
80+
export const SELECTOR_TYPES_HYDRATION_REQUIRED: SubBlockType[] = [
81+
'oauth-input',
82+
'channel-selector',
83+
'file-selector',
84+
'folder-selector',
85+
'project-selector',
86+
'knowledge-base-selector',
87+
'document-selector',
88+
] as const
89+
7690
export type ExtractToolOutput<T> = T extends ToolResponse ? T['output'] : never
7791

7892
export type ToolOutputToValueType<T> = T extends Record<string, any>

apps/sim/hooks/use-display-name.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,28 @@ export function useDisplayName(
516516
})
517517
.catch(() => {})
518518
.finally(() => setIsFetching(false))
519+
}
520+
// Google Drive files/folders (fetch by ID since no list endpoint via Picker API)
521+
else if (
522+
(provider === 'google-drive' || subBlock.serviceId === 'google-drive') &&
523+
typeof value === 'string' &&
524+
value
525+
) {
526+
const queryParams = new URLSearchParams({
527+
credentialId: context.credentialId,
528+
fileId: value,
529+
})
530+
fetch(`/api/tools/drive/file?${queryParams.toString()}`)
531+
.then((res) => res.json())
532+
.then((data) => {
533+
if (data.file?.id && data.file.name) {
534+
useDisplayNamesStore
535+
.getState()
536+
.setDisplayNames('files', context.credentialId!, { [data.file.id]: data.file.name })
537+
}
538+
})
539+
.catch(() => {})
540+
.finally(() => setIsFetching(false))
519541
} else {
520542
setIsFetching(false)
521543
}

apps/sim/stores/display-names/store.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ interface DisplayNamesStore {
4141
*/
4242
getDisplayName: (type: keyof DisplayNamesCache, context: string, id: string) => string | null
4343

44+
/**
45+
* Remove a single display name
46+
*/
47+
removeDisplayName: (type: keyof DisplayNamesCache, context: string, id: string) => void
48+
4449
/**
4550
* Clear all cached display names for a type/context
4651
*/
@@ -103,6 +108,22 @@ export const useDisplayNamesStore = create<DisplayNamesStore>((set, get) => ({
103108
return contextCache?.[id] || null
104109
},
105110

111+
removeDisplayName: (type, context, id) => {
112+
set((state) => {
113+
const contextCache = { ...state.cache[type][context] }
114+
delete contextCache[id]
115+
return {
116+
cache: {
117+
...state.cache,
118+
[type]: {
119+
...state.cache[type],
120+
[context]: contextCache,
121+
},
122+
},
123+
}
124+
})
125+
},
126+
106127
clearContext: (type, context) => {
107128
set((state) => {
108129
const newTypeCache = { ...state.cache[type] }

apps/sim/triggers/gmail/poller.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export const gmailPollingTrigger: TriggerConfig = {
3838
| string
3939
| null
4040
if (!credentialId) {
41-
return []
41+
// Return a sentinel to prevent infinite retry loops when credential is missing
42+
throw new Error('No Gmail credential selected')
4243
}
4344
try {
4445
const response = await fetch(`/api/tools/gmail/labels?credentialId=${credentialId}`)
@@ -55,7 +56,7 @@ export const gmailPollingTrigger: TriggerConfig = {
5556
return []
5657
} catch (error) {
5758
logger.error('Error fetching Gmail labels:', error)
58-
return []
59+
throw error
5960
}
6061
},
6162
dependsOn: ['triggerCredentials'],

apps/sim/triggers/outlook/poller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const outlookPollingTrigger: TriggerConfig = {
3838
| string
3939
| null
4040
if (!credentialId) {
41-
return []
41+
throw new Error('No Outlook credential selected')
4242
}
4343
try {
4444
const response = await fetch(`/api/tools/outlook/folders?credentialId=${credentialId}`)
@@ -55,7 +55,7 @@ export const outlookPollingTrigger: TriggerConfig = {
5555
return []
5656
} catch (error) {
5757
logger.error('Error fetching Outlook folders:', error)
58-
return []
58+
throw error
5959
}
6060
},
6161
dependsOn: ['triggerCredentials'],

0 commit comments

Comments
 (0)