Skip to content

Commit 5773df6

Browse files
authored
Make nodeData.widgets reactive (#6019)
Makes the litegraph `node.widgets` array `shallowReactive` and makes the `nodeData.widgets` a `reactiveComputed` derived from the litegraph widget data. ![reactive-widgets](https://github.com/user-attachments/assets/8eb8d712-8586-4f34-b699-30fc3dc3340b) Making changes to the structure of litegraph items is somewhat dangerous, but code search verifies that there are no custom nodes using `defineProperty` on `node.widgets` This fixes display of promoted widgets on subgraph node and any custom nodes that dynamically add or remove widgets. TODO: - Investigate occasional dropped widgets. - Some of this was confusion with `canvasOnly` widgets and widgets not implemented in vue. Will keep investigating, but I'm not terribly concerned with actual test cases and it being an objective improvement. Known Issue: - Node does not grow/shrink to fit changed widgets ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6019-Make-nodeData-widgets-reactive-2896d73d3650815691b6ee370a86a22c) by [Unito](https://www.unito.io)
1 parent bc281b2 commit 5773df6

File tree

2 files changed

+102
-36
lines changed

2 files changed

+102
-36
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {
2+
comfyExpect as expect,
3+
comfyPageFixture as test
4+
} from '../../../fixtures/ComfyPage'
5+
6+
test.describe('Vue Widget Reactivity', () => {
7+
test.beforeEach(async ({ comfyPage }) => {
8+
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
9+
await comfyPage.vueNodes.waitForNodes()
10+
})
11+
test('Should display added widgets', async ({ comfyPage }) => {
12+
const loadCheckpointNode = comfyPage.page.locator(
13+
'css=[data-testid="node-body-4"] > .lg-node-widgets > div'
14+
)
15+
await comfyPage.page.evaluate(() => {
16+
const node = window['graph']._nodes_by_id['4']
17+
node.widgets.push(node.widgets[0])
18+
})
19+
await expect(loadCheckpointNode).toHaveCount(2)
20+
await comfyPage.page.evaluate(() => {
21+
const node = window['graph']._nodes_by_id['4']
22+
node.widgets[2] = node.widgets[0]
23+
})
24+
await expect(loadCheckpointNode).toHaveCount(3)
25+
await comfyPage.page.evaluate(() => {
26+
const node = window['graph']._nodes_by_id['4']
27+
node.widgets.splice(0, 0, node.widgets[0])
28+
})
29+
await expect(loadCheckpointNode).toHaveCount(4)
30+
})
31+
test('Should hide removed widgets', async ({ comfyPage }) => {
32+
const loadCheckpointNode = comfyPage.page.locator(
33+
'css=[data-testid="node-body-3"] > .lg-node-widgets > div'
34+
)
35+
await comfyPage.page.evaluate(() => {
36+
const node = window['graph']._nodes_by_id['3']
37+
node.widgets.pop()
38+
})
39+
await expect(loadCheckpointNode).toHaveCount(5)
40+
await comfyPage.page.evaluate(() => {
41+
const node = window['graph']._nodes_by_id['3']
42+
node.widgets.length--
43+
})
44+
await expect(loadCheckpointNode).toHaveCount(4)
45+
await comfyPage.page.evaluate(() => {
46+
const node = window['graph']._nodes_by_id['3']
47+
node.widgets.splice(0, 1)
48+
})
49+
await expect(loadCheckpointNode).toHaveCount(3)
50+
})
51+
})

src/composables/graph/useGraphNodeManager.ts

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
* Vue node lifecycle management for LiteGraph integration
33
* Provides event-driven reactivity with performance optimizations
44
*/
5-
import { reactive } from 'vue'
5+
import { reactiveComputed } from '@vueuse/core'
6+
import { reactive, shallowReactive } from 'vue'
67

78
import { useChainCallback } from '@/composables/functional/useChainCallback'
89
import type {
910
INodeInputSlot,
1011
INodeOutputSlot
1112
} from '@/lib/litegraph/src/interfaces'
13+
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
1214
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
1315
import { LayoutSource } from '@/renderer/core/layout/types'
1416
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
@@ -132,44 +134,57 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
132134
})
133135
})
134136

135-
const safeWidgets = node.widgets?.map((widget) => {
136-
try {
137-
// TODO: Use widget.getReactiveData() once TypeScript types are updated
138-
let value = widget.value
139-
140-
// For combo widgets, if value is undefined, use the first option as default
141-
if (
142-
value === undefined &&
143-
widget.type === 'combo' &&
144-
widget.options?.values &&
145-
Array.isArray(widget.options.values) &&
146-
widget.options.values.length > 0
147-
) {
148-
value = widget.options.values[0]
149-
}
150-
const spec = nodeDefStore.getInputSpecForWidget(node, widget.name)
151-
const slotInfo = slotMetadata.get(widget.name)
152-
153-
return {
154-
name: widget.name,
155-
type: widget.type,
156-
value: value,
157-
label: widget.label,
158-
options: widget.options ? { ...widget.options } : undefined,
159-
callback: widget.callback,
160-
spec,
161-
slotMetadata: slotInfo,
162-
isDOMWidget: isDOMWidget(widget)
163-
}
164-
} catch (error) {
165-
return {
166-
name: widget.name || 'unknown',
167-
type: widget.type || 'text',
168-
value: undefined
169-
}
137+
const reactiveWidgets = shallowReactive<IBaseWidget[]>(node.widgets ?? [])
138+
Object.defineProperty(node, 'widgets', {
139+
get() {
140+
return reactiveWidgets
141+
},
142+
set(v) {
143+
reactiveWidgets.splice(0, reactiveWidgets.length, ...v)
170144
}
171145
})
172146

147+
const safeWidgets = reactiveComputed<SafeWidgetData[]>(
148+
() =>
149+
node.widgets?.map((widget) => {
150+
try {
151+
// TODO: Use widget.getReactiveData() once TypeScript types are updated
152+
let value = widget.value
153+
154+
// For combo widgets, if value is undefined, use the first option as default
155+
if (
156+
value === undefined &&
157+
widget.type === 'combo' &&
158+
widget.options?.values &&
159+
Array.isArray(widget.options.values) &&
160+
widget.options.values.length > 0
161+
) {
162+
value = widget.options.values[0]
163+
}
164+
const spec = nodeDefStore.getInputSpecForWidget(node, widget.name)
165+
const slotInfo = slotMetadata.get(widget.name)
166+
167+
return {
168+
name: widget.name,
169+
type: widget.type,
170+
value: value,
171+
label: widget.label,
172+
options: widget.options ? { ...widget.options } : undefined,
173+
callback: widget.callback,
174+
spec,
175+
slotMetadata: slotInfo,
176+
isDOMWidget: isDOMWidget(widget)
177+
}
178+
} catch (error) {
179+
return {
180+
name: widget.name || 'unknown',
181+
type: widget.type || 'text',
182+
value: undefined
183+
}
184+
}
185+
}) ?? []
186+
)
187+
173188
const nodeType =
174189
node.type ||
175190
node.constructor?.comfyClass ||

0 commit comments

Comments
 (0)