fix: preserve edge connection points in YAML export/import (#208)

YAML round-trip dropped every edge's handles: export never wrote them
and import hardcoded sourceHandle='bottom'/targetHandle='top-t'. After
import all connections collapsed onto slot 0 ("converge at a single
point"). Per-side handle counts were dropped too, so the extra bottom
slots did not even exist to attach to.

- yaml types: add sourceHandle/targetHandle to connections and
  top/bottom/left/rightHandles to nodes
- export: write each edge's real handles + per-side counts (only above
  the side default); orient parent-edge handles parent->child
- import: restore handle counts onto node data and use the stored
  handles, falling back to the legacy defaults for pre-existing YAML

Positions remain dagre-managed (unchanged); this restores connection
points only.

ha-relevant: yes
This commit is contained in:
Pouzor
2026-07-07 11:16:29 +02:00
parent 65a1f49f93
commit 7bc3e5d8a0
5 changed files with 148 additions and 7 deletions
@@ -181,4 +181,31 @@ describe('exportCanvasToYaml', () => {
expect(yamlStr).toContain('4000')
expect(yamlStr).toContain('star')
})
// Regression for issue #208: connection points were dropped on export, so
// every edge collapsed onto slot 0 after import.
it('preserves edge connection points (source/target handles) in links', () => {
const sw = makeNode({ label: 'Switch', type: 'switch', bottom_handles: 3 }, 'sw')
const s1 = makeNode({ label: 'Server1', type: 'server' }, 's1')
const edge: Edge<EdgeData> = {
...makeEdge('e1', 'sw', 's1', { type: 'ethernet' }),
sourceHandle: 'bottom-3',
targetHandle: 'top-t',
}
const result = yaml.load(exportCanvasToYaml([sw, s1], [edge])) as Record<string, unknown>[]
const swEntry = result.find((e) => e.label === 'Switch')!
const link = (swEntry.links as Record<string, unknown>[])[0]
expect(link.sourceHandle).toBe('bottom-3')
expect(link.targetHandle).toBe('top-t')
})
it('exports per-side handle counts only when above the side default', () => {
const nodes = [makeNode({ label: 'N', type: 'server', bottom_handles: 4, right_handles: 2, top_handles: 1, left_handles: 0 })]
const entry = (yaml.load(exportCanvasToYaml(nodes, [])) as Record<string, unknown>[])[0]
expect(entry.bottomHandles).toBe(4)
expect(entry.rightHandles).toBe(2)
// top default is 1, left default is 0 → omitted.
expect(entry).not.toHaveProperty('topHandles')
expect(entry).not.toHaveProperty('leftHandles')
})
})
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { parseYamlToCanvas } from '../importYaml'
import { exportCanvasToYaml } from '../exportYaml'
import type { Node, Edge } from '@xyflow/react'
import type { NodeData, EdgeData } from '@/types'
@@ -306,4 +307,65 @@ describe('parseYamlToCanvas', () => {
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('NonexistentHost'))
warnSpy.mockRestore()
})
// Regression for issue #208.
it('restores edge connection points from the YAML link', () => {
const yaml = `
- nodeType: switch
label: "Switch"
bottomHandles: 3
links:
- label: "Server1"
linkType: ethernet
sourceHandle: bottom-3
targetHandle: top-2-t
- nodeType: server
label: "Server1"
`
const { nodes, edges } = parseYamlToCanvas(yaml, empty, emptyEdges)
expect(edges).toHaveLength(1)
expect(edges[0].sourceHandle).toBe('bottom-3')
expect(edges[0].targetHandle).toBe('top-2-t')
const sw = nodes.find((n) => n.data.label === 'Switch')!
expect(sw.data.bottom_handles).toBe(3)
})
it('falls back to legacy handles when the YAML link omits them', () => {
const yaml = `
- nodeType: switch
label: "Switch"
links:
- label: "Server1"
linkType: ethernet
- nodeType: server
label: "Server1"
`
const { edges } = parseYamlToCanvas(yaml, empty, emptyEdges)
expect(edges[0].sourceHandle).toBe('bottom')
expect(edges[0].targetHandle).toBe('top-t')
})
it('round-trips connection points through export → import (issue #208)', () => {
const sw: Node<NodeData> = {
id: 'sw', type: 'switch', position: { x: 0, y: 0 },
data: { label: 'Switch', type: 'switch', status: 'online', services: [], bottom_handles: 3 },
}
const s1: Node<NodeData> = {
id: 's1', type: 'server', position: { x: 0, y: 0 },
data: { label: 'Server1', type: 'server', status: 'online', services: [] },
}
const edge: Edge<EdgeData> = {
id: 'e1', source: 'sw', target: 's1',
sourceHandle: 'bottom-3', targetHandle: 'top-t',
data: { type: 'ethernet' } as EdgeData,
}
const yamlStr = exportCanvasToYaml([sw, s1], [edge])
const { nodes, edges } = parseYamlToCanvas(yamlStr, empty, emptyEdges)
expect(edges).toHaveLength(1)
expect(edges[0].sourceHandle).toBe('bottom-3')
expect(edges[0].targetHandle).toBe('top-t')
expect(nodes.find((n) => n.data.label === 'Switch')!.data.bottom_handles).toBe(3)
})
})