mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 22:25:05 +00:00
refactor(useGraphNodeManager): derive widget slot linked from upstream resolvability
Replace the SUBGRAPH_INPUT_ID sentinel check in buildSlotMetadata with a check that the upstream node actually resolves via getNodeById. Promoted widgets (link origin = SUBGRAPH_INPUT sentinel) naturally render as editable instead of disabled with a phantom upstream chip, and the renderer no longer imports a litegraph sentinel constant. Test setup now uses a real upstream node + upstream.connect() instead of an unreferenced input.link id. Added regression test for the non-resolvable-upstream case. Amp-Thread-ID: https://ampcode.com/threads/T-019e3d8c-b6b4-731e-b3fb-9bdb6fefb1ae Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -102,12 +102,17 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
const input = node.addInput('prompt', 'STRING')
|
||||
// Associate the input slot with the widget (as widgetInputs extension does)
|
||||
input.widget = { name: 'prompt' }
|
||||
|
||||
// Start with a connected link
|
||||
input.link = 42
|
||||
|
||||
graph.add(node)
|
||||
return { graph, node }
|
||||
|
||||
// Real upstream node + real link — the renderer treats a slot as
|
||||
// `linked` only when the upstream resolves to an actual node.
|
||||
const upstream = new LGraphNode('upstream')
|
||||
upstream.addOutput('out', 'STRING')
|
||||
graph.add(upstream)
|
||||
const link = upstream.connect(0, node, 0)
|
||||
if (!link) throw new Error('Expected upstream.connect to produce a link')
|
||||
|
||||
return { graph, node, upstream, linkId: link.id }
|
||||
}
|
||||
|
||||
it('sets slotMetadata.linked to true when input has a link', () => {
|
||||
@@ -187,7 +192,28 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('updates slotMetadata for promoted widgets where SafeWidgetData.name differs from input.widget.name', async () => {
|
||||
it('does not mark a slot as linked when the link references a non-resolvable upstream (e.g. SubgraphInput sentinel)', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('test')
|
||||
node.addWidget('string', 'prompt', 'hello', () => undefined, {})
|
||||
const input = node.addInput('prompt', 'STRING')
|
||||
input.widget = { name: 'prompt' }
|
||||
// Reference a link that doesn't resolve to a real upstream node — mirrors
|
||||
// the subgraph promotion case where origin_id === SUBGRAPH_INPUT_ID and
|
||||
// getNodeById returns null. The renderer must treat the slot as editable
|
||||
// rather than disabled-with-phantom-upstream.
|
||||
input.link = 9999
|
||||
graph.add(node)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeData = vueNodeData.get(String(node.id))
|
||||
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
|
||||
|
||||
expect(widgetData?.slotMetadata?.linked).toBe(false)
|
||||
expect(widgetData?.slotMetadata?.originNodeId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('resolves slotMetadata for promoted widgets where SafeWidgetData.name differs from input.widget.name', () => {
|
||||
// Set up a subgraph with an interior node that has a "prompt" widget.
|
||||
// createPromotedWidgetView resolves against this interior node.
|
||||
const subgraph = createTestSubgraph()
|
||||
@@ -217,7 +243,6 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
hostNode.widgets = [promotedView]
|
||||
const input = hostNode.addInput('value', 'STRING')
|
||||
input.widget = { name: 'value' }
|
||||
input.link = 42
|
||||
graph.add(hostNode)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
@@ -228,21 +253,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
|
||||
expect(widgetData).toBeDefined()
|
||||
expect(widgetData?.slotName).toBe('value')
|
||||
expect(widgetData?.slotMetadata?.linked).toBe(true)
|
||||
|
||||
// Disconnect
|
||||
hostNode.inputs[0].link = null
|
||||
graph.trigger('node:slot-links:changed', {
|
||||
nodeId: hostNode.id,
|
||||
slotType: NodeSlotType.INPUT,
|
||||
slotIndex: 0,
|
||||
connected: false,
|
||||
linkId: 42
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(widgetData?.slotMetadata?.linked).toBe(false)
|
||||
expect(widgetData?.slotMetadata).toBeDefined()
|
||||
})
|
||||
|
||||
it('prefers exact _widget input matches before same-name fallbacks for promoted widgets', () => {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
resolvePromotedWidgetSource
|
||||
} from '@/core/graph/subgraph/resolveConcretePromotedWidget'
|
||||
import { resolveSubgraphInputTarget } from '@/core/graph/subgraph/resolveSubgraphInputTarget'
|
||||
import { SUBGRAPH_INPUT_ID } from '@/lib/litegraph/src/constants'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot
|
||||
@@ -387,25 +386,21 @@ function buildSlotMetadata(
|
||||
inputs?.forEach((input, index) => {
|
||||
let originNodeId: string | undefined
|
||||
let originOutputName: string | undefined
|
||||
// Promotion via SubgraphInput materialises a real link from the
|
||||
// SUBGRAPH_INPUT sentinel into the interior widget's input slot.
|
||||
// That link is internal plumbing — not an external connection — so
|
||||
// exclude it from `linked` (which downstream renders as disabled).
|
||||
let isPromotionLink = false
|
||||
|
||||
if (input.link != null && graphRef) {
|
||||
const link = graphRef.getLink(input.link)
|
||||
if (link) {
|
||||
// Resolvability gate: SubgraphInput-sentinel origins return null here,
|
||||
// so promoted widgets stay editable instead of disabled with no chip.
|
||||
const originNode = link ? graphRef.getNodeById(link.origin_id) : null
|
||||
if (link && originNode) {
|
||||
originNodeId = String(link.origin_id)
|
||||
const originNode = graphRef.getNodeById(link.origin_id)
|
||||
originOutputName = originNode?.outputs?.[link.origin_slot]?.name
|
||||
isPromotionLink = link.origin_id === SUBGRAPH_INPUT_ID
|
||||
originOutputName = originNode.outputs?.[link.origin_slot]?.name
|
||||
}
|
||||
}
|
||||
|
||||
const slotInfo: WidgetSlotMetadata = {
|
||||
index,
|
||||
linked: input.link != null && !isPromotionLink,
|
||||
linked: originNodeId != null,
|
||||
originNodeId,
|
||||
originOutputName,
|
||||
type: String(input.type)
|
||||
|
||||
Reference in New Issue
Block a user