fix: stabilize nested subgraph promoted widget resolution (#9282)

## Summary

Fix multiple issues with promoted widget resolution in nested subgraphs,
ensuring correct value propagation, slot matching, and rendering for
deeply nested promoted widgets.

## Changes

- **What**: Stabilize nested subgraph promoted widget resolution chain
- Use deep source keys for promoted widget values in Vue rendering mode
- Resolve effective widget options from the source widget instead of the
promoted view
  - Stabilize slot resolution for nested promoted widgets
  - Preserve combo value rendering for promoted subgraph widgets
- Prevent subgraph definition deletion while other nodes still reference
the same type
  - Clean up unused exported resolution types

## Review Focus

- `resolveConcretePromotedWidget.ts` — new recursive resolution logic
for deeply nested promoted widgets
- `useGraphNodeManager.ts` — option extraction now uses
`effectiveWidget` for promoted widgets
- `SubgraphNode.ts` — unpack no longer force-deletes definitions
referenced by other nodes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9282-fix-stabilize-nested-subgraph-promoted-widget-resolution-3146d73d365081208a4fe931bb7569cf)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Alexander Brown
2026-02-28 13:45:04 -08:00
committed by GitHub
parent 0ab3fdc2c9
commit dd1a1f77d6
24 changed files with 2866 additions and 147 deletions

View File

@@ -184,6 +184,8 @@ const processedWidgets = computed((): ProcessedWidget[] => {
for (const widget of widgets) {
if (!shouldRenderAsVue(widget)) continue
const isPromotedView = !!widget.nodeId
const vueComponent =
getComponent(widget.type) ||
(widget.isDOMWidget ? WidgetDOM : WidgetLegacy)
@@ -191,9 +193,12 @@ const processedWidgets = computed((): ProcessedWidget[] => {
const { slotMetadata } = widget
// Get metadata from store (registered during BaseWidget.setNodeId)
const bareWidgetId = stripGraphPrefix(widget.nodeId ?? nodeId)
const bareWidgetId = stripGraphPrefix(
widget.storeNodeId ?? widget.nodeId ?? nodeId
)
const storeWidgetName = widget.storeName ?? widget.name
const widgetState = graphId
? widgetValueStore.getWidget(graphId, bareWidgetId, widget.name)
? widgetValueStore.getWidget(graphId, bareWidgetId, storeWidgetName)
: undefined
// Get value from store (falls back to undefined if not registered)
@@ -205,7 +210,6 @@ const processedWidgets = computed((): ProcessedWidget[] => {
? { ...storeOptions, disabled: true }
: storeOptions
const isPromotedView = !!widget.nodeId
const borderStyle =
graphId &&
!isPromotedView &&

View File

@@ -96,6 +96,38 @@ describe('resolveWidgetFromHostNode', () => {
expect(resolved).toEqual({ node: innerNode, widget: innerWidget })
})
it('resolves nested promoted widget chain to deepest interior widget', () => {
const innerWidget = createWidget('inner_text')
const innerNode = createHostNode([innerWidget])
const middleNode = createHostNode([], {
isSubgraphNode: true,
innerNodesById: { '100': innerNode }
})
const middlePromotedWidget = {
...createPromotedWidget('inner_text', '100', 'inner_text'),
node: middleNode
} as TestPromotedWidget & { node: LGraphNode }
middleNode.widgets = [middlePromotedWidget]
const outerPromotedWidget = createPromotedWidget(
'outer_text',
'42',
'inner_text'
)
const hostNode = createHostNode([outerPromotedWidget], {
isSubgraphNode: true,
innerNodesById: { '42': middleNode }
})
const resolved = resolveWidgetFromHostNode(
hostNode,
outerPromotedWidget.name
)
expect(resolved).toEqual({ node: innerNode, widget: innerWidget })
})
it('returns undefined when promoted interior node is missing', () => {
const promotedWidget = createPromotedWidget(
'promoted_text',