diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/channel-selector/components/slack-channel-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/channel-selector/components/slack-channel-selector.tsx index faad342cba..56190fb6cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/channel-selector/components/slack-channel-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/channel-selector/components/slack-channel-selector.tsx @@ -147,10 +147,7 @@ export function SlackChannelSelector({
{cachedChannelName ? ( - <> - - {cachedChannelName} - + {cachedChannelName} ) : ( {label} )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/confluence-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/confluence-file-selector.tsx index dc496d7e50..edc34632a4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/confluence-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/confluence-file-selector.tsx @@ -210,21 +210,23 @@ export function ConfluenceFileSelector({ } const data = await response.json() - 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) + const fileInfo: ConfluenceFileInfo = { + id: data.id || pageId, + name: data.title || `Page ${pageId}`, + mimeType: 'confluence/page', + webViewLink: `https://${domain}/wiki/pages/${data.id}`, + modifiedTime: data.version?.when, + spaceId: data.spaceId, + url: `https://${domain}/wiki/pages/${data.id}`, + } + setSelectedFile(fileInfo) + onFileInfoChange?.(fileInfo) + + // Cache the page name in display names store + if (selectedCredentialId) { + useDisplayNamesStore + .getState() + .setDisplayNames('files', selectedCredentialId, { [fileInfo.id]: fileInfo.name }) } } catch (error) { logger.error('Error fetching page info:', error) @@ -394,6 +396,13 @@ export function ConfluenceFileSelector({ } }, [value, onFileInfoChange]) + // Fetch page info on mount if we have a value but no selectedFile state + useEffect(() => { + if (value && selectedCredentialId && domain && !selectedFile) { + fetchPageInfo(value) + } + }, [value, selectedCredentialId, domain, selectedFile, fetchPageInfo]) + // Handle file selection const handleSelectFile = (file: ConfluenceFileInfo) => { setSelectedFileId(file.id) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/jira-issue-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/jira-issue-selector.tsx index 534e2a8189..d7e2b26f3b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/jira-issue-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/jira-issue-selector.tsx @@ -435,6 +435,13 @@ export function JiraIssueSelector({ } }, [value, onIssueInfoChange]) + // Fetch issue info on mount if we have a value but no selectedIssue state + useEffect(() => { + if (value && selectedCredentialId && domain && projectId && !selectedIssue) { + fetchIssueInfo(value) + } + }, [value, selectedCredentialId, domain, projectId, selectedIssue, fetchIssueInfo]) + // Handle issue selection const handleSelectIssue = (issue: JiraIssueInfo) => { setSelectedIssueId(issue.id) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/teams-message-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/teams-message-selector.tsx index 75e2d5713e..5e233911b7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/teams-message-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/teams-message-selector.tsx @@ -84,6 +84,7 @@ export function TeamsMessageSelector({ const initialFetchRef = useRef(false) const [error, setError] = useState(null) const [selectionStage, setSelectionStage] = useState<'team' | 'channel' | 'chat'>(selectionType) + const lastRestoredValueRef = useRef(null) // Get cached display name const cachedMessageName = useDisplayNamesStore( @@ -240,6 +241,18 @@ export function TeamsMessageSelector({ setChannels(channelsData) + // Cache channel names in display names store + if (selectedCredentialId && channelsData.length > 0) { + const channelMap = channelsData.reduce( + (acc: Record, channel: TeamsMessageInfo) => { + acc[channel.channelId!] = channel.displayName + return acc + }, + {} + ) + useDisplayNamesStore.getState().setDisplayNames('files', selectedCredentialId, channelMap) + } + // If we have a selected channel ID, find it in the list if (selectedChannelId) { const channel = channelsData.find( @@ -304,6 +317,14 @@ export function TeamsMessageSelector({ setChats(chatsData) + if (selectedCredentialId && chatsData.length > 0) { + const chatMap = chatsData.reduce((acc: Record, chat: TeamsMessageInfo) => { + acc[chat.id] = chat.displayName + return acc + }, {}) + useDisplayNamesStore.getState().setDisplayNames('files', selectedCredentialId, chatMap) + } + // If we have a selected chat ID, find it in the list if (selectedChatId) { const chat = chatsData.find((c: TeamsMessageInfo) => c.chatId === selectedChatId) @@ -547,6 +568,19 @@ export function TeamsMessageSelector({ if (response.ok) { const data = await response.json() + + // Cache all chat names + if (data.chats && selectedCredentialId) { + const chatMap = data.chats.reduce( + (acc: Record, c: { id: string; displayName: string }) => { + acc[c.id] = c.displayName + return acc + }, + {} + ) + useDisplayNamesStore.getState().setDisplayNames('files', selectedCredentialId, chatMap) + } + const chat = data.chats.find((c: { id: string; displayName: string }) => c.id === chatId) if (chat) { const chatInfo: TeamsMessageInfo = { @@ -691,14 +725,20 @@ export function TeamsMessageSelector({ // Restore selection whenever the canonical value changes useEffect(() => { if (value && selectedCredentialId) { - if (selectionType === 'team') { - restoreTeamSelection(value) - } else if (selectionType === 'chat') { - restoreChatSelection(value) - } else if (selectionType === 'channel') { - restoreChannelSelection(value) + // Only restore if we haven't already restored this value + if (lastRestoredValueRef.current !== value) { + lastRestoredValueRef.current = value + + if (selectionType === 'team') { + restoreTeamSelection(value) + } else if (selectionType === 'chat') { + restoreChatSelection(value) + } else if (selectionType === 'channel') { + restoreChannelSelection(value) + } } } else { + lastRestoredValueRef.current = null setSelectedMessage(null) } }, [ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/wealthbox-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/wealthbox-file-selector.tsx index 3eded742f9..49ce0b9e2e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/wealthbox-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/file-selector/components/wealthbox-file-selector.tsx @@ -194,6 +194,14 @@ export function WealthboxFileSelector({ if (data.item) { setSelectedItem(data.item) onFileInfoChange?.(data.item) + + // Cache the item name in display names store + if (selectedCredentialId) { + useDisplayNamesStore + .getState() + .setDisplayNames('files', selectedCredentialId, { [data.item.id]: data.item.name }) + } + return data.item } } else { @@ -233,7 +241,20 @@ export function WealthboxFileSelector({ } }, [selectedCredentialId, open, fetchAvailableItems]) - // Fetch the selected item metadata only once when needed + // Fetch item info on mount if we have a value but no selectedItem state + useEffect(() => { + if (value && selectedCredentialId && !selectedItem) { + fetchItemById(value) + } + }, [value, selectedCredentialId, selectedItem, fetchItemById]) + + // Clear selectedItem when value is cleared + useEffect(() => { + if (!value) { + setSelectedItem(null) + onFileInfoChange?.(null) + } + }, [value, onFileInfoChange]) // Handle search input changes with debouncing const handleSearchChange = useCallback( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/project-selector/components/jira-project-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/project-selector/components/jira-project-selector.tsx index 0640ab9778..1f795eee50 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/project-selector/components/jira-project-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/project-selector/components/jira-project-selector.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Check, ChevronDown, RefreshCw } from 'lucide-react' +import { Check, ChevronDown, ExternalLink, RefreshCw, X } from 'lucide-react' import { JiraIcon } from '@/components/icons' import { Button } from '@/components/ui/button' import { @@ -74,6 +74,7 @@ export function JiraProjectSelector({ const [projects, setProjects] = useState([]) const [selectedCredentialId, setSelectedCredentialId] = useState(credentialId || '') const [selectedProjectId, setSelectedProjectId] = useState(value) + const [selectedProject, setSelectedProject] = useState(null) const [isLoading, setIsLoading] = useState(false) const [showOAuthModal, setShowOAuthModal] = useState(false) const initialFetchRef = useRef(false) @@ -210,8 +211,10 @@ export function JiraProjectSelector({ } if (projectInfo) { + setSelectedProject(projectInfo) onProjectInfoChange?.(projectInfo) } else { + setSelectedProject(null) onProjectInfoChange?.(null) } } catch (error) { @@ -322,6 +325,7 @@ export function JiraProjectSelector({ (project: JiraProjectInfo) => project.id === selectedProjectId ) if (projectInfo) { + setSelectedProject(projectInfo) onProjectInfoChange?.(projectInfo) } else if (!searchQuery && selectedProjectId) { // If we can't find the project in the list, try to fetch it directly @@ -370,10 +374,18 @@ export function JiraProjectSelector({ // Clear callback when value is cleared useEffect(() => { if (!value) { + setSelectedProject(null) onProjectInfoChange?.(null) } }, [value, onProjectInfoChange]) + // Fetch project info on mount if we have a value but no selectedProject state + useEffect(() => { + if (value && selectedCredentialId && domain && !selectedProject) { + fetchProjectInfo(value) + } + }, [value, selectedCredentialId, domain, selectedProject, fetchProjectInfo]) + // Handle open change const handleOpenChange = (isOpen: boolean) => { setOpen(isOpen) @@ -386,6 +398,7 @@ export function JiraProjectSelector({ // Handle project selection const handleSelectProject = (project: JiraProjectInfo) => { setSelectedProjectId(project.id) + setSelectedProject(project) onChange(project.id, project) onProjectInfoChange?.(project) setOpen(false) @@ -401,6 +414,7 @@ export function JiraProjectSelector({ // Clear selection const handleClearSelection = () => { setSelectedProjectId('') + setSelectedProject(null) setError(null) onChange('', undefined) onProjectInfoChange?.(null) @@ -558,6 +572,55 @@ export function JiraProjectSelector({ )} + + {/* Project preview */} + {showPreview && selectedProject && ( +
+
+ +
+
+
+ {selectedProject.avatarUrl ? ( + {selectedProject.name} + ) : ( + + )} +
+
+
+

{selectedProject.name}

+ + {selectedProject.key} + +
+ {selectedProject.url ? ( + e.stopPropagation()} + > + Open in Jira + + + ) : null} +
+
+
+ )}
{showOAuthModal && ( diff --git a/apps/sim/hooks/use-display-name.ts b/apps/sim/hooks/use-display-name.ts index fbe1eb0774..b4f49b3a11 100644 --- a/apps/sim/hooks/use-display-name.ts +++ b/apps/sim/hooks/use-display-name.ts @@ -234,23 +234,32 @@ export function useDisplayName( const projectContext = `${context.provider}-${context.credentialId}` setIsFetching(true) - if (context.provider === 'jira' && context.domain) { - fetch('/api/tools/jira/projects', { + if (context.provider === 'jira' && context.domain && context.credentialId) { + // Fetch access token then get project info + fetch('/api/auth/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ credentialId: context.credentialId, domain: context.domain }), + body: JSON.stringify({ credentialId: context.credentialId }), }) + .then((res) => res.json()) + .then((tokenData) => { + if (!tokenData.accessToken) throw new Error('No access token') + return fetch('/api/tools/jira/projects', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + domain: context.domain, + accessToken: tokenData.accessToken, + projectId: value, + }), + }) + }) .then((res) => res.json()) .then((data) => { - if (data.projects) { - const projectMap = data.projects.reduce( - (acc: Record, proj: { id: string; name: string }) => { - acc[proj.id] = proj.name - return acc - }, - {} - ) - useDisplayNamesStore.getState().setDisplayNames('projects', projectContext, projectMap) + if (data.project) { + useDisplayNamesStore + .getState() + .setDisplayNames('projects', projectContext, { [value as string]: data.project.name }) } }) .catch(() => {}) @@ -286,8 +295,6 @@ export function useDisplayName( context?.provider, context?.domain, context?.teamId, - cachedDisplayName, - isFetching, ]) // Auto-fetch files if needed (provider-specific) @@ -321,63 +328,75 @@ export function useDisplayName( .finally(() => setIsFetching(false)) } // Jira issues - else if (provider === 'jira' && context.domain && context.projectId) { - fetch('/api/tools/jira/issues', { + else if (provider === 'jira' && context.domain && context.projectId && context.credentialId) { + // Fetch access token then get issue info + fetch('/api/auth/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - credentialId: context.credentialId, - domain: context.domain, - projectId: context.projectId, - }), + body: JSON.stringify({ credentialId: context.credentialId }), }) + .then((res) => res.json()) + .then((tokenData) => { + if (!tokenData.accessToken) throw new Error('No access token') + return fetch('/api/tools/jira/issues', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + domain: context.domain, + accessToken: tokenData.accessToken, + issueKeys: [value], + }), + }) + }) .then((res) => res.json()) .then((data) => { - if (data.issues) { - const issueMap = data.issues.reduce( - (acc: Record, issue: { id: string; name: string }) => { - acc[issue.id] = issue.name - return acc - }, - {} - ) - useDisplayNamesStore - .getState() - .setDisplayNames('files', context.credentialId!, issueMap) + if (data.issues?.[0]) { + useDisplayNamesStore.getState().setDisplayNames('files', context.credentialId!, { + [value as string]: data.issues[0].name, + }) } }) .catch(() => {}) .finally(() => setIsFetching(false)) } // Confluence pages - else if (provider === 'confluence' && context.domain) { - fetch('/api/tools/confluence/pages', { + else if (provider === 'confluence' && context.domain && context.credentialId) { + // Fetch access token then get page info + fetch('/api/auth/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ credentialId: context.credentialId, domain: context.domain }), + body: JSON.stringify({ credentialId: context.credentialId }), }) + .then((res) => res.json()) + .then((tokenData) => { + if (!tokenData.accessToken) throw new Error('No access token') + return fetch('/api/tools/confluence/page', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + domain: context.domain, + accessToken: tokenData.accessToken, + pageId: value, + }), + }) + }) .then((res) => res.json()) .then((data) => { - if (data.files) { - const fileMap = data.files.reduce( - (acc: Record, file: { id: string; name: string }) => { - acc[file.id] = file.name - return acc - }, - {} - ) - useDisplayNamesStore.getState().setDisplayNames('files', context.credentialId!, fileMap) + if (data.id && data.title) { + useDisplayNamesStore.getState().setDisplayNames('files', context.credentialId!, { + [data.id]: data.title, + }) } }) .catch(() => {}) .finally(() => setIsFetching(false)) } // Microsoft Teams - else if (provider === 'microsoft-teams' && context.teamId) { - fetch('/api/tools/microsoft_teams/teams', { + else if (provider === 'microsoft-teams' && context.credentialId) { + fetch('/api/tools/microsoft-teams/teams', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ credentialId: context.credentialId }), + body: JSON.stringify({ credential: context.credentialId }), }) .then((res) => res.json()) .then((data) => { @@ -396,18 +415,14 @@ export function useDisplayName( .finally(() => setIsFetching(false)) } // Wealthbox - else if (provider === 'wealthbox') { - fetch('/api/tools/wealthbox/contacts', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ credentialId: context.credentialId }), - }) + else if (provider === 'wealthbox' && context.credentialId) { + fetch(`/api/tools/wealthbox/items?credentialId=${context.credentialId}&type=contact`) .then((res) => res.json()) .then((data) => { - if (data.contacts) { - const contactMap = data.contacts.reduce( - (acc: Record, contact: { id: string; name: string }) => { - acc[contact.id] = contact.name + if (data.items) { + const contactMap = data.items.reduce( + (acc: Record, item: { id: string; name: string }) => { + acc[item.id] = item.name return acc }, {} @@ -553,8 +568,6 @@ export function useDisplayName( context?.projectId, context?.teamId, context?.planId, - cachedDisplayName, - isFetching, ]) if (!subBlock || !value || typeof value !== 'string') {