From bb4ea76d309af7c89ce771e5ce05d1cca020e389 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 1 Sep 2025 12:04:58 +0530 Subject: [PATCH] (fix:connectorTree) path navigation fn --- ...torTree.tsx => ConnectorTreeComponent.tsx} | 677 ++++++++++-------- frontend/src/settings/Sources.tsx | 23 +- 2 files changed, 398 insertions(+), 302 deletions(-) rename frontend/src/components/{ConnectorTree.tsx => ConnectorTreeComponent.tsx} (65%) diff --git a/frontend/src/components/ConnectorTree.tsx b/frontend/src/components/ConnectorTreeComponent.tsx similarity index 65% rename from frontend/src/components/ConnectorTree.tsx rename to frontend/src/components/ConnectorTreeComponent.tsx index cee07aa4..a4258dfc 100644 --- a/frontend/src/components/ConnectorTree.tsx +++ b/frontend/src/components/ConnectorTreeComponent.tsx @@ -10,24 +10,21 @@ import FolderIcon from '../assets/folder.svg'; import ArrowLeft from '../assets/arrow-left.svg'; import ThreeDots from '../assets/three-dots.svg'; import EyeView from '../assets/eye-view.svg'; +import SearchIcon from '../assets/search.svg'; import { useOutsideAlerter } from '../hooks'; -interface ConnectorFileNode { - id: string; - name: string; - type: string; - size: string; - modifiedTime: string; +interface FileNode { + type?: string; token_count?: number; - mimeType?: string; - isFolder?: boolean; + size_bytes?: number; + [key: string]: any; } -interface ConnectorDirectoryStructure { - [key: string]: ConnectorFileNode; +interface DirectoryStructure { + [key: string]: FileNode; } -interface ConnectorTreeProps { +interface ConnectorTreeComponentProps { docId: string; sourceName: string; onBackToDocuments: () => void; @@ -37,21 +34,28 @@ interface SearchResult { name: string; path: string; isFile: boolean; - id: string; } -const ConnectorTree: React.FC = ({ +const ConnectorTreeComponent: React.FC = ({ docId, sourceName, onBackToDocuments, }) => { const { t } = useTranslation(); - const [directoryStructure, setDirectoryStructure] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [directoryStructure, setDirectoryStructure] = + useState(null); const [currentPath, setCurrentPath] = useState([]); const token = useSelector(selectToken); - const [selectedFile, setSelectedFile] = useState<{ id: string; name: string } | null>(null); const [activeMenuId, setActiveMenuId] = useState(null); - const menuRefs = useRef<{ [key: string]: React.RefObject }>({}); + const menuRefs = useRef<{ + [key: string]: React.RefObject; + }>({}); + const [selectedFile, setSelectedFile] = useState<{ + id: string; + name: string; + } | null>(null); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const searchDropdownRef = useRef(null); @@ -66,69 +70,369 @@ const ConnectorTree: React.FC = ({ false, ); - + const handleFileClick = (fileName: string) => { + const fullPath = [...currentPath, fileName].join('/'); + setSelectedFile({ + id: fullPath, + name: fileName, + }); + }; useEffect(() => { const fetchDirectoryStructure = async () => { try { + setLoading(true); const response = await userService.getDirectoryStructure(docId, token); const data = await response.json(); if (data && data.directory_structure) { - const structure: ConnectorDirectoryStructure = {}; - // Convert the directory structure to our format - Object.entries(data.directory_structure).forEach(([key, value]: [string, any]) => { - structure[key] = { - id: key, - name: key, - type: value.type || 'file', - size: value.size_bytes ? `${value.size_bytes} bytes` : '-', - modifiedTime: '-', - token_count: value.token_count, - isFolder: !value.type, - }; - }); - setDirectoryStructure(structure); - - // Update search results when directory structure changes - if (searchQuery && structure) { - setSearchResults(searchFiles(searchQuery, structure)); - } + setDirectoryStructure(data.directory_structure); } else { - // Handle invalid response format - console.log('Invalid response format'); + setError('Invalid response format'); } } catch (err) { - console.error('Failed to load directory structure', err); + setError('Failed to load directory structure'); + console.error(err); + } finally { + setLoading(false); } }; if (docId) { fetchDirectoryStructure(); } - }, [docId, token, searchQuery]); + }, [docId, token]); - const handleFileClick = (fileId: string, fileName: string) => { - setSelectedFile({ id: fileId, name: fileName }); - }; - - const navigateToDirectory = (_folderId: string, folderName: string) => { - setCurrentPath(prev => [...prev, folderName]); + const navigateToDirectory = (dirName: string) => { + setCurrentPath([...currentPath, dirName]); }; const navigateUp = () => { - if (currentPath.length > 0) { - setCurrentPath(prev => prev.slice(0, -1)); + setCurrentPath(currentPath.slice(0, -1)); + }; + + const getCurrentDirectory = (): DirectoryStructure => { + if (!directoryStructure) return {}; + + let current = directoryStructure; + for (const dir of currentPath) { + if (current[dir] && !current[dir].type) { + current = current[dir] as DirectoryStructure; + } else { + return {}; + } + } + return current; + }; + + const formatBytes = (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const getMenuRef = (id: string) => { + if (!menuRefs.current[id]) { + menuRefs.current[id] = React.createRef(); + } + return menuRefs.current[id]; + }; + + const handleMenuClick = ( + e: React.MouseEvent, + id: string, + ) => { + e.stopPropagation(); + setActiveMenuId(activeMenuId === id ? null : id); + }; + + const getActionOptions = ( + name: string, + isFile: boolean, + _itemId: string, + ): MenuOption[] => { + const options: MenuOption[] = []; + + options.push({ + icon: EyeView, + label: t('settings.sources.view'), + onClick: (event: React.SyntheticEvent) => { + event.stopPropagation(); + if (isFile) { + handleFileClick(name); + } else { + navigateToDirectory(name); + } + }, + iconWidth: 18, + iconHeight: 18, + variant: 'primary', + }); + + // No delete option for connector files + + return options; + }; + + const calculateDirectoryStats = ( + structure: DirectoryStructure, + ): { totalSize: number; totalTokens: number } => { + let totalSize = 0; + let totalTokens = 0; + + Object.entries(structure).forEach(([_, node]) => { + if (node.type) { + // It's a file + totalSize += node.size_bytes || 0; + totalTokens += node.token_count || 0; + } else { + // It's a directory, recurse + const stats = calculateDirectoryStats(node); + totalSize += stats.totalSize; + totalTokens += stats.totalTokens; + } + }); + + return { totalSize, totalTokens }; + }; + + const handleBackNavigation = () => { + if (selectedFile) { + setSelectedFile(null); + } else if (currentPath.length === 0) { + if (onBackToDocuments) { + onBackToDocuments(); + } + } else { + navigateUp(); } }; - const getCurrentDirectory = (): ConnectorDirectoryStructure => { - return directoryStructure || {}; + const renderPathNavigation = () => { + return ( +
+ {/* Left side with path navigation */} +
+ + +
+ + {sourceName} + + {currentPath.length > 0 && ( + <> + / + {currentPath.map((dir, index) => ( + + + {index < currentPath.length - 1 && ( + / + )} + + ))} + + )} +
+
+ +
+ {renderFileSearch()} +
+
+ ); + }; + + const renderFileTree = (directory: DirectoryStructure) => { + if (!directory) return []; + + // Create parent directory row + const parentRow = + currentPath.length > 0 + ? [ + + +
+ {t('settings.sources.parentFolderAlt')} + + .. + +
+ + + - + + + - + + + , + ] + : []; + + // Sort entries: directories first, then files, both alphabetically + const sortedEntries = Object.entries(directory).sort(([nameA, nodeA], [nameB, nodeB]) => { + const isFileA = !!nodeA.type; + const isFileB = !!nodeB.type; + + if (isFileA !== isFileB) { + return isFileA ? 1 : -1; // Directories first + } + + return nameA.localeCompare(nameB); // Alphabetical within each group + }); + + + // Process directories + const directoryRows = sortedEntries + .filter(([_, node]) => !node.type) + .map(([name, node]) => { + const itemId = `dir-${name}`; + const menuRef = getMenuRef(itemId); + + // Calculate directory stats + const dirStats = calculateDirectoryStats(node as DirectoryStructure); + + return ( + navigateToDirectory(name)} + > + +
+ {t('settings.sources.folderAlt')} + + {name} + +
+ + + {dirStats.totalTokens > 0 + ? dirStats.totalTokens.toLocaleString() + : '-'} + + + {dirStats.totalSize > 0 ? formatBytes(dirStats.totalSize) : '-'} + + +
+ + + setActiveMenuId(isOpen ? itemId : null) + } + options={getActionOptions(name, false, itemId)} + anchorRef={menuRef} + position="bottom-left" + offset={{ x: -4, y: 4 }} + /> +
+ + + ); + }); + + // Process files + const fileRows = sortedEntries + .filter(([_, node]) => !!node.type) + .map(([name, node]) => { + const itemId = `file-${name}`; + const menuRef = getMenuRef(itemId); + + return ( + handleFileClick(name)} + > + +
+ {t('settings.sources.fileAlt')} + + {name} + +
+ + + {node.token_count?.toLocaleString() || '-'} + + + {node.size_bytes ? formatBytes(node.size_bytes) : '-'} + + +
+ + + setActiveMenuId(isOpen ? itemId : null) + } + options={getActionOptions(name, true, itemId)} + anchorRef={menuRef} + position="bottom-left" + offset={{ x: -4, y: 4 }} + /> +
+ + + ); + }); + + return [...parentRow, ...directoryRows, ...fileRows]; }; const searchFiles = ( query: string, - structure: ConnectorDirectoryStructure, + structure: DirectoryStructure, currentPath: string[] = [], ): SearchResult[] => { let results: SearchResult[] = []; @@ -141,7 +445,6 @@ const ConnectorTree: React.FC = ({ name, path: fullPath, isFile: !!node.type, - id: node.id, }); } @@ -149,7 +452,7 @@ const ConnectorTree: React.FC = ({ // If it's a directory, search recursively results = [ ...results, - ...searchFiles(query, node as unknown as ConnectorDirectoryStructure, [ + ...searchFiles(query, node as DirectoryStructure, [ ...currentPath, name, ]), @@ -167,7 +470,7 @@ const ConnectorTree: React.FC = ({ setCurrentPath(pathParts); setSelectedFile({ - id: result.id, + id: result.path, name: fileName, }); } else { @@ -178,70 +481,6 @@ const ConnectorTree: React.FC = ({ setSearchResults([]); }; - const handleBackNavigation = () => { - if (selectedFile) { - setSelectedFile(null); - } else if (currentPath.length === 0) { - if (onBackToDocuments) { - onBackToDocuments(); - } - } else { - navigateUp(); - } - }; - - const getMenuRef = (itemId: string) => { - if (!menuRefs.current[itemId]) { - menuRefs.current[itemId] = React.createRef(); - } - return menuRefs.current[itemId]; - }; - - const handleMenuClick = (e: React.MouseEvent, itemId: string) => { - e.preventDefault(); - e.stopPropagation(); - - if (activeMenuId === itemId) { - setActiveMenuId(null); - return; - } - setActiveMenuId(itemId); - }; - - const getActionOptions = ( - name: string, - id: string, - isFile: boolean, - _itemId: string, - ): MenuOption[] => { - const options: MenuOption[] = []; - - options.push({ - icon: EyeView, - label: t('settings.sources.view'), - onClick: (event: React.SyntheticEvent) => { - event.stopPropagation(); - if (isFile) { - handleFileClick(id, name); - } else { - navigateToDirectory(id, name); - } - }, - iconWidth: 18, - iconHeight: 18, - variant: 'primary', - }); - - // Remove delete option for connector files since they're not on our servers - // Connector files will be managed through the main Google Drive integration - - return options; - }; - - - - const currentDirectory = getCurrentDirectory(); - const renderFileSearch = () => { return (
@@ -255,8 +494,8 @@ const ConnectorTree: React.FC = ({ } }} placeholder={t('settings.sources.searchFiles')} - className={`w-full h-[38px] border border-[#D1D9E0] px-4 py-2 dark:border-[#6A6A6A] - ${searchQuery ? 'rounded-t-[24px]' : 'rounded-[24px]'} + className={`w-full h-[38px] border border-[#D1D9E0] px-4 py-2 dark:border-[#6A6A6A] + ${searchQuery ? 'rounded-t-[24px]' : 'rounded-[24px]'} bg-transparent focus:outline-none dark:text-[#E0E0E0]`} /> @@ -300,177 +539,27 @@ const ConnectorTree: React.FC = ({ ); }; - const renderConnectorFileTree = (structure: ConnectorDirectoryStructure): React.ReactNode[] => { - const entries = Object.entries(structure); - const directories = entries.filter(([_, node]) => node.isFolder); - const files = entries.filter(([_, node]) => !node.isFolder); - - return [ - ...directories.map(([name, node]) => { - const itemId = `dir-${node.id}`; - const menuRef = getMenuRef(itemId); - - return ( - navigateToDirectory(node.id, name)} - > - -
- {t('settings.sources.folderAlt')} - - {name} - -
- - - - - - - {node.modifiedTime || '-'} - - -
- - - setActiveMenuId(isOpen ? itemId : null) - } - options={getActionOptions(name, node.id, false, itemId)} - anchorRef={menuRef} - position="bottom-left" - offset={{ x: -4, y: 4 }} - /> -
- - - ); - }), - ...files.map(([name, node]) => { - const itemId = `file-${node.id}`; - const menuRef = getMenuRef(itemId); - - return ( - handleFileClick(node.id, name)} - > - -
- {t('settings.sources.fileAlt')} - - {name} - -
- - - {node.token_count?.toLocaleString() || '-'} - - - {node.size || '-'} - - -
- - - setActiveMenuId(isOpen ? itemId : null) - } - options={getActionOptions(name, node.id, true, itemId)} - anchorRef={menuRef} - position="bottom-left" - offset={{ x: -4, y: 4 }} - /> -
- - - ); - }), - ]; + const handleFileSearch = (searchQuery: string) => { + if (directoryStructure) { + return searchFiles(searchQuery, directoryStructure); + } + return []; }; - const renderPathNavigation = () => { - return ( -
- {/* Left side with path navigation */} -
- + const handleFileSelect = (path: string) => { + const pathParts = path.split('/'); + const fileName = pathParts.pop() || ''; + setCurrentPath(pathParts); + setSelectedFile({ + id: path, + name: fileName, + }); + }; -
- - {sourceName} - - {currentPath.length > 0 && ( - <> - / - {currentPath.map((dir, index) => ( - - - {dir} - - {index < currentPath.length - 1 && ( - - / - - )} - - ))} - - )} - {selectedFile && ( - <> - / - - {selectedFile.name} - - - )} -
-
+ const currentDirectory = getCurrentDirectory(); - {/* Right side with search */} -
- {renderFileSearch()} -
-
- ); + const navigateToPath = (index: number) => { + setCurrentPath(currentPath.slice(0, index + 1)); }; return ( @@ -483,6 +572,8 @@ const ConnectorTree: React.FC = ({ documentName={sourceName} handleGoBack={() => setSelectedFile(null)} path={selectedFile.id} + onFileSearch={handleFileSearch} + onFileSelect={handleFileSelect} />
@@ -504,15 +595,11 @@ const ConnectorTree: React.FC = ({ {t('settings.sources.size')} - - - {t('settings.sources.actions')} - - + - - {renderConnectorFileTree(currentDirectory)} + + {renderFileTree(getCurrentDirectory())} @@ -523,4 +610,4 @@ const ConnectorTree: React.FC = ({ ); }; -export default ConnectorTree; +export default ConnectorTreeComponent; diff --git a/frontend/src/settings/Sources.tsx b/frontend/src/settings/Sources.tsx index e0473bb8..945bd16c 100644 --- a/frontend/src/settings/Sources.tsx +++ b/frontend/src/settings/Sources.tsx @@ -29,6 +29,7 @@ import { import Upload from '../upload/Upload'; import { formatDate } from '../utils/dateTimeUtils'; import FileTreeComponent from '../components/FileTreeComponent'; +import ConnectorTreeComponent from '../components/ConnectorTreeComponent'; import Chunks from '../components/Chunks'; const formatTokens = (tokens: number): string => { @@ -271,19 +272,27 @@ export default function Sources({ return documentToView ? (
- {documentToView.isNested ? ( - setDocumentToView(undefined)} /> ) : ( - setDocumentToView(undefined)} + setDocumentToView(undefined)} /> - )} + ) + ) : ( + setDocumentToView(undefined)} + /> + )}
) : (