feat: Ordner erstellen/löschen im File Explorer

Backend (WorkspaceEndpoint.cs):
- POST /api/workspace/mkdir – Ordner im Session-Workspace erstellen
- DELETE /api/workspace/dir – Leeren Ordner im Session-Workspace löschen
- POST /api/workspace/agent/mkdir – Ordner im Agent-Memory erstellen
- DELETE /api/workspace/agent/dir – Leeren Ordner im Agent-Memory löschen
- POST /api/workspace/browse/mkdir – Ordner im Folder-Browser erstellen
- Sicherheit: Path-Traversal-Schutz, Root nicht löschbar, nicht-leer → 409

Frontend (file-explorer.js):
- 📁+ Button im Header für neuen Ordner (File Explorer + Folder Browser)
- 🗑️ Button bei jedem Ordner (erscheint on hover, nur File Explorer)
- Validierung: ungültige Zeichen werden abgelehnt

CSS:
- base.css: Icon-Button mit zwei Icons (folder + plus)
- file-explorer.css: Ordner-Zeile als Flex (Label + Delete-Button)
This commit is contained in:
Meleki committed 2026-03-05 21:01:09 +01:00
1 parent d1355dcb0d
commit 8e7bda50f9
4 files changed
+335 -2

No files matched your search

+96 -2
View File
@@ -37,6 +37,7 @@ function openFileExplorer(opts = {}) {
<button class="fe-btn icon-btn" id="feBack" title="Zurück" disabled><i class="fa fa-arrow-left"></i></button>
<span class="fe-path" id="fePath">/</span>
</div>
<button class="fe-btn icon-btn" id="feNewDir" title="Neuer Ordner"><i class="fa fa-folder-open"></i> <i class="fa fa-plus"></i></button>
<button class="fe-btn icon-btn" id="feClose"><i class="fa fa-times"></i></button>
</div>
<div class="fe-body">
@@ -60,6 +61,7 @@ function openFileExplorer(opts = {}) {
document.body.appendChild(overlay);
document.getElementById('feBack').addEventListener('click', feGoBack);
document.getElementById('feNewDir').addEventListener('click', feCreateDir);
document.getElementById('feClose').addEventListener('click', closeFileExplorer);
document.getElementById('feSelectBtn').addEventListener('click', feDoSelect);
document.getElementById('feSaveBtn').addEventListener('click', feDoSave);
@@ -138,11 +140,24 @@ async function feLoadDir(path) {
if (entry.type === 'dir') {
item.classList.add('fe-dir');
item.innerHTML = '<i class="fa fa-folder"></i> ' + esc(entry.name);
item.addEventListener('click', () => {
const label = document.createElement('span');
label.className = 'fe-dir-label';
label.innerHTML = '<i class="fa fa-folder"></i> ' + esc(entry.name);
label.addEventListener('click', () => {
_feState.history.push(path);
feLoadDir(entry.path);
});
item.appendChild(label);
const delBtn = document.createElement('button');
delBtn.className = 'fe-btn fe-dir-del';
delBtn.title = 'Ordner löschen (nur wenn leer)';
delBtn.innerHTML = '<i class="fa fa-trash-o"></i>';
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
feDeleteDir(entry.path, entry.name);
});
item.appendChild(delBtn);
} else {
item.classList.add('fe-file');
const icon = entry.isText ? '<i class="fa fa-file-text-o"></i>' : getFileIcon(entry.ext);
@@ -505,6 +520,7 @@ function openFolderBrowser(opts = {}) {
<button class="fe-btn icon-btn" id="feBack" title="Zurück" disabled><i class="fa fa-arrow-left"></i></button>
<span class="fe-path" id="fePath">/</span>
</div>
<button class="fe-btn icon-btn" id="feNewDir" title="Neuer Ordner"><i class="fa fa-folder-open"></i> <i class="fa fa-plus"></i></button>
<button class="fe-btn icon-btn" id="feClose"><i class="fa fa-times"></i></button>
</div>
<div class="fe-body">
@@ -522,6 +538,7 @@ function openFolderBrowser(opts = {}) {
document.body.appendChild(overlay);
document.getElementById('feBack').addEventListener('click', fbGoBack);
document.getElementById('feNewDir').addEventListener('click', fbCreateDir);
document.getElementById('feClose').addEventListener('click', closeFileExplorer);
document.getElementById('feSelectBtn').addEventListener('click', () => {
if (_feState && _feState.onSelect) _feState.onSelect(_feState.currentAbsPath);
@@ -587,6 +604,83 @@ function fbGoBack() {
fbLoadDir(prev);
}
// ── Folder Browser: Create Directory ──
async function fbCreateDir() {
if (!_feState) return;
const name = prompt('Ordnername:');
if (!name || !name.trim()) return;
const dirName = name.trim();
if (/[\/\\:*?"<>|]/.test(dirName)) {
alert('Ungültiger Ordnername');
return;
}
const absPath = _feState.currentAbsPath.replace(/\/$/, '') + '/' + dirName;
try {
const resp = await fetch(`/api/workspace/browse/mkdir?path=${encodeURIComponent(absPath)}`, { method: 'POST' });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
alert(err.error || 'Fehler beim Erstellen');
return;
}
fbLoadDir(_feState.currentAbsPath);
} catch (e) {
alert('Netzwerk-Fehler');
}
}
// ── File Explorer: Create / Delete Directory ──
async function feCreateDir() {
if (!_feState) return;
const name = prompt('Ordnername:');
if (!name || !name.trim()) return;
const dirName = name.trim();
if (/[\/\\:*?"<>|]/.test(dirName)) {
alert('Ungültiger Ordnername');
return;
}
const newPath = _feState.currentPath ? _feState.currentPath + '/' + dirName : dirName;
try {
const url = _feApiUrl('mkdir', `path=${encodeURIComponent(newPath)}`);
const resp = await fetch(url, { method: 'POST' });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
alert(err.error || 'Fehler beim Erstellen');
return;
}
feLoadDir(_feState.currentPath);
} catch (e) {
alert('Netzwerk-Fehler');
}
}
async function feDeleteDir(dirPath, dirName) {
if (!_feState) return;
if (!confirm(`Ordner "${dirName}" löschen?\n(Nur möglich wenn leer)`)) return;
try {
const url = _feApiUrl('dir', `path=${encodeURIComponent(dirPath)}`);
const resp = await fetch(url, { method: 'DELETE' });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
alert(err.error || 'Fehler beim Löschen');
return;
}
feLoadDir(_feState.currentPath);
} catch (e) {
alert('Netzwerk-Fehler');
}
}
// ── Helpers ──
function getFileIcon(ext) {