mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
committed by
GitHub
parent
831ce91577
commit
f00d5516df
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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]'>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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] }
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user