Files
ComfyUI_frontend/src/extensions/core/groupNodeManage.ts
AustinMroz f2a0e5102e Cleanup app.graph usage (#7399)
Prior to the release of subgraphs, there was a single graph accessed
through `app.graph`. Now that there's multiple graphs, there's a lot of
code that needs to be reviewed and potentially updated depending on if
it cares about nearby nodes, all nodes, or something else requiring
specific attention.

This was done by simply changing the type of `app.graph` to unknown so
the typechecker will complain about every place it's currently used.
References were then updated to `app.rootGraph` if the previous usage
was correct, or actually rewritten.

By not getting rid of `app.graph`, this change already ensures that
there's no loss of functionality for custom nodes, but the prior typing
of `app.graph` can always be restored if future dissuasion of
`app.graph` usage creates issues.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7399-Cleanup-app-graph-usage-2c76d73d365081178743dfdcf07f44d0)
by [Unito](https://www.unito.io)
2025-12-11 23:37:34 -07:00

568 lines
17 KiB
TypeScript

import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import {
type LGraphNode,
type LGraphNodeConstructor,
LiteGraph
} from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { type ComfyApp, app } from '../../scripts/app'
import { $el } from '../../scripts/ui'
import { ComfyDialog } from '../../scripts/ui/dialog'
import { DraggableList } from '../../scripts/ui/draggableList'
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
import './groupNodeManage.css'
const ORDER: symbol = Symbol()
// @ts-expect-error fixme ts strict error
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<HTMLDialogElement> {
// @ts-expect-error fixme ts strict error
tabs: Record<
'Inputs' | 'Outputs' | 'Widgets',
{ tab: HTMLAnchorElement; page: HTMLElement }
>
selectedNodeIndex: number | null | undefined
selectedTab: keyof ManageGroupDialog['tabs'] = 'Inputs'
selectedGroup: string | undefined
modifications: Record<
string,
Record<
string,
Record<
string,
{ name?: string | undefined; visible?: boolean | undefined }
>
>
> = {}
// @ts-expect-error fixme ts strict error
nodeItems: any[]
app: ComfyApp
// @ts-expect-error fixme ts strict error
groupNodeType: LGraphNodeConstructor<LGraphNode>
groupNodeDef: any
groupData: any
// @ts-expect-error fixme ts strict error
innerNodesList: HTMLUListElement
// @ts-expect-error fixme ts strict error
widgetsPage: HTMLElement
// @ts-expect-error fixme ts strict error
inputsPage: HTMLElement
// @ts-expect-error fixme ts strict error
outputsPage: HTMLElement
draggable: any
get selectedNodeInnerIndex() {
// @ts-expect-error fixme ts strict error
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex
}
// @ts-expect-error fixme ts strict error
constructor(app) {
super()
this.app = app
this.element = $el('dialog.comfy-group-manage', {
parent: document.body
}) as HTMLDialogElement
}
// @ts-expect-error fixme ts strict error
changeTab(tab) {
this.tabs[this.selectedTab].tab.classList.remove('active')
this.tabs[this.selectedTab].page.classList.remove('active')
// @ts-expect-error fixme ts strict error
this.tabs[tab].tab.classList.add('active')
// @ts-expect-error fixme ts strict error
this.tabs[tab].page.classList.add('active')
this.selectedTab = tab
}
// @ts-expect-error fixme ts strict error
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[
`${PREFIX}${SEPARATOR}` + this.selectedGroup
] as unknown as LGraphNodeConstructor<LGraphNode>
this.groupNodeDef = this.groupNodeType.nodeData
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)
}
// @ts-expect-error fixme ts strict error
changeGroup(group, reset = true) {
this.selectedGroup = group
this.getGroupData()
const nodes = this.groupData.nodeData.nodes
// @ts-expect-error fixme ts strict error
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()
// @ts-expect-error fixme ts strict error
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',
// @ts-expect-error fixme ts strict error
({ 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(props: {
nodeIndex?: number
section: symbol
prop: string
value: any
}) {
const { nodeIndex, section, prop, value } = props
// @ts-expect-error fixme ts strict error
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
}
}
// @ts-expect-error fixme ts strict error
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
if (value === placeholder) value = ''
const mods =
// @ts-expect-error fixme ts strict error
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',
// @ts-expect-error fixme ts strict error
onchange: (e) => {
this.storeModification({
section,
prop,
value: { name: e.target.value }
})
}
}),
$el('label', { textContent: 'Visible' }, [
$el('input', {
type: 'checkbox',
checked,
disabled: !checkable,
// @ts-expect-error fixme ts strict error
onchange: (e) => {
this.storeModification({
section,
prop,
value: { visible: !!e.target.checked }
})
}
})
])
])
}
buildWidgetsPage() {
const widgets =
this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]
const items = Object.keys(widgets ?? {})
// @ts-expect-error fixme ts strict error
const type = app.rootGraph.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 ?? {})
// @ts-expect-error fixme ts strict error
const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
const config = type.config?.[this.selectedNodeInnerIndex]?.input
this.inputsPage.replaceChildren(
// @ts-expect-error fixme ts strict error
...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]
// @ts-expect-error fixme ts strict error
const type = app.rootGraph.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
// @ts-expect-error fixme ts strict error
.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
}
// @ts-expect-error fixme ts strict error
show(type?) {
const groupNodes = Object.keys(app.rootGraph.extra?.groupNodes ?? {}).sort(
(a, b) => a.localeCompare(b)
)
this.innerNodesList = $el(
'ul.comfy-group-manage-list-items'
) as HTMLUListElement
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]
// @ts-expect-error fixme ts strict error
].reduce((p, [name, page]: [string, HTMLElement]) => {
// @ts-expect-error fixme ts strict error
p[name] = {
tab: $el('a', {
onclick: () => {
this.changeTab(name)
},
textContent: name
}),
page
}
return p
}, {}) as any
const outer = $el('div.comfy-group-manage-outer', [
$el('header', [
$el('h2', 'Group Nodes'),
$el(
'select',
{
// @ts-expect-error fixme ts strict error
onchange: (e) => {
this.changeGroup(e.target.value)
}
},
groupNodes.map((g) =>
$el('option', {
textContent: g,
selected: `${PREFIX}${SEPARATOR}${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: () => {
const node = app.rootGraph.nodes.find(
(n) => n.type === `${PREFIX}${SEPARATOR}` + this.selectedGroup
)
if (node) {
useToastStore().addAlert(
'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}"`
)
) {
// @ts-expect-error fixme ts strict error
delete app.rootGraph.extra.groupNodes[this.selectedGroup]
LiteGraph.unregisterNodeType(
`${PREFIX}${SEPARATOR}` + 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) {
// @ts-expect-error fixme ts strict error
const type = app.rootGraph.extra.groupNodes[g]
let config = (type.config ??= {})
let nodeMods = this.modifications[g]?.nodes
if (nodeMods) {
const keys = Object.keys(nodeMods)
// @ts-expect-error fixme ts strict error
if (nodeMods[keys[0]][ORDER]) {
// If any node is reordered, they will all need sequencing
const orderedNodes = []
const orderedMods = {}
const orderedConfig = {}
for (const n of keys) {
// @ts-expect-error fixme ts strict error
const order = nodeMods[n][ORDER].order
orderedNodes[order] = type.nodes[+n]
// @ts-expect-error fixme ts strict error
orderedMods[order] = nodeMods[n]
orderedNodes[order].index = order
}
// Rewrite links
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
}
// Rewrite externals
if (type.external) {
for (const ext of type.external) {
ext[0] = type.nodes[ext[0]]
}
}
// Rewrite modifications
for (const id of keys) {
if (config[id]) {
// @ts-expect-error fixme ts strict error
orderedConfig[type.nodes[id].index] = config[id]
}
delete config[id]
}
type.nodes = orderedNodes
nodeMods = orderedMods
type.config = config = orderedConfig
}
merge(config, nodeMods)
}
// @ts-expect-error fixme ts strict error
types[g] = type
if (!nodesByType) {
nodesByType = app.rootGraph.nodes.reduce((p, n) => {
// @ts-expect-error fixme ts strict error
p[n.type] ??= []
// @ts-expect-error fixme ts strict error
p[n.type].push(n)
return p
}, {})
}
// @ts-expect-error fixme ts strict error
const nodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
if (nodes) recreateNodes.push(...nodes)
}
await GroupNodeConfig.registerFromWorkflow(types, {})
for (const node of recreateNodes) {
node.recreate()
}
this.modifications = {}
this.app.canvas.setDirty(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) => `${PREFIX}${SEPARATOR}${g}` === type) ??
groupNodes[0])
: groupNodes[0]
)
this.element.showModal()
this.element.addEventListener('close', () => {
this.draggable?.dispose()
this.element.remove()
})
}
}