import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { formatBytes } from '../utils/stringUtils'; import { formatDate } from '../utils/dateTimeUtils'; import { getSessionToken, setSessionToken, removeSessionToken, } from '../utils/providerUtils'; import ConnectorAuth from '../components/ConnectorAuth'; import FileIcon from '../assets/file.svg'; import FolderIcon from '../assets/folder.svg'; import CheckIcon from '../assets/checkmark.svg'; import SearchIcon from '../assets/search.svg'; import Input from './Input'; import { Table, TableContainer, TableHead, TableBody, TableRow, TableHeader, TableCell, } from './Table'; interface CloudFile { id: string; name: string; type: string; size?: number; modifiedTime: string; isFolder?: boolean; } interface CloudFilePickerProps { onSelectionChange: ( selectedFileIds: string[], selectedFolderIds?: string[], ) => void; onDisconnect?: () => void; provider: string; token: string | null; initialSelectedFiles?: string[]; initialSelectedFolders?: string[]; } export const FilePicker: React.FC = ({ onSelectionChange, onDisconnect, provider, token, initialSelectedFiles = [], }) => { const PROVIDER_CONFIG = { google_drive: { displayName: 'Drive', rootName: 'My Drive', }, } as const; const getProviderConfig = (provider: string) => { return ( PROVIDER_CONFIG[provider as keyof typeof PROVIDER_CONFIG] || { displayName: provider, rootName: 'Root', } ); }; const { t } = useTranslation(); const [files, setFiles] = useState([]); const [selectedFiles, setSelectedFiles] = useState(initialSelectedFiles); const [selectedFolders, setSelectedFolders] = useState([]); const [isLoading, setIsLoading] = useState(false); const [hasMoreFiles, setHasMoreFiles] = useState(false); const [nextPageToken, setNextPageToken] = useState(null); const [currentFolderId, setCurrentFolderId] = useState(null); const [folderPath, setFolderPath] = useState< Array<{ id: string | null; name: string }> >([ { id: null, name: getProviderConfig(provider).rootName, }, ]); const [searchQuery, setSearchQuery] = useState(''); const [authError, setAuthError] = useState(''); const [isConnected, setIsConnected] = useState(false); const [userEmail, setUserEmail] = useState(''); const scrollContainerRef = useRef(null); const searchTimeoutRef = useRef | null>(null); const isFolder = (file: CloudFile) => { return ( file.isFolder || file.type === 'application/vnd.google-apps.folder' || file.type === 'folder' ); }; const loadCloudFiles = useCallback( async ( sessionToken: string, folderId: string | null, pageToken?: string, searchQuery = '', ) => { setIsLoading(true); const apiHost = import.meta.env.VITE_API_HOST; if (!pageToken) { setFiles([]); } try { const response = await fetch(`${apiHost}/api/connectors/files`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ provider: provider, session_token: sessionToken, folder_id: folderId, limit: 10, page_token: pageToken, search_query: searchQuery, }), }); const data = await response.json(); if (data.success) { setFiles((prev) => pageToken ? [...prev, ...data.files] : data.files, ); setNextPageToken(data.next_page_token); setHasMoreFiles(!!data.next_page_token); } else { console.error('Error loading files:', data.error); if (!pageToken) { setFiles([]); } } } catch (err) { console.error('Error loading files:', err); if (!pageToken) { setFiles([]); } } finally { setIsLoading(false); } }, [token, provider], ); const validateAndLoadFiles = useCallback(async () => { const sessionToken = getSessionToken(provider); if (!sessionToken) { setIsConnected(false); return; } try { const apiHost = import.meta.env.VITE_API_HOST; const validateResponse = await fetch( `${apiHost}/api/connectors/validate-session`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ provider: provider, session_token: sessionToken, }), }, ); if (!validateResponse.ok) { removeSessionToken(provider); setIsConnected(false); setAuthError('Session expired. Please reconnect to Google Drive.'); return; } const validateData = await validateResponse.json(); if (validateData.success) { setUserEmail(validateData.user_email || 'Connected User'); setIsConnected(true); setAuthError(''); setFiles([]); setNextPageToken(null); setHasMoreFiles(false); setCurrentFolderId(null); setFolderPath([ { id: null, name: getProviderConfig(provider).rootName, }, ]); loadCloudFiles(sessionToken, null, undefined, ''); } else { removeSessionToken(provider); setIsConnected(false); setAuthError( validateData.error || 'Session expired. Please reconnect your account.', ); } } catch (error) { console.error('Error validating session:', error); setAuthError('Failed to validate session. Please reconnect.'); setIsConnected(false); } }, [provider, token, loadCloudFiles]); useEffect(() => { validateAndLoadFiles(); }, [validateAndLoadFiles]); const handleScroll = useCallback(() => { const scrollContainer = scrollContainerRef.current; if (!scrollContainer) return; const { scrollTop, scrollHeight, clientHeight } = scrollContainer; const isNearBottom = scrollHeight - scrollTop - clientHeight < 50; if (isNearBottom && hasMoreFiles && !isLoading && nextPageToken) { const sessionToken = getSessionToken(provider); if (sessionToken) { loadCloudFiles( sessionToken, currentFolderId, nextPageToken, searchQuery, ); } } }, [ hasMoreFiles, isLoading, nextPageToken, currentFolderId, searchQuery, provider, loadCloudFiles, ]); useEffect(() => { const scrollContainer = scrollContainerRef.current; if (scrollContainer) { scrollContainer.addEventListener('scroll', handleScroll); return () => scrollContainer.removeEventListener('scroll', handleScroll); } }, [handleScroll]); useEffect(() => { return () => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } }; }, []); const handleSearchChange = (query: string) => { setSearchQuery(query); if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } searchTimeoutRef.current = setTimeout(() => { const sessionToken = getSessionToken(provider); if (sessionToken) { loadCloudFiles(sessionToken, currentFolderId, undefined, query); } }, 300); }; const handleFolderClick = (folderId: string, folderName: string) => { if (folderId === currentFolderId) { return; } setIsLoading(true); setCurrentFolderId(folderId); setFolderPath((prev) => [...prev, { id: folderId, name: folderName }]); setSearchQuery(''); const sessionToken = getSessionToken(provider); if (sessionToken) { loadCloudFiles(sessionToken, folderId, undefined, ''); } }; const navigateBack = (index: number) => { if (index >= folderPath.length - 1) return; const newFolderPath = folderPath.slice(0, index + 1); const newFolderId = newFolderPath[newFolderPath.length - 1].id; setFolderPath(newFolderPath); setCurrentFolderId(newFolderId); setSearchQuery(''); const sessionToken = getSessionToken(provider); if (sessionToken) { loadCloudFiles(sessionToken, newFolderId, undefined, ''); } }; const handleFileSelect = (fileId: string, isFolder: boolean) => { if (isFolder) { const newSelectedFolders = selectedFolders.includes(fileId) ? selectedFolders.filter((id) => id !== fileId) : [...selectedFolders, fileId]; setSelectedFolders(newSelectedFolders); onSelectionChange(selectedFiles, newSelectedFolders); } else { const newSelectedFiles = selectedFiles.includes(fileId) ? selectedFiles.filter((id) => id !== fileId) : [...selectedFiles, fileId]; setSelectedFiles(newSelectedFiles); onSelectionChange(newSelectedFiles, selectedFolders); } }; return (
{authError && (
{authError}
)} { setUserEmail(data.user_email || 'Connected User'); setIsConnected(true); setAuthError(''); if (data.session_token) { setSessionToken(provider, data.session_token); loadCloudFiles(data.session_token, null); } }} onError={(error) => { setAuthError(error); setIsConnected(false); }} isConnected={isConnected} userEmail={userEmail} onDisconnect={() => { const sessionToken = getSessionToken(provider); if (sessionToken) { const apiHost = import.meta.env.VITE_API_HOST; fetch(`${apiHost}/api/connectors/disconnect`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ provider: provider, session_token: sessionToken, }), }).catch((err) => console.error( `Error disconnecting from ${getProviderConfig(provider).displayName}:`, err, ), ); } removeSessionToken(provider); setIsConnected(false); setFiles([]); setSelectedFiles([]); onSelectionChange([]); if (onDisconnect) { onDisconnect(); } }} /> {isConnected && (
{/* Breadcrumb navigation */}
{folderPath.map((path, index) => (
{index > 0 && /}
))}
Select Files from {getProviderConfig(provider).displayName}
handleSearchChange(e.target.value)} colorVariant="silver" borderVariant="thin" labelBgClassName="bg-[#EEE6FF78] dark:bg-[#2A262E]" leftIcon={ Search } />
{/* Selected Files Message */}
{t('filePicker.itemsSelected', { count: selectedFiles.length + selectedFolders.length, })}
{ <> {t('filePicker.name')} {t('filePicker.lastModified')} {t('filePicker.size')} {files.map((file, index) => ( { if (isFolder(file)) { handleFolderClick(file.id, file.name); } else { handleFileSelect(file.id, false); } }} >
{ e.stopPropagation(); handleFileSelect(file.id, isFolder(file)); }} > {(isFolder(file) ? selectedFolders : selectedFiles ).includes(file.id) && ( Selected )}
{isFolder(file)
{file.name}
{formatDate(file.modifiedTime)} {file.size ? formatBytes(file.size) : '-'}
))}
{isLoading && (
Loading more files...
)} }
)}
); };