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.
This commit is contained in:
digital-garbage
2026-08-22 20:06:27 +03:00
parent 641263c4a4
commit 95fb87db0d
2 changed files with 55 additions and 9 deletions
+8
View File
@@ -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
+47 -9
View File
@@ -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);
}