import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Tree } from 'react-arborist'; import { FiFile, FiFolder, FiFolderPlus, FiUploadCloud, FiDownloadCloud, FiTrash2, FiLoader, FiAlertTriangle, FiCheckCircle, FiChevronDown, FiChevronRight } from 'react-icons/fi'; import { useDropzone } from 'react-dropzone'; import { uploadInputFiles, downloadInputItem, deleteInputItem, createInputDirectory, moveInputItem } from '../api'; import { useFileManager } from '../context/FileManagerContext'; import Modal from '../components/Modal'; import JSZip from 'jszip'; import PourOverBackground from '../components/PourOverBackground'; import EnhancedButton from '../components/EnhancedButton'; /** * @typedef {object} InputNodeData * @property {string} id - Unique ID (can be the relative path) * @property {string} name - Display name (basename of the path) * @property {boolean} [isInternal] - Flag for placeholder nodes * @property {string} relativePath - Store the full relative path * @property {boolean} is_dir - Keep the directory flag * @property {InputNodeData[] | null} [children] - Children array or null if leaf */ /** @param {import("react-arborist").NodeRendererProps & { onNodeClick: Function, lastClick: { id: string | null, time: number }, newlyCreatedItemId: string | null }} props */ function Node({ node, style, dragHandle, onNodeClick, lastClick, newlyCreatedItemId }) { const Icon = node.isLeaf ? FiFile : FiFolder; const iconColor = node.isLeaf ? 'text-blue-400' : 'text-yellow-500'; const isNewlyCreated = node.id === newlyCreatedItemId; // Optional: add row‑click to select *and* open/close const handleRowClick = (e) => { const now = Date.now(); const DOUBLE_CLICK_THRESHOLD = 300; // ms if (lastClick.id === node.id && (now - lastClick.time) < DOUBLE_CLICK_THRESHOLD) { // Double click detected if (node.isInternal) { node.toggle(); // Toggle folder open/closed state } // Reset last click to prevent triple click issues onNodeClick(null, 0); } else { // Single click node.handleClick(e); // Let react-arborist handle selection onNodeClick(node.id, now); // Update last click state } }; // Optional: only the caret toggles const handleCaretClick = (e) => { e.stopPropagation(); // keep the row‑click logic independent node.toggle(); }; return (
{/* caret */} {node.isInternal && (node.isOpen ? : )} {/* icon + name */} {node.data.name}
); } function InputManager() { const { inputTreeData: treeData, isInputLoading: isLoading, inputError: error, fetchInputData, refreshInputData } = useFileManager(); /** @type {[InputNodeData[], Function]} */ const [selectedNodes, setSelectedNodes] = useState([]); const treeRef = useRef(null); console.log("InputManager RENDERED"); // Log component render // State for manual double-click tracking const [lastClick, setLastClick] = useState({ id: null, time: 0 }); // State for newly created item highlight const [newlyCreatedItemId, setNewlyCreatedItemId] = useState(null); // State to store names of items from the last upload for highlighting const [lastUploadedNames, setLastUploadedNames] = useState([]); // State to trigger the search effect after an upload cycle completes const [uploadCycle, setUploadCycle] = useState(0); // Modals State const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isCreateFolderModalOpen, setIsCreateFolderModalOpen] = useState(false); const [newFolderName, setNewFolderName] = useState(''); const [modalError, setModalError] = useState(null); const [isSubmittingModal, setIsSubmittingModal] = useState(false); // Upload Feedback State const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadError, setUploadError] = useState(null); const [lastUploadResult, setLastUploadResult] = useState(null); // Download state const [isDownloading, setIsDownloading] = useState(false); const [downloadStatus, setDownloadStatus] = useState({ message: '', error: false }); // Move/Drag State const [isMoving, setIsMoving] = useState(false); const [moveError, setMoveError] = useState(null); // State for shake animation trigger const [isShaking, setIsShaking] = useState(false); // --- State for PourOverBackground --- const [bgIsActive, setBgIsActive] = useState(false); const [triggerBgDownload, setTriggerBgDownload] = useState(false); const [triggerBgDelete, setTriggerBgDelete] = useState(false); // Refs for modal inputs const createFolderInputRef = useRef(null); useEffect(() => { fetchInputData(); // Fetch data on mount (uses cache if available) }, [fetchInputData]); // --- Handlers for PourOverBackground Callbacks --- const handleDownloadAnimComplete = useCallback(() => { setTriggerBgDownload(false); console.log("LOG: handleDownloadAnimComplete called"); }, []); const handleDeleteAnimComplete = useCallback(() => { setTriggerBgDelete(false); console.log("LOG: handleDeleteAnimComplete called"); }, []); // Effect to trigger shake animation on selection change useEffect(() => { if (selectedNodes.length > 0) { setIsShaking(true); const timer = setTimeout(() => { setIsShaking(false); }, 400); // Duration of the shake animation return () => clearTimeout(timer); // Cleanup timer on unmount or if selection changes again quickly } else { setIsShaking(false); // Ensure shaking stops if selection is cleared } }, [selectedNodes.length]); // Depend only on the length changing // Effect to handle newly created items: expand parents and scroll into view useEffect(() => { console.log(`LOG: Highlight EFFECT triggered. newlyCreatedItemId: ${newlyCreatedItemId}, treeData available: ${!!treeData}`); if (newlyCreatedItemId && treeRef.current && treeData) { console.log(`LOG: Highlight EFFECT processing ID: ${newlyCreatedItemId}`); const node = treeRef.current.get(newlyCreatedItemId); console.log(`LOG: Highlight EFFECT node found: ${!!node}`); if (node) { // Expand all parents let parent = node.parent; console.log("LOG: Highlight EFFECT expanding parents..."); while (parent && !parent.isOpen) { console.log(`LOG: Highlight EFFECT opening parent: ${parent.id}`); if (parent.id) { treeRef.current.open(parent.id, { autoSelect: false }); } else { console.warn("Could not open parent, ID missing:", parent); break; } parent = parent.parent; // Move up the tree } // Scroll the new node into view const timerId = setTimeout(() => { treeRef.current?.scrollTo(newlyCreatedItemId, "center"); console.log(`LOG: Highlight EFFECT scrolled to ${newlyCreatedItemId}`); }, 100); return () => clearTimeout(timerId); } else { console.warn(`Node with ID ${newlyCreatedItemId} not found in tree after creation/upload.`); } } }, [newlyCreatedItemId, treeData]); // Rerun when ID changes or tree data updates // Effect to find and highlight the first newly uploaded item after data refresh useEffect(() => { console.log(`LOG: Uploaded item search EFFECT triggered. uploadCycle: ${uploadCycle}, lastUploadedNames: [${lastUploadedNames.join(', ')}], treeData available: ${!!treeData}`); // Search the raw treeData, not just visible nodes if (lastUploadedNames.length > 0 && Array.isArray(treeData)) { let foundNodeId = null; console.log("LOG: Uploaded item search EFFECT searching raw treeData..."); // Recursive function to search the raw data structure const findNodeInData = (nodes) => { if (!Array.isArray(nodes)) return null; for (const node of nodes) { // node here is the raw data object (e.g., { id: 'path', name: 'name', ...}) // console.log(`LOG: Searching... Checking node: ${node.name} (ID: ${node.id})`); if (node.isdir && node.children) { console.log(`LOG: Searching... Checking directory node: ${node.name} (ID: ${node.id})`); // only log dir checks because there are a LOT of files here } if (lastUploadedNames.includes(node.name)) { console.log(`LOG: Uploaded item search EFFECT found match: ${node.name} (ID: ${node.id})`); return node.id; // Return the ID (path) of the first match } // If it's a directory, recurse into its children if (node.is_dir && node.children) { const foundInChildren = findNodeInData(node.children); if (foundInChildren) { return foundInChildren; } } } return null; }; // Start search from the root of treeData foundNodeId = findNodeInData(treeData); if (foundNodeId) { console.log(`LOG: Uploaded item search EFFECT setting newlyCreatedItemId = ${foundNodeId}`); setNewlyCreatedItemId(foundNodeId); // Clear the names list ONLY after successfully finding and setting the highlight console.log("LOG: Uploaded item search EFFECT clearing lastUploadedNames after success."); setLastUploadedNames([]); } else { console.log("LOG: Uploaded item search EFFECT found no matching node in current treeData. Will retry if treeData updates."); // Optional: Could implement a deeper search if needed, but start simple } } }, [treeData, lastUploadedNames, uploadCycle]); // Depend on treeData, names, and the upload cycle trigger const handleUpload = useCallback(async (fileList, targetPath) => { const files = Array.from(fileList); console.log(`LOG: handleUpload START. Target: ${targetPath}, Files:`, files); // Check whether any item is a directory (webkitRelativePath !== '') const containsDir = files.some(f => f.webkitRelativePath); console.log(`LOG: handleUpload containsDir: ${containsDir}`); let payload = files; if (containsDir) { console.log("LOG: handleUpload zipping directories..."); // zip each top‑level folder const groups = {}; files.forEach(f => { const [root] = f.webkitRelativePath.split('/'); (groups[root] ||= []).push(f); }); const zips = await Promise.all( Object.entries(groups).map(async ([folder, fs]) => { const zip = new JSZip(); fs.forEach(f => zip.file(f.webkitRelativePath.replace(`${folder}/`, ''), f) ); const blob = await zip.generateAsync({ type: 'blob' }); return new File([blob], `${folder}.zip`, { type: 'application/zip' }); }) ); payload = zips; } // Trigger background activity briefly on upload? (Optional) setBgIsActive(true); setTimeout(() => setBgIsActive(false), 1000); // Example: active for 1 sec console.log("LOG: handleUpload BEFORE uploadInputFiles call"); await uploadInputFiles(targetPath, payload); console.log("LOG: handleUpload AFTER uploadInputFiles call"); // Highlight is now set in onDrop *after* this completes // if (target !== '.') { // setNewlyCreatedItemId(targetPath); // } console.log("LOG: handleUpload BEFORE refreshInputData call"); refreshInputData(); // Refresh data after upload console.log("LOG: handleUpload AFTER refreshInputData call (refresh initiated)"); }, [refreshInputData]); const handleSelect = (nodes) => { const selectedData = nodes.map(n => n.data); console.log("LOG: handleSelect called. Selected nodes count:", selectedData.length); setSelectedNodes(selectedData); // Set background to active if something is selected setBgIsActive(selectedData.length > 0); if (selectedData.length > 0) { console.log(`LOG: Clearing newlyCreatedItemId (${newlyCreatedItemId}) due to handleSelect.`); setNewlyCreatedItemId(null); // Clear highlight on selection console.log(`LOG: Clearing lastUploadedNames due to handleSelect.`); setLastUploadedNames([]); // Also clear search list } }; const handleToggle = (id) => { console.log('LOG: handleToggle called. Toggled ID:', id); console.log(`LOG: Clearing newlyCreatedItemId (${newlyCreatedItemId}) due to handleToggle.`); setNewlyCreatedItemId(null); // Clear highlight on toggle console.log(`LOG: Clearing lastUploadedNames due to handleToggle.`); setLastUploadedNames([]); // Also clear search list }; // --- Move Handler --- /** @param {import("react-arborist").MoveHandler} args */ const handleMove = async ({ dragIds, parentId, index }) => { setIsMoving(true); setMoveError(null); console.log("Move detected:", { dragIds, parentId, index }); const parentNode = parentId ? treeRef.current?.get(parentId) : null; const parentPath = parentNode ? parentNode.data.relativePath : '.'; // Root is '.' console.log("Parent Node:", parentNode); console.log("Parent Path:", parentPath); if (parentNode && !parentNode.data.is_dir) { setMoveError("Cannot move items into a file."); setIsMoving(false); return; } let successCount = 0; let firstError = null; const results = await Promise.allSettled( dragIds.map(id => { const node = treeRef.current?.get(id); if (!node) return Promise.reject(new Error(`Node with ID ${id} not found.`)); const sourcePath = node.data.relativePath; const itemName = node.data.name; // Construct destination path: parentPath/itemName // Ensure no leading/trailing slashes on parentPath (unless it's root '.') // and no leading slash on itemName const cleanParentPath = parentPath === '.' ? '' : parentPath.replace(/^\/+|\/+$/g, ''); const cleanItemName = itemName.replace(/^\/+/,''); const destinationPath = cleanParentPath ? `${cleanParentPath}/${cleanItemName}` : cleanItemName; console.log(`Attempting move: ${sourcePath} -> ${destinationPath}`); return moveInputItem(sourcePath, destinationPath); }) ); results.forEach((result, i) => { const nodeId = dragIds[i]; const node = treeRef.current?.get(nodeId); const itemName = node?.data?.name || `Item ${i+1}`; if (result.status === 'fulfilled') { console.log(`Successfully moved ${itemName}:`, result.value); successCount++; } else { console.error(`Failed to move ${itemName}:`, result.reason); if (!firstError) { firstError = result.reason?.message || `Failed to move ${itemName}.`; } } }); setIsMoving(false); if (firstError) { setMoveError(`Move Error: ${firstError}${successCount > 0 ? ` (${successCount} items moved successfully)` : ''}`); } else { console.log(`Successfully moved ${successCount} item(s).`); } if (successCount > 0 || firstError) { setNewlyCreatedItemId(null); // Clear highlight after move attempt console.log(`LOG: Clearing newlyCreatedItemId (${newlyCreatedItemId}) after successful/failed move attempt.`); console.log(`LOG: Clearing lastUploadedNames after successful/failed move attempt.`); setLastUploadedNames([]); // Also clear search list refreshInputData(); // Refresh data after move setSelectedNodes([]); treeRef.current?.deselectAll(); } else { console.log("No changes detected from move operation, not refreshing."); } }; // --- End Move Handler --- const handleCreateFolder = async () => { if (!newFolderName.trim() || isSubmittingModal) return; let parentPath = '.'; let parentNode = null; // Keep track of the parent node // Target folder creation based on selection const selectedDir = selectedNodes.find(n => n.is_dir); if (selectedNodes.length === 1 && selectedDir) { parentPath = selectedDir.relativePath; parentNode = treeRef.current?.get(selectedDir.id); // Get the node itself } else if (selectedNodes.length > 0) { // If multiple items or a file is selected, find the parent of the first selected item const firstNodeId = selectedNodes[0].id; const firstNode = treeRef.current?.get(firstNodeId); const parent = firstNode?.parent; if (parent) { parentPath = parent.data.relativePath; parentNode = parent; // Get the parent node } else { parentPath = '.'; // Default to root if no parent found parentNode = null; } } // else defaults to '.' (root) // --- START EDIT: Attempt to open the parent directory --- if (parentNode && parentNode.id && !parentNode.isOpen && treeRef.current) { try { console.log(`LOG: handleCreateFolder attempting to open parent node: ${parentNode.id}`); await treeRef.current.open(parentNode.id, { autoSelect: false }); console.log(`LOG: handleCreateFolder successfully opened parent node: ${parentNode.id}`); } catch (openError) { console.warn(`LOG: handleCreateFolder failed to open parent node ${parentNode.id}:`, openError); // Continue anyway, maybe it doesn't exist yet or another issue } } // --- END EDIT --- const cleanParentPath = parentPath === '.' ? '' : parentPath.replace(/^\/+|\/+$/g, ''); const cleanNewFolderName = newFolderName.trim().replace(/^\/+|\/+$/g, ''); const fullPath = cleanParentPath ? `${cleanParentPath}/${cleanNewFolderName}` : cleanNewFolderName; setIsSubmittingModal(true); setModalError(null); try { await createInputDirectory(fullPath); setNewFolderName(''); setIsCreateFolderModalOpen(false); // Set highlight target *before* refresh setNewlyCreatedItemId(fullPath); refreshInputData(); // Refresh data after creation // Trigger background activity briefly setBgIsActive(true); setTimeout(() => setBgIsActive(false), 1000); } catch (e) { console.error("Failed to create directory:", e); setModalError(e.message || 'Could not create folder.'); } finally { setIsSubmittingModal(false); } setLastUploadedNames([]); // Also clear search list }; const handleDownload = async () => { if (selectedNodes.length === 0 || isDownloading || triggerBgDownload) return; // Prevent overlap console.log(`LOG: Clearing newlyCreatedItemId (${newlyCreatedItemId}) due to handleDownload start.`); setNewlyCreatedItemId(null); // Clear highlight on download start setIsDownloading(true); setDownloadStatus({ message: `Starting download for ${selectedNodes.length} item(s)...`, error: false }); console.log(`LOG: Clearing lastUploadedNames due to handleDownload start.`); setLastUploadedNames([]); // Also clear search list setTriggerBgDownload(true); // <-- Trigger background download animation let successCount = 0; let firstError = null; for (const node of selectedNodes) { try { setDownloadStatus({ message: `Downloading ${node.name}...`, error: false }); const response = await downloadInputItem(node.relativePath); const blob = await response.blob(); // Extract filename from content-disposition or use node name let filename = node.name; const disposition = response.headers.get('content-disposition'); if (disposition && disposition.indexOf('attachment') !== -1) { const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; const matches = filenameRegex.exec(disposition); if (matches != null && matches[1]) { filename = matches[1].replace(/['"]/g, ''); } } // If it's a directory download, API likely sends a zip if (node.is_dir && !filename.toLowerCase().endsWith('.zip')) { filename += '.zip'; } // Create temporary link and click const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); a.remove(); successCount++; // Small delay between downloads to prevent browser blocking await new Promise(resolve => setTimeout(resolve, 200)); } catch (error) { console.error(`Failed to download ${node.relativePath}:`, error); if (!firstError) { firstError = error.message || `Failed to download ${node.name}.`; } // Optionally break the loop on first error // break; } } setIsDownloading(false); if (firstError) { setDownloadStatus({ message: `Download failed: ${firstError}`, error: true }); } else { setDownloadStatus({ message: `Successfully downloaded ${successCount} item(s).`, error: false }); } // Clear status message after a delay setTimeout(() => setDownloadStatus({ message: '', error: false }), 5000); }; const handleDelete = async () => { if (selectedNodes.length === 0 || isSubmittingModal || triggerBgDelete) return; // Prevent overlap const itemsToDelete = [...selectedNodes]; setIsSubmittingModal(true); setModalError(null); setTriggerBgDelete(true); // <-- Trigger background delete animation setNewlyCreatedItemId(null); // Clear highlight on delete start console.log(`LOG: Clearing newlyCreatedItemId (${newlyCreatedItemId}) due to handleDelete start.`); console.log(`LOG: Clearing lastUploadedNames due to handleDelete start.`); setLastUploadedNames([]); // Also clear search list let successCount = 0; let firstError = null; const results = await Promise.allSettled( itemsToDelete.map(node => deleteInputItem(node.relativePath)) ); results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; } else { console.error(`Failed to delete ${itemsToDelete[index].relativePath}:`, result.reason); if (!firstError) { firstError = result.reason?.message || `Failed to delete ${itemsToDelete[index].name}.`; } } }); setIsSubmittingModal(false); if (firstError) { setModalError(`Error: ${firstError}${successCount > 0 ? ` (${successCount} items deleted successfully)` : ''}`); } else { console.log(`Successfully deleted ${successCount} items.`); setIsDeleteModalOpen(false); } if (successCount > 0) { // await fetchInitialData(); refreshInputData(); // Refresh data after deletion setSelectedNodes([]); setBgIsActive(false); // Deactivate background animation after deletion } }; const openCreateFolderModal = () => { setNewFolderName(''); setModalError(null); console.log(`LOG: Clearing newlyCreatedItemId (${newlyCreatedItemId}) due to openCreateFolderModal.`); setIsCreateFolderModalOpen(true); setNewlyCreatedItemId(null); // Clear highlight when opening modal console.log(`LOG: Clearing lastUploadedNames due to openCreateFolderModal.`); setLastUploadedNames([]); // Also clear search list // Autofocus the input field setTimeout(() => createFolderInputRef.current?.focus(), 100); } const openDeleteModal = () => { setModalError(null); console.log(`LOG: Clearing newlyCreatedItemId (${newlyCreatedItemId}) due to openDeleteModal.`); setIsDeleteModalOpen(true); setNewlyCreatedItemId(null); // Clear highlight when opening modal console.log(`LOG: Clearing lastUploadedNames due to openDeleteModal.`); setLastUploadedNames([]); // Also clear search list } const onDrop = useCallback( async acceptedFiles => { console.log("LOG: onDrop START. Files dropped:", acceptedFiles.length); if (!acceptedFiles.length) return; // Clear previous highlight search list console.log("LOG: onDrop clearing lastUploadedNames at start.") setLastUploadedNames([]); // --- Record potential names for highlighting --- Start const namesToHighlight = acceptedFiles.map(file => { // If it's a directory upload (folder dragged in), webkitRelativePath exists if (file.webkitRelativePath) { // Extract the top-level folder name return file.webkitRelativePath.split('/')[0]; } else { // Regular file upload return file.name; } }); // Remove duplicates (important if a folder contains multiple files) const uniqueNames = [...new Set(namesToHighlight)]; console.log("LOG: onDrop names recorded for potential highlight:", uniqueNames); setLastUploadedNames(uniqueNames); // --- Record potential names for highlighting --- End // Determine upload target based on selection let target = '.'; // Default to root let targetNode = null; // Track target node const selectedDir = selectedNodes.find(n => n.is_dir); if (selectedNodes.length === 1 && selectedDir) { target = selectedDir.relativePath; targetNode = treeRef.current?.get(selectedDir.id); // Get selected dir node } else if (selectedNodes.length > 0) { // If multiple items or a file is selected, upload to parent of first selected const firstNodeId = selectedNodes[0].id; const firstNode = treeRef.current?.get(firstNodeId); const parent = firstNode?.parent; if (parent) { target = parent.data.relativePath; targetNode = parent; // Target is the parent node } else { target = '.'; // Default to root if no parent targetNode = null; } } // --- START EDIT: Attempt to open the target directory --- if (targetNode && targetNode.id && !targetNode.isOpen && treeRef.current) { try { console.log(`LOG: onDrop attempting to open target node: ${targetNode.id}`); await treeRef.current.open(targetNode.id, { autoSelect: false }); console.log(`LOG: onDrop successfully opened target node: ${targetNode.id}`); } catch (openError) { console.warn(`LOG: onDrop failed to open target node ${targetNode.id}:`, openError); // Continue upload anyway } } // --- END EDIT --- console.log(`LOG: onDrop determined targetPath: ${target}`); console.log(`Uploading to target path: ${target}`); console.log("LOG: onDrop setting isUploading = true"); setIsUploading(true); setUploadError(null); setLastUploadResult(null); // Clear previous success message console.log(`LOG: onDrop explicitly clearing newlyCreatedItemId (${newlyCreatedItemId}) at start of try block.`); setNewlyCreatedItemId(null); // Explicitly clear any previous highlight try { console.log(`LOG: onDrop BEFORE handleUpload call. Target: ${target}`); await handleUpload(acceptedFiles, target); console.log("LOG: onDrop AFTER handleUpload call completed."); // Highlight is now handled by the new useEffect hook based on lastUploadedNames console.log("LOG: onDrop setting lastUploadResult = true"); setLastUploadResult(true); // Indicate success // Set names *and* trigger the search effect AFTER upload/refresh initiated console.log("LOG: onDrop setting lastUploadedNames and incrementing uploadCycle"); setLastUploadedNames(uniqueNames); // Set names just before incrementing cycle setUploadCycle(c => c + 1); // Trigger the search effect // Clear success message after a delay setTimeout(() => { setLastUploadResult(null); // Maybe clear highlight here too, after a longer delay? // setNewlyCreatedItemId(null); }, 5000); } catch (e) { console.error('LOG: onDrop CAUGHT ERROR:', e); console.error('Upload failed:', e); setUploadError(e.message ?? 'An error occurred during upload.'); // Clear error message after a delay setTimeout(() => setUploadError(null), 8000); } finally { console.log("LOG: onDrop setting isUploading = false (in finally block)"); setIsUploading(false); } }, [selectedNodes, handleUpload, treeRef] // Removed refreshInputData dependency here as it's called within handleUpload ); const { getRootProps, getInputProps, isDragActive, open: openFileDialog, } = useDropzone({ onDrop, noClick: true, // We have dedicated upload buttons noKeyboard: true, // Allow dropping directories (though handled via zipping in handleUpload) // Note: This doesn't directly enable folder uploads in all browsers without user selecting folder manually // accept: { 'application/zip': ['.zip'] }, // Primarily suggest zipping, but handleUpload adapts }); const actionButtonClass = "flex items-center gap-2 px-3 py-1.5 rounded bg-gray-700 hover:bg-gray-600 text-gray-200 text-sm transition-colors duration-200 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed"; // Pass click tracking props to Node const handleNodeClick = (id, time) => { setLastClick({ id, time }); // Set background active if a node is clicked (even if it becomes deselected on double click) setBgIsActive(true); console.log(`LOG: Clearing newlyCreatedItemId (${newlyCreatedItemId}) due to handleNodeClick.`); setNewlyCreatedItemId(null); // Clear highlight on click console.log(`LOG: Clearing lastUploadedNames due to handleNodeClick.`); setLastUploadedNames([]); // Also clear search list // Maybe reset after a short delay if only single click? // setTimeout(() => setBgIsActive(selectedNodes.length > 0), 400); // Check selection after double-click threshold }; // Define the background click handler similar to ConfigSelector const handleBackgroundClick = useCallback((e) => { // Don't clear if a modal is open if (isDeleteModalOpen || isCreateFolderModalOpen) return; const clickedOnRow = e.target.closest('[role="treeitem"]'); const clickedOnScrollbar = e.target.closest('.simplebar-scrollbar'); // Adjust if needed const clickedOnActionButton = e.target.closest('[data-action-button="true"]'); const clickedOnModal = e.target.closest('[role="dialog"]'); // Check if click is inside any modal if (!clickedOnRow && !clickedOnScrollbar && !clickedOnActionButton && !clickedOnModal) { if (treeRef.current?.deselectAll) { treeRef.current.deselectAll(); } if (typeof setSelectedNodes === 'function') { setSelectedNodes([]); setBgIsActive(false); // <-- Deactivate background on deselect all setNewlyCreatedItemId(null); // Clear highlight on background click console.log(`LOG: Clearing newlyCreatedItemId (${newlyCreatedItemId}) due to handleBackgroundClick.`); console.log(`LOG: Clearing lastUploadedNames due to handleBackgroundClick.`); setLastUploadedNames([]); // Also clear search list } } }, [isDeleteModalOpen, isCreateFolderModalOpen]); return ( // Outer div for dropzone and background click handling, no padding here
{/* --- Add PourOverBackground --- */} {/* Main Content Area (Wrapped and Centered) */}
{/* Header - Apply background, blur, padding, border */}

Input Manager

0 ? 'opacity-75' : ''}`} title={selectedNodes.length === 1 && selectedNodes[0].is_dir ? `Create folder inside '${selectedNodes[0].name}'` : (selectedNodes.length > 0 ? `Create folder alongside selected item(s)` : 'Create folder in root')} data-action-button="true" particleColor="#22c55e" > Create Folder 0 ? 'opacity-75' : ''}`} title={selectedNodes.length === 1 && selectedNodes[0].is_dir ? `Upload to '${selectedNodes[0].name}' (Folders will be zipped)` : (selectedNodes.length > 0 ? `Upload alongside selected item(s) (Folders will be zipped)`: `Upload to root (Folders will be zipped)`)} data-action-button="true" particleColor="#6366f1" > Upload 0 && !isDownloading ? 'animate-pulse-green-download' : 'hover:bg-green-700'} ${isShaking ? 'animate-shake' : ''}`} title="Download Selected Item(s)" disabled={selectedNodes.length === 0 || isDownloading} data-action-button="true" particleColor="#34d399" > {isDownloading ? : } {isDownloading ? 'Downloading...' : 'Download'} 0 ? 'animate-pulse-red' : 'text-red-400 hover:bg-red-900 hover:text-red-300'} ${isShaking ? 'animate-shake' : ''}`} title="Delete Selected Item(s)" disabled={selectedNodes.length === 0} data-action-button="true" particleColor="#ef4444" > Delete
{/* Loading/Error States for Initial Load - Apply background */} {isLoading && !treeData && (
Loading input files...
)} {error && !treeData && (

Error loading input structure:

{error}

)} {/* Tree View Area - Apply background */} {treeData && (
{/* Dropzone overlay */} {isDragActive && (

Drop files/folders here to upload

)} {/* Loading overlay during refresh */} {isLoading && (
)} {/* Tree or Empty State */} {(treeData && treeData.length > 0) ? ( { // Prevent dropping *onto* a file return dropTarget && !dropTarget.data.is_dir; }} children={(props) => } > ) : (

Input directory is empty.

)}
)} {/* Status Bar Area - Apply background */}
{/* Upload Status */} {isUploading && (
Uploading...
)} {uploadError &&

Upload Error: {uploadError}

} {lastUploadResult &&

Upload successful!

} {/* Download Status */} {isDownloading && (
{downloadStatus.message}
)} {!isDownloading && downloadStatus.message && (

{downloadStatus.message}

)} {/* Move Status */} {isMoving && (
Moving items...
)} {moveError &&

Move Error: {moveError}

}
{ /* End Main Content Wrapper */ } {/* --- Modals --- */} {/* Modals remain outside the centered content */} !isSubmittingModal && setIsDeleteModalOpen(false)} title="Confirm Deletion" >

Are you sure you want to delete the following {selectedNodes.length} item(s)? This action cannot be undone.

    {selectedNodes.map(node =>
  • {node.relativePath}
  • )}
{/* Modal-specific error display */} {modalError && (

{modalError}

)}
0 ? 'animate-pulse-red' : ''}`} disabled={isSubmittingModal || selectedNodes.length === 0} particleColor="#ef4444" > {isSubmittingModal ? : } {isSubmittingModal ? 'Deleting...' : 'Delete'}
{/* Create Folder Modal */} !isSubmittingModal && setIsCreateFolderModalOpen(false)} title="Create New Folder" > setNewFolderName(e.target.value)} placeholder="e.g., my_data or path/to/my_data" className={`w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 mb-1 ${!newFolderName.trim() && isCreateFolderModalOpen ? 'animate-pulse-bg-subtle' : ''}`} disabled={isSubmittingModal} onKeyDown={(e) => e.key === 'Enter' && !isSubmittingModal && newFolderName.trim() && handleCreateFolder()} /> {/* Modal-specific error display */} {modalError &&

Error: {modalError}

}
{isSubmittingModal ? : } {isSubmittingModal ? 'Creating...' : 'Create'}
); } export default InputManager;