mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
fix Vue node widgets should be in disabled state if their slots are connected with a link (#5834)
## Summary Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/5692 by making widget link connection status trigger on change so Vue widgets with connected links could properly switch to the `disabled` state when they are implicitly converted to inputs. ## Changes - **What**: Added `node:slot-links:changed` event tracking and reactive slot data synchronization for Vue widgets ```mermaid graph TD A[Widget Link Change] --> B[NodeInputSlot.link setter] B --> C{Is Widget Input?} C -->|Yes| D[Trigger slot-links:changed] C -->|No| E[End] D --> F[Graph Event Handler] F --> G[syncNodeSlotData] G --> H[Update Vue Reactive Data] H --> I[Widget Re-render] style A fill:#f9f9f9,stroke:#333,color:#000 style I fill:#f9f9f9,stroke:#333,color:#000 ``` ## Review Focus Widget reactivity performance with frequent link changes and event handler memory management in graph operations. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5834-fix-Vue-node-widgets-should-be-in-disabled-state-if-their-slots-are-connected-with-a-link-27c6d73d365081f6a6c3c1ddc3905c5e) by [Unito](https://www.unito.io)
This commit is contained in:
90
browser_tests/assets/vueNodes/linked-int-widget.json
Normal file
90
browser_tests/assets/vueNodes/linked-int-widget.json
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"id": "95ea19ba-456c-46e8-aa40-dc3ff135b746",
|
||||
"revision": 0,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 10,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "KSampler",
|
||||
"pos": [494.3333740234375, 142.3333282470703],
|
||||
"size": [444, 399],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "seed",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "seed"
|
||||
},
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [67, "randomize", 20, 8, "euler", "simple", 1]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "PrimitiveInt",
|
||||
"pos": [24.333343505859375, 149.6666717529297],
|
||||
"size": [444, 125],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [10]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveInt"
|
||||
},
|
||||
"widgets_values": [67, "randomize"]
|
||||
}
|
||||
],
|
||||
"links": [[10, 11, 0, 10, 4, "INT"]],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
},
|
||||
"frontendVersion": "1.28.6"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -119,4 +119,24 @@ export class VueNodeHelpers {
|
||||
await this.page.waitForSelector('[data-node-id]')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific widget by node title and widget name
|
||||
*/
|
||||
getWidgetByName(nodeTitle: string, widgetName: string): Locator {
|
||||
return this.getNodeByTitle(nodeTitle).locator(
|
||||
`_vue=[widget.name="${widgetName}"]`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controls for input number widgets (increment/decrement buttons and input)
|
||||
*/
|
||||
getInputNumberControls(widget: Locator) {
|
||||
return {
|
||||
input: widget.locator('input'),
|
||||
incrementButton: widget.locator('button').first(),
|
||||
decrementButton: widget.locator('button').last()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Integer Widget', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('should be disabled and not allow changing value when link connected to slot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('vueNodes/linked-int-widget')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const seedWidget = comfyPage.vueNodes.getWidgetByName('KSampler', 'seed')
|
||||
const controls = comfyPage.vueNodes.getInputNumberControls(seedWidget)
|
||||
const initialValue = Number(await controls.input.inputValue())
|
||||
|
||||
// Verify widget is disabled when linked
|
||||
await controls.incrementButton.click({ force: true })
|
||||
await expect(controls.input).toHaveValue(initialValue.toString())
|
||||
|
||||
await controls.decrementButton.click({ force: true })
|
||||
await expect(controls.input).toHaveValue(initialValue.toString())
|
||||
|
||||
await expect(seedWidget).toBeVisible()
|
||||
|
||||
// Delete the node that is linked to the slot (freeing up the widget)
|
||||
await comfyPage.vueNodes.getNodeByTitle('Int').click()
|
||||
await comfyPage.vueNodes.deleteSelected()
|
||||
|
||||
// Test widget works when unlinked
|
||||
await controls.incrementButton.click()
|
||||
await expect(controls.input).toHaveValue((initialValue + 1).toString())
|
||||
|
||||
await controls.decrementButton.click()
|
||||
await expect(controls.input).toHaveValue(initialValue.toString())
|
||||
})
|
||||
})
|
||||
@@ -15,7 +15,19 @@ import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import type { WidgetValue } from '@/types/simplifiedWidget'
|
||||
|
||||
import type { LGraph, LGraphNode } from '../../lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
LGraphTriggerAction,
|
||||
LGraphTriggerEvent,
|
||||
LGraphTriggerParam
|
||||
} from '../../lib/litegraph/src/litegraph'
|
||||
import { NodeSlotType } from '../../lib/litegraph/src/types/globalEnums'
|
||||
|
||||
export interface WidgetSlotMetadata {
|
||||
index: number
|
||||
linked: boolean
|
||||
}
|
||||
|
||||
export interface SafeWidgetData {
|
||||
name: string
|
||||
@@ -25,6 +37,7 @@ export interface SafeWidgetData {
|
||||
options?: Record<string, unknown>
|
||||
callback?: ((value: unknown) => void) | undefined
|
||||
spec?: InputSpec
|
||||
slotMetadata?: WidgetSlotMetadata
|
||||
}
|
||||
|
||||
export interface VueNodeData {
|
||||
@@ -68,6 +81,37 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
// Non-reactive storage for original LiteGraph nodes
|
||||
const nodeRefs = new Map<string, LGraphNode>()
|
||||
|
||||
const refreshNodeSlots = (nodeId: string) => {
|
||||
const nodeRef = nodeRefs.get(nodeId)
|
||||
const currentData = vueNodeData.get(nodeId)
|
||||
|
||||
if (!nodeRef || !currentData) return
|
||||
|
||||
// Only extract slot-related data instead of full node re-extraction
|
||||
const slotMetadata = new Map<string, WidgetSlotMetadata>()
|
||||
|
||||
nodeRef.inputs?.forEach((input, index) => {
|
||||
if (!input?.widget?.name) return
|
||||
slotMetadata.set(input.widget.name, {
|
||||
index,
|
||||
linked: input.link != null
|
||||
})
|
||||
})
|
||||
|
||||
// Update only widgets with new slot metadata, keeping other widget data intact
|
||||
const updatedWidgets = currentData.widgets?.map((widget) => {
|
||||
const slotInfo = slotMetadata.get(widget.name)
|
||||
return slotInfo ? { ...widget, slotMetadata: slotInfo } : widget
|
||||
})
|
||||
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
widgets: updatedWidgets,
|
||||
inputs: nodeRef.inputs ? [...nodeRef.inputs] : undefined,
|
||||
outputs: nodeRef.outputs ? [...nodeRef.outputs] : undefined
|
||||
})
|
||||
}
|
||||
|
||||
// Extract safe data from LiteGraph node for Vue consumption
|
||||
const extractVueNodeData = (node: LGraphNode): VueNodeData => {
|
||||
// Determine subgraph ID - null for root graph, string for subgraphs
|
||||
@@ -76,6 +120,16 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
? String(node.graph.id)
|
||||
: null
|
||||
// Extract safe widget data
|
||||
const slotMetadata = new Map<string, WidgetSlotMetadata>()
|
||||
|
||||
node.inputs?.forEach((input, index) => {
|
||||
if (!input?.widget?.name) return
|
||||
slotMetadata.set(input.widget.name, {
|
||||
index,
|
||||
linked: input.link != null
|
||||
})
|
||||
})
|
||||
|
||||
const safeWidgets = node.widgets?.map((widget) => {
|
||||
try {
|
||||
// TODO: Use widget.getReactiveData() once TypeScript types are updated
|
||||
@@ -92,6 +146,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
value = widget.options.values[0]
|
||||
}
|
||||
const spec = nodeDefStore.getInputSpecForWidget(node, widget.name)
|
||||
const slotInfo = slotMetadata.get(widget.name)
|
||||
|
||||
return {
|
||||
name: widget.name,
|
||||
@@ -100,7 +155,8 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
label: widget.label,
|
||||
options: widget.options ? { ...widget.options } : undefined,
|
||||
callback: widget.callback,
|
||||
spec
|
||||
spec,
|
||||
slotMetadata: slotInfo
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
@@ -375,7 +431,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
const createCleanupFunction = (
|
||||
originalOnNodeAdded: ((node: LGraphNode) => void) | undefined,
|
||||
originalOnNodeRemoved: ((node: LGraphNode) => void) | undefined,
|
||||
originalOnTrigger: ((action: string, param: unknown) => void) | undefined
|
||||
originalOnTrigger: ((event: LGraphTriggerEvent) => void) | undefined
|
||||
) => {
|
||||
return () => {
|
||||
// Restore original callbacks
|
||||
@@ -407,29 +463,19 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
handleNodeRemoved(node, originalOnNodeRemoved)
|
||||
}
|
||||
|
||||
// Listen for property change events from instrumented nodes
|
||||
graph.onTrigger = (action: string, param: unknown) => {
|
||||
if (
|
||||
action === 'node:property:changed' &&
|
||||
param &&
|
||||
typeof param === 'object'
|
||||
) {
|
||||
const event = param as {
|
||||
nodeId: string | number
|
||||
property: string
|
||||
oldValue: unknown
|
||||
newValue: unknown
|
||||
}
|
||||
|
||||
const nodeId = String(event.nodeId)
|
||||
const triggerHandlers: {
|
||||
[K in LGraphTriggerAction]: (event: LGraphTriggerParam<K>) => void
|
||||
} = {
|
||||
'node:property:changed': (propertyEvent) => {
|
||||
const nodeId = String(propertyEvent.nodeId)
|
||||
const currentData = vueNodeData.get(nodeId)
|
||||
|
||||
if (currentData) {
|
||||
switch (event.property) {
|
||||
switch (propertyEvent.property) {
|
||||
case 'title':
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
title: String(event.newValue)
|
||||
title: String(propertyEvent.newValue)
|
||||
})
|
||||
break
|
||||
case 'flags.collapsed':
|
||||
@@ -437,7 +483,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
...currentData,
|
||||
flags: {
|
||||
...currentData.flags,
|
||||
collapsed: Boolean(event.newValue)
|
||||
collapsed: Boolean(propertyEvent.newValue)
|
||||
}
|
||||
})
|
||||
break
|
||||
@@ -446,22 +492,25 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
...currentData,
|
||||
flags: {
|
||||
...currentData.flags,
|
||||
pinned: Boolean(event.newValue)
|
||||
pinned: Boolean(propertyEvent.newValue)
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'mode':
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
mode: typeof event.newValue === 'number' ? event.newValue : 0
|
||||
mode:
|
||||
typeof propertyEvent.newValue === 'number'
|
||||
? propertyEvent.newValue
|
||||
: 0
|
||||
})
|
||||
break
|
||||
case 'color':
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
color:
|
||||
typeof event.newValue === 'string'
|
||||
? event.newValue
|
||||
typeof propertyEvent.newValue === 'string'
|
||||
? propertyEvent.newValue
|
||||
: undefined
|
||||
})
|
||||
break
|
||||
@@ -469,40 +518,38 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
bgcolor:
|
||||
typeof event.newValue === 'string'
|
||||
? event.newValue
|
||||
typeof propertyEvent.newValue === 'string'
|
||||
? propertyEvent.newValue
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
action === 'node:slot-errors:changed' &&
|
||||
param &&
|
||||
typeof param === 'object'
|
||||
) {
|
||||
const event = param as { nodeId: string | number }
|
||||
const nodeId = String(event.nodeId)
|
||||
const litegraphNode = nodeRefs.get(nodeId)
|
||||
const currentData = vueNodeData.get(nodeId)
|
||||
|
||||
if (litegraphNode && currentData) {
|
||||
// Re-extract slot data with updated hasErrors properties
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
inputs: litegraphNode.inputs
|
||||
? [...litegraphNode.inputs]
|
||||
: undefined,
|
||||
outputs: litegraphNode.outputs
|
||||
? [...litegraphNode.outputs]
|
||||
: undefined
|
||||
})
|
||||
},
|
||||
'node:slot-errors:changed': (slotErrorsEvent) => {
|
||||
refreshNodeSlots(String(slotErrorsEvent.nodeId))
|
||||
},
|
||||
'node:slot-links:changed': (slotLinksEvent) => {
|
||||
if (slotLinksEvent.slotType === NodeSlotType.INPUT) {
|
||||
refreshNodeSlots(String(slotLinksEvent.nodeId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call original trigger handler if it exists
|
||||
if (originalOnTrigger) {
|
||||
originalOnTrigger(action, param)
|
||||
graph.onTrigger = (event: LGraphTriggerEvent) => {
|
||||
switch (event.type) {
|
||||
case 'node:property:changed':
|
||||
triggerHandlers['node:property:changed'](event)
|
||||
break
|
||||
case 'node:slot-errors:changed':
|
||||
triggerHandlers['node:slot-errors:changed'](event)
|
||||
break
|
||||
case 'node:slot-links:changed':
|
||||
triggerHandlers['node:slot-links:changed'](event)
|
||||
break
|
||||
}
|
||||
|
||||
// Chain to original handler
|
||||
originalOnTrigger?.(event)
|
||||
}
|
||||
|
||||
// Initialize state
|
||||
|
||||
@@ -57,6 +57,12 @@ import {
|
||||
splitPositionables
|
||||
} from './subgraph/subgraphUtils'
|
||||
import { Alignment, LGraphEventMode } from './types/globalEnums'
|
||||
import type {
|
||||
LGraphTriggerAction,
|
||||
LGraphTriggerEvent,
|
||||
LGraphTriggerHandler,
|
||||
LGraphTriggerParam
|
||||
} from './types/graphTriggers'
|
||||
import type {
|
||||
ExportedSubgraph,
|
||||
ExposedWidget,
|
||||
@@ -68,6 +74,11 @@ import type {
|
||||
} from './types/serialisation'
|
||||
import { getAllNestedItems } from './utils/collections'
|
||||
|
||||
export type {
|
||||
LGraphTriggerAction,
|
||||
LGraphTriggerParam
|
||||
} from './types/graphTriggers'
|
||||
|
||||
export interface LGraphState {
|
||||
lastGroupId: number
|
||||
lastNodeId: number
|
||||
@@ -257,7 +268,7 @@ export class LGraph
|
||||
onExecuteStep?(): void
|
||||
onNodeAdded?(node: LGraphNode): void
|
||||
onNodeRemoved?(node: LGraphNode): void
|
||||
onTrigger?(action: string, param: unknown): void
|
||||
onTrigger?: LGraphTriggerHandler
|
||||
onBeforeChange?(graph: LGraph, info?: LGraphNode): void
|
||||
onAfterChange?(graph: LGraph, info?: LGraphNode | null): void
|
||||
onConnectionChange?(node: LGraphNode): void
|
||||
@@ -1183,8 +1194,23 @@ export class LGraph
|
||||
}
|
||||
|
||||
// ********** GLOBALS *****************
|
||||
trigger<A extends LGraphTriggerAction>(
|
||||
action: A,
|
||||
param: LGraphTriggerParam<A>
|
||||
): void
|
||||
trigger(action: string, param: unknown): void
|
||||
trigger(action: string, param: unknown) {
|
||||
this.onTrigger?.(action, param)
|
||||
// Convert to discriminated union format for typed handlers
|
||||
const validEventTypes = new Set([
|
||||
'node:slot-links:changed',
|
||||
'node:slot-errors:changed',
|
||||
'node:property:changed'
|
||||
])
|
||||
|
||||
if (validEventTypes.has(action) && param && typeof param === 'object') {
|
||||
this.onTrigger?.({ type: action, ...param } as LGraphTriggerEvent)
|
||||
}
|
||||
// Don't handle unknown events - just ignore them
|
||||
}
|
||||
|
||||
/** @todo Clean up - never implemented. */
|
||||
|
||||
@@ -2851,7 +2851,17 @@ export class LGraphNode
|
||||
output.links ??= []
|
||||
output.links.push(link.id)
|
||||
// connect in input
|
||||
inputNode.inputs[inputIndex].link = link.id
|
||||
const targetInput = inputNode.inputs[inputIndex]
|
||||
targetInput.link = link.id
|
||||
if (targetInput.widget) {
|
||||
graph.trigger('node:slot-links:changed', {
|
||||
nodeId: inputNode.id,
|
||||
slotType: NodeSlotType.INPUT,
|
||||
slotIndex: inputIndex,
|
||||
connected: true,
|
||||
linkId: link.id
|
||||
})
|
||||
}
|
||||
|
||||
// Reroutes
|
||||
const reroutes = LLink.getReroutes(graph, link)
|
||||
@@ -3008,6 +3018,15 @@ export class LGraphNode
|
||||
const input = target.inputs[link_info.target_slot]
|
||||
// remove there
|
||||
input.link = null
|
||||
if (input.widget) {
|
||||
graph.trigger('node:slot-links:changed', {
|
||||
nodeId: target.id,
|
||||
slotType: NodeSlotType.INPUT,
|
||||
slotIndex: link_info.target_slot,
|
||||
connected: false,
|
||||
linkId: link_info.id
|
||||
})
|
||||
}
|
||||
|
||||
// remove the link from the links pool
|
||||
link_info.disconnect(graph, 'input')
|
||||
@@ -3044,6 +3063,15 @@ export class LGraphNode
|
||||
const input = target.inputs[link_info.target_slot]
|
||||
// remove other side link
|
||||
input.link = null
|
||||
if (input.widget) {
|
||||
graph.trigger('node:slot-links:changed', {
|
||||
nodeId: target.id,
|
||||
slotType: NodeSlotType.INPUT,
|
||||
slotIndex: link_info.target_slot,
|
||||
connected: false,
|
||||
linkId: link_info.id
|
||||
})
|
||||
}
|
||||
|
||||
// link_info hasn't been modified so its ok
|
||||
target.onConnectionsChange?.(
|
||||
@@ -3113,6 +3141,15 @@ export class LGraphNode
|
||||
const link_id = this.inputs[slot].link
|
||||
if (link_id != null) {
|
||||
this.inputs[slot].link = null
|
||||
if (input.widget) {
|
||||
graph.trigger('node:slot-links:changed', {
|
||||
nodeId: this.id,
|
||||
slotType: NodeSlotType.INPUT,
|
||||
slotIndex: slot,
|
||||
connected: false,
|
||||
linkId: link_id
|
||||
})
|
||||
}
|
||||
|
||||
// remove other side
|
||||
const link_info = graph._links.get(link_id)
|
||||
|
||||
@@ -102,7 +102,12 @@ export type {
|
||||
Positionable,
|
||||
Size
|
||||
} from './interfaces'
|
||||
export { LGraph } from './LGraph'
|
||||
export {
|
||||
LGraph,
|
||||
type LGraphTriggerAction,
|
||||
type LGraphTriggerParam
|
||||
} from './LGraph'
|
||||
export type { LGraphTriggerEvent } from './types/graphTriggers'
|
||||
export { BadgePosition, LGraphBadge } from './LGraphBadge'
|
||||
export { LGraphCanvas } from './LGraphCanvas'
|
||||
export { LGraphGroup } from './LGraphGroup'
|
||||
|
||||
38
src/lib/litegraph/src/types/graphTriggers.ts
Normal file
38
src/lib/litegraph/src/types/graphTriggers.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { NodeId } from '../LGraphNode'
|
||||
import type { NodeSlotType } from './globalEnums'
|
||||
|
||||
interface NodePropertyChangedEvent {
|
||||
type: 'node:property:changed'
|
||||
nodeId: NodeId
|
||||
property: string
|
||||
oldValue: unknown
|
||||
newValue: unknown
|
||||
}
|
||||
|
||||
interface NodeSlotErrorsChangedEvent {
|
||||
type: 'node:slot-errors:changed'
|
||||
nodeId: NodeId
|
||||
}
|
||||
|
||||
interface NodeSlotLinksChangedEvent {
|
||||
type: 'node:slot-links:changed'
|
||||
nodeId: NodeId
|
||||
slotType: NodeSlotType
|
||||
slotIndex: number
|
||||
connected: boolean
|
||||
linkId: number
|
||||
}
|
||||
|
||||
export type LGraphTriggerEvent =
|
||||
| NodePropertyChangedEvent
|
||||
| NodeSlotErrorsChangedEvent
|
||||
| NodeSlotLinksChangedEvent
|
||||
|
||||
export type LGraphTriggerAction = LGraphTriggerEvent['type']
|
||||
|
||||
export type LGraphTriggerParam<A extends LGraphTriggerAction> = Extract<
|
||||
LGraphTriggerEvent,
|
||||
{ type: A }
|
||||
>
|
||||
|
||||
export type LGraphTriggerHandler = (event: LGraphTriggerEvent) => void
|
||||
@@ -95,19 +95,19 @@ export function useSlotLayoutSync() {
|
||||
}
|
||||
}
|
||||
|
||||
graph.onTrigger = (action: string, param: any) => {
|
||||
graph.onTrigger = (event) => {
|
||||
if (
|
||||
action === 'node:property:changed' &&
|
||||
param?.property === 'flags.collapsed'
|
||||
event.type === 'node:property:changed' &&
|
||||
event.property === 'flags.collapsed'
|
||||
) {
|
||||
const node = graph.getNodeById(parseInt(String(param.nodeId)))
|
||||
const node = graph.getNodeById(parseInt(String(event.nodeId)))
|
||||
if (node) {
|
||||
computeAndRegisterSlots(node)
|
||||
}
|
||||
}
|
||||
if (origTrigger) {
|
||||
origTrigger.call(graph, action, param)
|
||||
}
|
||||
|
||||
// Chain to original handler
|
||||
origTrigger?.(event)
|
||||
}
|
||||
|
||||
graph.onAfterChange = (graph: any, node?: any) => {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
boundingRect: [0, 0, 0, 0]
|
||||
}"
|
||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||
:index="getWidgetInputIndex(widget)"
|
||||
:index="widget.slotMetadata?.index ?? 0"
|
||||
:dot-only="true"
|
||||
/>
|
||||
</div>
|
||||
@@ -56,12 +56,12 @@ import { computed, onErrorCaptured, ref } from 'vue'
|
||||
|
||||
import type {
|
||||
SafeWidgetData,
|
||||
VueNodeData
|
||||
VueNodeData,
|
||||
WidgetSlotMetadata
|
||||
} from '@/composables/graph/useGraphNodeManager'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
// Import widget components directly
|
||||
import WidgetInputText from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue'
|
||||
import {
|
||||
getComponent,
|
||||
@@ -110,6 +110,7 @@ interface ProcessedWidget {
|
||||
value: WidgetValue
|
||||
updateHandler: (value: unknown) => void
|
||||
tooltipConfig: any
|
||||
slotMetadata?: WidgetSlotMetadata
|
||||
}
|
||||
|
||||
const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
@@ -126,12 +127,24 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
|
||||
const vueComponent = getComponent(widget.type) || WidgetInputText
|
||||
|
||||
const slotMetadata = widget.slotMetadata
|
||||
|
||||
let widgetOptions = widget.options
|
||||
// Core feature: Disable Vue widgets when their input slots are connected
|
||||
// This prevents conflicting input sources - when a slot is linked to another
|
||||
// node's output, the widget should be read-only to avoid data conflicts
|
||||
if (slotMetadata?.linked) {
|
||||
widgetOptions = widget.options
|
||||
? { ...widget.options, disabled: true }
|
||||
: { disabled: true }
|
||||
}
|
||||
|
||||
const simplified: SimplifiedWidget = {
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
value: widget.value,
|
||||
label: widget.label,
|
||||
options: widget.options,
|
||||
options: widgetOptions,
|
||||
callback: widget.callback,
|
||||
spec: widget.spec
|
||||
}
|
||||
@@ -152,25 +165,11 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
simplified,
|
||||
value: widget.value,
|
||||
updateHandler,
|
||||
tooltipConfig
|
||||
tooltipConfig,
|
||||
slotMetadata
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// TODO: Refactor to avoid O(n) lookup - consider storing input index on widget creation
|
||||
// or restructuring data model to unify widgets and inputs
|
||||
// Map a widget to its corresponding input slot index
|
||||
const getWidgetInputIndex = (widget: ProcessedWidget): number => {
|
||||
const inputs = nodeData?.inputs
|
||||
if (!inputs) return 0
|
||||
|
||||
const idx = inputs.findIndex((input: any) => {
|
||||
if (!input || typeof input !== 'object') return false
|
||||
if (!('name' in input && 'type' in input)) return false
|
||||
return 'widget' in input && input.widget?.name === widget.name
|
||||
})
|
||||
return idx >= 0 ? idx : 0
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -90,6 +90,7 @@ const buttonTooltip = computed(() => {
|
||||
:step="stepValue"
|
||||
:use-grouping="useGrouping"
|
||||
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
|
||||
:aria-label="widget.name"
|
||||
:pt="{
|
||||
incrementButton:
|
||||
'!rounded-r-lg bg-transparent border-none hover:bg-zinc-500/30 active:bg-zinc-500/40',
|
||||
|
||||
@@ -283,6 +283,7 @@ LGraph {
|
||||
"nodes_actioning": [],
|
||||
"nodes_executedAction": [],
|
||||
"nodes_executing": [],
|
||||
"onTrigger": undefined,
|
||||
"revision": 0,
|
||||
"runningtime": 0,
|
||||
"starttime": 0,
|
||||
|
||||
Reference in New Issue
Block a user