Skip to content

Commit 35e6cab

Browse files
authored
Use v2 input spec for combo widget (#2878)
1 parent 8a47997 commit 35e6cab

File tree

7 files changed

+75
-122
lines changed

7 files changed

+75
-122
lines changed

src/composables/widgets/useComboWidget.ts

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,76 @@
11
import type { LGraphNode } from '@comfyorg/litegraph'
22
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
33

4+
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
45
import {
6+
ComboInputSpec,
57
type InputSpec,
6-
getComboSpecComboOptions,
78
isComboInputSpec
8-
} from '@/schemas/nodeDefSchema'
9-
import { addValueControlWidgets } from '@/scripts/widgets'
10-
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
11-
import { useWidgetStore } from '@/stores/widgetStore'
9+
} from '@/schemas/nodeDef/nodeDefSchemaV2'
10+
import {
11+
type ComfyWidgetConstructorV2,
12+
addValueControlWidgets
13+
} from '@/scripts/widgets'
1214

1315
import { useRemoteWidget } from './useRemoteWidget'
1416

17+
const getDefaultValue = (inputSpec: ComboInputSpec) => {
18+
if (inputSpec.default) return inputSpec.default
19+
if (inputSpec.options?.length) return inputSpec.options[0]
20+
if (inputSpec.remote) return 'Loading...'
21+
return undefined
22+
}
23+
1524
export const useComboWidget = () => {
16-
const widgetConstructor: ComfyWidgetConstructor = (
25+
const widgetConstructor: ComfyWidgetConstructorV2 = (
1726
node: LGraphNode,
18-
inputName: string,
19-
inputData: InputSpec
27+
inputSpec: InputSpec
2028
) => {
21-
if (!isComboInputSpec(inputData)) {
22-
throw new Error(`Invalid input data: ${inputData}`)
29+
if (!isComboInputSpec(inputSpec)) {
30+
throw new Error(`Invalid input data: ${inputSpec}`)
2331
}
2432

25-
const widgetStore = useWidgetStore()
26-
const inputOptions = inputData[1] ?? {}
27-
const comboOptions = getComboSpecComboOptions(inputData)
28-
29-
const defaultValue = widgetStore.getDefaultValue(inputData)
33+
const comboOptions = inputSpec.options ?? []
34+
const defaultValue = getDefaultValue(inputSpec)
3035

31-
const res = {
32-
widget: node.addWidget('combo', inputName, defaultValue, () => {}, {
36+
const widget = node.addWidget(
37+
'combo',
38+
inputSpec.name,
39+
defaultValue,
40+
() => {},
41+
{
3342
values: comboOptions
34-
}) as IComboWidget
35-
}
43+
}
44+
) as IComboWidget
3645

37-
if (inputOptions.remote) {
46+
if (inputSpec.remote) {
3847
const remoteWidget = useRemoteWidget({
39-
inputData,
48+
remoteConfig: inputSpec.remote,
4049
defaultValue,
4150
node,
42-
widget: res.widget
51+
widget
4352
})
44-
if (inputOptions.remote.refresh_button) remoteWidget.addRefreshButton()
53+
if (inputSpec.remote.refresh_button) remoteWidget.addRefreshButton()
4554

46-
const origOptions = res.widget.options
47-
res.widget.options = new Proxy(
48-
origOptions as Record<string | symbol, any>,
49-
{
50-
get(target, prop: string | symbol) {
51-
if (prop !== 'values') return target[prop]
52-
return remoteWidget.getValue()
53-
}
55+
const origOptions = widget.options
56+
widget.options = new Proxy(origOptions as Record<string | symbol, any>, {
57+
get(target, prop: string | symbol) {
58+
if (prop !== 'values') return target[prop]
59+
return remoteWidget.getValue()
5460
}
55-
)
61+
})
5662
}
5763

58-
if (inputOptions.control_after_generate) {
59-
res.widget.linkedWidgets = addValueControlWidgets(
64+
if (inputSpec.control_after_generate) {
65+
widget.linkedWidgets = addValueControlWidgets(
6066
node,
61-
res.widget,
67+
widget,
6268
undefined,
6369
undefined,
64-
inputData
70+
transformInputSpecV2ToV1(inputSpec)
6571
)
6672
}
67-
return res
73+
return widget
6874
}
6975

7076
return widgetConstructor

src/composables/widgets/useRemoteWidget.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { LGraphNode } from '@comfyorg/litegraph'
22
import { IWidget } from '@comfyorg/litegraph'
33
import axios from 'axios'
44

5-
import type { InputSpec, RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
5+
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
66

77
const MAX_RETRIES = 5
88
const TIMEOUT = 4096
@@ -67,16 +67,15 @@ const fetchData = async (
6767
export function useRemoteWidget<
6868
T extends string | number | boolean | object
6969
>(options: {
70-
inputData: InputSpec
70+
remoteConfig: RemoteWidgetConfig
7171
defaultValue: T
7272
node: LGraphNode
7373
widget: IWidget
7474
}) {
75-
const { inputData, defaultValue, node, widget } = options
76-
const config = (inputData[1]?.remote ?? {}) as RemoteWidgetConfig
77-
const { refresh = 0, max_retries = MAX_RETRIES } = config
75+
const { remoteConfig, defaultValue, node, widget } = options
76+
const { refresh = 0, max_retries = MAX_RETRIES } = remoteConfig
7877
const isPermanent = refresh <= 0
79-
const cacheKey = createCacheKey(config)
78+
const cacheKey = createCacheKey(remoteConfig)
8079
let isLoaded = false
8180
let refreshQueued = false
8281

@@ -131,7 +130,10 @@ export function useRemoteWidget<
131130

132131
try {
133132
currentEntry.controller = new AbortController()
134-
currentEntry.fetchPromise = fetchData(config, currentEntry.controller)
133+
currentEntry.fetchPromise = fetchData(
134+
remoteConfig,
135+
currentEntry.controller
136+
)
135137
const data = await currentEntry.fetchPromise
136138

137139
setSuccess(currentEntry, data)
@@ -146,11 +148,11 @@ export function useRemoteWidget<
146148
}
147149

148150
const onRefresh = () => {
149-
if (config.control_after_refresh) {
151+
if (remoteConfig.control_after_refresh) {
150152
const data = getCachedValue()
151153
if (!Array.isArray(data)) return // control_after_refresh is only supported for array values
152154

153-
switch (config.control_after_refresh) {
155+
switch (remoteConfig.control_after_refresh) {
154156
case 'first':
155157
widget.value = data[0] ?? defaultValue
156158
break

src/schemas/nodeDefSchema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ export const zComboInputOptions = zBaseInputOptions.extend({
7171
image_folder: z.enum(['input', 'output', 'temp']).optional(),
7272
allow_batch: z.boolean().optional(),
7373
video_upload: z.boolean().optional(),
74-
remote: zRemoteWidgetConfig.optional(),
75-
options: z.array(zComboOption).optional()
74+
options: z.array(zComboOption).optional(),
75+
remote: zRemoteWidgetConfig.optional()
7676
})
7777

7878
const zIntInputSpec = z.tuple([z.literal('INT'), zIntInputOptions.optional()])

src/scripts/widgets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,6 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
293293
BOOLEAN: transformWidgetConstructorV2ToV1(useBooleanWidget()),
294294
STRING: transformWidgetConstructorV2ToV1(useStringWidget()),
295295
MARKDOWN: transformWidgetConstructorV2ToV1(useMarkdownWidget()),
296-
COMBO: useComboWidget(),
296+
COMBO: transformWidgetConstructorV2ToV1(useComboWidget()),
297297
IMAGEUPLOAD: useImageUploadWidget()
298298
}

src/stores/widgetStore.ts

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import { defineStore } from 'pinia'
22
import { computed, ref } from 'vue'
33

44
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
5-
import {
6-
type ComboInputSpecV2,
7-
type InputSpec,
8-
isComboInputSpecV1
9-
} from '@/schemas/nodeDefSchema'
105
import { ComfyWidgetConstructor, ComfyWidgets } from '@/scripts/widgets'
116

127
export const useWidgetStore = defineStore('widget', () => {
@@ -45,42 +40,10 @@ export const useWidgetStore = defineStore('widget', () => {
4540
}
4641
}
4742

48-
function getDefaultValue(inputData: InputSpec) {
49-
if (Array.isArray(inputData[0]))
50-
return getDefaultValue(transformComboInput(inputData))
51-
52-
// @ts-expect-error InputSpec is not typed correctly
53-
const widgetType = getWidgetType(inputData[0], inputData[1]?.name)
54-
55-
const [_, props] = inputData
56-
57-
if (!props) return undefined
58-
if (props.default) return props.default
59-
60-
// @ts-expect-error InputSpec is not typed correctly
61-
if (widgetType === 'COMBO' && props.options?.length) return props.options[0]
62-
if (props.remote) return 'Loading...'
63-
return undefined
64-
}
65-
66-
const transformComboInput = (inputData: InputSpec): ComboInputSpecV2 => {
67-
// @ts-expect-error InputSpec is not typed correctly
68-
return isComboInputSpecV1(inputData)
69-
? [
70-
'COMBO',
71-
{
72-
options: inputData[0],
73-
...Object(inputData[1] || {})
74-
}
75-
]
76-
: inputData
77-
}
78-
7943
return {
8044
widgets,
8145
getWidgetType,
8246
inputIsWidget,
83-
registerCustomWidgets,
84-
getDefaultValue
47+
registerCustomWidgets
8548
}
8649
})
Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1-
import { createPinia, setActivePinia } from 'pinia'
21
import { beforeEach, describe, expect, it, vi } from 'vitest'
32

43
import { useComboWidget } from '@/composables/widgets/useComboWidget'
5-
import type { InputSpec } from '@/schemas/nodeDefSchema'
6-
7-
vi.mock('@/stores/widgetStore', () => ({
8-
useWidgetStore: () => ({
9-
getDefaultValue: vi.fn()
10-
})
11-
}))
4+
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
125

136
vi.mock('@/scripts/widgets', () => ({
147
addValueControlWidgets: vi.fn()
158
}))
169

1710
describe('useComboWidget', () => {
1811
beforeEach(() => {
19-
setActivePinia(createPinia())
2012
vi.clearAllMocks()
2113
})
2214

@@ -26,14 +18,12 @@ describe('useComboWidget', () => {
2618
addWidget: vi.fn().mockReturnValue({ options: {} } as any)
2719
}
2820

29-
const inputSpec: InputSpec = ['COMBO', undefined]
21+
const inputSpec: InputSpec = {
22+
type: 'COMBO',
23+
name: 'inputName'
24+
}
3025

31-
const widget = constructor(
32-
mockNode as any,
33-
'inputName',
34-
inputSpec,
35-
undefined as any
36-
)
26+
const widget = constructor(mockNode as any, inputSpec)
3727

3828
expect(mockNode.addWidget).toHaveBeenCalledWith(
3929
'combo',
@@ -44,8 +34,6 @@ describe('useComboWidget', () => {
4434
values: []
4535
})
4636
)
47-
expect(widget).toEqual({
48-
widget: { options: {} }
49-
})
37+
expect(widget).toEqual({ options: {} })
5038
})
5139
})

tests-ui/tests/composables/widgets/useRemoteWidget.test.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import axios from 'axios'
22
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
33

44
import { useRemoteWidget } from '@/composables/widgets/useRemoteWidget'
5-
import type { ComboInputSpecV2 } from '@/schemas/nodeDefSchema'
5+
import { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
66

77
vi.mock('axios', () => {
88
return {
@@ -29,22 +29,16 @@ vi.mock('@/stores/settingStore', () => ({
2929
const FIRST_BACKOFF = 1000 // backoff is 1s on first retry
3030
const DEFAULT_VALUE = 'Loading...'
3131

32-
function createMockInputData(overrides = {}): ComboInputSpecV2 {
33-
return [
34-
'COMBO',
35-
{
36-
name: 'test_widget',
37-
remote: {
38-
route: `/api/test/${Date.now()}${Math.random().toString(36).substring(2, 15)}`,
39-
refresh: 0,
40-
...overrides
41-
}
42-
}
43-
]
32+
function createMockConfig(overrides = {}): RemoteWidgetConfig {
33+
return {
34+
route: `/api/test/${Date.now()}${Math.random().toString(36).substring(2, 15)}`,
35+
refresh: 0,
36+
...overrides
37+
}
4438
}
4539

4640
const createMockOptions = (inputOverrides = {}) => ({
47-
inputData: createMockInputData(inputOverrides),
41+
remoteConfig: createMockConfig(inputOverrides),
4842
defaultValue: DEFAULT_VALUE,
4943
node: {} as any,
5044
widget: {} as any
@@ -81,7 +75,7 @@ async function getResolvedValue(hook: ReturnType<typeof useRemoteWidget>) {
8175
}
8276

8377
describe('useRemoteWidget', () => {
84-
let mockInputData: ComboInputSpecV2
78+
let mockConfig: RemoteWidgetConfig
8579

8680
beforeEach(() => {
8781
vi.clearAllMocks()
@@ -92,7 +86,7 @@ describe('useRemoteWidget', () => {
9286
vi.spyOn(Map.prototype, 'set').mockClear()
9387
vi.spyOn(Map.prototype, 'delete').mockClear()
9488

95-
mockInputData = createMockInputData()
89+
mockConfig = createMockConfig()
9690
})
9791

9892
afterEach(() => {

0 commit comments

Comments
 (0)