diff --git a/src/components/graph/widgets/DropdownComboWidget.vue b/src/components/graph/widgets/DropdownComboWidget.vue
new file mode 100644
index 000000000..b73728f90
--- /dev/null
+++ b/src/components/graph/widgets/DropdownComboWidget.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
diff --git a/src/composables/widgets/useComboWidget.ts b/src/composables/widgets/useComboWidget.ts
index 25becfd97..0f6466f82 100644
--- a/src/composables/widgets/useComboWidget.ts
+++ b/src/composables/widgets/useComboWidget.ts
@@ -1,5 +1,4 @@
import type { LGraphNode } from '@comfyorg/litegraph'
-import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { ref } from 'vue'
import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
@@ -15,14 +14,9 @@ import {
} from '@/scripts/domWidget'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
-import { useRemoteWidget } from './useRemoteWidget'
+import { useDropdownComboWidget } from './useDropdownComboWidget'
-const getDefaultValue = (inputSpec: ComboInputSpec) => {
- if (inputSpec.default) return inputSpec.default
- if (inputSpec.options?.length) return inputSpec.options[0]
- if (inputSpec.remote) return 'Loading...'
- return undefined
-}
+// Default value logic is now handled in useDropdownComboWidget
const addMultiSelectWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
const widgetValue = ref([])
@@ -45,53 +39,9 @@ const addMultiSelectWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
}
const addComboWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
- const defaultValue = getDefaultValue(inputSpec)
- const comboOptions = inputSpec.options ?? []
- const widget = node.addWidget(
- 'combo',
- inputSpec.name,
- defaultValue,
- () => {},
- {
- values: comboOptions
- }
- ) as IComboWidget
-
- if (inputSpec.remote) {
- const remoteWidget = useRemoteWidget({
- remoteConfig: inputSpec.remote,
- defaultValue,
- node,
- widget
- })
- if (inputSpec.remote.refresh_button) remoteWidget.addRefreshButton()
-
- const origOptions = widget.options
- widget.options = new Proxy(origOptions, {
- get(target, prop) {
- // Assertion: Proxy handler passthrough
- return prop !== 'values'
- ? target[prop as keyof typeof target]
- : remoteWidget.getValue()
- }
- })
- }
-
- if (inputSpec.control_after_generate) {
- // TODO: Re-implement control widget functionality without circular dependency
- console.warn(
- 'Control widget functionality temporarily disabled for combo widgets due to circular dependency'
- )
- // widget.linkedWidgets = addValueControlWidgets(
- // node,
- // widget,
- // undefined,
- // undefined,
- // transformInputSpecV2ToV1(inputSpec)
- // )
- }
-
- return widget
+ // Use the new dropdown combo widget for single-selection combo widgets
+ const dropdownWidget = useDropdownComboWidget()
+ return dropdownWidget(node, inputSpec)
}
export const useComboWidget = () => {
diff --git a/src/composables/widgets/useDropdownComboWidget.ts b/src/composables/widgets/useDropdownComboWidget.ts
new file mode 100644
index 000000000..2d299f013
--- /dev/null
+++ b/src/composables/widgets/useDropdownComboWidget.ts
@@ -0,0 +1,98 @@
+import type { LGraphNode } from '@comfyorg/litegraph'
+import { ref } from 'vue'
+
+import DropdownComboWidget from '@/components/graph/widgets/DropdownComboWidget.vue'
+import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
+import type {
+ ComboInputSpec,
+ InputSpec
+} from '@/schemas/nodeDef/nodeDefSchemaV2'
+import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
+import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
+import { addValueControlWidgets } from '@/scripts/widgets'
+
+import { useRemoteWidget } from './useRemoteWidget'
+
+const PADDING = 8
+
+const getDefaultValue = (inputSpec: ComboInputSpec) => {
+ if (inputSpec.default) return inputSpec.default
+ if (inputSpec.options?.length) return inputSpec.options[0]
+ if (inputSpec.remote) return 'Loading...'
+ return ''
+}
+
+export const useDropdownComboWidget = (
+ options: { defaultValue?: string } = {}
+) => {
+ const widgetConstructor: ComfyWidgetConstructorV2 = (
+ node: LGraphNode,
+ inputSpec: InputSpec
+ ) => {
+ // Type assertion to ComboInputSpec since this is specifically for combo widgets
+ const comboInputSpec = inputSpec as ComboInputSpec
+
+ // Initialize widget value
+ const defaultValue = options.defaultValue ?? getDefaultValue(comboInputSpec)
+ const widgetValue = ref(defaultValue)
+
+ // Create the widget instance
+ const widget = new ComponentWidgetImpl({
+ node,
+ name: inputSpec.name,
+ component: DropdownComboWidget,
+ inputSpec,
+ options: {
+ // Required: getter for widget value
+ getValue: () => widgetValue.value,
+
+ // Required: setter for widget value
+ setValue: (value: string) => {
+ widgetValue.value = value
+ },
+
+ // Optional: minimum height for the widget (dropdown needs some height)
+ getMinHeight: () => 42 + PADDING,
+
+ // Optional: whether to serialize this widget's value
+ serialize: true
+ }
+ })
+
+ // Handle remote widget functionality
+ if (comboInputSpec.remote) {
+ const remoteWidget = useRemoteWidget({
+ remoteConfig: comboInputSpec.remote,
+ defaultValue,
+ node,
+ widget: widget as any // Cast to be compatible with the remote widget interface
+ })
+ if (comboInputSpec.remote.refresh_button) {
+ remoteWidget.addRefreshButton()
+ }
+
+ // Update the widget to use remote data
+ // Note: The remote widget will handle updating the options through the inputSpec
+ }
+
+ // Handle control_after_generate widgets
+ if (comboInputSpec.control_after_generate) {
+ const linkedWidgets = addValueControlWidgets(
+ node,
+ widget as any, // Cast to be compatible with legacy widget interface
+ undefined,
+ undefined,
+ transformInputSpecV2ToV1(comboInputSpec)
+ )
+ // Store reference to linked widgets (mimicking original behavior)
+ ;(widget as any).linkedWidgets = linkedWidgets
+ }
+
+ // Register the widget with the node
+ addWidget(node, widget as any)
+
+ return widget
+ }
+
+ return widgetConstructor
+}
diff --git a/tests-ui/tests/composables/useDropdownComboWidget.test.ts b/tests-ui/tests/composables/useDropdownComboWidget.test.ts
new file mode 100644
index 000000000..16d92ddcc
--- /dev/null
+++ b/tests-ui/tests/composables/useDropdownComboWidget.test.ts
@@ -0,0 +1,109 @@
+import type { LGraphNode } from '@comfyorg/litegraph'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import { useDropdownComboWidget } from '@/composables/widgets/useDropdownComboWidget'
+import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
+
+// Mock the domWidget store and related dependencies
+vi.mock('@/scripts/domWidget', () => ({
+ ComponentWidgetImpl: vi.fn().mockImplementation((config) => ({
+ name: config.name,
+ value: '',
+ options: config.options
+ })),
+ addWidget: vi.fn()
+}))
+
+// Mock the scripts/widgets for control widgets
+vi.mock('@/scripts/widgets', () => ({
+ addValueControlWidgets: vi.fn().mockReturnValue([])
+}))
+
+// Mock the remote widget functionality
+vi.mock('@/composables/widgets/useRemoteWidget', () => ({
+ useRemoteWidget: vi.fn(() => ({
+ addRefreshButton: vi.fn()
+ }))
+}))
+
+const createMockNode = () => {
+ return {
+ widgets: [],
+ graph: {
+ setDirtyCanvas: vi.fn()
+ },
+ addWidget: vi.fn(),
+ addCustomWidget: vi.fn(),
+ setDirtyCanvas: vi.fn()
+ } as unknown as LGraphNode
+}
+
+describe('useDropdownComboWidget', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+ it('creates widget constructor successfully', () => {
+ const constructor = useDropdownComboWidget()
+ expect(constructor).toBeDefined()
+ expect(typeof constructor).toBe('function')
+ })
+
+ it('widget constructor handles input spec correctly', () => {
+ const constructor = useDropdownComboWidget()
+ const mockNode = createMockNode()
+
+ const inputSpec: ComboInputSpec = {
+ name: 'test_dropdown',
+ type: 'COMBO',
+ options: ['option1', 'option2', 'option3']
+ }
+
+ const widget = constructor(mockNode, inputSpec)
+
+ expect(widget).toBeDefined()
+ expect(widget.name).toBe('test_dropdown')
+ })
+
+ it('widget constructor accepts default value option', () => {
+ const constructor = useDropdownComboWidget({ defaultValue: 'custom' })
+ expect(constructor).toBeDefined()
+ expect(typeof constructor).toBe('function')
+ })
+
+ it('handles remote widgets correctly', () => {
+ const constructor = useDropdownComboWidget()
+ const mockNode = createMockNode()
+
+ const inputSpec: ComboInputSpec = {
+ name: 'remote_dropdown',
+ type: 'COMBO',
+ options: [],
+ remote: {
+ route: '/api/options',
+ refresh_button: true
+ }
+ }
+
+ const widget = constructor(mockNode, inputSpec)
+
+ expect(widget).toBeDefined()
+ expect(widget.name).toBe('remote_dropdown')
+ })
+
+ it('handles control_after_generate widgets correctly', () => {
+ const constructor = useDropdownComboWidget()
+ const mockNode = createMockNode()
+
+ const inputSpec: ComboInputSpec = {
+ name: 'control_dropdown',
+ type: 'COMBO',
+ options: ['option1', 'option2'],
+ control_after_generate: true
+ }
+
+ const widget = constructor(mockNode, inputSpec)
+
+ expect(widget).toBeDefined()
+ expect(widget.name).toBe('control_dropdown')
+ })
+})
diff --git a/tests-ui/tests/composables/widgets/useComboWidget.test.ts b/tests-ui/tests/composables/widgets/useComboWidget.test.ts
index cbdd74bab..d08a7a792 100644
--- a/tests-ui/tests/composables/widgets/useComboWidget.test.ts
+++ b/tests-ui/tests/composables/widgets/useComboWidget.test.ts
@@ -1,39 +1,89 @@
+import type { LGraphNode } from '@comfyorg/litegraph'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useComboWidget } from '@/composables/widgets/useComboWidget'
-import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
+import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
-vi.mock('@/scripts/widgets', () => ({
- addValueControlWidgets: vi.fn()
+// Mock the dropdown combo widget since COMBO now uses it
+vi.mock('@/composables/widgets/useDropdownComboWidget', () => ({
+ useDropdownComboWidget: vi.fn(() =>
+ vi.fn().mockReturnValue({ name: 'mockWidget' })
+ )
}))
+// Mock the domWidget store and related dependencies
+vi.mock('@/scripts/domWidget', () => ({
+ ComponentWidgetImpl: vi.fn().mockImplementation((config) => ({
+ name: config.name,
+ value: [],
+ options: config.options
+ })),
+ addWidget: vi.fn()
+}))
+
+const createMockNode = () => {
+ return {
+ widgets: [],
+ graph: {
+ setDirtyCanvas: vi.fn()
+ },
+ addWidget: vi.fn(),
+ addCustomWidget: vi.fn(),
+ setDirtyCanvas: vi.fn()
+ } as unknown as LGraphNode
+}
+
describe('useComboWidget', () => {
beforeEach(() => {
vi.clearAllMocks()
})
- it('should handle undefined spec', () => {
+ it('should delegate single-selection combo to dropdown widget', () => {
const constructor = useComboWidget()
- const mockNode = {
- addWidget: vi.fn().mockReturnValue({ options: {} } as any)
- }
+ const mockNode = createMockNode()
- const inputSpec: InputSpec = {
+ const inputSpec: ComboInputSpec = {
type: 'COMBO',
- name: 'inputName'
+ name: 'inputName',
+ options: ['option1', 'option2']
}
- const widget = constructor(mockNode as any, inputSpec)
+ const widget = constructor(mockNode, inputSpec)
- expect(mockNode.addWidget).toHaveBeenCalledWith(
- 'combo',
- 'inputName',
- undefined, // default value
- expect.any(Function), // callback
- expect.objectContaining({
- values: []
- })
+ // Should create a widget (delegated to dropdown widget)
+ expect(widget).toBeDefined()
+ expect(widget.name).toBe('mockWidget')
+ })
+
+ it('should use multi-select widget for multi_select combo', () => {
+ const constructor = useComboWidget()
+ const mockNode = createMockNode()
+
+ const inputSpec: ComboInputSpec = {
+ type: 'COMBO',
+ name: 'multiSelectInput',
+ options: ['option1', 'option2'],
+ multi_select: { placeholder: 'Select multiple' }
+ }
+
+ const widget = constructor(mockNode, inputSpec)
+
+ // Should create a multi-select widget
+ expect(widget).toBeDefined()
+ expect(widget.name).toBe('multiSelectInput')
+ })
+
+ it('should handle invalid input spec', () => {
+ const constructor = useComboWidget()
+ const mockNode = createMockNode()
+
+ const invalidSpec = {
+ type: 'NOT_COMBO',
+ name: 'invalidInput'
+ } as any
+
+ expect(() => constructor(mockNode, invalidSpec)).toThrow(
+ 'Invalid input data'
)
- expect(widget).toEqual({ options: {} })
})
})