From 8dc14f0cca820b244e0987be9fe5d45701cc35d5 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 17:12:14 +0200 Subject: [PATCH] feat(files): wire askConflict into the routed new-file 409 path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The is_new branch of routedSaveClicked in editor.js used to alert on 409 and force the user to manually pick a different filename. Restore the overwrite / keep-both / cancel prompt the legacy openEditorTextNew flow had (via askConflict, lost when Step 9 deleted legacySaveClicked). Flow on 409: * "overwrite" → re-POST /save with the same path. /save overwrites in place when the destination is a regular file. * "keep-both" → compose a suffixed path via fo.withCollisionSuffix (now multi-extension-aware after F3) and POST that. * "cancel" → leave the routed modal open with the user's typed content intact so they can edit the filename and retry. Defensively gates the askConflict + withCollisionSuffix calls on typeof === "function" so older bookmarks (or a dev environment with one of the modules missing) fall back to an alert rather than a TypeError. The 409 alert branch is preserved for that path. Note on when /save actually 409s: regular-file collisions overwrite silently (200). 409 fires only when the new path collides with a directory (or a symlink, or a non-file fs entry) — same contract as the legacy flow had. Verified live on /overlays/2 in Chromium with a real round-trip: 1. Click "+ new folder" → create tmp_409_probe 2. Click "+ new file" → type "tmp_409_probe" → click Create 3. /save returns 409 (destination is not a file) → askConflict opens with the colliding path displayed 4. Routed modal stays open behind the conflict dialog (typed content preserved) 5. Cancel on conflict → conflict closes, routed modal still open 6. Cleanup: delete the tmp_409_probe folder via the action API * No console errors throughout * Demo overlay state unchanged after cleanup pytest stays at 577 passed, 1 skipped, 3 deselected (no Python changes in F4). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../l4d2web/static/js/files-overlay/editor.js | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/l4d2web/l4d2web/static/js/files-overlay/editor.js b/l4d2web/l4d2web/static/js/files-overlay/editor.js index 03a3384..320a9c8 100644 --- a/l4d2web/l4d2web/static/js/files-overlay/editor.js +++ b/l4d2web/l4d2web/static/js/files-overlay/editor.js @@ -181,7 +181,10 @@ : ta.value; // is_new mode: relPath is empty; compose path from data-at-folder - // + filename. The /save endpoint creates the file when missing. + // + filename. The /save endpoint creates the file when missing, + // 409s when the destination already exists. On 409 we offer the + // same overwrite / keep-both / cancel prompt that the legacy + // create-new flow used (via askConflict in dialogs.js). if (!relPath) { if (!editedFilename) return; const atFolder = ta.dataset.atFolder || ""; @@ -192,11 +195,35 @@ 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; } + if (r.status === 409 && typeof fo.askConflict === "function") { + const action = await fo.askConflict(fullPath); + if (action === "overwrite") { + // /save overwrites in place when the destination is a file — + // a plain re-POST does the right thing. + const r2 = await postJson(`${baseUrl}/files/save`, { path: fullPath, content }); + if (r2.ok) { + if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted(); + scheduleRefresh(parentOf(fullPath)); + } else { + alert((r2.body && r2.body.error) || `Save failed (HTTP ${r2.status}).`); + } + } else if (action === "keep-both" && typeof fo.withCollisionSuffix === "function") { + const altered = fo.withCollisionSuffix(fullPath); + const r2 = await postJson(`${baseUrl}/files/save`, { path: altered, content }); + if (r2.ok) { + if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted(); + scheduleRefresh(parentOf(altered)); + } else { + alert((r2.body && r2.body.error) || `Save failed (HTTP ${r2.status}).`); + } + } + // "cancel" → leave the modal open so the user can edit the + // filename and try again without losing typed content. + return; + } + alert((r.body && r.body.error) || r.rawText || `Create failed (HTTP ${r.status}).`); return; }