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 type { FileItem } from "../../../types/index.js";
// Linus式数据结构创建意图与实际文件分离
interface CreateIntent {
id: string;
type: 'file' | 'directory';
defaultName: string;
currentName: string;
}
// 格式化文件大小
function formatFileSize(bytes?: number): string {
// 处理未定义或null的情况
@@ -84,6 +92,10 @@ interface FileManagerGridProps {
onFileDiff?: (file1: FileItem, file2: FileItem) => void;
onSystemDragStart?: (files: FileItem[]) => void;
onSystemDragEnd?: (e: DragEvent) => void;
// Linus式创建意图props
createIntent?: CreateIntent | null;
onConfirmCreate?: (name: string) => void;
onCancelCreate?: () => void;
}
const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => {
@@ -182,6 +194,9 @@ export function FileManagerGrid({
onFileDiff,
onSystemDragStart,
onSystemDragEnd,
createIntent,
onConfirmCreate,
onCancelCreate,
}: FileManagerGridProps) {
const { t } = useTranslation();
const gridRef = useRef<HTMLDivElement>(null);
@@ -1108,6 +1123,14 @@ export function FileManagerGrid({
</div>
) : 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">
{/* Linus式创建意图UI - 纯粹分离 */}
{createIntent && (
<CreateIntentGridItem
intent={createIntent}
onConfirm={onConfirmCreate}
onCancel={onCancelCreate}
/>
)}
{files.map((file) => {
const isSelected = selectedFiles.some(
(f) => f.path === file.path,
@@ -1218,6 +1241,14 @@ export function FileManagerGrid({
) : (
/* 列表视图 */
<div className="space-y-1">
{/* Linus式创建意图UI - 列表视图 */}
{createIntent && (
<CreateIntentListItem
intent={createIntent}
onConfirm={onConfirmCreate}
onCancel={onCancelCreate}
/>
)}
{files.map((file) => {
const isSelected = selectedFiles.some(
(f) => f.path === file.path,
@@ -1395,3 +1426,107 @@ export function FileManagerGrid({
</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;
}
// Linus式数据结构创建意图与实际文件完全分离
interface CreateIntent {
id: string;
type: 'file' | 'directory';
defaultName: string;
currentName: string;
}
// 内部组件,使用窗口管理器
function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
const { openWindow } = useWindowManager();
@@ -111,9 +119,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
const [undoHistory, setUndoHistory] = useState<UndoAction[]>([]);
// 编辑状态
// Linus式状态创建意图与文件编辑分离
const [createIntent, setCreateIntent] = useState<CreateIntent | null>(null);
const [editingFile, setEditingFile] = useState<FileItem | null>(null);
const [isCreatingNewFile, setIsCreatingNewFile] = useState(false);
// Hooks
const { selectedFiles, selectFile, selectAll, clearSelection, setSelection } =
@@ -476,43 +484,27 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
}
}
// Linus式创建纯粹的意图无副作用
function handleCreateNewFolder() {
const baseName = "NewFolder";
const uniqueName = generateUniqueName(baseName, "directory");
const folderPath = currentPath.endsWith("/")
? `${currentPath}${uniqueName}`
: `${currentPath}/${uniqueName}`;
// 直接进入编辑模式,使用唯一名字
const newFolder: FileItem = {
name: uniqueName,
type: "directory",
path: folderPath,
};
console.log("Starting edit for new folder with unique name:", newFolder);
setEditingFile(newFolder);
setIsCreatingNewFile(true);
const defaultName = generateUniqueName("NewFolder", "directory");
setCreateIntent({
id: Date.now().toString(),
type: 'directory',
defaultName,
currentName: defaultName
});
console.log("Create folder intent:", defaultName);
}
function handleCreateNewFile() {
const baseName = "NewFile.txt";
const uniqueName = generateUniqueName(baseName, "file");
const filePath = currentPath.endsWith("/")
? `${currentPath}${uniqueName}`
: `${currentPath}/${uniqueName}`;
// 直接进入编辑模式,使用唯一名字
const newFile: FileItem = {
name: uniqueName,
type: "file",
path: filePath,
size: 0,
};
console.log("Starting edit for new file with unique name:", newFile);
setEditingFile(newFile);
setIsCreatingNewFile(true);
const defaultName = generateUniqueName("NewFile.txt", "file");
setCreateIntent({
id: Date.now().toString(),
type: 'file',
defaultName,
currentName: defaultName
});
console.log("Create file intent:", defaultName);
}
// 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) {
if (!sshSessionId) return;
try {
// 确保SSH连接有效
await ensureSSHConnection();
if (isCreatingNewFile) {
// 新建项目:直接创建最终名字
console.log("Creating new item:", {
type: file.type,
name: newName,
path: currentPath,
hostId: currentHost?.id,
userId: currentHost?.userId,
});
console.log("Renaming existing item:", {
from: file.path,
to: newName,
});
if (file.type === "file") {
await createSSHFile(
sshSessionId,
currentPath,
newName,
"",
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 }),
);
}
await renameSSHItem(
sshSessionId,
file.path,
newName,
currentHost?.id,
currentHost?.userId?.toString(),
);
setIsCreatingNewFile(false);
} 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 }),
);
}
// 清除编辑状态
toast.success(t("fileManager.itemRenamedSuccessfully", { name: newName }));
setEditingFile(null);
handleRefreshDirectory();
} catch (error: any) {
console.error("Rename failed with error:", {
error,
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"));
}
console.error("Rename failed:", error);
toast.error(t("fileManager.failedToRenameItem"));
}
}
@@ -1074,18 +1050,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
setEditingFile(file);
}
// 取消编辑(现在也会保留项目)
async function handleCancelEdit() {
if (isCreatingNewFile && editingFile) {
// 取消时也使用默认名字创建项目
console.log(
"Creating item with default name on cancel:",
editingFile.name,
);
await handleRenameConfirm(editingFile, editingFile.name);
} else {
setEditingFile(null);
}
// Linus式取消编辑纯粹的取消无副作用
function handleCancelEdit() {
setEditingFile(null); // 简洁优雅
console.log("Edit cancelled - no side effects");
}
// 生成唯一名字(处理重名冲突)
@@ -1492,23 +1460,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
}
}, [currentHost?.id]);
// 过滤文件并添加新建的临时项目
let filteredFiles = files.filter((file) =>
// Linus式数据分离只过滤真实文件
const filteredFiles = files.filter((file) =>
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) {
return (
<div className="h-full flex items-center justify-center">
@@ -1661,6 +1617,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
onFileDiff={handleFileDiff}
onSystemDragStart={handleFileDragStart}
onSystemDragEnd={handleFileDragEnd}
createIntent={createIntent}
onConfirmCreate={handleConfirmCreate}
onCancelCreate={handleCancelCreate}
/>
{/* 右键菜单 */}