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
This commit is contained in:
Vikhyath Mondreti
2025-11-11 13:39:15 -08:00
committed by GitHub
parent 831ce91577
commit f00d5516df
9 changed files with 134 additions and 9 deletions

View File

@@ -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)

View File

@@ -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 })
}
}
}
},

View File

@@ -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`.

View File

@@ -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 (
<div className='flex items-center gap-[8px]'>

View File

@@ -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>

View File

@@ -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)
}

View File

@@ -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<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] }

View File

@@ -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'],

View File

@@ -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'],