mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 01:20:03 +00:00
fix: subgraph output slot labels not updating in v2 renderer (#9266)
## Summary Custom names set on subgraph output nodes are ignored in the v2 renderer — it always shows the data type name (e.g. "texts") instead of the user-defined label. Works correctly in v1. ## Changes - **What**: Made `outputs` in `extractVueNodeData` reactive via `shallowReactive` + `defineProperty` (matching the existing `inputs` pattern). Added a `node:slot-label:changed` graph trigger that `SubgraphNode` fires when input/output labels are renamed, so the Vue layer picks up the change. ## Review Focus - The `outputs` reactivity mirrors `inputs` exactly — same `shallowReactive` + setter pattern. The new trigger event forces `shallowReactive` to detect the deep property change by re-assigning the array. - Also handles input label renames for consistency, even though the current bug report is output-specific. ## Screenshots **v1 — output correctly shows custom label "output_text":** <img width="1076" height="628" alt="Screenshot 2026-02-26 at 4 43 00 PM" src="https://github.com/user-attachments/assets/b4d6ae4c-9970-4d99-a872-4ce1b28522f2" /> **v2 before fix — output shows type name "texts" instead of custom label:** <img width="808" height="298" alt="Screenshot 2026-02-26 at 4 43 30 PM" src="https://github.com/user-attachments/assets/cf06aa6c-6d4d-4be9-9bcd-dcc072ed1907" /> **v2 after fix — output correctly shows "output_text":** <img width="1013" height="292" alt="Screenshot 2026-02-26 at 5 14 44 PM" src="https://github.com/user-attachments/assets/3c43fa9b-0615-4758-bee6-be3481168675" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9266-fix-subgraph-output-slot-labels-not-updating-in-v2-renderer-3146d73d365081979327fd775a6ef62b) by [Unito](https://www.unito.io)
This commit is contained in:
committed by
GitHub
parent
9447a1f5d6
commit
5640eb7d92
@@ -243,6 +243,78 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Subgraph output slot label reactivity', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
it('updates output slot labels when node:slot-label:changed is triggered', async () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('test')
|
||||
node.addOutput('original_name', 'STRING')
|
||||
node.addOutput('other_name', 'STRING')
|
||||
graph.add(node)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeId = String(node.id)
|
||||
const nodeData = vueNodeData.get(nodeId)
|
||||
if (!nodeData?.outputs) throw new Error('Expected output data to exist')
|
||||
|
||||
expect(nodeData.outputs[0].label).toBeUndefined()
|
||||
expect(nodeData.outputs[1].label).toBeUndefined()
|
||||
|
||||
// Simulate what SubgraphNode does: set the label, then fire the trigger
|
||||
node.outputs[0].label = 'custom_label'
|
||||
graph.trigger('node:slot-label:changed', {
|
||||
nodeId: node.id,
|
||||
slotType: NodeSlotType.OUTPUT
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
const updatedData = vueNodeData.get(nodeId)
|
||||
expect(updatedData?.outputs?.[0]?.label).toBe('custom_label')
|
||||
expect(updatedData?.outputs?.[1]?.label).toBeUndefined()
|
||||
})
|
||||
|
||||
it('updates input slot labels when node:slot-label:changed is triggered', async () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('test')
|
||||
node.addInput('original_name', 'STRING')
|
||||
graph.add(node)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeId = String(node.id)
|
||||
const nodeData = vueNodeData.get(nodeId)
|
||||
if (!nodeData?.inputs) throw new Error('Expected input data to exist')
|
||||
|
||||
expect(nodeData.inputs[0].label).toBeUndefined()
|
||||
|
||||
node.inputs[0].label = 'custom_label'
|
||||
graph.trigger('node:slot-label:changed', {
|
||||
nodeId: node.id,
|
||||
slotType: NodeSlotType.INPUT
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
const updatedData = vueNodeData.get(nodeId)
|
||||
expect(updatedData?.inputs?.[0]?.label).toBe('custom_label')
|
||||
})
|
||||
|
||||
it('ignores node:slot-label:changed for unknown node ids', () => {
|
||||
const graph = new LGraph()
|
||||
useGraphNodeManager(graph)
|
||||
|
||||
expect(() =>
|
||||
graph.trigger('node:slot-label:changed', {
|
||||
nodeId: 'missing-node',
|
||||
slotType: NodeSlotType.OUTPUT
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Subgraph Promoted Pseudo Widgets', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
|
||||
@@ -396,7 +396,9 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
|
||||
},
|
||||
set(v) {
|
||||
reactiveWidgets.splice(0, reactiveWidgets.length, ...v)
|
||||
}
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
})
|
||||
}
|
||||
const reactiveInputs = shallowReactive<INodeInputSlot[]>(node.inputs ?? [])
|
||||
@@ -406,7 +408,20 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
|
||||
},
|
||||
set(v) {
|
||||
reactiveInputs.splice(0, reactiveInputs.length, ...v)
|
||||
}
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
})
|
||||
const reactiveOutputs = shallowReactive<INodeOutputSlot[]>(node.outputs ?? [])
|
||||
Object.defineProperty(node, 'outputs', {
|
||||
get() {
|
||||
return reactiveOutputs
|
||||
},
|
||||
set(v) {
|
||||
reactiveOutputs.splice(0, reactiveOutputs.length, ...v)
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
})
|
||||
|
||||
const safeWidgets = reactiveComputed<SafeWidgetData[]>(() => {
|
||||
@@ -448,7 +463,7 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
|
||||
hasErrors: !!node.has_errors,
|
||||
widgets: safeWidgets,
|
||||
inputs: reactiveInputs,
|
||||
outputs: node.outputs ? [...node.outputs] : undefined,
|
||||
outputs: reactiveOutputs,
|
||||
flags: node.flags ? { ...node.flags } : undefined,
|
||||
color: node.color || undefined,
|
||||
bgcolor: node.bgcolor || undefined,
|
||||
@@ -746,6 +761,20 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
if (slotLinksEvent.slotType === NodeSlotType.INPUT) {
|
||||
refreshNodeSlots(String(slotLinksEvent.nodeId))
|
||||
}
|
||||
},
|
||||
'node:slot-label:changed': (slotLabelEvent) => {
|
||||
const nodeId = String(slotLabelEvent.nodeId)
|
||||
const nodeRef = nodeRefs.get(nodeId)
|
||||
if (!nodeRef) return
|
||||
|
||||
// Force shallowReactive to detect the deep property change
|
||||
// by re-assigning the affected array through the defineProperty setter.
|
||||
if (slotLabelEvent.slotType !== NodeSlotType.OUTPUT && nodeRef.inputs) {
|
||||
nodeRef.inputs = [...nodeRef.inputs]
|
||||
}
|
||||
if (slotLabelEvent.slotType !== NodeSlotType.INPUT && nodeRef.outputs) {
|
||||
nodeRef.outputs = [...nodeRef.outputs]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,6 +789,9 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
case 'node:slot-links:changed':
|
||||
triggerHandlers['node:slot-links:changed'](event)
|
||||
break
|
||||
case 'node:slot-label:changed':
|
||||
triggerHandlers['node:slot-label:changed'](event)
|
||||
break
|
||||
}
|
||||
|
||||
// Chain to original handler
|
||||
|
||||
@@ -1336,7 +1336,8 @@ export class LGraph
|
||||
const validEventTypes = new Set([
|
||||
'node:slot-links:changed',
|
||||
'node:slot-errors:changed',
|
||||
'node:property:changed'
|
||||
'node:property:changed',
|
||||
'node:slot-label:changed'
|
||||
])
|
||||
|
||||
if (validEventTypes.has(action) && param && typeof param === 'object') {
|
||||
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
ExportedSubgraphInstance,
|
||||
ISerialisedNode
|
||||
} from '@/lib/litegraph/src/types/serialisation'
|
||||
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import {
|
||||
@@ -441,6 +442,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
if (input._widget) {
|
||||
input._widget.label = newName
|
||||
}
|
||||
this.graph?.trigger('node:slot-label:changed', {
|
||||
nodeId: this.id,
|
||||
slotType: NodeSlotType.INPUT
|
||||
})
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
@@ -453,6 +458,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
if (!output) throw new Error('Subgraph output not found')
|
||||
|
||||
output.label = newName
|
||||
this.graph?.trigger('node:slot-label:changed', {
|
||||
nodeId: this.id,
|
||||
slotType: NodeSlotType.OUTPUT
|
||||
})
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
|
||||
@@ -23,10 +23,17 @@ interface NodeSlotLinksChangedEvent {
|
||||
linkId: number
|
||||
}
|
||||
|
||||
interface NodeSlotLabelChangedEvent {
|
||||
type: 'node:slot-label:changed'
|
||||
nodeId: NodeId
|
||||
slotType?: NodeSlotType
|
||||
}
|
||||
|
||||
export type LGraphTriggerEvent =
|
||||
| NodePropertyChangedEvent
|
||||
| NodeSlotErrorsChangedEvent
|
||||
| NodeSlotLinksChangedEvent
|
||||
| NodeSlotLabelChangedEvent
|
||||
|
||||
export type LGraphTriggerAction = LGraphTriggerEvent['type']
|
||||
|
||||
|
||||
Reference in New Issue
Block a user