diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3a1962..bb816332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ is open pins the NODE, not the section it lives in. Slot 1 is always the leftmost button and slot 3 the one nearest Assets; an empty slot closes up and the remaining buttons keep their own numbers, so a shortcut's position never moves. Hovering says where it leads. - Pins ride with the project, like the other editor preferences. + Pins ride with the project, like the other editor preferences. A node opened this way is a + destination, not a stop inside Settings: Save and Cancel both close the window and return + you to the editor, rather than leaving you in the Models list you never asked for. - **Reverse a clip**, audio included, in `+ Add → Effects`. Unlike the other clip effects there is no preview-side equivalent, so the monitor switches that clip to a server-rendered @@ -96,6 +98,12 @@ looked exactly like one where the feature was switched off. ### Fixed +- **Closing Models & Pipeline mid-edit no longer leaves its edit buffer behind.** Escape, the + ✕, or switching to another section tore the section down without ending the edit, so the + next visit inherited a dirty flag and a baseline belonging to a config that had already + been replaced — a spurious "unsaved changes" prompt, and a Cancel that could splice stale + slots into freshly loaded ones. + - **Prompt templates can be renamed, deleted, and turned off.** The Composer's Templates bar applied a template and immediately forgot it, so there was no state to leave and no way to leave it — and clearing the prompt box by hand did nothing, because an empty global prompt diff --git a/movie_editor/frontend/models.js b/movie_editor/frontend/models.js index 67d5d15a..f07e8ead 100644 --- a/movie_editor/frontend/models.js +++ b/movie_editor/frontend/models.js @@ -950,7 +950,7 @@ cancel.onclick = () => { if (deferDirty && !confirm("Discard the changes to this node?")) return; restoreBaseline(); - _setView("pipeline"); + finishNodeEdit(); }; const save = el("button", "btn primary", "Save"); @@ -960,7 +960,7 @@ deferSave = false; await persistNow(); endEdit(); - _setView("pipeline"); + finishNodeEdit(); }; acts.append(cancel, save); @@ -2011,6 +2011,7 @@ // Internal: no unsaved-changes check. Used by Cancel/Save, which have already dealt // with the buffer, and by paths that removed the node they were editing. function _setView(v) { + if (v !== "node:" + _directNodeId) _directNodeId = null; if (view.startsWith("node:")) endEdit(); view = v; if (view.startsWith("node:")) beginEdit(); @@ -2622,6 +2623,24 @@ // A pinned button can ask for a node before the section has ever mounted, and the slots // only exist after loadAll(). Held here and applied once they do. let _openNodeOnMount = null; + // The node currently being shown as a DESTINATION rather than as a stop inside Settings. + // Set when a pinned button opens it; cleared as soon as the user navigates elsewhere, + // because from then on they are browsing Settings normally. + let _directNodeId = null; + + /** Leave a node page: back to the pipeline normally, but a node opened straight from a + * pinned button is somewhere the user went ON PURPOSE — dropping them into the Models + * list afterwards would undo the point of the shortcut. Finishing there closes Settings + * and returns them to the editor, saved or discarded alike. */ + function finishNodeEdit() { + if (_directNodeId && view === "node:" + _directNodeId) { + _directNodeId = null; + endEdit(); + window.SettingsWindow.close(); + return; + } + _setView("pipeline"); + } function mount(body, ctx) { container = el("div", "models-mount"); @@ -2640,7 +2659,11 @@ .then(() => { if (!container || !container.isConnected) return; const pending = _openNodeOnMount; _openNodeOnMount = null; - if (pending && slotById(pending)) { _setView("node:" + pending); return; } + if (pending && slotById(pending)) { + _setView("node:" + pending); + _directNodeId = pending; // after _setView, which clears it + return; + } render(); if (pending) { // Landing on the pipeline instead, without saying why, would look like the @@ -2658,6 +2681,14 @@ }); return () => { container = null; + // The section can be torn down mid-edit — Escape, the ✕, switching sections — and + // loadAll() replaces `config` on the next mount anyway. Leaving the edit buffer + // behind would carry deferDirty and a baseline for an object that no longer exists + // into that mount: a spurious "unsaved changes" prompt, and a Cancel that splices + // stale slots into freshly loaded ones. + endEdit(); + _directNodeId = null; + _openNodeOnMount = null; document.querySelectorAll(".mn-role-pop, .ns-overlay").forEach((n) => n.remove()); }; } @@ -2686,13 +2717,13 @@ // whether or not the section has been mounted before. openNode: (slotId) => { if (!slotId) return; - if (container && container.isConnected && slotById(slotId)) { - window.SettingsWindow.open("models"); - setView("node:" + slotId); - return; - } - _openNodeOnMount = slotId; + // One route, whatever is on screen: open() mounts (or REMOUNTS) the section, and the + // request is armed AFTER that call — a remount tears the old section down, which + // clears pending state, so arming first would lose it. mount()'s load is async, so + // this still lands before the node view is chosen. The remount costs one refetch and + // buys freshness, which is what a shortcut into a quick edit wants anyway. window.SettingsWindow.open("models"); + _openNodeOnMount = slotId; }, refresh: async () => { await ensureRoles().catch(() => {});