From dfcad531e2aacf9337f3b8ccfd653f7d7bb026a8 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Tue, 14 Apr 2026 00:55:34 +0000 Subject: [PATCH] fix(network): context menu on groups + group/ungroup in properties panel Context menu fix: - Group nodes pass pointer events through to children in React Flow, so right-clicking a group fires onPaneContextMenu instead of onNodeContextMenu - handlePaneContextMenu now checks for selected nodes and shows the node context menu (with align/group options) when any nodes are selected Properties panel multi-select: - Add Group section with type dropdown (Subnet, VLAN, Site, DMZ, Custom) - "Group into [Type]" button creates a group of the chosen type - Ungroup button appears when a group node is in the selection - useDiagramCommands.groupSelection now accepts a groupType param and uses it as the label and color key for the new group node Co-Authored-By: Claude Sonnet 4.6 --- .../network/hooks/useDiagramCommands.ts | 4 +- .../network/panels/PropertiesPanel.tsx | 50 +++++++++++++++++++ .../pages/NetworkDiagrams/DiagramEditor.tsx | 25 ++++++++-- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/network/hooks/useDiagramCommands.ts b/frontend/src/components/network/hooks/useDiagramCommands.ts index 4d57e08b..0e728ffb 100644 --- a/frontend/src/components/network/hooks/useDiagramCommands.ts +++ b/frontend/src/components/network/hooks/useDiagramCommands.ts @@ -123,7 +123,7 @@ export function useDiagramCommands({ const canDistribute = selectedNodes.length >= 3 // ── Grouping ─────────────────────────────────────────────────────────── - const groupSelection = useCallback(() => { + const groupSelection = useCallback((groupType: string = 'custom') => { if (selectedNodes.length < 2) return pushHistory(nodes, edges) const PADDING = 24 @@ -137,7 +137,7 @@ export function useDiagramCommands({ type: 'group', position: { x: minX, y: minY }, style: { width: maxX - minX, height: maxY - minY }, - data: { label: 'Group', groupType: 'custom' }, + data: { label: groupType.charAt(0).toUpperCase() + groupType.slice(1), groupType }, selected: false, } setNodes(prev => [ diff --git a/frontend/src/components/network/panels/PropertiesPanel.tsx b/frontend/src/components/network/panels/PropertiesPanel.tsx index d319a4da..229da3b7 100644 --- a/frontend/src/components/network/panels/PropertiesPanel.tsx +++ b/frontend/src/components/network/panels/PropertiesPanel.tsx @@ -4,6 +4,7 @@ import { AlignStartVertical, AlignCenterHorizontal, AlignEndVertical, AlignStartHorizontal, AlignCenterVertical, AlignEndHorizontal, AlignHorizontalSpaceAround, AlignVerticalSpaceAround, + BoxSelect, Ungroup, } from 'lucide-react' import { cn } from '@/lib/utils' import type { DeviceProperties, DiagramEdge } from '@/types' @@ -31,6 +32,10 @@ interface PropertiesPanelProps { onDistributeV: () => void canAlign: boolean canDistribute: boolean + canGroup: boolean + canUngroup: boolean + onGroupSelection: (groupType: string) => void + onUngroupSelection: () => void } type NodeStatus = 'online' | 'offline' | 'degraded' | 'unknown' @@ -84,6 +89,14 @@ function SectionDivider({ label }: { label: string }) { ) } +const GROUP_TYPES = [ + { value: 'subnet', label: 'Subnet' }, + { value: 'vlan', label: 'VLAN' }, + { value: 'site', label: 'Site' }, + { value: 'dmz', label: 'DMZ' }, + { value: 'custom', label: 'Custom' }, +] + export function PropertiesPanel({ selectedNode, selectedEdge, @@ -105,8 +118,13 @@ export function PropertiesPanel({ onDistributeV, canAlign, canDistribute, + canGroup, + canUngroup, + onGroupSelection, + onUngroupSelection, }: PropertiesPanelProps) { const [deleteConfirm, setDeleteConfirm] = useState(false) + const [pendingGroupType, setPendingGroupType] = useState('subnet') // Reset confirm state whenever the selection changes // eslint-disable-next-line react-hooks/set-state-in-effect @@ -178,6 +196,38 @@ export function PropertiesPanel({ )} + {(canGroup || canUngroup) && ( +
+
Grouping
+ {canGroup && ( +
+ + +
+ )} + {canUngroup && ( + + )} +
+ )} ) diff --git a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx index 5259f39a..a810cd93 100644 --- a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx +++ b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx @@ -399,11 +399,22 @@ function DiagramEditorInner() { const handlePaneContextMenu = useCallback((event: MouseEvent | React.MouseEvent) => { event.preventDefault() - setContextMenu({ - type: 'canvas', - position: { x: event.clientX, y: event.clientY }, - }) - }, []) + // Group nodes pass pointer events through to children, so right-clicking a group + // may fire onPaneContextMenu instead of onNodeContextMenu. If nodes are selected, + // show the node context menu so group/align/ungroup options are accessible. + const selected = getNodes().filter(n => n.selected) + if (selected.length > 0) { + setContextMenu({ + type: 'node', + position: { x: event.clientX, y: event.clientY }, + }) + } else { + setContextMenu({ + type: 'canvas', + position: { x: event.clientX, y: event.clientY }, + }) + } + }, [getNodes]) const closeContextMenu = useCallback(() => { setContextMenu(null) @@ -735,6 +746,10 @@ function DiagramEditorInner() { onDistributeV={diagramCommands.distributeVertically} canAlign={diagramCommands.canAlign} canDistribute={diagramCommands.canDistribute} + canGroup={diagramCommands.canGroup} + canUngroup={diagramCommands.canUngroup} + onGroupSelection={diagramCommands.groupSelection} + onUngroupSelection={diagramCommands.ungroupSelection} /> {contextMenu && (