diff --git a/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts new file mode 100644 index 000000000..6f3701c12 --- /dev/null +++ b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts @@ -0,0 +1,51 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +test.describe('Vue Widget Reactivity', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + test('Should display added widgets', async ({ comfyPage }) => { + const loadCheckpointNode = comfyPage.page.locator( + 'css=[data-testid="node-body-4"] > .lg-node-widgets > div' + ) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['4'] + node.widgets.push(node.widgets[0]) + }) + await expect(loadCheckpointNode).toHaveCount(2) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['4'] + node.widgets[2] = node.widgets[0] + }) + await expect(loadCheckpointNode).toHaveCount(3) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['4'] + node.widgets.splice(0, 0, node.widgets[0]) + }) + await expect(loadCheckpointNode).toHaveCount(4) + }) + test('Should hide removed widgets', async ({ comfyPage }) => { + const loadCheckpointNode = comfyPage.page.locator( + 'css=[data-testid="node-body-3"] > .lg-node-widgets > div' + ) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['3'] + node.widgets.pop() + }) + await expect(loadCheckpointNode).toHaveCount(5) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['3'] + node.widgets.length-- + }) + await expect(loadCheckpointNode).toHaveCount(4) + await comfyPage.page.evaluate(() => { + const node = window['graph']._nodes_by_id['3'] + node.widgets.splice(0, 1) + }) + await expect(loadCheckpointNode).toHaveCount(3) + }) +}) diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index be96d85cc..522c06ef1 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -2,13 +2,15 @@ * Vue node lifecycle management for LiteGraph integration * Provides event-driven reactivity with performance optimizations */ -import { reactive } from 'vue' +import { reactiveComputed } from '@vueuse/core' +import { reactive, shallowReactive } from 'vue' import { useChainCallback } from '@/composables/functional/useChainCallback' import type { INodeInputSlot, INodeOutputSlot } from '@/lib/litegraph/src/interfaces' +import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { LayoutSource } from '@/renderer/core/layout/types' import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' @@ -132,44 +134,57 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { }) }) - const safeWidgets = node.widgets?.map((widget) => { - try { - // TODO: Use widget.getReactiveData() once TypeScript types are updated - let value = widget.value - - // For combo widgets, if value is undefined, use the first option as default - if ( - value === undefined && - widget.type === 'combo' && - widget.options?.values && - Array.isArray(widget.options.values) && - widget.options.values.length > 0 - ) { - value = widget.options.values[0] - } - const spec = nodeDefStore.getInputSpecForWidget(node, widget.name) - const slotInfo = slotMetadata.get(widget.name) - - return { - name: widget.name, - type: widget.type, - value: value, - label: widget.label, - options: widget.options ? { ...widget.options } : undefined, - callback: widget.callback, - spec, - slotMetadata: slotInfo, - isDOMWidget: isDOMWidget(widget) - } - } catch (error) { - return { - name: widget.name || 'unknown', - type: widget.type || 'text', - value: undefined - } + const reactiveWidgets = shallowReactive(node.widgets ?? []) + Object.defineProperty(node, 'widgets', { + get() { + return reactiveWidgets + }, + set(v) { + reactiveWidgets.splice(0, reactiveWidgets.length, ...v) } }) + const safeWidgets = reactiveComputed( + () => + node.widgets?.map((widget) => { + try { + // TODO: Use widget.getReactiveData() once TypeScript types are updated + let value = widget.value + + // For combo widgets, if value is undefined, use the first option as default + if ( + value === undefined && + widget.type === 'combo' && + widget.options?.values && + Array.isArray(widget.options.values) && + widget.options.values.length > 0 + ) { + value = widget.options.values[0] + } + const spec = nodeDefStore.getInputSpecForWidget(node, widget.name) + const slotInfo = slotMetadata.get(widget.name) + + return { + name: widget.name, + type: widget.type, + value: value, + label: widget.label, + options: widget.options ? { ...widget.options } : undefined, + callback: widget.callback, + spec, + slotMetadata: slotInfo, + isDOMWidget: isDOMWidget(widget) + } + } catch (error) { + return { + name: widget.name || 'unknown', + type: widget.type || 'text', + value: undefined + } + } + }) ?? [] + ) + const nodeType = node.type || node.constructor?.comfyClass ||