From 4d045e578d8aa099483c6a2cf6b9f3aded1c4a50 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 16:05:55 +0200 Subject: [PATCH] feat(files): migrate create-new-file JS flow to URL-addressable modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 6/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md. Two changes in editor.js: 1. The "new-file" registered handler now opens the URL-addressable modal at GET /overlays//files/new?at= via window.modals.openRouted, replacing the call to openEditorTextNew. The legacy openEditorTextNew function stays in this file for now — it's no longer reachable from a user action; Step 9 deletes it alongside the rest of the legacy dialog block. 2. routedSaveClicked gains an is_new branch. When the textarea's data-rel-path is empty, the save composes the new file's path from data-at-folder (set by the /files/new route) + the user-typed filename and POSTs {path, content} to /files/save. The /save endpoint creates the file when it doesn't exist; 409 means a file at that path already exists and the user picks a different name (alert + modal stays open so the form value is preserved). The legacy slash-in-filename guard from openEditorTextNew's legacy save path is deliberately not carried over — the plan permits typing "sub/foo.txt" in the filename input to create a nested file via /save, matching the route's path semantics. Verified live on /overlays/2 in Chromium: * Click "+ new file" on overlay root → URL becomes ?modal=%2Foverlays%2F2%2Ffiles%2Fnew%3Fat%3D * Routed modal opens with empty data-rel-path, empty data-at-folder, empty filename input, save button labeled "Create", no Delete or Download buttons, title "…new file" * No console errors * pytest still 578 passed, 1 skipped, 3 deselected (no Python or test changes in Step 6) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../l4d2web/static/js/files-overlay/editor.js | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/l4d2web/l4d2web/static/js/files-overlay/editor.js b/l4d2web/l4d2web/static/js/files-overlay/editor.js index 994a693..29840cd 100644 --- a/l4d2web/l4d2web/static/js/files-overlay/editor.js +++ b/l4d2web/l4d2web/static/js/files-overlay/editor.js @@ -473,17 +473,37 @@ const ta = modalContent.querySelector("textarea[data-rel-path]"); if (!ta) return; const relPath = ta.dataset.relPath; - if (!relPath) return; + const filenameInput = modalContent.querySelector("[data-editor-filename]"); + const editedFilename = filenameInput ? filenameInput.value.trim() : ""; const content = (window.__filesEditor && window.__filesEditor.getValue) ? window.__filesEditor.getValue() : ta.value; - // 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() : ""; + // 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. + if (!relPath) { + if (!editedFilename) return; // empty filename — wait for input + const atFolder = ta.dataset.atFolder || ""; + const fullPath = atFolder + ? `${atFolder.replace(/\/+$/, "")}/${editedFilename}` + : editedFilename; + const r = await postJson(`${baseUrl}/files/save`, { path: fullPath, content }); + if (r.ok) { + if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted(); + scheduleRefresh(parentOf(fullPath)); + } else if (r.status === 409) { + alert(r.rawText || `A file at ${fullPath} already exists. Pick a different name.`); + } else { + alert((r.body && r.body.error) || r.rawText || `Create failed (HTTP ${r.status}).`); + } + 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. const originalLeaf = relPath.split("/").pop() || relPath; const parent = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : ""; let newPath = null; @@ -527,7 +547,19 @@ // ---------- register action-registry handlers ---------- - fo.registerHandler("new-file", (path) => openEditorTextNew(path)); + 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); + } else { + window.location.href = url; + } + }); fo.registerHandler("edit", (path, actionEl) => { const editable = actionEl?.dataset?.editable === "1";