feat: selectable marker shapes per edge endpoint
Replace the on/off arrowhead toggle with a per-end shape picker. Each end (start/end) independently selects: none, arrow, arrow-open, circle, diamond, or square. Markers still recolor live from the resolved stroke color. Frontend: - MarkerShape type + edgeMarkers util (normalizeMarker, MARKER_GEOMETRY); legacy boolean coerces to 'arrow' on read. - Per-shape <marker> inner geometry; symmetric shapes use fixed orient. - MarkerShapePicker reused in EdgeModal and CustomStyleModal. - Serializer normalizes to shape strings. Backend: - Edge marker columns Boolean -> String (default 'none'); TEXT migration. - normalize_marker() + validators coerce legacy bool / unknown values. ha-relevant: yes
This commit is contained in:
@@ -343,12 +343,12 @@ describe('canvasStore', () => {
|
||||
expect(edges[0].id).not.toBe(edges[1].id)
|
||||
})
|
||||
|
||||
it('onConnect preserves arrow markers from edge data', () => {
|
||||
const conn = Object.assign({ source: 'n1', target: 'n2', sourceHandle: null, targetHandle: null }, { type: 'ethernet', marker_start: true, marker_end: true })
|
||||
it('onConnect preserves endpoint marker shapes from edge data', () => {
|
||||
const conn = Object.assign({ source: 'n1', target: 'n2', sourceHandle: null, targetHandle: null }, { type: 'ethernet', marker_start: 'diamond', marker_end: 'arrow' })
|
||||
useCanvasStore.getState().onConnect(conn)
|
||||
const { edges } = useCanvasStore.getState()
|
||||
expect(edges[0].data?.marker_start).toBe(true)
|
||||
expect(edges[0].data?.marker_end).toBe(true)
|
||||
expect(edges[0].data?.marker_start).toBe('diamond')
|
||||
expect(edges[0].data?.marker_end).toBe('arrow')
|
||||
})
|
||||
|
||||
it('onConnect preserves sourceHandle and targetHandle for cluster edges', () => {
|
||||
@@ -1320,15 +1320,15 @@ describe('canvasStore — custom style apply', () => {
|
||||
const e2: Edge<EdgeData> = { id: 'e2', source: 'n1', target: 'n2', type: 'wifi', data: { type: 'wifi' } }
|
||||
useCanvasStore.setState({ nodes: [], edges: [e1, e2] })
|
||||
|
||||
useCanvasStore.getState().applyTypeEdgeStyle('ethernet', { color: '#00ff00', opacity: 1, pathStyle: 'smooth', animated: 'flow', arrowStart: true, arrowEnd: true })
|
||||
useCanvasStore.getState().applyTypeEdgeStyle('ethernet', { color: '#00ff00', opacity: 1, pathStyle: 'smooth', animated: 'flow', arrowStart: 'circle', arrowEnd: 'arrow' })
|
||||
|
||||
const updated1 = useCanvasStore.getState().edges.find((e) => e.id === 'e1')!
|
||||
const updated2 = useCanvasStore.getState().edges.find((e) => e.id === 'e2')!
|
||||
expect(updated1.data?.custom_color).toBe('#00ff00')
|
||||
expect(updated1.data?.path_style).toBe('smooth')
|
||||
expect(updated1.data?.animated).toBe('flow')
|
||||
expect(updated1.data?.marker_start).toBe(true)
|
||||
expect(updated1.data?.marker_end).toBe(true)
|
||||
expect(updated1.data?.marker_start).toBe('circle')
|
||||
expect(updated1.data?.marker_end).toBe('arrow')
|
||||
expect(updated2.data?.custom_color).toBeUndefined()
|
||||
expect(updated2.data?.marker_end).toBeUndefined()
|
||||
})
|
||||
@@ -1344,7 +1344,7 @@ describe('canvasStore — custom style apply', () => {
|
||||
proxmox: { borderColor: '#ff6e00', borderOpacity: 1, bgColor: '#111', bgOpacity: 1, iconColor: '#ff6e00', iconOpacity: 1, width: 0, height: 0 },
|
||||
},
|
||||
edges: {
|
||||
ethernet: { color: '#aabbcc', opacity: 1, pathStyle: 'bezier', animated: 'none', arrowStart: false, arrowEnd: true },
|
||||
ethernet: { color: '#aabbcc', opacity: 1, pathStyle: 'bezier', animated: 'none', arrowStart: 'none', arrowEnd: 'square' },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1354,8 +1354,8 @@ describe('canvasStore — custom style apply', () => {
|
||||
expect(np.data.custom_colors?.border).toBe('#ff6e00')
|
||||
expect(ns.data.custom_colors?.border).toBeUndefined()
|
||||
expect(e.data?.custom_color).toBe('#aabbcc')
|
||||
expect(e.data?.marker_end).toBe(true)
|
||||
expect(e.data?.marker_start).toBe(false)
|
||||
expect(e.data?.marker_end).toBe('square')
|
||||
expect(e.data?.marker_start).toBe('none')
|
||||
expect(useCanvasStore.getState().hasUnsavedChanges).toBe(true)
|
||||
})
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('themeStore', () => {
|
||||
it('setCustomStyle replaces the entire definition', () => {
|
||||
const def: CustomStyleDef = {
|
||||
nodes: { server: { borderColor: '#ff0000', borderOpacity: 1, bgColor: '#000000', bgOpacity: 1, iconColor: '#ff0000', iconOpacity: 1, width: 200, height: 80 } },
|
||||
edges: { ethernet: { color: '#00ff00', opacity: 0.8, pathStyle: 'bezier', animated: 'none', arrowStart: false, arrowEnd: false } },
|
||||
edges: { ethernet: { color: '#00ff00', opacity: 0.8, pathStyle: 'bezier', animated: 'none', arrowStart: 'none', arrowEnd: 'none' } },
|
||||
}
|
||||
useThemeStore.getState().setCustomStyle(def)
|
||||
expect(useThemeStore.getState().customStyle.nodes.server?.borderColor).toBe('#ff0000')
|
||||
|
||||
Reference in New Issue
Block a user