feat(network): add group/ungroup commands with bounding box calculation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,70 @@ export function useDiagramCommands({
|
||||
const canAlign = selectedNodes.length >= 2
|
||||
const canDistribute = selectedNodes.length >= 3
|
||||
|
||||
// ── Grouping ───────────────────────────────────────────────────────────
|
||||
const groupSelection = useCallback(() => {
|
||||
if (selectedNodes.length < 2) return
|
||||
pushHistory(nodes, edges)
|
||||
const PADDING = 24
|
||||
const minX = Math.min(...selectedNodes.map(n => n.position.x)) - PADDING
|
||||
const minY = Math.min(...selectedNodes.map(n => n.position.y)) - PADDING
|
||||
const maxX = Math.max(...selectedNodes.map(n => n.position.x + (n.measured?.width ?? 100))) + PADDING
|
||||
const maxY = Math.max(...selectedNodes.map(n => n.position.y + (n.measured?.height ?? 100))) + PADDING
|
||||
const groupId = `group-${Date.now()}`
|
||||
const groupNode: Node = {
|
||||
id: groupId,
|
||||
type: 'group',
|
||||
position: { x: minX, y: minY },
|
||||
style: { width: maxX - minX, height: maxY - minY },
|
||||
data: { label: 'Group', groupType: 'custom' },
|
||||
selected: false,
|
||||
}
|
||||
setNodes(prev => [
|
||||
groupNode,
|
||||
...prev.map(n =>
|
||||
n.selected
|
||||
? {
|
||||
...n,
|
||||
parentId: groupId,
|
||||
extent: 'parent' as const,
|
||||
position: { x: n.position.x - minX, y: n.position.y - minY },
|
||||
selected: false,
|
||||
}
|
||||
: n
|
||||
),
|
||||
])
|
||||
}, [nodes, edges, selectedNodes, pushHistory, setNodes])
|
||||
|
||||
const ungroupSelection = useCallback(() => {
|
||||
const selectedGroups = selectedNodes.filter(n => n.type === 'group')
|
||||
if (selectedGroups.length === 0) return
|
||||
pushHistory(nodes, edges)
|
||||
const groupIds = new Set(selectedGroups.map(g => g.id))
|
||||
setNodes(prev => {
|
||||
const groupPositions: Record<string, { x: number; y: number }> = {}
|
||||
for (const n of prev) {
|
||||
if (groupIds.has(n.id)) groupPositions[n.id] = n.position
|
||||
}
|
||||
return prev
|
||||
.filter(n => !groupIds.has(n.id))
|
||||
.map(n => {
|
||||
if (n.parentId && groupIds.has(n.parentId)) {
|
||||
const gPos = groupPositions[n.parentId] ?? { x: 0, y: 0 }
|
||||
return {
|
||||
...n,
|
||||
parentId: undefined,
|
||||
extent: undefined,
|
||||
position: { x: gPos.x + n.position.x, y: gPos.y + n.position.y },
|
||||
}
|
||||
}
|
||||
return n
|
||||
})
|
||||
})
|
||||
}, [nodes, edges, selectedNodes, pushHistory, setNodes])
|
||||
|
||||
const canGroup = selectedNodes.length >= 2 && !selectedNodes.some(n => n.type === 'group')
|
||||
const canUngroup = selectedNodes.some(n => n.type === 'group')
|
||||
|
||||
return {
|
||||
alignLeft,
|
||||
alignRight,
|
||||
@@ -134,5 +198,9 @@ export function useDiagramCommands({
|
||||
canAlign,
|
||||
canDistribute,
|
||||
selectedNodes,
|
||||
groupSelection,
|
||||
ungroupSelection,
|
||||
canGroup,
|
||||
canUngroup,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo, useState, useRef, useEffect } from 'react'
|
||||
import { NodeProps, NodeResizer, useReactFlow } from '@xyflow/react'
|
||||
import { NodeResizer, useReactFlow, type NodeProps } from '@xyflow/react'
|
||||
import type { GroupNodeData } from '@/types/network-diagram'
|
||||
|
||||
const GROUP_COLORS: Record<string, string> = {
|
||||
|
||||
@@ -757,10 +757,10 @@ function DiagramEditorInner() {
|
||||
onDistributeV={diagramCommands.distributeVertically}
|
||||
canAlign={contextMenu.type === 'node' ? diagramCommands.canAlign : false}
|
||||
canDistribute={contextMenu.type === 'node' ? diagramCommands.canDistribute : false}
|
||||
onGroupSelection={() => {}}
|
||||
onUngroupSelection={() => {}}
|
||||
canGroup={false}
|
||||
canUngroup={false}
|
||||
onGroupSelection={diagramCommands.groupSelection}
|
||||
onUngroupSelection={diagramCommands.ungroupSelection}
|
||||
canGroup={contextMenu?.type === 'node' ? diagramCommands.canGroup : false}
|
||||
canUngroup={contextMenu?.type === 'node' ? diagramCommands.canUngroup : false}
|
||||
/>
|
||||
)}
|
||||
{pendingDeleteNodeId && (
|
||||
|
||||
Reference in New Issue
Block a user