FIX: Comprehensive file manager UI/UX improvements and bug fixes
- Fix missing i18n for terminal.terminalWithPath translation key - Update keyboard shortcuts: remove Ctrl+T conflicts, change refresh to Ctrl+Y, rename shortcut to F6 - Remove click-to-rename functionality to prevent accidental renaming - Fix drag preview z-index and positioning issues during file operations - Remove false download trigger when dragging files to original position - Fix 'Must be handling a user gesture' error in drag-to-desktop functionality - Remove useless minimize button from file editor and diff viewer windows - Improve context menu z-index hierarchy for better layering - Add comprehensive drag state management and visual feedback 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -225,7 +225,7 @@ export function FileManagerContextMenu({
|
||||
? t("fileManager.openTerminalInFolder")
|
||||
: t("fileManager.openTerminalInFileLocation"),
|
||||
action: () => onOpenTerminal(targetPath),
|
||||
shortcut: "Ctrl+T",
|
||||
shortcut: "Ctrl+Shift+T",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -257,30 +257,15 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Download function
|
||||
if (hasFiles && onDownload) {
|
||||
// Download function - unified download that uses best available method
|
||||
if (hasFiles && onDragToDesktop) {
|
||||
menuItems.push({
|
||||
icon: <Download className="w-4 h-4" />,
|
||||
label: isMultipleFiles
|
||||
? t("fileManager.downloadFiles", { count: files.length })
|
||||
: t("fileManager.downloadFile"),
|
||||
action: () => onDownload(files),
|
||||
shortcut: "Ctrl+D",
|
||||
});
|
||||
}
|
||||
|
||||
// Drag to desktop menu item (supports browser and desktop apps)
|
||||
if (hasFiles && onDragToDesktop) {
|
||||
const isModernBrowser = "showSaveFilePicker" in window;
|
||||
menuItems.push({
|
||||
icon: <ExternalLink className="w-4 h-4" />,
|
||||
label: isMultipleFiles
|
||||
? t("fileManager.saveFilesToSystem", { count: files.length })
|
||||
: t("fileManager.saveToSystem"),
|
||||
action: () => onDragToDesktop(),
|
||||
shortcut: isModernBrowser
|
||||
? t("fileManager.selectLocationToSave")
|
||||
: t("fileManager.downloadToDefaultLocation"),
|
||||
shortcut: "Ctrl+D",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -314,7 +299,7 @@ export function FileManagerContextMenu({
|
||||
|
||||
// Add separator (if above functions exist)
|
||||
if (
|
||||
(hasFiles && (onPreview || onDownload || onDragToDesktop)) ||
|
||||
(hasFiles && (onPreview || onDragToDesktop)) ||
|
||||
(isSingleFile &&
|
||||
files[0].type === "file" &&
|
||||
(onPinFile || onUnpinFile)) ||
|
||||
@@ -329,7 +314,7 @@ export function FileManagerContextMenu({
|
||||
icon: <Edit3 className="w-4 h-4" />,
|
||||
label: t("fileManager.rename"),
|
||||
action: () => onRename(files[0]),
|
||||
shortcut: "F2",
|
||||
shortcut: "F6",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -397,7 +382,7 @@ export function FileManagerContextMenu({
|
||||
icon: <Terminal className="w-4 h-4" />,
|
||||
label: t("fileManager.openTerminalHere"),
|
||||
action: () => onOpenTerminal(currentPath),
|
||||
shortcut: "Ctrl+T",
|
||||
shortcut: "Ctrl+Shift+T",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -447,7 +432,7 @@ export function FileManagerContextMenu({
|
||||
icon: <RefreshCw className="w-4 h-4" />,
|
||||
label: t("fileManager.refresh"),
|
||||
action: onRefresh,
|
||||
shortcut: "F5",
|
||||
shortcut: "Ctrl+Y",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -487,12 +472,12 @@ export function FileManagerContextMenu({
|
||||
return (
|
||||
<>
|
||||
{/* Transparent overlay to capture click events */}
|
||||
<div className="fixed inset-0 z-40" />
|
||||
<div className="fixed inset-0 z-[99990]" />
|
||||
|
||||
{/* Menu body */}
|
||||
<div
|
||||
data-context-menu
|
||||
className="fixed bg-dark-bg border border-dark-border rounded-lg shadow-xl min-w-[180px] max-w-[250px] z-50 overflow-hidden"
|
||||
className="fixed bg-dark-bg border border-dark-border rounded-lg shadow-xl min-w-[180px] max-w-[250px] z-[99995] overflow-hidden"
|
||||
style={{
|
||||
left: menuPosition.x,
|
||||
top: menuPosition.y,
|
||||
|
||||
@@ -744,13 +744,10 @@ export function FileManagerGrid({
|
||||
e.stopPropagation();
|
||||
|
||||
if (dragState.type === "internal") {
|
||||
// Internal drag to empty area: trigger download
|
||||
console.log(
|
||||
"Internal drag to empty area detected, triggering download",
|
||||
);
|
||||
if (onDownload && dragState.files.length > 0) {
|
||||
onDownload(dragState.files);
|
||||
}
|
||||
// Internal drag to empty area: just cancel the drag operation
|
||||
console.log("Internal drag to empty area - cancelling drag operation");
|
||||
// Do not trigger download here - system drag end will handle it if truly outside window
|
||||
setDragState({ type: "none", files: [], counter: 0 });
|
||||
} else if (dragState.type === "external") {
|
||||
// External drag: handle file upload
|
||||
if (onUpload && e.dataTransfer.files.length > 0) {
|
||||
@@ -914,15 +911,18 @@ export function FileManagerGrid({
|
||||
onDelete(selectedFiles);
|
||||
}
|
||||
break;
|
||||
case "F2":
|
||||
case "F6":
|
||||
if (selectedFiles.length === 1 && onStartEdit) {
|
||||
event.preventDefault();
|
||||
onStartEdit(selectedFiles[0]);
|
||||
}
|
||||
break;
|
||||
case "F5":
|
||||
event.preventDefault();
|
||||
onRefresh();
|
||||
case "y":
|
||||
case "Y":
|
||||
if ((event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
onRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -1148,7 +1148,7 @@ export function FileManagerGrid({
|
||||
"hover:bg-accent hover:text-accent-foreground border-2 border-transparent",
|
||||
isSelected && "bg-primary/20 border-primary",
|
||||
dragState.target?.path === file.path &&
|
||||
"bg-muted border-primary border-dashed",
|
||||
"bg-muted border-primary border-dashed relative z-10",
|
||||
dragState.files.some((f) => f.path === file.path) &&
|
||||
"opacity-50",
|
||||
)}
|
||||
@@ -1188,15 +1188,8 @@ export function FileManagerGrid({
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
className="text-xs text-foreground truncate cursor-pointer hover:bg-accent px-1 py-0.5 rounded transition-colors duration-150 w-fit max-w-full text-center"
|
||||
title={`${file.name} (click to rename)`}
|
||||
onClick={(e) => {
|
||||
// Prevent file selection event
|
||||
if (onStartEdit) {
|
||||
e.stopPropagation();
|
||||
onStartEdit(file);
|
||||
}
|
||||
}}
|
||||
className="text-xs text-foreground truncate px-1 py-0.5 rounded w-fit max-w-full text-center"
|
||||
title={file.name}
|
||||
>
|
||||
{file.name}
|
||||
</p>
|
||||
@@ -1248,7 +1241,7 @@ export function FileManagerGrid({
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
isSelected && "bg-primary/20",
|
||||
dragState.target?.path === file.path &&
|
||||
"bg-muted border-primary border-dashed",
|
||||
"bg-muted border-primary border-dashed relative z-10",
|
||||
dragState.files.some((f) => f.path === file.path) &&
|
||||
"opacity-50",
|
||||
)}
|
||||
@@ -1288,15 +1281,8 @@ export function FileManagerGrid({
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
className="text-sm text-foreground truncate cursor-pointer hover:bg-accent px-1 py-0.5 rounded transition-colors duration-150 w-fit max-w-full"
|
||||
title={`${file.name} (click to rename)`}
|
||||
onClick={(e) => {
|
||||
// Prevent file selection event
|
||||
if (onStartEdit) {
|
||||
e.stopPropagation();
|
||||
onStartEdit(file);
|
||||
}
|
||||
}}
|
||||
className="text-sm text-foreground truncate px-1 py-0.5 rounded w-fit max-w-full"
|
||||
title={file.name}
|
||||
>
|
||||
{file.name}
|
||||
</p>
|
||||
@@ -1373,10 +1359,10 @@ export function FileManagerGrid({
|
||||
dragState.files.length > 0 &&
|
||||
dragState.mousePosition && (
|
||||
<div
|
||||
className="fixed z-50 pointer-events-none"
|
||||
className="fixed z-[99999] pointer-events-none"
|
||||
style={{
|
||||
left: dragState.mousePosition.x + 16,
|
||||
top: dragState.mousePosition.y - 8,
|
||||
left: dragState.mousePosition.x + 24,
|
||||
top: dragState.mousePosition.y - 40,
|
||||
}}
|
||||
>
|
||||
<div className="bg-background border border-border rounded-md shadow-md px-3 py-2 flex items-center gap-2">
|
||||
|
||||
@@ -206,45 +206,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
// SSH keepalive timer
|
||||
const keepaliveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Debounced directory loading for path changes
|
||||
const debouncedLoadDirectory = useCallback((path: string) => {
|
||||
// Clear any existing timer
|
||||
if (pathChangeTimerRef.current) {
|
||||
clearTimeout(pathChangeTimerRef.current);
|
||||
}
|
||||
|
||||
// Set new timer for debounced loading
|
||||
pathChangeTimerRef.current = setTimeout(() => {
|
||||
if (path !== lastPathChangeRef.current && sshSessionId) {
|
||||
console.log("Loading directory after path change:", path);
|
||||
lastPathChangeRef.current = path;
|
||||
loadDirectory(path);
|
||||
}
|
||||
}, 150); // 150ms debounce for path changes
|
||||
}, [sshSessionId, loadDirectory]);
|
||||
|
||||
// File list update - only reload when path changes, not on initial connection
|
||||
useEffect(() => {
|
||||
if (sshSessionId && currentPath) {
|
||||
// Skip the first load since it's handled in initializeSSHConnection
|
||||
if (!initialLoadDoneRef.current) {
|
||||
initialLoadDoneRef.current = true;
|
||||
lastPathChangeRef.current = currentPath;
|
||||
return;
|
||||
}
|
||||
|
||||
// Use debounced loading for path changes to prevent rapid clicking issues
|
||||
debouncedLoadDirectory(currentPath);
|
||||
}
|
||||
|
||||
// Cleanup timer on unmount or dependency change
|
||||
return () => {
|
||||
if (pathChangeTimerRef.current) {
|
||||
clearTimeout(pathChangeTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [sshSessionId, currentPath, debouncedLoadDirectory]);
|
||||
|
||||
// Handle file drag to external
|
||||
const handleFileDragStart = useCallback(
|
||||
(files: FileItem[]) => {
|
||||
@@ -273,10 +234,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
e.clientY > window.innerHeight - margin;
|
||||
|
||||
if (isOutside) {
|
||||
// Delay execution to avoid conflicts with other events
|
||||
setTimeout(() => {
|
||||
systemDrag.handleDragEnd(e);
|
||||
}, 100);
|
||||
// Execute immediately to preserve user gesture context
|
||||
systemDrag.handleDragEnd(e);
|
||||
} else {
|
||||
// Cancel drag
|
||||
systemDrag.cancelDragToSystem();
|
||||
@@ -386,6 +345,45 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
}
|
||||
}, [sshSessionId, isLoading, clearSelection, t]);
|
||||
|
||||
// Debounced directory loading for path changes
|
||||
const debouncedLoadDirectory = useCallback((path: string) => {
|
||||
// Clear any existing timer
|
||||
if (pathChangeTimerRef.current) {
|
||||
clearTimeout(pathChangeTimerRef.current);
|
||||
}
|
||||
|
||||
// Set new timer for debounced loading
|
||||
pathChangeTimerRef.current = setTimeout(() => {
|
||||
if (path !== lastPathChangeRef.current && sshSessionId) {
|
||||
console.log("Loading directory after path change:", path);
|
||||
lastPathChangeRef.current = path;
|
||||
loadDirectory(path);
|
||||
}
|
||||
}, 150); // 150ms debounce for path changes
|
||||
}, [sshSessionId, loadDirectory]);
|
||||
|
||||
// File list update - only reload when path changes, not on initial connection
|
||||
useEffect(() => {
|
||||
if (sshSessionId && currentPath) {
|
||||
// Skip the first load since it's handled in initializeSSHConnection
|
||||
if (!initialLoadDoneRef.current) {
|
||||
initialLoadDoneRef.current = true;
|
||||
lastPathChangeRef.current = currentPath;
|
||||
return;
|
||||
}
|
||||
|
||||
// Use debounced loading for path changes to prevent rapid clicking issues
|
||||
debouncedLoadDirectory(currentPath);
|
||||
}
|
||||
|
||||
// Cleanup timer on unmount or dependency change
|
||||
return () => {
|
||||
if (pathChangeTimerRef.current) {
|
||||
clearTimeout(pathChangeTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [sshSessionId, currentPath, debouncedLoadDirectory]);
|
||||
|
||||
// Debounced refresh function - prevent excessive clicking
|
||||
const handleRefreshDirectory = useCallback(() => {
|
||||
const now = Date.now();
|
||||
@@ -400,6 +398,31 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
loadDirectory(currentPath);
|
||||
}, [currentPath, lastRefreshTime, loadDirectory]);
|
||||
|
||||
// Global keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Check if input box or editable element has focus, skip if so
|
||||
const activeElement = document.activeElement;
|
||||
if (
|
||||
activeElement &&
|
||||
(activeElement.tagName === "INPUT" ||
|
||||
activeElement.tagName === "TEXTAREA" ||
|
||||
activeElement.contentEditable === "true")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Ctrl+Shift+T for opening terminal
|
||||
if (event.key === "T" && event.ctrlKey && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleOpenTerminal(currentPath);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [currentPath]);
|
||||
|
||||
function handleFilesDropped(fileList: FileList) {
|
||||
if (!sshSessionId) {
|
||||
toast.error(t("fileManager.noSSHConnection"));
|
||||
@@ -1382,7 +1405,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
});
|
||||
|
||||
toast.success(
|
||||
t("fileManager.terminalWithPath", { host: currentHost.name, path }),
|
||||
t("terminal.terminalWithPath", { host: currentHost.name, path }),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export function DiffWindow({
|
||||
initialY = 100,
|
||||
}: DiffWindowProps) {
|
||||
const { t } = useTranslation();
|
||||
const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } =
|
||||
const { closeWindow, maximizeWindow, focusWindow, windows } =
|
||||
useWindowManager();
|
||||
|
||||
const currentWindow = windows.find((w) => w.id === windowId);
|
||||
@@ -35,10 +35,6 @@ export function DiffWindow({
|
||||
closeWindow(windowId);
|
||||
};
|
||||
|
||||
const handleMinimize = () => {
|
||||
minimizeWindow(windowId);
|
||||
};
|
||||
|
||||
const handleMaximize = () => {
|
||||
maximizeWindow(windowId);
|
||||
};
|
||||
@@ -61,7 +57,6 @@ export function DiffWindow({
|
||||
minWidth={800}
|
||||
minHeight={500}
|
||||
onClose={handleClose}
|
||||
onMinimize={handleMinimize}
|
||||
onMaximize={handleMaximize}
|
||||
onFocus={handleFocus}
|
||||
isMaximized={currentWindow.isMaximized}
|
||||
|
||||
@@ -56,7 +56,6 @@ export function FileWindow({
|
||||
}: FileWindowProps) {
|
||||
const {
|
||||
closeWindow,
|
||||
minimizeWindow,
|
||||
maximizeWindow,
|
||||
focusWindow,
|
||||
updateWindow,
|
||||
@@ -329,10 +328,6 @@ export function FileWindow({
|
||||
closeWindow(windowId);
|
||||
};
|
||||
|
||||
const handleMinimize = () => {
|
||||
minimizeWindow(windowId);
|
||||
};
|
||||
|
||||
const handleMaximize = () => {
|
||||
maximizeWindow(windowId);
|
||||
};
|
||||
@@ -355,7 +350,6 @@ export function FileWindow({
|
||||
minWidth={400}
|
||||
minHeight={300}
|
||||
onClose={handleClose}
|
||||
onMinimize={handleMinimize}
|
||||
onMaximize={handleMaximize}
|
||||
onFocus={handleFocus}
|
||||
isMaximized={currentWindow.isMaximized}
|
||||
|
||||
Reference in New Issue
Block a user