From 6366663f03a30e55e1a680068d487bf59a2eb0ef Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 8 Sep 2025 19:09:19 +0530 Subject: [PATCH] (refactor:uploads) separate file picker --- frontend/src/components/FilePicker.tsx | 480 ++++++++++++++++++++++++ frontend/src/upload/Upload.tsx | 498 +++---------------------- 2 files changed, 524 insertions(+), 454 deletions(-) create mode 100644 frontend/src/components/FilePicker.tsx diff --git a/frontend/src/components/FilePicker.tsx b/frontend/src/components/FilePicker.tsx new file mode 100644 index 00000000..8c95c6a2 --- /dev/null +++ b/frontend/src/components/FilePicker.tsx @@ -0,0 +1,480 @@ +import React, { useState, useEffect, useCallback, useRef } from 'react'; +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'; + +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 [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>([{ + id: null, + name: 'Drive' + }]); + const [searchQuery, setSearchQuery] = useState(''); + const [authError, setAuthError] = useState(''); + const [isConnected, setIsConnected] = useState(false); + + 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 providerDisplayNames = { + google_drive: 'Drive', + }; + + const getConnectorDisplayName = (provider: string) => { + return providerDisplayNames[provider as keyof typeof providerDisplayNames] || provider; + }; + + const loadCloudFiles = useCallback( + async ( + sessionToken: string, + folderId: string | null, + pageToken?: string, + searchQuery: string = '' + ) => { + 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) { + + setIsConnected(true); + setAuthError(''); + + setFiles([]); + setNextPageToken(null); + setHasMoreFiles(false); + setCurrentFolderId(null); + setFolderPath([{id: null, name: provider === 'google_drive' ? 'My Drive' : + provider === 'onedrive' ? 'My OneDrive' : + provider === 'sharepoint' ? 'SharePoint' : 'Root'}]); + 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); + } + }; + + // Render authentication UI + if (!isConnected) { + return ( +
+ {authError && ( +
{authError}
+ )} + { + setIsConnected(true); + setAuthError(''); + + if (data.session_token) { + setSessionToken(provider, data.session_token); + loadCloudFiles(data.session_token, null); + } + }} + onError={(error) => { + setAuthError(error); + setIsConnected(false); + }} + /> +
+ ); + } + + // Render file browser UI + return ( +
+ {/* Connected state indicator */} +
+
+
+ + + + Connected to {getConnectorDisplayName(provider)} +
+ +
+
+ +
+
+ {/* Breadcrumb navigation */} +
+ {folderPath.map((path, index) => ( +
+ {index > 0 && /} + +
+ ))} +
+ + {/* Search input */} +
+
+ handleSearchChange(e.target.value)} + className="w-full px-3 py-2 pr-10 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" + /> + +
+
+ +
+

+ Select Files from {getConnectorDisplayName(provider)} +

