From 95fb87db0d0d52ecbdeb4d8145c58aa902c23a75 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:06:27 +0300 Subject: [PATCH] fix(editor): dismissing pipeline setup could be undone by an in-flight reopen chooseFamily reopens the modal after three round-trips, so a close during that window was reverted. User closes now supersede pending continuations. --- CHANGELOG.md | 8 ++++ movie_editor/frontend/pipeline_setup.js | 56 +++++++++++++++++++++---- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ec95cdb..f9cd2bb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,14 @@ looked exactly like one where the feature was switched off. ### Fixed +- **"No, I'll use my own pipeline" left the setup modal on screen.** Picking a model family + starts a chain of requests that reopens the modal with fresh prerequisites; dismissing it + mid-flight closed it, then the reply landed and put it straight back — and since opting out + installs nothing, the prerequisites were still missing, so it always reopened. Closes the + user asks for now supersede anything in flight. The choice is also recorded before the + save round-trip rather than after it, and a save that fails says so instead of leaving the + project quietly still on the built-in pipeline. + - **An output can be wired to several destinations while adding a node**, not only after. The Add-node panel offered one destination per output and told you to finish the job on the node's page; it now uses the same multi-destination editor the node page does, with diff --git a/movie_editor/frontend/pipeline_setup.js b/movie_editor/frontend/pipeline_setup.js index ed0e1370..39365653 100644 --- a/movie_editor/frontend/pipeline_setup.js +++ b/movie_editor/frontend/pipeline_setup.js @@ -20,12 +20,27 @@ try { localStorage.setItem(LS_KEY, "1"); } catch (_) {} } + // Bumped by every close the USER asked for. chooseFamily / applyFamily / useOwnPipeline + // each span two or three round-trips and then reopen the modal with fresh deps; without + // this, a continuation that started before the user dismissed the modal reopens it + // afterwards — which is exactly what "No, I'll use my own pipeline" looked like, since + // disabling the built-in pipeline installs nothing, so `needs_setup` is still true when + // the in-flight chooseFamily lands. + let _closeToken = 0; + function closeModal() { if (_pollTimer) { clearTimeout(_pollTimer); _pollTimer = null; } overlay?.remove(); overlay = null; } + // Close AND supersede anything already in flight. Use this for every close the user + // initiates; closeModal() alone is for internal teardown before a reopen. + function dismissModal() { + _closeToken += 1; + closeModal(); + } + function builtInPipelineActive(st) { const m = st.models || {}; return !m.disable_core; @@ -43,13 +58,29 @@ } async function useOwnPipeline() { - if (!S.get().project) return; - const models = JSON.parse(JSON.stringify(S.get().models || S.get().project?.models || { slots: [] })); - models.disable_core = true; - await API.saveModels(S.get().project.id, models); - await refreshModels(models); + const proj = S.get().project; + if (!proj) { + // Nothing to write the choice to. Still close: leaving the modal up with a button + // that does nothing when pressed is the worse of the two failures. + dismissModal(); + return; + } + // Dismissed BEFORE the awaits: the decision is already made, and holding the modal open + // for two round-trips is what let an in-flight family lookup reopen it afterwards. setDismissed(); - closeModal(); + dismissModal(); + const models = JSON.parse(JSON.stringify(S.get().models || proj.models || { slots: [] })); + models.disable_core = true; + try { + await API.saveModels(proj.id, models); + } catch (e) { + // The choice did NOT persist — saying so beats a project that quietly still runs the + // built-in pipeline on the next Generate. + alert("Could not save the pipeline choice: " + (e?.message || e) + + "\n\nSet it in Models → Enable built-in pipeline."); + return; + } + await refreshModels(models); } function packList(deps) { @@ -236,9 +267,12 @@ async function chooseFamily(key) { if (!S.get().project) return; + const tok = _closeToken; await applyFamily(key); + if (tok !== _closeToken) return; // user closed the modal while this was in flight let deps; try { deps = await API.pipelineDeps(S.get().project?.id); } catch (_) { closeModal(); return; } + if (tok !== _closeToken) return; if (!deps?.needs_setup) { closeModal(); return; } openModal(deps); } @@ -382,12 +416,12 @@ foot.append(ownBtn); const dismissBtn = el("button", "btn ghost", "Close"); dismissBtn.type = "button"; - dismissBtn.onclick = () => closeModal(); + dismissBtn.onclick = () => dismissModal(); foot.append(dismissBtn); modal.append(foot); overlay.append(modal); - overlay.addEventListener("click", (e) => { if (e.target === overlay) closeModal(); }); + overlay.addEventListener("click", (e) => { if (e.target === overlay) dismissModal(); }); document.body.append(overlay); } @@ -395,7 +429,7 @@ // not open behind it — or, worse, on top of the editor the moment the wizard finishes. function markHandled() { setDismissed(); - closeModal(); + dismissModal(); } async function maybePrompt() { @@ -404,8 +438,10 @@ if (dismissed()) return; const st = S.get(); if (!st.project || !builtInPipelineActive(st)) return; + const tok = _closeToken; let deps; try { deps = await API.pipelineDeps(S.get().project?.id); } catch (_) { return; } + if (tok !== _closeToken) return; if (!deps?.needs_setup) return; openModal(deps); } @@ -414,8 +450,10 @@ // after it has been dismissed once (the auto-prompt is one-shot per browser). async function open() { if (!S.get().project) return; + const tok = _closeToken; let deps; try { deps = await API.pipelineDeps(S.get().project?.id); } catch (_) { return; } + if (tok !== _closeToken) return; openModal(deps); }