Fix/widget ordering consistency (#5106)

* feat: input ordered nodes

* fix: ensure node input order upon creation using input_order

* refactor: back to the original state of migrations.ts

* refactor: remove console.logs

* test: fix widget ordering tests

* fix: any types
This commit is contained in:
Simula_r
2025-08-20 11:07:40 -07:00
committed by GitHub
parent 180f95182d
commit 1e9d4c7c37
7 changed files with 572 additions and 6 deletions

View File

@@ -187,7 +187,7 @@ export { LGraphButton, type LGraphButtonOptions } from './LGraphButton'
export { MovingOutputLink } from './canvas/MovingOutputLink'
export { ToOutputRenderLink } from './canvas/ToOutputRenderLink'
export { ToInputFromIoNodeLink } from './canvas/ToInputFromIoNodeLink'
export type { TWidgetType, IWidgetOptions } from './types/widgets'
export type { TWidgetType, TWidgetValue, IWidgetOptions } from './types/widgets'
export {
findUsedSubgraphIds,
getDirectSubgraphIds,

View File

@@ -232,7 +232,13 @@ export const zComfyNodeDef = z.object({
* Comfy Org account.
* https://docs.comfy.org/tutorials/api-nodes/overview
*/
api_node: z.boolean().optional()
api_node: z.boolean().optional(),
/**
* Specifies the order of inputs for each input category.
* Used to ensure consistent widget ordering regardless of JSON serialization.
* Keys are 'required', 'optional', etc., values are arrays of input names.
*/
input_order: z.record(z.array(z.string())).optional()
})
// `/object_info`

View File

@@ -50,6 +50,7 @@ import {
isVideoNode,
migrateWidgetsValues
} from '@/utils/litegraphUtil'
import { getOrderedInputSpecs } from '@/utils/nodeDefOrderingUtil'
import { useExtensionService } from './extensionService'
@@ -248,9 +249,14 @@ export const useLitegraphService = () => {
* @internal Add inputs to the node.
*/
#addInputs(inputs: Record<string, InputSpec>) {
for (const inputSpec of Object.values(inputs))
// Use input_order if available to ensure consistent widget ordering
const nodeDefImpl = ComfyNode.nodeData as ComfyNodeDefImpl
const orderedInputSpecs = getOrderedInputSpecs(nodeDefImpl, inputs)
// Create sockets and widgets in the determined order
for (const inputSpec of orderedInputSpecs)
this.#addInputSocket(inputSpec)
for (const inputSpec of Object.values(inputs))
for (const inputSpec of orderedInputSpecs)
this.#addInputWidget(inputSpec)
}
@@ -508,9 +514,14 @@ export const useLitegraphService = () => {
* @internal Add inputs to the node.
*/
#addInputs(inputs: Record<string, InputSpec>) {
for (const inputSpec of Object.values(inputs))
// Use input_order if available to ensure consistent widget ordering
const nodeDefImpl = ComfyNode.nodeData as ComfyNodeDefImpl
const orderedInputSpecs = getOrderedInputSpecs(nodeDefImpl, inputs)
// Create sockets and widgets in the determined order
for (const inputSpec of orderedInputSpecs)
this.#addInputSocket(inputSpec)
for (const inputSpec of Object.values(inputs))
for (const inputSpec of orderedInputSpecs)
this.#addInputWidget(inputSpec)
}

View File

@@ -63,6 +63,10 @@ export class ComfyNodeDefImpl
* @deprecated Use `outputs[n].tooltip` instead
*/
readonly output_tooltips?: string[]
/**
* Order of inputs for each category (required, optional, hidden)
*/
readonly input_order?: Record<string, string[]>
// V2 fields
readonly inputs: Record<string, InputSpecV2>
@@ -130,6 +134,7 @@ export class ComfyNodeDefImpl
this.output_is_list = obj.output_is_list
this.output_name = obj.output_name
this.output_tooltips = obj.output_tooltips
this.input_order = obj.input_order
// Initialize V2 fields
const defV2 = transformNodeDefV1ToV2(obj)

View File

@@ -0,0 +1,108 @@
import { TWidgetValue } from '@/lib/litegraph/src/litegraph'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
/**
* Gets an ordered array of InputSpec objects based on input_order.
* This is designed to work with V2 format used by litegraphService.
*
* @param nodeDefImpl - The ComfyNodeDefImpl containing both V1 and V2 formats
* @param inputs - The V2 format inputs (flat Record<string, InputSpec>)
* @returns Array of InputSpec objects in the correct order
*/
export function getOrderedInputSpecs(
nodeDefImpl: ComfyNodeDefImpl,
inputs: Record<string, InputSpec>
): InputSpec[] {
const orderedInputSpecs: InputSpec[] = []
// If no input_order, return default Object.values order
if (!nodeDefImpl.input_order) {
return Object.values(inputs)
}
// Process required inputs in specified order
if (nodeDefImpl.input_order.required) {
for (const name of nodeDefImpl.input_order.required) {
const inputSpec = inputs[name]
if (inputSpec && !inputSpec.isOptional) {
orderedInputSpecs.push(inputSpec)
}
}
}
// Process optional inputs in specified order
if (nodeDefImpl.input_order.optional) {
for (const name of nodeDefImpl.input_order.optional) {
const inputSpec = inputs[name]
if (inputSpec && inputSpec.isOptional) {
orderedInputSpecs.push(inputSpec)
}
}
}
// Add any remaining inputs not specified in input_order
const processedNames = new Set(orderedInputSpecs.map((spec) => spec.name))
for (const inputSpec of Object.values(inputs)) {
if (!processedNames.has(inputSpec.name)) {
orderedInputSpecs.push(inputSpec)
}
}
return orderedInputSpecs
}
/**
* Reorders widget values based on the input_order to match expected widget order.
* This is used when widgets were created in a different order than input_order specifies.
*
* @param widgetValues - The current widget values array
* @param currentWidgetOrder - The current order of widget names
* @param inputOrder - The desired order from input_order
* @returns Reordered widget values array
*/
export function sortWidgetValuesByInputOrder(
widgetValues: TWidgetValue[],
currentWidgetOrder: string[],
inputOrder: string[]
): TWidgetValue[] {
if (!inputOrder || inputOrder.length === 0) {
return widgetValues
}
// Create a map of widget name to value
const valueMap = new Map<string, TWidgetValue>()
currentWidgetOrder.forEach((name, index) => {
if (index < widgetValues.length) {
valueMap.set(name, widgetValues[index])
}
})
// Reorder based on input_order
const reordered: TWidgetValue[] = []
const usedNames = new Set<string>()
// First, add values in the order specified by input_order
for (const name of inputOrder) {
if (valueMap.has(name)) {
reordered.push(valueMap.get(name))
usedNames.add(name)
}
}
// Then add any remaining values not in input_order
for (const [name, value] of valueMap.entries()) {
if (!usedNames.has(name)) {
reordered.push(value)
}
}
// If there are extra values not in the map, append them
if (widgetValues.length > currentWidgetOrder.length) {
for (let i = currentWidgetOrder.length; i < widgetValues.length; i++) {
reordered.push(widgetValues[i])
}
}
return reordered
}