mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-26 23:43:40 +00:00
refactor: move undo/redo to Edit menu and implement unified menu system
Removes the upper toolbar and consolidates undo/redo controls into the Edit menu for a cleaner interface with more screen space. Changes: - Move undo/redo buttons from Toolbar to Edit menu with descriptions - Remove Toolbar component from App layout - Implement closeAllMenus event system for coordinated menu management - Add event listeners to close menus when clicking on graph/timeline canvas - Add cross-menu closing: context menus close menu bar and vice versa - Fix React warning by deferring event dispatch with setTimeout Benefits: - Cleaner UI with more vertical space for graph editor - Unified menu system prevents multiple menus open simultaneously - Context menus and menu bar dropdowns coordinate properly - Consistent UX: clicking anywhere closes open menus 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
fab5c035a5
commit
5275b52f0a
4 changed files with 111 additions and 7 deletions
|
|
@ -5,7 +5,6 @@ import LeftPanel, { type LeftPanelRef } from "./components/Panels/LeftPanel";
|
|||
import RightPanel from "./components/Panels/RightPanel";
|
||||
import BottomPanel from "./components/Timeline/BottomPanel";
|
||||
import DocumentTabs from "./components/Workspace/DocumentTabs";
|
||||
import Toolbar from "./components/Toolbar/Toolbar";
|
||||
import MenuBar from "./components/Menu/MenuBar";
|
||||
import DocumentManager from "./components/Workspace/DocumentManager";
|
||||
import KeyboardShortcutsHelp from "./components/Common/KeyboardShortcutsHelp";
|
||||
|
|
@ -24,9 +23,8 @@ import type { ExportOptions } from "./utils/graphExport";
|
|||
*
|
||||
* Layout:
|
||||
* - Header with title
|
||||
* - Menu bar (File, Edit, View)
|
||||
* - Menu bar (File, Edit, View) with undo/redo controls
|
||||
* - Document tabs for multi-file support
|
||||
* - Toolbar for graph editing controls
|
||||
* - Main graph editor canvas
|
||||
*
|
||||
* Features:
|
||||
|
|
@ -141,9 +139,6 @@ function AppContent() {
|
|||
{/* Document Tabs */}
|
||||
<DocumentTabs />
|
||||
|
||||
{/* Toolbar */}
|
||||
{activeDocumentId && <Toolbar />}
|
||||
|
||||
{/* Main content area with side panels and bottom panel */}
|
||||
<main className="flex-1 overflow-hidden flex flex-col">
|
||||
{/* Top section: Left panel, graph editor, right panel */}
|
||||
|
|
|
|||
|
|
@ -223,6 +223,20 @@ const GraphEditor = ({ onNodeSelect, onEdgeSelect, onAddNodeRequest, onExportReq
|
|||
getCurrentViewport,
|
||||
]);
|
||||
|
||||
// Listen for custom event to close all menus (including context menus)
|
||||
useEffect(() => {
|
||||
const handleCloseAllMenus = (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
// Don't close if the event came from context menu itself (source: 'contextmenu')
|
||||
if (customEvent.detail?.source !== 'contextmenu') {
|
||||
setContextMenu(null);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('closeAllMenus', handleCloseAllMenus);
|
||||
return () => window.removeEventListener('closeAllMenus', handleCloseAllMenus);
|
||||
}, []);
|
||||
|
||||
// Save viewport periodically (debounced)
|
||||
const handleViewportChange = useCallback(
|
||||
(_event: MouseEvent | TouchEvent | null, viewport: Viewport) => {
|
||||
|
|
@ -455,6 +469,10 @@ const GraphEditor = ({ onNodeSelect, onEdgeSelect, onAddNodeRequest, onExportReq
|
|||
y: event.clientY,
|
||||
type: "pane",
|
||||
});
|
||||
// Close other menus when opening context menu (after state update)
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('closeAllMenus', { detail: { source: 'contextmenu' } }));
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
// Handle right-click on node
|
||||
|
|
@ -467,6 +485,10 @@ const GraphEditor = ({ onNodeSelect, onEdgeSelect, onAddNodeRequest, onExportReq
|
|||
type: "node",
|
||||
target: node,
|
||||
});
|
||||
// Close other menus when opening context menu (after state update)
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('closeAllMenus', { detail: { source: 'contextmenu' } }));
|
||||
}, 0);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
|
@ -481,6 +503,10 @@ const GraphEditor = ({ onNodeSelect, onEdgeSelect, onAddNodeRequest, onExportReq
|
|||
type: "edge",
|
||||
target: edge,
|
||||
});
|
||||
// Close other menus when opening context menu (after state update)
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('closeAllMenus', { detail: { source: 'contextmenu' } }));
|
||||
}, 0);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
|
@ -490,6 +516,8 @@ const GraphEditor = ({ onNodeSelect, onEdgeSelect, onAddNodeRequest, onExportReq
|
|||
if (contextMenu) {
|
||||
setContextMenu(null);
|
||||
}
|
||||
// Close all menus (menu bar dropdowns and context menus) when clicking on the graph canvas
|
||||
window.dispatchEvent(new Event('closeAllMenus'));
|
||||
}, [contextMenu]);
|
||||
|
||||
// Shared node creation logic (used by context menu and left panel)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useState, useCallback, useRef, useEffect } from 'react';
|
|||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
||||
import { useGraphWithHistory } from '../../hooks/useGraphWithHistory';
|
||||
import { useDocumentHistory } from '../../hooks/useDocumentHistory';
|
||||
import DocumentManager from '../Workspace/DocumentManager';
|
||||
import NodeTypeConfigModal from '../Config/NodeTypeConfig';
|
||||
import EdgeTypeConfigModal from '../Config/EdgeTypeConfig';
|
||||
|
|
@ -51,6 +52,21 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
|||
} = useWorkspaceStore();
|
||||
|
||||
const { clearGraph } = useGraphWithHistory();
|
||||
const { undo, redo, canUndo, canRedo, undoDescription, redoDescription } = useDocumentHistory();
|
||||
|
||||
// Listen for custom event to close all menus (e.g., from graph canvas clicks, context menu opens)
|
||||
useEffect(() => {
|
||||
const handleCloseMenuEvent = (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
// Don't close if the event came from MenuBar itself (source: 'menubar')
|
||||
if (customEvent.detail?.source !== 'menubar') {
|
||||
setActiveMenu(null);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('closeAllMenus', handleCloseMenuEvent);
|
||||
return () => window.removeEventListener('closeAllMenus', handleCloseMenuEvent);
|
||||
}, []);
|
||||
|
||||
// Close menu when clicking outside
|
||||
useEffect(() => {
|
||||
|
|
@ -67,7 +83,17 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
|||
}, [activeMenu]);
|
||||
|
||||
const toggleMenu = useCallback((menuName: string) => {
|
||||
setActiveMenu((current) => (current === menuName ? null : menuName));
|
||||
setActiveMenu((current) => {
|
||||
const newMenu = current === menuName ? null : menuName;
|
||||
// When opening a menu (not closing), dispatch event to close context menus after state updates
|
||||
if (newMenu !== null && current !== menuName) {
|
||||
// Use setTimeout to dispatch after the render phase completes
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('closeAllMenus', { detail: { source: 'menubar' } }));
|
||||
}, 0);
|
||||
}
|
||||
return newMenu;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
|
|
@ -148,6 +174,16 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
|||
closeMenu();
|
||||
}, [closeMenu]);
|
||||
|
||||
const handleUndo = useCallback(() => {
|
||||
undo();
|
||||
closeMenu();
|
||||
}, [undo, closeMenu]);
|
||||
|
||||
const handleRedo = useCallback(() => {
|
||||
redo();
|
||||
closeMenu();
|
||||
}, [redo, closeMenu]);
|
||||
|
||||
const handleClearGraph = useCallback(async () => {
|
||||
const confirmed = await confirm({
|
||||
title: 'Clear Current Graph',
|
||||
|
|
@ -301,6 +337,31 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
|||
|
||||
{activeMenu === 'edit' && (
|
||||
<div className="absolute top-full left-0 mt-1 w-56 bg-white border border-gray-200 rounded-md shadow-lg py-1 z-50">
|
||||
<button
|
||||
onClick={handleUndo}
|
||||
disabled={!canUndo}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center justify-between disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
title={undoDescription ? `Undo: ${undoDescription}` : 'Undo'}
|
||||
>
|
||||
<span>Undo{undoDescription ? `: ${undoDescription}` : ''}</span>
|
||||
{getShortcutLabel('undo') && (
|
||||
<span className="text-xs text-gray-400">{getShortcutLabel('undo')}</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRedo}
|
||||
disabled={!canRedo}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center justify-between disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
title={redoDescription ? `Redo: ${redoDescription}` : 'Redo'}
|
||||
>
|
||||
<span>Redo{redoDescription ? `: ${redoDescription}` : ''}</span>
|
||||
{getShortcutLabel('redo') && (
|
||||
<span className="text-xs text-gray-400">{getShortcutLabel('redo')}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<hr className="my-1 border-gray-200" />
|
||||
|
||||
<button
|
||||
onClick={handleConfigureActors}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
|
|
|
|||
|
|
@ -171,6 +171,20 @@ const TimelineViewInner: React.FC = () => {
|
|||
setEdges(layoutEdges);
|
||||
}, [layoutNodes, layoutEdges, setNodes, setEdges]);
|
||||
|
||||
// Listen for custom event to close all menus (including context menus)
|
||||
React.useEffect(() => {
|
||||
const handleCloseAllMenus = (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
// Don't close if the event came from context menu itself (source: 'contextmenu')
|
||||
if (customEvent.detail?.source !== 'contextmenu') {
|
||||
setContextMenu(null);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('closeAllMenus', handleCloseAllMenus);
|
||||
return () => window.removeEventListener('closeAllMenus', handleCloseAllMenus);
|
||||
}, []);
|
||||
|
||||
// Handle node click - switch to state
|
||||
const handleNodeClick = useCallback(
|
||||
(_event: React.MouseEvent, node: Node) => {
|
||||
|
|
@ -185,6 +199,8 @@ const TimelineViewInner: React.FC = () => {
|
|||
if (contextMenu) {
|
||||
setContextMenu(null);
|
||||
}
|
||||
// Close all menus (menu bar dropdowns and context menus) when clicking on the timeline canvas
|
||||
window.dispatchEvent(new Event('closeAllMenus'));
|
||||
}, [contextMenu]);
|
||||
|
||||
// Handle node context menu
|
||||
|
|
@ -196,6 +212,10 @@ const TimelineViewInner: React.FC = () => {
|
|||
y: event.clientY,
|
||||
stateId: node.id,
|
||||
});
|
||||
// Close other menus when opening context menu (after state update)
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('closeAllMenus', { detail: { source: 'contextmenu' } }));
|
||||
}, 0);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue