DocHub/frontend/src/components/FileExplorer.tsx
Tianyang 39761b4ce7 fix: lock sidebar width and add rendered markdown preview
Sidebar overflowed horizontally on long file/folder names, which broke
vertical scrolling too — locked width with min/max-width plus
overflow-x: hidden, and truncate folder names with ellipsis.

Documents now open in a rendered preview (marked + DOMPurify) by
default, with an 编辑/预览 toggle to switch to the CodeMirror source view.
2026-08-01 18:30:50 +08:00

120 lines
3.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import { listDocuments, type DocumentFile } from '../api/client';
interface FileExplorerProps {
onSelectFile: (key: string) => void;
selectedKey?: string | null;
refreshKey?: number;
}
export default function FileExplorer({ onSelectFile, selectedKey, refreshKey }: FileExplorerProps) {
const [folders, setFolders] = useState<string[]>([]);
const [files, setFiles] = useState<DocumentFile[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
listDocuments('')
.then((data) => {
setFolders(data.folders);
setFiles(data.files);
})
.catch(() => setError('无法加载文档目录'));
}, [refreshKey]);
if (error) {
return <div data-testid="file-explorer-error">{error}</div>;
}
return (
<div data-testid="file-explorer">
{folders.map((folder) => (
<FolderNode key={folder} name={folder} path={folder} onSelectFile={onSelectFile} selectedKey={selectedKey} />
))}
{files.map((file) => (
<FileRow key={file.key} file={file} onSelectFile={onSelectFile} selectedKey={selectedKey} />
))}
</div>
);
}
function FileRow({
file,
onSelectFile,
selectedKey,
}: {
file: DocumentFile;
onSelectFile: (key: string) => void;
selectedKey?: string | null;
}) {
return (
<div
data-testid={`file-${file.key}`}
className={selectedKey === file.key ? 'tree-item file-row active' : 'tree-item file-row'}
onClick={() => onSelectFile(file.key)}
>
<svg className="file-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
<path d="M14 2v6h6" />
</svg>
<span className="filename">{file.name}</span>
</div>
);
}
function FolderNode({
name,
path,
onSelectFile,
selectedKey,
}: {
name: string;
path: string;
onSelectFile: (key: string) => void;
selectedKey?: string | null;
}) {
const [expanded, setExpanded] = useState(false);
const [loaded, setLoaded] = useState(false);
const [folders, setFolders] = useState<string[]>([]);
const [files, setFiles] = useState<DocumentFile[]>([]);
function toggle() {
if (!expanded && !loaded) {
listDocuments(path).then((data) => {
setFolders(data.folders);
setFiles(data.files);
setLoaded(true);
});
}
setExpanded((prev) => !prev);
}
return (
<div>
<div data-testid={`folder-${path}`} className="tree-item folder" onClick={toggle}>
<span className={expanded ? 'chevron open' : 'chevron'}>
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z" />
</svg>
</span>
<span className="foldername">{name}</span>
</div>
{expanded && (
<div className="tree-children">
{folders.map((folder) => (
<FolderNode
key={`${path}/${folder}`}
name={folder}
path={`${path}/${folder}`}
onSelectFile={onSelectFile}
selectedKey={selectedKey}
/>
))}
{files.map((file) => (
<FileRow key={file.key} file={file} onSelectFile={onSelectFile} selectedKey={selectedKey} />
))}
</div>
)}
</div>
);
}