u
This commit is contained in:
735
src/components/vfs/VfsFileExplorer.jsx
Normal file
735
src/components/vfs/VfsFileExplorer.jsx
Normal file
@@ -0,0 +1,735 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Button from '@mui/material/Button';
|
||||
import InputBase from '@mui/material/InputBase';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import SearchRoundedIcon from '@mui/icons-material/SearchRounded';
|
||||
import ClearRoundedIcon from '@mui/icons-material/ClearRounded';
|
||||
import RefreshRoundedIcon from '@mui/icons-material/RefreshRounded';
|
||||
import NoteAddRoundedIcon from '@mui/icons-material/NoteAddRounded';
|
||||
import CloudUploadRoundedIcon from '@mui/icons-material/CloudUploadRounded';
|
||||
import DownloadRoundedIcon from '@mui/icons-material/DownloadRounded';
|
||||
import DeleteOutlineRoundedIcon from '@mui/icons-material/DeleteOutlineRounded';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import VisibilityRoundedIcon from '@mui/icons-material/VisibilityRounded';
|
||||
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded';
|
||||
import FolderRoundedIcon from '@mui/icons-material/FolderRounded';
|
||||
import FolderOpenRoundedIcon from '@mui/icons-material/FolderOpenRounded';
|
||||
import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded';
|
||||
import KeyboardArrowRightRoundedIcon from '@mui/icons-material/KeyboardArrowRightRounded';
|
||||
import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded';
|
||||
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
||||
|
||||
import VfsFileIcon, { getCategoryBadge } from './VfsFileIcon';
|
||||
import { formatVfsSize, formatVfsDate } from './vfsUtils';
|
||||
|
||||
/**
|
||||
* Returns all ancestor directory paths for a file or folder path.
|
||||
* e.g. "docs/api/v1/spec.json" -> ["docs", "docs/api", "docs/api/v1"]
|
||||
*/
|
||||
function getParentFolderPaths(filePath) {
|
||||
if (!filePath) return [];
|
||||
const clean = String(filePath).replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
const parts = clean.split('/').filter(Boolean);
|
||||
const paths = [];
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
paths.push(parts.slice(0, i + 1).join('/'));
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a hierarchical tree node structure from a flat list of VfsFiles.
|
||||
* In every node, folders and files are separated and can be rendered recursively.
|
||||
*/
|
||||
function buildFileTree(files, searchQuery = '') {
|
||||
const q = searchQuery ? searchQuery.trim().toLowerCase() : '';
|
||||
const filtered = (files || []).filter((f) => {
|
||||
if (!q) return true;
|
||||
return f.name.toLowerCase().includes(q) || (f.mime && f.mime.toLowerCase().includes(q));
|
||||
});
|
||||
|
||||
const root = {
|
||||
name: '',
|
||||
path: '',
|
||||
isDirectory: true,
|
||||
folders: new Map(),
|
||||
files: [],
|
||||
};
|
||||
|
||||
for (const file of filtered) {
|
||||
const cleanPath = String(file.name || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
if (!cleanPath) continue;
|
||||
|
||||
const parts = cleanPath.split('/').filter(Boolean);
|
||||
let current = root;
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const folderName = parts[i];
|
||||
const folderPath = parts.slice(0, i + 1).join('/');
|
||||
|
||||
if (!current.folders.has(folderName)) {
|
||||
current.folders.set(folderName, {
|
||||
name: folderName,
|
||||
path: folderPath,
|
||||
isDirectory: true,
|
||||
folders: new Map(),
|
||||
files: [],
|
||||
});
|
||||
}
|
||||
current = current.folders.get(folderName);
|
||||
}
|
||||
|
||||
const fileName = parts[parts.length - 1];
|
||||
current.files.push({
|
||||
name: fileName,
|
||||
path: cleanPath,
|
||||
isDirectory: false,
|
||||
fileData: file,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate recursive file counts
|
||||
function countFiles(node) {
|
||||
let count = node.files.length;
|
||||
for (const folder of node.folders.values()) {
|
||||
count += countFiles(folder);
|
||||
}
|
||||
node.totalFileCount = count;
|
||||
return count;
|
||||
}
|
||||
countFiles(root);
|
||||
|
||||
return { root, totalCount: filtered.length };
|
||||
}
|
||||
|
||||
export default class VfsFileExplorer extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initialExpanded = new Set();
|
||||
if (props.selectedFileName) {
|
||||
for (const p of getParentFolderPaths(props.selectedFileName)) {
|
||||
initialExpanded.add(p);
|
||||
}
|
||||
}
|
||||
this.state = {
|
||||
menuAnchor: null,
|
||||
menuFile: null,
|
||||
deleteConfirmFile: null,
|
||||
isDraggingOver: false,
|
||||
expandedFolders: initialExpanded,
|
||||
};
|
||||
this.fileInputRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.selectedFileName && this.props.selectedFileName !== prevProps.selectedFileName) {
|
||||
const parents = getParentFolderPaths(this.props.selectedFileName);
|
||||
if (parents.length > 0) {
|
||||
this.setState((prev) => {
|
||||
const next = new Set(prev.expandedFolders);
|
||||
let changed = false;
|
||||
for (const p of parents) {
|
||||
if (!next.has(p)) {
|
||||
next.add(p);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? { expandedFolders: next } : null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleFolder = (folderPath) => {
|
||||
this.setState((prev) => {
|
||||
const next = new Set(prev.expandedFolders);
|
||||
if (next.has(folderPath)) {
|
||||
next.delete(folderPath);
|
||||
} else {
|
||||
next.add(folderPath);
|
||||
}
|
||||
return { expandedFolders: next };
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
handleOpenMenu = (e, file) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setState({ menuAnchor: e.currentTarget, menuFile: file });
|
||||
};
|
||||
|
||||
handleCloseMenu = () => {
|
||||
this.setState({ menuAnchor: null, menuFile: null });
|
||||
};
|
||||
|
||||
handleTriggerUpload = () => {
|
||||
this.fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
handleFileInputChange = (e) => {
|
||||
const files = e.target?.files;
|
||||
if (files && files.length > 0) {
|
||||
this.props.onUpload(Array.from(files));
|
||||
}
|
||||
if (e.target) e.target.value = '';
|
||||
};
|
||||
|
||||
handleDragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.dataTransfer?.types?.includes('Files')) {
|
||||
this.setState({ isDraggingOver: true });
|
||||
}
|
||||
};
|
||||
|
||||
handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!this.state.isDraggingOver && e.dataTransfer?.types?.includes('Files')) {
|
||||
this.setState({ isDraggingOver: true });
|
||||
}
|
||||
};
|
||||
|
||||
handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.currentTarget.contains(e.relatedTarget)) return;
|
||||
this.setState({ isDraggingOver: false });
|
||||
};
|
||||
|
||||
handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.setState({ isDraggingOver: false });
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
this.props.onUpload(Array.from(files));
|
||||
}
|
||||
};
|
||||
|
||||
handleCopyPath = (file) => {
|
||||
if (!file) return;
|
||||
navigator.clipboard?.writeText?.(file.name);
|
||||
this.handleCloseMenu();
|
||||
};
|
||||
|
||||
handleMentionInChat = (file) => {
|
||||
if (!file) return;
|
||||
if (this.props.onMentionInChat) {
|
||||
this.props.onMentionInChat(file);
|
||||
}
|
||||
this.handleCloseMenu();
|
||||
};
|
||||
|
||||
renderFolderNode = (folder, depth) => {
|
||||
const { searchQuery } = this.props;
|
||||
const isSearching = Boolean(searchQuery && searchQuery.trim());
|
||||
const isExpanded = isSearching || this.state.expandedFolders.has(folder.path);
|
||||
|
||||
const subfolders = Array.from(folder.folders.values()).sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
|
||||
);
|
||||
const files = [...folder.files].sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
|
||||
);
|
||||
|
||||
return (
|
||||
<Box key={folder.path} sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Folder Header Row */}
|
||||
<Box
|
||||
onClick={() => this.toggleFolder(folder.path)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
py: 0.5,
|
||||
px: 1,
|
||||
pl: `${depth * 14 + 6}px`,
|
||||
borderRadius: 1.5,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
transition: 'background-color 0.1s',
|
||||
'&:hover': {
|
||||
bgcolor: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Chevron */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 18,
|
||||
height: 18,
|
||||
mr: 0.4,
|
||||
color: 'text.secondary',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<KeyboardArrowDownRoundedIcon sx={{ fontSize: 17 }} />
|
||||
) : (
|
||||
<KeyboardArrowRightRoundedIcon sx={{ fontSize: 17 }} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Folder Icon */}
|
||||
{isExpanded ? (
|
||||
<FolderOpenRoundedIcon
|
||||
sx={{ fontSize: 18, color: 'primary.main', mr: 0.8, flexShrink: 0 }}
|
||||
/>
|
||||
) : (
|
||||
<FolderRoundedIcon
|
||||
sx={{ fontSize: 18, opacity: 0.75, color: 'text.secondary', mr: 0.8, flexShrink: 0 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Folder Name */}
|
||||
<Typography
|
||||
variant="body2"
|
||||
noWrap
|
||||
fontWeight={600}
|
||||
sx={{ fontSize: '0.82rem', fontFamily: 'monospace', color: 'text.primary', flexGrow: 1 }}
|
||||
>
|
||||
{folder.name}
|
||||
</Typography>
|
||||
|
||||
{/* Total Files Count in Folder */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '0.7rem',
|
||||
color: 'text.disabled',
|
||||
ml: 1,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{folder.totalFileCount}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Folder Children (Directories FIRST, then files) */}
|
||||
{isExpanded && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{/* 1. Subdirectories FIRST */}
|
||||
{subfolders.map((sub) => this.renderFolderNode(sub, depth + 1))}
|
||||
|
||||
{/* 2. Files NEXT */}
|
||||
{files.map((file) => this.renderFileNode(file, depth + 1))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
renderFileNode = (file, depth) => {
|
||||
const { selectedFileName, onSelectFile } = this.props;
|
||||
const isSelected = file.path === selectedFileName;
|
||||
const badge = getCategoryBadge(file.path, file.fileData.mime);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={file.path}
|
||||
onClick={() => onSelectFile(file.path)}
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
py: 0.5,
|
||||
px: 1,
|
||||
pl: `${depth * 14 + 26}px`,
|
||||
borderRadius: 1.5,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
bgcolor: isSelected
|
||||
? theme.palette.mode === 'dark'
|
||||
? 'primary.dark'
|
||||
: 'action.selected'
|
||||
: 'transparent',
|
||||
border: '1px solid',
|
||||
borderColor: isSelected ? 'primary.main' : 'transparent',
|
||||
transition: 'background-color 0.1s, border-color 0.1s',
|
||||
'&:hover': {
|
||||
bgcolor: isSelected
|
||||
? theme.palette.mode === 'dark'
|
||||
? 'primary.dark'
|
||||
: 'action.selected'
|
||||
: 'action.hover',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<VfsFileIcon
|
||||
fileName={file.path}
|
||||
mime={file.fileData.mime}
|
||||
sx={{ mr: 1, flexShrink: 0, fontSize: 18 }}
|
||||
/>
|
||||
|
||||
<Box sx={{ minWidth: 0, flexGrow: 1, pr: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={isSelected ? 600 : 500}
|
||||
noWrap
|
||||
sx={{ fontFamily: 'monospace', fontSize: '0.82rem' }}
|
||||
>
|
||||
{file.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={badge.label}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 17,
|
||||
fontSize: '0.62rem',
|
||||
fontWeight: 600,
|
||||
borderRadius: 1,
|
||||
bgcolor: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)',
|
||||
color: badge.color,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mt: 0.15 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||
{formatVfsSize(file.fileData.size)}
|
||||
</Typography>
|
||||
{file.fileData.updated_at && (
|
||||
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.68rem' }}>
|
||||
{formatVfsDate(file.fileData.updated_at)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Quick action buttons on hover / select */}
|
||||
<Box
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.2,
|
||||
opacity: isSelected ? 1 : 0.7,
|
||||
'&:hover': { opacity: 1 },
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Herunterladen">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => this.props.onDownload(file.path)}
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
<DownloadRoundedIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => this.handleOpenMenu(e, file.fileData)}
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
<MoreVertRoundedIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
renderTree() {
|
||||
const { files, searchQuery } = this.props;
|
||||
const { root, totalCount } = buildFileTree(files, searchQuery);
|
||||
|
||||
if (totalCount === 0) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
py: 6,
|
||||
px: 2,
|
||||
textAlign: 'center',
|
||||
color: 'text.secondary',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<FolderRoundedIcon sx={{ fontSize: 44, opacity: 0.3 }} />
|
||||
<Typography variant="body2">
|
||||
{searchQuery ? `Keine Dateien für "${searchQuery}" gefunden` : 'Keine Dateien im VFS vorhanden'}
|
||||
</Typography>
|
||||
{!searchQuery && (
|
||||
<Typography variant="caption" sx={{ maxWidth: 220 }}>
|
||||
Klicke oben auf Upload oder erstelle eine neue Datei.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const rootFolders = Array.from(root.folders.values()).sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
|
||||
);
|
||||
const rootFiles = [...root.files].sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
||||
{/* Top-level directories FIRST */}
|
||||
{rootFolders.map((folder) => this.renderFolderNode(folder, 0))}
|
||||
|
||||
{/* Top-level files NEXT */}
|
||||
{rootFiles.map((file) => this.renderFileNode(file, 0))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
loading,
|
||||
onRefresh,
|
||||
onOpenNewDialog,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
files,
|
||||
} = this.props;
|
||||
|
||||
const { menuAnchor, menuFile, isDraggingOver } = this.state;
|
||||
const isText = menuFile?.isText;
|
||||
|
||||
return (
|
||||
<Box
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
onDrop={this.handleDrop}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
bgcolor: isDraggingOver ? 'action.selected' : 'inherit',
|
||||
transition: 'background-color 0.15s',
|
||||
}}
|
||||
>
|
||||
{/* Hidden Upload Input */}
|
||||
<input
|
||||
ref={this.fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={this.handleFileInputChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{/* Drag & Drop Overlay */}
|
||||
{isDraggingOver && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 8,
|
||||
border: '2px dashed',
|
||||
borderColor: 'primary.main',
|
||||
borderRadius: 3,
|
||||
bgcolor: 'background.paper',
|
||||
opacity: 0.94,
|
||||
zIndex: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 1,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<CloudUploadRoundedIcon color="primary" sx={{ fontSize: 44 }} />
|
||||
<Typography variant="subtitle1" fontWeight={700} color="primary.main">
|
||||
Dateien im VFS ablegen
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Die Dateien werden sofort auf den Server hochgeladen
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Explorer Header / Actions */}
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1.25,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{/* Top Bar with Action Buttons */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
{loading && <CircularProgress size={16} thickness={4} />}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, ml: 'auto' }}>
|
||||
<Tooltip title="Aktualisieren">
|
||||
<span>
|
||||
<IconButton size="small" onClick={onRefresh} disabled={loading} sx={{ p: 0.6 }}>
|
||||
<RefreshRoundedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Datei(en) hochladen">
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<CloudUploadRoundedIcon />}
|
||||
onClick={this.handleTriggerUpload}
|
||||
sx={{ borderRadius: 2, py: 0.3, px: 1, fontSize: '0.75rem' }}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Neue Datei anlegen">
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<NoteAddRoundedIcon />}
|
||||
onClick={onOpenNewDialog}
|
||||
sx={{ borderRadius: 2, py: 0.3, px: 1, fontSize: '0.75rem' }}
|
||||
>
|
||||
Neu
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Search Filter Bar */}
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
px: 1,
|
||||
py: 0.25,
|
||||
borderRadius: 2,
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
|
||||
border: '1px solid transparent',
|
||||
'&:focus-within': {
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.09)' : '#fff',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<SearchRoundedIcon sx={{ fontSize: 18, color: 'text.secondary', mr: 0.75 }} />
|
||||
<InputBase
|
||||
fullWidth
|
||||
placeholder="VFS durchsuchen…"
|
||||
value={searchQuery || ''}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
sx={{ fontSize: '0.82rem' }}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<IconButton size="small" onClick={() => onSearchChange('')} sx={{ p: 0.2 }}>
|
||||
<ClearRoundedIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Scrollable File List */}
|
||||
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 1.5 }}>
|
||||
{this.renderTree()}
|
||||
</Box>
|
||||
|
||||
{/* Context Menu */}
|
||||
<Menu
|
||||
anchorEl={menuAnchor}
|
||||
open={Boolean(menuAnchor)}
|
||||
onClose={this.handleCloseMenu}
|
||||
PaperProps={{ sx: { borderRadius: 2, minWidth: 180 } }}
|
||||
>
|
||||
{isText && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (menuFile) this.props.onSelectFile(menuFile.name);
|
||||
this.handleCloseMenu();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<EditRoundedIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Im Editor bearbeiten" />
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{!isText && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (menuFile) this.props.onSelectFile(menuFile.name);
|
||||
this.handleCloseMenu();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<VisibilityRoundedIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Vorschau" />
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (menuFile) this.props.onDownload(menuFile.name);
|
||||
this.handleCloseMenu();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<DownloadRoundedIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Herunterladen" />
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={() => this.handleCopyPath(menuFile)}>
|
||||
<ListItemIcon>
|
||||
<ContentCopyRoundedIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Pfad kopieren" />
|
||||
</MenuItem>
|
||||
|
||||
{this.props.onMentionInChat && (
|
||||
<MenuItem onClick={() => this.handleMentionInChat(menuFile)}>
|
||||
<ListItemIcon>
|
||||
<ChatBubbleOutlineRoundedIcon fontSize="small" color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="In Chat erwähnen" />
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (menuFile && window.confirm(`Möchtest du "${menuFile.name}" wirklich aus dem VFS löschen?`)) {
|
||||
this.props.onDelete(menuFile.name);
|
||||
}
|
||||
this.handleCloseMenu();
|
||||
}}
|
||||
sx={{ color: 'error.main' }}
|
||||
>
|
||||
<ListItemIcon sx={{ color: 'error.main' }}>
|
||||
<DeleteOutlineRoundedIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Datei löschen" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user