u
This commit is contained in:
@@ -680,7 +680,7 @@ export const setting = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Content-addressed thumb AVIF by fullHash (SHA-256 hex of full image bytes).
|
||||
* Content-addressed thumb AVIF by fullHash (8-char alphanumeric, see dashback pic.js).
|
||||
* Auth: session cookie. Browser-cacheable (private, immutable, ETag).
|
||||
*/
|
||||
export const thumb = {
|
||||
@@ -2021,6 +2021,34 @@ export const vfs = {
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/vfs/mkdir
|
||||
* Create a directory in VFS.
|
||||
* @param {string} path
|
||||
*/
|
||||
mkdir(path) {
|
||||
const clean = String(path || '').replace(/^\/+/, '');
|
||||
return http('/api/vfs/mkdir', {
|
||||
method: 'POST',
|
||||
body: { path: clean },
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/vfs/rename
|
||||
* Rename or move a file or directory in VFS.
|
||||
* @param {string} oldPath
|
||||
* @param {string} newPath
|
||||
*/
|
||||
rename(oldPath, newPath) {
|
||||
const cleanOld = String(oldPath || '').replace(/^\/+/, '');
|
||||
const cleanNew = String(newPath || '').replace(/^\/+/, '');
|
||||
return http('/api/vfs/rename', {
|
||||
method: 'POST',
|
||||
body: { oldPath: cleanOld, newPath: cleanNew },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -67,6 +67,7 @@ const initialState = {
|
||||
searchQuery: '',
|
||||
vfsWidth: 480,
|
||||
newFileDialogOpen: false,
|
||||
newFileDefaultFolder: '',
|
||||
};
|
||||
|
||||
let vfsAutomat;
|
||||
@@ -105,8 +106,8 @@ const actions = {
|
||||
vfsAutomat.setState({ searchQuery });
|
||||
},
|
||||
|
||||
setNewFileDialogOpen(newFileDialogOpen) {
|
||||
vfsAutomat.setState({ newFileDialogOpen });
|
||||
setNewFileDialogOpen(newFileDialogOpen, newFileDefaultFolder = '') {
|
||||
vfsAutomat.setState({ newFileDialogOpen, newFileDefaultFolder });
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -283,6 +284,54 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a file or directory from VFS.
|
||||
* @param {string} path
|
||||
*/
|
||||
async deletePath(path) {
|
||||
return actions.deleteFile(path);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new directory in VFS.
|
||||
* @param {string} path
|
||||
*/
|
||||
async mkdir(path) {
|
||||
const clean = String(path || '').trim().replace(/^[/\\]+/, '');
|
||||
if (!clean) throw new Error('Pfad darf nicht leer sein');
|
||||
try {
|
||||
const res = await vfsApi.mkdir(clean);
|
||||
await actions.loadFiles(true);
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error('[vfsAutomat] mkdir failed:', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Rename or move a file or directory in VFS.
|
||||
* @param {string} oldPath
|
||||
* @param {string} newPath
|
||||
*/
|
||||
async renameFile(oldPath, newPath) {
|
||||
const cleanOld = String(oldPath || '').trim().replace(/^[/\\]+/, '');
|
||||
const cleanNew = String(newPath || '').trim().replace(/^[/\\]+/, '');
|
||||
if (!cleanOld || !cleanNew) throw new Error('Alter und neuer Pfad dürfen nicht leer sein');
|
||||
try {
|
||||
const res = await vfsApi.rename(cleanOld, cleanNew);
|
||||
const isCurrentSelected = vfsAutomat.state.selectedFileName === cleanOld;
|
||||
await actions.loadFiles(true);
|
||||
if (isCurrentSelected) {
|
||||
await actions.selectFile(cleanNew);
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error('[vfsAutomat] renameFile failed:', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload one or more files to VFS.
|
||||
* @param {File[]|FileList|File} files
|
||||
|
||||
@@ -26,6 +26,9 @@ import DirectionsWalkRoundedIcon from '@mui/icons-material/DirectionsWalkRounded
|
||||
import DirectionsRunRoundedIcon from '@mui/icons-material/DirectionsRunRounded';
|
||||
import PsychologyRoundedIcon from '@mui/icons-material/PsychologyRounded';
|
||||
import FolderOpenRoundedIcon from '@mui/icons-material/FolderOpenRounded';
|
||||
import DarkModeRoundedIcon from '@mui/icons-material/DarkModeRounded';
|
||||
import LightModeRoundedIcon from '@mui/icons-material/LightModeRounded';
|
||||
import CycloneRoundedIcon from '@mui/icons-material/CycloneRounded';
|
||||
|
||||
import { setting as settingApi } from '../../api';
|
||||
import chatAutomat, { selectPendingAskUser } from '../../automats/chatAutomat';
|
||||
@@ -627,6 +630,17 @@ export default class ChatDrawer extends PureComponent {
|
||||
this.setState({ modelreasoning: current });
|
||||
}
|
||||
};
|
||||
handleSetModelslug = async (nextSlug) => {
|
||||
if (this.state.modelslug === nextSlug) return;
|
||||
const current = this.state.modelslug;
|
||||
this.setState({ modelslug: nextSlug });
|
||||
try {
|
||||
await settingApi.upsert('modelslug', { value: nextSlug });
|
||||
} catch (err) {
|
||||
console.error('[ChatDrawer] Failed to save modelslug:', err);
|
||||
this.setState({ modelslug: current });
|
||||
}
|
||||
};
|
||||
|
||||
handleModelslugChange = (e) => {
|
||||
this.setState({ modelslug: e.target.value });
|
||||
@@ -871,6 +885,79 @@ export default class ChatDrawer extends PureComponent {
|
||||
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 0.3, my: 0.5 }} />
|
||||
|
||||
{/* Group 3: Model preset shortcuts (moon -> luna, sun -> sol, universe -> astra) */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.3,
|
||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)'),
|
||||
p: 0.3,
|
||||
borderRadius: 2,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{/* moon toggle (openai/gpt-6-luna) */}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => this.handleSetModelslug('openai/gpt-6-luna')}
|
||||
aria-label="Luna (openai/gpt-6-luna)"
|
||||
sx={{
|
||||
p: 0.3,
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: modelslug === 'openai/gpt-6-luna' ? 'primary.main' : 'transparent',
|
||||
color: modelslug === 'openai/gpt-6-luna' ? 'primary.contrastText' : 'text.secondary',
|
||||
'&:hover': {
|
||||
bgcolor: modelslug === 'openai/gpt-6-luna' ? 'primary.dark' : 'action.selected',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DarkModeRoundedIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
|
||||
{/* sun toggle (openai/gpt-6.1-sol) */}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => this.handleSetModelslug('openai/gpt-6.1-sol')}
|
||||
aria-label="Sol (openai/gpt-6.1-sol)"
|
||||
sx={{
|
||||
p: 0.3,
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: modelslug === 'openai/gpt-6.1-sol' ? 'primary.main' : 'transparent',
|
||||
color: modelslug === 'openai/gpt-6.1-sol' ? 'primary.contrastText' : 'text.secondary',
|
||||
'&:hover': {
|
||||
bgcolor: modelslug === 'openai/gpt-6.1-sol' ? 'primary.dark' : 'action.selected',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LightModeRoundedIcon sx={{ fontSize: 17 }} />
|
||||
</IconButton>
|
||||
|
||||
{/* universe toggle (openai/gpt-6-astra) */}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => this.handleSetModelslug('openai/gpt-6-astra')}
|
||||
aria-label="Astra (openai/gpt-6-astra)"
|
||||
sx={{
|
||||
p: 0.3,
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: modelslug === 'openai/gpt-6-astra' ? 'primary.main' : 'transparent',
|
||||
color: modelslug === 'openai/gpt-6-astra' ? 'primary.contrastText' : 'text.secondary',
|
||||
'&:hover': {
|
||||
bgcolor: modelslug === 'openai/gpt-6-astra' ? 'primary.dark' : 'action.selected',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CycloneRoundedIcon sx={{ fontSize: 17 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* modelslug input */}
|
||||
<InputBase
|
||||
value={modelslug || ''}
|
||||
@@ -1167,6 +1254,8 @@ export default class ChatDrawer extends PureComponent {
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={this.handleSend}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
@@ -1444,6 +1533,12 @@ export default class ChatDrawer extends PureComponent {
|
||||
onFocus={() => this.setState({ isInputFocused: true })}
|
||||
onBlur={() => this.setState({ isInputFocused: false })}
|
||||
disabled={!canChat}
|
||||
inputProps={{
|
||||
spellCheck: false,
|
||||
autoCorrect: 'off',
|
||||
autoCapitalize: 'off',
|
||||
autoComplete: 'off',
|
||||
}}
|
||||
placeholder={
|
||||
pendingAsk
|
||||
? 'Bitte Frage oben beantworten…'
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: '' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
105
src/components/vfs/VfsNewFolderDialog.jsx
Normal file
105
src/components/vfs/VfsNewFolderDialog.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
107
src/components/vfs/VfsRenameDialog.jsx
Normal file
107
src/components/vfs/VfsRenameDialog.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user