Files
ausboss-ComfyUI-AusBoss/js/shared/paste_ids.mjs
ausboss d0658ea8b2 Workflow Switches and Callout: follow the copies after a paste
Copy and paste give every pasted node and group a new id, and so do
duplicate, Alt + drag and dragging a saved workflow onto the canvas. A
Workflow Switches card remembers its parts by id, so a whole workflow
pasted into a new tab read "empty" on every row and nothing could be
switched on; pasted into another workflow it could switch unrelated nodes
that had the old numbers. A Callout's links to a node or group broke the
same way.

The frontend's paste (LGraphCanvas._deserializeItems) returns which new
node each old id became. js/shared/paste_ids.mjs hands that on after every
paste, and a pasted card or note points at the copies of what it names.
An id the paste did not copy is kept, so a card pasted on its own still
switches the original parts.
2026-10-29 11:05:44 -05:00

71 lines
3.0 KiB
JavaScript

// What a paste did to ids. Copy and paste give every pasted node and group a
// new id, and so do duplicate, Alt + drag and dropping a saved workflow on the
// canvas: the frontend sends all of them through
// LGraphCanvas._deserializeItems. A node that remembers other nodes by id
// (Workflow Switches, a Callout's links) would keep naming the old ones.
// Pasted on their own, its copies point at nothing; pasted into another
// workflow, they can point at an unrelated node that has the old number.
//
// The paste returns which new node each old id became. onPaste hands that
// on, so a pasted node can follow the copies of what it points at. A frontend
// without the method pastes as before and no handler runs.
//
// No app import, so tests/paste_ids.test.mjs runs it under node:test.
const handlers = new Set();
let installed = false;
// Old id (as text) -> the copy's id, for the nodes and the groups of one
// paste. The frontend adds the groups it is handed one by one, in order, so
// the groups it made are matched to the old ids by position; when the counts
// differ, no group is matched rather than a wrong one.
export function pastedIds(result, groupIds = [], isGroup = () => false) {
const nodes = new Map();
for (const [id, node] of result?.nodes ?? []) {
if (node?.id !== undefined && node.id !== null) nodes.set(String(id), node.id);
}
const created = [...(result?.created ?? [])];
const groups = new Map();
const made = created.filter(isGroup);
if (made.length === groupIds.length) {
groupIds.forEach((id, index) => {
if (id !== undefined && id !== null && made[index]?.id !== undefined) groups.set(String(id), made[index].id);
});
}
return { nodes, groups, created };
}
// Runs `handler({ nodes, groups, created })` after every paste.
export function onPaste(handler) {
if (typeof handler === "function") handlers.add(handler);
install();
}
function install() {
if (installed) return;
const lite = globalThis.LiteGraph;
const proto = lite?.LGraphCanvas?.prototype;
const original = proto?._deserializeItems;
if (typeof original !== "function") return;
installed = true;
const isGroup = (item) => Boolean(lite.LGraphGroup && item instanceof lite.LGraphGroup);
// A method whose result is needed, not a callback: chainCallback would drop
// the result, so wrap it and always hand the frontend's own result back.
function deserializeItemsWithIds(items, ...rest) {
// Read before the paste, which gives the groups it is handed new ids.
const groupIds = Array.isArray(items?.groups) ? items.groups.map((group) => group?.id) : [];
const result = original.call(this, items, ...rest);
if (!result || !handlers.size) return result;
const ids = pastedIds(result, groupIds, isGroup);
for (const handler of handlers) {
try {
handler(ids);
} catch (error) {
console.warn("[AusBoss] after a paste:", error);
}
}
return result;
}
proto._deserializeItems = deserializeItemsWithIds;
}