Migrate forceInput widgets_values (#3337)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2025-04-06 21:27:42 -04:00
committed by GitHub
parent a2b3048b94
commit 2c02d4ebb3
11 changed files with 211 additions and 2 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -296,6 +296,17 @@
"Comfy_Window_UnloadConfirmation": {
"name": "Show confirmation when closing window"
},
"Comfy_Workflow_AutoSave": {
"name": "Auto Save",
"options": {
"off": "off",
"after delay": "after delay"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "Auto Save Delay (ms)",
"tooltip": "Only applies if Auto Save is set to \"after delay\"."
},
"Comfy_Workflow_ConfirmDelete": {
"name": "Show confirmation when deleting workflows"
},

View File

@@ -296,6 +296,17 @@
"Comfy_Window_UnloadConfirmation": {
"name": "Mostrar confirmación al cerrar la ventana"
},
"Comfy_Workflow_AutoSave": {
"name": "Auto Guardar",
"options": {
"after delay": "después de retraso",
"off": "desactivado"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "Retraso de Auto Guardar (ms)",
"tooltip": "Solo se aplica si Auto Guardar está configurado en \"después de retraso\"."
},
"Comfy_Workflow_ConfirmDelete": {
"name": "Mostrar confirmación al eliminar flujos de trabajo"
},

View File

@@ -296,6 +296,17 @@
"Comfy_Window_UnloadConfirmation": {
"name": "Afficher une confirmation lors de la fermeture de la fenêtre"
},
"Comfy_Workflow_AutoSave": {
"name": "Auto Sauvegarde",
"options": {
"after delay": "après délai",
"off": "désactivé"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "Délai de l'Auto Sauvegarde (ms)",
"tooltip": "S'applique uniquement si l'Auto Sauvegarde est réglée sur \"après délai\"."
},
"Comfy_Workflow_ConfirmDelete": {
"name": "Afficher une confirmation lors de la suppression des flux de travail"
},

View File

@@ -296,6 +296,17 @@
"Comfy_Window_UnloadConfirmation": {
"name": "ウィンドウを閉じるときに確認を表示"
},
"Comfy_Workflow_AutoSave": {
"name": "自動保存",
"options": {
"after delay": "遅延後",
"off": "オフ"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "自動保存遅延ms",
"tooltip": "自動保存が「遅延後」に設定されている場合のみ適用されます。"
},
"Comfy_Workflow_ConfirmDelete": {
"name": "ワークフローを削除する際に確認を表示"
},

View File

@@ -296,6 +296,17 @@
"Comfy_Window_UnloadConfirmation": {
"name": "창 닫을 때 확인 표시"
},
"Comfy_Workflow_AutoSave": {
"name": "자동 저장",
"options": {
"after delay": "지연 후",
"off": "끄기"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "자동 저장 지연 시간 (ms)",
"tooltip": "자동 저장이 \"지연 후\"로 설정된 경우에만 적용됩니다."
},
"Comfy_Workflow_ConfirmDelete": {
"name": "워크플로 삭제 시 확인 표시"
},

View File

@@ -296,6 +296,17 @@
"Comfy_Window_UnloadConfirmation": {
"name": "Показать подтверждение при закрытии окна"
},
"Comfy_Workflow_AutoSave": {
"name": "Автосохранение",
"options": {
"after delay": "после задержки",
"off": "выключено"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "Задержка автосохранения (мс)",
"tooltip": "Применяется только если автосохранение установлено на \"после задержки\"."
},
"Comfy_Workflow_ConfirmDelete": {
"name": "Показать подтверждение при удалении рабочих процессов"
},

View File

@@ -296,6 +296,17 @@
"Comfy_Window_UnloadConfirmation": {
"name": "关闭窗口时显示确认"
},
"Comfy_Workflow_AutoSave": {
"name": "自动保存",
"options": {
"after delay": "延迟后",
"off": "关闭"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "自动保存延迟(毫秒)",
"tooltip": "仅在自动保存设置为“延迟后”时适用。"
},
"Comfy_Workflow_ConfirmDelete": {
"name": "删除工作流时显示确认"
},

View File

@@ -33,7 +33,11 @@ import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useToastStore } from '@/stores/toastStore'
import { useWidgetStore } from '@/stores/widgetStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { isImageNode, isVideoNode } from '@/utils/litegraphUtil'
import {
isImageNode,
isVideoNode,
migrateWidgetsValues
} from '@/utils/litegraphUtil'
import { useExtensionService } from './extensionService'
@@ -262,6 +266,12 @@ export const useLitegraphService = () => {
}
)
data.widgets_values = migrateWidgetsValues(
ComfyNode.nodeData.inputs,
this.widgets ?? [],
data.widgets_values ?? []
)
super.configure(data)
}
}

View File

@@ -1,8 +1,13 @@
import type { ColorOption } from '@comfyorg/litegraph'
import { LGraphGroup, LGraphNode, isColorable } from '@comfyorg/litegraph'
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
import type {
IComboWidget,
IWidget
} from '@comfyorg/litegraph/dist/types/widgets'
import _ from 'lodash'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
type ImageNode = LGraphNode & { imgs: HTMLImageElement[] | undefined }
type VideoNode = LGraphNode & {
videoContainer: HTMLElement | undefined
@@ -70,3 +75,32 @@ export function executeWidgetsCallback(
}
}
}
/**
* Since frontend version 1.16, forceInput input is no longer treated
* as widget. So we need to remove the dummy widget value serialized
* from workflows prior to v1.16.
* Ref: https://github.com/Comfy-Org/ComfyUI_frontend/pull/3326
*
* @param nodeDef the node definition
* @param widgets the widgets on the node instance (from node definition)
* @param widgetsValues the widgets values to populate the node during configuration
* @returns the widgets values without the dummy widget values
*/
export function migrateWidgetsValues<TWidgetValue>(
inputDefs: Record<string, InputSpec>,
widgets: IWidget[],
widgetsValues: TWidgetValue[]
): TWidgetValue[] {
const widgetNames = new Set(widgets.map((w) => w.name))
const originalWidgetsInputs = Object.values(inputDefs).filter(
(input) => widgetNames.has(input.name) || input.forceInput
)
if (originalWidgetsInputs.length === widgetsValues?.length) {
return _.zip(originalWidgetsInputs, widgetsValues)
.filter(([input]) => !input?.forceInput)
.map(([_, value]) => value as TWidgetValue)
}
return widgetsValues
}

View File

@@ -0,0 +1,88 @@
import type { IWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { describe, expect, it } from 'vitest'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { migrateWidgetsValues } from '@/utils/litegraphUtil'
describe('migrateWidgetsValues', () => {
it('should remove widget values for forceInput inputs', () => {
const inputDefs: Record<string, InputSpec> = {
normalInput: {
type: 'INT',
name: 'normalInput'
},
forceInputField: {
type: 'STRING',
name: 'forceInputField',
forceInput: true
},
anotherNormal: {
type: 'FLOAT',
name: 'anotherNormal'
}
}
const widgets: IWidget[] = [
{ name: 'normalInput', type: 'number' },
{ name: 'anotherNormal', type: 'number' }
] as unknown as IWidget[]
const widgetValues = [42, 'dummy value', 3.14]
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
expect(result).toEqual([42, 3.14])
})
it('should return original values if lengths do not match', () => {
const inputDefs: Record<string, InputSpec> = {
input1: {
type: 'INT',
name: 'input1',
forceInput: true
}
}
const widgets: IWidget[] = []
const widgetValues = [42, 'extra value']
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
expect(result).toEqual(widgetValues)
})
it('should handle empty widgets and values', () => {
const inputDefs: Record<string, InputSpec> = {}
const widgets: IWidget[] = []
const widgetValues: any[] = []
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
expect(result).toEqual([])
})
it('should preserve order of non-forceInput widget values', () => {
const inputDefs: Record<string, InputSpec> = {
first: {
type: 'INT',
name: 'first'
},
forced: {
type: 'STRING',
name: 'forced',
forceInput: true
},
last: {
type: 'FLOAT',
name: 'last'
}
}
const widgets: IWidget[] = [
{ name: 'first', type: 'number' },
{ name: 'last', type: 'number' }
] as unknown as IWidget[]
const widgetValues = ['first value', 'dummy', 'last value']
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
expect(result).toEqual(['first value', 'last value'])
})
})