fix: preserve user widget order through _syncPromotions

Replace linked-first ordering in _buildPromotionPersistenceState with
an order-preserving merge that keeps existing store entries in their
current position while pruning stale entries and appending new ones.

This fixes a regression from 74a48ab2 where removing the
shouldPersistLinkedOnly guard caused _syncPromotions to always
overwrite user-reordered widget order with linked-first ordering.

Fixes Notion: Bug: Subgraph widget reorder causes UI and panel mismatch
This commit is contained in:
dante01yoon
2026-04-07 16:00:02 +09:00
parent 84f401bbe9
commit 2f1ac34395
2 changed files with 74 additions and 5 deletions

View File

@@ -840,8 +840,52 @@ describe('SubgraphNode.widgets getter', () => {
subgraphNode.id
)
expect(promotions).toStrictEqual([
{ sourceNodeId: String(linkedNodeA.id), sourceWidgetName: 'string_a' },
{ sourceNodeId: String(independentNode.id), sourceWidgetName: 'string_a' }
{
sourceNodeId: String(independentNode.id),
sourceWidgetName: 'string_a'
},
{ sourceNodeId: String(linkedNodeA.id), sourceWidgetName: 'string_a' }
])
})
test('syncPromotions preserves user-reordered store order', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'widget_a', type: '*' }]
})
const subgraphNode = createTestSubgraphNode(subgraph, { id: 97 })
subgraphNode.graph?.add(subgraphNode)
const linkedNode = new LGraphNode('LinkedNode')
const linkedInput = linkedNode.addInput('widget_a', '*')
linkedNode.addWidget('text', 'widget_a', 'val_a', () => {})
linkedInput.widget = { name: 'widget_a' }
subgraph.add(linkedNode)
subgraph.inputNode.slots[0].connect(linkedInput, linkedNode)
const independentNode = new LGraphNode('IndependentNode')
independentNode.addWidget('text', 'indep_widget', 'val_b', () => {})
subgraph.add(independentNode)
// Simulate user reorder: independent first, then linked
setPromotions(subgraphNode, [
[String(independentNode.id), 'indep_widget'],
[String(linkedNode.id), 'widget_a']
])
callSyncPromotions(subgraphNode)
const promotions = usePromotionStore().getPromotions(
subgraphNode.rootGraph.id,
subgraphNode.id
)
// Order must be preserved: independent first, linked second
expect(promotions).toStrictEqual([
{
sourceNodeId: String(independentNode.id),
sourceWidgetName: 'indep_widget'
},
{ sourceNodeId: String(linkedNode.id), sourceWidgetName: 'widget_a' }
])
})

View File

@@ -385,13 +385,38 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
fallbackStoredEntries
)
const desiredEntries = shouldPersistLinkedOnly
? linkedPromotionEntries
: [...linkedPromotionEntries, ...fallbackStoredEntries]
return {
mergedEntries: shouldPersistLinkedOnly
? linkedPromotionEntries
: [...linkedPromotionEntries, ...fallbackStoredEntries]
mergedEntries: this._orderPreservingMerge(entries, desiredEntries)
}
}
private _orderPreservingMerge(
currentEntries: PromotedWidgetSource[],
desiredEntries: PromotedWidgetSource[]
): PromotedWidgetSource[] {
const makeKey = (e: PromotedWidgetSource) =>
this._makePromotionEntryKey(
e.sourceNodeId,
e.sourceWidgetName,
e.disambiguatingSourceNodeId
)
const desiredByKey = new Map(desiredEntries.map((e) => [makeKey(e), e]))
const currentKeys = new Set(currentEntries.map(makeKey))
const preserved = currentEntries
.filter((e) => desiredByKey.has(makeKey(e)))
.map((e) => desiredByKey.get(makeKey(e))!)
const added = desiredEntries.filter((e) => !currentKeys.has(makeKey(e)))
return [...preserved, ...added]
}
private _collectLinkedAndFallbackEntries(
entries: PromotedWidgetSource[],
linkedEntries: LinkedPromotionEntry[]