refactor(canvas): promote collapsed to first-class NodeData field + edge rewire
Three follow-ups to PR #158 review: 1. Promote collapsed to NodeData.collapsed The flag was previously stashed inside NodeData.custom_colors, which is a colors/style object — semantically wrong. Move it to a first-class boolean on NodeData. Persistence keeps the existing API shape: serialize writes it into the custom_colors blob (alongside width/height/z_order, matching how groupRect already stashes layout metadata), and deserialize hoists it back. Legacy saves from the original PR shape load correctly. 2. Re-route cross-zone edges to the collapsed ancestor Previously any edge touching a hidden node was dropped, so a Zigbee coordinator outside a collapsed mesh lost all visible links to it. rewireEdgesForCollapse now walks each endpoint up the parentId chain to its nearest visible ancestor, surfaces a single stub edge on the collapsed zone, de-dupes parallel rewires (a 20-device mesh becomes one stub, not twenty), and drops edges that would self-loop on a zone or reference an orphan. 3. Revert package-lock.json churn The 63-line diff from the original PR was npm-version drift (libc arrays stripped from optional deps), unrelated to the feature. Tests: - canvasStore.collapse: updated to assert on data.collapsed. - collapseFilter: 8 cases for visibility + 7 for edge rewire, covering cross-boundary, nested collapse, sibling self-loop, mesh dedup, and orphan endpoints. - canvasSerializer.collapse: round-trip + legacy-shape compat.
This commit is contained in:
@@ -17,7 +17,7 @@ import '@xyflow/react/dist/style.css'
|
||||
import { useCanvasStore } from '@/stores/canvasStore'
|
||||
import { useThemeStore } from '@/stores/themeStore'
|
||||
import { THEMES } from '@/utils/themes'
|
||||
import { getVisibleNodeIds, filterVisibleEdges } from '@/utils/collapseFilter'
|
||||
import { getVisibleNodeIds, rewireEdgesForCollapse } from '@/utils/collapseFilter'
|
||||
import { nodeTypes } from './nodes/nodeTypes'
|
||||
import { edgeTypes } from './edges/edgeTypes'
|
||||
import { SearchBar } from './SearchBar'
|
||||
@@ -63,8 +63,8 @@ export function CanvasContainer({ onConnect: onConnectProp, onEdgeDoubleClick, o
|
||||
[nodes, visibleNodeIds],
|
||||
)
|
||||
const visibleEdges = useMemo(
|
||||
() => filterVisibleEdges(edges, visibleNodeIds),
|
||||
[edges, visibleNodeIds],
|
||||
() => rewireEdgesForCollapse(edges, nodes, visibleNodeIds),
|
||||
[edges, nodes, visibleNodeIds],
|
||||
)
|
||||
|
||||
const onNodeClick = useCallback((e: React.MouseEvent, node: Node<NodeData>) => {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function GroupRectNode({ id, data, selected }: NodeProps<Node<NodeData>>)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
|
||||
const rc = data.custom_colors ?? {}
|
||||
const isCollapsed = rc.collapsed ?? false
|
||||
const isCollapsed = data.collapsed ?? false
|
||||
const borderColor = rc.border ?? '#00d4ff'
|
||||
const borderStyle = rc.border_style ?? 'solid'
|
||||
const borderWidth = rc.border_width ?? 2
|
||||
|
||||
@@ -2,73 +2,31 @@ import { describe, it, expect } from 'vitest'
|
||||
import type { NodeData } from '@/types'
|
||||
|
||||
/**
|
||||
* Unit tests for GroupRectNode collapse/expand feature
|
||||
*
|
||||
* Integration tests verify:
|
||||
* - Type definitions for collapsed state
|
||||
* - Store action toggles collapse flag
|
||||
* - Component renders with proper state
|
||||
* - UI updates reflect collapse state
|
||||
*
|
||||
* Full end-to-end testing happens in CanvasContainer tests
|
||||
* which render the complete canvas with store integration
|
||||
* Type-level assertions for the collapse feature. Behavioral coverage lives
|
||||
* in:
|
||||
* - src/stores/__tests__/canvasStore.collapse.test.ts (store action)
|
||||
* - src/utils/__tests__/collapseFilter.test.ts (BFS + edge rewire)
|
||||
* - src/utils/__tests__/canvasSerializer.collapse.test.ts (round-trip)
|
||||
*/
|
||||
describe('GroupRectNode - Collapse/Expand Feature', () => {
|
||||
it('NodeData type supports collapsed property', () => {
|
||||
describe('NodeData.collapsed', () => {
|
||||
it('accepts a boolean collapsed flag as a first-class field', () => {
|
||||
const nodeData: NodeData = {
|
||||
label: 'Test Zone',
|
||||
type: 'groupRect',
|
||||
status: 'online',
|
||||
services: [],
|
||||
custom_colors: {
|
||||
collapsed: true,
|
||||
},
|
||||
collapsed: true,
|
||||
}
|
||||
|
||||
expect(nodeData.custom_colors?.collapsed).toBe(true)
|
||||
expect(nodeData.collapsed).toBe(true)
|
||||
})
|
||||
|
||||
it('NodeData supports collapsed as optional property', () => {
|
||||
it('treats a missing flag as expanded', () => {
|
||||
const nodeData: NodeData = {
|
||||
label: 'Test Zone',
|
||||
type: 'groupRect',
|
||||
status: 'online',
|
||||
services: [],
|
||||
}
|
||||
|
||||
expect(nodeData.custom_colors?.collapsed).toBeUndefined()
|
||||
})
|
||||
|
||||
it('collapsed state can be toggled', () => {
|
||||
let isCollapsed = false
|
||||
const toggle = () => {
|
||||
isCollapsed = !isCollapsed
|
||||
}
|
||||
|
||||
toggle()
|
||||
expect(isCollapsed).toBe(true)
|
||||
toggle()
|
||||
expect(isCollapsed).toBe(false)
|
||||
})
|
||||
|
||||
it('supports multi-level zone nesting with collapse state', () => {
|
||||
const parentZone: NodeData = {
|
||||
label: 'Parent Zone',
|
||||
type: 'groupRect',
|
||||
status: 'online',
|
||||
services: [],
|
||||
custom_colors: { collapsed: false },
|
||||
}
|
||||
|
||||
const childZone: NodeData = {
|
||||
label: 'Child Zone',
|
||||
type: 'groupRect',
|
||||
status: 'online',
|
||||
services: [],
|
||||
custom_colors: { collapsed: false },
|
||||
}
|
||||
|
||||
expect(parentZone.custom_colors?.collapsed).toBe(false)
|
||||
expect(childZone.custom_colors?.collapsed).toBe(false)
|
||||
expect(nodeData.collapsed).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user