Implement Windows-style inline file/folder renaming and creation
- Remove popup dialogs for rename and new file/folder operations - Add inline editing mode with input field replacing file name display - Support both grid and list view modes for inline editing - Key features: - Click file name to start editing - Enter to confirm, Escape to cancel - Auto-focus and select text when editing starts - Visual feedback with blue border on edit input - Cancel new items removes them from filesystem - New file/folder workflow: - Creates with default name immediately - Starts inline editing automatically - User can rename or cancel (which deletes the item) - Maintain full keyboard navigation and accessibility - Preserve all existing selection and context menu functionality
This commit is contained in:
@@ -43,6 +43,10 @@ interface FileManagerGridProps {
|
|||||||
onUpload?: (files: FileList) => void;
|
onUpload?: (files: FileList) => void;
|
||||||
onContextMenu?: (event: React.MouseEvent, file?: FileItem) => void;
|
onContextMenu?: (event: React.MouseEvent, file?: FileItem) => void;
|
||||||
viewMode?: 'grid' | 'list';
|
viewMode?: 'grid' | 'list';
|
||||||
|
onRename?: (file: FileItem, newName: string) => void;
|
||||||
|
editingFile?: FileItem | null;
|
||||||
|
onStartEdit?: (file: FileItem) => void;
|
||||||
|
onCancelEdit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFileIcon = (fileName: string, isDirectory: boolean, viewMode: 'grid' | 'list' = 'grid') => {
|
const getFileIcon = (fileName: string, isDirectory: boolean, viewMode: 'grid' | 'list' = 'grid') => {
|
||||||
@@ -129,12 +133,55 @@ export function FileManagerGrid({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
onUpload,
|
onUpload,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
viewMode = 'grid'
|
viewMode = 'grid',
|
||||||
|
onRename,
|
||||||
|
editingFile,
|
||||||
|
onStartEdit,
|
||||||
|
onCancelEdit
|
||||||
}: FileManagerGridProps) {
|
}: FileManagerGridProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const gridRef = useRef<HTMLDivElement>(null);
|
const gridRef = useRef<HTMLDivElement>(null);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [dragCounter, setDragCounter] = useState(0);
|
const [dragCounter, setDragCounter] = useState(0);
|
||||||
|
const [editingName, setEditingName] = useState('');
|
||||||
|
const editInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// 开始编辑时设置初始名称
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingFile) {
|
||||||
|
setEditingName(editingFile.name);
|
||||||
|
// 延迟聚焦以确保DOM已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
editInputRef.current?.focus();
|
||||||
|
editInputRef.current?.select();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [editingFile]);
|
||||||
|
|
||||||
|
// 处理编辑确认
|
||||||
|
const handleEditConfirm = () => {
|
||||||
|
if (editingFile && onRename && editingName.trim() && editingName !== editingFile.name) {
|
||||||
|
onRename(editingFile, editingName.trim());
|
||||||
|
}
|
||||||
|
onCancelEdit?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理编辑取消
|
||||||
|
const handleEditCancel = () => {
|
||||||
|
setEditingName('');
|
||||||
|
onCancelEdit?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理输入框按键
|
||||||
|
const handleEditKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleEditConfirm();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleEditCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
const [isSelecting, setIsSelecting] = useState(false);
|
const [isSelecting, setIsSelecting] = useState(false);
|
||||||
const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null);
|
const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null);
|
||||||
const [selectionRect, setSelectionRect] = useState<{ x: number; y: number; width: number; height: number } | null>(null);
|
const [selectionRect, setSelectionRect] = useState<{ x: number; y: number; width: number; height: number } | null>(null);
|
||||||
@@ -480,9 +527,32 @@ export function FileManagerGrid({
|
|||||||
|
|
||||||
{/* 文件名 */}
|
{/* 文件名 */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<p className="text-xs text-white truncate" title={file.name}>
|
{editingFile?.path === file.path ? (
|
||||||
{file.name}
|
<input
|
||||||
</p>
|
ref={editInputRef}
|
||||||
|
type="text"
|
||||||
|
value={editingName}
|
||||||
|
onChange={(e) => setEditingName(e.target.value)}
|
||||||
|
onKeyDown={handleEditKeyDown}
|
||||||
|
onBlur={handleEditConfirm}
|
||||||
|
className="w-full text-xs bg-white text-black px-1 py-0.5 rounded border-2 border-blue-500 text-center"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
className="text-xs text-white truncate cursor-pointer"
|
||||||
|
title={file.name}
|
||||||
|
onClick={(e) => {
|
||||||
|
// 阻止文件选择事件
|
||||||
|
if (onStartEdit) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onStartEdit(file);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{file.size && file.type === 'file' && (
|
{file.size && file.type === 'file' && (
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
{formatFileSize(file.size)}
|
{formatFileSize(file.size)}
|
||||||
@@ -522,9 +592,32 @@ export function FileManagerGrid({
|
|||||||
|
|
||||||
{/* 文件信息 */}
|
{/* 文件信息 */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm text-white truncate" title={file.name}>
|
{editingFile?.path === file.path ? (
|
||||||
{file.name}
|
<input
|
||||||
</p>
|
ref={editInputRef}
|
||||||
|
type="text"
|
||||||
|
value={editingName}
|
||||||
|
onChange={(e) => setEditingName(e.target.value)}
|
||||||
|
onKeyDown={handleEditKeyDown}
|
||||||
|
onBlur={handleEditConfirm}
|
||||||
|
className="w-full text-sm bg-white text-black px-2 py-1 rounded border-2 border-blue-500"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
className="text-sm text-white truncate cursor-pointer"
|
||||||
|
title={file.name}
|
||||||
|
onClick={(e) => {
|
||||||
|
// 阻止文件选择事件
|
||||||
|
if (onStartEdit) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onStartEdit(file);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{file.modified && (
|
{file.modified && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{file.modified}
|
{file.modified}
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
operation: 'copy' | 'cut';
|
operation: 'copy' | 'cut';
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
// 编辑状态
|
||||||
|
const [editingFile, setEditingFile] = useState<FileItem | null>(null);
|
||||||
|
const [isCreatingNewFile, setIsCreatingNewFile] = useState(false);
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const {
|
const {
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
@@ -292,17 +296,25 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
async function handleCreateNewFolder() {
|
async function handleCreateNewFolder() {
|
||||||
if (!sshSessionId) return;
|
if (!sshSessionId) return;
|
||||||
|
|
||||||
const folderName = prompt(t("fileManager.enterFolderName"));
|
const defaultName = "New Folder";
|
||||||
if (!folderName) return;
|
const folderPath = currentPath.endsWith('/')
|
||||||
|
? `${currentPath}${defaultName}`
|
||||||
|
: `${currentPath}/${defaultName}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const folderPath = currentPath.endsWith('/')
|
|
||||||
? `${currentPath}${folderName}`
|
|
||||||
: `${currentPath}/${folderName}`;
|
|
||||||
|
|
||||||
await createSSHFolder(sshSessionId, folderPath);
|
await createSSHFolder(sshSessionId, folderPath);
|
||||||
toast.success(t("fileManager.folderCreatedSuccessfully", { name: folderName }));
|
|
||||||
loadDirectory(currentPath);
|
// 重新加载目录
|
||||||
|
await loadDirectory(currentPath);
|
||||||
|
|
||||||
|
// 找到新创建的文件夹并开始编辑
|
||||||
|
const newFolder: FileItem = {
|
||||||
|
name: defaultName,
|
||||||
|
type: 'directory',
|
||||||
|
path: folderPath
|
||||||
|
};
|
||||||
|
setEditingFile(newFolder);
|
||||||
|
setIsCreatingNewFile(true);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(t("fileManager.failedToCreateFolder"));
|
toast.error(t("fileManager.failedToCreateFolder"));
|
||||||
console.error("Create folder failed:", error);
|
console.error("Create folder failed:", error);
|
||||||
@@ -312,17 +324,26 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
async function handleCreateNewFile() {
|
async function handleCreateNewFile() {
|
||||||
if (!sshSessionId) return;
|
if (!sshSessionId) return;
|
||||||
|
|
||||||
const fileName = prompt(t("fileManager.enterFileName"));
|
const defaultName = "New File.txt";
|
||||||
if (!fileName) return;
|
const filePath = currentPath.endsWith('/')
|
||||||
|
? `${currentPath}${defaultName}`
|
||||||
|
: `${currentPath}/${defaultName}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filePath = currentPath.endsWith('/')
|
|
||||||
? `${currentPath}${fileName}`
|
|
||||||
: `${currentPath}/${fileName}`;
|
|
||||||
|
|
||||||
await createSSHFile(sshSessionId, filePath, "");
|
await createSSHFile(sshSessionId, filePath, "");
|
||||||
toast.success(t("fileManager.fileCreatedSuccessfully", { name: fileName }));
|
|
||||||
loadDirectory(currentPath);
|
// 重新加载目录
|
||||||
|
await loadDirectory(currentPath);
|
||||||
|
|
||||||
|
// 找到新创建的文件并开始编辑
|
||||||
|
const newFile: FileItem = {
|
||||||
|
name: defaultName,
|
||||||
|
type: 'file',
|
||||||
|
path: filePath,
|
||||||
|
size: 0
|
||||||
|
};
|
||||||
|
setEditingFile(newFile);
|
||||||
|
setIsCreatingNewFile(true);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(t("fileManager.failedToCreateFile"));
|
toast.error(t("fileManager.failedToCreateFile"));
|
||||||
console.error("Create file failed:", error);
|
console.error("Create file failed:", error);
|
||||||
@@ -401,13 +422,39 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleRenameFile(file: FileItem) {
|
function handleRenameFile(file: FileItem) {
|
||||||
|
setEditingFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理重命名确认
|
||||||
|
async function handleRenameConfirm(file: FileItem, newName: string) {
|
||||||
if (!sshSessionId) return;
|
if (!sshSessionId) return;
|
||||||
|
|
||||||
const newName = prompt(t("fileManager.enterNewName"), file.name);
|
const oldPath = file.path;
|
||||||
if (!newName || newName === file.name) return;
|
const newPath = file.path.replace(file.name, newName);
|
||||||
|
|
||||||
// TODO: 实现重命名功能
|
try {
|
||||||
toast.info("重命名功能正在开发中...");
|
await renameSSHItem(sshSessionId, oldPath, newPath);
|
||||||
|
toast.success(t("fileManager.itemRenamedSuccessfully", { name: newName }));
|
||||||
|
loadDirectory(currentPath);
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(t("fileManager.failedToRenameItem"));
|
||||||
|
console.error("Rename failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始编辑文件名
|
||||||
|
function handleStartEdit(file: FileItem) {
|
||||||
|
setEditingFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消编辑
|
||||||
|
function handleCancelEdit() {
|
||||||
|
if (isCreatingNewFile && editingFile) {
|
||||||
|
// 如果是新建文件/文件夹且取消了编辑,删除刚创建的项目
|
||||||
|
handleDeleteFiles([editingFile]);
|
||||||
|
setIsCreatingNewFile(false);
|
||||||
|
}
|
||||||
|
setEditingFile(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤文件
|
// 过滤文件
|
||||||
@@ -540,6 +587,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
onUpload={handleFilesDropped}
|
onUpload={handleFilesDropped}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
|
onRename={handleRenameConfirm}
|
||||||
|
editingFile={editingFile}
|
||||||
|
onStartEdit={handleStartEdit}
|
||||||
|
onCancelEdit={handleCancelEdit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 右键菜单 */}
|
{/* 右键菜单 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user