Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client'

import { useRef } from 'react'

export function useFetchAttemptGuard() {
const lastAttemptKeyRef = useRef<string>('')

const shouldAttempt = (key: string): boolean => {
return lastAttemptKeyRef.current !== key
}

const markAttempt = (key: string) => {
lastAttemptKeyRef.current = key
}

const reset = () => {
lastAttemptKeyRef.current = ''
}

return { shouldAttempt, markAttempt, reset }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
CommandList,
} from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { useFetchAttemptGuard } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/hooks/use-fetch-attempt-guard'

export interface SlackChannelInfo {
id: string
Expand Down Expand Up @@ -43,8 +44,8 @@ export function SlackChannelSelector({
const [open, setOpen] = useState(false)
const [selectedChannel, setSelectedChannel] = useState<SlackChannelInfo | null>(null)
const [initialFetchDone, setInitialFetchDone] = useState(false)
const { shouldAttempt, markAttempt, reset } = useFetchAttemptGuard()

// Fetch channels from Slack API
const fetchChannels = useCallback(async () => {
if (!credential) return

Expand All @@ -66,28 +67,28 @@ export function SlackChannelSelector({
setChannels([])
} else {
setChannels(data.channels)
setInitialFetchDone(true)
}
} catch (err) {
if ((err as Error).name === 'AbortError') return
setError((err as Error).message)
setChannels([])
} finally {
setInitialFetchDone(true)
setLoading(false)
}
}, [credential])

// Handle dropdown open/close - fetch channels when opening
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen)

// Only fetch channels when opening the dropdown and if we have valid credential
if (isOpen && credential && (!initialFetchDone || channels.length === 0)) {
const attemptKey = `slack-channels:${credential}`
if (!shouldAttempt(attemptKey)) return
markAttempt(attemptKey)
fetchChannels()
}
}

// Sync selected channel with value prop
useEffect(() => {
if (value && channels.length > 0) {
const channelInfo = channels.find((c) => c.id === value)
Expand All @@ -97,12 +98,13 @@ export function SlackChannelSelector({
}
}, [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()
const attemptKey = `slack-channels:init:${credential}`
if (shouldAttempt(attemptKey)) {
markAttempt(attemptKey)
fetchChannels()
}
}
}, [value, selectedChannel, loading, initialFetchDone, credential, fetchChannels])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getServiceIdFromScopes,
type OAuthProvider,
} from '@/lib/oauth'
import { useFetchAttemptGuard } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/hooks/use-fetch-attempt-guard'
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal'

const logger = createLogger('ConfluenceFileSelector')
Expand Down Expand Up @@ -75,33 +76,30 @@ export function ConfluenceFileSelector({
const [showOAuthModal, setShowOAuthModal] = useState(false)
const initialFetchRef = useRef(false)
const [error, setError] = useState<string | null>(null)
// Keep internal credential in sync with prop (handles late arrival and BFCache restores)
const { shouldAttempt, markAttempt, reset } = useFetchAttemptGuard()

useEffect(() => {
if (credentialId && credentialId !== selectedCredentialId) {
setSelectedCredentialId(credentialId)
}
}, [credentialId, selectedCredentialId])

// Handle search with debounce
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)

const handleSearch = (value: string) => {
// Clear any existing timeout
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current)
}

// Set a new timeout
searchTimeoutRef.current = setTimeout(() => {
if (value.length > 2) {
fetchFiles(value)
} else if (value.length === 0) {
fetchFiles()
}
}, 500) // 500ms debounce
}, 500)
}

