mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
Use v2 input spec for combo widget (#2878)
This commit is contained in:
@@ -1,70 +1,76 @@
|
|||||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||||
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||||
|
|
||||||
|
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||||
import {
|
import {
|
||||||
|
ComboInputSpec,
|
||||||
type InputSpec,
|
type InputSpec,
|
||||||
getComboSpecComboOptions,
|
|
||||||
isComboInputSpec
|
isComboInputSpec
|
||||||
} from '@/schemas/nodeDefSchema'
|
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
import { addValueControlWidgets } from '@/scripts/widgets'
|
import {
|
||||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
type ComfyWidgetConstructorV2,
|
||||||
import { useWidgetStore } from '@/stores/widgetStore'
|
addValueControlWidgets
|
||||||
|
} from '@/scripts/widgets'
|
||||||
|
|
||||||
import { useRemoteWidget } from './useRemoteWidget'
|
import { useRemoteWidget } from './useRemoteWidget'
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
export const useComboWidget = () => {
|
export const useComboWidget = () => {
|
||||||
const widgetConstructor: ComfyWidgetConstructor = (
|
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||||
node: LGraphNode,
|
node: LGraphNode,
|
||||||
inputName: string,
|
inputSpec: InputSpec
|
||||||
inputData: InputSpec
|
|
||||||
) => {
|
) => {
|
||||||
if (!isComboInputSpec(inputData)) {
|
if (!isComboInputSpec(inputSpec)) {
|
||||||
throw new Error(`Invalid input data: ${inputData}`)
|
throw new Error(`Invalid input data: ${inputSpec}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const widgetStore = useWidgetStore()
|
const comboOptions = inputSpec.options ?? []
|
||||||
const inputOptions = inputData[1] ?? {}
|
const defaultValue = getDefaultValue(inputSpec)
|
||||||
const comboOptions = getComboSpecComboOptions(inputData)
|
|
||||||
|
|
||||||
const defaultValue = widgetStore.getDefaultValue(inputData)
|
const widget = node.addWidget(
|
||||||
|
'combo',
|
||||||
const res = {
|
inputSpec.name,
|
||||||
widget: node.addWidget('combo', inputName, defaultValue, () => {}, {
|
defaultValue,
|
||||||
|
() => {},
|
||||||
|
{
|
||||||
values: comboOptions
|
values: comboOptions
|
||||||
}) as IComboWidget
|
}
|
||||||
}
|
) as IComboWidget
|
||||||
|
|
||||||
if (inputOptions.remote) {
|
if (inputSpec.remote) {
|
||||||
const remoteWidget = useRemoteWidget({
|
const remoteWidget = useRemoteWidget({
|
||||||
inputData,
|
remoteConfig: inputSpec.remote,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
node,
|
node,
|
||||||
widget: res.widget
|
widget
|
||||||
})
|
})
|
||||||
if (inputOptions.remote.refresh_button) remoteWidget.addRefreshButton()
|
if (inputSpec.remote.refresh_button) remoteWidget.addRefreshButton()
|
||||||
|
|
||||||
const origOptions = res.widget.options
|
const origOptions = widget.options
|
||||||
res.widget.options = new Proxy(
|
widget.options = new Proxy(origOptions as Record<string | symbol, any>, {
|
||||||
origOptions as Record<string | symbol, any>,
|
get(target, prop: string | symbol) {
|
||||||
{
|
if (prop !== 'values') return target[prop]
|
||||||
get(target, prop: string | symbol) {
|
return remoteWidget.getValue()
|
||||||
if (prop !== 'values') return target[prop]
|
|
||||||
return remoteWidget.getValue()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputOptions.control_after_generate) {
|
if (inputSpec.control_after_generate) {
|
||||||
res.widget.linkedWidgets = addValueControlWidgets(
|
widget.linkedWidgets = addValueControlWidgets(
|
||||||
node,
|
node,
|
||||||
res.widget,
|
widget,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
inputData
|
transformInputSpecV2ToV1(inputSpec)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return res
|
return widget
|
||||||
}
|
}
|
||||||
|
|
||||||
return widgetConstructor
|
return widgetConstructor
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { LGraphNode } from '@comfyorg/litegraph'
|
|||||||
import { IWidget } from '@comfyorg/litegraph'
|
import { IWidget } from '@comfyorg/litegraph'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import type { InputSpec, RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||||
|
|
||||||
const MAX_RETRIES = 5
|
const MAX_RETRIES = 5
|
||||||
const TIMEOUT = 4096
|
const TIMEOUT = 4096
|
||||||
@@ -67,16 +67,15 @@ const fetchData = async (
|
|||||||
export function useRemoteWidget<
|
export function useRemoteWidget<
|
||||||
T extends string | number | boolean | object
|
T extends string | number | boolean | object
|
||||||
>(options: {
|
>(options: {
|
||||||
inputData: InputSpec
|
remoteConfig: RemoteWidgetConfig
|
||||||
defaultValue: T
|
defaultValue: T
|
||||||
node: LGraphNode
|
node: LGraphNode
|
||||||
widget: IWidget
|
widget: IWidget
|
||||||
}) {
|
}) {
|
||||||
const { inputData, defaultValue, node, widget } = options
|
const { remoteConfig, defaultValue, node, widget } = options
|
||||||
const config = (inputData[1]?.remote ?? {}) as RemoteWidgetConfig
|
const { refresh = 0, max_retries = MAX_RETRIES } = remoteConfig
|
||||||
const { refresh = 0, max_retries = MAX_RETRIES } = config
|
|
||||||
const isPermanent = refresh <= 0
|
const isPermanent = refresh <= 0
|
||||||
const cacheKey = createCacheKey(config)
|
const cacheKey = createCacheKey(remoteConfig)
|
||||||
let isLoaded = false
|
let isLoaded = false
|
||||||
let refreshQueued = false
|
let refreshQueued = false
|
||||||
|
|
||||||
@@ -131,7 +130,10 @@ export function useRemoteWidget<
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
currentEntry.controller = new AbortController()
|
currentEntry.controller = new AbortController()
|
||||||
currentEntry.fetchPromise = fetchData(config, currentEntry.controller)
|
currentEntry.fetchPromise = fetchData(
|
||||||
|
remoteConfig,
|
||||||
|
currentEntry.controller
|
||||||
|
)
|
||||||
const data = await currentEntry.fetchPromise
|
const data = await currentEntry.fetchPromise
|
||||||
|
|
||||||
setSuccess(currentEntry, data)
|
setSuccess(currentEntry, data)
|
||||||
@@ -146,11 +148,11 @@ export function useRemoteWidget<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onRefresh = () => {
|
const onRefresh = () => {
|
||||||
if (config.control_after_refresh) {
|
if (remoteConfig.control_after_refresh) {
|
||||||
const data = getCachedValue()
|
const data = getCachedValue()
|
||||||
if (!Array.isArray(data)) return // control_after_refresh is only supported for array values
|
if (!Array.isArray(data)) return // control_after_refresh is only supported for array values
|
||||||
|
|
||||||
switch (config.control_after_refresh) {
|
switch (remoteConfig.control_after_refresh) {
|
||||||
case 'first':
|
case 'first':
|
||||||
widget.value = data[0] ?? defaultValue
|
widget.value = data[0] ?? defaultValue
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ export const zComboInputOptions = zBaseInputOptions.extend({
|
|||||||
image_folder: z.enum(['input', 'output', 'temp']).optional(),
|
image_folder: z.enum(['input', 'output', 'temp']).optional(),
|
||||||
allow_batch: z.boolean().optional(),
|
allow_batch: z.boolean().optional(),
|
||||||
video_upload: z.boolean().optional(),
|
video_upload: z.boolean().optional(),
|
||||||
remote: zRemoteWidgetConfig.optional(),
|
options: z.array(zComboOption).optional(),
|
||||||
options: z.array(zComboOption).optional()
|
remote: zRemoteWidgetConfig.optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
const zIntInputSpec = z.tuple([z.literal('INT'), zIntInputOptions.optional()])
|
const zIntInputSpec = z.tuple([z.literal('INT'), zIntInputOptions.optional()])
|
||||||
|
|||||||
@@ -293,6 +293,6 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
|||||||
BOOLEAN: transformWidgetConstructorV2ToV1(useBooleanWidget()),
|
BOOLEAN: transformWidgetConstructorV2ToV1(useBooleanWidget()),
|
||||||
STRING: transformWidgetConstructorV2ToV1(useStringWidget()),
|
STRING: transformWidgetConstructorV2ToV1(useStringWidget()),
|
||||||
MARKDOWN: transformWidgetConstructorV2ToV1(useMarkdownWidget()),
|
MARKDOWN: transformWidgetConstructorV2ToV1(useMarkdownWidget()),
|
||||||
COMBO: useComboWidget(),
|
COMBO: transformWidgetConstructorV2ToV1(useComboWidget()),
|
||||||
IMAGEUPLOAD: useImageUploadWidget()
|
IMAGEUPLOAD: useImageUploadWidget()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import { defineStore } from 'pinia'
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
import {
|
|
||||||
type ComboInputSpecV2,
|
|
||||||
type InputSpec,
|
|
||||||
isComboInputSpecV1
|
|
||||||
} from '@/schemas/nodeDefSchema'
|
|
||||||
import { ComfyWidgetConstructor, ComfyWidgets } from '@/scripts/widgets'
|
import { ComfyWidgetConstructor, ComfyWidgets } from '@/scripts/widgets'
|
||||||
|
|
||||||
export const useWidgetStore = defineStore('widget', () => {
|
export const useWidgetStore = defineStore('widget', () => {
|
||||||
@@ -45,42 +40,10 @@ export const useWidgetStore = defineStore('widget', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultValue(inputData: InputSpec) {
|
|
||||||
if (Array.isArray(inputData[0]))
|
|
||||||
return getDefaultValue(transformComboInput(inputData))
|
|
||||||
|
|
||||||
// @ts-expect-error InputSpec is not typed correctly
|
|
||||||
const widgetType = getWidgetType(inputData[0], inputData[1]?.name)
|
|
||||||
|
|
||||||
const [_, props] = inputData
|
|
||||||
|
|
||||||
if (!props) return undefined
|
|
||||||
if (props.default) return props.default
|
|
||||||
|
|
||||||
// @ts-expect-error InputSpec is not typed correctly
|
|
||||||
if (widgetType === 'COMBO' && props.options?.length) return props.options[0]
|
|
||||||
if (props.remote) return 'Loading...'
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const transformComboInput = (inputData: InputSpec): ComboInputSpecV2 => {
|
|
||||||
// @ts-expect-error InputSpec is not typed correctly
|
|
||||||
return isComboInputSpecV1(inputData)
|
|
||||||
? [
|
|
||||||
'COMBO',
|
|
||||||
{
|
|
||||||
options: inputData[0],
|
|
||||||
...Object(inputData[1] || {})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: inputData
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
widgets,
|
widgets,
|
||||||
getWidgetType,
|
getWidgetType,
|
||||||
inputIsWidget,
|
inputIsWidget,
|
||||||
registerCustomWidgets,
|
registerCustomWidgets
|
||||||
getDefaultValue
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import { createPinia, setActivePinia } from 'pinia'
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import { useComboWidget } from '@/composables/widgets/useComboWidget'
|
import { useComboWidget } from '@/composables/widgets/useComboWidget'
|
||||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
|
|
||||||
vi.mock('@/stores/widgetStore', () => ({
|
|
||||||
useWidgetStore: () => ({
|
|
||||||
getDefaultValue: vi.fn()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/scripts/widgets', () => ({
|
vi.mock('@/scripts/widgets', () => ({
|
||||||
addValueControlWidgets: vi.fn()
|
addValueControlWidgets: vi.fn()
|
||||||
@@ -16,7 +9,6 @@ vi.mock('@/scripts/widgets', () => ({
|
|||||||
|
|
||||||
describe('useComboWidget', () => {
|
describe('useComboWidget', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createPinia())
|
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -26,14 +18,12 @@ describe('useComboWidget', () => {
|
|||||||
addWidget: vi.fn().mockReturnValue({ options: {} } as any)
|
addWidget: vi.fn().mockReturnValue({ options: {} } as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputSpec: InputSpec = ['COMBO', undefined]
|
const inputSpec: InputSpec = {
|
||||||
|
type: 'COMBO',
|
||||||
|
name: 'inputName'
|
||||||
|
}
|
||||||
|
|
||||||
const widget = constructor(
|
const widget = constructor(mockNode as any, inputSpec)
|
||||||
mockNode as any,
|
|
||||||
'inputName',
|
|
||||||
inputSpec,
|
|
||||||
undefined as any
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(mockNode.addWidget).toHaveBeenCalledWith(
|
expect(mockNode.addWidget).toHaveBeenCalledWith(
|
||||||
'combo',
|
'combo',
|
||||||
@@ -44,8 +34,6 @@ describe('useComboWidget', () => {
|
|||||||
values: []
|
values: []
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
expect(widget).toEqual({
|
expect(widget).toEqual({ options: {} })
|
||||||
widget: { options: {} }
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios from 'axios'
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import { useRemoteWidget } from '@/composables/widgets/useRemoteWidget'
|
import { useRemoteWidget } from '@/composables/widgets/useRemoteWidget'
|
||||||
import type { ComboInputSpecV2 } from '@/schemas/nodeDefSchema'
|
import { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||||
|
|
||||||
vi.mock('axios', () => {
|
vi.mock('axios', () => {
|
||||||
return {
|
return {
|
||||||
@@ -29,22 +29,16 @@ vi.mock('@/stores/settingStore', () => ({
|
|||||||
const FIRST_BACKOFF = 1000 // backoff is 1s on first retry
|
const FIRST_BACKOFF = 1000 // backoff is 1s on first retry
|
||||||
const DEFAULT_VALUE = 'Loading...'
|
const DEFAULT_VALUE = 'Loading...'
|
||||||
|
|
||||||
function createMockInputData(overrides = {}): ComboInputSpecV2 {
|
function createMockConfig(overrides = {}): RemoteWidgetConfig {
|
||||||
return [
|
return {
|
||||||
'COMBO',
|
route: `/api/test/${Date.now()}${Math.random().toString(36).substring(2, 15)}`,
|
||||||
{
|
refresh: 0,
|
||||||
name: 'test_widget',
|
...overrides
|
||||||
remote: {
|
}
|
||||||
route: `/api/test/${Date.now()}${Math.random().toString(36).substring(2, 15)}`,
|
|
||||||
refresh: 0,
|
|
||||||
...overrides
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createMockOptions = (inputOverrides = {}) => ({
|
const createMockOptions = (inputOverrides = {}) => ({
|
||||||
inputData: createMockInputData(inputOverrides),
|
remoteConfig: createMockConfig(inputOverrides),
|
||||||
defaultValue: DEFAULT_VALUE,
|
defaultValue: DEFAULT_VALUE,
|
||||||
node: {} as any,
|
node: {} as any,
|
||||||
widget: {} as any
|
widget: {} as any
|
||||||
@@ -81,7 +75,7 @@ async function getResolvedValue(hook: ReturnType<typeof useRemoteWidget>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('useRemoteWidget', () => {
|
describe('useRemoteWidget', () => {
|
||||||
let mockInputData: ComboInputSpecV2
|
let mockConfig: RemoteWidgetConfig
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
@@ -92,7 +86,7 @@ describe('useRemoteWidget', () => {
|
|||||||
vi.spyOn(Map.prototype, 'set').mockClear()
|
vi.spyOn(Map.prototype, 'set').mockClear()
|
||||||
vi.spyOn(Map.prototype, 'delete').mockClear()
|
vi.spyOn(Map.prototype, 'delete').mockClear()
|
||||||
|
|
||||||
mockInputData = createMockInputData()
|
mockConfig = createMockConfig()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user