This commit is contained in:
seb
2026-10-07 21:28:26 +02:00
parent ee5a30bb21
commit d49ec77043
509 changed files with 2969 additions and 924 deletions

View File

@@ -30,6 +30,7 @@ export default class VfsEditorPanel extends PureComponent {
isUploading: s.isUploading,
searchQuery: s.searchQuery,
newFileDialogOpen: s.newFileDialogOpen,
newFileDefaultFolder: s.newFileDefaultFolder,
};
}
@@ -49,6 +50,7 @@ export default class VfsEditorPanel extends PureComponent {
isUploading: state.isUploading,
searchQuery: state.searchQuery,
newFileDialogOpen: state.newFileDialogOpen,
newFileDefaultFolder: state.newFileDefaultFolder,
}));
if (this.state.files.length === 0 && !this.state.loadingFiles) {
@@ -180,10 +182,12 @@ export default class VfsEditorPanel extends PureComponent {
searchQuery={searchQuery}
onSelectFile={(name) => vfsAutomat.actions.selectFile(name)}
onRefresh={() => vfsAutomat.actions.loadFiles()}
onOpenNewDialog={() => vfsAutomat.actions.setNewFileDialogOpen(true)}
onOpenNewDialog={(parentDir) => vfsAutomat.actions.setNewFileDialogOpen(true, parentDir)}
onSearchChange={(q) => vfsAutomat.actions.setSearchQuery(q)}
onDownload={(name) => vfsAutomat.actions.downloadFile(name)}
onDelete={(name) => vfsAutomat.actions.deleteFile(name)}
onDelete={(name) => vfsAutomat.actions.deletePath(name)}
onRename={(oldPath, newPath) => vfsAutomat.actions.renameFile(oldPath, newPath)}
onMkdir={(dirPath) => vfsAutomat.actions.mkdir(dirPath)}
onUpload={(filesToUpload) => vfsAutomat.actions.uploadFiles(filesToUpload)}
onMentionInChat={this.handleMentionInChat}
/>
@@ -194,6 +198,7 @@ export default class VfsEditorPanel extends PureComponent {
{/* New File Dialog */}
<VfsNewFileDialog
open={newFileDialogOpen}
defaultFolder={this.state.newFileDefaultFolder}
onClose={() => vfsAutomat.actions.setNewFileDialogOpen(false)}
onCreate={(name) => vfsAutomat.actions.createFile(name)}
isCreating={isSaving}

View File

@@ -23,9 +23,13 @@ import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownR
import KeyboardArrowRightRoundedIcon from '@mui/icons-material/KeyboardArrowRightRounded';
import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded';
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
import CreateNewFolderRoundedIcon from '@mui/icons-material/CreateNewFolderRounded';
import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded';
import VfsFileIcon from './VfsFileIcon';
import { formatVfsSize, formatVfsDate } from './vfsUtils';
import VfsNewFolderDialog from './VfsNewFolderDialog';
import VfsRenameDialog from './VfsRenameDialog';
/**
* Returns all ancestor directory paths for a file or folder path.
@@ -119,6 +123,14 @@ export default class VfsFileExplorer extends PureComponent {
this.state = {
menuAnchor: null,
menuFile: null,
folderMenuAnchor: null,
folderMenuNode: null,
renameDialogOpen: false,
renameItem: null,
isRenaming: false,
newFolderDialogOpen: false,
targetParentFolder: '',
isCreatingFolder: false,
deleteConfirmFile: null,
isDraggingOver: false,
expandedFolders: initialExpanded,
@@ -168,6 +180,58 @@ export default class VfsFileExplorer extends PureComponent {
this.setState({ menuAnchor: null, menuFile: null });
};
handleOpenFolderMenu = (e, folder) => {
e.stopPropagation();
e.preventDefault();
this.setState({ folderMenuAnchor: e.currentTarget, folderMenuNode: folder });
};
handleCloseFolderMenu = () => {
this.setState({ folderMenuAnchor: null, folderMenuNode: null });
};
handleOpenRename = (item) => {
this.setState({
renameDialogOpen: true,
renameItem: item,
menuAnchor: null,
folderMenuAnchor: null,
folderMenuNode: null,
});
};
handleRenameItem = async (oldPath, newPath) => {
if (!this.props.onRename) return;
this.setState({ isRenaming: true });
try {
await this.props.onRename(oldPath, newPath);
this.setState({ renameDialogOpen: false, renameItem: null, isRenaming: false });
} catch (err) {
this.setState({ isRenaming: false });
alert(err.message || 'Umbenennen fehlgeschlagen');
}
};
handleCreateFolder = async (folderPath) => {
if (!this.props.onMkdir) return;
this.setState({ isCreatingFolder: true });
try {
await this.props.onMkdir(folderPath);
this.setState((prev) => {
const next = new Set(prev.expandedFolders);
next.add(folderPath);
return {
newFolderDialogOpen: false,
isCreatingFolder: false,
expandedFolders: next,
};
});
} catch (err) {
this.setState({ isCreatingFolder: false });
alert(err.message || 'Ordner konnte nicht erstellt werden');
}
};
handleTriggerUpload = () => {
this.fileInputRef.current?.click();
};
@@ -313,6 +377,27 @@ export default class VfsFileExplorer extends PureComponent {
>
{folder.totalFileCount}
</Typography>
{/* Folder menu trigger */}
<Box
onClick={(e) => e.stopPropagation()}
sx={{
display: 'flex',
alignItems: 'center',
ml: 0.5,
opacity: 0.7,
'&:hover': { opacity: 1 },
}}
>
<IconButton
size="small"
onClick={(e) => this.handleOpenFolderMenu(e, folder)}
aria-label="Ordner-Optionen"
sx={{ p: 0.4 }}
>
<MoreVertRoundedIcon sx={{ fontSize: 16 }} />
</IconButton>
</Box>
</Box>
{/* Folder Children (Directories FIRST, then files) */}
@@ -471,7 +556,19 @@ export default class VfsFileExplorer extends PureComponent {
onSearchChange,
} = this.props;
const { menuAnchor, menuFile, isDraggingOver } = this.state;
const {
menuAnchor,
menuFile,
folderMenuAnchor,
folderMenuNode,
newFolderDialogOpen,
targetParentFolder,
isCreatingFolder,
renameDialogOpen,
renameItem,
isRenaming,
isDraggingOver,
} = this.state;
const isText = menuFile?.isText;
return (
@@ -590,10 +687,20 @@ export default class VfsFileExplorer extends PureComponent {
<CloudUploadRoundedIcon fontSize="small" />
</IconButton>
{/* New Folder Icon Button */}
<IconButton
size="small"
onClick={() => this.setState({ newFolderDialogOpen: true, targetParentFolder: '' })}
aria-label="Neuen Ordner anlegen"
sx={{ p: 0.75 }}
>
<CreateNewFolderRoundedIcon fontSize="small" />
</IconButton>
{/* New File Icon Button (no word) */}
<IconButton
size="small"
onClick={onOpenNewDialog}
onClick={() => onOpenNewDialog?.('')}
aria-label="Neue Datei anlegen"
sx={{ p: 0.75 }}
>
@@ -661,6 +768,25 @@ export default class VfsFileExplorer extends PureComponent {
<ListItemText primary="Pfad kopieren" />
</MenuItem>
<MenuItem
onClick={() => {
const file = menuFile;
this.handleCloseMenu();
if (file) {
this.handleOpenRename({
name: file.name.split('/').pop(),
path: file.name,
isDirectory: false,
});
}
}}
>
<ListItemIcon>
<DriveFileRenameOutlineRoundedIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Umbenennen" />
</MenuItem>
{this.props.onMentionInChat && (
<MenuItem onClick={() => this.handleMentionInChat(menuFile)}>
<ListItemIcon>
@@ -685,6 +811,99 @@ export default class VfsFileExplorer extends PureComponent {
<ListItemText primary="Datei löschen" />
</MenuItem>
</Menu>
{/* Folder Context Menu */}
<Menu
anchorEl={folderMenuAnchor}
open={Boolean(folderMenuAnchor)}
onClose={this.handleCloseFolderMenu}
sx={{ zIndex: (theme) => theme.zIndex.modal + 20 }}
PaperProps={{ sx: { borderRadius: 2, minWidth: 190 } }}
>
<MenuItem
onClick={() => {
const folder = folderMenuNode;
this.handleCloseFolderMenu();
this.setState({
newFolderDialogOpen: true,
targetParentFolder: folder ? folder.path : '',
});
}}
>
<ListItemIcon>
<CreateNewFolderRoundedIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Neuer Unterordner" />
</MenuItem>
<MenuItem
onClick={() => {
const folder = folderMenuNode;
this.handleCloseFolderMenu();
if (this.props.onOpenNewDialog) {
this.props.onOpenNewDialog(folder ? folder.path : '');
}
}}
>
<ListItemIcon>
<NoteAddRoundedIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Neue Datei im Ordner" />
</MenuItem>
<MenuItem
onClick={() => {
const folder = folderMenuNode;
this.handleCloseFolderMenu();
if (folder) {
this.handleOpenRename({
name: folder.name,
path: folder.path,
isDirectory: true,
});
}
}}
>
<ListItemIcon>
<DriveFileRenameOutlineRoundedIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Ordner umbenennen" />
</MenuItem>
<MenuItem
onClick={() => {
const folder = folderMenuNode;
this.handleCloseFolderMenu();
if (folder && window.confirm(`Möchtest du den Ordner "${folder.path}" und alle enthaltenen Dateien wirklich löschen?`)) {
this.props.onDelete(folder.path);
}
}}
sx={{ color: 'error.main' }}
>
<ListItemIcon sx={{ color: 'error.main' }}>
<DeleteOutlineRoundedIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Ordner löschen" />
</MenuItem>
</Menu>
{/* New Folder Dialog */}
<VfsNewFolderDialog
open={newFolderDialogOpen}
parentFolder={targetParentFolder}
onClose={() => this.setState({ newFolderDialogOpen: false })}
onCreate={this.handleCreateFolder}
isCreating={isCreatingFolder}
/>
{/* Rename Dialog */}
<VfsRenameDialog
open={renameDialogOpen}
item={renameItem}
onClose={() => this.setState({ renameDialogOpen: false, renameItem: null })}
onRename={this.handleRenameItem}
isRenaming={isRenaming}
/>
</Box>
);
}

View File

@@ -11,15 +11,17 @@ import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
export default class VfsNewFileDialog extends PureComponent {
constructor(props) {
super(props);
const folder = props.defaultFolder ? `${String(props.defaultFolder).replace(/^\/+/, '').replace(/\/+$/, '')}/` : '';
this.state = {
filename: '',
filename: folder,
error: '',
};
}
componentDidUpdate(prevProps) {
if (!prevProps.open && this.props.open) {
this.setState({ filename: '', error: '' });
const folder = this.props.defaultFolder ? `${String(this.props.defaultFolder).replace(/^\/+/, '').replace(/\/+$/, '')}/` : '';
this.setState({ filename: folder, error: '' });
}
}

View File

@@ -0,0 +1,105 @@
import React, { PureComponent } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import CreateNewFolderRoundedIcon from '@mui/icons-material/CreateNewFolderRounded';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
export default class VfsNewFolderDialog extends PureComponent {
constructor(props) {
super(props);
this.state = {
folderName: '',
error: '',
};
}
componentDidUpdate(prevProps) {
if (!prevProps.open && this.props.open) {
this.setState({ folderName: '', error: '' });
}
}
handleSubmit = (e) => {
e?.preventDefault();
const raw = this.state.folderName.trim();
if (!raw) {
this.setState({ error: 'Bitte gib einen Ordnernamen ein.' });
return;
}
if (raw.includes('..') || raw.includes('\\')) {
this.setState({ error: 'Ungültige Zeichen im Ordnernamen.' });
return;
}
const clean = raw.replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
const parent = String(this.props.parentFolder || '').trim().replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
const fullPath = parent ? `${parent}/${clean}` : clean;
this.props.onCreate?.(fullPath);
};
render() {
const { open, onClose, isCreating, parentFolder } = this.props;
const { folderName, error } = this.state;
const cleanParent = String(parentFolder || '').trim().replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="xs"
fullWidth
sx={{ zIndex: (theme) => theme.zIndex.modal + 20 }}
PaperProps={{
sx: { borderRadius: 3, p: 1 },
}}
>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1.2, pb: 1 }}>
<CreateNewFolderRoundedIcon color="primary" />
<Typography variant="h6" fontWeight={700}>
Neuen Ordner erstellen
</Typography>
</DialogTitle>
<form onSubmit={this.handleSubmit} noValidate autoComplete="off">
<DialogContent sx={{ pt: 1 }}>
{cleanParent && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1, fontFamily: 'monospace' }}>
In: {cleanParent}/
</Typography>
)}
<SingleLineTextEdit
autoFocus
fullWidth
size="small"
label=""
value={folderName}
onChange={(nextVal) => this.setState({ folderName: nextVal, error: '' })}
error={Boolean(error)}
helperText={error}
disabled={isCreating}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2, pt: 1 }}>
<Button onClick={onClose} disabled={isCreating} color="inherit">
Abbrechen
</Button>
<Button
type="submit"
variant="contained"
disabled={isCreating || !folderName.trim()}
sx={{ borderRadius: 2 }}
>
{isCreating ? 'Erstelle…' : 'Erstellen'}
</Button>
</DialogActions>
</form>
</Dialog>
);
}
}

