refactor(groupNode): remove all @ts-expect-error suppressions

Amp-Thread-ID: https://ampcode.com/threads/T-019ba9df-6ee8-7541-9105-c657cd9ec692
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
DrJKL
2026-01-10 14:51:12 -08:00
parent 97ca9f489e
commit d23736b310
6 changed files with 376 additions and 300 deletions

View File

@@ -1,12 +1,19 @@
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import { t } from '@/i18n'
import type { GroupNodeWorkflowData } from '@/lib/litegraph/src/LGraph'
import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink'
import type {
GroupNodeInputConfig,
GroupNodeInputsSpec,
GroupNodeOutputType,
PartialLinkInfo
} from './groupNodeTypes'
import { LLink, type SerialisedLLinkArray } from '@/lib/litegraph/src/LLink'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import {
type ExecutableLGraphNode,
type ExecutionId,
type ISerialisedNode,
LGraphNode,
type LGraphNodeConstructor,
LiteGraph,
@@ -54,9 +61,8 @@ interface GroupNodeOutput {
interface GroupNodeData extends Omit<
GroupNodeWorkflowData['nodes'][number],
'inputs' | 'outputs'
'inputs' | 'outputs' | 'widgets_values'
> {
title?: string
widgets_values?: unknown[]
inputs?: GroupNodeInput[]
outputs?: GroupNodeOutput[]
@@ -241,7 +247,13 @@ export class GroupNodeConfig {
>
nodeInputs: Record<number, Record<string, string>>
outputVisibility: boolean[]
nodeDef: (ComfyNodeDef & { [GROUP]: GroupNodeConfig }) | undefined
nodeDef:
| (Omit<ComfyNodeDef, 'input' | 'output'> & {
input: GroupNodeInputsSpec
output: GroupNodeOutputType[]
[GROUP]: GroupNodeConfig
})
| undefined
inputs!: unknown[]
linksFrom!: LinksFromMap
linksTo!: LinksToMap
@@ -297,8 +309,11 @@ export class GroupNodeConfig {
}
this.#convertedToProcess = []
if (!this.nodeDef) return
await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, this.nodeDef)
useNodeDefStore().addNodeDef(this.nodeDef)
const finalizedDef = this.nodeDef as ComfyNodeDef & {
[GROUP]: GroupNodeConfig
}
await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, finalizedDef)
useNodeDefStore().addNodeDef(finalizedDef)
}
getLinks() {
@@ -520,9 +535,13 @@ export class GroupNodeConfig {
node: GroupNodeData,
inputName: string,
seenInputs: Record<string, number>,
config: unknown[],
inputConfig: unknown[],
extra?: Record<string, unknown>
) {
): {
name: string
config: GroupNodeInputConfig
customConfig: { name?: string; visible?: boolean } | undefined
} {
const nodeConfig = this.nodeData.config?.[node.index ?? -1] as
| NodeConfigEntry
| undefined
@@ -543,28 +562,34 @@ export class GroupNodeConfig {
}
seenInputs[key] = (seenInputs[key] ?? 1) + 1
const typeName = String(inputConfig[0])
let options =
typeof inputConfig[1] === 'object' && inputConfig[1] !== null
? (inputConfig[1] as Record<string, unknown>)
: undefined
if (inputName === 'seed' || inputName === 'noise_seed') {
if (!extra) extra = {}
extra.control_after_generate = `${prefix}control_after_generate`
}
if (config[0] === 'IMAGEUPLOAD') {
if (typeName === 'IMAGEUPLOAD') {
if (!extra) extra = {}
const nodeIndex = node.index ?? -1
const configOptions =
typeof config[1] === 'object' && config[1] !== null ? config[1] : {}
const widgetKey =
'widget' in configOptions && typeof configOptions.widget === 'string'
? configOptions.widget
options && 'widget' in options && typeof options.widget === 'string'
? options.widget
: 'image'
extra.widget = this.oldToNewWidgetMap[nodeIndex]?.[widgetKey] ?? 'image'
}
if (extra) {
const configObj =
typeof config[1] === 'object' && config[1] ? config[1] : {}
config = [config[0], { ...configObj, ...extra }]
options = { ...(options ?? {}), ...extra }
}
const config: GroupNodeInputConfig = options
? [typeName, options]
: [typeName]
return { name, config, customConfig }
}
@@ -608,7 +633,6 @@ export class GroupNodeConfig {
inputs[inputName] as unknown[]
)
if (this.nodeDef?.input?.required) {
// @ts-expect-error legacy dynamic input assignment
this.nodeDef.input.required[name] = config
}
widgetMap[inputName] = name
@@ -641,14 +665,15 @@ export class GroupNodeConfig {
unknown,
Record<string, unknown>
]
const output = { widget: primitiveConfig }
const output = { widget: primitiveConfig } as unknown as Parameters<
typeof mergeIfValid
>[0]
const config = mergeIfValid(
// @ts-expect-error slot type mismatch - legacy API
output,
targetWidget,
targetWidget as Parameters<typeof mergeIfValid>[1],
false,
undefined,
primitiveConfig
primitiveConfig as Parameters<typeof mergeIfValid>[4]
)
const inputConfig = inputs[inputName]?.[1]
primitiveConfig[1] =
@@ -713,7 +738,6 @@ export class GroupNodeConfig {
if (customConfig?.visible === false) continue
if (this.nodeDef?.input?.required) {
// @ts-expect-error legacy dynamic input assignment
this.nodeDef.input.required[name] = config
}
inputMap[i] = this.inputCount++
@@ -757,7 +781,6 @@ export class GroupNodeConfig {
)
if (this.nodeDef?.input?.required) {
// @ts-expect-error legacy dynamic input assignment
this.nodeDef.input.required[name] = config
}
this.newToOldWidgetMap[name] = { node, inputName }
@@ -851,8 +874,7 @@ export class GroupNodeConfig {
node,
slot: outputId
}
// @ts-expect-error legacy dynamic output type assignment
this.nodeDef.output.push(defOutput[outputId])
this.nodeDef.output.push(defOutput[outputId] as GroupNodeOutputType)
this.nodeDef.output_is_list?.push(
def.output_is_list?.[outputId] ?? false
)
@@ -951,8 +973,13 @@ export class GroupNodeHandler {
for (const w of innerNode.widgets ?? []) {
if (w.type === 'converted-widget') {
// @ts-expect-error legacy widget property for converted widgets
w.serializeValue = w.origSerializeValue
type SerializeValueFn = (node: LGraphNode, index: number) => unknown
const convertedWidget = w as typeof w & {
origSerializeValue?: SerializeValueFn
}
if (convertedWidget.origSerializeValue) {
w.serializeValue = convertedWidget.origSerializeValue
}
}
}
@@ -978,20 +1005,18 @@ export class GroupNodeHandler {
return inputNode
}
// @ts-expect-error returns partial link object, not full LLink
innerNode.getInputLink = (slot: number) => {
innerNode.getInputLink = ((slot: number): PartialLinkInfo | null => {
const nodeIdx = innerNode.index ?? 0
const externalSlot = this.groupData.oldToNewInputMap[nodeIdx]?.[slot]
if (externalSlot != null) {
// The inner node is connected via the group node inputs
const linkId = this.node.inputs[externalSlot].link
if (linkId == null) return null
const existingLink = app.rootGraph.links[linkId]
if (!existingLink) return null
// Use the outer link, but update the target to the inner node
return {
...existingLink,
origin_id: existingLink.origin_id,
origin_slot: existingLink.origin_slot,
target_id: innerNode.id,
target_slot: +slot
}
@@ -1001,21 +1026,18 @@ export class GroupNodeHandler {
if (!innerLink) return null
const linkSrcIdx = innerLink[0]
if (linkSrcIdx == null) return null
// Use the inner link, but update the origin node to be inner node id
return {
origin_id: innerNodes[Number(linkSrcIdx)].id,
origin_slot: innerLink[1],
target_id: innerNode.id,
target_slot: +slot
}
}
}) as typeof innerNode.getInputLink
}
}
this.node.updateLink = (link) => {
// Replace the group node reference with the internal node
// @ts-expect-error Can this be removed? Or replaced with: LLink.create(link.asSerialisable())
link = { ...link }
this.node.updateLink = (inputLink) => {
const link = LLink.create(inputLink.asSerialisable())
const output = this.groupData.newToOldOutputMap[link.origin_slot]
if (!output || !this.innerNodes) return null
const nodeIdx = output.node.index ?? 0
@@ -1063,8 +1085,7 @@ export class GroupNodeHandler {
if (!n.type) return null
const innerNode = LiteGraph.createNode(n.type)
if (!innerNode) return null
// @ts-expect-error legacy node data format used for configure
innerNode.configure(n)
innerNode.configure(n as ISerialisedNode)
innerNode.id = `${this.node.id}:${i}`
innerNode.graph = this.node.graph
return innerNode
@@ -1085,10 +1106,9 @@ export class GroupNodeHandler {
for (const node of this.innerNodes ?? []) {
node.graph ??= this.node.graph
// Create minimal DTOs rather than cloning the node
const currentId = String(node.id)
// @ts-expect-error temporary id reassignment for DTO creation
node.id = currentId.split(':').at(-1)
const shortId = currentId.split(':').at(-1) ?? currentId
node.id = shortId
const aVeryRealNode = new ExecutableGroupNodeChildDTO(
node,
subgraphInstanceIdPath,
@@ -1103,7 +1123,6 @@ export class GroupNodeHandler {
return nodes
}
// @ts-expect-error recreate returns null if creation fails
this.node.recreate = async () => {
const id = this.node.id
const sz = this.node.size
@@ -1139,11 +1158,9 @@ export class GroupNodeHandler {
this.node as LGraphNode & { convertToNodes: () => LGraphNode[] }
).convertToNodes = () => {
const addInnerNodes = () => {
// Clone the node data so we dont mutate it for other nodes
const c = { ...this.groupData.nodeData }
c.nodes = [...c.nodes]
// @ts-expect-error getInnerNodes called without args in legacy conversion code
const innerNodes = this.node.getInnerNodes?.()
const innerNodes = this.innerNodes
const ids: (string | number)[] = []
for (let i = 0; i < c.nodes.length; i++) {
let id: string | number | undefined = innerNodes?.[i]?.id
@@ -1153,7 +1170,6 @@ export class GroupNodeHandler {
} else {
ids.push(id)
}
// @ts-expect-error adding id to node copy for serialization
c.nodes[i] = { ...c.nodes[i], id }
}
deserialiseAndCreate(JSON.stringify(c), app.canvas)
@@ -1182,7 +1198,6 @@ export class GroupNodeHandler {
if (!newNode.widgets || !innerNode) continue
// @ts-expect-error index property access on ExecutableLGraphNode
const map = this.groupData.oldToNewWidgetMap[innerNode.index ?? 0]
if (map) {
const widgets = Object.keys(map)
@@ -1305,14 +1320,13 @@ export class GroupNodeHandler {
null,
{
content: 'Convert to nodes',
// @ts-expect-error async callback not expected by legacy menu API
callback: async () => {
const convertFn = (
handlerNode as LGraphNode & {
convertToNodes?: () => LGraphNode[]
}
).convertToNodes
return convertFn?.()
convertFn?.()
}
},
{
@@ -1519,7 +1533,6 @@ export class GroupNodeHandler {
if (!innerNode) continue
if (innerNode.type === 'PrimitiveNode') {
// @ts-expect-error primitiveValue is a custom property on PrimitiveNode
innerNode.primitiveValue = newValue
const primitiveLinked = this.groupData.primitiveToWidget[nodeIdx]
for (const linked of primitiveLinked ?? []) {
@@ -1748,12 +1761,7 @@ export class GroupNodeHandler {
this.groupData.oldToNewInputMap[Number(targetId)]?.[Number(targetSlot)]
if (mappedSlot == null) continue
if (typeof originSlot === 'number' || typeof originSlot === 'string') {
originNode.connect(
originSlot,
// @ts-expect-error Valid - uses deprecated interface (node ID instead of node reference)
this.node.id,
mappedSlot
)
originNode.connect(originSlot, this.node, mappedSlot)
}
}
}
@@ -1783,13 +1791,13 @@ export class GroupNodeHandler {
}
static getHandler(node: LGraphNode): GroupNodeHandler | undefined {
// @ts-expect-error GROUP symbol indexing on LGraphNode
let handler = node[GROUP] as GroupNodeHandler | undefined
// Handler may not be set yet if nodeCreated async hook hasn't run
// Create it synchronously if needed
type GroupNodeWithHandler = LGraphNode & {
[GROUP]?: GroupNodeHandler
}
let handler = (node as GroupNodeWithHandler)[GROUP]
if (!handler && GroupNodeHandler.isGroupNode(node)) {
handler = new GroupNodeHandler(node)
;(node as LGraphNode & { [GROUP]: GroupNodeHandler })[GROUP] = handler
;(node as GroupNodeWithHandler)[GROUP] = handler
}
return handler
}
@@ -1948,8 +1956,9 @@ const ext: ComfyExtension = {
items.push({
content: `Convert to Group Node (Deprecated)`,
disabled: !convertEnabled,
// @ts-expect-error async callback - legacy menu API doesn't expect Promise
callback: async () => convertSelectedNodesToGroupNode()
callback: async () => {
await convertSelectedNodesToGroupNode()
}
})
const groups = canvas.graph?.extra?.groupNodes
@@ -1975,8 +1984,9 @@ const ext: ComfyExtension = {
{
content: `Convert to Group Node (Deprecated)`,
disabled: !convertEnabled,
// @ts-expect-error async callback - legacy menu API doesn't expect Promise
callback: async () => convertSelectedNodesToGroupNode()
callback: async () => {
await convertSelectedNodesToGroupNode()
}
}
]
},

View File

@@ -1,5 +1,8 @@
import { merge } from 'es-toolkit'
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import {
type GroupNodeWorkflowData,
type LGraphNode,
type LGraphNodeConstructor,
LiteGraph
@@ -13,70 +16,56 @@ import { DraggableList } from '../../scripts/ui/draggableList'
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
import './groupNodeManage.css'
const ORDER: symbol = Symbol()
const ORDER: unique symbol = Symbol('ORDER')
// @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
interface NodeModification {
name?: string
visible?: boolean
}
interface OrderModification {
order: number
}
type NodeModifications = Record<string, NodeModification> & {
[ORDER]?: OrderModification
}
type DragEndEvent = CustomEvent<{
element: Element
oldPosition: number
newPosition: number
}>
export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
// @ts-expect-error fixme ts strict error
tabs: Record<
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[]
modifications: Record<string, { nodes?: Record<string, NodeModifications> }> =
{}
nodeItems: HTMLLIElement[] = []
app: ComfyApp
// @ts-expect-error fixme ts strict error
groupNodeType: LGraphNodeConstructor<LGraphNode>
groupNodeDef: any
groupData: any
groupNodeType!: LGraphNodeConstructor<LGraphNode>
groupNodeDef: unknown
groupData: ReturnType<typeof GroupNodeHandler.getGroupData> | null = null
// @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
innerNodesList!: HTMLUListElement
widgetsPage!: HTMLElement
inputsPage!: HTMLElement
outputsPage!: HTMLElement
draggable: DraggableList | null = null
get selectedNodeInnerIndex() {
// @ts-expect-error fixme ts strict error
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex
get selectedNodeInnerIndex(): number {
if (this.selectedNodeIndex == null) return 0
const item = this.nodeItems[this.selectedNodeIndex]
return +(item?.dataset?.nodeindex ?? 0)
}
// @ts-expect-error fixme ts strict error
constructor(app) {
constructor(app: ComfyApp) {
super()
this.app = app
this.element = $el('dialog.comfy-group-manage', {
@@ -84,19 +73,15 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
}) as HTMLDialogElement
}
// @ts-expect-error fixme ts strict error
changeTab(tab) {
changeTab(tab: keyof ManageGroupDialog['tabs']) {
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?) {
changeNode(index: number, force?: boolean) {
if (!force && this.selectedNodeIndex === index) return
if (this.selectedNodeIndex != null) {
@@ -126,19 +111,17 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)
}
// @ts-expect-error fixme ts strict error
changeGroup(group, reset = true) {
changeGroup(group: string, reset = true) {
this.selectedGroup = group
this.getGroupData()
const nodes = this.groupData.nodeData.nodes
// @ts-expect-error fixme ts strict error
const nodes = this.groupData?.nodeData.nodes ?? []
this.nodeItems = nodes.map((n, i) =>
$el(
'li.draggable-item',
{
dataset: {
nodeindex: n.index + ''
nodeindex: String(n.index ?? i)
},
onclick: () => {
this.changeNode(i)
@@ -159,7 +142,7 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
)
]
)
)
) as HTMLLIElement[]
this.innerNodesList.replaceChildren(...this.nodeItems)
@@ -167,63 +150,76 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
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
const items = this.draggable?.getAllItems() ?? []
let index = items.findIndex((item: Element) =>
item.classList.contains('selected')
)
if (index === -1) index = this.selectedNodeIndex ?? 0
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
})
}
this.draggable.addEventListener('dragend', (e: Event) => {
const detail = (e as DragEndEvent).detail
const { oldPosition, newPosition } = detail
if (oldPosition === newPosition) return
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0])
for (let i = 0; i < ordered.length; i++) {
const nodeIndex = ordered[i].index
if (nodeIndex == null) continue
this.storeModification({
nodeIndex,
section: ORDER,
prop: 'order',
value: i
})
}
)
})
}
storeModification(props: {
nodeIndex?: number
section: symbol
section: string | typeof ORDER
prop: string
value: any
value: unknown
}) {
const { nodeIndex, section, prop, value } = props
// @ts-expect-error fixme ts strict error
if (!this.selectedGroup) return
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)
const nodeKey = String(nodeIndex ?? this.selectedNodeInnerIndex)
const nodeMod = (nodesMod[nodeKey] ??= {} as NodeModifications)
if (section === ORDER) {
nodeMod[ORDER] = { order: value as number }
} else {
typeMod[prop] = value
const sectionMod = (nodeMod[section] ??= {})
if (typeof value === 'object' && value !== null) {
Object.assign(sectionMod, value)
} else {
Object.assign(sectionMod, { [prop]: value })
}
}
}
// @ts-expect-error fixme ts strict error
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
getEditElement(
section: string,
prop: string | number,
value: string,
placeholder: string,
checked: boolean,
checkable = true
) {
if (value === placeholder) value = ''
const mods =
// @ts-expect-error fixme ts strict error
this.modifications[this.selectedGroup]?.nodes?.[
this.selectedNodeInnerIndex
]?.[section]?.[prop]
const mods = this.selectedGroup
? this.modifications[this.selectedGroup]?.nodes?.[
this.selectedNodeInnerIndex
]?.[section]
: undefined
if (mods) {
if (mods.name != null) {
value = mods.name
@@ -238,12 +234,11 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
value,
placeholder,
type: 'text',
// @ts-expect-error fixme ts strict error
onchange: (e) => {
onchange: (e: Event) => {
this.storeModification({
section,
prop,
value: { name: e.target.value }
prop: String(prop),
value: { name: (e.target as HTMLInputElement).value }
})
}
}),
@@ -252,12 +247,11 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
type: 'checkbox',
checked,
disabled: !checkable,
// @ts-expect-error fixme ts strict error
onchange: (e) => {
onchange: (e: Event) => {
this.storeModification({
section,
prop,
value: { visible: !!e.target.checked }
prop: String(prop),
value: { visible: !!(e.target as HTMLInputElement).checked }
})
}
})
@@ -267,17 +261,22 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
buildWidgetsPage() {
const widgets =
this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]
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
const type = this.selectedGroup
? app.rootGraph.extra?.groupNodes?.[this.selectedGroup]
: undefined
const config = (
type?.config as
| Record<number, { input?: Record<string, { visible?: boolean }> }>
| undefined
)?.[this.selectedNodeInnerIndex]?.input
this.widgetsPage.replaceChildren(
...items.map((oldName) => {
return this.getEditElement(
'input',
oldName,
widgets[oldName],
widgets?.[oldName] ?? '',
oldName,
config?.[oldName]?.visible !== false
)
@@ -287,54 +286,68 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
}
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
}
const inputs = this.groupData?.nodeInputs[this.selectedNodeInnerIndex] ?? {}
const items = Object.keys(inputs)
const type = this.selectedGroup
? app.rootGraph.extra?.groupNodes?.[this.selectedGroup]
: undefined
const config = (
type?.config as
| Record<number, { input?: Record<string, { visible?: boolean }> }>
| undefined
)?.[this.selectedNodeInnerIndex]?.input
const filteredElements = items
.map((oldName) => {
const value = inputs[oldName]
if (!value) {
return null
}
return this.getEditElement(
'input',
oldName,
value,
oldName,
config?.[oldName]?.visible !== false
)
})
.filter(Boolean)
)
return this.getEditElement(
'input',
oldName,
value as string,
oldName,
config?.[oldName]?.visible !== false
)
})
.filter((el): el is HTMLDivElement => el !== null)
this.inputsPage.replaceChildren(...filteredElements)
return !!items.length
}
buildOutputsPage() {
const nodes = this.groupData.nodeData.nodes
const innerNodeDef = this.groupData.getNodeDef(
nodes[this.selectedNodeInnerIndex]
)
const outputs = innerNodeDef?.output ?? []
const nodes = this.groupData?.nodeData.nodes ?? []
const nodeData = nodes[this.selectedNodeInnerIndex]
const innerNodeDef = nodeData
? this.groupData?.getNodeDef(
nodeData as Parameters<typeof this.groupData.getNodeDef>[0]
)
: undefined
const outputs = (innerNodeDef?.output ?? []) as string[]
const groupOutputs =
this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex]
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'
const workflowType = this.selectedGroup
? app.rootGraph.extra?.groupNodes?.[this.selectedGroup]
: undefined
const config = (
workflowType?.config as
| Record<
number,
{ output?: Record<number, { name?: string; visible?: boolean }> }
>
| undefined
)?.[this.selectedNodeInnerIndex]?.output
const node = nodes[this.selectedNodeInnerIndex]
const checkable = node?.type !== 'PrimitiveNode'
this.outputsPage.replaceChildren(
...outputs
// @ts-expect-error fixme ts strict error
.map((type, slot) => {
.map((outputType: string, slot: number) => {
const groupOutputIndex = groupOutputs?.[slot]
const oldName = innerNodeDef.output_name?.[slot] ?? type
let value = config?.[slot]?.name
const oldName = (innerNodeDef?.output_name?.[slot] ??
outputType) as string
let value = config?.[slot]?.name ?? ''
const visible = config?.[slot]?.visible || groupOutputIndex != null
if (!value || value === oldName) {
value = ''
@@ -353,8 +366,7 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
return !!outputs.length
}
// @ts-expect-error fixme ts strict error
show(type?) {
override show(type?: string) {
const groupNodes = Object.keys(app.rootGraph.extra?.groupNodes ?? {}).sort(
(a, b) => a.localeCompare(b)
)
@@ -371,24 +383,28 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
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
type TabName = keyof ManageGroupDialog['tabs']
this.tabs = (
[
['Inputs', this.inputsPage],
['Widgets', this.widgetsPage],
['Outputs', this.outputsPage]
] as [TabName, HTMLElement][]
).reduce(
(p, [name, page]) => {
p[name] = {
tab: $el('a', {
onclick: () => {
this.changeTab(name)
},
textContent: name
}) as HTMLAnchorElement,
page
}
return p
},
{} as ManageGroupDialog['tabs']
)
const outer = $el('div.comfy-group-manage-outer', [
$el('header', [
@@ -396,9 +412,8 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
$el(
'select',
{
// @ts-expect-error fixme ts strict error
onchange: (e) => {
this.changeGroup(e.target.value)
onchange: (e: Event) => {
this.changeGroup((e.target as HTMLSelectElement).value)
}
},
groupNodes.map((g) =>
@@ -439,8 +454,9 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
`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]
if (this.selectedGroup && app.rootGraph.extra?.groupNodes) {
delete app.rootGraph.extra.groupNodes[this.selectedGroup]
}
LiteGraph.unregisterNodeType(
`${PREFIX}${SEPARATOR}` + this.selectedGroup
)
@@ -454,97 +470,105 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
'button.comfy-btn',
{
onclick: async () => {
let nodesByType
let recreateNodes = []
const types = {}
let nodesByType: Record<string, LGraphNode[]> | null = null
const recreateNodes: LGraphNode[] = []
const types: Record<string, GroupNodeWorkflowData> = {}
for (const g in this.modifications) {
// @ts-expect-error fixme ts strict error
const type = app.rootGraph.extra.groupNodes[g]
let config = (type.config ??= {})
const groupNodeData = app.rootGraph.extra?.groupNodes?.[g]
if (!groupNodeData) continue
let config = (groupNodeData.config ??= {}) as Record<
number,
unknown
>
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]) {
const firstMod = nodeMods[keys[0]]
if (firstMod?.[ORDER]) {
// If any node is reordered, they will all need sequencing
const orderedNodes = []
const orderedMods = {}
const orderedConfig = {}
const orderedNodes: typeof groupNodeData.nodes = []
const orderedMods: Record<string, NodeModifications> = {}
const orderedConfig: Record<number, unknown> = {}
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
const order = nodeMods[n]?.[ORDER]?.order ?? 0
orderedNodes[order] = groupNodeData.nodes[+n]
orderedMods[order] = nodeMods[n]
orderedNodes[order].index = order
}
// Rewrite links
for (const l of type.links) {
// @ts-expect-error l[0]/l[2] used as node index
if (l[0] != null) l[0] = type.nodes[l[0]].index
// @ts-expect-error l[0]/l[2] used as node index
if (l[2] != null) l[2] = type.nodes[l[2]].index
for (const l of groupNodeData.links) {
const srcIdx = l[1]
const tgtIdx = l[3]
if (srcIdx != null)
l[1] =
groupNodeData.nodes[srcIdx as number]?.index ?? srcIdx
if (tgtIdx != null)
l[3] =
groupNodeData.nodes[tgtIdx as number]?.index ?? tgtIdx
}
// Rewrite externals
if (type.external) {
for (const ext of type.external) {
if (groupNodeData.external) {
for (const ext of groupNodeData.external) {
if (ext[0] != null) {
// @ts-expect-error ext[0] used as node index
ext[0] = type.nodes[ext[0]].index
ext[0] =
groupNodeData.nodes[ext[0] as number]?.index ??
ext[0]
}
}
}
// Rewrite modifications
for (const id of keys) {
// @ts-expect-error id used as node index
if (config[id]) {
// @ts-expect-error fixme ts strict error
orderedConfig[type.nodes[id].index] = config[id]
const nodeIdx = +id
if (config[nodeIdx]) {
const newIdx =
groupNodeData.nodes[nodeIdx]?.index ?? nodeIdx
orderedConfig[newIdx] = config[nodeIdx]
}
// @ts-expect-error id used as config key
delete config[id]
delete config[nodeIdx]
}
type.nodes = orderedNodes
groupNodeData.nodes = orderedNodes
nodeMods = orderedMods
type.config = config = orderedConfig
groupNodeData.config = config = orderedConfig
}
merge(config, nodeMods)
merge(config, nodeMods as Record<string, unknown>)
}
// @ts-expect-error fixme ts strict error
types[g] = type
types[g] = groupNodeData
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
}, {})
nodesByType = app.rootGraph.nodes.reduce(
(p, n) => {
const nodeType = n.type ?? ''
;(p[nodeType] ??= []).push(n)
return p
},
{} as Record<string, LGraphNode[]>
)
}
// @ts-expect-error fixme ts strict error
const nodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
if (nodes) recreateNodes.push(...nodes)
const groupTypeNodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
if (groupTypeNodes) recreateNodes.push(...groupTypeNodes)
}
await GroupNodeConfig.registerFromWorkflow(types, [])
for (const node of recreateNodes) {
node.recreate()
;(node as LGraphNode & { recreate?: () => void }).recreate?.()
}
this.modifications = {}
this.app.canvas.setDirty(true, true)
this.changeGroup(this.selectedGroup, false)
if (this.selectedGroup) {
this.changeGroup(this.selectedGroup, false)
}
}
},
'Save'

View File

@@ -0,0 +1,47 @@
import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink'
import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
/** Serialized node data within a group node workflow, with group-specific index */
export interface GroupNodeSerializedNode extends Partial<ISerialisedNode> {
/** Position of this node within the group */
index?: number
}
export interface GroupNodeWorkflowData {
external: (number | string)[][]
links: SerialisedLLinkArray[]
nodes: GroupNodeSerializedNode[]
config?: Record<number, unknown>
}
/**
* Input config tuple type for group nodes.
* First element is the input type name (e.g. 'INT', 'FLOAT', 'MODEL', etc.)
* Second element (optional) is the input options object.
*/
export type GroupNodeInputConfig = [string, Record<string, unknown>?]
/**
* Mutable inputs specification for group nodes that are built dynamically.
* Uses a more permissive type than ComfyInputsSpec to allow dynamic assignment.
*/
export interface GroupNodeInputsSpec {
required: Record<string, GroupNodeInputConfig>
optional?: Record<string, GroupNodeInputConfig>
}
/**
* Output type for group nodes - can be a type string or an array of combo options.
*/
export type GroupNodeOutputType = string | (string | number)[]
/**
* Partial link info used internally by group node getInputLink override.
* Contains only the properties needed for group node execution context.
*/
export interface PartialLinkInfo {
origin_id: string | number
origin_slot: number | string
target_id: string | number
target_slot: number
}

View File

@@ -15,7 +15,7 @@ import { LGraphGroup } from './LGraphGroup'
import { LGraphNode } from './LGraphNode'
import type { NodeId } from './LGraphNode'
import { LLink } from './LLink'
import type { LinkId, SerialisedLLinkArray } from './LLink'
import type { LinkId } from './LLink'
import { MapProxyHandler } from './MapProxyHandler'
import { Reroute } from './Reroute'
import type { RerouteId } from './Reroute'
@@ -63,6 +63,7 @@ import type {
LGraphTriggerHandler,
LGraphTriggerParam
} from './types/graphTriggers'
import type { GroupNodeWorkflowData } from '@/extensions/core/groupNodeTypes'
import type {
ExportedSubgraph,
ExposedWidget,
@@ -74,6 +75,8 @@ import type {
} from './types/serialisation'
import { getAllNestedItems } from './utils/collections'
export type { GroupNodeWorkflowData } from '@/extensions/core/groupNodeTypes'
export type {
LGraphTriggerAction,
LGraphTriggerParam
@@ -102,18 +105,6 @@ export interface LGraphConfig {
links_ontop?: any
}
export interface GroupNodeWorkflowData {
external: (number | string)[][]
links: SerialisedLLinkArray[]
nodes: {
index?: number
type?: string
inputs?: unknown[]
outputs?: unknown[]
}[]
config?: Record<number, unknown>
}
export interface LGraphExtra extends Dictionary<unknown> {
reroutes?: SerialisableReroute[]
linkExtensions?: { id: number; parentId: number | undefined }[]

View File

@@ -103,10 +103,12 @@ export type {
Size
} from './interfaces'
export {
type GroupNodeWorkflowData,
LGraph,
type LGraphTriggerAction,
type LGraphTriggerParam
} from './LGraph'
export type { LGraphTriggerEvent } from './types/graphTriggers'
export { BadgePosition, LGraphBadge } from './LGraphBadge'
export { LGraphCanvas } from './LGraphCanvas'

View File

@@ -113,7 +113,7 @@ declare module '@/lib/litegraph/src/litegraph' {
): ExecutableLGraphNode[]
/** @deprecated groupNode */
convertToNodes?(): LGraphNode[]
recreate?(): Promise<LGraphNode>
recreate?(): Promise<LGraphNode | null>
refreshComboInNode?(defs: Record<string, ComfyNodeDef>)
/** @deprecated groupNode */
updateLink?(link: LLink): LLink | null
@@ -143,6 +143,8 @@ declare module '@/lib/litegraph/src/litegraph' {
index?: number
runningInternalNodeId?: NodeId
/** @deprecated Used by PrimitiveNode for group node value propagation */
primitiveValue?: unknown
comfyClass?: string