mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(display-names): move display name mapping to central store + access pattern via hook (#1881)
* improvmenet(display-names-store): add displaynames store to map ids to human readable names for creds, etc * fix workflow in workflow * dot protection for secrets * hide from preview certain fields * fix rest of cases * fix confluence * fix type errors * remove redundant workflow dropdown code * remove comments * revert preview card * fix [object Object] bug * fix lint
This commit is contained in:
committed by
GitHub
parent
991b0e31ad
commit
82731850b2
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Check, ChevronDown, Hash, Lock, RefreshCw } from 'lucide-react'
|
||||
import { SlackIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
export interface SlackChannelInfo {
|
||||
id: string
|
||||
@@ -41,9 +42,19 @@ export function SlackChannelSelector({
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedChannel, setSelectedChannel] = useState<SlackChannelInfo | null>(null)
|
||||
const [initialFetchDone, setInitialFetchDone] = useState(false)
|
||||
|
||||
// Get cached display name
|
||||
const cachedChannelName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!credential || !value) return null
|
||||
return state.cache.channels[credential]?.[value] || null
|
||||
},
|
||||
[credential, value]
|
||||
)
|
||||
)
|
||||
|
||||
// Fetch channels from Slack API
|
||||
const fetchChannels = useCallback(async () => {
|
||||
if (!credential) return
|
||||
@@ -76,6 +87,18 @@ export function SlackChannelSelector({
|
||||
} else {
|
||||
setChannels(data.channels)
|
||||
setInitialFetchDone(true)
|
||||
|
||||
// Cache channel names in display names store
|
||||
if (credential) {
|
||||
const channelMap = data.channels.reduce(
|
||||
(acc: Record<string, string>, ch: SlackChannelInfo) => {
|
||||
acc[ch.id] = `#${ch.name}`
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('channels', credential, channelMap)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if ((err as Error).name === 'AbortError') return
|
||||
@@ -97,27 +120,7 @@ export function SlackChannelSelector({
|
||||
}
|
||||
}
|
||||
|
||||
// Sync selected channel with value prop
|
||||
useEffect(() => {
|
||||
if (value && channels.length > 0) {
|
||||
const channelInfo = channels.find((c) => c.id === value)
|
||||
setSelectedChannel(channelInfo || null)
|
||||
} else if (!value) {
|
||||
setSelectedChannel(null)
|
||||
}
|
||||
}, [value, channels])
|
||||
|
||||
// If we have a value but no channel info and haven't fetched yet, get just that channel
|
||||
useEffect(() => {
|
||||
if (value && !selectedChannel && !loading && !initialFetchDone && credential) {
|
||||
// For now, we'll fetch all channels when needed
|
||||
// In the future, we could optimize to fetch just the selected channel
|
||||
fetchChannels()
|
||||
}
|
||||
}, [value, selectedChannel, loading, initialFetchDone, credential, fetchChannels])
|
||||
|
||||
const handleSelectChannel = (channel: SlackChannelInfo) => {
|
||||
setSelectedChannel(channel)
|
||||
onChange(channel.id, channel)
|
||||
setOpen(false)
|
||||
}
|
||||
@@ -143,15 +146,10 @@ export function SlackChannelSelector({
|
||||
>
|
||||
<div className='flex max-w-[calc(100%-20px)] items-center gap-2 overflow-hidden'>
|
||||
<SlackIcon className='h-4 w-4 text-[#611f69]' />
|
||||
{selectedChannel ? (
|
||||
<>
|
||||
{getChannelIcon(selectedChannel)}
|
||||
<span className='truncate font-normal'>{formatChannelName(selectedChannel)}</span>
|
||||
</>
|
||||
) : value ? (
|
||||
{cachedChannelName ? (
|
||||
<>
|
||||
<Hash className='h-1.5 w-1.5' />
|
||||
<span className='truncate font-normal'>{value}</span>
|
||||
<span className='truncate font-normal'>{cachedChannelName}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className='truncate text-muted-foreground'>{label}</span>
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('CredentialSelector')
|
||||
@@ -116,6 +117,17 @@ export function CredentialSelector({
|
||||
setHasForeignMeta(foreignMetaFound)
|
||||
setCredentials(creds)
|
||||
|
||||
// Cache credential names in display names store
|
||||
if (effectiveProviderId) {
|
||||
const credentialMap = creds.reduce((acc: Record<string, string>, cred: Credential) => {
|
||||
acc[cred.id] = cred.name
|
||||
return acc
|
||||
}, {})
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('credentials', effectiveProviderId, credentialMap)
|
||||
}
|
||||
|
||||
// Do not auto-select or reset. We only show what's persisted.
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Check, ChevronDown, FileText, RefreshCw } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -15,24 +15,8 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
|
||||
interface DocumentData {
|
||||
id: string
|
||||
knowledgeBaseId: string
|
||||
filename: string
|
||||
fileUrl: string
|
||||
fileSize: number
|
||||
mimeType: string
|
||||
chunkCount: number
|
||||
tokenCount: number
|
||||
characterCount: number
|
||||
processingStatus: string
|
||||
processingStartedAt: Date | null
|
||||
processingCompletedAt: Date | null
|
||||
processingError: string | null
|
||||
enabled: boolean
|
||||
uploadedAt: Date
|
||||
}
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
import { type DocumentData, useKnowledgeStore } from '@/stores/knowledge/store'
|
||||
|
||||
interface DocumentSelectorProps {
|
||||
blockId: string
|
||||
@@ -51,110 +35,107 @@ export function DocumentSelector({
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
}: DocumentSelectorProps) {
|
||||
const [documents, setDocuments] = useState<DocumentData[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedDocument, setSelectedDocument] = useState<DocumentData | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Use the proper hook to get the current value and setter
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
|
||||
// Get the knowledge base ID from the same block's knowledgeBaseId subblock
|
||||
const [knowledgeBaseId] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const normalizedKnowledgeBaseId =
|
||||
typeof knowledgeBaseId === 'string' && knowledgeBaseId.trim().length > 0
|
||||
? knowledgeBaseId
|
||||
: null
|
||||
|
||||
const documentsCache = useKnowledgeStore(
|
||||
useCallback(
|
||||
(state) =>
|
||||
normalizedKnowledgeBaseId ? state.documents[normalizedKnowledgeBaseId] : undefined,
|
||||
[normalizedKnowledgeBaseId]
|
||||
)
|
||||
)
|
||||
|
||||
const isDocumentsLoading = useKnowledgeStore(
|
||||
useCallback(
|
||||
(state) =>
|
||||
normalizedKnowledgeBaseId ? state.isDocumentsLoading(normalizedKnowledgeBaseId) : false,
|
||||
[normalizedKnowledgeBaseId]
|
||||
)
|
||||
)
|
||||
|
||||
const getDocuments = useKnowledgeStore((state) => state.getDocuments)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
||||
const isDisabled = finalDisabled
|
||||
|
||||
// Fetch documents for the selected knowledge base
|
||||
const fetchDocuments = useCallback(async () => {
|
||||
if (!knowledgeBaseId) {
|
||||
setDocuments([])
|
||||
const documents = useMemo<DocumentData[]>(() => {
|
||||
if (!documentsCache) return []
|
||||
return documentsCache.documents ?? []
|
||||
}, [documentsCache])
|
||||
|
||||
const loadDocuments = useCallback(async () => {
|
||||
if (!normalizedKnowledgeBaseId) {
|
||||
setError('No knowledge base selected')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/knowledge/${knowledgeBaseId}/documents`)
|
||||
const fetchedDocuments = await getDocuments(normalizedKnowledgeBaseId)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch documents: ${response.statusText}`)
|
||||
if (fetchedDocuments.length > 0) {
|
||||
const documentMap = fetchedDocuments.reduce<Record<string, string>>((acc, doc) => {
|
||||
acc[doc.id] = doc.filename
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('documents', normalizedKnowledgeBaseId, documentMap)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch documents')
|
||||
}
|
||||
|
||||
const fetchedDocuments = result.data.documents || result.data || []
|
||||
setDocuments(fetchedDocuments)
|
||||
} catch (err) {
|
||||
if ((err as Error).name === 'AbortError') return
|
||||
setError((err as Error).message)
|
||||
setDocuments([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
if (err instanceof Error && err.name === 'AbortError') return
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch documents')
|
||||
}
|
||||
}, [knowledgeBaseId])
|
||||
}, [normalizedKnowledgeBaseId, getDocuments])
|
||||
|
||||
// Handle dropdown open/close - fetch documents when opening
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
if (isPreview) return
|
||||
if (isDisabled) return
|
||||
if (isPreview || isDisabled) return
|
||||
|
||||
setOpen(isOpen)
|
||||
|
||||
// Fetch fresh documents when opening the dropdown
|
||||
if (isOpen) {
|
||||
fetchDocuments()
|
||||
if (isOpen && (!documentsCache || !documentsCache.documents.length)) {
|
||||
void loadDocuments()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle document selection
|
||||
const handleSelectDocument = (document: DocumentData) => {
|
||||
if (isPreview) return
|
||||
|
||||
setSelectedDocument(document)
|
||||
setStoreValue(document.id)
|
||||
onDocumentSelect?.(document.id)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
// Sync selected document with value prop
|
||||
useEffect(() => {
|
||||
if (isDisabled) return
|
||||
if (value && documents.length > 0) {
|
||||
const docInfo = documents.find((doc) => doc.id === value)
|
||||
setSelectedDocument(docInfo || null)
|
||||
} else {
|
||||
setSelectedDocument(null)
|
||||
}
|
||||
}, [value, documents, isDisabled])
|
||||
|
||||
// Reset documents when knowledge base changes
|
||||
useEffect(() => {
|
||||
setDocuments([])
|
||||
setSelectedDocument(null)
|
||||
setError(null)
|
||||
}, [knowledgeBaseId])
|
||||
}, [normalizedKnowledgeBaseId])
|
||||
|
||||
// Fetch documents when knowledge base is available
|
||||
useEffect(() => {
|
||||
if (knowledgeBaseId && !isPreview && !isDisabled) {
|
||||
fetchDocuments()
|
||||
}
|
||||
}, [knowledgeBaseId, isPreview, isDisabled, fetchDocuments])
|
||||
if (!normalizedKnowledgeBaseId || documents.length === 0) return
|
||||
|
||||
const formatDocumentName = (document: DocumentData) => {
|
||||
return document.filename
|
||||
}
|
||||
const documentMap = documents.reduce<Record<string, string>>((acc, doc) => {
|
||||
acc[doc.id] = doc.filename
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('documents', normalizedKnowledgeBaseId as string, documentMap)
|
||||
}, [documents, normalizedKnowledgeBaseId])
|
||||
|
||||
const formatDocumentName = (document: DocumentData) => document.filename
|
||||
|
||||
const getDocumentDescription = (document: DocumentData) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
@@ -171,6 +152,18 @@ export function DocumentSelector({
|
||||
}
|
||||
|
||||
const label = subBlock.placeholder || 'Select document'
|
||||
const isLoading = isDocumentsLoading && !error
|
||||
|
||||
// Always use cached display name
|
||||
const displayName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!normalizedKnowledgeBaseId || !value || typeof value !== 'string') return null
|
||||
return state.cache.documents[normalizedKnowledgeBaseId]?.[value] || null
|
||||
},
|
||||
[normalizedKnowledgeBaseId, value]
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
@@ -185,8 +178,8 @@ export function DocumentSelector({
|
||||
>
|
||||
<div className='flex max-w-[calc(100%-20px)] items-center gap-2 overflow-hidden'>
|
||||
<FileText className='h-4 w-4 text-muted-foreground' />
|
||||
{selectedDocument ? (
|
||||
<span className='truncate font-normal'>{formatDocumentName(selectedDocument)}</span>
|
||||
{displayName ? (
|
||||
<span className='truncate font-normal'>{displayName}</span>
|
||||
) : (
|
||||
<span className='truncate text-muted-foreground'>{label}</span>
|
||||
)}
|
||||
@@ -199,7 +192,7 @@ export function DocumentSelector({
|
||||
<CommandInput placeholder='Search documents...' />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{loading ? (
|
||||
{isLoading ? (
|
||||
<div className='flex items-center justify-center p-4'>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='ml-2'>Loading documents...</span>
|
||||
@@ -208,7 +201,7 @@ export function DocumentSelector({
|
||||
<div className='p-4 text-center'>
|
||||
<p className='text-destructive text-sm'>{error}</p>
|
||||
</div>
|
||||
) : !knowledgeBaseId ? (
|
||||
) : !normalizedKnowledgeBaseId ? (
|
||||
<div className='p-4 text-center'>
|
||||
<p className='font-medium text-sm'>No knowledge base selected</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type OAuthProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
const logger = createLogger('ConfluenceFileSelector')
|
||||
|
||||
@@ -75,6 +76,18 @@ export function ConfluenceFileSelector({
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||
const initialFetchRef = useRef(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Get cached display name
|
||||
const cachedFileName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const effectiveCredentialId = credentialId || selectedCredentialId
|
||||
if (!effectiveCredentialId || !value) return null
|
||||
return state.cache.files[effectiveCredentialId]?.[value] || null
|
||||
},
|
||||
[credentialId, selectedCredentialId, value]
|
||||
)
|
||||
)
|
||||
// Keep internal credential in sync with prop (handles late arrival and BFCache restores)
|
||||
useEffect(() => {
|
||||
if (credentialId && credentialId !== selectedCredentialId) {
|
||||
@@ -306,7 +319,19 @@ export function ConfluenceFileSelector({
|
||||
logger.info(`Received ${data.files?.length || 0} files from API`)
|
||||
setFiles(data.files || [])
|
||||
|
||||
// If we have a selected file ID, find the file info
|
||||
// Cache file names in display names store
|
||||
if (selectedCredentialId && data.files) {
|
||||
const fileMap = data.files.reduce(
|
||||
(acc: Record<string, string>, file: ConfluenceFileInfo) => {
|
||||
acc[file.id] = file.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', selectedCredentialId, fileMap)
|
||||
}
|
||||
|
||||
// If we have a selected file ID, update state and notify parent
|
||||
if (selectedFileId) {
|
||||
const fileInfo = data.files.find((file: ConfluenceFileInfo) => file.id === selectedFileId)
|
||||
if (fileInfo) {
|
||||
@@ -354,21 +379,6 @@ export function ConfluenceFileSelector({
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the selected page metadata once credentials and domain are ready or changed
|
||||
useEffect(() => {
|
||||
if (value && selectedCredentialId && !selectedFile && domain && domain.includes('.')) {
|
||||
fetchPageInfo(value)
|
||||
}
|
||||
}, [
|
||||
value,
|
||||
selectedCredentialId,
|
||||
selectedFile,
|
||||
domain,
|
||||
fetchPageInfo,
|
||||
workflowId,
|
||||
isForeignCredential,
|
||||
])
|
||||
|
||||
// Keep internal selectedFileId in sync with the value prop
|
||||
useEffect(() => {
|
||||
if (value !== selectedFileId) {
|
||||
@@ -376,7 +386,7 @@ export function ConfluenceFileSelector({
|
||||
}
|
||||
}, [value])
|
||||
|
||||
// Clear preview when value is cleared (e.g., collaborator cleared or domain change cascade)
|
||||
// Clear callback when value is cleared
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setSelectedFile(null)
|
||||
@@ -403,7 +413,6 @@ export function ConfluenceFileSelector({
|
||||
// Clear selection
|
||||
const handleClearSelection = () => {
|
||||
setSelectedFileId('')
|
||||
setSelectedFile(null)
|
||||
onChange('', undefined)
|
||||
onFileInfoChange?.(null)
|
||||
}
|
||||
@@ -421,10 +430,10 @@ export function ConfluenceFileSelector({
|
||||
disabled={disabled || !domain || isForeignCredential}
|
||||
>
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedFile ? (
|
||||
{cachedFileName ? (
|
||||
<>
|
||||
<ConfluenceIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedFile.name}</span>
|
||||
<span className='truncate font-normal'>{cachedFileName}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -554,7 +563,6 @@ export function ConfluenceFileSelector({
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
{/* File preview */}
|
||||
{showPreview && selectedFile && selectedFileId && selectedFile.id === selectedFileId && (
|
||||
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
|
||||
<div className='absolute top-2 right-2'>
|
||||
@@ -580,7 +588,7 @@ export function ConfluenceFileSelector({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{selectedFile.webViewLink ? (
|
||||
{selectedFile.webViewLink && (
|
||||
<a
|
||||
href={selectedFile.webViewLink}
|
||||
target='_blank'
|
||||
@@ -591,8 +599,6 @@ export function ConfluenceFileSelector({
|
||||
<span>Open in Confluence</span>
|
||||
<ExternalLink className='h-3 w-3' />
|
||||
</a>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
const logger = createLogger('GoogleCalendarSelector')
|
||||
|
||||
@@ -56,6 +57,17 @@ export function GoogleCalendarSelector({
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [initialFetchDone, setInitialFetchDone] = useState(false)
|
||||
|
||||
// Get cached display name
|
||||
const cachedCalendarName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!credentialId || !value) return null
|
||||
return state.cache.files[credentialId]?.[value] || null
|
||||
},
|
||||
[credentialId, value]
|
||||
)
|
||||
)
|
||||
|
||||
const fetchCalendarsFromAPI = useCallback(async (): Promise<GoogleCalendarInfo[]> => {
|
||||
if (!credentialId) {
|
||||
throw new Error('Google Calendar account is required')
|
||||
@@ -87,15 +99,19 @@ export function GoogleCalendarSelector({
|
||||
const calendars = await fetchCalendarsFromAPI()
|
||||
setCalendars(calendars)
|
||||
|
||||
const currentSelectedId = selectedCalendarId
|
||||
if (currentSelectedId) {
|
||||
const calendarInfo = calendars.find(
|
||||
(calendar: GoogleCalendarInfo) => calendar.id === currentSelectedId
|
||||
)
|
||||
if (calendarInfo) {
|
||||
setSelectedCalendar(calendarInfo)
|
||||
onCalendarInfoChange?.(calendarInfo)
|
||||
}
|
||||
// Cache calendar names
|
||||
if (credentialId && calendars.length > 0) {
|
||||
const calendarMap = calendars.reduce<Record<string, string>>((acc, cal) => {
|
||||
acc[cal.id] = cal.summary
|
||||
return acc
|
||||
}, {})
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', credentialId, calendarMap)
|
||||
}
|
||||
|
||||
// Update selected calendar if we have a value
|
||||
if (selectedCalendarId && calendars.length > 0) {
|
||||
const calendar = calendars.find((c) => c.id === selectedCalendarId)
|
||||
setSelectedCalendar(calendar || null)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching calendars:', error)
|
||||
@@ -105,7 +121,7 @@ export function GoogleCalendarSelector({
|
||||
setIsLoading(false)
|
||||
setInitialFetchDone(true)
|
||||
}
|
||||
}, [fetchCalendarsFromAPI, selectedCalendarId, onCalendarInfoChange])
|
||||
}, [fetchCalendarsFromAPI, credentialId])
|
||||
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
setOpen(isOpen)
|
||||
@@ -115,67 +131,12 @@ export function GoogleCalendarSelector({
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSelectedCalendarInfo = useCallback(async () => {
|
||||
if (!selectedCalendarId) return
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const calendars = await fetchCalendarsFromAPI()
|
||||
|
||||
if (calendars.length > 0) {
|
||||
const calendarInfo = calendars.find(
|
||||
(calendar: GoogleCalendarInfo) => calendar.id === selectedCalendarId
|
||||
)
|
||||
if (calendarInfo) {
|
||||
setSelectedCalendar(calendarInfo)
|
||||
onCalendarInfoChange?.(calendarInfo)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching calendar info:', error)
|
||||
setError((error as Error).message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [fetchCalendarsFromAPI, selectedCalendarId, onCalendarInfoChange])
|
||||
|
||||
// Fetch selected calendar info when component mounts or dependencies change
|
||||
useEffect(() => {
|
||||
if (value && credentialId && (!selectedCalendar || selectedCalendar.id !== value)) {
|
||||
fetchSelectedCalendarInfo()
|
||||
}
|
||||
}, [value, credentialId, selectedCalendar, fetchSelectedCalendarInfo])
|
||||
|
||||
// Sync with external value
|
||||
// Sync selected ID with external value
|
||||
useEffect(() => {
|
||||
if (value !== selectedCalendarId) {
|
||||
setSelectedCalendarId(value)
|
||||
|
||||
// Find calendar info for the new value
|
||||
if (value && calendars.length > 0) {
|
||||
const calendarInfo = calendars.find((calendar) => calendar.id === value)
|
||||
setSelectedCalendar(calendarInfo || null)
|
||||
onCalendarInfoChange?.(calendarInfo || null)
|
||||
} else if (value) {
|
||||
// If we have a value but no calendar info, we might need to fetch it
|
||||
if (!selectedCalendar || selectedCalendar.id !== value) {
|
||||
fetchSelectedCalendarInfo()
|
||||
}
|
||||
} else {
|
||||
setSelectedCalendar(null)
|
||||
onCalendarInfoChange?.(null)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
value,
|
||||
calendars,
|
||||
selectedCalendarId,
|
||||
selectedCalendar,
|
||||
fetchSelectedCalendarInfo,
|
||||
onCalendarInfoChange,
|
||||
])
|
||||
}, [value, selectedCalendarId])
|
||||
|
||||
// Handle calendar selection
|
||||
const handleSelectCalendar = (calendar: GoogleCalendarInfo) => {
|
||||
@@ -189,7 +150,6 @@ export function GoogleCalendarSelector({
|
||||
// Clear selection
|
||||
const handleClearSelection = () => {
|
||||
setSelectedCalendarId('')
|
||||
setSelectedCalendar(null)
|
||||
onChange('', undefined)
|
||||
onCalendarInfoChange?.(null)
|
||||
setError(null)
|
||||
@@ -215,17 +175,10 @@ export function GoogleCalendarSelector({
|
||||
disabled={disabled || !credentialId}
|
||||
>
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedCalendar ? (
|
||||
{cachedCalendarName ? (
|
||||
<>
|
||||
<div
|
||||
className='h-3 w-3 flex-shrink-0 rounded-full'
|
||||
style={{
|
||||
backgroundColor: selectedCalendar.backgroundColor || '#4285f4',
|
||||
}}
|
||||
/>
|
||||
<span className='truncate font-normal'>
|
||||
{getCalendarDisplayName(selectedCalendar)}
|
||||
</span>
|
||||
<GoogleCalendarIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{cachedCalendarName}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -298,7 +251,6 @@ export function GoogleCalendarSelector({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Calendar preview */}
|
||||
{showPreview && selectedCalendar && (
|
||||
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
|
||||
<div className='absolute top-2 right-2'>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
parseProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
const logger = createLogger('GoogleDrivePicker')
|
||||
|
||||
@@ -100,6 +101,15 @@ export function GoogleDrivePicker({
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setCredentials(data.credentials)
|
||||
|
||||
const credentialMap = (data.credentials || []).reduce(
|
||||
(acc: Record<string, string>, cred: Credential) => {
|
||||
acc[cred.id] = cred.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('credentials', providerId, credentialMap)
|
||||
if (credentialId && !data.credentials.some((c: any) => c.id === credentialId)) {
|
||||
setSelectedCredentialId('')
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type OAuthProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
const logger = createLogger('JiraIssueSelector')
|
||||
|
||||
@@ -78,6 +79,18 @@ export function JiraIssueSelector({
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [cloudId, setCloudId] = useState<string | null>(null)
|
||||
|
||||
// Get cached display name
|
||||
const cachedIssueName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const effectiveCredentialId = credentialId || selectedCredentialId
|
||||
if (!effectiveCredentialId || !value) return null
|
||||
return state.cache.files[effectiveCredentialId]?.[value] || null
|
||||
},
|
||||
[credentialId, selectedCredentialId, value]
|
||||
)
|
||||
)
|
||||
|
||||
// Keep local credential state in sync with persisted credentialId prop
|
||||
useEffect(() => {
|
||||
if (credentialId && credentialId !== selectedCredentialId) {
|
||||
@@ -224,8 +237,6 @@ export function JiraIssueSelector({
|
||||
} catch (error) {
|
||||
logger.error('Error fetching issue info:', error)
|
||||
setError((error as Error).message)
|
||||
// Clear selection on error to prevent infinite retry loops
|
||||
setSelectedIssue(null)
|
||||
onIssueInfoChange?.(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
@@ -342,7 +353,19 @@ export function JiraIssueSelector({
|
||||
logger.info(`Received ${foundIssues.length} issues from API`)
|
||||
setIssues(foundIssues)
|
||||
|
||||
// If we have a selected issue ID, find the issue info
|
||||
// Cache issue names in display names store
|
||||
if (selectedCredentialId && foundIssues.length > 0) {
|
||||
const issueMap = foundIssues.reduce(
|
||||
(acc: Record<string, string>, issue: JiraIssueInfo) => {
|
||||
acc[issue.id] = issue.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', selectedCredentialId, issueMap)
|
||||
}
|
||||
|
||||
// If we have a selected issue ID, update state and notify parent
|
||||
if (selectedIssueId) {
|
||||
const issueInfo = foundIssues.find((issue: JiraIssueInfo) => issue.id === selectedIssueId)
|
||||
if (issueInfo) {
|
||||
@@ -397,18 +420,6 @@ export function JiraIssueSelector({
|
||||
}
|
||||
|
||||
// Fetch selected issue metadata once credentials are ready or changed
|
||||
useEffect(() => {
|
||||
if (
|
||||
value &&
|
||||
selectedCredentialId &&
|
||||
domain &&
|
||||
domain.includes('.') &&
|
||||
(!selectedIssue || selectedIssue.id !== value)
|
||||
) {
|
||||
fetchIssueInfo(value)
|
||||
}
|
||||
}, [value, selectedCredentialId, selectedIssue, domain, fetchIssueInfo])
|
||||
|
||||
// Keep internal selectedIssueId in sync with the value prop
|
||||
useEffect(() => {
|
||||
if (value !== selectedIssueId) {
|
||||
@@ -422,7 +433,7 @@ export function JiraIssueSelector({
|
||||
setError(null)
|
||||
onIssueInfoChange?.(null)
|
||||
}
|
||||
}, [value])
|
||||
}, [value, onIssueInfoChange])
|
||||
|
||||
// Handle issue selection
|
||||
const handleSelectIssue = (issue: JiraIssueInfo) => {
|
||||
@@ -443,8 +454,7 @@ export function JiraIssueSelector({
|
||||
// Clear selection
|
||||
const handleClearSelection = () => {
|
||||
setSelectedIssueId('')
|
||||
setSelectedIssue(null)
|
||||
setError(null) // Clear any existing errors
|
||||
setError(null)
|
||||
onChange('', undefined)
|
||||
onIssueInfoChange?.(null)
|
||||
}
|
||||
@@ -462,10 +472,10 @@ export function JiraIssueSelector({
|
||||
disabled={disabled || !domain || !selectedCredentialId || isForeignCredential}
|
||||
>
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedIssue ? (
|
||||
{cachedIssueName ? (
|
||||
<>
|
||||
<JiraIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedIssue.name}</span>
|
||||
<span className='truncate font-normal'>{cachedIssueName}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -595,7 +605,6 @@ export function JiraIssueSelector({
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
{/* Issue preview */}
|
||||
{showPreview && selectedIssue && (
|
||||
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
|
||||
<div className='absolute top-2 right-2'>
|
||||
@@ -621,7 +630,7 @@ export function JiraIssueSelector({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{selectedIssue.webViewLink ? (
|
||||
{selectedIssue.webViewLink && (
|
||||
<a
|
||||
href={selectedIssue.webViewLink}
|
||||
target='_blank'
|
||||
@@ -632,8 +641,6 @@ export function JiraIssueSelector({
|
||||
<span>Open in Jira</span>
|
||||
<ExternalLink className='h-3 w-3' />
|
||||
</a>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
parseProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
import type { PlannerTask } from '@/tools/microsoft_planner/types'
|
||||
|
||||
const logger = createLogger('MicrosoftFileSelector')
|
||||
@@ -90,14 +91,24 @@ export function MicrosoftFileSelector({
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||
const [credentialsLoaded, setCredentialsLoaded] = useState(false)
|
||||
const initialFetchRef = useRef(false)
|
||||
// Track the last (credentialId, fileId) we attempted to resolve to avoid tight retry loops
|
||||
const lastMetaAttemptRef = useRef<string>('')
|
||||
|
||||
// Handle Microsoft Planner task selection
|
||||
const [plannerTasks, setPlannerTasks] = useState<PlannerTask[]>([])
|
||||
const [isLoadingTasks, setIsLoadingTasks] = useState(false)
|
||||
const [selectedTask, setSelectedTask] = useState<PlannerTask | null>(null)
|
||||
|
||||
// Get cached display name
|
||||
const cachedFileName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const effectiveCredentialId = credentialId || selectedCredentialId
|
||||
if (!effectiveCredentialId || !value) return null
|
||||
return state.cache.files[effectiveCredentialId]?.[value] || null
|
||||
},
|
||||
[credentialId, selectedCredentialId, value]
|
||||
)
|
||||
)
|
||||
|
||||
// Determine the appropriate service ID based on provider and scopes
|
||||
const getServiceId = (): string => {
|
||||
if (serviceId) return serviceId
|
||||
@@ -179,6 +190,18 @@ export function MicrosoftFileSelector({
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setAvailableFiles(data.files || [])
|
||||
|
||||
// Cache file names in display names store
|
||||
if (selectedCredentialId && data.files) {
|
||||
const fileMap = data.files.reduce(
|
||||
(acc: Record<string, string>, file: MicrosoftFileInfo) => {
|
||||
acc[file.id] = file.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', selectedCredentialId, fileMap)
|
||||
}
|
||||
} else {
|
||||
const txt = await response.text()
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
@@ -472,11 +495,9 @@ 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
|
||||
setSelectedFile(taskAsFileInfo)
|
||||
onChange(taskId, taskAsFileInfo)
|
||||
onFileInfoChange?.(taskAsFileInfo)
|
||||
setOpen(false)
|
||||
@@ -500,76 +521,22 @@ export function MicrosoftFileSelector({
|
||||
|
||||
if (!selectedCredentialId) {
|
||||
// No credentials - clear everything
|
||||
if (selectedFile) {
|
||||
setSelectedFile(null)
|
||||
setSelectedFileId('')
|
||||
onChange('')
|
||||
}
|
||||
setSelectedFileId('')
|
||||
onChange('')
|
||||
// Reset memo when credential is cleared
|
||||
lastMetaAttemptRef.current = ''
|
||||
} else if (prevCredentialId && prevCredentialId !== selectedCredentialId) {
|
||||
// Credentials changed (not initial load) - clear file info to force refetch
|
||||
if (selectedFile) {
|
||||
setSelectedFile(null)
|
||||
}
|
||||
// Reset memo when switching credentials
|
||||
lastMetaAttemptRef.current = ''
|
||||
}
|
||||
}, [selectedCredentialId, selectedFile, onChange])
|
||||
}, [selectedCredentialId, onChange])
|
||||
|
||||
// Fetch the selected file metadata once credentials are loaded or changed
|
||||
// Keep internal selectedFileId in sync with the value prop
|
||||
useEffect(() => {
|
||||
// Fetch metadata when the external value doesn't match our current selectedFile
|
||||
if (
|
||||
value &&
|
||||
selectedCredentialId &&
|
||||
credentialsLoaded &&
|
||||
(!selectedFile || selectedFile.id !== value) &&
|
||||
!isLoadingSelectedFile
|
||||
) {
|
||||
// Avoid tight retry loops by memoizing the last attempt tuple
|
||||
const attemptKey = `${selectedCredentialId}::${value}`
|
||||
if (lastMetaAttemptRef.current === attemptKey) {
|
||||
return
|
||||
}
|
||||
lastMetaAttemptRef.current = attemptKey
|
||||
|
||||
if (serviceId === 'microsoft-planner') {
|
||||
void fetchPlannerTaskById(value)
|
||||
} else {
|
||||
void fetchFileById(value)
|
||||
}
|
||||
if (value !== selectedFileId) {
|
||||
setSelectedFileId(value)
|
||||
}
|
||||
}, [
|
||||
value,
|
||||
selectedCredentialId,
|
||||
credentialsLoaded,
|
||||
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,
|
||||
])
|
||||
}, [value, selectedFileId])
|
||||
|
||||
// Handle selecting a file from the available files
|
||||
const handleFileSelect = (file: MicrosoftFileInfo) => {
|
||||
@@ -578,7 +545,7 @@ export function MicrosoftFileSelector({
|
||||
onChange(file.id, file)
|
||||
onFileInfoChange?.(file)
|
||||
setOpen(false)
|
||||
setSearchQuery('') // Clear search when file is selected
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
// Handle adding a new credential
|
||||
@@ -593,6 +560,7 @@ export function MicrosoftFileSelector({
|
||||
const handleClearSelection = () => {
|
||||
setSelectedFileId('')
|
||||
setSelectedFile(null)
|
||||
setSelectedTask(null)
|
||||
onChange('', undefined)
|
||||
onFileInfoChange?.(null)
|
||||
}
|
||||
@@ -787,15 +755,10 @@ export function MicrosoftFileSelector({
|
||||
}
|
||||
>
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{canShowPreview ? (
|
||||
{cachedFileName ? (
|
||||
<>
|
||||
{getFileIcon(selectedFile, 'sm')}
|
||||
<span className='truncate font-normal'>{selectedFile.name}</span>
|
||||
</>
|
||||
) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? (
|
||||
<>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='truncate text-muted-foreground'>Loading document...</span>
|
||||
{getProviderIcon(provider)}
|
||||
<span className='truncate font-normal'>{cachedFileName}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -961,7 +924,6 @@ export function MicrosoftFileSelector({
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
{/* File preview */}
|
||||
{canShowPreview && (
|
||||
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
|
||||
<div className='absolute top-2 right-2'>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type OAuthProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
const logger = createLogger('TeamsMessageSelector')
|
||||
|
||||
@@ -84,6 +85,17 @@ export function TeamsMessageSelector({
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectionStage, setSelectionStage] = useState<'team' | 'channel' | 'chat'>(selectionType)
|
||||
|
||||
// Get cached display name
|
||||
const cachedMessageName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!credential || !value) return null
|
||||
return state.cache.files[credential]?.[value] || null
|
||||
},
|
||||
[credential, value]
|
||||
)
|
||||
)
|
||||
|
||||
// Determine the appropriate service ID based on provider and scopes
|
||||
const getServiceId = (): string => {
|
||||
if (serviceId) return serviceId
|
||||
@@ -156,6 +168,15 @@ export function TeamsMessageSelector({
|
||||
|
||||
setTeams(teamsData)
|
||||
|
||||
// Cache team names in display names store
|
||||
if (selectedCredentialId && teamsData.length > 0) {
|
||||
const teamMap = teamsData.reduce((acc: Record<string, string>, team: TeamsMessageInfo) => {
|
||||
acc[team.id] = team.displayName
|
||||
return acc
|
||||
}, {})
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', selectedCredentialId, teamMap)
|
||||
}
|
||||
|
||||
// If we have a selected team ID, find it in the list
|
||||
if (selectedTeamId) {
|
||||
const team = teamsData.find((t: TeamsMessageInfo) => t.teamId === selectedTeamId)
|
||||
@@ -702,10 +723,10 @@ export function TeamsMessageSelector({
|
||||
disabled={disabled || isForeignCredential}
|
||||
>
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedMessage ? (
|
||||
{cachedMessageName ? (
|
||||
<>
|
||||
<MicrosoftTeamsIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedMessage.displayName}</span>
|
||||
<span className='truncate font-normal'>{cachedMessageName}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Check, ChevronDown, RefreshCw, X } from 'lucide-react'
|
||||
import { Check, ChevronDown, X } from 'lucide-react'
|
||||
import { WealthboxIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type OAuthProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
const logger = createLogger('WealthboxFileSelector')
|
||||
|
||||
@@ -73,6 +74,18 @@ export function WealthboxFileSelector({
|
||||
const [credentialsLoaded, setCredentialsLoaded] = useState(false)
|
||||
const initialFetchRef = useRef(false)
|
||||
|
||||
// Get cached display name
|
||||
const cachedItemName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const effectiveCredentialId = credentialId || selectedCredentialId
|
||||
if (!effectiveCredentialId || !value) return null
|
||||
return state.cache.files[effectiveCredentialId]?.[value] || null
|
||||
},
|
||||
[credentialId, selectedCredentialId, value]
|
||||
)
|
||||
)
|
||||
|
||||
// Determine the appropriate service ID based on provider and scopes
|
||||
const getServiceId = (): string => {
|
||||
if (serviceId) return serviceId
|
||||
@@ -135,6 +148,18 @@ export function WealthboxFileSelector({
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setAvailableItems(data.items || [])
|
||||
|
||||
// Cache item names in display names store
|
||||
if (selectedCredentialId && data.items) {
|
||||
const itemMap = data.items.reduce(
|
||||
(acc: Record<string, string>, item: WealthboxItemInfo) => {
|
||||
acc[item.id] = item.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', selectedCredentialId, itemMap)
|
||||
}
|
||||
} else {
|
||||
logger.error('Error fetching available items:', {
|
||||
error: await response.text(),
|
||||
@@ -209,26 +234,6 @@ export function WealthboxFileSelector({
|
||||
}, [selectedCredentialId, open, fetchAvailableItems])
|
||||
|
||||
// Fetch the selected item metadata only once when needed
|
||||
useEffect(() => {
|
||||
if (
|
||||
value &&
|
||||
value !== selectedItemId &&
|
||||
selectedCredentialId &&
|
||||
credentialsLoaded &&
|
||||
!selectedItem &&
|
||||
!isLoadingSelectedItem
|
||||
) {
|
||||
fetchItemById(value)
|
||||
}
|
||||
}, [
|
||||
value,
|
||||
selectedItemId,
|
||||
selectedCredentialId,
|
||||
credentialsLoaded,
|
||||
selectedItem,
|
||||
isLoadingSelectedItem,
|
||||
fetchItemById,
|
||||
])
|
||||
|
||||
// Handle search input changes with debouncing
|
||||
const handleSearchChange = useCallback(
|
||||
@@ -281,7 +286,6 @@ export function WealthboxFileSelector({
|
||||
// Clear selection
|
||||
const handleClearSelection = () => {
|
||||
setSelectedItemId('')
|
||||
setSelectedItem(null)
|
||||
onChange('', undefined)
|
||||
onFileInfoChange?.(null)
|
||||
}
|
||||
@@ -319,15 +323,10 @@ export function WealthboxFileSelector({
|
||||
className='w-full justify-between'
|
||||
disabled={disabled}
|
||||
>
|
||||
{selectedItem ? (
|
||||
{cachedItemName ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<WealthboxIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedItem.name}</span>
|
||||
</div>
|
||||
) : selectedItemId && isLoadingSelectedItem && selectedCredentialId ? (
|
||||
<div className='flex items-center gap-2'>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='text-muted-foreground'>Loading...</span>
|
||||
<span className='truncate font-normal'>{cachedItemName}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { type Credential, getProviderIdFromServiceId, getServiceIdFromScopes } from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
const logger = createLogger('FolderSelector')
|
||||
|
||||
@@ -65,12 +66,24 @@ export function FolderSelector({
|
||||
credentialId || ''
|
||||
)
|
||||
const [selectedFolderId, setSelectedFolderId] = useState('')
|
||||
const [selectedFolder, setSelectedFolder] = useState<FolderInfo | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isLoadingSelectedFolder, setIsLoadingSelectedFolder] = useState(false)
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||
const initialFetchRef = useRef(false)
|
||||
|
||||
// Get cached display name
|
||||
const cachedFolderName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const effectiveCredentialId = credentialId || selectedCredentialId
|
||||
const effectiveValue = isPreview && previewValue !== undefined ? previewValue : value
|
||||
if (!effectiveCredentialId || !effectiveValue) return null
|
||||
return state.cache.folders[effectiveCredentialId]?.[effectiveValue] || null
|
||||
},
|
||||
[credentialId, selectedCredentialId, value, isPreview, previewValue]
|
||||
)
|
||||
)
|
||||
|
||||
// Initialize selectedFolderId with the effective value
|
||||
useEffect(() => {
|
||||
if (isPreview && previewValue !== undefined) {
|
||||
@@ -168,7 +181,6 @@ export function FolderSelector({
|
||||
messagesTotal: folder.totalItemCount,
|
||||
messagesUnread: folder.unreadItemCount,
|
||||
}
|
||||
setSelectedFolder(folderInfo)
|
||||
onFolderInfoChange?.(folderInfo)
|
||||
return folderInfo
|
||||
}
|
||||
@@ -181,7 +193,6 @@ export function FolderSelector({
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.label) {
|
||||
setSelectedFolder(data.label)
|
||||
onFolderInfoChange?.(data.label)
|
||||
return data.label
|
||||
}
|
||||
@@ -239,14 +250,27 @@ export function FolderSelector({
|
||||
const folderList = provider === 'outlook' ? data.folders : data.labels
|
||||
setFolders(folderList || [])
|
||||
|
||||
// If we have a selected folder ID, find the folder info
|
||||
if (selectedFolderId) {
|
||||
// Cache folder names in display names store
|
||||
if (selectedCredentialId && folderList) {
|
||||
const folderMap = folderList.reduce(
|
||||
(acc: Record<string, string>, folder: FolderInfo) => {
|
||||
acc[folder.id] = folder.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('folders', selectedCredentialId, folderMap)
|
||||
}
|
||||
|
||||
// Only notify parent if callback exists
|
||||
if (selectedFolderId && onFolderInfoChange) {
|
||||
const folderInfo = folderList.find(
|
||||
(folder: FolderInfo) => folder.id === selectedFolderId
|
||||
)
|
||||
if (folderInfo) {
|
||||
setSelectedFolder(folderInfo)
|
||||
onFolderInfoChange?.(folderInfo)
|
||||
onFolderInfoChange(folderInfo)
|
||||
} else if (!searchQuery && provider !== 'outlook') {
|
||||
// Only try to fetch by ID for Gmail if this is not a search query
|
||||
// and we couldn't find the folder in the list
|
||||
@@ -303,33 +327,11 @@ export function FolderSelector({
|
||||
if (currentValue !== selectedFolderId) {
|
||||
setSelectedFolderId(currentValue || '')
|
||||
}
|
||||
}, [value, isPreview, previewValue, disabled])
|
||||
|
||||
// Fetch the selected folder metadata once credentials are ready or value changes
|
||||
useEffect(() => {
|
||||
if (disabled) return
|
||||
const currentValue = isPreview ? (previewValue as string) : (value as string)
|
||||
if (
|
||||
currentValue &&
|
||||
selectedCredentialId &&
|
||||
(!selectedFolder || selectedFolder.id !== currentValue)
|
||||
) {
|
||||
fetchFolderById(currentValue)
|
||||
}
|
||||
}, [
|
||||
value,
|
||||
selectedCredentialId,
|
||||
selectedFolder,
|
||||
fetchFolderById,
|
||||
isPreview,
|
||||
previewValue,
|
||||
disabled,
|
||||
])
|
||||
}, [value, isPreview, previewValue, disabled, selectedFolderId])
|
||||
|
||||
// Handle folder selection
|
||||
const handleSelectFolder = (folder: FolderInfo) => {
|
||||
setSelectedFolderId(folder.id)
|
||||
setSelectedFolder(folder)
|
||||
if (!isPreview) {
|
||||
onChange(folder.id, folder)
|
||||
}
|
||||
@@ -385,10 +387,10 @@ export function FolderSelector({
|
||||
className='w-full justify-between'
|
||||
disabled={disabled || isForeignCredential}
|
||||
>
|
||||
{selectedFolder ? (
|
||||
{cachedFolderName ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
{getFolderIcon('sm')}
|
||||
<span className='truncate font-normal'>{selectedFolder.name}</span>
|
||||
<span className='truncate font-normal'>{cachedFolderName}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Combobox, type ComboboxOption } from '@/components/emcn/components/combobox/combobox'
|
||||
@@ -29,12 +29,14 @@ export function KnowledgeBaseSelector({
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
const { loadingKnowledgeBasesList } = useKnowledgeStore()
|
||||
const knowledgeBasesList = useKnowledgeStore((state) => state.knowledgeBasesList)
|
||||
const knowledgeBasesMap = useKnowledgeStore((state) => state.knowledgeBases)
|
||||
const loadingKnowledgeBasesList = useKnowledgeStore((state) => state.loadingKnowledgeBasesList)
|
||||
const getKnowledgeBasesList = useKnowledgeStore((state) => state.getKnowledgeBasesList)
|
||||
const getKnowledgeBase = useKnowledgeStore((state) => state.getKnowledgeBase)
|
||||
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBaseData[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [initialFetchDone, setInitialFetchDone] = useState(false)
|
||||
const hasRequestedListRef = useRef(false)
|
||||
|
||||
// Use the proper hook to get the current value and setter - this prevents infinite loops
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
@@ -47,13 +49,24 @@ export function KnowledgeBaseSelector({
|
||||
/**
|
||||
* Convert knowledge bases to combobox options format
|
||||
*/
|
||||
const combinedKnowledgeBases = useMemo<KnowledgeBaseData[]>(() => {
|
||||
const merged = new Map<string, KnowledgeBaseData>()
|
||||
knowledgeBasesList.forEach((kb) => {
|
||||
merged.set(kb.id, kb)
|
||||
})
|
||||
Object.values(knowledgeBasesMap).forEach((kb) => {
|
||||
merged.set(kb.id, kb)
|
||||
})
|
||||
return Array.from(merged.values())
|
||||
}, [knowledgeBasesList, knowledgeBasesMap])
|
||||
|
||||
const options = useMemo<ComboboxOption[]>(() => {
|
||||
return knowledgeBases.map((kb) => ({
|
||||
return combinedKnowledgeBases.map((kb) => ({
|
||||
label: kb.name,
|
||||
value: kb.id,
|
||||
icon: PackageSearchIcon,
|
||||
}))
|
||||
}, [knowledgeBases])
|
||||
}, [combinedKnowledgeBases])
|
||||
|
||||
/**
|
||||
* Parse value into array of selected IDs
|
||||
@@ -74,51 +87,18 @@ export function KnowledgeBaseSelector({
|
||||
/**
|
||||
* Compute selected knowledge bases for tag display
|
||||
*/
|
||||
const selectedKnowledgeBases = useMemo(() => {
|
||||
if (selectedIds.length > 0 && knowledgeBases.length > 0) {
|
||||
return knowledgeBases.filter((kb) => selectedIds.includes(kb.id))
|
||||
}
|
||||
return []
|
||||
}, [selectedIds, knowledgeBases])
|
||||
const selectedKnowledgeBases = useMemo<KnowledgeBaseData[]>(() => {
|
||||
if (selectedIds.length === 0) return []
|
||||
|
||||
/**
|
||||
* Fetch knowledge bases directly from API
|
||||
*/
|
||||
const fetchKnowledgeBases = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const lookup = new Map<string, KnowledgeBaseData>()
|
||||
combinedKnowledgeBases.forEach((kb) => {
|
||||
lookup.set(kb.id, kb)
|
||||
})
|
||||
|
||||
try {
|
||||
const url = workspaceId ? `/api/knowledge?workspaceId=${workspaceId}` : '/api/knowledge'
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch knowledge bases: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch knowledge bases')
|
||||
}
|
||||
|
||||
const data = result.data || []
|
||||
setKnowledgeBases(data)
|
||||
setInitialFetchDone(true)
|
||||
} catch (err) {
|
||||
if ((err as Error).name === 'AbortError') return
|
||||
setError((err as Error).message)
|
||||
setKnowledgeBases([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [workspaceId])
|
||||
return selectedIds
|
||||
.map((id) => lookup.get(id))
|
||||
.filter((kb): kb is KnowledgeBaseData => Boolean(kb))
|
||||
}, [selectedIds, combinedKnowledgeBases])
|
||||
|
||||
/**
|
||||
* Handle single selection
|
||||
@@ -168,10 +148,39 @@ export function KnowledgeBaseSelector({
|
||||
* Fetch knowledge bases on initial mount
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!initialFetchDone && !loading && !isPreview) {
|
||||
fetchKnowledgeBases()
|
||||
if (hasRequestedListRef.current) return
|
||||
|
||||
let cancelled = false
|
||||
hasRequestedListRef.current = true
|
||||
setError(null)
|
||||
getKnowledgeBasesList(workspaceId).catch((err) => {
|
||||
if (cancelled) return
|
||||
setError(err instanceof Error ? err.message : 'Failed to load knowledge bases')
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [initialFetchDone, loading, isPreview, fetchKnowledgeBases])
|
||||
}, [workspaceId, getKnowledgeBasesList])
|
||||
|
||||
/**
|
||||
* Ensure selected knowledge bases are cached
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (selectedIds.length === 0) return
|
||||
|
||||
selectedIds.forEach((id) => {
|
||||
const isKnown =
|
||||
Boolean(knowledgeBasesMap[id]) ||
|
||||
knowledgeBasesList.some((knowledgeBase) => knowledgeBase.id === id)
|
||||
|
||||
if (!isKnown) {
|
||||
void getKnowledgeBase(id).catch(() => {
|
||||
// Ignore fetch errors here; they will surface via display hooks if needed
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [selectedIds, knowledgeBasesList, knowledgeBasesMap, getKnowledgeBase])
|
||||
|
||||
const label =
|
||||
subBlock.placeholder || (isMultiSelect ? 'Select knowledge bases' : 'Select knowledge base')
|
||||
@@ -212,7 +221,7 @@ export function KnowledgeBaseSelector({
|
||||
onMultiSelectChange={handleMultiSelectChange}
|
||||
placeholder={label}
|
||||
disabled={disabled || isPreview}
|
||||
isLoading={loading || loadingKnowledgeBasesList}
|
||||
isLoading={loadingKnowledgeBasesList}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Check, ChevronDown, ExternalLink, RefreshCw, X } from 'lucide-react'
|
||||
import { Check, ChevronDown, RefreshCw } from 'lucide-react'
|
||||
import { JiraIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type OAuthProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
const logger = createLogger('JiraProjectSelector')
|
||||
|
||||
@@ -73,13 +74,24 @@ export function JiraProjectSelector({
|
||||
const [projects, setProjects] = useState<JiraProjectInfo[]>([])
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState<string>(credentialId || '')
|
||||
const [selectedProjectId, setSelectedProjectId] = useState(value)
|
||||
const [selectedProject, setSelectedProject] = useState<JiraProjectInfo | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||
const initialFetchRef = useRef(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [cloudId, setCloudId] = useState<string | null>(null)
|
||||
|
||||
// Get cached display name
|
||||
const cachedProjectName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const effectiveCredentialId = credentialId || selectedCredentialId
|
||||
if (!effectiveCredentialId || !value) return null
|
||||
return state.cache.projects[`jira-${effectiveCredentialId}`]?.[value] || null
|
||||
},
|
||||
[credentialId, selectedCredentialId, value]
|
||||
)
|
||||
)
|
||||
|
||||
// Handle search with debounce
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
@@ -198,10 +210,8 @@ export function JiraProjectSelector({
|
||||
}
|
||||
|
||||
if (projectInfo) {
|
||||
setSelectedProject(projectInfo)
|
||||
onProjectInfoChange?.(projectInfo)
|
||||
} else {
|
||||
setSelectedProject(null)
|
||||
onProjectInfoChange?.(null)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -292,13 +302,26 @@ export function JiraProjectSelector({
|
||||
logger.info(`Received ${foundProjects.length} projects from API`)
|
||||
setProjects(foundProjects)
|
||||
|
||||
// Cache project names in display names store
|
||||
if (selectedCredentialId && foundProjects.length > 0) {
|
||||
const projectMap = foundProjects.reduce(
|
||||
(acc: Record<string, string>, proj: JiraProjectInfo) => {
|
||||
acc[proj.id] = proj.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('projects', `jira-${selectedCredentialId}`, projectMap)
|
||||
}
|
||||
|
||||
// If we have a selected project ID, find the project info
|
||||
if (selectedProjectId) {
|
||||
const projectInfo = foundProjects.find(
|
||||
(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
|
||||
@@ -337,26 +360,16 @@ export function JiraProjectSelector({
|
||||
}
|
||||
}, [credentialId, selectedCredentialId])
|
||||
|
||||
// Fetch the selected project metadata once credentials are ready or changed
|
||||
useEffect(() => {
|
||||
if (value && selectedCredentialId && domain && domain.includes('.')) {
|
||||
if (!selectedProject || selectedProject.id !== value) {
|
||||
fetchProjectInfo(value)
|
||||
}
|
||||
}
|
||||
}, [value, selectedCredentialId, domain, fetchProjectInfo, selectedProject])
|
||||
|
||||
// Keep internal selectedProjectId in sync with the value prop
|
||||
useEffect(() => {
|
||||
if (value !== selectedProjectId) {
|
||||
setSelectedProjectId(value)
|
||||
}
|
||||
}, [value])
|
||||
}, [value, selectedProjectId])
|
||||
|
||||
// Clear local preview when value is cleared remotely or via collaborator
|
||||
// Clear callback when value is cleared
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setSelectedProject(null)
|
||||
onProjectInfoChange?.(null)
|
||||
}
|
||||
}, [value, onProjectInfoChange])
|
||||
@@ -373,7 +386,6 @@ export function JiraProjectSelector({
|
||||
// Handle project selection
|
||||
const handleSelectProject = (project: JiraProjectInfo) => {
|
||||
setSelectedProjectId(project.id)
|
||||
setSelectedProject(project)
|
||||
onChange(project.id, project)
|
||||
onProjectInfoChange?.(project)
|
||||
setOpen(false)
|
||||
@@ -389,14 +401,11 @@ export function JiraProjectSelector({
|
||||
// Clear selection
|
||||
const handleClearSelection = () => {
|
||||
setSelectedProjectId('')
|
||||
setSelectedProject(null)
|
||||
setError(null)
|
||||
onChange('', undefined)
|
||||
onProjectInfoChange?.(null)
|
||||
}
|
||||
|
||||
const canShowPreview = !!(showPreview && selectedProject && value && selectedProject.id === value)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-2'>
|
||||
@@ -409,15 +418,10 @@ export function JiraProjectSelector({
|
||||
className='w-full justify-between'
|
||||
disabled={disabled || !domain || !selectedCredentialId || isForeignCredential}
|
||||
>
|
||||
{canShowPreview ? (
|
||||
{cachedProjectName ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<JiraIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedProject.name}</span>
|
||||
</div>
|
||||
) : selectedProjectId ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<JiraIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedProjectId}</span>
|
||||
<span className='truncate font-normal'>{cachedProjectName}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
@@ -554,55 +558,6 @@ export function JiraProjectSelector({
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
{/* Project preview */}
|
||||
{canShowPreview && (
|
||||
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
|
||||
<div className='absolute top-2 right-2'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-5 w-5 hover:bg-muted'
|
||||
onClick={handleClearSelection}
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex items-center gap-3 pr-4'>
|
||||
<div className='flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-muted/20'>
|
||||
{selectedProject.avatarUrl ? (
|
||||
<img
|
||||
src={selectedProject.avatarUrl}
|
||||
alt={selectedProject.name}
|
||||
className='h-4 w-4 rounded'
|
||||
/>
|
||||
) : (
|
||||
<JiraIcon className='h-4 w-4' />
|
||||
)}
|
||||
</div>
|
||||
<div className='min-w-0 flex-1 overflow-hidden'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<h4 className='truncate font-medium text-xs'>{selectedProject.name}</h4>
|
||||
<span className='whitespace-nowrap text-muted-foreground text-xs'>
|
||||
{selectedProject.key}
|
||||
</span>
|
||||
</div>
|
||||
{selectedProject.url && (
|
||||
<a
|
||||
href={selectedProject.url}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center gap-1 text-foreground text-xs hover:underline'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span>Open in Jira</span>
|
||||
<ExternalLink className='h-3 w-3' />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showOAuthModal && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Check, ChevronDown, RefreshCw } from 'lucide-react'
|
||||
import { LinearIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
export interface LinearProjectInfo {
|
||||
id: string
|
||||
@@ -40,7 +41,17 @@ export function LinearProjectSelector({
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedProject, setSelectedProject] = useState<LinearProjectInfo | null>(null)
|
||||
|
||||
// Get cached display name
|
||||
const cachedProjectName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!credential || !value) return null
|
||||
return state.cache.projects[`linear-${credential}`]?.[value] || null
|
||||
},
|
||||
[credential, value]
|
||||
)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!credential || !teamId) return
|
||||
@@ -68,10 +79,18 @@ export function LinearProjectSelector({
|
||||
} else {
|
||||
setProjects(data.projects)
|
||||
|
||||
// Find selected project info if we have a value
|
||||
if (value) {
|
||||
const projectInfo = data.projects.find((p: LinearProjectInfo) => p.id === value)
|
||||
setSelectedProject(projectInfo || null)
|
||||
// Cache project names in display names store
|
||||
if (credential && data.projects) {
|
||||
const projectMap = data.projects.reduce(
|
||||
(acc: Record<string, string>, proj: LinearProjectInfo) => {
|
||||
acc[proj.id] = proj.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('projects', `linear-${credential}`, projectMap)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -84,18 +103,7 @@ export function LinearProjectSelector({
|
||||
return () => controller.abort()
|
||||
}, [credential, teamId, value, workflowId])
|
||||
|
||||
// Sync selected project with value prop
|
||||
useEffect(() => {
|
||||
if (value && projects.length > 0) {
|
||||
const projectInfo = projects.find((p) => p.id === value)
|
||||
setSelectedProject(projectInfo || null)
|
||||
} else if (!value) {
|
||||
setSelectedProject(null)
|
||||
}
|
||||
}, [value, projects])
|
||||
|
||||
const handleSelectProject = (project: LinearProjectInfo) => {
|
||||
setSelectedProject(project)
|
||||
onChange(project.id, project)
|
||||
setOpen(false)
|
||||
}
|
||||
@@ -114,10 +122,10 @@ export function LinearProjectSelector({
|
||||
className='w-full justify-between'
|
||||
disabled={disabled || !credential || !teamId}
|
||||
>
|
||||
{selectedProject ? (
|
||||
{cachedProjectName ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<LinearIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedProject.name}</span>
|
||||
<span className='truncate font-normal'>{cachedProjectName}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Check, ChevronDown, RefreshCw } from 'lucide-react'
|
||||
import { LinearIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
export interface LinearTeamInfo {
|
||||
id: string
|
||||
@@ -39,7 +40,17 @@ export function LinearTeamSelector({
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedTeam, setSelectedTeam] = useState<LinearTeamInfo | null>(null)
|
||||
|
||||
// Get cached display name
|
||||
const cachedTeamName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!credential || !value) return null
|
||||
return state.cache.projects[`linear-${credential}`]?.[value] || null
|
||||
},
|
||||
[credential, value]
|
||||
)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!credential) return
|
||||
@@ -64,10 +75,18 @@ export function LinearTeamSelector({
|
||||
} else {
|
||||
setTeams(data.teams)
|
||||
|
||||
// Find selected team info if we have a value
|
||||
if (value) {
|
||||
const teamInfo = data.teams.find((t: LinearTeamInfo) => t.id === value)
|
||||
setSelectedTeam(teamInfo || null)
|
||||
// Cache team names in display names store
|
||||
if (credential && data.teams) {
|
||||
const teamMap = data.teams.reduce(
|
||||
(acc: Record<string, string>, team: LinearTeamInfo) => {
|
||||
acc[team.id] = team.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('projects', `linear-${credential}`, teamMap)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -80,18 +99,7 @@ export function LinearTeamSelector({
|
||||
return () => controller.abort()
|
||||
}, [credential, value, workflowId])
|
||||
|
||||
// Sync selected team with value prop
|
||||
useEffect(() => {
|
||||
if (value && teams.length > 0) {
|
||||
const teamInfo = teams.find((t) => t.id === value)
|
||||
setSelectedTeam(teamInfo || null)
|
||||
} else if (!value) {
|
||||
setSelectedTeam(null)
|
||||
}
|
||||
}, [value, teams])
|
||||
|
||||
const handleSelectTeam = (team: LinearTeamInfo) => {
|
||||
setSelectedTeam(team)
|
||||
onChange(team.id, team)
|
||||
setOpen(false)
|
||||
}
|
||||
@@ -110,10 +118,10 @@ export function LinearTeamSelector({
|
||||
className='w-full justify-between'
|
||||
disabled={disabled || !credential}
|
||||
>
|
||||
{selectedTeam ? (
|
||||
{cachedTeamName ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<LinearIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedTeam.name}</span>
|
||||
<span className='truncate font-normal'>{cachedTeamName}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('ToolCredentialSelector')
|
||||
@@ -87,6 +88,18 @@ export function ToolCredentialSelector({
|
||||
const data = await response.json()
|
||||
setCredentials(data.credentials || [])
|
||||
|
||||
// Cache credential names for block previews
|
||||
if (provider) {
|
||||
const credentialMap = (data.credentials || []).reduce(
|
||||
(acc: Record<string, string>, cred: Credential) => {
|
||||
acc[cred.id] = cred.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('credentials', provider, credentialMap)
|
||||
}
|
||||
|
||||
if (
|
||||
value &&
|
||||
!(data.credentials || []).some((cred: Credential) => cred.id === value) &&
|
||||
@@ -99,7 +112,19 @@ export function ToolCredentialSelector({
|
||||
if (metaResp.ok) {
|
||||
const meta = await metaResp.json()
|
||||
if (meta.credentials?.length) {
|
||||
setCredentials([meta.credentials[0], ...(data.credentials || [])])
|
||||
const combinedCredentials = [meta.credentials[0], ...(data.credentials || [])]
|
||||
setCredentials(combinedCredentials)
|
||||
|
||||
const credentialMap = combinedCredentials.reduce(
|
||||
(acc: Record<string, string>, cred: Credential) => {
|
||||
acc[cred.id] = cred.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('credentials', provider, credentialMap)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -9,6 +9,8 @@ import { cn } from '@/lib/utils'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useCredentialDisplay } from '@/hooks/use-credential-display'
|
||||
import { useDisplayName } from '@/hooks/use-display-name'
|
||||
import { usePanelEditorStore } from '@/stores/panel-new/editor/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
@@ -85,7 +87,6 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||
const getDisplayValue = (value: unknown): string => {
|
||||
if (value == null || value === '') return '-'
|
||||
|
||||
// Handle table row arrays (from table component)
|
||||
if (isTableRowArray(value)) {
|
||||
const nonEmptyRows = value.filter((row) => {
|
||||
const cellValues = Object.values(row.cells)
|
||||
@@ -106,7 +107,6 @@ const getDisplayValue = (value: unknown): string => {
|
||||
return `${nonEmptyRows.length} rows`
|
||||
}
|
||||
|
||||
// Handle field format arrays (from input-format, response-format)
|
||||
if (isFieldFormatArray(value)) {
|
||||
const namedFields = value.filter((field) => field.name && field.name.trim() !== '')
|
||||
if (namedFields.length === 0) return '-'
|
||||
@@ -115,7 +115,6 @@ const getDisplayValue = (value: unknown): string => {
|
||||
return `${namedFields[0].name}, ${namedFields[1].name} +${namedFields.length - 2}`
|
||||
}
|
||||
|
||||
// Handle input mapping objects (from input-mapping component)
|
||||
if (isPlainObject(value)) {
|
||||
const entries = Object.entries(value).filter(
|
||||
([, val]) => val !== null && val !== undefined && val !== ''
|
||||
@@ -134,19 +133,27 @@ const getDisplayValue = (value: unknown): string => {
|
||||
return entries.length > 2 ? `${preview} +${entries.length - 2}` : preview
|
||||
}
|
||||
|
||||
// Handle arrays of primitives
|
||||
if (Array.isArray(value)) {
|
||||
const nonEmptyItems = value.filter((item) => item !== null && item !== undefined && item !== '')
|
||||
if (nonEmptyItems.length === 0) return '-'
|
||||
if (nonEmptyItems.length === 1) return String(nonEmptyItems[0])
|
||||
if (nonEmptyItems.length === 2) return `${nonEmptyItems[0]}, ${nonEmptyItems[1]}`
|
||||
return `${nonEmptyItems[0]}, ${nonEmptyItems[1]} +${nonEmptyItems.length - 2}`
|
||||
|
||||
const getItemDisplayValue = (item: unknown): string => {
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
const obj = item as Record<string, unknown>
|
||||
return String(obj.title || obj.name || obj.label || obj.id || JSON.stringify(item))
|
||||
}
|
||||
return String(item)
|
||||
}
|
||||
|
||||
if (nonEmptyItems.length === 1) return getItemDisplayValue(nonEmptyItems[0])
|
||||
if (nonEmptyItems.length === 2) {
|
||||
return `${getItemDisplayValue(nonEmptyItems[0])}, ${getItemDisplayValue(nonEmptyItems[1])}`
|
||||
}
|
||||
return `${getItemDisplayValue(nonEmptyItems[0])}, ${getItemDisplayValue(nonEmptyItems[1])} +${nonEmptyItems.length - 2}`
|
||||
}
|
||||
|
||||
// Handle primitive values
|
||||
const stringValue = String(value)
|
||||
if (stringValue === '[object Object]') {
|
||||
// Fallback for unhandled object types - try to show something useful
|
||||
try {
|
||||
const json = JSON.stringify(value)
|
||||
if (json.length <= 40) return json
|
||||
@@ -161,19 +168,97 @@ const getDisplayValue = (value: unknown): string => {
|
||||
|
||||
/**
|
||||
* Renders a single subblock row with title and optional value.
|
||||
* Automatically hydrates IDs to display names for all selector types.
|
||||
*/
|
||||
const SubBlockRow = ({ title, value }: { title: string; value?: string }) => (
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<span className='min-w-0 truncate text-[#AEAEAE] text-[14px]' title={title}>
|
||||
{title}
|
||||
</span>
|
||||
{value !== undefined && (
|
||||
<span className='flex-1 truncate text-right text-[#FFFFFF] text-[14px]' title={value}>
|
||||
{value}
|
||||
const SubBlockRow = ({
|
||||
title,
|
||||
value,
|
||||
subBlock,
|
||||
rawValue,
|
||||
workspaceId,
|
||||
allSubBlockValues,
|
||||
}: {
|
||||
title: string
|
||||
value?: string
|
||||
subBlock?: SubBlockConfig
|
||||
rawValue?: unknown
|
||||
workspaceId?: string
|
||||
allSubBlockValues?: Record<string, { value: unknown }>
|
||||
}) => {
|
||||
const getStringValue = useCallback(
|
||||
(key?: string): string | undefined => {
|
||||
if (!key || !allSubBlockValues) return undefined
|
||||
const candidate = allSubBlockValues[key]?.value
|
||||
return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined
|
||||
},
|
||||
[allSubBlockValues]
|
||||
)
|
||||
|
||||
const dependencyValues = useMemo(() => {
|
||||
if (!subBlock?.dependsOn?.length) return {}
|
||||
return subBlock.dependsOn.reduce<Record<string, string>>((accumulator, dependency) => {
|
||||
const dependencyValue = getStringValue(dependency)
|
||||
if (dependencyValue) {
|
||||
accumulator[dependency] = dependencyValue
|
||||
}
|
||||
return accumulator
|
||||
}, {})
|
||||
}, [getStringValue, subBlock?.dependsOn])
|
||||
|
||||
const { displayName: credentialName } = useCredentialDisplay(
|
||||
subBlock?.type === 'oauth-input' && typeof rawValue === 'string' ? rawValue : undefined,
|
||||
subBlock?.provider
|
||||
)
|
||||
|
||||
const credentialId = dependencyValues.credential
|
||||
const knowledgeBaseId = dependencyValues.knowledgeBaseId
|
||||
|
||||
const dropdownLabel = useMemo(() => {
|
||||
if (!subBlock || (subBlock.type !== 'dropdown' && subBlock.type !== 'combobox')) return null
|
||||
if (!rawValue || typeof rawValue !== 'string') return null
|
||||
|
||||
const options = typeof subBlock.options === 'function' ? subBlock.options() : subBlock.options
|
||||
if (!options) return null
|
||||
|
||||
const option = options.find((opt) =>
|
||||
typeof opt === 'string' ? opt === rawValue : opt.id === rawValue
|
||||
)
|
||||
|
||||
if (!option) return null
|
||||
return typeof option === 'string' ? option : option.label
|
||||
}, [subBlock, rawValue])
|
||||
|
||||
const genericDisplayName = useDisplayName(subBlock, rawValue, {
|
||||
workspaceId,
|
||||
provider: subBlock?.provider,
|
||||
credentialId: typeof credentialId === 'string' ? credentialId : undefined,
|
||||
knowledgeBaseId: typeof knowledgeBaseId === 'string' ? knowledgeBaseId : undefined,
|
||||
domain: getStringValue('domain'),
|
||||
teamId: getStringValue('teamId'),
|
||||
projectId: getStringValue('projectId'),
|
||||
planId: getStringValue('planId'),
|
||||
})
|
||||
|
||||
const isPasswordField = subBlock?.password === true
|
||||
const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null
|
||||
const displayValue = maskedValue || credentialName || dropdownLabel || genericDisplayName || value
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<span className='min-w-0 truncate text-[#AEAEAE] text-[14px]' title={title}>
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
{displayValue !== undefined && (
|
||||
<span
|
||||
className='flex-1 truncate text-right text-[#FFFFFF] text-[14px]'
|
||||
title={displayValue}
|
||||
>
|
||||
{displayValue}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
id,
|
||||
@@ -186,6 +271,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
|
||||
const params = useParams()
|
||||
const currentWorkflowId = params.workflowId as string
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
const currentWorkflow = useCurrentWorkflow()
|
||||
const currentBlock = currentWorkflow.getBlockById(id)
|
||||
@@ -345,6 +431,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
|
||||
const visibleSubBlocks = config.subBlocks.filter((block) => {
|
||||
if (block.hidden) return false
|
||||
if (block.hideFromPreview) return false
|
||||
|
||||
if (block.requiresFeature && !isTruthy(getEnv(block.requiresFeature))) {
|
||||
return false
|
||||
@@ -547,7 +634,6 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
typeof currentStoreBlock?.height === 'number' ? currentStoreBlock.height : undefined
|
||||
const prevWidth = 250 // fixed across the app for workflow blocks
|
||||
|
||||
// Only update store if something actually changed to prevent unnecessary reflows
|
||||
if (prevHeight !== calculatedHeight || prevWidth !== FIXED_WIDTH) {
|
||||
updateBlockLayoutMetrics(id, { width: FIXED_WIDTH, height: calculatedHeight })
|
||||
updateNodeInternals(id)
|
||||
@@ -791,13 +877,20 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
/>
|
||||
))
|
||||
: subBlockRows.map((row, rowIndex) =>
|
||||
row.map((subBlock) => (
|
||||
<SubBlockRow
|
||||
key={`${subBlock.id}-${rowIndex}`}
|
||||
title={subBlock.title ?? subBlock.id}
|
||||
value={getDisplayValue(subBlockState[subBlock.id]?.value)}
|
||||
/>
|
||||
))
|
||||
row.map((subBlock) => {
|
||||
const rawValue = subBlockState[subBlock.id]?.value
|
||||
return (
|
||||
<SubBlockRow
|
||||
key={`${subBlock.id}-${rowIndex}`}
|
||||
title={subBlock.title ?? subBlock.id}
|
||||
value={getDisplayValue(rawValue)}
|
||||
subBlock={subBlock}
|
||||
rawValue={rawValue}
|
||||
workspaceId={workspaceId}
|
||||
allSubBlockValues={subBlockState}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)}
|
||||
{shouldShowDefaultHandles && <SubBlockRow title='error' />}
|
||||
</div>
|
||||
|
||||
@@ -144,6 +144,7 @@ export interface SubBlockConfig {
|
||||
showCopyButton?: boolean
|
||||
connectionDroppable?: boolean
|
||||
hidden?: boolean
|
||||
hideFromPreview?: boolean // Hide this subblock from the workflow block preview
|
||||
requiresFeature?: string // Environment variable name that must be truthy for this subblock to be visible
|
||||
description?: string
|
||||
value?: (params: Record<string, any>) => string
|
||||
|
||||
54
apps/sim/hooks/use-credential-display.ts
Normal file
54
apps/sim/hooks/use-credential-display.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
|
||||
/**
|
||||
* Hook to get display name for a credential ID
|
||||
* Automatically fetches if not cached
|
||||
*/
|
||||
export function useCredentialDisplay(credentialId: string | undefined, provider?: string) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// Select the actual cached value from the store (not just the getter)
|
||||
// This ensures the component re-renders when the cache is populated
|
||||
const displayName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!credentialId || !provider) return null
|
||||
return state.cache.credentials[provider]?.[credentialId] || null
|
||||
},
|
||||
[credentialId, provider]
|
||||
)
|
||||
)
|
||||
|
||||
// Fetch if not cached
|
||||
useEffect(() => {
|
||||
if (!credentialId || !provider || displayName || isLoading) return
|
||||
|
||||
setIsLoading(true)
|
||||
fetch(`/api/auth/oauth/credentials?provider=${encodeURIComponent(provider)}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.credentials) {
|
||||
const credentialMap = data.credentials.reduce(
|
||||
(acc: Record<string, string>, cred: { id: string; name: string }) => {
|
||||
acc[cred.id] = cred.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('credentials', provider, credentialMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently fail
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [credentialId, provider, displayName, isLoading])
|
||||
|
||||
return {
|
||||
displayName,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
555
apps/sim/hooks/use-display-name.ts
Normal file
555
apps/sim/hooks/use-display-name.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useDisplayNamesStore } from '@/stores/display-names/store'
|
||||
import { useKnowledgeStore } from '@/stores/knowledge/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
/**
|
||||
* Generic hook to get display name for any selector value
|
||||
* Automatically fetches if not cached
|
||||
*/
|
||||
export function useDisplayName(
|
||||
subBlock: SubBlockConfig | undefined,
|
||||
value: unknown,
|
||||
context?: {
|
||||
workspaceId?: string
|
||||
credentialId?: string
|
||||
provider?: string
|
||||
knowledgeBaseId?: string
|
||||
domain?: string
|
||||
teamId?: string
|
||||
projectId?: string
|
||||
planId?: string
|
||||
}
|
||||
): string | null {
|
||||
const getCachedKnowledgeBase = useKnowledgeStore((state) => state.getCachedKnowledgeBase)
|
||||
const getKnowledgeBase = useKnowledgeStore((state) => state.getKnowledgeBase)
|
||||
const getDocuments = useKnowledgeStore((state) => state.getDocuments)
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
|
||||
const cachedDisplayName = useDisplayNamesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!subBlock || !value || typeof value !== 'string') return null
|
||||
|
||||
// Channels
|
||||
if (subBlock.type === 'channel-selector' && context?.credentialId) {
|
||||
return state.cache.channels[context.credentialId]?.[value] || null
|
||||
}
|
||||
|
||||
// Workflows
|
||||
if (subBlock.id === 'workflowId') {
|
||||
return state.cache.workflows.global?.[value] || null
|
||||
}
|
||||
|
||||
// Files
|
||||
if (subBlock.type === 'file-selector' && context?.credentialId) {
|
||||
return state.cache.files[context.credentialId]?.[value] || null
|
||||
}
|
||||
|
||||
// Folders
|
||||
if (subBlock.type === 'folder-selector' && context?.credentialId) {
|
||||
return state.cache.folders[context.credentialId]?.[value] || null
|
||||
}
|
||||
|
||||
// Projects
|
||||
if (subBlock.type === 'project-selector' && context?.provider && context?.credentialId) {
|
||||
const projectContext = `${context.provider}-${context.credentialId}`
|
||||
return state.cache.projects[projectContext]?.[value] || null
|
||||
}
|
||||
|
||||
// Documents
|
||||
if (subBlock.type === 'document-selector' && context?.knowledgeBaseId) {
|
||||
return state.cache.documents[context.knowledgeBaseId]?.[value] || null
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
[subBlock, value, context?.credentialId, context?.provider, context?.knowledgeBaseId]
|
||||
)
|
||||
)
|
||||
|
||||
// Auto-fetch knowledge bases if needed
|
||||
useEffect(() => {
|
||||
if (
|
||||
subBlock?.type === 'knowledge-base-selector' &&
|
||||
typeof value === 'string' &&
|
||||
value &&
|
||||
!isFetching
|
||||
) {
|
||||
const kb = getCachedKnowledgeBase(value)
|
||||
if (!kb) {
|
||||
setIsFetching(true)
|
||||
getKnowledgeBase(value)
|
||||
.catch(() => {
|
||||
// Silently fail
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [subBlock?.type, value, isFetching, getCachedKnowledgeBase, getKnowledgeBase])
|
||||
|
||||
// Auto-fetch documents if needed
|
||||
useEffect(() => {
|
||||
if (
|
||||
subBlock?.type === 'document-selector' &&
|
||||
context?.knowledgeBaseId &&
|
||||
typeof value === 'string' &&
|
||||
value &&
|
||||
!cachedDisplayName &&
|
||||
!isFetching
|
||||
) {
|
||||
setIsFetching(true)
|
||||
getDocuments(context.knowledgeBaseId)
|
||||
.then((docs) => {
|
||||
if (docs.length > 0) {
|
||||
const documentMap = docs.reduce<Record<string, string>>((acc, doc) => {
|
||||
acc[doc.id] = doc.filename
|
||||
return acc
|
||||
}, {})
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('documents', context.knowledgeBaseId!, documentMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently fail
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
})
|
||||
}
|
||||
}, [subBlock?.type, value, context?.knowledgeBaseId, cachedDisplayName, isFetching, getDocuments])
|
||||
|
||||
// Auto-fetch workflows if needed
|
||||
useEffect(() => {
|
||||
if (subBlock?.id !== 'workflowId' || typeof value !== 'string' || !value) return
|
||||
if (cachedDisplayName || isFetching) return
|
||||
|
||||
const workflows = useWorkflowRegistry.getState().workflows
|
||||
if (!workflows[value]) return
|
||||
|
||||
const workflowMap = Object.entries(workflows).reduce<Record<string, string>>(
|
||||
(acc, [id, workflow]) => {
|
||||
acc[id] = workflow.name || `Workflow ${id.slice(0, 8)}`
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
useDisplayNamesStore.getState().setDisplayNames('workflows', 'global', workflowMap)
|
||||
}, [subBlock?.id, value, cachedDisplayName, isFetching])
|
||||
|
||||
// Auto-fetch channels if needed
|
||||
useEffect(() => {
|
||||
if (subBlock?.type !== 'channel-selector' || !context?.credentialId || !value) return
|
||||
if (cachedDisplayName || isFetching) return
|
||||
|
||||
setIsFetching(true)
|
||||
fetch('/api/tools/slack/channels', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credential: context.credentialId }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.channels) {
|
||||
const channelMap = data.channels.reduce(
|
||||
(acc: Record<string, string>, ch: { id: string; name: string }) => {
|
||||
acc[ch.id] = ch.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('channels', context.credentialId!, channelMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently fail
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
})
|
||||
}, [subBlock?.type, value, context?.credentialId, cachedDisplayName, isFetching])
|
||||
|
||||
// Auto-fetch folders if needed (Gmail/Outlook)
|
||||
useEffect(() => {
|
||||
if (subBlock?.type !== 'folder-selector' || !context?.credentialId || !value) return
|
||||
if (cachedDisplayName || isFetching) return
|
||||
|
||||
setIsFetching(true)
|
||||
const provider = subBlock.provider || 'gmail'
|
||||
const apiEndpoint =
|
||||
provider === 'outlook'
|
||||
? `/api/tools/outlook/folders?credentialId=${context.credentialId}`
|
||||
: `/api/tools/gmail/labels?credentialId=${context.credentialId}`
|
||||
|
||||
fetch(apiEndpoint)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const folderList = provider === 'outlook' ? data.folders : data.labels
|
||||
if (folderList) {
|
||||
const folderMap = folderList.reduce(
|
||||
(acc: Record<string, string>, folder: { id: string; name: string }) => {
|
||||
acc[folder.id] = folder.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('folders', context.credentialId!, folderMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently fail
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
})
|
||||
}, [
|
||||
subBlock?.type,
|
||||
subBlock?.provider,
|
||||
value,
|
||||
context?.credentialId,
|
||||
cachedDisplayName,
|
||||
isFetching,
|
||||
])
|
||||
|
||||
// Auto-fetch projects if needed (Jira, Linear)
|
||||
useEffect(() => {
|
||||
if (
|
||||
subBlock?.type !== 'project-selector' ||
|
||||
!context?.credentialId ||
|
||||
!context?.provider ||
|
||||
!value
|
||||
)
|
||||
return
|
||||
if (cachedDisplayName || isFetching) return
|
||||
|
||||
const projectContext = `${context.provider}-${context.credentialId}`
|
||||
setIsFetching(true)
|
||||
|
||||
if (context.provider === 'jira' && context.domain) {
|
||||
fetch('/api/tools/jira/projects', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credentialId: context.credentialId, domain: context.domain }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.projects) {
|
||||
const projectMap = data.projects.reduce(
|
||||
(acc: Record<string, string>, proj: { id: string; name: string }) => {
|
||||
acc[proj.id] = proj.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('projects', projectContext, projectMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
} else if (context.provider === 'linear' && context.teamId) {
|
||||
fetch('/api/tools/linear/projects', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credential: context.credentialId, teamId: context.teamId }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.projects) {
|
||||
const projectMap = data.projects.reduce(
|
||||
(acc: Record<string, string>, proj: { id: string; name: string }) => {
|
||||
acc[proj.id] = proj.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('projects', projectContext, projectMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
} else {
|
||||
setIsFetching(false)
|
||||
}
|
||||
}, [
|
||||
subBlock?.type,
|
||||
value,
|
||||
context?.credentialId,
|
||||
context?.provider,
|
||||
context?.domain,
|
||||
context?.teamId,
|
||||
cachedDisplayName,
|
||||
isFetching,
|
||||
])
|
||||
|
||||
// Auto-fetch files if needed (provider-specific)
|
||||
useEffect(() => {
|
||||
if (subBlock?.type !== 'file-selector' || !context?.credentialId || !value) return
|
||||
if (cachedDisplayName || isFetching) return
|
||||
|
||||
setIsFetching(true)
|
||||
const provider = subBlock.provider || context.provider
|
||||
const serviceId = subBlock.serviceId
|
||||
|
||||
// Google Calendar
|
||||
if (provider === 'google-calendar') {
|
||||
fetch(`/api/tools/google_calendar/calendars?credentialId=${context.credentialId}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.calendars) {
|
||||
const calendarMap = data.calendars.reduce(
|
||||
(acc: Record<string, string>, cal: { id: string; summary: string }) => {
|
||||
acc[cal.id] = cal.summary
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('files', context.credentialId!, calendarMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
}
|
||||
// Jira issues
|
||||
else if (provider === 'jira' && context.domain && context.projectId) {
|
||||
fetch('/api/tools/jira/issues', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
credentialId: context.credentialId,
|
||||
domain: context.domain,
|
||||
projectId: context.projectId,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.issues) {
|
||||
const issueMap = data.issues.reduce(
|
||||
(acc: Record<string, string>, issue: { id: string; name: string }) => {
|
||||
acc[issue.id] = issue.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('files', context.credentialId!, issueMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
}
|
||||
// Confluence pages
|
||||
else if (provider === 'confluence' && context.domain) {
|
||||
fetch('/api/tools/confluence/pages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credentialId: context.credentialId, domain: context.domain }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.files) {
|
||||
const fileMap = data.files.reduce(
|
||||
(acc: Record<string, string>, file: { id: string; name: string }) => {
|
||||
acc[file.id] = file.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', context.credentialId!, fileMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
}
|
||||
// Microsoft Teams
|
||||
else if (provider === 'microsoft-teams' && context.teamId) {
|
||||
fetch('/api/tools/microsoft_teams/teams', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credentialId: context.credentialId }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.teams) {
|
||||
const teamMap = data.teams.reduce(
|
||||
(acc: Record<string, string>, team: { id: string; displayName: string }) => {
|
||||
acc[team.id] = team.displayName
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', context.credentialId!, teamMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.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 }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.contacts) {
|
||||
const contactMap = data.contacts.reduce(
|
||||
(acc: Record<string, string>, contact: { id: string; name: string }) => {
|
||||
acc[contact.id] = contact.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore
|
||||
.getState()
|
||||
.setDisplayNames('files', context.credentialId!, contactMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
}
|
||||
// OneDrive files
|
||||
else if (serviceId === 'onedrive' && subBlock.mimeType === 'file') {
|
||||
fetch(`/api/tools/onedrive/files?credentialId=${context.credentialId}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.files) {
|
||||
const fileMap = data.files.reduce(
|
||||
(acc: Record<string, string>, file: { id: string; name: string }) => {
|
||||
acc[file.id] = file.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', context.credentialId!, fileMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
}
|
||||
// OneDrive folders
|
||||
else if (serviceId === 'onedrive' && subBlock.mimeType !== 'file') {
|
||||
fetch(`/api/tools/onedrive/folders?credentialId=${context.credentialId}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.files) {
|
||||
const fileMap = data.files.reduce(
|
||||
(acc: Record<string, string>, file: { id: string; name: string }) => {
|
||||
acc[file.id] = file.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', context.credentialId!, fileMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
}
|
||||
// SharePoint sites
|
||||
else if (serviceId === 'sharepoint') {
|
||||
fetch(`/api/tools/sharepoint/sites?credentialId=${context.credentialId}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.files) {
|
||||
const fileMap = data.files.reduce(
|
||||
(acc: Record<string, string>, file: { id: string; name: string }) => {
|
||||
acc[file.id] = file.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', context.credentialId!, fileMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
}
|
||||
// Microsoft Excel/Word
|
||||
else if (provider === 'microsoft-excel' || provider === 'microsoft-word') {
|
||||
fetch(`/api/auth/oauth/microsoft/files?credentialId=${context.credentialId}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.files) {
|
||||
const fileMap = data.files.reduce(
|
||||
(acc: Record<string, string>, file: { id: string; name: string }) => {
|
||||
acc[file.id] = file.name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', context.credentialId!, fileMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
}
|
||||
// Microsoft Planner tasks
|
||||
else if (provider === 'microsoft-planner' && context.planId) {
|
||||
fetch(
|
||||
`/api/tools/microsoft_planner/tasks?credentialId=${context.credentialId}&planId=${context.planId}`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.tasks) {
|
||||
const taskMap = data.tasks.reduce(
|
||||
(acc: Record<string, string>, task: { id: string; title: string }) => {
|
||||
acc[task.id] = task.title
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
useDisplayNamesStore.getState().setDisplayNames('files', context.credentialId!, taskMap)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsFetching(false))
|
||||
} else {
|
||||
setIsFetching(false)
|
||||
}
|
||||
}, [
|
||||
subBlock?.type,
|
||||
subBlock?.provider,
|
||||
subBlock?.serviceId,
|
||||
subBlock?.mimeType,
|
||||
value,
|
||||
context?.credentialId,
|
||||
context?.provider,
|
||||
context?.domain,
|
||||
context?.projectId,
|
||||
context?.teamId,
|
||||
context?.planId,
|
||||
cachedDisplayName,
|
||||
isFetching,
|
||||
])
|
||||
|
||||
if (!subBlock || !value || typeof value !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Credentials - handled separately by useCredentialDisplay
|
||||
if (subBlock.type === 'oauth-input') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Knowledge Bases - use existing knowledge store
|
||||
if (subBlock.type === 'knowledge-base-selector') {
|
||||
const kb = getCachedKnowledgeBase(value)
|
||||
return kb?.name || null
|
||||
}
|
||||
|
||||
// Return the cached display name (which triggers re-render when populated)
|
||||
return cachedDisplayName
|
||||
}
|
||||
122
apps/sim/stores/display-names/store.ts
Normal file
122
apps/sim/stores/display-names/store.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { create } from 'zustand'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('DisplayNamesStore')
|
||||
|
||||
/**
|
||||
* Generic cache for ID-to-name mappings for all selector types
|
||||
* Structure: { type: { context: { id: name } } }
|
||||
*
|
||||
*/
|
||||
interface DisplayNamesCache {
|
||||
credentials: Record<string, Record<string, string>> // provider -> id -> name
|
||||
channels: Record<string, Record<string, string>> // credentialContext -> id -> name
|
||||
knowledgeBases: Record<string, Record<string, string>> // workspaceId -> id -> name
|
||||
workflows: Record<string, Record<string, string>> // always 'global' -> id -> name
|
||||
files: Record<string, Record<string, string>> // credentialContext -> id -> name
|
||||
folders: Record<string, Record<string, string>> // credentialContext -> id -> name
|
||||
projects: Record<string, Record<string, string>> // provider-credential -> id -> name
|
||||
documents: Record<string, Record<string, string>> // knowledgeBaseId -> id -> name
|
||||
}
|
||||
|
||||
interface DisplayNamesStore {
|
||||
cache: DisplayNamesCache
|
||||
|
||||
/**
|
||||
* Set a display name for an ID
|
||||
*/
|
||||
setDisplayName: (type: keyof DisplayNamesCache, context: string, id: string, name: string) => void
|
||||
|
||||
/**
|
||||
* Set multiple display names at once
|
||||
*/
|
||||
setDisplayNames: (
|
||||
type: keyof DisplayNamesCache,
|
||||
context: string,
|
||||
items: Record<string, string>
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Get a display name for an ID
|
||||
*/
|
||||
getDisplayName: (type: keyof DisplayNamesCache, context: string, id: string) => string | null
|
||||
|
||||
/**
|
||||
* Clear all cached display names for a type/context
|
||||
*/
|
||||
clearContext: (type: keyof DisplayNamesCache, context: string) => void
|
||||
|
||||
/**
|
||||
* Clear all cached display names
|
||||
*/
|
||||
clearAll: () => void
|
||||
}
|
||||
|
||||
const initialCache: DisplayNamesCache = {
|
||||
credentials: {},
|
||||
channels: {},
|
||||
knowledgeBases: {},
|
||||
workflows: {},
|
||||
files: {},
|
||||
folders: {},
|
||||
projects: {},
|
||||
documents: {},
|
||||
}
|
||||
|
||||
export const useDisplayNamesStore = create<DisplayNamesStore>((set, get) => ({
|
||||
cache: initialCache,
|
||||
|
||||
setDisplayName: (type, context, id, name) => {
|
||||
set((state) => ({
|
||||
cache: {
|
||||
...state.cache,
|
||||
[type]: {
|
||||
...state.cache[type],
|
||||
[context]: {
|
||||
...state.cache[type][context],
|
||||
[id]: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
},
|
||||
|
||||
setDisplayNames: (type, context, items) => {
|
||||
set((state) => ({
|
||||
cache: {
|
||||
...state.cache,
|
||||
[type]: {
|
||||
...state.cache[type],
|
||||
[context]: {
|
||||
...state.cache[type][context],
|
||||
...items,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
logger.info(`Cached ${Object.keys(items).length} display names`, { type, context })
|
||||
},
|
||||
|
||||
getDisplayName: (type, context, id) => {
|
||||
const contextCache = get().cache[type][context]
|
||||
return contextCache?.[id] || null
|
||||
},
|
||||
|
||||
clearContext: (type, context) => {
|
||||
set((state) => {
|
||||
const newTypeCache = { ...state.cache[type] }
|
||||
delete newTypeCache[context]
|
||||
return {
|
||||
cache: {
|
||||
...state.cache,
|
||||
[type]: newTypeCache,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
clearAll: () => {
|
||||
set({ cache: initialCache })
|
||||
},
|
||||
}))
|
||||
@@ -50,6 +50,7 @@ export const airtableWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Connect your Airtable account using the "Select Airtable credential" button above.',
|
||||
@@ -70,6 +71,7 @@ export const airtableWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'airtable_webhook',
|
||||
},
|
||||
|
||||
@@ -59,6 +59,7 @@ export const genericWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Copy the webhook URL and use it in your external service or API.',
|
||||
@@ -79,6 +80,7 @@ export const genericWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'generic_webhook',
|
||||
},
|
||||
|
||||
@@ -78,6 +78,7 @@ export const githubIssueClosedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -104,6 +105,7 @@ export const githubIssueClosedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_issue_closed',
|
||||
condition: {
|
||||
|
||||
@@ -78,6 +78,7 @@ export const githubIssueCommentTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -105,6 +106,7 @@ export const githubIssueCommentTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_issue_comment',
|
||||
condition: {
|
||||
|
||||
@@ -99,6 +99,7 @@ export const githubIssueOpenedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -125,6 +126,7 @@ export const githubIssueOpenedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_issue_opened',
|
||||
condition: {
|
||||
|
||||
@@ -79,6 +79,7 @@ export const githubPRClosedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -105,6 +106,7 @@ export const githubPRClosedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_closed',
|
||||
condition: {
|
||||
|
||||
@@ -78,6 +78,7 @@ export const githubPRCommentTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -105,6 +106,7 @@ export const githubPRCommentTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_comment',
|
||||
condition: {
|
||||
|
||||
@@ -78,6 +78,7 @@ export const githubPRMergedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -104,6 +105,7 @@ export const githubPRMergedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_merged',
|
||||
condition: {
|
||||
|
||||
@@ -78,6 +78,7 @@ export const githubPROpenedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -104,6 +105,7 @@ export const githubPROpenedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_opened',
|
||||
condition: {
|
||||
|
||||
@@ -79,6 +79,7 @@ export const githubPRReviewedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -105,6 +106,7 @@ export const githubPRReviewedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_reviewed',
|
||||
condition: {
|
||||
|
||||
@@ -78,6 +78,7 @@ export const githubPushTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -104,6 +105,7 @@ export const githubPushTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_push',
|
||||
condition: {
|
||||
|
||||
@@ -78,6 +78,7 @@ export const githubReleasePublishedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -104,6 +105,7 @@ export const githubReleasePublishedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_release_published',
|
||||
condition: {
|
||||
|
||||
@@ -75,6 +75,7 @@ export const githubWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -101,6 +102,7 @@ export const githubWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_webhook',
|
||||
condition: {
|
||||
|
||||
@@ -79,6 +79,7 @@ export const githubWorkflowRunTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your GitHub Repository > Settings > Webhooks.',
|
||||
@@ -105,6 +106,7 @@ export const githubWorkflowRunTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_workflow_run',
|
||||
condition: {
|
||||
|
||||
@@ -105,6 +105,7 @@ export const gmailPollingTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Connect your Gmail account using OAuth credentials',
|
||||
@@ -122,6 +123,7 @@ export const gmailPollingTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'gmail_poller',
|
||||
},
|
||||
|
||||
@@ -62,6 +62,7 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Open your Google Form → More (⋮) → Script editor.',
|
||||
@@ -148,6 +149,7 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'google_forms_webhook',
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@ export function getTrigger(triggerId: string): TriggerConfig {
|
||||
readOnly: true,
|
||||
collapsible: true,
|
||||
defaultCollapsed: true,
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
|
||||
@@ -59,6 +59,7 @@ export const jiraIssueCommentedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('comment_created'),
|
||||
mode: 'trigger',
|
||||
@@ -71,6 +72,7 @@ export const jiraIssueCommentedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'jira_issue_commented',
|
||||
condition: {
|
||||
|
||||
@@ -68,6 +68,7 @@ export const jiraIssueCreatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('jira:issue_created'),
|
||||
mode: 'trigger',
|
||||
@@ -80,6 +81,7 @@ export const jiraIssueCreatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'jira_issue_created',
|
||||
condition: {
|
||||
|
||||
@@ -59,6 +59,7 @@ export const jiraIssueDeletedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('jira:issue_deleted'),
|
||||
mode: 'trigger',
|
||||
@@ -71,6 +72,7 @@ export const jiraIssueDeletedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'jira_issue_deleted',
|
||||
condition: {
|
||||
|
||||
@@ -73,6 +73,7 @@ export const jiraIssueUpdatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('jira:issue_updated'),
|
||||
mode: 'trigger',
|
||||
@@ -85,6 +86,7 @@ export const jiraIssueUpdatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'jira_issue_updated',
|
||||
condition: {
|
||||
|
||||
@@ -46,6 +46,7 @@ export const jiraWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('All Events'),
|
||||
mode: 'trigger',
|
||||
@@ -58,6 +59,7 @@ export const jiraWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'jira_webhook',
|
||||
condition: {
|
||||
|
||||
@@ -59,6 +59,7 @@ export const jiraWorklogCreatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('worklog_created'),
|
||||
mode: 'trigger',
|
||||
@@ -71,6 +72,7 @@ export const jiraWorklogCreatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'jira_worklog_created',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearCommentCreatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Comment (create)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearCommentCreatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_comment_created',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearCommentUpdatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Comment (update)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearCommentUpdatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_comment_updated',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearCustomerRequestCreatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Customer Requests'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearCustomerRequestCreatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_customer_request_created',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearCustomerRequestUpdatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('CustomerNeed (update)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearCustomerRequestUpdatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_customer_request_updated',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearCycleCreatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Cycle (create)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearCycleCreatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_cycle_created',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearCycleUpdatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Cycle (update)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearCycleUpdatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_cycle_updated',
|
||||
condition: {
|
||||
|
||||
@@ -51,6 +51,7 @@ export const linearIssueCreatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Issue (create)'),
|
||||
mode: 'trigger',
|
||||
@@ -63,6 +64,7 @@ export const linearIssueCreatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_issue_created',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearIssueRemovedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Issue (remove)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearIssueRemovedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_issue_removed',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearIssueUpdatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Issue (update)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearIssueUpdatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_issue_updated',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearLabelCreatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('IssueLabel (create)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearLabelCreatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_label_created',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearLabelUpdatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('IssueLabel (update)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearLabelUpdatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_label_updated',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearProjectCreatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Project (create)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearProjectCreatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_project_created',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearProjectUpdateCreatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('ProjectUpdate (create)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearProjectUpdateCreatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_project_update_created',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearProjectUpdatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Project (update)'),
|
||||
mode: 'trigger',
|
||||
@@ -54,6 +55,7 @@ export const linearProjectUpdatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_project_updated',
|
||||
condition: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const linearWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions(
|
||||
'all events',
|
||||
@@ -57,6 +58,7 @@ export const linearWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_webhook',
|
||||
condition: {
|
||||
|
||||
@@ -54,6 +54,7 @@ export const microsoftTeamsChatSubscriptionTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Connect your Microsoft Teams account and grant the required permissions.',
|
||||
@@ -75,6 +76,7 @@ export const microsoftTeamsChatSubscriptionTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'microsoftteams_chat_subscription',
|
||||
condition: {
|
||||
|
||||
@@ -54,6 +54,7 @@ export const microsoftTeamsWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Open Microsoft Teams and go to the team where you want to add the webhook.',
|
||||
@@ -79,6 +80,7 @@ export const microsoftTeamsWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'microsoftteams_webhook',
|
||||
condition: {
|
||||
|
||||
@@ -95,6 +95,7 @@ export const outlookPollingTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Connect your Microsoft account using OAuth credentials',
|
||||
@@ -112,6 +113,7 @@ export const outlookPollingTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'outlook_poller',
|
||||
},
|
||||
|
||||
@@ -48,12 +48,14 @@ export const slackWebhookTrigger: TriggerConfig = {
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.join(''),
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'slack_webhook',
|
||||
},
|
||||
|
||||
@@ -168,6 +168,7 @@ export const stripeWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your Stripe Dashboard at <a href="https://dashboard.stripe.com/webhooks" target="_blank" rel="noopener noreferrer">https://dashboard.stripe.com/webhooks</a>',
|
||||
@@ -190,6 +191,7 @@ export const stripeWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'stripe_webhook',
|
||||
},
|
||||
|
||||
@@ -33,6 +33,7 @@ export const telegramWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Message "/newbot" to <a href="https://t.me/BotFather" target="_blank" rel="noopener noreferrer" class="text-muted-foreground underline transition-colors hover:text-muted-foreground/80">@BotFather</a> in Telegram to create a bot and copy its token.',
|
||||
@@ -50,6 +51,7 @@ export const telegramWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'telegram_webhook',
|
||||
},
|
||||
|
||||
@@ -52,6 +52,7 @@ export const twilioVoiceWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Enter a TwiML Response above - this tells Twilio what to do when a call comes in (e.g., play a message, record, gather input). Note: Use square brackets [Tag] instead of angle brackets for TwiML tags',
|
||||
@@ -72,6 +73,7 @@ export const twilioVoiceWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'twilio_voice_webhook',
|
||||
},
|
||||
|
||||
@@ -64,6 +64,7 @@ export const typeformWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Get your Typeform Personal Access Token from <a href="https://admin.typeform.com/account#/section/tokens" target="_blank" rel="noopener noreferrer">https://admin.typeform.com/account#/section/tokens</a>',
|
||||
@@ -84,6 +85,7 @@ export const typeformWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'typeform_webhook',
|
||||
},
|
||||
|
||||
@@ -56,6 +56,7 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Connect your Webflow account using the "Select Webflow credential" button above.',
|
||||
@@ -80,6 +81,7 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_collection_item_changed',
|
||||
condition: {
|
||||
|
||||
@@ -69,6 +69,7 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Connect your Webflow account using the "Select Webflow credential" button above.',
|
||||
@@ -93,6 +94,7 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_collection_item_created',
|
||||
condition: {
|
||||
|
||||
@@ -56,6 +56,7 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Connect your Webflow account using the "Select Webflow credential" button above.',
|
||||
@@ -81,6 +82,7 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_collection_item_deleted',
|
||||
condition: {
|
||||
|
||||
@@ -43,6 +43,7 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Connect your Webflow account using the "Select Webflow credential" button above.',
|
||||
@@ -64,6 +65,7 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_form_submission',
|
||||
},
|
||||
|
||||
@@ -34,6 +34,7 @@ export const whatsappWebhookTrigger: TriggerConfig = {
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'Go to your <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener noreferrer" class="text-muted-foreground underline transition-colors hover:text-muted-foreground/80">Meta for Developers Apps</a> page and navigate to the "Build with us" --> "App Events" section.',
|
||||
@@ -56,6 +57,7 @@ export const whatsappWebhookTrigger: TriggerConfig = {
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'whatsapp_webhook',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user