+ + {selectedFiles.length + selectedFolders.length > 0 + ? `${selectedFiles.length + selectedFolders.length} item${(selectedFiles.length + selectedFolders.length) !== 1 ? 's' : ''} selected` + : '' + } + +
+
+ +
+ {isLoading && files.length === 0 ? ( +
+
+
+ Loading files... +
+
+ ) : files.length === 0 ? ( +
+ No files found in your {getConnectorDisplayName(provider)} +
+ ) : ( + <> +
+ {files.map((file, index) => ( +
+
+
{ + e.stopPropagation(); + handleFileSelect(file.id, isFolder(file)); + }} + > +
+ {(isFolder(file) ? selectedFolders : selectedFiles).includes(file.id) && ( + Selected + )} +
+
+
{ + if (isFolder(file)) { + handleFolderClick(file.id, file.name); + } else { + handleFileSelect(file.id, false); + } + }} + > +
+ {isFolder(file) +
+
+

+ {file.name} +

+

+ {file.size && `${formatBytes(file.size)} • `}Modified {formatDate(file.modifiedTime)} +

+
+
+
+
+ ))} +
+ + {isLoading && ( +
+
+
+ Loading more files... +
+
+ )} + + )} +
+
+
+ ); +}; diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 44369162..76a6cd36 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -1,12 +1,10 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import userService from '../api/services/userService'; -import { getSessionToken, setSessionToken, removeSessionToken } from '../utils/providerUtils'; -import { formatDate } from '../utils/dateTimeUtils'; -import { formatBytes } from '../utils/stringUtils'; +import { getSessionToken } from '../utils/providerUtils'; import FileUpload from '../assets/file_upload.svg'; import WebsiteCollect from '../assets/website_collect.svg'; import Dropdown from '../components/Dropdown'; @@ -28,11 +26,8 @@ import { IngestorFormSchemas, IngestorType, } from './types/ingestor'; -import FileIcon from '../assets/file.svg'; -import FolderIcon from '../assets/folder.svg'; -import SearchIcon from '../assets/search.svg'; -import CheckIcon from '../assets/checkmark.svg'; -import ConnectorAuth from '../components/ConnectorAuth'; + +import {FilePicker} from '../components/FilePicker'; function Upload({ receivedFile = [], @@ -56,22 +51,17 @@ function Upload({ const [activeTab, setActiveTab] = useState(renderTab); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); - // Google Drive state - const [isGoogleDriveConnected, setIsGoogleDriveConnected] = useState(false); - const [googleDriveFiles, setGoogleDriveFiles] = useState([]); + // Connector state const [selectedFiles, setSelectedFiles] = useState([]); - const [isLoadingFiles, setIsLoadingFiles] = useState(false); - const [isAuthenticating, setIsAuthenticating] = useState(false); - const [userEmail, setUserEmail] = useState(''); - const [authError, setAuthError] = useState(''); - const [currentFolderId, setCurrentFolderId] = useState(null); - const [folderPath, setFolderPath] = useState>([{id: null, name: 'My Drive'}]); - const [searchQuery, setSearchQuery] = useState(''); + const [selectedFolders, setSelectedFolders] = useState([]); - const [nextPageToken, setNextPageToken] = useState(null); - const [hasMoreFiles, setHasMoreFiles] = useState(false); - const scrollContainerRef = useRef(null); - const searchTimeoutRef = useRef(null); + // Helper function to check if ingestor type is a connector + const isConnectorType = (type: string) => { + return type === 'google_drive' || type === 'onedrive' || type === 'sharepoint'; + }; + + + const renderFormFields = () => { const schema = IngestorFormSchemas[ingestor.type]; @@ -456,26 +446,16 @@ function Upload({ let configData; - if (ingestor.type === 'google_drive') { + if (isConnectorType(ingestor.type)) { const sessionToken = getSessionToken(ingestor.type); - const selectedItems = googleDriveFiles.filter(file => selectedFiles.includes(file.id)); - const selectedFolderIds = selectedItems - .filter(item => item.type === 'application/vnd.google-apps.folder' || item.isFolder) - .map(folder => folder.id); - - const selectedFileIds = selectedItems - .filter(item => item.type !== 'application/vnd.google-apps.folder' && !item.isFolder) - .map(file => file.id); - configData = { - file_ids: selectedFileIds, - folder_ids: selectedFolderIds, - recursive: ingestor.config.recursive, - session_token: sessionToken || null + provider: ingestor.type, + session_token: sessionToken, + file_ids: selectedFiles, + folder_ids: selectedFolders, }; } else { - configData = { ...ingestor.config }; } @@ -507,185 +487,24 @@ function Upload({ xhr.send(formData); }; - useEffect(() => { - if (ingestor.type === 'google_drive') { - const sessionToken = getSessionToken(ingestor.type); - - if (sessionToken) { - // Auto-authenticate if session token exists - setIsGoogleDriveConnected(true); - setAuthError(''); - - // Fetch user email and files using the existing session token - - fetchUserEmailAndLoadFiles(sessionToken); - } - } - }, [ingestor.type]); - - const fetchUserEmailAndLoadFiles = async (sessionToken: string) => { - 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: 'google_drive', session_token: sessionToken }) - }); - - if (!validateResponse.ok) { - removeSessionToken(ingestor.type); - setIsGoogleDriveConnected(false); - setAuthError('Session expired. Please reconnect to Google Drive.'); - return; - } - - const validateData = await validateResponse.json(); - - if (validateData.success) { - setUserEmail(validateData.user_email || 'Connected User'); - // reset pagination state and files - setGoogleDriveFiles([]); + - setNextPageToken(null); - setHasMoreFiles(false); - loadGoogleDriveFiles(sessionToken, null, undefined, ''); - } else { - removeSessionToken(ingestor.type); - setIsGoogleDriveConnected(false); - setAuthError(validateData.error || 'Session expired. Please reconnect your Google Drive account and make sure to grant offline access.'); - } - } catch (error) { - console.error('Error validating Google Drive session:', error); - setAuthError('Failed to validate session. Please reconnect.'); - setIsGoogleDriveConnected(false); - } - }; - - const loadGoogleDriveFiles = useCallback( - ( - sessionToken: string, - folderId: string | null, - pageToken?: string, - searchQuery: string = '' - ) => { - - setIsLoadingFiles(true); - const apiHost = import.meta.env.VITE_API_HOST; - if (!pageToken) { - setGoogleDriveFiles([]); - } - - fetch(`${apiHost}/api/connectors/files`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ - provider: 'google_drive', - session_token: sessionToken, - folder_id: folderId, - limit: 10, - page_token: pageToken, - search_query: searchQuery - }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - setGoogleDriveFiles(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) { - setGoogleDriveFiles([]); - } - } - }) - .catch(err => { - console.error('Error loading files:', err); - if (!pageToken) { - setGoogleDriveFiles([]); - } - }) - .finally(() => { - setIsLoadingFiles(false); - }); - }, - [token] - ); - - const debouncedSearch = useCallback((query: string) => { - if (searchTimeoutRef.current) { - clearTimeout(searchTimeoutRef.current); - } - - searchTimeoutRef.current = window.setTimeout(() => { - const sessionToken = getSessionToken(ingestor.type); - if (sessionToken) { - loadGoogleDriveFiles(sessionToken, currentFolderId, undefined, query); - } - }, 300); - }, [ingestor.type, currentFolderId, loadGoogleDriveFiles]); - - // Handle file selection - const handleFileSelect = (fileId: string) => { - setSelectedFiles(prev => { - if (prev.includes(fileId)) { - return prev.filter(id => id !== fileId); - } else { - return [...prev, fileId]; - } - }); - }; - - const handleFolderClick = (folderId: string, folderName: string) => { - if (folderId === currentFolderId) { - return; - } - - setIsLoadingFiles(true); - - setCurrentFolderId(folderId); - setFolderPath(prev => [...prev, { id: folderId, name: folderName }]); - - setSearchQuery(''); - - const sessionToken = getSessionToken(ingestor.type); - if (sessionToken) { - loadGoogleDriveFiles(sessionToken, folderId, undefined, ''); - } - }; - - const navigateBack = (index: number) => { - if (index < folderPath.length - 1) { - const newPath = folderPath.slice(0, index + 1); - const targetFolderId = newPath[newPath.length - 1].id; - setIsLoadingFiles(true); - setFolderPath(newPath); - setCurrentFolderId(targetFolderId); - - setSearchQuery(''); - const sessionToken = getSessionToken(ingestor.type); - if (sessionToken) { - loadGoogleDriveFiles(sessionToken, targetFolderId, undefined, ''); - } - } - }; + - const { getRootProps, getInputProps, isDragActive } = useDropzone({ + + + + + + + + + const { getRootProps, getInputProps } = useDropzone({ onDrop, multiple: true, onDragEnter: doNothing, @@ -723,8 +542,8 @@ function Upload({ if (!remoteName?.trim()) { return true; } - if (ingestor.type === 'google_drive') { - return !isGoogleDriveConnected || selectedFiles.length === 0; + if (isConnectorType(ingestor.type)) { + return selectedFiles.length === 0; } const formFields: FormField[] = IngestorFormSchemas[ingestor.type]; @@ -891,218 +710,17 @@ function Upload({ required={true} labelBgClassName="bg-white dark:bg-charleston-green-2" /> - {ingestor.type === 'google_drive' && ( -
- {authError && ( -
-

- ⚠️ {authError} -

-
- )} - - {!isGoogleDriveConnected ? ( - { - setUserEmail(data.user_email); - setIsGoogleDriveConnected(true); - setIsAuthenticating(false); - setAuthError(''); - - if (data.session_token) { - setSessionToken(ingestor.type, data.session_token); - loadGoogleDriveFiles(data.session_token, null); - } - }} - onError={(error) => { - setAuthError(error); - setIsAuthenticating(false); - setIsGoogleDriveConnected(false); - }} - /> - ) : ( -
- {/* Connection Status */} -
-
- - - - Connected as {userEmail} -
- -
- - {/* File Browser */} -
-
- {/* Breadcrumb navigation */} -
- {folderPath.map((path, index) => ( -
- {index > 0 && /} - -
- ))} -
- - {/* Search input */} -
-
- { - const newQuery = e.target.value; - setSearchQuery(newQuery); - debouncedSearch(newQuery); - }} - className="w-full px-3 py-2 pr-10 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" - /> - -
-
- -
-

- Select Files from Google Drive -

- - {selectedFiles.length > 0 - ? `${selectedFiles.length} file${selectedFiles.length !== 1 ? 's' : ''} selected` - : '' - } - -
-
- -
- {isLoadingFiles && googleDriveFiles.length === 0 ? ( -
-
-
- Loading files... -
-
- ) : googleDriveFiles.length === 0 ? ( -
- No files found in your Google Drive -
- ) : ( - <> -
- {googleDriveFiles.map((file) => ( -
-
-
{ - e.stopPropagation(); - handleFileSelect(file.id); - }} - > -
- {selectedFiles.includes(file.id) && ( - Selected - )} -
-
-
{ - if (file.type === 'application/vnd.google-apps.folder' || file.isFolder) { - handleFolderClick(file.id, file.name); - } else { - handleFileSelect(file.id); - } - }} - > -
- {file.type -
-
-

- {file.name} -

-

- {file.size && `${formatBytes(file.size)} • `}Modified {formatDate(file.modifiedTime)} -

-
-
-
-
- ))} -
- - {isLoadingFiles && ( -
-
-
- Loading more files... -
-
- )} - - )} -
- - - -
-
- )} -
+ {isConnectorType(ingestor.type) && ( + { + setSelectedFiles(selectedFileIds); + setSelectedFolders(selectedFolderIds); + }} + provider={ingestor.type} + token={token} + initialSelectedFiles={selectedFiles} + initialSelectedFolders={selectedFolders} + /> )} {renderFormFields()} @@ -1153,37 +771,9 @@ function Upload({ ); } - useEffect(() => { - const scrollContainer = scrollContainerRef.current; - - const handleScroll = () => { - if (!scrollContainer) return; - - const { scrollTop, scrollHeight, clientHeight } = scrollContainer; - const isNearBottom = scrollHeight - scrollTop - clientHeight < 50; - - if (isNearBottom && hasMoreFiles && !isLoadingFiles && nextPageToken) { - const sessionToken = getSessionToken(ingestor.type); - if (sessionToken) { - loadGoogleDriveFiles(sessionToken, currentFolderId, nextPageToken); - } - } - }; - - scrollContainer?.addEventListener('scroll', handleScroll); - - return () => { - scrollContainer?.removeEventListener('scroll', handleScroll); - }; - }, [hasMoreFiles, isLoadingFiles, nextPageToken, currentFolderId, ingestor.type]); + - useEffect(() => { - return () => { - if (searchTimeoutRef.current) { - clearTimeout(searchTimeoutRef.current); - } - }; - }, []); + return (