// Clean up the timeout on unmount
useEffect(() => {
return () => {
if (searchTimeoutRef.current) {
Expand All @@ -110,19 +108,16 @@ export function ConfluenceFileSelector({
}
}, [])

// Determine the appropriate service ID based on provider and scopes
const getServiceId = (): string => {
if (serviceId) return serviceId
return getServiceIdFromScopes(provider, requiredScopes)
}

// Determine the appropriate provider ID based on service and scopes
const getProviderId = (): string => {
const effectiveServiceId = getServiceId()
return getProviderIdFromServiceId(effectiveServiceId)
}

// Fetch available credentials for this provider
const fetchCredentials = useCallback(async () => {
setIsLoading(true)
try {
Expand All @@ -140,12 +135,10 @@ export function ConfluenceFileSelector({
}
}, [provider, getProviderId, selectedCredentialId])

// Fetch page info when we have a selected file ID
const fetchPageInfo = useCallback(
async (pageId: string) => {
if (!selectedCredentialId || !domain) return

// Validate domain format
const trimmedDomain = domain.trim().toLowerCase()
if (!trimmedDomain.includes('.')) {
setError(
Expand All @@ -158,7 +151,6 @@ export function ConfluenceFileSelector({
setError(null)

try {
// Get the access token from the selected credential
const tokenResponse = await fetch('/api/auth/oauth/token', {
method: 'POST',
headers: {
Expand All @@ -178,7 +170,6 @@ export function ConfluenceFileSelector({
const tokenData = await tokenResponse.json()
const accessToken = tokenData.accessToken

// Use the access token to fetch the page info
const response = await fetch('/api/tools/confluence/page', {
method: 'POST',
headers: {
Expand Down Expand Up @@ -223,13 +214,11 @@ export function ConfluenceFileSelector({
[selectedCredentialId, domain, onFileInfoChange, workflowId]
)

// Fetch pages from Confluence
const fetchFiles = useCallback(
async (searchQuery?: string) => {
if (!selectedCredentialId || !domain) return
if (isForeignCredential) return

// Validate domain format
const trimmedDomain = domain.trim().toLowerCase()
if (!trimmedDomain.includes('.')) {
setError(
Expand All @@ -244,7 +233,6 @@ export function ConfluenceFileSelector({
setError(null)

try {
// Get the access token from the selected credential
const tokenResponse = await fetch('/api/auth/oauth/token', {
method: 'POST',
headers: {
Expand All @@ -260,7 +248,6 @@ export function ConfluenceFileSelector({
const errorData = await tokenResponse.json()
logger.error('Access token error:', errorData)

// If there's a token error, we might need to reconnect the account
setError('Authentication failed. Please reconnect your Confluence account.')
setIsLoading(false)
return
Expand All @@ -276,7 +263,6 @@ export function ConfluenceFileSelector({
return
}

// Simply fetch pages directly using the endpoint
const response = await fetch('/api/tools/confluence/pages', {
method: 'POST',
headers: {
Expand Down Expand Up @@ -306,14 +292,12 @@ 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
if (selectedFileId) {
const fileInfo = data.files.find((file: ConfluenceFileInfo) => file.id === selectedFileId)
if (fileInfo) {
setSelectedFile(fileInfo)
onFileInfoChange?.(fileInfo)
} else if (!searchQuery && selectedFileId) {
// If we can't find the file in the list, try to fetch it directly
fetchPageInfo(selectedFileId)
}
}
Expand All @@ -336,27 +320,26 @@ export function ConfluenceFileSelector({
]
)

// Fetch credentials on initial mount
useEffect(() => {
if (!initialFetchRef.current) {
fetchCredentials()
initialFetchRef.current = true
}
}, [fetchCredentials])

// Only fetch files when the dropdown is opened, not on credential selection
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen)

// Only fetch files when opening the dropdown and if we have valid credentials and domain
if (isOpen && !isForeignCredential && selectedCredentialId && domain && domain.includes('.')) {
fetchFiles()
}
}

// Fetch the selected page metadata once credentials and domain are ready or changed
useEffect(() => {
if (value && selectedCredentialId && !selectedFile && domain && domain.includes('.')) {
const key = `${selectedCredentialId}:${domain}:confluence:${value}`
if (!shouldAttempt(key)) return
markAttempt(key)
fetchPageInfo(value)
}
}, [
Expand All @@ -369,22 +352,23 @@ export function ConfluenceFileSelector({
isForeignCredential,
])

// Keep internal selectedFileId in sync with the value prop
useEffect(() => {
reset()
}, [selectedCredentialId, domain, reset])

useEffect(() => {
if (value !== selectedFileId) {
setSelectedFileId(value)
}
}, [value])

// Clear preview when value is cleared (e.g., collaborator cleared or domain change cascade)
useEffect(() => {
if (!value) {
setSelectedFile(null)
onFileInfoChange?.(null)
}
}, [value, onFileInfoChange])

// Handle file selection
const handleSelectFile = (file: ConfluenceFileInfo) => {
setSelectedFileId(file.id)
setSelectedFile(file)
Expand All @@ -393,14 +377,12 @@ export function ConfluenceFileSelector({
setOpen(false)
}

// Handle adding a new credential
const handleAddCredential = () => {
// Show the OAuth modal
setShowOAuthModal(true)
setOpen(false)
}

// Clear selection
const handleClearSelection = () => {
setSelectedFileId('')
setSelectedFile(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { useFetchAttemptGuard } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/hooks/use-fetch-attempt-guard'

const logger = createLogger('GoogleCalendarSelector')

Expand Down Expand Up @@ -55,6 +56,7 @@ export function GoogleCalendarSelector({
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [initialFetchDone, setInitialFetchDone] = useState(false)
const { shouldAttempt, markAttempt, reset } = useFetchAttemptGuard()

const fetchCalendarsFromAPI = useCallback(async (): Promise<GoogleCalendarInfo[]> => {
if (!credentialId) {
Expand Down Expand Up @@ -141,25 +143,28 @@ export function GoogleCalendarSelector({
}
}, [fetchCalendarsFromAPI, selectedCalendarId, onCalendarInfoChange])

// Fetch selected calendar info when component mounts or dependencies change
useEffect(() => {
if (value && credentialId && (!selectedCalendar || selectedCalendar.id !== value)) {
const key = `${credentialId}:gcal:${value}`
if (!shouldAttempt(key)) return
markAttempt(key)
fetchSelectedCalendarInfo()
}
}, [value, credentialId, selectedCalendar, fetchSelectedCalendarInfo])

// Sync with external value
useEffect(() => {
reset()
}, [credentialId, reset])

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()
}
Expand All @@ -177,7 +182,6 @@ export function GoogleCalendarSelector({
onCalendarInfoChange,
])

// Handle calendar selection
const handleSelectCalendar = (calendar: GoogleCalendarInfo) => {
setSelectedCalendarId(calendar.id)
setSelectedCalendar(calendar)
Expand All @@ -186,7 +190,6 @@ export function GoogleCalendarSelector({
setOpen(false)
}

// Clear selection
const handleClearSelection = () => {
setSelectedCalendarId('')
setSelectedCalendar(null)
Expand All @@ -195,7 +198,6 @@ export function GoogleCalendarSelector({
setError(null)
}

// Get calendar display name
const getCalendarDisplayName = (calendar: GoogleCalendarInfo) => {
if (calendar.primary) {
return `${calendar.summary} (Primary)`
Expand Down
Loading