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:
@@ -42,6 +42,7 @@ const mockStore = {
|
||||
snapshotHistory: vi.fn(),
|
||||
createGroup: vi.fn(),
|
||||
ungroup: vi.fn(),
|
||||
removeFromGroup: vi.fn(),
|
||||
}
|
||||
|
||||
function setupStore(overrides = {}) {
|
||||
@@ -230,4 +231,45 @@ describe('GroupDetailPanel', () => {
|
||||
fireEvent.click(screen.getByText('Child Node Alpha'))
|
||||
expect(setSelectedNode).toHaveBeenCalledWith('c1')
|
||||
})
|
||||
|
||||
it('removes a child from the group via the remove button', () => {
|
||||
const removeFromGroup = vi.fn()
|
||||
const snapshotHistory = vi.fn()
|
||||
const group = makeGroupNode()
|
||||
const child = makeNode('c1', { parentId: 'g1', data: { label: 'Router', type: 'router', status: 'online', services: [] } })
|
||||
setupStore({ nodes: [group, child], selectedNodeId: 'g1', selectedNodeIds: ['g1'], removeFromGroup, snapshotHistory })
|
||||
renderPanel()
|
||||
fireEvent.click(screen.getByRole('button', { name: /remove router from group/i }))
|
||||
expect(removeFromGroup).toHaveBeenCalledWith('g1', 'c1')
|
||||
expect(snapshotHistory).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders the existing group description from notes', () => {
|
||||
const group = makeGroupNode()
|
||||
group.data = { ...group.data, notes: 'Critical DMZ hosts' } as typeof group.data
|
||||
setupStore({ nodes: [group], selectedNodeId: 'g1', selectedNodeIds: ['g1'] })
|
||||
renderPanel()
|
||||
expect((screen.getByLabelText('Description') as HTMLTextAreaElement).value).toBe('Critical DMZ hosts')
|
||||
})
|
||||
|
||||
it('commits the description on blur', () => {
|
||||
const updateNode = vi.fn()
|
||||
const snapshotHistory = vi.fn()
|
||||
const group = makeGroupNode()
|
||||
setupStore({ nodes: [group], selectedNodeId: 'g1', selectedNodeIds: ['g1'], updateNode, snapshotHistory })
|
||||
renderPanel()
|
||||
const textarea = screen.getByLabelText('Description')
|
||||
fireEvent.change(textarea, { target: { value: 'New notes' } })
|
||||
fireEvent.blur(textarea)
|
||||
expect(updateNode).toHaveBeenCalledWith('g1', { notes: 'New notes' })
|
||||
})
|
||||
|
||||
it('does not commit the description on blur when unchanged', () => {
|
||||
const updateNode = vi.fn()
|
||||
const group = makeGroupNode()
|
||||
setupStore({ nodes: [group], selectedNodeId: 'g1', selectedNodeIds: ['g1'], updateNode })
|
||||
renderPanel()
|
||||
fireEvent.blur(screen.getByLabelText('Description'))
|
||||
expect(updateNode).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user