From 33a2e529f64f02c3946a3cf35898ba1e87c9fd83 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 13:29:46 +0200 Subject: [PATCH] fix(files): support rename-on-save in URL-addressable modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 9's new save delegation read only the editor content, not the filename input — so typing a new filename and clicking Save silently discarded the rename and wrote to the original path. Matches the legacy save handler's payload.new_path contract: if the user edited the filename, compose new_path = parent/filename and send it. 409 conflict (destination exists) shows an alert and keeps the modal open so the user can adjust. Also exposes rawText in fetchJson return so plain-text server error messages (e.g. "destination already exists") reach the alert call. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/l4d2web/static/js/files-overlay.js | 35 ++++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/l4d2web/l4d2web/static/js/files-overlay.js b/l4d2web/l4d2web/static/js/files-overlay.js index 103d246..8c2cd26 100644 --- a/l4d2web/l4d2web/static/js/files-overlay.js +++ b/l4d2web/l4d2web/static/js/files-overlay.js @@ -89,13 +89,14 @@ options.credentials = "same-origin"; const response = await fetch(url, options); let body = null; + let rawText = ""; try { - const text = await response.text(); - body = text ? JSON.parse(text) : null; + rawText = await response.text(); + body = rawText ? JSON.parse(rawText) : null; } catch (_e) { body = null; } - return { ok: response.ok, status: response.status, body }; + return { ok: response.ok, status: response.status, body, rawText }; } async function postJson(url, payload) { @@ -609,12 +610,34 @@ const content = (window.__filesEditor && window.__filesEditor.getValue) ? window.__filesEditor.getValue() : ta.value; - const r = await postJson(`${baseUrl}/files/save`, { path: relPath, content }); + + // Rename-on-save: if the user edited the filename input, compose the + // new path (sibling rename only — joining parent of relPath with the + // new filename). Send payload.new_path so the server moves and writes + // atomically. Matches the legacy save handler's contract. + const filenameInput = modalContent.querySelector("[data-editor-filename]"); + const editedFilename = filenameInput ? filenameInput.value.trim() : ""; + const originalLeaf = relPath.split("/").pop() || relPath; + const parent = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : ""; + let newPath = null; + if (editedFilename && editedFilename !== originalLeaf) { + newPath = parent ? `${parent}/${editedFilename}` : editedFilename; + } + + const payload = { path: relPath, content }; + if (newPath) payload.new_path = newPath; + + const r = await postJson(`${baseUrl}/files/save`, payload); if (r.ok) { if (typeof window.closeModal === "function") window.closeModal(); - scheduleRefresh(parentOf(relPath)); + scheduleRefresh(parentOf(newPath || relPath)); + } else if (r.status === 409) { + // Conflict (destination already exists) — show error and keep modal + // open so the user can pick a different filename. + alert(r.rawText || `Conflict: destination already exists.`); + return; } else { - alert((r.body && r.body.error) || `Save failed (HTTP ${r.status}).`); + alert((r.body && r.body.error) || r.rawText || `Save failed (HTTP ${r.status}).`); } return; }