From 77575fa6b2fbf6fe03554d038a765b4bb251782f Mon Sep 17 00:00:00 2001 From: Edouard Durand Date: Sun, 27 Sep 2026 07:03:48 +0000 Subject: [PATCH] Close a workflow's tabs when you delete it from the file list --- tests/js/sync.test.mjs | 34 ++++++++++++++++++++++++++++++++++ web/rigshare/files.js | 10 ++++++++-- web/rigshare/sync.js | 22 ++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/tests/js/sync.test.mjs b/tests/js/sync.test.mjs index fd5ecb5..6c8cee4 100644 --- a/tests/js/sync.test.mjs +++ b/tests/js/sync.test.mjs @@ -128,3 +128,37 @@ test("a file renamed by someone else re-points the open tab and rejoins its room sync.tick(); assert.equal(sent.at(-1).room, null, "moved out of reach: the tab leaves the room"); }); + +test("deleting a folder closes its tabs, the active one through ComfyUI's close", async () => { + const a = { path: "workflows/team/a.json", isTemporary: false, isModified: true }; + const b = { path: "workflows/team/sub/b.json", isTemporary: false }; + const other = { path: "workflows/other.json", isTemporary: false }; + const store = { + activeWorkflow: a, openWorkflows: [a, b, other], + closeWorkflow(wf) { this.openWorkflows = this.openWorkflows.filter((w) => w !== wf); }, + }; + const commands = []; + const client = Object.assign(new EventTarget(), { + online: true, perms: { edit: true }, rooms: [], users: [], + on(type, fn) { this.addEventListener(type, (e) => fn(e.detail)); }, + emit(type, detail) { this.dispatchEvent(new CustomEvent(type, { detail })); }, + send() { return true; }, + }); + const app = { + extensionManager: { workflow: store, command: { async execute(id) { + commands.push([id, store.activeWorkflow.isModified]); + store.closeWorkflow(store.activeWorkflow); + store.activeWorkflow = store.openWorkflows[0]; + } } }, + canvas: null, loadGraphData: async () => {}, + rootGraph: { serialize: () => ({ nodes: [], links: [], extra: {} }), extra: {}, getNodeById: () => null }, + }; + const sync = new RoomSync(app, new EventTarget(), client); + const toasts = []; + client.on("toast", (t) => toasts.push(t)); + await sync.closeTabsUnder("workflows/team"); + assert.deepEqual(store.openWorkflows, [other]); + assert.deepEqual(commands, [["Workspace.CloseWorkflow", false]], "no unsaved-changes prompt"); + client.emit("room_closed", { room: "file:workflows/team/a.json", name: "a" }); + assert.equal(toasts.length, 0, "our own delete: no private-copy notice"); +}); diff --git a/web/rigshare/files.js b/web/rigshare/files.js index db42e0b..df406b3 100644 --- a/web/rigshare/files.js +++ b/web/rigshare/files.js @@ -310,10 +310,15 @@ export class FilesBrowser { async remove(e) { const what = e.type === "folder" ? `the folder "${e.name}"` : `"${e.name}"`; if (!(await this.confirm("Delete", `Delete ${what}? This cannot be undone.`))) return; + // Its open tabs close first (ComfyUI would otherwise keep them as orphans). + const path = `workflows/${e.path}`; + const closing = this.sync.tabsUnder(path).map((wf) => `file:${wf.path}`); + await this.sync.closeTabsUnder(path); + let done; if (e.type === "folder") { - await this.run(() => this.client.request("POST", "/rigshare/api/workspace/delete-folder", { path: e.path }), "Deleted"); + done = await this.run(() => this.client.request("POST", "/rigshare/api/workspace/delete-folder", { path: e.path }), "Deleted"); } else { - await this.run(async () => { + done = await this.run(async () => { const store = this.app.extensionManager.workflow; await store.syncWorkflows?.(); const wf = store.getWorkflowByPath?.(`workflows/${e.path}`); @@ -325,6 +330,7 @@ export class FilesBrowser { } }, "Deleted"); } + if (!done) for (const key of closing) this.sync.denied.delete(key); // still there: reopening it rejoins await this.refreshComfy(); this.reload(); } diff --git a/web/rigshare/sync.js b/web/rigshare/sync.js index 2820868..482d717 100644 --- a/web/rigshare/sync.js +++ b/web/rigshare/sync.js @@ -141,6 +141,7 @@ export class RoomSync extends EventTarget { }); client.on("room_closed", (msg) => { // The file was deleted: the tab stays open as a private copy. + if (this.denied.has(msg.room) && !this.findTab({ key: msg.room })) return; // we deleted it and closed the tab this.denied.add(msg.room); if (msg.room === this.targetKey) { this.ready = false; @@ -524,6 +525,27 @@ export class RoomSync extends EventTarget { !wf.isTemporary && (wf.path === path || wf.path?.startsWith(path + "/"))); } + /** + * We are deleting a file (or folder): close its tabs first, without the + * "unsaved changes" prompt (the user already confirmed the delete). The + * active tab goes through ComfyUI's own close, which opens the next tab. + */ + async closeTabsUnder(path) { + const store = this.store; + const tabs = this.tabsUnder(path); + if (!tabs.length) return; + for (const wf of tabs) { + this.denied.add(`file:${wf.path}`); // our own delete: no "private copy" notice + if (wf === store.activeWorkflow) continue; + store.closeWorkflow?.(wf); + } + const active = store.activeWorkflow; + if (active && tabs.includes(active)) { + active.isModified = false; + await this.app.extensionManager?.command?.execute("Workspace.CloseWorkflow"); + } + } + /** * Someone renamed or moved a file (or folder) we have open: point our tabs * at the new path. The file already moved on the server, so ComfyUI's own