From b40fa3aa6ee4529420c2b959d85ffd545fb1a5f7 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 18 Aug 2025 20:21:23 -0700 Subject: [PATCH] fix(picker-ui): picker UI confusing when credential not set + Microsoft OAuth Fixes (#1016) * fix(picker-ui): picker UI confusing when credential not set * remove comments * remove chevron down * fix collaboration oauth * fix jira" * fix * fix ms excel selector * fix selectors for MS blocks * fix ms selectors * fix * fix ms onedrive and sharepoint * fix to grey out dropdowns * fix background fetches * fix planner * fix confluence * fix * fix confluence realtime sharing * fix outlook folder selector * check outlook folder * make shared hook --------- Co-authored-by: waleedlatif1 --- .../channel-selector-input.tsx | 30 +- .../credential-selector.tsx | 36 +- .../components/confluence-file-selector.tsx | 279 ++++---- .../components/google-drive-picker.tsx | 192 ++---- .../components/jira-issue-selector.tsx | 233 +++---- .../components/microsoft-file-selector.tsx | 595 +++++++++++------- .../components/teams-message-selector.tsx | 234 +++---- .../file-selector/file-selector-input.tsx | 147 +++-- .../components/folder-selector-input.tsx | 11 + .../folder-selector/folder-selector.tsx | 314 +++++---- .../components/jira-project-selector.tsx | 260 ++++---- .../project-selector-input.tsx | 35 +- .../sub-block/hooks/use-foreign-credential.ts | 50 ++ apps/sim/tools/google_sheets/read.ts | 7 +- 14 files changed, 1326 insertions(+), 1097 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-foreign-credential.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/channel-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/channel-selector-input.tsx index eac138805e..1dbacd709d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/channel-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/channel-selector-input.tsx @@ -33,6 +33,10 @@ export function ChannelSelectorInput({ // Use the proper hook to get the current value and setter (same as file-selector) const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) + // Reactive upstream fields + const [authMethod] = useSubBlockValue(blockId, 'authMethod') + const [botToken] = useSubBlockValue(blockId, 'botToken') + const [connectedCredential] = useSubBlockValue(blockId, 'credential') const [selectedChannelId, setSelectedChannelId] = useState('') const [_channelInfo, setChannelInfo] = useState(null) @@ -40,17 +44,14 @@ export function ChannelSelectorInput({ const provider = subBlock.provider || 'slack' const isSlack = provider === 'slack' - // Get the credential for the provider - use provided credential or fall back to store - const authMethod = getValue(blockId, 'authMethod') as string - const botToken = getValue(blockId, 'botToken') as string - + // Get the credential for the provider - use provided credential or fall back to reactive values let credential: string if (providedCredential) { credential = providedCredential - } else if (authMethod === 'bot_token' && botToken) { - credential = botToken + } else if ((authMethod as string) === 'bot_token' && (botToken as string)) { + credential = botToken as string } else { - credential = (getValue(blockId, 'credential') as string) || '' + credential = (connectedCredential as string) || '' } // Use preview value when in preview mode, otherwise use store value @@ -58,18 +59,11 @@ export function ChannelSelectorInput({ // Get the current value from the store or prop value if in preview mode (same pattern as file-selector) useEffect(() => { - if (isPreview && previewValue !== undefined) { - const value = previewValue - if (value && typeof value === 'string') { - setSelectedChannelId(value) - } - } else { - const value = getValue(blockId, subBlock.id) - if (value && typeof value === 'string') { - setSelectedChannelId(value) - } + const val = isPreview && previewValue !== undefined ? previewValue : storeValue + if (val && typeof val === 'string') { + setSelectedChannelId(val) } - }, [blockId, subBlock.id, getValue, isPreview, previewValue]) + }, [isPreview, previewValue, storeValue]) // Handle channel selection (same pattern as file-selector) const handleChannelChange = (channelId: string, info?: SlackChannelInfo) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx index d76b94c7b5..2eec5197c0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx @@ -124,12 +124,10 @@ export function CredentialSelector({ } }, [effectiveProviderId, selectedId, activeWorkflowId]) - // Fetch credentials on initial mount + // Fetch credentials on initial mount and whenever the subblock value changes externally useEffect(() => { fetchCredentials() - // This effect should only run once on mount, so empty dependency array - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [fetchCredentials, effectiveValue]) // When the selectedId changes (e.g., collaborator saved a credential), determine if it's foreign useEffect(() => { @@ -180,6 +178,19 @@ export function CredentialSelector({ } }, [fetchCredentials]) + // Also handle BFCache restores (back/forward navigation) where visibility change may not fire reliably + useEffect(() => { + const handlePageShow = (event: any) => { + if (event?.persisted) { + fetchCredentials() + } + } + window.addEventListener('pageshow', handlePageShow) + return () => { + window.removeEventListener('pageshow', handlePageShow) + } + }, [fetchCredentials]) + // Handle popover open to fetch fresh credentials const handleOpenChange = (isOpen: boolean) => { setOpen(isOpen) @@ -193,6 +204,13 @@ export function CredentialSelector({ const selectedCredential = credentials.find((cred) => cred.id === selectedId) const isForeign = !!(selectedId && !selectedCredential && hasForeignMeta) + // If the list doesn’t contain the effective value but meta says it exists, synthesize a non-leaky placeholder to render stable UI + const displayName = selectedCredential + ? selectedCredential.name + : isForeign + ? 'Saved by collaborator' + : undefined + // Handle selection const handleSelect = (credentialId: string) => { const previousId = selectedId || (effectiveValue as string) || '' @@ -263,15 +281,9 @@ export function CredentialSelector({
{getProviderIcon(provider)} - {selectedCredential - ? selectedCredential.name - : isForeign - ? 'Saved by collaborator' - : label} + {displayName || label}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx index 8c3baa727e..d5489b94b4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx @@ -46,6 +46,8 @@ interface ConfluenceFileSelectorProps { showPreview?: boolean onFileInfoChange?: (fileInfo: ConfluenceFileInfo | null) => void credentialId?: string + workflowId?: string + isForeignCredential?: boolean } export function ConfluenceFileSelector({ @@ -60,6 +62,8 @@ export function ConfluenceFileSelector({ showPreview = true, onFileInfoChange, credentialId, + workflowId, + isForeignCredential = false, }: ConfluenceFileSelectorProps) { const [open, setOpen] = useState(false) const [credentials, setCredentials] = useState([]) @@ -71,6 +75,12 @@ export function ConfluenceFileSelector({ const [showOAuthModal, setShowOAuthModal] = useState(false) const initialFetchRef = useRef(false) const [error, setError] = useState(null) + // Keep internal credential in sync with prop (handles late arrival and BFCache restores) + useEffect(() => { + if (credentialId && credentialId !== selectedCredentialId) { + setSelectedCredentialId(credentialId) + } + }, [credentialId, selectedCredentialId]) // Handle search with debounce const searchTimeoutRef = useRef(null) @@ -156,6 +166,7 @@ export function ConfluenceFileSelector({ }, body: JSON.stringify({ credentialId: selectedCredentialId, + workflowId, }), }) @@ -189,6 +200,18 @@ export function ConfluenceFileSelector({ if (data.file) { setSelectedFile(data.file) onFileInfoChange?.(data.file) + } else { + const fileInfo: ConfluenceFileInfo = { + id: data.id || pageId, + name: data.title || `Page ${pageId}`, + mimeType: 'confluence/page', + webViewLink: undefined, + modifiedTime: undefined, + spaceId: undefined, + url: undefined, + } + setSelectedFile(fileInfo) + onFileInfoChange?.(fileInfo) } } catch (error) { logger.error('Error fetching page info:', error) @@ -197,13 +220,14 @@ export function ConfluenceFileSelector({ setIsLoading(false) } }, - [selectedCredentialId, domain, onFileInfoChange] + [selectedCredentialId, domain, onFileInfoChange, workflowId] ) // Fetch pages from Confluence const fetchFiles = useCallback( async (searchQuery?: string) => { if (!selectedCredentialId || !domain) return + if (isForeignCredential) return // Validate domain format const trimmedDomain = domain.trim().toLowerCase() @@ -228,6 +252,7 @@ export function ConfluenceFileSelector({ }, body: JSON.stringify({ credentialId: selectedCredentialId, + workflowId, }), }) @@ -267,6 +292,12 @@ export function ConfluenceFileSelector({ if (!response.ok) { const errorData = await response.json() + if (response.status === 401 || response.status === 403) { + logger.info('Confluence pages fetch unauthorized (expected for collaborator)') + setFiles([]) + setIsLoading(false) + return + } logger.error('Confluence API error:', errorData) throw new Error(errorData.error || 'Failed to fetch pages') } @@ -294,7 +325,15 @@ export function ConfluenceFileSelector({ setIsLoading(false) } }, - [selectedCredentialId, domain, selectedFileId, onFileInfoChange, fetchPageInfo] + [ + selectedCredentialId, + domain, + selectedFileId, + onFileInfoChange, + fetchPageInfo, + workflowId, + isForeignCredential, + ] ) // Fetch credentials on initial mount @@ -310,7 +349,7 @@ export function ConfluenceFileSelector({ setOpen(isOpen) // Only fetch files when opening the dropdown and if we have valid credentials and domain - if (isOpen && selectedCredentialId && domain && domain.includes('.')) { + if (isOpen && !isForeignCredential && selectedCredentialId && domain && domain.includes('.')) { fetchFiles() } } @@ -320,7 +359,15 @@ export function ConfluenceFileSelector({ if (value && selectedCredentialId && !selectedFile && domain && domain.includes('.')) { fetchPageInfo(value) } - }, [value, selectedCredentialId, selectedFile, domain, fetchPageInfo]) + }, [ + value, + selectedCredentialId, + selectedFile, + domain, + fetchPageInfo, + workflowId, + isForeignCredential, + ]) // Keep internal selectedFileId in sync with the value prop useEffect(() => { @@ -363,7 +410,7 @@ export function ConfluenceFileSelector({ role='combobox' aria-expanded={open} className='h-10 w-full min-w-0 justify-between' - disabled={disabled || !domain} + disabled={disabled || !domain || isForeignCredential} >
{selectedFile ? ( @@ -381,118 +428,122 @@ export function ConfluenceFileSelector({ - - {/* Current account indicator */} - {selectedCredentialId && credentials.length > 0 && ( -
-
- - - {credentials.find((cred) => cred.id === selectedCredentialId)?.name || - 'Unknown'} - -
- {credentials.length > 1 && ( - - )} -
- )} - - - - - - {isLoading ? ( -
- - Loading pages... -
- ) : error ? ( -
-

{error}

-
- ) : credentials.length === 0 ? ( -
-

No accounts connected.

-

- Connect a Confluence account to continue. -

-
- ) : ( -
-

No pages found.

-

- Try a different search or account. -

-
+ {!isForeignCredential && ( + + {/* Current account indicator */} + {selectedCredentialId && credentials.length > 0 && ( +
+
+ + + {credentials.find((cred) => cred.id === selectedCredentialId)?.name || + 'Unknown'} + +
+ {credentials.length > 1 && ( + )} - +
+ )} - {/* Account selection - only show if we have multiple accounts */} - {credentials.length > 1 && ( - -
- Switch Account -
- {credentials.map((cred) => ( - setSelectedCredentialId(cred.id)} - > -
- - {cred.name} -
- {cred.id === selectedCredentialId && } -
- ))} -
- )} - - {/* Files list */} - {files.length > 0 && ( - -
- Pages -
- {files.map((file) => ( - handleSelectFile(file)} - > -
- - {file.name} -
- {file.id === selectedFileId && } -
- ))} -
- )} - - {/* Connect account option - only show if no credentials */} - {credentials.length === 0 && ( - - -
- - Connect Confluence account + + + + + {isLoading ? ( +
+ + Loading pages...
- - - )} -
-
- + ) : error ? ( +
+

{error}

+
+ ) : credentials.length === 0 ? ( +
+

No accounts connected.

+

+ Connect a Confluence account to continue. +

+
+ ) : ( +
+

No pages found.

+

+ Try a different search or account. +

+
+ )} + + + {/* Account selection - only show if we have multiple accounts */} + {credentials.length > 1 && ( + +
+ Switch Account +
+ {credentials.map((cred) => ( + setSelectedCredentialId(cred.id)} + > +
+ + {cred.name} +
+ {cred.id === selectedCredentialId && ( + + )} +
+ ))} +
+ )} + + {/* Files list */} + {files.length > 0 && ( + +
+ Pages +
+ {files.map((file) => ( + handleSelectFile(file)} + > +
+ + {file.name} +
+ {file.id === selectedFileId && } +
+ ))} +
+ )} + + {/* Connect account option - only show if no credentials */} + {credentials.length === 0 && ( + + +
+ + Connect Confluence account +
+
+
+ )} + + + + )} {/* File preview */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx index 52130ea3b2..3335babae5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx @@ -1,18 +1,10 @@ 'use client' import { useCallback, useEffect, useRef, useState } from 'react' -import { Check, ChevronDown, ExternalLink, FileIcon, FolderIcon, RefreshCw, X } from 'lucide-react' +import { ExternalLink, FileIcon, FolderIcon, RefreshCw, X } from 'lucide-react' import useDrivePicker from 'react-google-drive-picker' import { GoogleDocsIcon, GoogleSheetsIcon } from '@/components/icons' import { Button } from '@/components/ui/button' -import { - Command, - CommandEmpty, - CommandGroup, - CommandItem, - CommandList, -} from '@/components/ui/command' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { getEnv } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { @@ -74,7 +66,6 @@ export function GoogleDrivePicker({ credentialId, workflowId, }: GoogleDrivePickerProps) { - const [open, setOpen] = useState(false) const [credentials, setCredentials] = useState([]) const [selectedCredentialId, setSelectedCredentialId] = useState('') const [selectedFileId, setSelectedFileId] = useState(value) @@ -237,8 +228,9 @@ export function GoogleDrivePicker({ ]) // Fetch the access token for the selected credential - const fetchAccessToken = async (): Promise => { - if (!selectedCredentialId) { + const fetchAccessToken = async (credentialOverrideId?: string): Promise => { + const effectiveCredentialId = credentialOverrideId || selectedCredentialId + if (!effectiveCredentialId) { logger.error('No credential ID selected for Google Drive Picker') return null } @@ -246,7 +238,7 @@ export function GoogleDrivePicker({ setIsLoading(true) try { const url = new URL('/api/auth/oauth/token', window.location.origin) - url.searchParams.set('credentialId', selectedCredentialId) + url.searchParams.set('credentialId', effectiveCredentialId) // include workflowId if available via global registry (server adds session owner otherwise) const response = await fetch(url.toString()) @@ -265,10 +257,10 @@ export function GoogleDrivePicker({ } // Handle opening the Google Drive Picker - const handleOpenPicker = async () => { + const handleOpenPicker = async (credentialOverrideId?: string) => { try { // First, get the access token for the selected credential - const accessToken = await fetchAccessToken() + const accessToken = await fetchAccessToken(credentialOverrideId) if (!accessToken) { logger.error('Failed to get access token for Google Drive Picker') @@ -335,7 +327,6 @@ export function GoogleDrivePicker({ const handleAddCredential = () => { // Show the OAuth modal setShowOAuthModal(true) - setOpen(false) } // Clear selection @@ -426,136 +417,47 @@ export function GoogleDrivePicker({ return ( <>
- - - - - - {/* Current account indicator */} - {selectedCredentialId && credentials.length > 0 && ( -
-
- {getProviderIcon(provider)} - - {credentials.find((cred) => cred.id === selectedCredentialId)?.name || - 'Unknown'} - -
- {credentials.length > 1 && ( - - )} -
+ -
- - )} - - {/* Connect account option - only show if no credentials */} - {credentials.length === 0 && ( - - -
- {getProviderIcon(provider)} - Connect {getProviderName(provider)} account -
-
-
- )} - - - - +
+ {/* File preview */} {showPreview && selectedFile && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx index 6c0a5cc4b4..a58beb806d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx @@ -48,6 +48,7 @@ interface JiraIssueSelectorProps { projectId?: string credentialId?: string isForeignCredential?: boolean + workflowId?: string } export function JiraIssueSelector({ @@ -63,6 +64,8 @@ export function JiraIssueSelector({ onIssueInfoChange, projectId, credentialId, + isForeignCredential = false, + workflowId, }: JiraIssueSelectorProps) { const [open, setOpen] = useState(false) const [credentials, setCredentials] = useState([]) @@ -168,6 +171,7 @@ export function JiraIssueSelector({ }, body: JSON.stringify({ credentialId: selectedCredentialId, + workflowId, }), }) @@ -264,6 +268,7 @@ export function JiraIssueSelector({ }, body: JSON.stringify({ credentialId: selectedCredentialId, + workflowId, }), }) @@ -377,6 +382,10 @@ export function JiraIssueSelector({ // Handle open change const handleOpenChange = (isOpen: boolean) => { + if (disabled || isForeignCredential) { + setOpen(false) + return + } setOpen(isOpen) // Only fetch recent/default issues when opening the dropdown @@ -451,7 +460,7 @@ export function JiraIssueSelector({ role='combobox' aria-expanded={open} className='h-10 w-full min-w-0 justify-between' - disabled={disabled || !domain || !selectedCredentialId} + disabled={disabled || !domain || !selectedCredentialId || isForeignCredential} >
{selectedIssue ? ( @@ -469,118 +478,122 @@ export function JiraIssueSelector({ - - {/* Current account indicator */} - {selectedCredentialId && credentials.length > 0 && ( -
-
- - - {credentials.find((cred) => cred.id === selectedCredentialId)?.name || - 'Unknown'} - -
- {credentials.length > 1 && ( - - )} -
- )} - - - - - - {isLoading ? ( -
- - Loading issues... -
- ) : error ? ( -
-

{error}

-
- ) : credentials.length === 0 ? ( -
-

No accounts connected.

-

- Connect a Jira account to continue. -

-
- ) : ( -
-

No issues found.

-

- Try a different search or account. -

-
+ {!isForeignCredential && ( + + {/* Current account indicator */} + {selectedCredentialId && credentials.length > 0 && ( +
+
+ + + {credentials.find((cred) => cred.id === selectedCredentialId)?.name || + 'Unknown'} + +
+ {credentials.length > 1 && ( + )} - +
+ )} - {/* Account selection - only show if we have multiple accounts */} - {credentials.length > 1 && ( - -
- Switch Account -
- {credentials.map((cred) => ( - setSelectedCredentialId(cred.id)} - > -
- - {cred.name} -
- {cred.id === selectedCredentialId && } -
- ))} -
- )} - - {/* Issues list */} - {issues.length > 0 && ( - -
- Issues -
- {issues.map((issue) => ( - handleSelectIssue(issue)} - > -
- - {issue.name} -
- {issue.id === selectedIssueId && } -
- ))} -
- )} - - {/* Connect account option - only show if no credentials */} - {credentials.length === 0 && ( - - -
- - Connect Jira account + + + + + {isLoading ? ( +
+ + Loading issues...
- - - )} -
-
- + ) : error ? ( +
+

{error}

+
+ ) : credentials.length === 0 ? ( +
+

No accounts connected.

+

+ Connect a Jira account to continue. +

+
+ ) : ( +
+

No issues found.

+

+ Try a different search or account. +

+
+ )} + + + {/* Account selection - only show if we have multiple accounts */} + {credentials.length > 1 && ( + +
+ Switch Account +
+ {credentials.map((cred) => ( + setSelectedCredentialId(cred.id)} + > +
+ + {cred.name} +
+ {cred.id === selectedCredentialId && ( + + )} +
+ ))} +
+ )} + + {/* Issues list */} + {issues.length > 0 && ( + +
+ Issues +
+ {issues.map((issue) => ( + handleSelectIssue(issue)} + > +
+ + {issue.name} +
+ {issue.id === selectedIssueId && } +
+ ))} +
+ )} + + {/* Connect account option - only show if no credentials */} + {credentials.length === 0 && ( + + +
+ + Connect Jira account +
+
+
+ )} + + + + )} {/* Issue preview */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index d11c3e547f..38c3908bcc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -55,6 +55,9 @@ interface MicrosoftFileSelectorProps { showPreview?: boolean onFileInfoChange?: (fileInfo: MicrosoftFileInfo | null) => void planId?: string + workflowId?: string + credentialId?: string + isForeignCredential?: boolean } export function MicrosoftFileSelector({ @@ -68,10 +71,13 @@ export function MicrosoftFileSelector({ showPreview = true, onFileInfoChange, planId, + workflowId, + credentialId, + isForeignCredential = false, }: MicrosoftFileSelectorProps) { const [open, setOpen] = useState(false) const [credentials, setCredentials] = useState([]) - const [selectedCredentialId, setSelectedCredentialId] = useState('') + const [selectedCredentialId, setSelectedCredentialId] = useState(credentialId || '') const [selectedFileId, setSelectedFileId] = useState(value) const [selectedFile, setSelectedFile] = useState(null) const [isLoading, setIsLoading] = useState(false) @@ -112,23 +118,11 @@ export function MicrosoftFileSelector({ const data = await response.json() setCredentials(data.credentials) - // Auto-select logic for credentials - if (data.credentials.length > 0) { - // If we already have a selected credential ID, check if it's valid - if ( - selectedCredentialId && - data.credentials.some((cred: Credential) => cred.id === selectedCredentialId) - ) { - // Keep the current selection - } else { - // Otherwise, select the default or first credential - const defaultCred = data.credentials.find((cred: Credential) => cred.isDefault) - if (defaultCred) { - setSelectedCredentialId(defaultCred.id) - } else if (data.credentials.length === 1) { - setSelectedCredentialId(data.credentials[0].id) - } - } + // If a credentialId prop is provided (collaborator case), do not auto-select + if (!credentialId && data.credentials.length > 0 && !selectedCredentialId) { + const defaultCred = data.credentials.find((cred: Credential) => cred.isDefault) + if (defaultCred) setSelectedCredentialId(defaultCred.id) + else if (data.credentials.length === 1) setSelectedCredentialId(data.credentials[0].id) } } } catch (error) { @@ -137,11 +131,18 @@ export function MicrosoftFileSelector({ setIsLoading(false) setCredentialsLoaded(true) } - }, [provider, getProviderId, selectedCredentialId]) + }, [provider, getProviderId, selectedCredentialId, credentialId]) + + // Keep internal credential in sync with prop + useEffect(() => { + if (credentialId && credentialId !== selectedCredentialId) { + setSelectedCredentialId(credentialId) + } + }, [credentialId, selectedCredentialId]) // Fetch available files for the selected credential const fetchAvailableFiles = useCallback(async () => { - if (!selectedCredentialId) return + if (!selectedCredentialId || isForeignCredential) return setIsLoadingFiles(true) try { @@ -170,9 +171,13 @@ export function MicrosoftFileSelector({ const data = await response.json() setAvailableFiles(data.files || []) } else { - logger.error('Error fetching available files:', { - error: await response.text(), - }) + const txt = await response.text() + if (response.status === 401 || response.status === 403) { + // Suppress noisy auth errors for collaborators; lists are intentionally gated + logger.info('Skipping list fetch (auth)', { status: response.status }) + } else { + logger.warn('Non-OK list fetch', { status: response.status, txt }) + } setAvailableFiles([]) } } catch (error) { @@ -181,7 +186,7 @@ export function MicrosoftFileSelector({ } finally { setIsLoadingFiles(false) } - }, [selectedCredentialId, searchQuery, serviceId]) + }, [selectedCredentialId, searchQuery, serviceId, isForeignCredential]) // Fetch a single file by ID when we have a selectedFileId but no metadata const fetchFileById = useCallback( @@ -190,49 +195,90 @@ export function MicrosoftFileSelector({ setIsLoadingSelectedFile(true) try { - // Construct query parameters - const queryParams = new URLSearchParams({ - credentialId: selectedCredentialId, - fileId: fileId, - }) - - // Route to correct endpoint based on service - let endpoint: string - if (serviceId === 'onedrive') { - endpoint = `/api/tools/onedrive/folder?${queryParams.toString()}` - } else if (serviceId === 'sharepoint') { - // Change from fileId to siteId for SharePoint - const sharepointParams = new URLSearchParams({ - credentialId: selectedCredentialId, - siteId: fileId, // Use siteId instead of fileId + // Use owner-scoped token for OneDrive items (files/folders) and Excel + if (serviceId !== 'sharepoint') { + const tokenRes = await fetch('/api/auth/oauth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credentialId: selectedCredentialId, workflowId }), }) - endpoint = `/api/tools/sharepoint/site?${sharepointParams.toString()}` - } else { - endpoint = `/api/auth/oauth/microsoft/file?${queryParams.toString()}` + if (!tokenRes.ok) { + const err = await tokenRes.text() + logger.error('Failed to get access token for Microsoft file fetch', { err }) + return null + } + const { accessToken } = await tokenRes.json() + if (!accessToken) return null + + const graphUrl = + `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(fileId)}?` + + new URLSearchParams({ + $select: + 'id,name,webUrl,thumbnails,createdDateTime,lastModifiedDateTime,size,createdBy,file,folder', + }).toString() + const resp = await fetch(graphUrl, { + headers: { Authorization: `Bearer ${accessToken}` }, + }) + if (!resp.ok) { + const t = await resp.text() + // For 404/403, keep current selection; this often means the item moved or is shared differently. + if (resp.status !== 404 && resp.status !== 403) { + logger.warn('Graph error fetching file by ID', { status: resp.status, t }) + } + return null + } + const file = await resp.json() + const fileInfo: MicrosoftFileInfo = { + id: file.id, + name: file.name, + mimeType: + file?.file?.mimeType || (file.folder ? 'application/vnd.ms-onedrive.folder' : ''), + iconLink: file.thumbnails?.[0]?.small?.url, + webViewLink: file.webUrl, + thumbnailLink: file.thumbnails?.[0]?.medium?.url, + createdTime: file.createdDateTime, + modifiedTime: file.lastModifiedDateTime, + size: file.size?.toString(), + owners: file.createdBy + ? [ + { + displayName: file.createdBy.user?.displayName || 'Unknown', + emailAddress: file.createdBy.user?.email || '', + }, + ] + : [], + } + setSelectedFile(fileInfo) + onFileInfoChange?.(fileInfo) + return fileInfo } - const response = await fetch(endpoint) - - if (response.ok) { - const data = await response.json() - if (data.file) { - setSelectedFile(data.file) - onFileInfoChange?.(data.file) - return data.file - } - } else { - const errorText = await response.text() - logger.error('Error fetching file by ID:', { error: errorText }) - - // If file not found or access denied, clear the selection - if (response.status === 404 || response.status === 403) { - logger.info('File not accessible, clearing selection') - setSelectedFileId('') - onChange('') - onFileInfoChange?.(null) + // SharePoint site: fetch via Graph sites endpoint for collaborator visibility + const tokenRes = await fetch('/api/auth/oauth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credentialId: selectedCredentialId, workflowId }), + }) + if (!tokenRes.ok) return null + const { accessToken: spToken } = await tokenRes.json() + if (!spToken) return null + const spResp = await fetch( + `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(fileId)}?$select=id,displayName,webUrl`, + { + headers: { Authorization: `Bearer ${spToken}` }, } + ) + if (!spResp.ok) return null + const site = await spResp.json() + const siteInfo: MicrosoftFileInfo = { + id: site.id, + name: site.displayName, + mimeType: 'sharepoint/site', + webViewLink: site.webUrl, } - return null + setSelectedFile(siteInfo) + onFileInfoChange?.(siteInfo) + return siteInfo } catch (error) { logger.error('Error fetching file by ID:', { error }) return null @@ -240,16 +286,22 @@ export function MicrosoftFileSelector({ setIsLoadingSelectedFile(false) } }, - [selectedCredentialId, onFileInfoChange, serviceId] + [selectedCredentialId, onFileInfoChange, serviceId, workflowId, onChange] ) // Fetch Microsoft Planner tasks when planId and credentials are available const fetchPlannerTasks = useCallback(async () => { - if (!selectedCredentialId || !planId || serviceId !== 'microsoft-planner') { + if ( + !selectedCredentialId || + !planId || + serviceId !== 'microsoft-planner' || + isForeignCredential + ) { logger.info('Skipping task fetch - missing requirements:', { selectedCredentialId: !!selectedCredentialId, planId: !!planId, serviceId, + isForeignCredential, }) return } @@ -296,11 +348,17 @@ export function MicrosoftFileSelector({ setPlannerTasks(transformedTasks) } else { const errorText = await response.text() - logger.error('API response not ok:', { - status: response.status, - statusText: response.statusText, - errorText, - }) + if (response.status === 401 || response.status === 403) { + logger.info('Planner list fetch unauthorized (expected for collaborator)', { + status: response.status, + }) + } else { + logger.warn('Planner tasks fetch non-OK', { + status: response.status, + statusText: response.statusText, + errorText, + }) + } setPlannerTasks([]) } } catch (error) { @@ -309,7 +367,50 @@ export function MicrosoftFileSelector({ } finally { setIsLoadingTasks(false) } - }, [selectedCredentialId, planId, serviceId]) + }, [selectedCredentialId, planId, serviceId, isForeignCredential]) + + // Fetch a single planner task by ID for collaborator preview + const fetchPlannerTaskById = useCallback( + async (taskId: string) => { + if (!selectedCredentialId || !taskId || serviceId !== 'microsoft-planner') return null + setIsLoadingTasks(true) + try { + const tokenRes = await fetch('/api/auth/oauth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credentialId: selectedCredentialId, workflowId }), + }) + if (!tokenRes.ok) return null + const { accessToken } = await tokenRes.json() + if (!accessToken) return null + const resp = await fetch( + `https://graph.microsoft.com/v1.0/planner/tasks/${encodeURIComponent(taskId)}`, + { + headers: { Authorization: `Bearer ${accessToken}` }, + } + ) + if (!resp.ok) return null + const task = await resp.json() + const taskAsFileInfo: MicrosoftFileInfo = { + id: task.id, + name: task.title, + mimeType: 'planner/task', + webViewLink: `https://tasks.office.com/planner/task/${task.id}`, + createdTime: task.createdDateTime, + modifiedTime: task.createdDateTime, + } + setSelectedTask(task) + setSelectedFile(taskAsFileInfo) + onFileInfoChange?.(taskAsFileInfo) + return taskAsFileInfo + } catch { + return null + } finally { + setIsLoadingTasks(false) + } + }, + [selectedCredentialId, workflowId, onFileInfoChange, serviceId] + ) // Fetch credentials on initial mount useEffect(() => { @@ -339,10 +440,15 @@ export function MicrosoftFileSelector({ // Fetch planner tasks when credentials and planId change useEffect(() => { - if (serviceId === 'microsoft-planner' && selectedCredentialId && planId) { + if ( + serviceId === 'microsoft-planner' && + selectedCredentialId && + planId && + !isForeignCredential + ) { fetchPlannerTasks() } - }, [selectedCredentialId, planId, serviceId, fetchPlannerTasks]) + }, [selectedCredentialId, planId, serviceId, isForeignCredential, fetchPlannerTasks]) // Handle task selection for planner const handleTaskSelect = (task: PlannerTask) => { @@ -357,26 +463,23 @@ export function MicrosoftFileSelector({ modifiedTime: task.createdDateTime, } + // Update internal state first to avoid race with list refetch setSelectedFileId(taskId) setSelectedFile(taskAsFileInfo) setSelectedTask(task) + // Then propagate up onChange(taskId, taskAsFileInfo) onFileInfoChange?.(taskAsFileInfo) setOpen(false) setSearchQuery('') } - // Keep internal selectedFileId in sync with the value prop + // Keep internal selectedFileId in sync with the value prop (do not clear selectedFile; we'll resolve new metadata below) useEffect(() => { if (value !== selectedFileId) { - const previousFileId = selectedFileId setSelectedFileId(value) - // Only clear selected file info if we had a different file before (not initial load) - if (previousFileId && previousFileId !== value && selectedFile) { - setSelectedFile(null) - } } - }, [value, selectedFileId, selectedFile]) + }, [value, selectedFileId]) // Track previous credential ID to detect changes const prevCredentialIdRef = useRef('') @@ -403,18 +506,19 @@ export function MicrosoftFileSelector({ // Fetch the selected file metadata once credentials are loaded or changed useEffect(() => { - // Only fetch if we have both a file ID and credentials, credentials are loaded, but no file info yet + // Fetch metadata when the external value doesn't match our current selectedFile if ( value && selectedCredentialId && credentialsLoaded && - !selectedFile && - !isLoadingSelectedFile && - serviceId !== 'microsoft-planner' && - serviceId !== 'sharepoint' && - serviceId !== 'onedrive' + (!selectedFile || selectedFile.id !== value) && + !isLoadingSelectedFile ) { - fetchFileById(value) + if (serviceId === 'microsoft-planner') { + void fetchPlannerTaskById(value) + } else { + fetchFileById(value) + } } }, [ value, @@ -423,9 +527,30 @@ export function MicrosoftFileSelector({ selectedFile, isLoadingSelectedFile, fetchFileById, + fetchPlannerTaskById, serviceId, ]) + // Resolve planner task selection for collaborators + useEffect(() => { + if ( + value && + selectedCredentialId && + credentialsLoaded && + !selectedTask && + serviceId === 'microsoft-planner' + ) { + void fetchPlannerTaskById(value) + } + }, [ + value, + selectedCredentialId, + credentialsLoaded, + selectedTask, + serviceId, + fetchPlannerTaskById, + ]) + // Handle selecting a file from the available files const handleFileSelect = (file: MicrosoftFileInfo) => { setSelectedFileId(file.id) @@ -620,7 +745,9 @@ export function MicrosoftFileSelector({ role='combobox' aria-expanded={open} className='h-10 w-full min-w-0 justify-between' - disabled={disabled || (serviceId === 'microsoft-planner' && !planId)} + disabled={ + disabled || isForeignCredential || (serviceId === 'microsoft-planner' && !planId) + } >
{selectedFile ? ( @@ -643,154 +770,158 @@ export function MicrosoftFileSelector({ - - {/* Current account indicator */} - {selectedCredentialId && credentials.length > 0 && ( -
-
- {getProviderIcon(provider)} - - {credentials.find((cred) => cred.id === selectedCredentialId)?.name || - 'Unknown'} - + {!isForeignCredential && ( + + {/* Current account indicator */} + {selectedCredentialId && credentials.length > 0 && ( +
+
+ {getProviderIcon(provider)} + + {credentials.find((cred) => cred.id === selectedCredentialId)?.name || + 'Unknown'} + +
+ {credentials.length > 1 && ( + + )}
- {credentials.length > 1 && ( - - )} -
- )} + )} - - - - - {isLoading || isLoadingFiles || isLoadingTasks ? ( -
- - Loading... -
- ) : credentials.length === 0 ? ( -
-

No accounts connected.

-

- Connect a {getProviderName(provider)} account to continue. -

-
- ) : serviceId === 'microsoft-planner' && !planId ? ( -
-

Plan ID required.

-

- Please enter a Plan ID first to see tasks. -

-
- ) : filteredTasks.length === 0 ? ( -
-

{getEmptyStateText().title}

-

- {getEmptyStateText().description} -

-
- ) : null} -
- - {/* Account selection - only show if we have multiple accounts */} - {credentials.length > 1 && ( - -
- Switch Account -
- {credentials.map((cred) => ( - setSelectedCredentialId(cred.id)} - > -
- {getProviderIcon(cred.provider)} - {cred.name} -
- {cred.id === selectedCredentialId && } -
- ))} -
- )} - - {/* Available files/tasks - only show if we have credentials and items */} - {credentials.length > 0 && selectedCredentialId && filteredTasks.length > 0 && ( - -
- {getFileTypeTitleCase()} -
- {filteredTasks.map((item) => { - const isPlanner = serviceId === 'microsoft-planner' - const isPlannerTask = isPlanner && 'title' in item - const plannerTask = item as PlannerTask - const fileInfo = item as MicrosoftFileInfo - - const displayName = isPlannerTask ? plannerTask.title : fileInfo.name - const dateField = isPlannerTask - ? plannerTask.createdDateTime - : fileInfo.createdTime - - return ( - - isPlannerTask - ? handleTaskSelect(plannerTask) - : handleFileSelect(fileInfo) - } - > -
- {getFileIcon( - isPlannerTask - ? { - ...fileInfo, - id: plannerTask.id || '', - name: plannerTask.title, - mimeType: 'planner/task', - } - : fileInfo, - 'sm' - )} -
- {displayName} - {dateField && ( -
- Modified {new Date(dateField).toLocaleDateString()} -
- )} -
-
- {item.id === selectedFileId && } -
- ) - })} -
- )} - - {/* Connect account option - only show if no credentials */} - {credentials.length === 0 && ( - - -
- {getProviderIcon(provider)} - Connect {getProviderName(provider)} account + + + + + {isLoading || isLoadingFiles || isLoadingTasks ? ( +
+ + Loading...
- - - )} -
-
- + ) : credentials.length === 0 ? ( +
+

No accounts connected.

+

+ Connect a {getProviderName(provider)} account to continue. +

+
+ ) : serviceId === 'microsoft-planner' && !planId ? ( +
+

Plan ID required.

+

+ Please enter a Plan ID first to see tasks. +

+
+ ) : filteredTasks.length === 0 ? ( +
+

{getEmptyStateText().title}

+

+ {getEmptyStateText().description} +

+
+ ) : null} + + + {/* Account selection - only show if we have multiple accounts */} + {credentials.length > 1 && ( + +
+ Switch Account +
+ {credentials.map((cred) => ( + setSelectedCredentialId(cred.id)} + > +
+ {getProviderIcon(cred.provider)} + {cred.name} +
+ {cred.id === selectedCredentialId && ( + + )} +
+ ))} +
+ )} + + {/* Available files/tasks - only show if we have credentials and items */} + {credentials.length > 0 && selectedCredentialId && filteredTasks.length > 0 && ( + +
+ {getFileTypeTitleCase()} +
+ {filteredTasks.map((item) => { + const isPlanner = serviceId === 'microsoft-planner' + const isPlannerTask = isPlanner && 'title' in item + const plannerTask = item as PlannerTask + const fileInfo = item as MicrosoftFileInfo + + const displayName = isPlannerTask ? plannerTask.title : fileInfo.name + const dateField = isPlannerTask + ? plannerTask.createdDateTime + : fileInfo.createdTime + + return ( + + isPlannerTask + ? handleTaskSelect(plannerTask) + : handleFileSelect(fileInfo) + } + > +
+ {getFileIcon( + isPlannerTask + ? { + ...fileInfo, + id: plannerTask.id || '', + name: plannerTask.title, + mimeType: 'planner/task', + } + : fileInfo, + 'sm' + )} +
+ {displayName} + {dateField && ( +
+ Modified {new Date(dateField).toLocaleDateString()} +
+ )} +
+
+ {item.id === selectedFileId && } +
+ ) + })} +
+ )} + + {/* Connect account option - only show if no credentials */} + {credentials.length === 0 && ( + + +
+ {getProviderIcon(provider)} + Connect {getProviderName(provider)} account +
+
+
+ )} + + + + )} {/* File preview */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx index ecbaf2ae8b..170d8360cc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx @@ -48,6 +48,7 @@ interface TeamsMessageSelectorProps { selectionType?: 'team' | 'channel' | 'chat' initialTeamId?: string workflowId: string + isForeignCredential?: boolean } export function TeamsMessageSelector({ @@ -64,6 +65,7 @@ export function TeamsMessageSelector({ selectionType = 'team', initialTeamId, workflowId, + isForeignCredential = false, }: TeamsMessageSelectorProps) { const [open, setOpen] = useState(false) const [credentials, setCredentials] = useState([]) @@ -324,6 +326,10 @@ export function TeamsMessageSelector({ // Handle open change const handleOpenChange = (isOpen: boolean) => { + if (disabled || isForeignCredential) { + setOpen(false) + return + } setOpen(isOpen) // Only fetch data when opening the dropdown if (isOpen && selectedCredentialId) { @@ -693,7 +699,7 @@ export function TeamsMessageSelector({ role='combobox' aria-expanded={open} className='h-10 w-full min-w-0 justify-between' - disabled={disabled} + disabled={disabled || isForeignCredential} >
{selectedMessage ? ( @@ -715,120 +721,124 @@ export function TeamsMessageSelector({ - - {/* Current account indicator */} - {selectedCredentialId && credentials.length > 0 && ( -
-
- - - {credentials.find((cred) => cred.id === selectedCredentialId)?.name || - 'Unknown'} - -
- {credentials.length > 1 && ( - - )} -
- )} - - - - - - {isLoading ? ( -
- - Loading {selectionStage}s... -
- ) : error ? ( -
-

{error}

- {selectionStage === 'chat' && error.includes('teams') && ( -

- There was an issue fetching chats. Please try again or connect a different - account. -

- )} -
- ) : credentials.length === 0 ? ( -
-

No accounts connected.

-

- Connect a Microsoft Teams account to{' '} - {selectionStage === 'chat' - ? 'access your chats' - : selectionStage === 'channel' - ? 'see your channels' - : 'continue'} - . -

-
- ) : ( -
-

No {selectionStage}s found.

-

- {selectionStage === 'team' - ? 'Try a different account.' - : selectionStage === 'channel' - ? selectedTeamId - ? 'This team has no channels or you may not have access.' - : 'Please select a team first to see its channels.' - : 'Try a different account or check if you have any active chats.'} -

-
+ {!isForeignCredential && ( + + {/* Current account indicator */} + {selectedCredentialId && credentials.length > 0 && ( +
+
+ + + {credentials.find((cred) => cred.id === selectedCredentialId)?.name || + 'Unknown'} + +
+ {credentials.length > 1 && ( + )} - +
+ )} - {/* Account selection - only show if we have multiple accounts */} - {credentials.length > 1 && ( - -
- Switch Account -
- {credentials.map((cred) => ( - { - setSelectedCredentialId(cred.id) - setOpen(false) - }} - > -
- - {cred.name} -
- {cred.id === selectedCredentialId && } -
- ))} -
- )} - - {/* Display appropriate options based on selection stage */} - {renderSelectionOptions()} - - {/* Connect account option - only show if no credentials */} - {credentials.length === 0 && ( - - -
- - Connect Microsoft Teams account + + + + + {isLoading ? ( +
+ + Loading {selectionStage}s...
- - - )} -
-
- + ) : error ? ( +
+

{error}

+ {selectionStage === 'chat' && error.includes('teams') && ( +

+ There was an issue fetching chats. Please try again or connect a + different account. +

+ )} +
+ ) : credentials.length === 0 ? ( +
+

No accounts connected.

+

+ Connect a Microsoft Teams account to{' '} + {selectionStage === 'chat' + ? 'access your chats' + : selectionStage === 'channel' + ? 'see your channels' + : 'continue'} + . +

+
+ ) : ( +
+

No {selectionStage}s found.

+

+ {selectionStage === 'team' + ? 'Try a different account.' + : selectionStage === 'channel' + ? selectedTeamId + ? 'This team has no channels or you may not have access.' + : 'Please select a team first to see its channels.' + : 'Try a different account or check if you have any active chats.'} +

+
+ )} + + + {/* Account selection - only show if we have multiple accounts */} + {credentials.length > 1 && ( + +
+ Switch Account +
+ {credentials.map((cred) => ( + { + setSelectedCredentialId(cred.id) + setOpen(false) + }} + > +
+ + {cred.name} +
+ {cred.id === selectedCredentialId && ( + + )} +
+ ))} +
+ )} + + {/* Display appropriate options based on selection stage */} + {renderSelectionOptions()} + + {/* Connect account option - only show if no credentials */} + {credentials.length === 0 && ( + + +
+ + Connect Microsoft Teams account +
+
+
+ )} + + + + )} {/* Selection preview */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx index 16a9d19956..9875817e0b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx @@ -22,6 +22,7 @@ import { WealthboxFileSelector, type WealthboxItemInfo, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components' +import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-foreign-credential' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' @@ -70,6 +71,7 @@ export function FileSelectorInput({ // Use the proper hook to get the current value and setter const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) + const [connectedCredential] = useSubBlockValue(blockId, 'credential') const [selectedFileId, setSelectedFileId] = useState('') const [_fileInfo, setFileInfo] = useState(null) const [selectedIssueId, setSelectedIssueId] = useState('') @@ -84,34 +86,10 @@ export function FileSelectorInput({ const [wealthboxItemInfo, setWealthboxItemInfo] = useState(null) // Determine if the persisted credential belongs to the current viewer - const [isForeignCredential, setIsForeignCredential] = useState(false) - useEffect(() => { - const cred = (getValue(blockId, 'credential') as string) || '' - if (!cred) { - setIsForeignCredential(false) - return - } - let aborted = false - ;(async () => { - try { - const resp = await fetch(`/api/auth/oauth/credentials?credentialId=${cred}`) - if (aborted) return - if (!resp.ok) { - setIsForeignCredential(true) - return - } - const data = await resp.json() - // If credential not returned for this session user, it's foreign - setIsForeignCredential(!(data.credentials && data.credentials.length === 1)) - } catch { - setIsForeignCredential(true) - } - })() - return () => { - aborted = true - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [blockId, getValue(blockId, 'credential')]) + const { isForeignCredential } = useForeignCredential( + subBlock.provider || subBlock.serviceId || '', + (connectedCredential as string) || '' + ) // Get provider-specific values const provider = subBlock.provider || 'google-drive' @@ -254,7 +232,7 @@ export function FileSelectorInput({ // Render Google Calendar selector if (isGoogleCalendar) { - const credential = (getValue(blockId, 'credential') as string) || '' + const credential = (connectedCredential as string) || '' return ( @@ -321,7 +299,7 @@ export function FileSelectorInput({ // Render the appropriate picker based on provider if (isConfluence) { - const credential = (getValue(blockId, 'credential') as string) || '' + const credential = (connectedCredential as string) || '' return ( @@ -347,6 +325,8 @@ export function FileSelectorInput({ showPreview={true} onFileInfoChange={setFileInfo as (info: ConfluenceFileInfo | null) => void} credentialId={credential} + workflowId={workflowIdFromUrl} + isForeignCredential={isForeignCredential} />
@@ -361,7 +341,7 @@ export function FileSelectorInput({ } if (isJira) { - const credential = jiraCredential + const credential = (connectedCredential as string) || '' return ( @@ -391,6 +371,7 @@ export function FileSelectorInput({ credentialId={credential} projectId={(getValue(blockId, 'projectId') as string) || ''} isForeignCredential={isForeignCredential} + workflowId={activeWorkflowId || ''} />
@@ -413,8 +394,8 @@ export function FileSelectorInput({ } if (isMicrosoftExcel) { - // Get credential using the same pattern as other tools - const credential = (getValue(blockId, 'credential') as string) || '' + // Get credential reactively + const credential = (connectedCredential as string) || '' return ( @@ -431,6 +412,9 @@ export function FileSelectorInput({ disabled={disabled || !credential} showPreview={true} onFileInfoChange={setFileInfo as (info: MicrosoftFileInfo | null) => void} + workflowId={activeWorkflowId || ''} + credentialId={credential} + isForeignCredential={isForeignCredential} />
@@ -446,8 +430,8 @@ export function FileSelectorInput({ // Handle Microsoft Word selector if (isMicrosoftWord) { - // Get credential using the same pattern as other tools - const credential = (getValue(blockId, 'credential') as string) || '' + // Get credential reactively + const credential = (connectedCredential as string) || '' return ( @@ -479,7 +463,7 @@ export function FileSelectorInput({ // Handle Microsoft OneDrive selector if (isMicrosoftOneDrive) { - const credential = (getValue(blockId, 'credential') as string) || '' + const credential = (connectedCredential as string) || '' return ( @@ -496,6 +480,9 @@ export function FileSelectorInput({ disabled={disabled || !credential} showPreview={true} onFileInfoChange={setFileInfo as (info: MicrosoftFileInfo | null) => void} + workflowId={activeWorkflowId || ''} + credentialId={credential} + isForeignCredential={isForeignCredential} />
@@ -511,7 +498,7 @@ export function FileSelectorInput({ // Handle Microsoft SharePoint selector if (isMicrosoftSharePoint) { - const credential = (getValue(blockId, 'credential') as string) || '' + const credential = (connectedCredential as string) || '' return ( @@ -528,6 +515,9 @@ export function FileSelectorInput({ disabled={disabled || !credential} showPreview={true} onFileInfoChange={setFileInfo as (info: MicrosoftFileInfo | null) => void} + workflowId={activeWorkflowId || ''} + credentialId={credential} + isForeignCredential={isForeignCredential} />
@@ -543,7 +533,7 @@ export function FileSelectorInput({ // Handle Microsoft Planner task selector if (isMicrosoftPlanner) { - const credential = (getValue(blockId, 'credential') as string) || '' + const credential = (connectedCredential as string) || '' const planId = (getValue(blockId, 'planId') as string) || '' return ( @@ -562,6 +552,9 @@ export function FileSelectorInput({ showPreview={true} onFileInfoChange={setFileInfo as (info: MicrosoftFileInfo | null) => void} planId={planId} + workflowId={activeWorkflowId || ''} + credentialId={credential} + isForeignCredential={isForeignCredential} />
@@ -582,7 +575,7 @@ export function FileSelectorInput({ // Handle Microsoft Teams selector if (isMicrosoftTeams) { // Get credential using the same pattern as other tools - const credential = (getValue(blockId, 'credential') as string) || '' + const credential = (connectedCredential as string) || '' // Determine the selector type based on the subBlock ID let selectionType: 'team' | 'channel' | 'chat' = 'team' @@ -633,6 +626,7 @@ export function FileSelectorInput({ selectionType={selectionType} initialTeamId={selectedTeamId} workflowId={activeWorkflowId || ''} + isForeignCredential={isForeignCredential} />
@@ -648,8 +642,8 @@ export function FileSelectorInput({ // Render Wealthbox selector if (isWealthbox) { - // Get credential using the same pattern as other tools - const credential = (getValue(blockId, 'credential') as string) || '' + // Get credential reactively + const credential = (connectedCredential as string) || '' // Only handle contacts now - both notes and tasks use short-input if (subBlock.id === 'contactId') { @@ -697,32 +691,47 @@ export function FileSelectorInput({ } // Default to Google Drive picker - return ( - { - setSelectedFileId(val) - setFileInfo(info || null) - collaborativeSetSubblockValue(blockId, subBlock.id, val) - }} - provider={provider} - requiredScopes={subBlock.requiredScopes || []} - label={subBlock.placeholder || 'Select file'} - disabled={disabled} - serviceId={subBlock.serviceId} - mimeTypeFilter={subBlock.mimeType} - showPreview={true} - onFileInfoChange={setFileInfo} - clientId={clientId} - apiKey={apiKey} - credentialId={ - ((isPreview && previewContextValues?.credential?.value) || - (getValue(blockId, 'credential') as string) || - '') as string - } - workflowId={workflowIdFromUrl} - /> - ) + { + const credential = ((isPreview && previewContextValues?.credential?.value) || + (connectedCredential as string) || + '') as string + + return ( + + + +
+ { + setSelectedFileId(val) + setFileInfo(info || null) + collaborativeSetSubblockValue(blockId, subBlock.id, val) + }} + provider={provider} + requiredScopes={subBlock.requiredScopes || []} + label={subBlock.placeholder || 'Select file'} + disabled={disabled || !credential} + serviceId={subBlock.serviceId} + mimeTypeFilter={subBlock.mimeType} + showPreview={true} + onFileInfoChange={setFileInfo} + clientId={clientId} + apiKey={apiKey} + credentialId={credential} + workflowId={workflowIdFromUrl} + /> +
+
+ {!credential && ( + +

Please select Google Drive credentials first

+
+ )} +
+
+ ) + } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx index a8d5119073..bdf6c71305 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx @@ -5,9 +5,11 @@ import { type FolderInfo, FolderSelector, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector' +import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-foreign-credential' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' interface FolderSelectorInputProps { blockId: string @@ -25,9 +27,15 @@ export function FolderSelectorInput({ previewValue, }: FolderSelectorInputProps) { const [storeValue, _setStoreValue] = useSubBlockValue(blockId, subBlock.id) + const [connectedCredential] = useSubBlockValue(blockId, 'credential') const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() + const { activeWorkflowId } = useWorkflowRegistry() const [selectedFolderId, setSelectedFolderId] = useState('') const [_folderInfo, setFolderInfo] = useState(null) + const { isForeignCredential } = useForeignCredential( + subBlock.provider || subBlock.serviceId || 'outlook', + (connectedCredential as string) || '' + ) // Get the current value from the store or prop value if in preview mode useEffect(() => { @@ -67,6 +75,9 @@ export function FolderSelectorInput({ disabled={disabled} serviceId={subBlock.serviceId} onFolderInfoChange={setFolderInfo} + credentialId={(connectedCredential as string) || ''} + workflowId={activeWorkflowId || ''} + isForeignCredential={isForeignCredential} /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx index ee5eb7e562..d331b8685c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx @@ -38,6 +38,9 @@ interface FolderSelectorProps { onFolderInfoChange?: (folderInfo: FolderInfo | null) => void isPreview?: boolean previewValue?: any | null + credentialId?: string + workflowId?: string + isForeignCredential?: boolean } export function FolderSelector({ @@ -51,11 +54,16 @@ export function FolderSelector({ onFolderInfoChange, isPreview = false, previewValue, + credentialId, + workflowId, + isForeignCredential = false, }: FolderSelectorProps) { const [open, setOpen] = useState(false) const [credentials, setCredentials] = useState([]) const [folders, setFolders] = useState([]) - const [selectedCredentialId, setSelectedCredentialId] = useState('') + const [selectedCredentialId, setSelectedCredentialId] = useState( + credentialId || '' + ) const [selectedFolderId, setSelectedFolderId] = useState('') const [selectedFolder, setSelectedFolder] = useState(null) const [isLoading, setIsLoading] = useState(false) @@ -72,6 +80,13 @@ export function FolderSelector({ } }, [value, isPreview, previewValue]) + // Keep internal credential in sync with prop + useEffect(() => { + if (credentialId && credentialId !== selectedCredentialId) { + setSelectedCredentialId(credentialId) + } + }, [credentialId, selectedCredentialId]) + // Determine the appropriate service ID based on provider and scopes const getServiceId = (): string => { if (serviceId) return serviceId @@ -124,18 +139,43 @@ export function FolderSelector({ // Fetch a single folder by ID when we have a selectedFolderId but no metadata const fetchFolderById = useCallback( async (folderId: string) => { - if (!selectedCredentialId || !folderId || provider === 'outlook') return null + if (!selectedCredentialId || !folderId) return null setIsLoadingSelectedFolder(true) try { - // Construct query parameters + if (provider === 'outlook') { + // Resolve Outlook folder name with owner-scoped token + const tokenRes = await fetch('/api/auth/oauth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credentialId: selectedCredentialId, workflowId }), + }) + if (!tokenRes.ok) return null + const { accessToken } = await tokenRes.json() + if (!accessToken) return null + const resp = await fetch( + `https://graph.microsoft.com/v1.0/me/mailFolders/${encodeURIComponent(folderId)}`, + { headers: { Authorization: `Bearer ${accessToken}` } } + ) + if (!resp.ok) return null + const folder = await resp.json() + const folderInfo: FolderInfo = { + id: folder.id, + name: folder.displayName, + type: 'folder', + messagesTotal: folder.totalItemCount, + messagesUnread: folder.unreadItemCount, + } + setSelectedFolder(folderInfo) + onFolderInfoChange?.(folderInfo) + return folderInfo + } + // Gmail label resolution const queryParams = new URLSearchParams({ credentialId: selectedCredentialId, labelId: folderId, }) - const response = await fetch(`/api/tools/gmail/label?${queryParams.toString()}`) - if (response.ok) { const data = await response.json() if (data.label) { @@ -156,7 +196,7 @@ export function FolderSelector({ setIsLoadingSelectedFolder(false) } }, - [selectedCredentialId, onFolderInfoChange, provider] + [selectedCredentialId, onFolderInfoChange, provider, workflowId] ) // Fetch folders from Gmail or Outlook @@ -178,6 +218,12 @@ export function FolderSelector({ // Determine the API endpoint based on provider let apiEndpoint: string if (provider === 'outlook') { + // Skip list fetch for collaborators; only show selected + if (isForeignCredential) { + setFolders([]) + setIsLoading(false) + return + } apiEndpoint = `/api/tools/outlook/folders?${queryParams.toString()}` } else { // Default to Gmail @@ -206,9 +252,12 @@ export function FolderSelector({ } } } else { - logger.error('Error fetching folders:', { - error: await response.text(), - }) + const text = await response.text() + if (response.status === 401 || response.status === 403) { + logger.info('Folder list fetch unauthorized (expected for collaborator)') + } else { + logger.warn('Error fetching folders', { status: response.status, text }) + } setFolders([]) } } catch (error) { @@ -218,7 +267,14 @@ export function FolderSelector({ setIsLoading(false) } }, - [selectedCredentialId, selectedFolderId, onFolderInfoChange, fetchFolderById, provider] + [ + selectedCredentialId, + selectedFolderId, + onFolderInfoChange, + fetchFolderById, + provider, + isForeignCredential, + ] ) // Fetch credentials on initial mount @@ -244,21 +300,17 @@ export function FolderSelector({ } }, [value, isPreview, previewValue]) - // Fetch the selected folder metadata once credentials are ready (Gmail only) + // Fetch the selected folder metadata once credentials are ready or value changes useEffect(() => { - const currentValue = isPreview ? previewValue : value - if (currentValue && selectedCredentialId && !selectedFolder && provider !== 'outlook') { + const currentValue = isPreview ? (previewValue as string) : (value as string) + if ( + currentValue && + selectedCredentialId && + (!selectedFolder || selectedFolder.id !== currentValue) + ) { fetchFolderById(currentValue) } - }, [ - value, - selectedCredentialId, - selectedFolder, - fetchFolderById, - provider, - isPreview, - previewValue, - ]) + }, [value, selectedCredentialId, selectedFolder, fetchFolderById, isPreview, previewValue]) // Handle folder selection const handleSelectFolder = (folder: FolderInfo) => { @@ -317,7 +369,7 @@ export function FolderSelector({ role='combobox' aria-expanded={open} className='w-full justify-between' - disabled={disabled} + disabled={disabled || isForeignCredential} > {selectedFolder ? (
@@ -333,114 +385,120 @@ export function FolderSelector({ - - {/* Current account indicator */} - {selectedCredentialId && credentials.length > 0 && ( -
-
- - {credentials.find((cred) => cred.id === selectedCredentialId)?.name || - 'Unknown'} - -
- {credentials.length > 1 && ( - - )} -
- )} - - - - - - {isLoading ? ( -
- - Loading {getFolderLabel()}... -
- ) : credentials.length === 0 ? ( -
-

No accounts connected.

-

- Connect a {getProviderName()} account to continue. -

-
- ) : ( -
-

No {getFolderLabel()} found.

-

- Try a different search or account. -

-
+ {!isForeignCredential && ( + + {/* Current account indicator */} + {selectedCredentialId && credentials.length > 0 && ( +
+
+ + {credentials.find((cred) => cred.id === selectedCredentialId)?.name || + 'Unknown'} + +
+ {credentials.length > 1 && ( + )} - +
+ )} - {/* Account selection - only show if we have multiple accounts */} - {credentials.length > 1 && ( - -
- Switch Account -
- {credentials.map((cred) => ( - setSelectedCredentialId(cred.id)} - > -
- {cred.name} -
- {cred.id === selectedCredentialId && } -
- ))} -
- )} - - {/* Folders list */} - {folders.length > 0 && ( - -
- {getFolderLabel().charAt(0).toUpperCase() + getFolderLabel().slice(1)} -
- {folders.map((folder) => ( - handleSelectFolder(folder)} - > -
- {getFolderIcon('sm')} - {folder.name} - {folder.id === selectedFolderId && } -
-
- ))} -
- )} - - {/* Connect account option - only show if no credentials */} - {credentials.length === 0 && ( - - -
- Connect {getProviderName()} account + + + + + {isLoading ? ( +
+ + Loading {getFolderLabel()}...
- - - )} -
-
- + ) : credentials.length === 0 ? ( +
+

No accounts connected.

+

+ Connect a {getProviderName()} account to continue. +

+
+ ) : ( +
+

No {getFolderLabel()} found.

+

+ Try a different search or account. +

+
+ )} + + + {/* Account selection - only show if we have multiple accounts */} + {credentials.length > 1 && ( + +
+ Switch Account +
+ {credentials.map((cred) => ( + setSelectedCredentialId(cred.id)} + > +
+ {cred.name} +
+ {cred.id === selectedCredentialId && ( + + )} +
+ ))} +
+ )} + + {/* Folders list */} + {folders.length > 0 && ( + +
+ {getFolderLabel().charAt(0).toUpperCase() + getFolderLabel().slice(1)} +
+ {folders.map((folder) => ( + handleSelectFolder(folder)} + > +
+ {getFolderIcon('sm')} + {folder.name} + {folder.id === selectedFolderId && ( + + )} +
+
+ ))} +
+ )} + + {/* Connect account option - only show if no credentials */} + {credentials.length === 0 && ( + + +
+ Connect {getProviderName()} account +
+
+
+ )} + + + + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx index fb0ac63b8a..e52e4a6837 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx @@ -50,6 +50,7 @@ interface JiraProjectSelectorProps { onProjectInfoChange?: (projectInfo: JiraProjectInfo | null) => void credentialId?: string isForeignCredential?: boolean + workflowId?: string } export function JiraProjectSelector({ @@ -64,6 +65,8 @@ export function JiraProjectSelector({ showPreview = true, onProjectInfoChange, credentialId, + isForeignCredential = false, + workflowId, }: JiraProjectSelectorProps) { const [open, setOpen] = useState(false) const [credentials, setCredentials] = useState([]) @@ -153,6 +156,7 @@ export function JiraProjectSelector({ }, body: JSON.stringify({ credentialId: selectedCredentialId, + workflowId, }), }) @@ -238,6 +242,7 @@ export function JiraProjectSelector({ }, body: JSON.stringify({ credentialId: selectedCredentialId, + workflowId, }), }) @@ -334,16 +339,12 @@ export function JiraProjectSelector({ // Fetch the selected project metadata once credentials are ready or changed useEffect(() => { - if ( - value && - selectedCredentialId && - domain && - domain.includes('.') && - (!selectedProject || selectedProject.id !== value) - ) { - fetchProjectInfo(value) + if (value && selectedCredentialId && domain && domain.includes('.')) { + if (!selectedProject || selectedProject.id !== value) { + fetchProjectInfo(value) + } } - }, [value, selectedCredentialId, selectedProject, domain, fetchProjectInfo]) + }, [value, selectedCredentialId, domain, fetchProjectInfo, selectedProject]) // Keep internal selectedProjectId in sync with the value prop useEffect(() => { @@ -396,7 +397,7 @@ export function JiraProjectSelector({ role='combobox' aria-expanded={open} className='w-full justify-between' - disabled={disabled || !domain || !selectedCredentialId} + disabled={disabled || !domain || !selectedCredentialId || isForeignCredential} > {selectedProject ? (
@@ -417,126 +418,131 @@ export function JiraProjectSelector({ - - {/* Current account indicator */} - {selectedCredentialId && credentials.length > 0 && ( -
-
- - - {credentials.find((cred) => cred.id === selectedCredentialId)?.name || - 'Unknown'} - -
- {credentials.length > 1 && ( - - )} -
- )} - - - - - - {isLoading ? ( -
- - Loading projects... -
- ) : error ? ( -
-

{error}

-
- ) : credentials.length === 0 ? ( -
-

No accounts connected.

-

- Connect a Jira account to continue. -

-
- ) : ( -
-

No projects found.

-

- Try a different search or account. -

-
+ {!isForeignCredential && ( + + {selectedCredentialId && credentials.length > 0 && ( +
+
+ + + {credentials.find((cred) => cred.id === selectedCredentialId)?.name || + 'Unknown'} + +
+ {credentials.length > 1 && ( + )} - +
+ )} - {/* Account selection - only show if we have multiple accounts */} - {credentials.length > 1 && ( - -
- Switch Account -
- {credentials.map((cred) => ( - setSelectedCredentialId(cred.id)} - > -
- - {cred.name} -
- {cred.id === selectedCredentialId && } -
- ))} -
- )} - - {/* Projects list */} - {projects.length > 0 && ( - -
- Projects -
- {projects.map((project) => ( - handleSelectProject(project)} - > -
- {project.avatarUrl ? ( - {project.name} - ) : ( - - )} - {project.name} -
- {project.id === selectedProjectId && } -
- ))} -
- )} - - {/* Connect account option - only show if no credentials */} - {credentials.length === 0 && ( - - -
- - Connect Jira account + + + + + {isLoading ? ( +
+ + Loading projects...
- - - )} -
-
- + ) : error ? ( +
+

{error}

+
+ ) : credentials.length === 0 ? ( +
+

No accounts connected.

+

+ Connect a Jira account to continue. +

+
+ ) : ( +
+

No projects found.

+

+ Try a different search or account. +

+
+ )} + + + {/* Account selection - only show if we have multiple accounts */} + {credentials.length > 1 && ( + +
+ Switch Account +
+ {credentials.map((cred) => ( + setSelectedCredentialId(cred.id)} + > +
+ + {cred.name} +
+ {cred.id === selectedCredentialId && ( + + )} +
+ ))} +
+ )} + + {/* Projects list */} + {projects.length > 0 && ( + +
+ Projects +
+ {projects.map((project) => ( + handleSelectProject(project)} + > +
+ {project.avatarUrl ? ( + {project.name} + ) : ( + + )} + {project.name} +
+ {project.id === selectedProjectId && ( + + )} +
+ ))} +
+ )} + + {/* Connect account option - only show if no credentials */} + {credentials.length === 0 && ( + + +
+ + Connect Jira account +
+
+
+ )} + + + + )} {/* Project preview */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx index 15545a757c..cd44cb9aea 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx @@ -18,6 +18,7 @@ import { type LinearTeamInfo, LinearTeamSelector, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector' +import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-foreign-credential' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' @@ -43,10 +44,13 @@ export function ProjectSelectorInput({ const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const [selectedProjectId, setSelectedProjectId] = useState('') const [_projectInfo, setProjectInfo] = useState(null) - const [isForeignCredential, setIsForeignCredential] = useState(false) - // Use the proper hook to get the current value and setter const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) + const [connectedCredential] = useSubBlockValue(blockId, 'credential') + const { isForeignCredential } = useForeignCredential( + subBlock.provider || subBlock.serviceId || 'jira', + (connectedCredential as string) || '' + ) // Local setters for related Jira fields to ensure immediate UI clearing const [_issueKeyValue, setIssueKeyValue] = useSubBlockValue(blockId, 'issueKey') const [_manualIssueKeyValue, setManualIssueKeyValue] = useSubBlockValue( @@ -70,32 +74,6 @@ export function ProjectSelectorInput({ const botToken = '' // Verify Jira credential belongs to current user; if not, treat as absent - useEffect(() => { - const cred = (jiraCredential as string) || '' - if (!cred) { - setIsForeignCredential(false) - return - } - let aborted = false - ;(async () => { - try { - const resp = await fetch(`/api/auth/oauth/credentials?credentialId=${cred}`) - if (aborted) return - if (!resp.ok) { - setIsForeignCredential(true) - return - } - const data = await resp.json() - setIsForeignCredential(!(data.credentials && data.credentials.length === 1)) - } catch { - setIsForeignCredential(true) - } - })() - return () => { - aborted = true - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [blockId, jiraCredential]) // Get the current value from the store or prop value if in preview mode useEffect(() => { @@ -240,6 +218,7 @@ export function ProjectSelectorInput({ onProjectInfoChange={setProjectInfo} credentialId={(jiraCredential as string) || ''} isForeignCredential={isForeignCredential} + workflowId={activeWorkflowId || ''} />
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-foreign-credential.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-foreign-credential.ts new file mode 100644 index 0000000000..39474efc54 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-foreign-credential.ts @@ -0,0 +1,50 @@ +import { useEffect, useMemo, useState } from 'react' + +export function useForeignCredential( + provider: string | undefined, + credentialId: string | undefined +) { + const [isForeign, setIsForeign] = useState(false) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const normalizedProvider = useMemo(() => (provider || '').toString(), [provider]) + const normalizedCredentialId = useMemo(() => credentialId || '', [credentialId]) + + useEffect(() => { + let cancelled = false + async function check() { + setLoading(true) + setError(null) + try { + if (!normalizedCredentialId) { + if (!cancelled) setIsForeign(false) + return + } + const res = await fetch( + `/api/auth/oauth/credentials?provider=${encodeURIComponent(normalizedProvider)}` + ) + if (!res.ok) { + if (!cancelled) setIsForeign(true) + return + } + const data = await res.json() + const isOwn = (data.credentials || []).some((c: any) => c.id === normalizedCredentialId) + if (!cancelled) setIsForeign(!isOwn) + } catch (e) { + if (!cancelled) { + setIsForeign(true) + setError((e as Error).message) + } + } finally { + if (!cancelled) setLoading(false) + } + } + void check() + return () => { + cancelled = true + } + }, [normalizedProvider, normalizedCredentialId]) + + return { isForeignCredential: isForeign, loading, error } +} diff --git a/apps/sim/tools/google_sheets/read.ts b/apps/sim/tools/google_sheets/read.ts index 073cf17b67..0e2f982345 100644 --- a/apps/sim/tools/google_sheets/read.ts +++ b/apps/sim/tools/google_sheets/read.ts @@ -42,9 +42,12 @@ export const readTool: ToolConfig