Eliminate file creation duplicate logic with Linus-style redesign

Following "good taste" principles to separate create intent from actual files:

DATA STRUCTURE REDESIGN:
- Add CreateIntent interface to separate intent from reality
- Replace mixed virtual/real file handling with pure separation
- Remove isCreatingNewFile state that caused confusion

ELIMINATE SPECIAL CASES:
- Cancel operation now has zero side effects (was creating default files)
- Remove complex conditional logic in handleCancelEdit
- Separate handleConfirmCreate from handleRenameConfirm responsibilities

SIMPLIFY USER FLOW:
- Create intent → Show UI → Confirm → Create file
- Cancel intent → Clean state → No side effects
- No more "NewFolder" + "UserName" duplicate creation

UI COMPONENTS:
- Add CreateIntentGridItem and CreateIntentListItem
- Render create intent separately from real files
- Focus/select input automatically with ESC/Enter handling

Resolves: Users reporting duplicate files on creation
Core fix: Eliminates the "special case" of cancel-creates-file
Result: Predictable, elegant file creation flow
This commit is contained in:
ZacharyZcR
2025-09-19 01:53:14 +08:00
parent 9b817488ff
commit 2019b81254
2 changed files with 230 additions and 136 deletions

View File

@@ -25,6 +25,14 @@ import {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { FileItem } from "../../../types/index.js"; import type { FileItem } from "../../../types/index.js";
// Linus式数据结构创建意图与实际文件分离
interface CreateIntent {
id: string;
type: 'file' | 'directory';
defaultName: string;
currentName: string;
}
// 格式化文件大小 // 格式化文件大小
function formatFileSize(bytes?: number): string { function formatFileSize(bytes?: number): string {
// 处理未定义或null的情况 // 处理未定义或null的情况
@@ -84,6 +92,10 @@ interface FileManagerGridProps {
onFileDiff?: (file1: FileItem, file2: FileItem) => void; onFileDiff?: (file1: FileItem, file2: FileItem) => void;
onSystemDragStart?: (files: FileItem[]) => void; onSystemDragStart?: (files: FileItem[]) => void;
onSystemDragEnd?: (e: DragEvent) => void; onSystemDragEnd?: (e: DragEvent) => void;
// Linus式创建意图props
createIntent?: CreateIntent | null;
onConfirmCreate?: (name: string) => void;
onCancelCreate?: () => void;
} }
const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => { const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => {
@@ -182,6 +194,9 @@ export function FileManagerGrid({
onFileDiff, onFileDiff,
onSystemDragStart, onSystemDragStart,
onSystemDragEnd, onSystemDragEnd,
createIntent,
onConfirmCreate,
onCancelCreate,
}: FileManagerGridProps) { }: FileManagerGridProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const gridRef = useRef<HTMLDivElement>(null); const gridRef = useRef<HTMLDivElement>(null);
@@ -1108,6 +1123,14 @@ export function FileManagerGrid({
</div> </div>
) : viewMode === "grid" ? ( ) : viewMode === "grid" ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
{/* Linus式创建意图UI - 纯粹分离 */}
{createIntent && (
<CreateIntentGridItem
intent={createIntent}
onConfirm={onConfirmCreate}
onCancel={onCancelCreate}
/>
)}
{files.map((file) => { {files.map((file) => {
const isSelected = selectedFiles.some( const isSelected = selectedFiles.some(
(f) => f.path === file.path, (f) => f.path === file.path,
@@ -1218,6 +1241,14 @@ export function FileManagerGrid({
) : ( ) : (
/* 列表视图 */ /* 列表视图 */
<div className="space-y-1"> <div className="space-y-1">
{/* Linus式创建意图UI - 列表视图 */}
{createIntent && (
<CreateIntentListItem
intent={createIntent}
onConfirm={onConfirmCreate}
onCancel={onCancelCreate}
/>
)}
{files.map((file) => { {files.map((file) => {
const isSelected = selectedFiles.some( const isSelected = selectedFiles.some(
(f) => f.path === file.path, (f) => f.path === file.path,
@@ -1395,3 +1426,107 @@ export function FileManagerGrid({
</div> </div>
); );
} }
// Linus式创建意图组件Grid视图
function CreateIntentGridItem({
intent,
onConfirm,
onCancel,
}: {
intent: CreateIntent;
onConfirm?: (name: string) => void;
onCancel?: () => void;
}) {
const [inputName, setInputName] = useState(intent.currentName);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
inputRef.current?.select();
}, []);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
onConfirm?.(inputName.trim());
} else if (e.key === "Escape") {
e.preventDefault();
onCancel?.();
}
};
return (
<div className="group p-3 rounded-lg border-2 border-dashed border-primary bg-primary/10 transition-all">
<div className="flex flex-col items-center text-center">
<div className="mb-2">
{intent.type === 'directory' ? (
<Folder className="w-8 h-8 text-primary" />
) : (
<File className="w-8 h-8 text-primary" />
)}
</div>
<input
ref={inputRef}
type="text"
value={inputName}
onChange={(e) => setInputName(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={() => onConfirm?.(inputName.trim())}
className="w-full max-w-[120px] rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-xs text-center text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px] outline-none"
placeholder={intent.type === 'directory' ? 'Folder name' : 'File name'}
/>
</div>
</div>
);
}
// Linus式创建意图组件List视图
function CreateIntentListItem({
intent,
onConfirm,
onCancel,
}: {
intent: CreateIntent;
onConfirm?: (name: string) => void;
onCancel?: () => void;
}) {
const [inputName, setInputName] = useState(intent.currentName);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
inputRef.current?.select();
}, []);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
onConfirm?.(inputName.trim());
} else if (e.key === "Escape") {
e.preventDefault();
onCancel?.();
}
};
return (
<div className="flex items-center gap-3 p-2 rounded border-2 border-dashed border-primary bg-primary/10 transition-all">
<div className="flex-shrink-0">
{intent.type === 'directory' ? (
<Folder className="w-6 h-6 text-primary" />
) : (
<File className="w-6 h-6 text-primary" />
)}
</div>
<input
ref={inputRef}
type="text"
value={inputName}
onChange={(e) => setInputName(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={() => onConfirm?.(inputName.trim())}
className="flex-1 min-w-0 max-w-[200px] rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-sm text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px] outline-none"
placeholder={intent.type === 'directory' ? 'Folder name' : 'File name'}
/>
</div>
);
}

View File

@@ -52,6 +52,14 @@ interface FileManagerModernProps {
onClose?: () => void; onClose?: () => void;
} }
// Linus式数据结构创建意图与实际文件完全分离
interface CreateIntent {
id: string;
type: 'file' | 'directory';
defaultName: string;
currentName: string;
}
// 内部组件,使用窗口管理器 // 内部组件,使用窗口管理器
function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
const { openWindow } = useWindowManager(); const { openWindow } = useWindowManager();
@@ -111,9 +119,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
const [undoHistory, setUndoHistory] = useState<UndoAction[]>([]); const [undoHistory, setUndoHistory] = useState<UndoAction[]>([]);
// 编辑状态 // Linus式状态创建意图与文件编辑分离
const [createIntent, setCreateIntent] = useState<CreateIntent | null>(null);
const [editingFile, setEditingFile] = useState<FileItem | null>(null); const [editingFile, setEditingFile] = useState<FileItem | null>(null);
const [isCreatingNewFile, setIsCreatingNewFile] = useState(false);
// Hooks // Hooks
const { selectedFiles, selectFile, selectAll, clearSelection, setSelection } = const { selectedFiles, selectFile, selectAll, clearSelection, setSelection } =
@@ -476,43 +484,27 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
} }
} }
// Linus式创建纯粹的意图无副作用
function handleCreateNewFolder() { function handleCreateNewFolder() {
const baseName = "NewFolder"; const defaultName = generateUniqueName("NewFolder", "directory");
const uniqueName = generateUniqueName(baseName, "directory"); setCreateIntent({
const folderPath = currentPath.endsWith("/") id: Date.now().toString(),
? `${currentPath}${uniqueName}` type: 'directory',
: `${currentPath}/${uniqueName}`; defaultName,
currentName: defaultName
// 直接进入编辑模式,使用唯一名字 });
const newFolder: FileItem = { console.log("Create folder intent:", defaultName);
name: uniqueName,
type: "directory",
path: folderPath,
};
console.log("Starting edit for new folder with unique name:", newFolder);
setEditingFile(newFolder);
setIsCreatingNewFile(true);
} }
function handleCreateNewFile() { function handleCreateNewFile() {
const baseName = "NewFile.txt"; const defaultName = generateUniqueName("NewFile.txt", "file");
const uniqueName = generateUniqueName(baseName, "file"); setCreateIntent({
const filePath = currentPath.endsWith("/") id: Date.now().toString(),
? `${currentPath}${uniqueName}` type: 'file',
: `${currentPath}/${uniqueName}`; defaultName,
currentName: defaultName
// 直接进入编辑模式,使用唯一名字 });
const newFile: FileItem = { console.log("Create file intent:", defaultName);
name: uniqueName,
type: "file",
path: filePath,
size: 0,
};
console.log("Starting edit for new file with unique name:", newFile);
setEditingFile(newFile);
setIsCreatingNewFile(true);
} }
// Handle symlink resolution // Handle symlink resolution
@@ -980,92 +972,76 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
} }
} }
// 处理重命名/创建确认 // Linus式创建确认纯粹的创建无混杂逻辑
async function handleConfirmCreate(name: string) {
if (!createIntent || !sshSessionId) return;
try {
await ensureSSHConnection();
console.log(`Creating ${createIntent.type}:`, name);
if (createIntent.type === "file") {
await createSSHFile(
sshSessionId,
currentPath,
name,
"",
currentHost?.id,
currentHost?.userId?.toString(),
);
toast.success(t("fileManager.fileCreatedSuccessfully", { name }));
} else {
await createSSHFolder(
sshSessionId,
currentPath,
name,
currentHost?.id,
currentHost?.userId?.toString(),
);
toast.success(t("fileManager.folderCreatedSuccessfully", { name }));
}
setCreateIntent(null); // 清理意图
handleRefreshDirectory();
} catch (error: any) {
console.error("Create failed:", error);
toast.error(t("fileManager.failedToCreateItem"));
}
}
// Linus式取消零副作用
function handleCancelCreate() {
setCreateIntent(null); // 就这么简单!
console.log("Create cancelled - no side effects");
}
// 纯粹的重命名确认:只处理真实文件
async function handleRenameConfirm(file: FileItem, newName: string) { async function handleRenameConfirm(file: FileItem, newName: string) {
if (!sshSessionId) return; if (!sshSessionId) return;
try { try {
// 确保SSH连接有效
await ensureSSHConnection(); await ensureSSHConnection();
if (isCreatingNewFile) { console.log("Renaming existing item:", {
// 新建项目:直接创建最终名字 from: file.path,
console.log("Creating new item:", { to: newName,
type: file.type, });
name: newName,
path: currentPath,
hostId: currentHost?.id,
userId: currentHost?.userId,
});
if (file.type === "file") { await renameSSHItem(
await createSSHFile( sshSessionId,
sshSessionId, file.path,
currentPath, newName,
newName, currentHost?.id,
"", currentHost?.userId?.toString(),
currentHost?.id, );
currentHost?.userId?.toString(),
);
toast.success(
t("fileManager.fileCreatedSuccessfully", { name: newName }),
);
} else if (file.type === "directory") {
await createSSHFolder(
sshSessionId,
currentPath,
newName,
currentHost?.id,
currentHost?.userId?.toString(),
);
toast.success(
t("fileManager.folderCreatedSuccessfully", { name: newName }),
);
}
setIsCreatingNewFile(false); toast.success(t("fileManager.itemRenamedSuccessfully", { name: newName }));
} else {
// 现有项目:重命名
console.log("Renaming existing item:", {
from: file.path,
to: newName,
hostId: currentHost?.id,
userId: currentHost?.userId,
});
await renameSSHItem(
sshSessionId,
file.path,
newName,
currentHost?.id,
currentHost?.userId?.toString(),
);
toast.success(
t("fileManager.itemRenamedSuccessfully", { name: newName }),
);
}
// 清除编辑状态
setEditingFile(null); setEditingFile(null);
handleRefreshDirectory(); handleRefreshDirectory();
} catch (error: any) { } catch (error: any) {
console.error("Rename failed with error:", { console.error("Rename failed:", error);
error, toast.error(t("fileManager.failedToRenameItem"));
oldPath,
newName,
message: error.message,
});
if (
error.message?.includes("connection") ||
error.message?.includes("established")
) {
toast.error(
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
);
} else {
toast.error(t("fileManager.failedToRenameItem"));
}
} }
} }
@@ -1074,18 +1050,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
setEditingFile(file); setEditingFile(file);
} }
// 取消编辑(现在也会保留项目) // Linus式取消编辑纯粹的取消无副作用
async function handleCancelEdit() { function handleCancelEdit() {
if (isCreatingNewFile && editingFile) { setEditingFile(null); // 简洁优雅
// 取消时也使用默认名字创建项目 console.log("Edit cancelled - no side effects");
console.log(
"Creating item with default name on cancel:",
editingFile.name,
);
await handleRenameConfirm(editingFile, editingFile.name);
} else {
setEditingFile(null);
}
} }
// 生成唯一名字(处理重名冲突) // 生成唯一名字(处理重名冲突)
@@ -1492,23 +1460,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
} }
}, [currentHost?.id]); }, [currentHost?.id]);
// 过滤文件并添加新建的临时项目 // Linus式数据分离只过滤真实文件
let filteredFiles = files.filter((file) => const filteredFiles = files.filter((file) =>
file.name.toLowerCase().includes(searchQuery.toLowerCase()), file.name.toLowerCase().includes(searchQuery.toLowerCase()),
); );
// 如果正在创建新项目,将其添加到列表中
if (isCreatingNewFile && editingFile) {
// 检查是否已经存在同名项目,避免重复
const exists = filteredFiles.some((f) => f.path === editingFile.path);
if (
!exists &&
editingFile.name.toLowerCase().includes(searchQuery.toLowerCase())
) {
filteredFiles = [editingFile, ...filteredFiles]; // 将新项目放在前面
}
}
if (!currentHost) { if (!currentHost) {
return ( return (
<div className="h-full flex items-center justify-center"> <div className="h-full flex items-center justify-center">
@@ -1661,6 +1617,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
onFileDiff={handleFileDiff} onFileDiff={handleFileDiff}
onSystemDragStart={handleFileDragStart} onSystemDragStart={handleFileDragStart}
onSystemDragEnd={handleFileDragEnd} onSystemDragEnd={handleFileDragEnd}
createIntent={createIntent}
onConfirmCreate={handleConfirmCreate}
onCancelCreate={handleCancelCreate}
/> />
{/* 右键菜单 */} {/* 右键菜单 */}