Add password toggle, hover tooltips, and silent refresh for actions
All checks were successful
Automated Container Build / build-and-push (push) Successful in 37s

This commit is contained in:
Elijah 2026-05-23 09:11:37 -07:00
parent 0ca098e713
commit 807775a176
2 changed files with 29 additions and 73 deletions

View file

@ -297,17 +297,16 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
}
}
// Load files on path change
const loadFiles = useCallback(async (path?: string) => {
const loadFiles = useCallback(async (path?: string, silent: boolean = false) => {
const targetPath = path ?? currentPath;
setLoading(true);
if (!silent) setLoading(true);
try {
const data = await api.listDirectory(targetPath);
setFiles(data.files || []);
} catch {
setFiles([]);
if (!silent) setFiles([]);
} finally {
setLoading(false);
if (!silent) setLoading(false);
}
}, [currentPath]);
@ -332,10 +331,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
}
}, [showMoveModal, loadFolderTree]);
// Keyboard shortcuts
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
// Don't trigger shortcuts when typing in inputs
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
if (e.key === 'Backspace') {
@ -357,7 +354,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
return () => window.removeEventListener('keydown', handleKeyDown);
}, [files, selectedFiles]);
// Close context menu on click outside
useEffect(() => {
const handleClickOutside = () => {
setContextMenu(null);
@ -367,18 +363,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
return () => window.removeEventListener('click', handleClickOutside);
}, []);
async function loadPinned() {
setLoading(true);
try {
const data = await api.listPinned();
setFiles(data.items || []);
} catch {
setFiles([]);
} finally {
setLoading(false);
}
}
async function loadTrash() {
setLoading(true);
try {
@ -409,12 +393,10 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
}
}
// Effect to keep pinned list loaded for the sidebar
const [pinnedFiles, setPinnedFiles] = useState<FileItem[]>([]);
useEffect(() => {
if (pinnedExpanded) {
api.listPinned().then(data => {
// Sort by pinnedOrder
const items = data.items || [];
items.sort((a: FileItem, b: FileItem) => {
const idxA = pinnedOrder.indexOf(a.path);
@ -431,7 +413,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
async function handlePinnedDragEnd(sourcePath: string, destPath: string) {
const newOrder = [...pinnedOrder];
// Ensure all current pinned files are in order array
const currentPaths = pinnedFiles.map(f => f.path);
for (const p of currentPaths) {
if (!newOrder.includes(p)) newOrder.push(p);
@ -469,7 +450,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
function handleFileClick(file: FileItem, e: React.MouseEvent) {
if (renaming === file.path) return;
if (e.shiftKey) {
// Multi-select with shift
setSelectedFiles(prev => {
const next = new Set(prev);
if (next.has(file.path)) {
@ -480,7 +460,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
return next;
});
} else {
// Single click to open/navigate
if (file.is_dir) navigateToFolder(file);
else setPreviewFile(file);
}
@ -600,21 +579,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
async function handlePin(file: FileItem) {
await api.togglePin(file.path);
loadFiles();
if (pinnedExpanded) {
api.listPinned().then(data => {
const items = data.items || [];
items.sort((a: FileItem, b: FileItem) => {
const idxA = pinnedOrder.indexOf(a.path);
const idxB = pinnedOrder.indexOf(b.path);
if (idxA === -1 && idxB === -1) return a.name.localeCompare(b.name);
if (idxA === -1) return 1;
if (idxB === -1) return -1;
return idxA - idxB;
});
setPinnedFiles(items);
}).catch(console.error);
}
loadFiles(undefined, true);
}
async function handleRename(path: string, originalName: string, isDir: boolean) {
@ -633,7 +598,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
await api.rename(path, finalName);
setRenaming(null);
setNewName('');
loadFiles();
loadFiles(undefined, true);
}
async function handleCreateFolder() {
@ -642,7 +607,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
await api.createFolder(folderPath);
setShowNewFolder(false);
setNewFolderName('');
loadFiles();
loadFiles(undefined, true);
}
async function handleRestore(file: FileItem) {
@ -661,14 +626,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
await Promise.all(movingFiles.map(src => api.move(src, moveModalPath === '.' ? '.' : moveModalPath)));
setShowMoveModal(false);
setMovingFiles([]);
loadFiles();
loadFiles(undefined, true);
setSelectedFiles(new Set());
} catch (err) {
console.error(err);
}
}
// Drag and drop upload
function handleDragOver(e: React.DragEvent) {
if (draggedFile) return;
if (!e.dataTransfer.types.includes('Files')) return;
@ -711,7 +675,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
setDraggedFile(null);
try {
await api.move(sourcePath, file.path);
loadFiles();
loadFiles(undefined, true);
} catch (err) {
console.error(err);
}
@ -741,7 +705,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
if (draggedFile) return;
const droppedFiles = Array.from(e.dataTransfer.files);
await Promise.all(droppedFiles.map(file => uploadFile(file)));
loadFiles();
loadFiles(undefined, true);
}
async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
@ -749,10 +713,9 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
if (!uploadFiles) return;
await Promise.all(Array.from(uploadFiles).map(file => uploadFile(file)));
e.target.value = '';
loadFiles();
loadFiles(undefined, true);
}
// Breadcrumb path segments
const pathSegments = currentPath === '.' ? [] : currentPath.split('/');
function formatSize(bytes: number): string {
@ -851,7 +814,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
let displayFiles = searchResults !== null ? searchResults : files;
displayFiles = displayFiles.filter(f => f.name !== '.uploads').sort((a, b) => {
// Directories always first regardless of sort type
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
let cmp = 0;
@ -921,7 +883,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
return (
<div className="flex h-screen" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
{/* === SIDEBAR === */}
<aside
className="w-60 flex-shrink-0 flex flex-col border-r"
style={{
@ -929,7 +890,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
borderColor: 'var(--color-border-subtle)',
}}
>
{/* Logo */}
<div className="flex items-center gap-3 px-5 py-5">
<div
className="w-9 h-9 rounded-xl flex items-center justify-center"
@ -944,7 +904,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</span>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-2 space-y-1 overflow-y-auto">
{[
{ id: 'files' as SidebarSection, icon: Folder, label: 'All Files' },
@ -964,7 +923,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</button>
))}
{/* Pinned Collapsible Section */}
<div className="py-1">
<button
onClick={() => {
@ -1055,7 +1013,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
))}
</nav>
{/* Settings Footer */}
<div className="px-3 py-4 border-t" style={{ borderColor: 'var(--color-border-subtle)' }}>
<button
id="nav-settings"
@ -1072,14 +1029,12 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</div>
</aside>
{/* === MAIN CONTENT === */}
<main
className="flex-1 flex flex-col overflow-hidden"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* Top Bar */}
<header
className="flex items-center gap-4 px-6 py-3.5 border-b"
style={{
@ -1087,7 +1042,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
borderColor: 'var(--color-border-subtle)',
}}
>
{/* Breadcrumbs */}
<div className="flex items-center gap-1 flex-1 min-w-0">
<button
onClick={() => { navigateTo('.'); setActiveSection('files'); }}
@ -1112,7 +1066,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
))}
</div>
{/* Search Bar */}
{activeSection !== 'settings' && (
<div className="relative w-72">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
@ -1140,7 +1093,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</div>
)}
{/* Action Buttons */}
<div className="flex items-center gap-2">
{activeSection === 'trash' && (
<button
@ -1231,7 +1183,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</div>
</header>
{/* Dynamic Content Area */}
{activeSection === 'settings' ? (
<SettingsPage onLogout={onLogout} />
) : activeSection === 'storage' ? (
@ -1240,7 +1191,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<SharedFilesPage />
) : (
<div className="flex flex-1 overflow-hidden">
{/* File Area */}
<div
className="flex-1 overflow-y-auto p-6 relative"
onContextMenu={activeSection === 'files' ? handleBgContextMenu : undefined}
@ -1250,7 +1200,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
>
{/* Lasso selection box */}
{selectionBox && (
<div
className="fixed z-50 pointer-events-none"
@ -1265,7 +1214,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
/>
)}
{/* Drag and drop overlay */}
<AnimatePresence>
{dragOver && (
<motion.div
@ -1291,7 +1239,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
)}
</AnimatePresence>
{/* New Folder Input */}
<AnimatePresence>
{showNewFolder && (
<motion.div
@ -1346,7 +1293,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
)}
</div>
) : viewMode === 'grid' ? (
/* Grid View */
<div className="space-y-8">
<div>
{groupedFiles.map((group, groupIdx) => (
@ -1435,7 +1381,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</div>
</div>
) : (
/* List View */
<div className="space-y-8">
<div>
{groupedFiles.map((group, groupIdx) => (
@ -1518,7 +1463,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
)}
</div>
{/* Bulk Actions Bar */}
<AnimatePresence>
{selectedFiles.size > 0 && (
<motion.div
@ -1544,6 +1488,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<button
className="p-2 hover:bg-white/5 rounded-lg transition-colors flex items-center gap-2 text-sm"
style={{ color: 'var(--color-text-primary)' }}
title="Download Selected"
onClick={() => {
const form = document.createElement('form');
form.method = 'POST';
@ -1569,6 +1514,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<button
className="p-2 hover:bg-white/5 rounded-lg transition-colors flex items-center gap-2 text-sm"
style={{ color: 'var(--color-text-primary)' }}
title="Move Selected"
onClick={() => {
setMovingFiles(Array.from(selectedFiles));
setMoveModalPath('.');
@ -1579,13 +1525,14 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</button>
<button
className="p-2 hover:bg-red-500/10 rounded-lg transition-colors flex items-center gap-2 text-sm text-red-500"
title="Delete Selected"
onClick={async () => {
if (confirm(`Move ${selectedFiles.size} items to trash?`)) {
for (const path of Array.from(selectedFiles)) {
await api.deleteFile(path).catch(() => {});
}
setSelectedFiles(new Set());
loadFiles();
loadFiles(undefined, true);
}
}}
>
@ -1597,6 +1544,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
onClick={() => setSelectedFiles(new Set())}
className="p-2 hover:bg-white/5 rounded-lg transition-colors"
style={{ color: 'var(--color-text-tertiary)' }}
title="Clear Selection"
>
<X className="w-4 h-4" />
</button>
@ -1609,7 +1557,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
)}
</main>
{/* === CONTEXT MENU === */}
<AnimatePresence>
{contextMenu && (
<motion.div
@ -1692,8 +1639,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
label="Move to Trash"
danger
onClick={() => {
api.deleteFile(contextMenu.file.path).then(() => loadFiles());
setContextMenu(null);
api.deleteFile(contextMenu.file.path).then(() => loadFiles(undefined, true));
}}
/>
</>