import { useState, useEffect, useRef, useCallback } from 'react'; import { IconButton, Tooltip, ToggleButton, ToggleButtonGroup } from '@mui/material'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import DeleteIcon from '@mui/icons-material/Delete'; import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import SyncAltIcon from '@mui/icons-material/SyncAlt'; import RemoveIcon from '@mui/icons-material/Remove'; import { usePanelStore } from '../../stores/panelStore'; import { useGraphWithHistory } from '../../hooks/useGraphWithHistory'; import { useDocumentHistory } from '../../hooks/useDocumentHistory'; import { useConfirm } from '../../hooks/useConfirm'; import GraphMetrics from '../Common/GraphMetrics'; import { getIconComponent } from '../../utils/iconUtils'; import type { Actor, Relation, EdgeDirectionality } from '../../types'; /** * RightPanel - Context-aware properties panel on the right side * * Features: * - Shows properties of selected node(s) or edge(s) * - Live property updates (no save button) * - Connection information for actors * - Multi-selection support * - Non-modal design (doesn't block graph view) * - Collapsible */ interface Props { selectedNode: Actor | null; selectedEdge: Relation | null; onClose: () => void; } /** * PanelHeader - Reusable header component for right panel views */ interface PanelHeaderProps { title: string; onClose?: () => void; onCollapse: () => void; } const PanelHeader = ({ title, onClose, onCollapse }: PanelHeaderProps) => (

{title}

{onClose && ( )}
); const RightPanel = ({ selectedNode, selectedEdge, onClose }: Props) => { const { rightPanelCollapsed, rightPanelWidth, collapseRightPanel, expandRightPanel, } = usePanelStore(); const { nodes, edges, nodeTypes, edgeTypes, updateNode, updateEdge, deleteNode, deleteEdge, setEdges } = useGraphWithHistory(); const { pushToHistory } = useDocumentHistory(); const { confirm, ConfirmDialogComponent } = useConfirm(); // Node property states const [actorType, setActorType] = useState(''); const [actorLabel, setActorLabel] = useState(''); const [actorDescription, setActorDescription] = useState(''); const labelInputRef = useRef(null); // Edge property states const [relationType, setRelationType] = useState(''); const [relationLabel, setRelationLabel] = useState(''); const [relationDirectionality, setRelationDirectionality] = useState('directed'); // Track if user has made changes const [hasNodeChanges, setHasNodeChanges] = useState(false); const [hasEdgeChanges, setHasEdgeChanges] = useState(false); // Update state when selected node changes useEffect(() => { if (selectedNode) { setActorType(selectedNode.data?.type || ''); setActorLabel(selectedNode.data?.label || ''); setActorDescription(selectedNode.data?.description || ''); setHasNodeChanges(false); // Focus and select the label input when node is selected setTimeout(() => { if (labelInputRef.current && !rightPanelCollapsed) { labelInputRef.current.focus(); labelInputRef.current.select(); } }, 100); } }, [selectedNode, rightPanelCollapsed]); // Update state when selected edge changes useEffect(() => { if (selectedEdge && selectedEdge.data) { setRelationType(selectedEdge.data.type || ''); const typeLabel = edgeTypes.find((et) => et.id === selectedEdge.data?.type)?.label; const hasCustomLabel = selectedEdge.data.label && selectedEdge.data.label !== typeLabel; setRelationLabel((hasCustomLabel && selectedEdge.data.label) || ''); const edgeTypeConfig = edgeTypes.find((et) => et.id === selectedEdge.data?.type); setRelationDirectionality(selectedEdge.data.directionality || edgeTypeConfig?.defaultDirectionality || 'directed'); setHasEdgeChanges(false); } }, [selectedEdge, edgeTypes]); // Live update node properties (debounced) const updateNodeProperties = useCallback(() => { if (!selectedNode || !hasNodeChanges) return; updateNode(selectedNode.id, { data: { type: actorType, label: actorLabel, description: actorDescription || undefined, }, }); setHasNodeChanges(false); }, [selectedNode, actorType, actorLabel, actorDescription, hasNodeChanges, updateNode]); // Live update edge properties (debounced) const updateEdgeProperties = useCallback(() => { if (!selectedEdge || !hasEdgeChanges) return; updateEdge(selectedEdge.id, { type: relationType, label: relationLabel.trim() || undefined, directionality: relationDirectionality, }); setHasEdgeChanges(false); }, [selectedEdge, relationType, relationLabel, relationDirectionality, hasEdgeChanges, updateEdge]); // Debounce live updates useEffect(() => { const timeoutId = setTimeout(() => { if (hasNodeChanges) { updateNodeProperties(); } if (hasEdgeChanges) { updateEdgeProperties(); } }, 500); // 500ms debounce return () => clearTimeout(timeoutId); }, [hasNodeChanges, hasEdgeChanges, updateNodeProperties, updateEdgeProperties]); // Handle node deletion const handleDeleteNode = async () => { if (!selectedNode) return; const confirmed = await confirm({ title: 'Delete Actor', message: 'Are you sure you want to delete this actor? All connected relations will also be deleted.', confirmLabel: 'Delete', severity: 'danger', }); if (confirmed) { deleteNode(selectedNode.id); onClose(); } }; // Handle edge deletion const handleDeleteEdge = async () => { if (!selectedEdge) return; const confirmed = await confirm({ title: 'Delete Relation', message: 'Are you sure you want to delete this relation?', confirmLabel: 'Delete', severity: 'danger', }); if (confirmed) { deleteEdge(selectedEdge.id); onClose(); } }; // Handle reverse direction const handleReverseDirection = () => { if (!selectedEdge) return; // Push to history BEFORE mutation pushToHistory('Reverse Relation Direction'); // Update the edges array with the reversed edge const updatedEdges = edges.map(edge => { if (edge.id === selectedEdge.id) { return { ...edge, source: edge.target, target: edge.source, sourceHandle: edge.targetHandle, targetHandle: edge.sourceHandle, }; } return edge; }); // Apply the update (setEdges is a pass-through without history tracking) setEdges(updatedEdges); }; // Get connections for selected node const getNodeConnections = () => { if (!selectedNode) return []; return edges.filter( (edge) => edge.source === selectedNode.id || edge.target === selectedNode.id ); }; const selectedNodeTypeConfig = nodeTypes.find((nt) => nt.id === actorType); const selectedEdgeTypeConfig = edgeTypes.find((et) => et.id === relationType); // Collapsed view if (rightPanelCollapsed) { return (
{ConfirmDialogComponent}
); } // No selection state - show graph metrics if (!selectedNode && !selectedEdge) { return (
{ConfirmDialogComponent}
); } // Node properties view if (selectedNode) { const connections = getNodeConnections(); return (
{/* Scrollable content */}
{/* Actor Type */}
{selectedNodeTypeConfig && (
{selectedNodeTypeConfig.label}
)}
{/* Actor Label */}
{ setActorLabel(e.target.value); setHasNodeChanges(true); }} placeholder="Enter actor name" className="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" />
{/* Description */}