feat: Advanced file editor and file tree improvements (#444)

# Features
- File drag and drop upload: Support uploading files and folders via drag and drop
- Binary file handling: Detect binary files and display a friendly message instead of trying to edit them
- Folder download: Download folders as ZIP files (using JSZip library)
- Context menu integration: Full right-click context menu for file operations (rename, delete, copy path, download, new file/folder)
This commit is contained in:
朱见
2026-03-03 20:19:46 +08:00
committed by GitHub
parent 503c384685
commit 97689588aa
30 changed files with 2270 additions and 94 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { api } from '../../../utils/api';
import type { Project } from '../../../types/app';
import type { FileTreeNode } from '../types/types';
@@ -6,11 +6,18 @@ import type { FileTreeNode } from '../types/types';
type UseFileTreeDataResult = {
files: FileTreeNode[];
loading: boolean;
refreshFiles: () => void;
};
export function useFileTreeData(selectedProject: Project | null): UseFileTreeDataResult {
const [files, setFiles] = useState<FileTreeNode[]>([]);
const [loading, setLoading] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const abortControllerRef = useRef<AbortController | null>(null);
const refreshFiles = useCallback(() => {
setRefreshKey((prev) => prev + 1);
}, []);
useEffect(() => {
const projectName = selectedProject?.name;
@@ -21,7 +28,12 @@ export function useFileTreeData(selectedProject: Project | null): UseFileTreeDat
return;
}
const abortController = new AbortController();
// Abort previous request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
// Track mount state so aborted or late responses do not enqueue stale state updates.
let isActive = true;
@@ -30,7 +42,7 @@ export function useFileTreeData(selectedProject: Project | null): UseFileTreeDat
setLoading(true);
}
try {
const response = await api.getFiles(projectName, { signal: abortController.signal });
const response = await api.getFiles(projectName, { signal: abortControllerRef.current!.signal });
if (!response.ok) {
const errorText = await response.text();
@@ -65,12 +77,13 @@ export function useFileTreeData(selectedProject: Project | null): UseFileTreeDat
return () => {
isActive = false;
abortController.abort();
abortControllerRef.current?.abort();
};
}, [selectedProject?.name]);
}, [selectedProject?.name, refreshKey]);
return {
files,
loading,
refreshFiles,
};
}