import React from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DirectoryTree } from './DirectoryTree'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useFileSystemAccess } from '@/hooks/useFileSystemAccess'; import { cn, formatPathForDisplay } from '@/lib/utils'; import { toast } from '@/components/ui'; import { RiCheckboxBlankLine, RiCheckboxLine, } from '@remixicon/react'; import { useDeviceInfo } from '@/lib/device'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { DirectoryAutocomplete, type DirectoryAutocompleteHandle } from './DirectoryAutocomplete'; import { setDirectoryShowHidden, useDirectoryShowHidden, } from '@/lib/directoryShowHidden'; interface DirectoryExplorerDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } export const DirectoryExplorerDialog: React.FC = ({ open, onOpenChange, }) => { const { currentDirectory, homeDirectory, isHomeReady } = useDirectoryStore(); const { addProject, getActiveProject } = useProjectsStore(); const [pendingPath, setPendingPath] = React.useState(null); const [pathInputValue, setPathInputValue] = React.useState(''); const [hasUserSelection, setHasUserSelection] = React.useState(false); const [isConfirming, setIsConfirming] = React.useState(false); const showHidden = useDirectoryShowHidden(); const { isDesktop, requestAccess, startAccessing } = useFileSystemAccess(); const { isMobile } = useDeviceInfo(); const [autocompleteVisible, setAutocompleteVisible] = React.useState(false); const autocompleteRef = React.useRef(null); // Helper to format path for display const formatPath = React.useCallback((path: string | null) => { if (!path) return ''; return formatPathForDisplay(path, homeDirectory); }, [homeDirectory]); // Reset state when dialog opens React.useEffect(() => { if (open) { setHasUserSelection(false); setIsConfirming(false); setAutocompleteVisible(false); // Initialize with active project or current directory const activeProject = getActiveProject(); const initialPath = activeProject?.path || currentDirectory || homeDirectory || ''; setPendingPath(initialPath); setPathInputValue(formatPath(initialPath)); } }, [open, currentDirectory, homeDirectory, formatPath, getActiveProject]); // Set initial pending path to home when ready (only if not yet selected) React.useEffect(() => { if (!open || hasUserSelection || pendingPath) { return; } if (homeDirectory && isHomeReady) { setPendingPath(homeDirectory); setHasUserSelection(true); setPathInputValue('~'); } }, [open, hasUserSelection, pendingPath, homeDirectory, isHomeReady]); const handleClose = React.useCallback(() => { onOpenChange(false); }, [onOpenChange]); const finalizeSelection = React.useCallback(async (targetPath: string) => { if (!targetPath || isConfirming) { return; } setIsConfirming(true); try { let resolvedPath = targetPath; let projectId: string | undefined; if (isDesktop) { const accessResult = await requestAccess(targetPath); if (!accessResult.success) { toast.error('Unable to access directory', { description: accessResult.error || 'Desktop denied directory access.', }); return; } resolvedPath = accessResult.path ?? targetPath; projectId = accessResult.projectId; const startResult = await startAccessing(resolvedPath); if (!startResult.success) { toast.error('Failed to open directory', { description: startResult.error || 'Desktop could not grant file access.', }); return; } } const added = addProject(resolvedPath, { id: projectId }); if (!added) { toast.error('Failed to add project', { description: 'Please select a valid directory path.', }); return; } handleClose(); } catch (error) { toast.error('Failed to select directory', { description: error instanceof Error ? error.message : 'Unknown error occurred.', }); } finally { setIsConfirming(false); } }, [ addProject, handleClose, isDesktop, requestAccess, startAccessing, isConfirming, ]); const handleConfirm = React.useCallback(async () => { const pathToUse = pathInputValue.trim() || pendingPath; if (!pathToUse) { return; } await finalizeSelection(pathToUse); }, [finalizeSelection, pathInputValue, pendingPath]); const handleSelectPath = React.useCallback((path: string) => { setPendingPath(path); setHasUserSelection(true); setPathInputValue(formatPath(path)); }, [formatPath]); const handleDoubleClickPath = React.useCallback(async (path: string) => { setPendingPath(path); setHasUserSelection(true); setPathInputValue(formatPath(path)); await finalizeSelection(path); }, [finalizeSelection, formatPath]); const handlePathInputChange = React.useCallback((e: React.ChangeEvent) => { const value = e.target.value; setPathInputValue(value); setHasUserSelection(true); // Show autocomplete when typing a path setAutocompleteVisible(value.startsWith('/') || value.startsWith('~')); // Update pending path if it looks like a valid path if (value.startsWith('/') || value.startsWith('~')) { // Expand ~ to home directory const expandedPath = value.startsWith('~') && homeDirectory ? value.replace(/^~/, homeDirectory) : value; setPendingPath(expandedPath); } }, [homeDirectory]); const handlePathInputKeyDown = React.useCallback((e: React.KeyboardEvent) => { // Let autocomplete handle the key first if visible if (autocompleteRef.current?.handleKeyDown(e)) { return; } if (e.key === 'Enter') { e.preventDefault(); handleConfirm(); } }, [handleConfirm]); const handleAutocompleteSuggestion = React.useCallback((path: string) => { setPendingPath(path); setHasUserSelection(true); setPathInputValue(formatPath(path)); // Keep autocomplete open to allow further drilling down }, [formatPath]); const handleAutocompleteClose = React.useCallback(() => { setAutocompleteVisible(false); }, []); const toggleShowHidden = React.useCallback(() => { setDirectoryShowHidden(!showHidden); }, [showHidden]); const showHiddenToggle = ( ); const dialogHeader = ( Add project directory
Choose a folder to add as a project. {showHiddenToggle}
); const pathInputSection = (
); const treeSection = (
); // Mobile: use flex layout where tree takes remaining space const mobileContent = (
{pathInputSection}
{showHiddenToggle}
); const desktopContent = (
{pathInputSection} {treeSection}
); const renderActionButtons = () => ( <> ); if (isMobile) { return ( onOpenChange(false)} title="Add project directory" className="max-w-full" contentMaxHeightClassName="max-h-[min(70vh,520px)] h-[min(70vh,520px)]" footer={
{renderActionButtons()}
} > {mobileContent}
); } return ( { // Prevent auto-focus on input to avoid text selection e.preventDefault(); }} > {dialogHeader} {desktopContent} {renderActionButtons()} ); };