View File

@@ -0,0 +1,107 @@
import React, { PureComponent } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
export default class VfsRenameDialog extends PureComponent {
constructor(props) {
super(props);
this.state = {
targetPath: props.item?.path || '',
error: '',
};
}
componentDidUpdate(prevProps) {
if (!prevProps.open && this.props.open) {
this.setState({
targetPath: this.props.item?.path || '',
error: '',
});
}
}
handleSubmit = (e) => {
e?.preventDefault();
const raw = this.state.targetPath.trim();
if (!raw) {
this.setState({ error: 'Bitte gib einen neuen Namen bzw. Pfad ein.' });
return;
}
if (raw.includes('..') || raw.includes('\\')) {
this.setState({ error: 'Ungültige Zeichen im Pfad.' });
return;
}
const cleanNew = raw.replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
const cleanOld = String(this.props.item?.path || '').replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
if (cleanNew === cleanOld) {
this.props.onClose?.();
return;
}
this.props.onRename?.(cleanOld, cleanNew);
};
render() {
const { open, onClose, isRenaming, item } = this.props;
const { targetPath, error } = this.state;
const isDir = Boolean(item?.isDirectory);
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="xs"
fullWidth
sx={{ zIndex: (theme) => theme.zIndex.modal + 20 }}
PaperProps={{
sx: { borderRadius: 3, p: 1 },
}}
>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1.2, pb: 1 }}>
<DriveFileRenameOutlineRoundedIcon color="primary" />
<Typography variant="h6" fontWeight={700}>
{isDir ? 'Ordner umbenennen' : 'Datei umbenennen'}
</Typography>
</DialogTitle>
<form onSubmit={this.handleSubmit} noValidate autoComplete="off">
<DialogContent sx={{ pt: 1 }}>
<SingleLineTextEdit
autoFocus
fullWidth
size="small"
label=""
value={targetPath}
onChange={(nextVal) => this.setState({ targetPath: nextVal, error: '' })}
error={Boolean(error)}
helperText={error}
disabled={isRenaming}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2, pt: 1 }}>
<Button onClick={onClose} disabled={isRenaming} color="inherit">
Abbrechen
</Button>
<Button
type="submit"
variant="contained"
disabled={isRenaming || !targetPath.trim()}
sx={{ borderRadius: 2 }}
>
{isRenaming ? 'Speichere…' : 'Umbenennen'}
</Button>
</DialogActions>
</form>
</Dialog>
);
}
}