| | import { $el, ComfyDialog } from "../../scripts/ui.js"; |
| | import { DraggableList } from "../../scripts/ui/draggableList.js"; |
| | import { addStylesheet } from "../../scripts/utils.js"; |
| | import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js"; |
| |
|
| | addStylesheet(import.meta.url); |
| |
|
| | const ORDER = Symbol(); |
| |
|
| | function merge(target, source) { |
| | if (typeof target === "object" && typeof source === "object") { |
| | for (const key in source) { |
| | const sv = source[key]; |
| | if (typeof sv === "object") { |
| | let tv = target[key]; |
| | if (!tv) tv = target[key] = {}; |
| | merge(tv, source[key]); |
| | } else { |
| | target[key] = sv; |
| | } |
| | } |
| | } |
| |
|
| | return target; |
| | } |
| |
|
| | export class ManageGroupDialog extends ComfyDialog { |
| | |
| | tabs = {}; |
| | |
| | selectedNodeIndex; |
| | |
| | selectedTab = "Inputs"; |
| | |
| | selectedGroup; |
| |
|
| | |
| | modifications = {}; |
| |
|
| | get selectedNodeInnerIndex() { |
| | return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex; |
| | } |
| |
|
| | constructor(app) { |
| | super(); |
| | this.app = app; |
| | this.element = $el("dialog.comfy-group-manage", { |
| | parent: document.body, |
| | }); |
| | } |
| |
|
| | changeTab(tab) { |
| | this.tabs[this.selectedTab].tab.classList.remove("active"); |
| | this.tabs[this.selectedTab].page.classList.remove("active"); |
| | this.tabs[tab].tab.classList.add("active"); |
| | this.tabs[tab].page.classList.add("active"); |
| | this.selectedTab = tab; |
| | } |
| |
|
| | changeNode(index, force) { |
| | if (!force && this.selectedNodeIndex === index) return; |
| |
|
| | if (this.selectedNodeIndex != null) { |
| | this.nodeItems[this.selectedNodeIndex].classList.remove("selected"); |
| | } |
| | this.nodeItems[index].classList.add("selected"); |
| | this.selectedNodeIndex = index; |
| |
|
| | if (!this.buildInputsPage() && this.selectedTab === "Inputs") { |
| | this.changeTab("Widgets"); |
| | } |
| | if (!this.buildWidgetsPage() && this.selectedTab === "Widgets") { |
| | this.changeTab("Outputs"); |
| | } |
| | if (!this.buildOutputsPage() && this.selectedTab === "Outputs") { |
| | this.changeTab("Inputs"); |
| | } |
| |
|
| | this.changeTab(this.selectedTab); |
| | } |
| |
|
| | getGroupData() { |
| | this.groupNodeType = LiteGraph.registered_node_types["workflow/" + this.selectedGroup]; |
| | this.groupNodeDef = this.groupNodeType.nodeData; |
| | this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType); |
| | } |
| |
|
| | changeGroup(group, reset = true) { |
| | this.selectedGroup = group; |
| | this.getGroupData(); |
| |
|
| | const nodes = this.groupData.nodeData.nodes; |
| | this.nodeItems = nodes.map((n, i) => |
| | $el( |
| | "li.draggable-item", |
| | { |
| | dataset: { |
| | nodeindex: n.index + "", |
| | }, |
| | onclick: () => { |
| | this.changeNode(i); |
| | }, |
| | }, |
| | [ |
| | $el("span.drag-handle"), |
| | $el( |
| | "div", |
| | { |
| | textContent: n.title ?? n.type, |
| | }, |
| | n.title |
| | ? $el("span", { |
| | textContent: n.type, |
| | }) |
| | : [] |
| | ), |
| | ] |
| | ) |
| | ); |
| |
|
| | this.innerNodesList.replaceChildren(...this.nodeItems); |
| |
|
| | if (reset) { |
| | this.selectedNodeIndex = null; |
| | this.changeNode(0); |
| | } else { |
| | const items = this.draggable.getAllItems(); |
| | let index = items.findIndex(item => item.classList.contains("selected")); |
| | if(index === -1) index = this.selectedNodeIndex; |
| | this.changeNode(index, true); |
| | } |
| |
|
| | const ordered = [...nodes]; |
| | this.draggable?.dispose(); |
| | this.draggable = new DraggableList(this.innerNodesList, "li"); |
| | this.draggable.addEventListener("dragend", ({ detail: { oldPosition, newPosition } }) => { |
| | if (oldPosition === newPosition) return; |
| | ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]); |
| | for (let i = 0; i < ordered.length; i++) { |
| | this.storeModification({ nodeIndex: ordered[i].index, section: ORDER, prop: "order", value: i }); |
| | } |
| | }); |
| | } |
| |
|
| | storeModification({ nodeIndex, section, prop, value }) { |
| | const groupMod = (this.modifications[this.selectedGroup] ??= {}); |
| | const nodesMod = (groupMod.nodes ??= {}); |
| | const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {}); |
| | const typeMod = (nodeMod[section] ??= {}); |
| | if (typeof value === "object") { |
| | const objMod = (typeMod[prop] ??= {}); |
| | Object.assign(objMod, value); |
| | } else { |
| | typeMod[prop] = value; |
| | } |
| | } |
| |
|
| | getEditElement(section, prop, value, placeholder, checked, checkable = true) { |
| | if (value === placeholder) value = ""; |
| |
|
| | const mods = this.modifications[this.selectedGroup]?.nodes?.[this.selectedNodeInnerIndex]?.[section]?.[prop]; |
| | if (mods) { |
| | if (mods.name != null) { |
| | value = mods.name; |
| | } |
| | if (mods.visible != null) { |
| | checked = mods.visible; |
| | } |
| | } |
| |
|
| | return $el("div", [ |
| | $el("input", { |
| | value, |
| | placeholder, |
| | type: "text", |
| | onchange: (e) => { |
| | this.storeModification({ section, prop, value: { name: e.target.value } }); |
| | }, |
| | }), |
| | $el("label", { textContent: "Visible" }, [ |
| | $el("input", { |
| | type: "checkbox", |
| | checked, |
| | disabled: !checkable, |
| | onchange: (e) => { |
| | this.storeModification({ section, prop, value: { visible: !!e.target.checked } }); |
| | }, |
| | }), |
| | ]), |
| | ]); |
| | } |
| |
|
| | buildWidgetsPage() { |
| | const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]; |
| | const items = Object.keys(widgets ?? {}); |
| | const type = app.graph.extra.groupNodes[this.selectedGroup]; |
| | const config = type.config?.[this.selectedNodeInnerIndex]?.input; |
| | this.widgetsPage.replaceChildren( |
| | ...items.map((oldName) => { |
| | return this.getEditElement("input", oldName, widgets[oldName], oldName, config?.[oldName]?.visible !== false); |
| | }) |
| | ); |
| | return !!items.length; |
| | } |
| |
|
| | buildInputsPage() { |
| | const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex]; |
| | const items = Object.keys(inputs ?? {}); |
| | const type = app.graph.extra.groupNodes[this.selectedGroup]; |
| | const config = type.config?.[this.selectedNodeInnerIndex]?.input; |
| | this.inputsPage.replaceChildren( |
| | ...items |
| | .map((oldName) => { |
| | let value = inputs[oldName]; |
| | if (!value) { |
| | return; |
| | } |
| |
|
| | return this.getEditElement("input", oldName, value, oldName, config?.[oldName]?.visible !== false); |
| | }) |
| | .filter(Boolean) |
| | ); |
| | return !!items.length; |
| | } |
| |
|
| | buildOutputsPage() { |
| | const nodes = this.groupData.nodeData.nodes; |
| | const innerNodeDef = this.groupData.getNodeDef(nodes[this.selectedNodeInnerIndex]); |
| | const outputs = innerNodeDef?.output ?? []; |
| | const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex]; |
| |
|
| | const type = app.graph.extra.groupNodes[this.selectedGroup]; |
| | const config = type.config?.[this.selectedNodeInnerIndex]?.output; |
| | const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex]; |
| | const checkable = node.type !== "PrimitiveNode"; |
| | this.outputsPage.replaceChildren( |
| | ...outputs |
| | .map((type, slot) => { |
| | const groupOutputIndex = groupOutputs?.[slot]; |
| | const oldName = innerNodeDef.output_name?.[slot] ?? type; |
| | let value = config?.[slot]?.name; |
| | const visible = config?.[slot]?.visible || groupOutputIndex != null; |
| | if (!value || value === oldName) { |
| | value = ""; |
| | } |
| | return this.getEditElement("output", slot, value, oldName, visible, checkable); |
| | }) |
| | .filter(Boolean) |
| | ); |
| | return !!outputs.length; |
| | } |
| |
|
| | show(type) { |
| | const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort((a, b) => a.localeCompare(b)); |
| |
|
| | this.innerNodesList = $el("ul.comfy-group-manage-list-items"); |
| | this.widgetsPage = $el("section.comfy-group-manage-node-page"); |
| | this.inputsPage = $el("section.comfy-group-manage-node-page"); |
| | this.outputsPage = $el("section.comfy-group-manage-node-page"); |
| | const pages = $el("div", [this.widgetsPage, this.inputsPage, this.outputsPage]); |
| |
|
| | this.tabs = [ |
| | ["Inputs", this.inputsPage], |
| | ["Widgets", this.widgetsPage], |
| | ["Outputs", this.outputsPage], |
| | ].reduce((p, [name, page]) => { |
| | p[name] = { |
| | tab: $el("a", { |
| | onclick: () => { |
| | this.changeTab(name); |
| | }, |
| | textContent: name, |
| | }), |
| | page, |
| | }; |
| | return p; |
| | }, {}); |
| |
|
| | const outer = $el("div.comfy-group-manage-outer", [ |
| | $el("header", [ |
| | $el("h2", "Group Nodes"), |
| | $el( |
| | "select", |
| | { |
| | onchange: (e) => { |
| | this.changeGroup(e.target.value); |
| | }, |
| | }, |
| | groupNodes.map((g) => |
| | $el("option", { |
| | textContent: g, |
| | selected: "workflow/" + g === type, |
| | value: g, |
| | }) |
| | ) |
| | ), |
| | ]), |
| | $el("main", [ |
| | $el("section.comfy-group-manage-list", this.innerNodesList), |
| | $el("section.comfy-group-manage-node", [ |
| | $el( |
| | "header", |
| | Object.values(this.tabs).map((t) => t.tab) |
| | ), |
| | pages, |
| | ]), |
| | ]), |
| | $el("footer", [ |
| | $el( |
| | "button.comfy-btn", |
| | { |
| | onclick: (e) => { |
| | const node = app.graph._nodes.find((n) => n.type === "workflow/" + this.selectedGroup); |
| | if (node) { |
| | alert("This group node is in use in the current workflow, please first remove these."); |
| | return; |
| | } |
| | if (confirm(`Are you sure you want to remove the node: "${this.selectedGroup}"`)) { |
| | delete app.graph.extra.groupNodes[this.selectedGroup]; |
| | LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup); |
| | } |
| | this.show(); |
| | }, |
| | }, |
| | "Delete Group Node" |
| | ), |
| | $el( |
| | "button.comfy-btn", |
| | { |
| | onclick: async () => { |
| | let nodesByType; |
| | let recreateNodes = []; |
| | const types = {}; |
| | for (const g in this.modifications) { |
| | const type = app.graph.extra.groupNodes[g]; |
| | let config = (type.config ??= {}); |
| |
|
| | let nodeMods = this.modifications[g]?.nodes; |
| | if (nodeMods) { |
| | const keys = Object.keys(nodeMods); |
| | if (nodeMods[keys[0]][ORDER]) { |
| | |
| | const orderedNodes = []; |
| | const orderedMods = {}; |
| | const orderedConfig = {}; |
| |
|
| | for (const n of keys) { |
| | const order = nodeMods[n][ORDER].order; |
| | orderedNodes[order] = type.nodes[+n]; |
| | orderedMods[order] = nodeMods[n]; |
| | orderedNodes[order].index = order; |
| | } |
| |
|
| | |
| | for (const l of type.links) { |
| | if (l[0] != null) l[0] = type.nodes[l[0]].index; |
| | if (l[2] != null) l[2] = type.nodes[l[2]].index; |
| | } |
| |
|
| | |
| | if (type.external) { |
| | for (const ext of type.external) { |
| | ext[0] = type.nodes[ext[0]]; |
| | } |
| | } |
| |
|
| | |
| | for (const id of keys) { |
| | if (config[id]) { |
| | orderedConfig[type.nodes[id].index] = config[id]; |
| | } |
| | delete config[id]; |
| | } |
| |
|
| | type.nodes = orderedNodes; |
| | nodeMods = orderedMods; |
| | type.config = config = orderedConfig; |
| | } |
| |
|
| | merge(config, nodeMods); |
| | } |
| |
|
| | types[g] = type; |
| |
|
| | if (!nodesByType) { |
| | nodesByType = app.graph._nodes.reduce((p, n) => { |
| | p[n.type] ??= []; |
| | p[n.type].push(n); |
| | return p; |
| | }, {}); |
| | } |
| |
|
| | const nodes = nodesByType["workflow/" + g]; |
| | if (nodes) recreateNodes.push(...nodes); |
| | } |
| |
|
| | await GroupNodeConfig.registerFromWorkflow(types, {}); |
| |
|
| | for (const node of recreateNodes) { |
| | node.recreate(); |
| | } |
| |
|
| | this.modifications = {}; |
| | this.app.graph.setDirtyCanvas(true, true); |
| | this.changeGroup(this.selectedGroup, false); |
| | }, |
| | }, |
| | "Save" |
| | ), |
| | $el("button.comfy-btn", { onclick: () => this.element.close() }, "Close"), |
| | ]), |
| | ]); |
| |
|
| | this.element.replaceChildren(outer); |
| | this.changeGroup(type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]); |
| | this.element.showModal(); |
| |
|
| | this.element.addEventListener("close", () => { |
| | this.draggable?.dispose(); |
| | }); |
| | } |
| | } |