fix: unify slot-promoted widgets with PromotedWidgetSlot system

Replace -1 nodeId entries in proxyWidgets with real interior node IDs.
Slot-promoted widget copies from _setWidget() now carry sourceNodeId
and sourceWidgetName properties, enabling syncPromotedWidgets to
convert all entries uniformly to PromotedWidgetSlot instances.

- Add sourceNodeId/sourceWidgetName to _setWidget() copies
- Pass interiorNode through all _setWidget() call sites
- syncPromotedWidgets: convert legacy -1 entries and uncovered copies
  to PromotedWidgetSlots
- proxyWidgets getter: emit real IDs from copies
- Remove -1 special cases from TabSubgraphInputs and SubgraphEditor
- Remove redundant widgets_values restoration for -1 entries

Amp-Thread-ID: https://ampcode.com/threads/T-019c54e5-7ece-752b-8d00-c993fab9ee8e
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-02-12 19:24:53 -08:00
parent 75b5606890
commit eb269aeee6
4 changed files with 117 additions and 48 deletions

View File

@@ -84,13 +84,6 @@ const widgetsList = computed((): NodeWidgetsList => {
const result: NodeWidgetsList = []
for (const [nodeId, widgetName] of proxyWidgetsOrder) {
if (nodeId === '-1') {
// Native widget on the SubgraphNode itself
const widget = node.widgets?.find((w) => w.name === widgetName)
if (widget) result.push({ node, widget })
continue
}
// Resolve the interior node and widget from the subgraph
const interiorNode = node.subgraph.getNodeById(nodeId)
if (!interiorNode) continue
const widget = interiorNode.widgets?.find((w) => w.name === widgetName)

View File

@@ -70,11 +70,6 @@ const activeWidgets = computed<WidgetItem[]>({
if (!activeNode.value) return []
const node = activeNode.value
function mapWidgets([id, name]: [string, string]): WidgetItem[] {
if (id === '-1') {
const widget = node.widgets.find((w) => w.name === name)
if (!widget) return []
return [[{ id: -1, title: '(Linked)', type: '' }, widget]]
}
const wNode = node.subgraph._nodes_by_id[id]
if (!wNode?.widgets) return []
const widget = wNode.widgets.find((w) => w.name === name)
@@ -172,10 +167,21 @@ function showAll() {
function hideAll() {
const node = activeNode.value
if (!node) return
// Slot-promoted widgets (exposed via SubgraphInput links) can't be hidden
const slotPromoted = new Set<string>()
for (const slot of node.subgraph.inputNode.slots) {
for (const linkId of slot.linkIds) {
const link = node.subgraph.getLink(linkId)
if (!link) continue
const { inputNode, input } = link.resolve(node.subgraph)
if (!inputNode || !input?.widget?.name) continue
slotPromoted.add(`${inputNode.id}:${input.widget.name}`)
}
}
proxyWidgets.value = proxyWidgets.value.filter(
(propertyItem) =>
!filteredActive.value.some(matchesWidgetItem(propertyItem)) ||
propertyItem[0] === '-1'
slotPromoted.has(`${propertyItem[0]}:${propertyItem[1]}`)
)
}
function showRecommended() {

View File

@@ -56,32 +56,71 @@ function syncPromotedWidgets(
const canvasStore = useCanvasStore()
const parsed = parseProxyWidgets(property)
// Snapshot native widgets before filtering so we can restore them
const widgets = node.widgets ?? []
const nativeWidgets = widgets.filter(
(w) => !(w instanceof PromotedWidgetSlot)
)
// Remove existing PromotedWidgetSlot instances and native widgets
// that will be re-ordered by the parsed list.
// Dispose DOM adapters on slots being removed.
// Dispose DOM adapters on existing PromotedWidgetSlots being removed.
for (const w of widgets) {
if (w instanceof PromotedWidgetSlot) w.disposeDomAdapter()
}
node.widgets = widgets.filter((w) => {
if (w instanceof PromotedWidgetSlot) return false
return !parsed.some(([, name]) => w.name === name)
// Collect slot-promoted copies created by _setWidget() during configure.
// These have sourceNodeId/sourceWidgetName set via Object.defineProperties.
const copies = widgets.filter(
(
w
): w is IBaseWidget & { sourceNodeId: string; sourceWidgetName: string } =>
!(w instanceof PromotedWidgetSlot) &&
'sourceNodeId' in w &&
'sourceWidgetName' in w
)
// Remove all promoted widgets (both PromotedWidgetSlots and copies)
node.widgets = widgets.filter(
(w) =>
!(w instanceof PromotedWidgetSlot) &&
!(copies as IBaseWidget[]).includes(w)
)
// Track which source widgets are covered by the parsed list
const covered = new Set<string>()
// Create PromotedWidgetSlots for all parsed entries.
// Legacy `-1` entries are resolved to real IDs via the copies.
const newSlots: IBaseWidget[] = parsed.flatMap(([nodeId, widgetName]) => {
let resolvedNodeId = nodeId
let resolvedWidgetName = widgetName
if (nodeId === '-1') {
const copy = copies.find((w) => w.name === widgetName)
if (!copy) return []
resolvedNodeId = copy.sourceNodeId
resolvedWidgetName = copy.sourceWidgetName
}
covered.add(`${resolvedNodeId}:${resolvedWidgetName}`)
return [
new PromotedWidgetSlot(
node as SubgraphNode,
resolvedNodeId,
resolvedWidgetName
)
]
})
// Create new PromotedWidgetSlot for each promoted entry
const newSlots: IBaseWidget[] = parsed.flatMap(([nodeId, widgetName]) => {
if (nodeId === '-1') {
const widget = nativeWidgets.find((w) => w.name === widgetName)
return widget ? [widget] : []
}
return [new PromotedWidgetSlot(node as SubgraphNode, nodeId, widgetName)]
})
// Add PromotedWidgetSlots for any copies not in the parsed list
// (e.g. old workflows that didn't serialize slot-promoted entries)
for (const copy of copies) {
const key = `${copy.sourceNodeId}:${copy.sourceWidgetName}`
if (covered.has(key)) continue
newSlots.unshift(
new PromotedWidgetSlot(
node as SubgraphNode,
copy.sourceNodeId,
copy.sourceWidgetName
)
)
}
node.widgets.push(...newSlots)
canvasStore.canvas?.setDirty(true, true)
@@ -103,21 +142,17 @@ const onConfigure = function (
Object.defineProperty(this.properties, 'proxyWidgets', {
get: () =>
this.widgets.map((w) =>
w instanceof PromotedWidgetSlot
? [w.sourceNodeId, w.sourceWidgetName]
: ['-1', w.name]
),
this.widgets.map((w) => {
if (w instanceof PromotedWidgetSlot)
return [w.sourceNodeId, w.sourceWidgetName]
if ('sourceNodeId' in w && 'sourceWidgetName' in w)
return [String(w.sourceNodeId), String(w.sourceWidgetName)]
return ['-1', w.name]
}),
set: (value: NodeProperty) => syncPromotedWidgets(this, value)
})
if (serialisedNode.properties?.proxyWidgets) {
syncPromotedWidgets(this, serialisedNode.properties.proxyWidgets)
const parsed = parseProxyWidgets(serialisedNode.properties.proxyWidgets)
serialisedNode.widgets_values?.forEach((v, index) => {
if (parsed[index]?.[0] !== '-1') return
const widget = this.widgets.find((w) => w.name == parsed[index][1])
if (v !== null && widget) widget.value = v
})
}
}

View File

@@ -93,8 +93,14 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const linkId = subgraphInput.linkIds[0]
const { inputNode, input } = subgraph.links[linkId].resolve(subgraph)
const widget = inputNode?.widgets?.find?.((w) => w.name == name)
if (widget)
this._setWidget(subgraphInput, existingInput, widget, input?.widget)
if (widget && inputNode)
this._setWidget(
subgraphInput,
existingInput,
widget,
input?.widget,
inputNode
)
return
}
const input = this.addInput(name, type)
@@ -206,7 +212,19 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
if (!widget) return
const widgetLocator = e.detail.input.widget
this._setWidget(subgraphInput, input, widget, widgetLocator)
// Resolve the interior node from the subgraph input's link
const linkId = subgraphInput.linkIds[0]
const link = linkId != null ? this.subgraph.getLink(linkId) : undefined
const interiorNode = link?.resolve(this.subgraph).inputNode
if (!interiorNode) return
this._setWidget(
subgraphInput,
input,
widget,
widgetLocator,
interiorNode
)
},
{ signal }
)
@@ -323,7 +341,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const widget = inputNode.getWidgetFromSlot(targetInput)
if (!widget) continue
this._setWidget(subgraphInput, input, widget, targetInput.widget)
this._setWidget(
subgraphInput,
input,
widget,
targetInput.widget,
inputNode
)
break
}
}
@@ -333,7 +357,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
subgraphInput: Readonly<SubgraphInput>,
input: INodeInputSlot,
widget: Readonly<IBaseWidget>,
inputWidget: IWidgetLocator | undefined
inputWidget: IWidgetLocator | undefined,
interiorNode: LGraphNode
) {
// Use the first matching widget
const promotedWidget =
@@ -349,6 +374,16 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
// and copies the result as a data property.
const sourceWidget = widget as IBaseWidget
Object.defineProperties(promotedWidget, {
sourceNodeId: {
value: String(interiorNode.id),
configurable: true,
enumerable: true
},
sourceWidgetName: {
value: sourceWidget.name,
configurable: true,
enumerable: true
},
name: {
get: () => subgraphInput.name,
set() {},