feat: editable node groups (add/remove members, description)
Make Node Groups mutable instead of fixed-at-creation: - removeFromGroup / addToGroup store actions (inverse pair, history-aware) - right panel: per-member remove button + editable group description (reuses data.notes, no backend/serializer change) - drag a node over a group → confirm modal to add it (getIntersectingNodes) Tests: +21 (store, panel, canvas detection, modal). ha-relevant: yes
This commit is contained in:
@@ -9,6 +9,9 @@ import type { NodeData, EdgeData } from '@/types'
|
||||
// Capture props passed to ReactFlow so we can test the callbacks
|
||||
let rfProps: Record<string, unknown> = {}
|
||||
|
||||
// Hoisted holder so the mock factory can read the configurable intersection set.
|
||||
const rf = vi.hoisted(() => ({ intersecting: [] as unknown[] }))
|
||||
|
||||
vi.mock('@xyflow/react', () => ({
|
||||
ReactFlow: (props: Record<string, unknown>) => {
|
||||
rfProps = props
|
||||
@@ -21,7 +24,13 @@ vi.mock('@xyflow/react', () => ({
|
||||
ConnectionMode: { Loose: 'loose' },
|
||||
SelectionMode: { Partial: 'partial' },
|
||||
Position: { Top: 'top', Right: 'right', Bottom: 'bottom', Left: 'left' },
|
||||
useReactFlow: () => ({ fitView: vi.fn() }),
|
||||
useReactFlow: () => ({
|
||||
fitView: vi.fn(),
|
||||
screenToFlowPosition: vi.fn(),
|
||||
getIntersectingNodes: () => rf.intersecting,
|
||||
setNodes: vi.fn(),
|
||||
getNodes: () => [],
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@xyflow/react/dist/style.css', () => ({}))
|
||||
@@ -42,6 +51,7 @@ function makeEdge(id: string): Edge<EdgeData> {
|
||||
describe('CanvasContainer', () => {
|
||||
beforeEach(() => {
|
||||
rfProps = {}
|
||||
rf.intersecting = []
|
||||
useCanvasStore.setState({ nodes: [], edges: [], selectedNodeId: null })
|
||||
useThemeStore.setState({ activeTheme: 'default' })
|
||||
})
|
||||
@@ -154,6 +164,49 @@ describe('CanvasContainer', () => {
|
||||
expect(rfProps.onNodeDragStart).toBe(onNodeDragStart)
|
||||
})
|
||||
|
||||
// ── Drag onto group → onRequestAddToGroup ─────────────────────────────────
|
||||
|
||||
function groupNode(id: string): Node<NodeData> {
|
||||
return { id, type: 'group', position: { x: 0, y: 0 }, data: { label: id, type: 'group', status: 'unknown', services: [] } }
|
||||
}
|
||||
|
||||
it('fires onRequestAddToGroup when a node is dropped over a group', () => {
|
||||
const onRequestAddToGroup = vi.fn()
|
||||
const node = makeNode('n1')
|
||||
const group = groupNode('g1')
|
||||
rf.intersecting = [group]
|
||||
render(<CanvasContainer onRequestAddToGroup={onRequestAddToGroup} />)
|
||||
;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
|
||||
expect(onRequestAddToGroup).toHaveBeenCalledWith({ nodeId: 'n1', groupId: 'g1' })
|
||||
})
|
||||
|
||||
it('does not fire onRequestAddToGroup when no group is under the node', () => {
|
||||
const onRequestAddToGroup = vi.fn()
|
||||
const node = makeNode('n1')
|
||||
rf.intersecting = [makeNode('n2')]
|
||||
render(<CanvasContainer onRequestAddToGroup={onRequestAddToGroup} />)
|
||||
;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
|
||||
expect(onRequestAddToGroup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not fire onRequestAddToGroup for an already-parented node', () => {
|
||||
const onRequestAddToGroup = vi.fn()
|
||||
const node = { ...makeNode('n1'), parentId: 'gOther' }
|
||||
rf.intersecting = [groupNode('g1')]
|
||||
render(<CanvasContainer onRequestAddToGroup={onRequestAddToGroup} />)
|
||||
;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
|
||||
expect(onRequestAddToGroup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not fire onRequestAddToGroup when the dragged node is itself a group', () => {
|
||||
const onRequestAddToGroup = vi.fn()
|
||||
const node = groupNode('g2')
|
||||
rf.intersecting = [groupNode('g1')]
|
||||
render(<CanvasContainer onRequestAddToGroup={onRequestAddToGroup} />)
|
||||
;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
|
||||
expect(onRequestAddToGroup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// ── Canvas settings ───────────────────────────────────────────────────────
|
||||
|
||||
it('enables snapToGrid', () => {
|
||||
|
||||
Reference in New Issue
Block a user