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/tree", HandleTree);
|
||||||
app.MapGet("/api/workspace/file", HandleFile);
|
app.MapGet("/api/workspace/file", HandleFile);
|
||||||
|
app.MapPut("/api/workspace/file", HandleFileSave);
|
||||||
app.MapGet("/api/workspace/browse", HandleBrowse);
|
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)
|
private static async Task HandleBrowse(HttpContext ctx)
|
||||||
{
|
{
|
||||||
var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/";
|
var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/";
|
||||||
|
|||||||
@@ -116,6 +116,15 @@
|
|||||||
height: 100%;
|
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 {
|
.fe-mde-wrap .EasyMDEContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -261,3 +270,19 @@
|
|||||||
flex: 1;
|
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>
|
||||||
<div class="fe-footer" id="feFooter" hidden>
|
<div class="fe-footer" id="feFooter" hidden>
|
||||||
<span class="fe-info" id="feInfo"></span>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -51,6 +54,7 @@ function openFileExplorer(opts = {}) {
|
|||||||
document.getElementById('feBack').addEventListener('click', feGoBack);
|
document.getElementById('feBack').addEventListener('click', feGoBack);
|
||||||
document.getElementById('feClose').addEventListener('click', closeFileExplorer);
|
document.getElementById('feClose').addEventListener('click', closeFileExplorer);
|
||||||
document.getElementById('feSelectBtn').addEventListener('click', feDoSelect);
|
document.getElementById('feSelectBtn').addEventListener('click', feDoSelect);
|
||||||
|
document.getElementById('feSaveBtn').addEventListener('click', feDoSave);
|
||||||
|
|
||||||
// ESC
|
// ESC
|
||||||
_feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); };
|
_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)}`;
|
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;
|
footer.hidden = false;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -239,22 +249,25 @@ function feDestroyEditor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function feInitCodeMirror(container, data, ext) {
|
function feInitCodeMirror(container, data, ext) {
|
||||||
container.innerHTML = '<div class="fe-cm-wrap"><textarea id="feCmTextarea"></textarea></div>';
|
container.innerHTML = '<div class="fe-cm-wrap"><div id="feCmHost"></div></div>';
|
||||||
|
|
||||||
const textarea = document.getElementById('feCmTextarea');
|
|
||||||
textarea.value = data.content;
|
|
||||||
|
|
||||||
|
const host = document.getElementById('feCmHost');
|
||||||
const mode = getCmMode(ext);
|
const mode = getCmMode(ext);
|
||||||
|
const originalContent = data.content;
|
||||||
|
|
||||||
const cm = CodeMirror.fromTextArea(textarea, {
|
const cm = CodeMirror(host, {
|
||||||
|
value: data.content,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
readOnly: true,
|
readOnly: false,
|
||||||
lineWrapping: false,
|
lineWrapping: false,
|
||||||
tabSize: 4,
|
tabSize: 4,
|
||||||
indentWithTabs: false,
|
indentWithTabs: false,
|
||||||
extraKeys: {}
|
extraKeys: {
|
||||||
|
'Ctrl-S': () => feDoSave(),
|
||||||
|
'Cmd-S': () => feDoSave()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Selektion-Tracking für Zeilen-Info
|
// Selektion-Tracking für Zeilen-Info
|
||||||
@@ -263,6 +276,15 @@ function feInitCodeMirror(container, data, ext) {
|
|||||||
feUpdateSelectionInfo(cm, data);
|
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._editor = cm;
|
||||||
_feState._editorType = 'codemirror';
|
_feState._editorType = 'codemirror';
|
||||||
}
|
}
|
||||||
@@ -374,6 +396,55 @@ function feDoSelect() {
|
|||||||
closeFileExplorer();
|
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 ──
|
// ── Folder Browser Mode ──
|
||||||
// Nutzt /api/workspace/browse mit absolutem Pfad, zum Ordner-Auswählen.
|
// Nutzt /api/workspace/browse mit absolutem Pfad, zum Ordner-Auswählen.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user