diff --git a/plugins/file-manager/src/frontend/FileManagerContextMenu.tsx b/plugins/file-manager/src/frontend/FileManagerContextMenu.tsx index f4301261a..e58f52dc3 100644 --- a/plugins/file-manager/src/frontend/FileManagerContextMenu.tsx +++ b/plugins/file-manager/src/frontend/FileManagerContextMenu.tsx @@ -378,7 +378,7 @@ export function FileManagerContextMenu({ icon: , label: t("fileManager.rename"), action: () => onRename(files[0]), - shortcut: "F6", + shortcut: "F2", }); } diff --git a/plugins/file-manager/src/frontend/FileManagerGrid.tsx b/plugins/file-manager/src/frontend/FileManagerGrid.tsx index 6d63f83d7..9e3ad5355 100644 --- a/plugins/file-manager/src/frontend/FileManagerGrid.tsx +++ b/plugins/file-manager/src/frontend/FileManagerGrid.tsx @@ -1032,6 +1032,7 @@ export function FileManagerGrid({ onDelete(selectedFiles); } break; + case "F2": case "F6": if (selectedFiles.length === 1 && onStartEdit) { event.preventDefault(); @@ -1418,6 +1419,7 @@ export function FileManagerGrid({ const isSelected = selectedFiles.some( (f) => f.path === file.path, ); + const isEditing = editingFile?.path === file.path; return (
{getFileIcon(file, viewMode, compact)}
- {editingFile?.path === file.path ? ( + {isEditing ? ( setEditingName(e.target.value)} onKeyDown={handleEditKeyDown} onBlur={handleEditConfirm} - className="flex-1 min-w-0 max-w-[200px] border border-accent-brand/60 bg-card px-2 py-1 text-xs rounded-none outline-none focus:ring-1 focus:ring-accent-brand/50 pointer-events-auto" + className={cn( + "flex-1 min-w-0 border border-accent-brand/60 bg-card px-2 py-0 rounded-none outline-none focus:ring-1 focus:ring-accent-brand/50 pointer-events-auto", + compact ? "h-5 text-[11px]" : "h-6 text-xs", + )} onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} /> @@ -1493,13 +1501,13 @@ export function FileManagerGrid({ )}
- {listColumns.isVisible("modified") && ( + {!isEditing && listColumns.isVisible("modified") && ( {file.modified || "—"} )} - {listColumns.isVisible("owner") && ( + {!isEditing && listColumns.isVisible("owner") && ( {file.owner ? `${file.owner}${file.group ? `:${file.group}` : ""}` @@ -1507,7 +1515,7 @@ export function FileManagerGrid({ )} - {listColumns.isVisible("size") && ( + {!isEditing && listColumns.isVisible("size") && ( {file.type === "file" && file.size !== undefined && @@ -1517,7 +1525,7 @@ export function FileManagerGrid({ )} - {listColumns.isVisible("permissions") && ( + {!isEditing && listColumns.isVisible("permissions") && ( {file.permissions || "—"} diff --git a/plugins/file-manager/tests/frontend/FileManagerGrid.test.tsx b/plugins/file-manager/tests/frontend/FileManagerGrid.test.tsx new file mode 100644 index 000000000..fafd1636b --- /dev/null +++ b/plugins/file-manager/tests/frontend/FileManagerGrid.test.tsx @@ -0,0 +1,91 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; +import { FileManagerGrid } from "../../src/frontend/FileManagerGrid"; +import type { FileItem } from "../../src/frontend/host-types"; + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), + initReactI18next: { type: "3rdParty", init: () => {} }, +})); + +vi.mock("@tanstack/react-virtual", () => ({ + useVirtualizer: ({ count }: { count: number }) => ({ + getTotalSize: () => count * 29, + getVirtualItems: () => + Array.from({ length: count }, (_, index) => ({ + index, + key: index, + start: index * 29, + size: 29, + })), + measureElement: () => {}, + measure: () => {}, + }), +})); + +const file: FileItem = { + name: "notes.txt", + path: "/notes.txt", + type: "file", + modified: "2026-09-01", + owner: "owner", + permissions: "-rw-r--r--", +}; +const props = { + files: [file], + selectedFiles: [file], + viewMode: "list" as const, + onFileOpen: vi.fn(), + onSelectionChange: vi.fn(), + onRefresh: vi.fn(), +}; + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe("remote file rename", () => { + it.each(["F2", "F6"])("starts rename with %s in the visible pane", (key) => { + vi.spyOn(HTMLElement.prototype, "offsetParent", "get").mockReturnValue( + document.body, + ); + const onStartEdit = vi.fn(); + render(); + fireEvent.keyDown(document, { key }); + expect(onStartEdit).toHaveBeenCalledExactlyOnceWith(file); + }); + + it("does not rename files in a hidden pane", () => { + vi.spyOn(HTMLElement.prototype, "offsetParent", "get").mockReturnValue( + null, + ); + const onStartEdit = vi.fn(); + render(); + fireEvent.keyDown(document, { key: "F2" }); + expect(onStartEdit).not.toHaveBeenCalled(); + }); + + it("hides metadata while editing and commits the new name", () => { + const onRename = vi.fn(); + const { rerender } = render(); + expect(screen.getByText(file.modified!)).toBeInTheDocument(); + rerender( + , + ); + expect(screen.queryByText(file.modified!)).not.toBeInTheDocument(); + expect(screen.queryByText(file.permissions!)).not.toBeInTheDocument(); + const input = screen.getByRole("textbox"); + fireEvent.change(input, { target: { value: "renamed.txt" } }); + fireEvent.keyDown(input, { key: "Enter" }); + expect(onRename).toHaveBeenCalledWith(file, "renamed.txt"); + rerender(); + expect(screen.getByText(file.modified!)).toBeInTheDocument(); + }); +});