File-Explorer: Editor editierbar + Speichern-Button
This commit is contained in:
1 parent
f74409dd89
commit
b84dff6d8c
3 files changed
+157
-8
No files matched your search
@@ -38,6 +38,7 @@ public static class WorkspaceEndpoint
|
||||
{
|
||||
app.MapGet("/api/workspace/tree", HandleTree);
|
||||
app.MapGet("/api/workspace/file", HandleFile);
|
||||
app.MapPut("/api/workspace/file", HandleFileSave);
|
||||
app.MapGet("/api/workspace/browse", HandleBrowse);
|
||||
}
|
||||
|
||||
@@ -158,6 +159,58 @@ public static class WorkspaceEndpoint
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleFileSave(HttpContext ctx)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||
var session = await db.GetSession(sessionId ?? "");
|
||||
if (session == null || string.IsNullOrEmpty(session.Workspace))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
||||
return;
|
||||
}
|
||||
|
||||
var basePath = Path.GetFullPath(session.Workspace);
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(ctx.Request.Body);
|
||||
var content = await reader.ReadToEndAsync();
|
||||
|
||||
await File.WriteAllTextAsync(targetPath, content);
|
||||
|
||||
var info = new FileInfo(targetPath);
|
||||
var lineCount = content.Split('\n').Length;
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
path = filePath,
|
||||
name = info.Name,
|
||||
size = info.Length,
|
||||
lineCount
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleBrowse(HttpContext ctx)
|
||||
{
|
||||
var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/";
|
||||
|
||||
@@ -116,6 +116,15 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fe-cm-wrap > div {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
/* kein overflow:hidden – CodeMirror positioniert sein internes textarea
|
||||
mit negativem Offset; overflow:hidden würde es clippen → kein Input */
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fe-mde-wrap .EasyMDEContainer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -261,3 +270,19 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fe-footer-actions {
|
||||
display: flex;
|
||||
gap: .5em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fe-save-btn:not(:disabled) {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.fe-save-btn:not(:disabled):hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ function openFileExplorer(opts = {}) {
|
||||
</div>
|
||||
<div class="fe-footer" id="feFooter" hidden>
|
||||
<span class="fe-info" id="feInfo"></span>
|
||||
<button class="fe-btn fe-select-btn" id="feSelectBtn">Einfügen</button>
|
||||
<div class="fe-footer-actions">
|
||||
<button class="fe-btn fe-save-btn" id="feSaveBtn" disabled hidden>Speichern</button>
|
||||
<button class="fe-btn fe-select-btn" id="feSelectBtn">Einfügen</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -51,6 +54,7 @@ function openFileExplorer(opts = {}) {
|
||||
document.getElementById('feBack').addEventListener('click', feGoBack);
|
||||
document.getElementById('feClose').addEventListener('click', closeFileExplorer);
|
||||
document.getElementById('feSelectBtn').addEventListener('click', feDoSelect);
|
||||
document.getElementById('feSaveBtn').addEventListener('click', feDoSave);
|
||||
|
||||
// ESC
|
||||
_feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); };
|
||||
@@ -220,6 +224,12 @@ async function feSelectFile(entry, itemEl) {
|
||||
}
|
||||
|
||||
info.textContent = `${data.path} — ${data.lineCount} Zeilen, ${formatSize(data.size)}`;
|
||||
|
||||
const saveBtn = document.getElementById('feSaveBtn');
|
||||
const selectBtn = document.getElementById('feSelectBtn');
|
||||
saveBtn.hidden = false;
|
||||
saveBtn.disabled = true;
|
||||
selectBtn.hidden = !_feState.onSelect;
|
||||
footer.hidden = false;
|
||||
|
||||
} catch (e) {
|
||||
@@ -239,22 +249,25 @@ function feDestroyEditor() {
|
||||
}
|
||||
|
||||
function feInitCodeMirror(container, data, ext) {
|
||||
container.innerHTML = '<div class="fe-cm-wrap"><textarea id="feCmTextarea"></textarea></div>';
|
||||
|
||||
const textarea = document.getElementById('feCmTextarea');
|
||||
textarea.value = data.content;
|
||||
container.innerHTML = '<div class="fe-cm-wrap"><div id="feCmHost"></div></div>';
|
||||
|
||||
const host = document.getElementById('feCmHost');
|
||||
const mode = getCmMode(ext);
|
||||
const originalContent = data.content;
|
||||
|
||||
const cm = CodeMirror.fromTextArea(textarea, {
|
||||
const cm = CodeMirror(host, {
|
||||
value: data.content,
|
||||
mode: mode,
|
||||
theme: 'default',
|
||||
lineNumbers: true,
|
||||
readOnly: true,
|
||||
readOnly: false,
|
||||
lineWrapping: false,
|
||||
tabSize: 4,
|
||||
indentWithTabs: false,
|
||||
extraKeys: {}
|
||||
extraKeys: {
|
||||
'Ctrl-S': () => feDoSave(),
|
||||
'Cmd-S': () => feDoSave()
|
||||
}
|
||||
});
|
||||
|
||||
// Selektion-Tracking für Zeilen-Info
|
||||
@@ -263,6 +276,15 @@ function feInitCodeMirror(container, data, ext) {
|
||||
feUpdateSelectionInfo(cm, data);
|
||||
});
|
||||
|
||||
// Änderungs-Tracking
|
||||
cm.on('change', () => {
|
||||
const saveBtn = document.getElementById('feSaveBtn');
|
||||
if (saveBtn) saveBtn.disabled = cm.getValue() === originalContent;
|
||||
});
|
||||
|
||||
// Refresh nötig wenn CM in einem Container ohne finale Größe initialisiert wird
|
||||
requestAnimationFrame(() => cm.refresh());
|
||||
|
||||
_feState._editor = cm;
|
||||
_feState._editorType = 'codemirror';
|
||||
}
|
||||
@@ -374,6 +396,55 @@ function feDoSelect() {
|
||||
closeFileExplorer();
|
||||
}
|
||||
|
||||
async function feDoSave() {
|
||||
if (!_feState || !_feState.selectedFile) return;
|
||||
|
||||
const cm = feGetCm();
|
||||
if (!cm) return;
|
||||
|
||||
const saveBtn = document.getElementById('feSaveBtn');
|
||||
if (saveBtn && saveBtn.disabled) return;
|
||||
|
||||
const content = cm.getValue();
|
||||
const path = _feState.selectedFile.path;
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Speichern...';
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/api/workspace/file?sessionId=${_feState.sessionId}&path=${encodeURIComponent(path)}`,
|
||||
{ method: 'PUT', body: content }
|
||||
);
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
alert('Fehler beim Speichern: ' + (err.error || resp.status));
|
||||
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; }
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await resp.json();
|
||||
|
||||
// State aktualisieren
|
||||
_feState.selectedFile.content = content;
|
||||
_feState.selectedFile.size = result.size;
|
||||
_feState.selectedFile.lineCount = result.lineCount;
|
||||
|
||||
const info = document.getElementById('feInfo');
|
||||
if (info) info.textContent = `${path} — ${result.lineCount} Zeilen, ${formatSize(result.size)}`;
|
||||
|
||||
if (saveBtn) { saveBtn.textContent = 'Speichern'; }
|
||||
// bleibt disabled, da gespeicherter Stand == aktueller Stand
|
||||
|
||||
} catch (e) {
|
||||
alert('Netzwerk-Fehler beim Speichern');
|
||||
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Folder Browser Mode ──
|
||||
// Nutzt /api/workspace/browse mit absolutem Pfad, zum Ordner-Auswählen.
|
||||
|
||||
|
||||
Reference in new issue
Block a user