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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 => [
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(canGroup || canUngroup) && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-[10px] font-medium text-muted uppercase tracking-wider">Grouping</div>
|
||||
{canGroup && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<select
|
||||
value={pendingGroupType}
|
||||
onChange={e => setPendingGroupType(e.target.value)}
|
||||
className="w-full rounded border border-default bg-input px-2 py-1.5 text-xs text-primary focus:border-accent focus:outline-none"
|
||||
>
|
||||
{GROUP_TYPES.map(gt => (
|
||||
<option key={gt.value} value={gt.value}>{gt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={() => onGroupSelection(pendingGroupType)}
|
||||
className="flex items-center justify-center gap-1.5 px-2 py-1.5 rounded bg-elevated hover:bg-card border border-default hover:border-hover transition-colors text-muted-foreground hover:text-primary text-xs"
|
||||
>
|
||||
<BoxSelect size={13} /> Group into {GROUP_TYPES.find(g => g.value === pendingGroupType)?.label}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{canUngroup && (
|
||||
<button
|
||||
onClick={onUngroupSelection}
|
||||
className="flex items-center justify-center gap-1.5 px-2 py-1.5 rounded bg-elevated hover:bg-card border border-default hover:border-hover transition-colors text-muted-foreground hover:text-primary text-xs"
|
||||
>
|
||||
<Ungroup size={13} /> Ungroup
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
{contextMenu && (
|
||||
|
||||
Reference in New Issue
Block a user