From 764db7906087e19827133b0833aec7307f62939a Mon Sep 17 00:00:00 2001 From: chihlasm Date: Mon, 13 Apr 2026 20:11:12 +0000 Subject: [PATCH] feat(network): add alignment toolbar to PropertiesPanel for multi-select Co-Authored-By: Claude Sonnet 4.6 --- .../network/hooks/useDiagramCommands.ts | 2 +- .../network/panels/PropertiesPanel.tsx | 87 ++++++++++++++++++- .../pages/NetworkDiagrams/DiagramEditor.tsx | 11 +++ 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/network/hooks/useDiagramCommands.ts b/frontend/src/components/network/hooks/useDiagramCommands.ts index 4707932c..6a361d73 100644 --- a/frontend/src/components/network/hooks/useDiagramCommands.ts +++ b/frontend/src/components/network/hooks/useDiagramCommands.ts @@ -1,5 +1,5 @@ import { useCallback } from 'react' -import { Node, Edge } from '@xyflow/react' +import type { Node, Edge } from '@xyflow/react' interface UseDiagramCommandsParams { nodes: Node[] diff --git a/frontend/src/components/network/panels/PropertiesPanel.tsx b/frontend/src/components/network/panels/PropertiesPanel.tsx index 99b1b43a..b92c6708 100644 --- a/frontend/src/components/network/panels/PropertiesPanel.tsx +++ b/frontend/src/components/network/panels/PropertiesPanel.tsx @@ -1,5 +1,10 @@ import { useCallback, useState, useEffect } from 'react' -import { Trash2, Minus, Spline, GitBranch, BringToFront, SendToBack } from 'lucide-react' +import { + Trash2, Minus, Spline, GitBranch, BringToFront, SendToBack, + AlignStartVertical, AlignCenterHorizontal, AlignEndVertical, + AlignStartHorizontal, AlignCenterVertical, AlignEndHorizontal, + AlignHorizontalSpaceAround, AlignVerticalSpaceAround, +} from 'lucide-react' import { cn } from '@/lib/utils' import type { DeviceProperties, DiagramEdge } from '@/types' import type { Node, Edge } from '@xyflow/react' @@ -15,6 +20,17 @@ interface PropertiesPanelProps { onSendToBack: (nodeId: string) => void onDeleteNode: (nodeId: string) => void onDeleteEdge: (edgeId: string) => void + selectedNodeCount: number + onAlignLeft: () => void + onAlignRight: () => void + onAlignCenterH: () => void + onAlignTop: () => void + onAlignBottom: () => void + onAlignCenterV: () => void + onDistributeH: () => void + onDistributeV: () => void + canAlign: boolean + canDistribute: boolean } type NodeStatus = 'online' | 'offline' | 'degraded' | 'unknown' @@ -78,6 +94,17 @@ export function PropertiesPanel({ onSendToBack, onDeleteNode, onDeleteEdge, + selectedNodeCount, + onAlignLeft, + onAlignRight, + onAlignCenterH, + onAlignTop, + onAlignBottom, + onAlignCenterV, + onDistributeH, + onDistributeV, + canAlign, + canDistribute, }: PropertiesPanelProps) { const [deleteConfirm, setDeleteConfirm] = useState(false) @@ -98,6 +125,64 @@ export function PropertiesPanel({ onNodeUpdate(selectedNode.id, { label: value } as Partial) }, [selectedNode, onNodeUpdate]) + if (!selectedNode && !selectedEdge && selectedNodeCount >= 2) { + return ( +
+
+
+ {selectedNodeCount} nodes selected +
+
+
+ {canAlign && ( +
+
Align
+
+ {([ + { label: 'Left', icon: AlignStartVertical, action: onAlignLeft }, + { label: 'Center', icon: AlignCenterHorizontal, action: onAlignCenterH }, + { label: 'Right', icon: AlignEndVertical, action: onAlignRight }, + { label: 'Top', icon: AlignStartHorizontal, action: onAlignTop }, + { label: 'Middle', icon: AlignCenterVertical, action: onAlignCenterV }, + { label: 'Bottom', icon: AlignEndHorizontal, action: onAlignBottom }, + ] as const).map(({ label, icon: Icon, action }) => ( + + ))} +
+
+ )} + {canDistribute && ( +
+
Distribute
+
+ + +
+
+ )} +
+
+ ) + } + if (!selectedNode && !selectedEdge) { return (
diff --git a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx index 1da1cd7a..7b04fcff 100644 --- a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx +++ b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx @@ -709,6 +709,17 @@ function DiagramEditorInner() { onSendToBack={handleSendToBack} onDeleteNode={handleDeleteNode} onDeleteEdge={handleDeleteEdge} + selectedNodeCount={nodes.filter(n => n.selected).length} + onAlignLeft={diagramCommands.alignLeft} + onAlignRight={diagramCommands.alignRight} + onAlignCenterH={diagramCommands.alignCenterH} + onAlignTop={diagramCommands.alignTop} + onAlignBottom={diagramCommands.alignBottom} + onAlignCenterV={diagramCommands.alignCenterV} + onDistributeH={diagramCommands.distributeHorizontally} + onDistributeV={diagramCommands.distributeVertically} + canAlign={diagramCommands.canAlign} + canDistribute={diagramCommands.canDistribute} />
{contextMenu && (