Compare commits

...

3 Commits

Author SHA1 Message Date
bymyself
4c205683ab chore: trigger CI 2026-04-10 18:16:53 -07:00
GitHub Action
395abae28f [automated] Apply ESLint and Oxfmt fixes 2026-04-10 18:16:53 -07:00
bymyself
527770d8e5 fix(#10988): register replacement node widgets in WidgetValueStore and update link types
- Register new node's widgets via setNodeId after replacement, mirroring
  what graph.add() does. Without this, Nodes 2.0 (Vue) reads defaults
  from the store instead of the transferred values.
- Update link.type in transferOutputConnections to match the new output
  slot type, fixing port color mismatches after replacement.

Fixes #10988
2026-04-10 18:16:53 -07:00
2 changed files with 110 additions and 5 deletions

View File

@@ -45,6 +45,14 @@ vi.mock('@/i18n', () => ({
params ? `${key}:${JSON.stringify(params)}` : key
}))
vi.mock('@/lib/litegraph/src/utils/type', () => ({
isNodeBindable: (widget: unknown): boolean =>
typeof widget === 'object' &&
widget !== null &&
'setNodeId' in widget &&
typeof (widget as Record<string, unknown>).setNodeId === 'function'
}))
const { mockRemoveMissingNodesByType } = vi.hoisted(() => ({
mockRemoveMissingNodesByType: vi.fn()
}))
@@ -137,7 +145,7 @@ function createPlaceholderNode(
function createNewNode(
inputs: { name: string; link: number | null }[] = [],
outputs: { name: string; links: number[] | null }[] = [],
outputs: { name: string; links: number[] | null; type?: string }[] = [],
widgets: { name: string; value: unknown }[] = []
): LGraphNode {
return fromAny<LGraphNode, unknown>({
@@ -150,8 +158,16 @@ function createNewNode(
flags: {},
has_errors: false,
inputs: inputs.map((i) => ({ ...i, type: 'IMAGE' })),
outputs: outputs.map((o) => ({ ...o, type: 'IMAGE' })),
widgets: widgets.map((w) => ({ ...w, type: 'combo', options: {} })),
outputs: outputs.map((o) => ({
...o,
type: o.type ?? 'IMAGE'
})),
widgets: widgets.map((w) => ({
...w,
type: 'combo',
options: {},
setNodeId: vi.fn()
})),
configure: vi.fn(),
serialize: vi.fn()
})
@@ -660,6 +676,83 @@ describe('useNodeReplacement', () => {
// Should still succeed (dot-notation skipped gracefully)
expect(result).toEqual(['ImageBatch'])
})
it('should register new node widgets with WidgetValueStore via setNodeId', () => {
const placeholder = createPlaceholderNode(1, 'OldNode')
placeholder.last_serialization!.widgets_values = [42, 'hello']
const graph = createMockGraph([placeholder])
placeholder.graph = graph
Object.assign(app, { rootGraph: graph })
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
const newNode = createNewNode(
[],
[],
[
{ name: 'exposure', value: 0 },
{ name: 'annotation', value: '' }
]
)
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
const { replaceNodesInPlace } = useNodeReplacement()
replaceNodesInPlace([
makeMissingNodeType('OldNode', {
new_node_id: 'NewNode',
old_node_id: 'OldNode',
old_widget_ids: ['gain', 'label'],
input_mapping: [
{ new_id: 'exposure', old_id: 'gain' },
{ new_id: 'annotation', old_id: 'label' }
],
output_mapping: null
})
])
// Each widget's setNodeId should be called with the node ID
for (const widget of newNode.widgets!) {
const bindable = widget as unknown as {
setNodeId: ReturnType<typeof vi.fn>
}
expect(bindable.setNodeId).toHaveBeenCalledWith(1)
}
})
it('should update link type to match new output slot type', () => {
const link = createMockLink(20, 1, 0, 5, 0)
const placeholder = createPlaceholderNode(
1,
'OldNode',
[],
[{ name: 'output_image', links: [20] }]
)
const graph = createMockGraph([placeholder], [link])
placeholder.graph = graph
Object.assign(app, { rootGraph: graph })
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
const newNode = createNewNode(
[],
[{ name: 'image_mean', links: null, type: 'FLOAT' }]
)
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
const { replaceNodesInPlace } = useNodeReplacement()
replaceNodesInPlace([
makeMissingNodeType('OldNode', {
new_node_id: 'NewNode',
old_node_id: 'OldNode',
old_widget_ids: null,
input_mapping: null,
output_mapping: [{ new_idx: 0, old_idx: 0 }]
})
])
expect(link.type).toBe('FLOAT')
})
})
describe('placeholder detection predicate', () => {

View File

@@ -2,6 +2,7 @@ import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
import type { TWidgetValue } from '@/lib/litegraph/src/types/widgets'
import { isNodeBindable } from '@/lib/litegraph/src/utils/type'
import { t } from '@/i18n'
import type { NodeReplacement } from '@/platform/nodeReplacement/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
@@ -62,15 +63,18 @@ function transferOutputConnections(
): void {
const oldLinks = oldNode.outputs?.[oldOutputIdx]?.links
if (!oldLinks?.length) return
if (!newNode.outputs?.[newOutputIdx]) return
const newOutput = newNode.outputs?.[newOutputIdx]
if (!newOutput) return
for (const linkId of oldLinks) {
const link = graph.links.get(linkId)
if (!link) continue
link.origin_id = newNode.id
link.origin_slot = newOutputIdx
link.type = newOutput.type ?? link.type
}
newNode.outputs[newOutputIdx].links = [...oldLinks]
newOutput.links = [...oldLinks]
oldNode.outputs[oldOutputIdx].links = []
}
@@ -218,6 +222,14 @@ function replaceWithMapping(
}
}
// Register the new node's widgets with the WidgetValueStore.
// replaceWithMapping bypasses graph.add(), which normally handles this
// registration. Without it, Nodes 2.0 (Vue) reads default/missing values
// from the store instead of the transferred widget values.
for (const widget of newNode.widgets ?? []) {
if (isNodeBindable(widget)) widget.setNodeId(newNode.id)
}
newNode.has_errors = false
}