Compare commits

...

2 Commits

Author SHA1 Message Date
bymyself
f28ccf2b5f fix: widgets disappear when reordering in SubgraphEditor properties panel
SubgraphEditor.vue reordered widgets via activeWidgets, a lossy computed
that resolves [nodeId, widgetName] tuples to widget objects then converts
back. When widgets can't be resolved (e.g. during graph mutations),
entries are silently dropped.

Fix by reordering proxyWidgets.value directly (the source of truth),
matching how TabSubgraphInputs.vue already handles this correctly.

Amp-Thread-ID: https://ampcode.com/threads/T-019c73f9-0348-715e-9ef1-f6add8677e27
2026-02-19 20:37:47 -08:00
bymyself
3a4c76bf98 test: add proxyWidgets reordering tests
Add tests documenting that:
- Direct reordering of proxyWidgets tuples preserves all entries
- Widget references remain correct after reorder
- The activeWidgets round-trip is lossy when nodes are unresolvable

Amp-Thread-ID: https://ampcode.com/threads/T-019c73f9-0348-715e-9ef1-f6add8677e27
2026-02-19 20:37:27 -08:00
2 changed files with 99 additions and 7 deletions

View File

@@ -196,7 +196,7 @@ function setDraggableState() {
'.draggable-item'
)
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems = []
const reorderedItems: HTMLElement[] = []
let oldPosition = -1
this.getAllItems().forEach((item, index) => {
@@ -212,17 +212,27 @@ function setDraggableState() {
reorderedItems[newIndex] = item
})
if (oldPosition === -1) {
console.error('[SubgraphEditor] draggableItem not found in items')
return
}
for (let index = 0; index < this.getAllItems().length; index++) {
const item = reorderedItems[index]
if (typeof item === 'undefined') {
reorderedItems[index] = this.draggableItem
reorderedItems[index] = this.draggableItem as HTMLElement
}
}
const newPosition = reorderedItems.indexOf(this.draggableItem)
const aw = activeWidgets.value
const [w] = aw.splice(oldPosition, 1)
aw.splice(newPosition, 0, w)
activeWidgets.value = aw
const newPosition = reorderedItems.indexOf(
this.draggableItem as HTMLElement
)
const pw = proxyWidgets.value
const [w] = pw.splice(oldPosition, 1)
pw.splice(newPosition, 0, w)
proxyWidgets.value = pw
triggerRef(proxyWidgets)
}
}
watch(filteredActive, () => {

View File

@@ -125,6 +125,88 @@ describe('Subgraph proxyWidgets', () => {
subgraphNode.widgets[0].computedHeight = 10
expect(subgraphNode.widgets[0].value).toBe('value')
})
describe('proxyWidgets reordering', () => {
test('reordering proxyWidgets directly preserves all entries', () => {
const [subgraphNode, innerNodes] = setupSubgraph(2)
innerNodes[0].addWidget('text', 'widgetA', 'valueA', () => {})
innerNodes[1].addWidget('text', 'widgetB', 'valueB', () => {})
subgraphNode.properties.proxyWidgets = [
['1', 'widgetA'],
['2', 'widgetB']
]
expect(subgraphNode.widgets).toHaveLength(2)
const proxyWidgets = parseProxyWidgets(
subgraphNode.properties.proxyWidgets
)
const [first, second] = proxyWidgets
subgraphNode.properties.proxyWidgets = [second, first]
expect(subgraphNode.properties.proxyWidgets).toStrictEqual([
['2', 'widgetB'],
['1', 'widgetA']
])
expect(subgraphNode.widgets).toHaveLength(2)
})
test('reordering maintains correct widget references after swap', () => {
const [subgraphNode, innerNodes] = setupSubgraph(2)
innerNodes[0].addWidget('text', 'widgetA', 'valueA', () => {})
innerNodes[1].addWidget('text', 'widgetB', 'valueB', () => {})
subgraphNode.properties.proxyWidgets = [
['1', 'widgetA'],
['2', 'widgetB']
]
expect(subgraphNode.widgets[0].value).toBe('valueA')
expect(subgraphNode.widgets[1].value).toBe('valueB')
const proxyWidgets = parseProxyWidgets(
subgraphNode.properties.proxyWidgets
)
subgraphNode.properties.proxyWidgets = [proxyWidgets[1], proxyWidgets[0]]
expect(subgraphNode.widgets[0].value).toBe('valueB')
expect(subgraphNode.widgets[1].value).toBe('valueA')
})
test('activeWidgets round-trip drops unresolvable widgets', () => {
const [subgraphNode, innerNodes] = setupSubgraph(2)
innerNodes[0].addWidget('text', 'widgetA', 'valueA', () => {})
innerNodes[1].addWidget('text', 'widgetB', 'valueB', () => {})
subgraphNode.properties.proxyWidgets = [
['1', 'widgetA'],
['2', 'widgetB']
]
expect(subgraphNode.widgets).toHaveLength(2)
// Simulate the lossy activeWidgets getter: resolving [nodeId, widgetName]
// tuples by looking up nodes/widgets. If a node is missing, it returns [].
const subgraph = subgraphNode.subgraph
function mapWidgets([id, name]: [string, string]) {
const wNode = subgraph._nodes_by_id[id]
if (!wNode?.widgets) return []
const widget = wNode.widgets.find((w) => w.name === name)
if (!widget) return []
return [[wNode, widget]]
}
// Remove a node to make it unresolvable
subgraph.remove(innerNodes[0])
const proxyWidgets = parseProxyWidgets(
subgraphNode.properties.proxyWidgets
)
const resolved = proxyWidgets.flatMap(mapWidgets)
// The lossy round-trip drops the entry for the removed node
expect(resolved).toHaveLength(1)
expect(proxyWidgets).toHaveLength(2)
})
})
test('Prevents duplicate promotion', () => {
const [subgraphNode, innerNodes] = setupSubgraph(1)
innerNodes[0].addWidget('text', 'stringWidget', 'value', () => {})