From 10f93b863bdb15eb79eb10de7b4b3e0bcbb4952a Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 16:20:27 +0200 Subject: [PATCH] feat(files): delete legacy editor dialog + gut editor.js legacy paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 9/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md. The inline block in overlay_detail.html is gone. All editor flows (text edit, binary replace, create new) now exclusively use the URL-addressable modal swapped into #modal-content. editor.js is now single-purpose (URL-addressable only). Removed: * editorDialog reference and the editorEls DOM-ref struct * The legacy editor state object * CM6 bridge wrappers + UI helpers (getEditorValue/setEditorValue/ setEditorTitle/updateByteCount/updateRenameHint/updateSaveEnabled/ setQueuedReplacement) — they only ever drove the legacy dialog * withCollisionSuffix (uploads.js still has its copy for the upload-conflict path; editor.js no longer needs it since the URL-addressable conflict path is "alert + keep modal open" rather than overwrite/keep-both) * openEditorTextNew and openEditorForFile — both functions were already unreachable from a user action after Steps 6/8 * inLegacyEditor predicate * Direct-bound listeners on editorEls.filename / contentBox / editorDialog (input, keydown for Ctrl+S, close) * Legacy branches in every delegated handler (dragover, dragleave, drop, change, click) * legacySaveClicked and legacyDeleteClicked What stays: * Routed state (routedReplacement, isRoutedBinaryMode, setRoutedReplacement, updateRoutedBinarySaveEnabled) * Delegated dragover/dragleave/drop/change/click handlers — now single-path each, no legacy/routed branching * Filename input delegated listener for routed binary mode (so rename-only Replace stays reachable) * modal-container close listener that clears routedReplacement * routedSaveClicked (text edit + is_new), routedReplaceClicked (binary, with rename-or-replace fork), routedDeleteClicked * "new-file" and "edit" registered handlers (the "edit" handler is no longer split editable/binary — the server picks the template branch) routedDeleteClicked gained one capability that was missing in Step 8: it now reads rel-path from either the textarea (text mode) OR the .files-editor-binary panel, so deletion works for binary files in the URL-addressable modal too (previously routed-mode binary delete fell through to legacy, which is now gone). Test added: test_overlay_detail_no_longer_renders_legacy_editor_dialog asserts the legacy dialog markup is absent from /overlays/ while the other inline dialogs are still present (Step 3 didn't move them). Numbers: editor.js: 660 → 309 lines (-351). Plan estimated ~200; actual is ~50% larger due to module-header comments + the rename-or-replace fork in routedReplaceClicked. Pure-routing-only and single-mode per click, which was the structural goal. Total across files-overlay/: 1432 → 1191 lines. pytest: 579 → 580 passed, 1 skipped, 3 deselected. Verified live on /overlays/2 in Chromium: * id="files-editor-modal" not in DOM; new-folder/delete/conflict dialogs still present * "+ new file" → routed modal, Create button * Click other.cfg (editable) → routed modal, Save button, content pre-filled * Click test.png (binary) → routed modal, Replace button, initially disabled * No console errors Co-Authored-By: Claude Opus 4.7 (1M context) --- .../l4d2web/static/js/files-overlay/editor.js | 573 +++--------------- l4d2web/l4d2web/templates/overlay_detail.html | 62 -- l4d2web/tests/test_url_addressable_modals.py | 14 + 3 files changed, 89 insertions(+), 560 deletions(-) diff --git a/l4d2web/l4d2web/static/js/files-overlay/editor.js b/l4d2web/l4d2web/static/js/files-overlay/editor.js index c965183..03a3384 100644 --- a/l4d2web/l4d2web/static/js/files-overlay/editor.js +++ b/l4d2web/l4d2web/static/js/files-overlay/editor.js @@ -1,28 +1,29 @@ -// files-overlay/editor.js — Phase A, Step 2. +// files-overlay/editor.js — URL-addressable editor only (post-Step 9). // -// Owns the editor flows during Phase A. Dual-purpose: drives both the -// legacy inline #files-editor-modal (binary-replace + create- -// new-file) and the URL-addressable modal swapped into #modal-content -// (editable text files). Phase B migrates the legacy flows to URL- -// addressable too and removes the legacy branches here. +// After Phase B Step 9 deleted the legacy , +// this module is single-purpose: it drives save / delete / replace / +// new-file flows through the URL-addressable modal swapped into +// #modal-content. The server template overlay_file_editor.html picks +// one of three modes based on the route + the target file: +// * is_new (GET /overlays//files/new?at=) +// * text edit (GET /overlays//files/edit?path=, editable) +// * binary-replace (GET /overlays//files/edit?path=, !editable) // -// Action-registry dispatch (registered into __filesOverlay): -// * "new-file" → openEditorTextNew(folder) (legacy dialog) -// * "edit" → URL-addressable modal for editable files; -// openEditorForFile(path, false) for binary. +// Dispatch (registered into __filesOverlay): +// * "new-file" → opens the new-file route via window.modals.openRouted +// * "edit" → opens the edit route via window.modals.openRouted; +// the server picks the template branch // -// Save / delete: a single document-level click listener handles both -// the legacy dialog and the URL-addressable modal, discriminated by -// which ancestor contains the click target. +// Click delegation on .files-editor-save branches on which panel is in +// #modal-content — text textarea → routedSaveClicked, binary panel → +// routedReplaceClicked. Delete and replace-zone clicks have a single +// path each. // -// Direct-bound (per plan, escape hatch): filename `input`, content -// textarea `input` + `keydown` — high-frequency events on persistent -// inputs inside the persistent legacy dialog. -// -// Cross-module deps consumed via window.__filesOverlay (set by -// core.js): manager, overlayId, baseUrl, csrfToken, helpers, plus -// askConflict (set by the legacy files-overlay.js, used here for save -// 409-conflict handling). +// All listeners are document-level delegated. #modal-content is +// swapped on every modal open, so direct binding wouldn't survive a +// re-open. Module-scope routedReplacement holds the queued File for +// binary-replace mode; cleared on modal-container close so reopening +// starts fresh. (function () { "use strict"; @@ -32,239 +33,18 @@ const { overlayId, baseUrl, csrfToken } = fo; const { - joinPath, parentOf, basename, humanSize, - fetchJson, postJson, postForm, scheduleRefresh, + parentOf, humanSize, + postJson, postForm, scheduleRefresh, } = fo.helpers; - // Legacy inline dialog — present in Phase A. Phase B deletes it; the - // legacy-branch handlers below short-circuit on its absence. - const editorDialog = document.getElementById("files-editor-modal"); + // ---------- routed binary-replace state ---------- - // ---------- legacy editor state + DOM refs ---------- + let routedReplacement = null; - const editor = { - mode: null, // "text" | "binary" - creating: false, - originalPath: null, - folder: null, - queuedReplacement: null, // File object - }; - - const editorEls = editorDialog ? { - title: editorDialog.querySelector(".files-editor-title-text"), - filename: editorDialog.querySelector(".files-editor-filename"), - renameHint: editorDialog.querySelector(".files-editor-rename-hint"), - renameFrom: editorDialog.querySelector(".files-rename-from"), - renameTo: editorDialog.querySelector(".files-rename-to"), - textPanel: editorDialog.querySelector(".files-editor-text"), - contentBox: editorDialog.querySelector(".files-editor-content"), - byteCount: editorDialog.querySelector(".files-editor-byte-count"), - binaryPanel: editorDialog.querySelector(".files-editor-binary"), - binarySize: editorDialog.querySelector(".files-editor-binary-size"), - replaceZone: editorDialog.querySelector(".files-editor-replace-zone"), - replaceIdle: editorDialog.querySelector(".files-editor-replace-idle"), - replaceQueued: editorDialog.querySelector(".files-editor-replace-queued"), - replaceName: editorDialog.querySelector(".files-editor-replace-name"), - replaceSize: editorDialog.querySelector(".files-editor-replace-size"), - replaceClear: editorDialog.querySelector(".files-editor-replace-clear"), - replaceBrowse: editorDialog.querySelector(".files-editor-replace-browse"), - replaceInput: editorDialog.querySelector(".files-editor-replace-input"), - deleteBtn: editorDialog.querySelector(".files-editor-delete"), - downloadBtn: editorDialog.querySelector(".files-editor-download"), - saveBtn: editorDialog.querySelector(".files-editor-save"), - } : null; - - // ---------- CM6 bridge + UI updates (legacy dialog) ---------- - - // Bridge to the CodeMirror 6 controller, set up by static/js/editor.js - // on the .files-editor-content textarea. Falls back to the textarea - // directly if the bundle didn't load (no-JS fallback / file open - // before the controller has been mounted). - function getEditorValue() { - return (window.__filesEditor && window.__filesEditor.getValue) - ? window.__filesEditor.getValue() - : editorEls.contentBox.value; - } - function setEditorValue(text) { - if (window.__filesEditor && window.__filesEditor.setContent) { - window.__filesEditor.setContent(text); - } else { - editorEls.contentBox.value = text; - } - } - - function setEditorTitle(text) { - editorEls.title.textContent = text; - } - - function updateByteCount() { - const bytes = new TextEncoder().encode(getEditorValue()).length; - editorEls.byteCount.textContent = `UTF-8 · ${bytes} bytes`; - } - - function updateRenameHint() { - const current = editorEls.filename.value.trim(); - const original = basename(editor.originalPath || ""); - if (editor.creating || !current || current === original) { - editorEls.renameHint.hidden = true; - return; - } - editorEls.renameFrom.textContent = original; - editorEls.renameTo.textContent = current; - editorEls.renameHint.hidden = false; - } - - function updateSaveEnabled() { - if (editor.mode === "binary" && !editor.creating) { - const filenameChanged = - editorEls.filename.value.trim() !== basename(editor.originalPath || ""); - const hasReplacement = !!editor.queuedReplacement; - editorEls.saveBtn.disabled = !filenameChanged && !hasReplacement; - editorEls.saveBtn.textContent = "Save"; - } else if (editor.creating) { - editorEls.saveBtn.disabled = !editorEls.filename.value.trim(); - editorEls.saveBtn.textContent = "Create"; - } else { - editorEls.saveBtn.disabled = false; - editorEls.saveBtn.textContent = "Save"; - } - } - - function setQueuedReplacement(file) { - editor.queuedReplacement = file; - if (file) { - editorEls.replaceIdle.hidden = true; - editorEls.replaceQueued.hidden = false; - editorEls.replaceName.textContent = file.name; - editorEls.replaceSize.textContent = humanSize(file.size); - } else { - editorEls.replaceIdle.hidden = false; - editorEls.replaceQueued.hidden = true; - } - updateSaveEnabled(); - } - - // ---------- legacy editor openers ---------- - - function openEditorTextNew(folder) { - if (!editorDialog) return; - editor.mode = "text"; - editor.creating = true; - editor.originalPath = null; - editor.folder = folder; - editor.queuedReplacement = null; - - setEditorTitle(`${folder ? folder + "/" : ""}…new file`); - editorEls.filename.value = ""; - editorEls.filename.disabled = false; - setEditorValue(""); - editorEls.contentBox.disabled = false; - editorEls.renameHint.hidden = true; - editorEls.textPanel.hidden = false; - editorEls.binaryPanel.hidden = true; - editorEls.deleteBtn.hidden = true; - editorEls.downloadBtn.hidden = true; - editorEls.saveBtn.textContent = "Create"; - updateByteCount(); - updateSaveEnabled(); - editorDialog.showModal(); - setTimeout(() => editorEls.filename.focus(), 0); - } - - async function openEditorForFile(path, isEditable) { - if (!editorDialog) return; - editor.creating = false; - editor.originalPath = path; - editor.folder = parentOf(path); - editor.queuedReplacement = null; - setQueuedReplacement(null); - - editorEls.filename.value = basename(path); - editorEls.filename.disabled = false; - editorEls.renameHint.hidden = true; - editorEls.deleteBtn.hidden = false; - editorEls.downloadBtn.hidden = false; - editorEls.downloadBtn.href = `${baseUrl}/files/download?path=${encodeURIComponent(path)}`; - setEditorTitle(path); - - if (isEditable) { - editor.mode = "text"; - editorEls.textPanel.hidden = false; - editorEls.binaryPanel.hidden = true; - setEditorValue("Loading…"); - editorEls.contentBox.disabled = true; - - const r = await fetchJson( - `${baseUrl}/files/content?path=${encodeURIComponent(path)}` - ); - if (r.ok && r.body) { - setEditorValue(r.body.content); - editorEls.contentBox.disabled = false; - updateByteCount(); - updateSaveEnabled(); - editorDialog.showModal(); - setTimeout(() => editorEls.contentBox.focus(), 0); - return; - } - - // Fallback: server says not editable. Re-open as binary. - editorEls.contentBox.disabled = false; - editor.mode = "binary"; - } else { - editor.mode = "binary"; - } - - // Binary mode setup. - editorEls.textPanel.hidden = true; - editorEls.binaryPanel.hidden = false; - editorEls.binarySize.textContent = "—"; // server gave us no size; cosmetic - updateSaveEnabled(); - editorDialog.showModal(); - setTimeout(() => editorEls.filename.focus(), 0); - } - - // ---------- direct-bound persistent-input listeners (legacy dialog) ---------- - - if (editorDialog) { - // Per plan: filename + content textarea input/keydown stay direct- - // bound. They're high-frequency events on persistent inputs inside - // the persistent legacy dialog; delegation would add per-keystroke - // selector-matching overhead. - editorEls.filename.addEventListener("input", () => { - updateRenameHint(); - updateSaveEnabled(); - }); - editorEls.contentBox.addEventListener("input", () => { - updateByteCount(); - updateSaveEnabled(); - }); - editorEls.contentBox.addEventListener("keydown", (event) => { - if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") { - event.preventDefault(); - editorEls.saveBtn.click(); - } - }); - editorDialog.addEventListener("close", () => { - setQueuedReplacement(null); - }); - } - - // ---------- delegated handlers (legacy + URL-addressable) ---------- - - function inLegacyEditor(el) { - return !!editorDialog && editorDialog.contains(el); - } function inRoutedEditor(el) { const mc = document.getElementById("modal-content"); return !!mc && mc.contains(el); } - - // ---------- routed binary-replace state ---------- - - // The queued File for the URL-addressable binary editor. Cleared on - // modal-container close so reopening starts fresh. - let routedReplacement = null; - function isRoutedBinaryMode(modalContent) { return !!modalContent?.querySelector(".files-editor-binary"); } @@ -287,9 +67,9 @@ updateRoutedBinarySaveEnabled(modalContent); } - // Enables the Save (Replace) button in routed binary mode when either - // a replacement file is queued OR the filename input has been edited. - // Mirrors legacy updateSaveEnabled's binary-mode branch. + // Enables Save (labeled "Replace" in binary mode) when either a file + // is queued OR the filename input has been edited. Rename-only is a + // valid Replace and routes to /files/move below. function updateRoutedBinarySaveEnabled(modalContent) { const panel = modalContent.querySelector(".files-editor-binary[data-rel-path]"); const saveBtn = modalContent.querySelector(".files-editor-save"); @@ -303,14 +83,11 @@ saveBtn.disabled = !routedReplacement && !filenameChanged; } - // Replace-zone drag — delegated for both legacy and routed binary - // modes. The zone lives inside #files-editor-modal (legacy) or - // #modal-content (routed); the in-{legacy,routed}Editor checks - // dispatch to the right reaction. + // ---------- delegated handlers (#modal-content only) ---------- + document.addEventListener("dragover", (event) => { const zone = event.target?.closest?.(".files-editor-replace-zone"); - if (!zone) return; - if (!inLegacyEditor(zone) && !inRoutedEditor(zone)) return; + if (!zone || !inRoutedEditor(zone)) return; if (Array.from(event.dataTransfer.types).includes("Files")) { event.preventDefault(); zone.classList.add("is-drop-target"); @@ -318,255 +95,78 @@ }); document.addEventListener("dragleave", (event) => { const zone = event.target?.closest?.(".files-editor-replace-zone"); - if (!zone) return; - if (!inLegacyEditor(zone) && !inRoutedEditor(zone)) return; + if (!zone || !inRoutedEditor(zone)) return; zone.classList.remove("is-drop-target"); }); document.addEventListener("drop", (event) => { const zone = event.target?.closest?.(".files-editor-replace-zone"); - if (!zone) return; - const isLegacy = inLegacyEditor(zone); - const isRouted = !isLegacy && inRoutedEditor(zone); - if (!isLegacy && !isRouted) return; + if (!zone || !inRoutedEditor(zone)) return; if (!Array.from(event.dataTransfer.types).includes("Files")) return; event.preventDefault(); zone.classList.remove("is-drop-target"); const f = event.dataTransfer.files && event.dataTransfer.files[0]; - if (!f) return; - if (isLegacy) { - setQueuedReplacement(f); - } else { - setRoutedReplacement(document.getElementById("modal-content"), f); - } + if (f) setRoutedReplacement(document.getElementById("modal-content"), f); }); - // Replace-input change (browse-fallback): low frequency, delegated - // for both editors. document.addEventListener("change", (event) => { const input = event.target?.closest?.(".files-editor-replace-input"); - if (!input) return; - const isLegacy = inLegacyEditor(input); - const isRouted = !isLegacy && inRoutedEditor(input); - if (!isLegacy && !isRouted) return; + if (!input || !inRoutedEditor(input)) return; const f = input.files && input.files[0]; - if (!f) return; - if (isLegacy) { - setQueuedReplacement(f); - } else { - setRoutedReplacement(document.getElementById("modal-content"), f); - } + if (f) setRoutedReplacement(document.getElementById("modal-content"), f); }); - // Filename input in routed binary mode: re-evaluate Save enablement - // when the user types a new name (rename-only is a valid Replace). + // Filename input: re-evaluate Save enablement in routed binary mode + // so rename-only Replace becomes reachable without queueing a file. document.addEventListener("input", (event) => { const input = event.target?.closest?.("[data-editor-filename]"); if (!input || !inRoutedEditor(input)) return; const mc = document.getElementById("modal-content"); - if (!mc || !isRoutedBinaryMode(mc)) return; - updateRoutedBinarySaveEnabled(mc); + if (mc && isRoutedBinaryMode(mc)) updateRoutedBinarySaveEnabled(mc); }); document.addEventListener("click", async (event) => { const target = event.target; if (!target) return; - // Replace-clear (both editors). const clearBtn = target.closest?.(".files-editor-replace-clear"); - if (clearBtn) { - if (inLegacyEditor(clearBtn)) { - setQueuedReplacement(null); - return; - } - if (inRoutedEditor(clearBtn)) { - setRoutedReplacement(document.getElementById("modal-content"), null); - return; - } + if (clearBtn && inRoutedEditor(clearBtn)) { + setRoutedReplacement(document.getElementById("modal-content"), null); + return; } - // Replace-browse (both editors) → trigger hidden file input. const browseBtn = target.closest?.(".files-editor-replace-browse"); - if (browseBtn) { - if (inLegacyEditor(browseBtn)) { - editorEls.replaceInput.click(); - return; - } - if (inRoutedEditor(browseBtn)) { - const mc = document.getElementById("modal-content"); - const fileInput = mc?.querySelector(".files-editor-replace-input"); - fileInput?.click(); - return; - } + if (browseBtn && inRoutedEditor(browseBtn)) { + const mc = document.getElementById("modal-content"); + const fileInput = mc?.querySelector(".files-editor-replace-input"); + fileInput?.click(); + return; } - // Save (both editors). Routed mode further branches on text vs. - // binary panel. const saveBtn = target.closest?.(".files-editor-save"); - if (saveBtn) { - if (inLegacyEditor(saveBtn)) { - await legacySaveClicked(); - return; - } - if (inRoutedEditor(saveBtn)) { - const mc = document.getElementById("modal-content"); - if (isRoutedBinaryMode(mc)) { - await routedReplaceClicked(); - } else { - await routedSaveClicked(); - } - return; + if (saveBtn && inRoutedEditor(saveBtn)) { + const mc = document.getElementById("modal-content"); + if (isRoutedBinaryMode(mc)) { + await routedReplaceClicked(); + } else { + await routedSaveClicked(); } + return; } - // Delete (both editors). const delBtn = target.closest?.(".files-editor-delete"); - if (delBtn) { - if (inLegacyEditor(delBtn)) { - await legacyDeleteClicked(); - return; - } - if (inRoutedEditor(delBtn)) { - await routedDeleteClicked(); - return; - } + if (delBtn && inRoutedEditor(delBtn)) { + await routedDeleteClicked(); + return; } }); - // Clear routed binary state when the modal closes. Otherwise the next - // open would still see a "queued" replacement from the prior session. + // Clear queued replacement when the modal closes (Esc, backdrop, + // dismiss button, programmatic closeRouted). Otherwise reopening + // would inherit a stale queued File from the prior session. document.getElementById("modal-container")?.addEventListener("close", () => { routedReplacement = null; }); - async function legacySaveClicked() { - const filename = editorEls.filename.value.trim(); - if (!filename) return; - if (filename.includes("/")) { - alert("Filename can't contain '/'. Use drag-to-move to relocate."); - return; - } - const folder = editor.folder || ""; - const newRel = joinPath(folder, filename); - - if (editor.creating) { - // Text-flavor create → /save with no new_path. - const r = await postJson(`${baseUrl}/files/save`, { - path: newRel, - content: getEditorValue(), - }); - if (r.ok) { - editorDialog.close(); - scheduleRefresh(folder); - } else if (r.status === 409) { - // askConflict lives in the legacy files-overlay.js for Phase A; - // exposed there on __filesOverlay. Phase A Step 3 moves it to - // dialogs.js (still on __filesOverlay), so this call site - // doesn't change in Step 3. - const askConflict = fo.askConflict; - const withCollisionSuffix = fo.withCollisionSuffix; - if (typeof askConflict !== "function" || typeof withCollisionSuffix !== "function") { - alert(`Save failed (HTTP ${r.status}).`); - return; - } - const action = await askConflict(newRel); - if (action === "overwrite") { - const r2 = await postJson(`${baseUrl}/files/save`, { - path: newRel, - content: getEditorValue(), - }); - if (r2.ok) { - editorDialog.close(); - scheduleRefresh(folder); - } else { - alert( - (r2.body && r2.body.error) || - `Save failed (HTTP ${r2.status}).` - ); - } - } else if (action === "keep-both") { - const altered = withCollisionSuffix(newRel); - const r2 = await postJson(`${baseUrl}/files/save`, { - path: altered, - content: getEditorValue(), - }); - if (r2.ok) { - editorDialog.close(); - scheduleRefresh(folder); - } - } - } else { - alert((r.body && r.body.error) || `Save failed (HTTP ${r.status}).`); - } - return; - } - - const renaming = newRel !== editor.originalPath; - if (editor.mode === "text") { - const payload = { - path: editor.originalPath, - content: getEditorValue(), - }; - if (renaming) payload.new_path = newRel; - const r = await postJson(`${baseUrl}/files/save`, payload); - if (r.ok) { - editorDialog.close(); - scheduleRefresh(folder); - } else { - alert( - (r.body && r.body.error) || `Save failed (HTTP ${r.status}).` - ); - } - return; - } - - // Binary mode. - const fd = new FormData(); - fd.append("path", editor.originalPath); - fd.append("csrf_token", csrfToken); - if (renaming) fd.append("new_path", newRel); - if (editor.queuedReplacement) { - fd.append("file", editor.queuedReplacement); - const r = await postForm(`${baseUrl}/files/replace`, fd); - if (r.ok) { - editorDialog.close(); - scheduleRefresh(folder); - } else { - alert( - (r.body && r.body.error) || `Replace failed (HTTP ${r.status}).` - ); - } - } else if (renaming) { - // Rename only via /move (no content change). - const r = await postJson(`${baseUrl}/files/move`, { - src: editor.originalPath, - dst: newRel, - }); - if (r.ok) { - editorDialog.close(); - scheduleRefresh(folder); - } else { - alert( - (r.body && r.body.error) || `Rename failed (HTTP ${r.status}).` - ); - } - } - } - - async function legacyDeleteClicked() { - if (!editor.originalPath) return; - if (!confirm(`Delete ${editor.originalPath}?`)) return; - const fd = new FormData(); - fd.append("path", editor.originalPath); - fd.append("csrf_token", csrfToken); - const r = await postForm(`${baseUrl}/files/delete`, fd); - if (r.ok) { - editorDialog.close(); - scheduleRefresh(parentOf(editor.originalPath)); - } else { - alert((r.body && r.body.error) || `Delete failed (HTTP ${r.status}).`); - } - } - - // ---------- URL-addressable modal (#modal-content) save / delete ---------- + // ---------- save / replace / delete ---------- async function routedSaveClicked() { const modalContent = document.getElementById("modal-content"); @@ -580,12 +180,10 @@ ? window.__filesEditor.getValue() : ta.value; - // is_new mode: relPath is empty; the new file's path comes from - // data-at-folder + filename input. The server route at - // /overlays//files/new sets data-at-folder; the /save endpoint - // creates the file when the path doesn't already exist. + // is_new mode: relPath is empty; compose path from data-at-folder + // + filename. The /save endpoint creates the file when missing. if (!relPath) { - if (!editedFilename) return; // empty filename — wait for input + if (!editedFilename) return; const atFolder = ta.dataset.atFolder || ""; const fullPath = atFolder ? `${atFolder.replace(/\/+$/, "")}/${editedFilename}` @@ -602,19 +200,15 @@ return; } - // Edit mode: rename-on-save if the filename input changed. Compose - // a sibling rename (parent of relPath + new filename), send - // payload.new_path so the server moves and writes atomically. + // Edit mode: rename-on-save (sibling rename only — parent stays). 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.modals?.closeRouted === "function") window.modals.closeRouted(); @@ -634,7 +228,6 @@ const relPath = panel.dataset.relPath; if (!relPath) return; - // Rename-on-replace: same shape as routedSaveClicked, sibling-only. const filenameInput = modalContent.querySelector("[data-editor-filename]"); const editedFilename = filenameInput ? filenameInput.value.trim() : ""; const originalLeaf = relPath.split("/").pop() || relPath; @@ -644,9 +237,6 @@ ? (parent ? `${parent}/${editedFilename}` : editedFilename) : null; - // Two flows: a queued replacement → /files/replace (multipart); - // rename-only (no queued file) → /files/move. Mirrors the legacy - // binary save handler. if (routedReplacement) { const fd = new FormData(); fd.append("path", relPath); @@ -664,10 +254,7 @@ return; } if (renaming) { - const r = await postJson(`${baseUrl}/files/move`, { - src: relPath, - dst: newPath, - }); + const r = await postJson(`${baseUrl}/files/move`, { src: relPath, dst: newPath }); if (r.ok) { if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted(); scheduleRefresh(parentOf(newPath)); @@ -680,9 +267,10 @@ async function routedDeleteClicked() { const modalContent = document.getElementById("modal-content"); if (!modalContent) return; + // rel-path lives on the text-mode textarea OR on the binary panel. const ta = modalContent.querySelector("textarea[data-rel-path]"); - if (!ta) return; - const relPath = ta.dataset.relPath; + const panel = modalContent.querySelector(".files-editor-binary[data-rel-path]"); + const relPath = ta?.dataset?.relPath || panel?.dataset?.relPath; if (!relPath) return; if (!confirm(`Delete ${relPath}?`)) return; const fd = new FormData(); @@ -700,11 +288,6 @@ // ---------- register action-registry handlers ---------- fo.registerHandler("new-file", (path) => { - // Phase B Step 6: create-new-file uses the URL-addressable modal - // (via the new GET /overlays//files/new?at= route). - // The legacy openEditorTextNew remains in this file until Step 9 - // deletes the legacy dialog block wholesale; it's no longer - // reachable from a user action. const url = `/overlays/${overlayId}/files/new?at=${encodeURIComponent(path)}`; if (typeof window.modals?.openRouted === "function") { window.modals.openRouted(url); @@ -714,19 +297,13 @@ }); fo.registerHandler("edit", (path, _actionEl) => { - // Phase B Step 8: both editable text and binary files route to the - // URL-addressable modal. The server's /files/edit route picks the - // template branch (text editor vs. binary-replace panel) based on - // is_editable(target). The data-editable attribute on the action - // element is now informational only — the server is the - // source of truth. - const editUrl = `/overlays/${overlayId}/files/edit?path=${encodeURIComponent(path)}`; + // Both text and binary files use the same edit route — the server + // picks the template branch based on is_editable(target). + const url = `/overlays/${overlayId}/files/edit?path=${encodeURIComponent(path)}`; if (typeof window.modals?.openRouted === "function") { - window.modals.openRouted(editUrl); + window.modals.openRouted(url); } else { - // Graceful fallback if modals.js didn't load — full-page nav - // hits the same route and renders the standalone editor page. - window.location.href = editUrl; + window.location.href = url; } }); })(); diff --git a/l4d2web/l4d2web/templates/overlay_detail.html b/l4d2web/l4d2web/templates/overlay_detail.html index 95c3ec6..9a14548 100644 --- a/l4d2web/l4d2web/templates/overlay_detail.html +++ b/l4d2web/l4d2web/templates/overlay_detail.html @@ -162,68 +162,6 @@ {% endif %} {% if files_can_edit %} - - - - - -