mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 15:53:42 +00:00
fix: allow double click on state nodes and improve their design
This commit is contained in:
parent
89117415ed
commit
3ab90e5dd3
3 changed files with 129 additions and 69 deletions
17
src/App.tsx
17
src/App.tsx
|
|
@ -49,10 +49,11 @@ function AppContent() {
|
||||||
const leftPanelRef = useRef<LeftPanelRef>(null);
|
const leftPanelRef = useRef<LeftPanelRef>(null);
|
||||||
const [selectedNode, setSelectedNode] = useState<Actor | null>(null);
|
const [selectedNode, setSelectedNode] = useState<Actor | null>(null);
|
||||||
const [selectedEdge, setSelectedEdge] = useState<Relation | null>(null);
|
const [selectedEdge, setSelectedEdge] = useState<Relation | null>(null);
|
||||||
const [addNodeCallback, setAddNodeCallback] = useState<
|
// Use refs for callbacks to avoid triggering re-renders
|
||||||
|
const addNodeCallbackRef = useRef<
|
||||||
((nodeTypeId: string, position?: { x: number; y: number }) => void) | null
|
((nodeTypeId: string, position?: { x: number; y: number }) => void) | null
|
||||||
>(null);
|
>(null);
|
||||||
const [exportCallback, setExportCallback] = useState<
|
const exportCallbackRef = useRef<
|
||||||
((format: "png" | "svg", options?: ExportOptions) => Promise<void>) | null
|
((format: "png" | "svg", options?: ExportOptions) => Promise<void>) | null
|
||||||
>(null);
|
>(null);
|
||||||
const { fitView } = useReactFlow();
|
const { fitView } = useReactFlow();
|
||||||
|
|
@ -133,7 +134,7 @@ function AppContent() {
|
||||||
onOpenHelp={() => setShowKeyboardHelp(true)}
|
onOpenHelp={() => setShowKeyboardHelp(true)}
|
||||||
onFitView={handleFitView}
|
onFitView={handleFitView}
|
||||||
onSelectAll={handleSelectAll}
|
onSelectAll={handleSelectAll}
|
||||||
onExport={exportCallback || undefined}
|
onExport={exportCallbackRef.current || undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Document Tabs */}
|
{/* Document Tabs */}
|
||||||
|
|
@ -151,7 +152,7 @@ function AppContent() {
|
||||||
setSelectedNode(null);
|
setSelectedNode(null);
|
||||||
setSelectedEdge(null);
|
setSelectedEdge(null);
|
||||||
}}
|
}}
|
||||||
onAddNode={addNodeCallback || undefined}
|
onAddNode={addNodeCallbackRef.current || undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -179,13 +180,17 @@ function AppContent() {
|
||||||
nodeTypeId: string,
|
nodeTypeId: string,
|
||||||
position?: { x: number; y: number },
|
position?: { x: number; y: number },
|
||||||
) => void,
|
) => void,
|
||||||
) => setAddNodeCallback(() => callback)}
|
) => {
|
||||||
|
addNodeCallbackRef.current = callback;
|
||||||
|
}}
|
||||||
onExportRequest={(
|
onExportRequest={(
|
||||||
callback: (
|
callback: (
|
||||||
format: "png" | "svg",
|
format: "png" | "svg",
|
||||||
options?: ExportOptions,
|
options?: ExportOptions,
|
||||||
) => Promise<void>,
|
) => Promise<void>,
|
||||||
) => setExportCallback(() => callback)}
|
) => {
|
||||||
|
exportCallbackRef.current = callback;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Handle, Position, NodeProps } from 'reactflow';
|
import { Handle, Position, NodeProps } from "reactflow";
|
||||||
import type { ConstellationState } from '../../types/timeline';
|
import type { ConstellationState } from "../../types/timeline";
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
||||||
|
|
||||||
interface StateNodeData {
|
interface StateNodeData {
|
||||||
state: ConstellationState;
|
state: ConstellationState;
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
|
onRename?: (stateId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -16,29 +16,29 @@ const StateNode: React.FC<NodeProps<StateNodeData>> = ({ data, selected }) => {
|
||||||
|
|
||||||
// Format date if present
|
// Format date if present
|
||||||
const dateStr = state.metadata?.date
|
const dateStr = state.metadata?.date
|
||||||
? new Date(state.metadata.date).toLocaleDateString('en-US', {
|
? new Date(state.metadata.date).toLocaleDateString("en-US", {
|
||||||
month: 'short',
|
month: "short",
|
||||||
day: 'numeric',
|
day: "numeric",
|
||||||
year: 'numeric',
|
year: "numeric",
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Get custom color or default
|
// Get custom color or default
|
||||||
const color = state.metadata?.color || '#3b82f6';
|
const color = state.metadata?.color || "#3b82f6";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
px-3 py-2 rounded-lg border-2 bg-white shadow-sm
|
px-2 py-1.5 rounded-lg border-2 bg-white shadow-sm
|
||||||
transition-all cursor-pointer
|
transition-all cursor-pointer
|
||||||
${selected ? 'border-blue-500 shadow-md' : 'border-gray-300'}
|
${selected ? "border-blue-500 shadow-md" : "border-gray-300"}
|
||||||
${isCurrent ? 'ring-2 ring-green-400' : ''}
|
${isCurrent ? "ring-2 ring-green-400" : ""}
|
||||||
hover:shadow-lg
|
hover:shadow-lg
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
minWidth: '120px',
|
minWidth: "100px",
|
||||||
maxWidth: '200px',
|
maxWidth: "180px",
|
||||||
borderColor: selected ? '#3b82f6' : isCurrent ? '#10b981' : '#d1d5db',
|
borderColor: selected ? "#3b82f6" : isCurrent ? "#10b981" : "#d1d5db",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Handles for connections */}
|
{/* Handles for connections */}
|
||||||
|
|
@ -54,37 +54,34 @@ const StateNode: React.FC<NodeProps<StateNodeData>> = ({ data, selected }) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-center gap-1.5">
|
||||||
{isCurrent && (
|
|
||||||
<CheckCircleIcon
|
|
||||||
className="text-green-500 flex-shrink-0"
|
|
||||||
style={{ fontSize: '16px' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-semibold text-sm truncate" title={state.label}>
|
<div className="font-semibold text-xs truncate" title={state.label}>
|
||||||
{state.label}
|
{state.label}
|
||||||
</div>
|
</div>
|
||||||
{dateStr && (
|
{dateStr && (
|
||||||
<div className="text-xs text-gray-500">{dateStr}</div>
|
<div className="text-[10px] text-gray-500">{dateStr}</div>
|
||||||
)}
|
)}
|
||||||
{state.description && (
|
{state.description && (
|
||||||
<div className="text-xs text-gray-600 truncate mt-1" title={state.description}>
|
<div
|
||||||
|
className="text-[10px] text-gray-600 truncate mt-0.5"
|
||||||
|
title={state.description}
|
||||||
|
>
|
||||||
{state.description}
|
{state.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{state.metadata?.tags && state.metadata.tags.length > 0 && (
|
{state.metadata?.tags && state.metadata.tags.length > 0 && (
|
||||||
<div className="flex gap-1 mt-1 flex-wrap">
|
<div className="flex gap-1 mt-0.5 flex-wrap">
|
||||||
{state.metadata.tags.slice(0, 2).map((tag) => (
|
{state.metadata.tags.slice(0, 2).map((tag) => (
|
||||||
<span
|
<span
|
||||||
key={tag}
|
key={tag}
|
||||||
className="text-xs bg-blue-100 text-blue-700 px-1 rounded"
|
className="text-[10px] bg-blue-100 text-blue-700 px-1 rounded"
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{state.metadata.tags.length > 2 && (
|
{state.metadata.tags.length > 2 && (
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-[10px] text-gray-500">
|
||||||
+{state.metadata.tags.length - 2}
|
+{state.metadata.tags.length - 2}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useMemo, useCallback, useState } from 'react';
|
import React, { useMemo, useCallback, useState } from "react";
|
||||||
import ReactFlow, {
|
import ReactFlow, {
|
||||||
Background,
|
Background,
|
||||||
Controls,
|
Controls,
|
||||||
|
|
@ -9,18 +9,18 @@ import ReactFlow, {
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
BackgroundVariant,
|
BackgroundVariant,
|
||||||
ReactFlowProvider,
|
ReactFlowProvider,
|
||||||
} from 'reactflow';
|
} from "reactflow";
|
||||||
import 'reactflow/dist/style.css';
|
import "reactflow/dist/style.css";
|
||||||
import { useTimelineStore } from '../../stores/timelineStore';
|
import { useTimelineStore } from "../../stores/timelineStore";
|
||||||
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
import { useWorkspaceStore } from "../../stores/workspaceStore";
|
||||||
import StateNode from './StateNode';
|
import StateNode from "./StateNode";
|
||||||
import ContextMenu from '../Editor/ContextMenu';
|
import ContextMenu from "../Editor/ContextMenu";
|
||||||
import RenameStateDialog from './RenameStateDialog';
|
import RenameStateDialog from "./RenameStateDialog";
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import FileCopyIcon from '@mui/icons-material/FileCopy';
|
import FileCopyIcon from "@mui/icons-material/FileCopy";
|
||||||
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
import CallSplitIcon from "@mui/icons-material/CallSplit";
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import type { ConstellationState, StateId } from '../../types/timeline';
|
import type { ConstellationState, StateId } from "../../types/timeline";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Layout states in a horizontal timeline with branches
|
* Layout states in a horizontal timeline with branches
|
||||||
|
|
@ -28,7 +28,7 @@ import type { ConstellationState, StateId } from '../../types/timeline';
|
||||||
function layoutStates(
|
function layoutStates(
|
||||||
states: ConstellationState[],
|
states: ConstellationState[],
|
||||||
currentStateId: StateId,
|
currentStateId: StateId,
|
||||||
rootStateId: StateId
|
rootStateId: StateId,
|
||||||
): { nodes: Node[]; edges: Edge[] } {
|
): { nodes: Node[]; edges: Edge[] } {
|
||||||
const horizontalSpacing = 200;
|
const horizontalSpacing = 200;
|
||||||
const verticalSpacing = 100;
|
const verticalSpacing = 100;
|
||||||
|
|
@ -91,7 +91,7 @@ function layoutStates(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: state.id,
|
id: state.id,
|
||||||
type: 'stateNode',
|
type: "stateNode",
|
||||||
position: {
|
position: {
|
||||||
x: level * horizontalSpacing,
|
x: level * horizontalSpacing,
|
||||||
y: lane * verticalSpacing,
|
y: lane * verticalSpacing,
|
||||||
|
|
@ -111,11 +111,11 @@ function layoutStates(
|
||||||
id: `${state.parentStateId}-${state.id}`,
|
id: `${state.parentStateId}-${state.id}`,
|
||||||
source: state.parentStateId,
|
source: state.parentStateId,
|
||||||
target: state.id,
|
target: state.id,
|
||||||
type: 'smoothstep',
|
type: "smoothstep",
|
||||||
animated: state.id === currentStateId,
|
animated: state.id === currentStateId,
|
||||||
style: {
|
style: {
|
||||||
strokeWidth: state.id === currentStateId ? 3 : 2,
|
strokeWidth: state.id === currentStateId ? 3 : 2,
|
||||||
stroke: state.id === currentStateId ? '#10b981' : '#9ca3af',
|
stroke: state.id === currentStateId ? "#10b981" : "#9ca3af",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +129,14 @@ function layoutStates(
|
||||||
*/
|
*/
|
||||||
const TimelineViewInner: React.FC = () => {
|
const TimelineViewInner: React.FC = () => {
|
||||||
const activeDocumentId = useWorkspaceStore((state) => state.activeDocumentId);
|
const activeDocumentId = useWorkspaceStore((state) => state.activeDocumentId);
|
||||||
const { timelines, switchToState, updateState, duplicateState, duplicateStateAsChild, deleteState } = useTimelineStore();
|
const {
|
||||||
|
timelines,
|
||||||
|
switchToState,
|
||||||
|
updateState,
|
||||||
|
duplicateState,
|
||||||
|
duplicateStateAsChild,
|
||||||
|
deleteState,
|
||||||
|
} = useTimelineStore();
|
||||||
|
|
||||||
const timeline = activeDocumentId ? timelines.get(activeDocumentId) : null;
|
const timeline = activeDocumentId ? timelines.get(activeDocumentId) : null;
|
||||||
|
|
||||||
|
|
@ -152,14 +159,44 @@ const TimelineViewInner: React.FC = () => {
|
||||||
return Array.from(timeline.states.values());
|
return Array.from(timeline.states.values());
|
||||||
}, [timeline]);
|
}, [timeline]);
|
||||||
|
|
||||||
|
// Handle rename request from node
|
||||||
|
const handleRenameRequest = useCallback(
|
||||||
|
(stateId: string) => {
|
||||||
|
console.log("Rename requested for state:", stateId);
|
||||||
|
const state = timeline?.states.get(stateId);
|
||||||
|
if (state) {
|
||||||
|
setRenameDialog({
|
||||||
|
stateId: stateId,
|
||||||
|
currentLabel: state.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[timeline],
|
||||||
|
);
|
||||||
|
|
||||||
// Layout nodes and edges
|
// Layout nodes and edges
|
||||||
const { nodes: layoutNodes, edges: layoutEdges } = useMemo(() => {
|
const { nodes: layoutNodes, edges: layoutEdges } = useMemo(() => {
|
||||||
if (!timeline || states.length === 0) {
|
if (!timeline || states.length === 0) {
|
||||||
return { nodes: [], edges: [] };
|
return { nodes: [], edges: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
return layoutStates(states, timeline.currentStateId, timeline.rootStateId);
|
const { nodes, edges } = layoutStates(
|
||||||
}, [states, timeline]);
|
states,
|
||||||
|
timeline.currentStateId,
|
||||||
|
timeline.rootStateId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add rename handler to each node's data
|
||||||
|
const nodesWithRename = nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
onRename: handleRenameRequest,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { nodes: nodesWithRename, edges };
|
||||||
|
}, [states, timeline, handleRenameRequest]);
|
||||||
|
|
||||||
// React Flow state
|
// React Flow state
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(layoutNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(layoutNodes);
|
||||||
|
|
@ -176,22 +213,34 @@ const TimelineViewInner: React.FC = () => {
|
||||||
const handleCloseAllMenus = (event: Event) => {
|
const handleCloseAllMenus = (event: Event) => {
|
||||||
const customEvent = event as CustomEvent;
|
const customEvent = event as CustomEvent;
|
||||||
// Don't close if the event came from context menu itself (source: 'contextmenu')
|
// Don't close if the event came from context menu itself (source: 'contextmenu')
|
||||||
if (customEvent.detail?.source !== 'contextmenu') {
|
if (customEvent.detail?.source !== "contextmenu") {
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('closeAllMenus', handleCloseAllMenus);
|
window.addEventListener("closeAllMenus", handleCloseAllMenus);
|
||||||
return () => window.removeEventListener('closeAllMenus', handleCloseAllMenus);
|
return () =>
|
||||||
|
window.removeEventListener("closeAllMenus", handleCloseAllMenus);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle node click - switch to state
|
// Handle node click - switch to state
|
||||||
const handleNodeClick = useCallback(
|
const handleNodeClick = useCallback(
|
||||||
(_event: React.MouseEvent, node: Node) => {
|
(_event: React.MouseEvent, node: Node) => {
|
||||||
|
console.log("Single click on node:", node.id);
|
||||||
switchToState(node.id);
|
switchToState(node.id);
|
||||||
setContextMenu(null); // Close context menu if open
|
setContextMenu(null); // Close context menu if open
|
||||||
},
|
},
|
||||||
[switchToState]
|
[switchToState],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle node click - switch to state
|
||||||
|
const handleNodeDoubleClick = useCallback(
|
||||||
|
(_event: React.MouseEvent, node: Node) => {
|
||||||
|
console.log("Double click on node:", node.id);
|
||||||
|
handleRenameRequest(node.id);
|
||||||
|
setContextMenu(null); // Close context menu if open
|
||||||
|
},
|
||||||
|
[handleRenameRequest],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle pane click - close context menu
|
// Handle pane click - close context menu
|
||||||
|
|
@ -200,7 +249,7 @@ const TimelineViewInner: React.FC = () => {
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}
|
}
|
||||||
// Close all menus (menu bar dropdowns and context menus) when clicking on the timeline canvas
|
// Close all menus (menu bar dropdowns and context menus) when clicking on the timeline canvas
|
||||||
window.dispatchEvent(new Event('closeAllMenus'));
|
window.dispatchEvent(new Event("closeAllMenus"));
|
||||||
}, [contextMenu]);
|
}, [contextMenu]);
|
||||||
|
|
||||||
// Handle node context menu
|
// Handle node context menu
|
||||||
|
|
@ -214,10 +263,14 @@ const TimelineViewInner: React.FC = () => {
|
||||||
});
|
});
|
||||||
// Close other menus when opening context menu (after state update)
|
// Close other menus when opening context menu (after state update)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.dispatchEvent(new CustomEvent('closeAllMenus', { detail: { source: 'contextmenu' } }));
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("closeAllMenus", {
|
||||||
|
detail: { source: "contextmenu" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Context menu actions
|
// Context menu actions
|
||||||
|
|
@ -260,7 +313,7 @@ const TimelineViewInner: React.FC = () => {
|
||||||
updateState(renameDialog.stateId, { label: newLabel });
|
updateState(renameDialog.stateId, { label: newLabel });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[renameDialog, updateState]
|
[renameDialog, updateState],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Custom node types
|
// Custom node types
|
||||||
|
|
@ -268,7 +321,7 @@ const TimelineViewInner: React.FC = () => {
|
||||||
() => ({
|
() => ({
|
||||||
stateNode: StateNode,
|
stateNode: StateNode,
|
||||||
}),
|
}),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!timeline) {
|
if (!timeline) {
|
||||||
|
|
@ -276,7 +329,9 @@ const TimelineViewInner: React.FC = () => {
|
||||||
<div className="flex items-center justify-center h-full text-gray-500">
|
<div className="flex items-center justify-center h-full text-gray-500">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p>No timeline for this document.</p>
|
<p>No timeline for this document.</p>
|
||||||
<p className="text-sm mt-1">Create a timeline to manage multiple states.</p>
|
<p className="text-sm mt-1">
|
||||||
|
Create a timeline to manage multiple states.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -298,6 +353,7 @@ const TimelineViewInner: React.FC = () => {
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
onNodeContextMenu={handleNodeContextMenu}
|
onNodeContextMenu={handleNodeContextMenu}
|
||||||
onPaneClick={handlePaneClick}
|
onPaneClick={handlePaneClick}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
|
|
@ -310,10 +366,12 @@ const TimelineViewInner: React.FC = () => {
|
||||||
panOnDrag={true}
|
panOnDrag={true}
|
||||||
zoomOnScroll={true}
|
zoomOnScroll={true}
|
||||||
zoomOnPinch={true}
|
zoomOnPinch={true}
|
||||||
|
zoomOnDoubleClick={false}
|
||||||
preventScrolling={false}
|
preventScrolling={false}
|
||||||
panOnScroll={false}
|
panOnScroll={false}
|
||||||
selectionOnDrag={false}
|
selectionOnDrag={false}
|
||||||
selectNodesOnDrag={false}
|
selectNodesOnDrag={false}
|
||||||
|
nodesFocusable={false}
|
||||||
proOptions={{ hideAttribution: true }}
|
proOptions={{ hideAttribution: true }}
|
||||||
>
|
>
|
||||||
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
|
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
|
||||||
|
|
@ -329,22 +387,22 @@ const TimelineViewInner: React.FC = () => {
|
||||||
{
|
{
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: 'Rename',
|
label: "Rename",
|
||||||
icon: <EditIcon fontSize="small" />,
|
icon: <EditIcon fontSize="small" />,
|
||||||
onClick: handleRenameFromMenu,
|
onClick: handleRenameFromMenu,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Duplicate',
|
title: "Duplicate",
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: 'Duplicate (Parallel)',
|
label: "Duplicate (Parallel)",
|
||||||
icon: <FileCopyIcon fontSize="small" />,
|
icon: <FileCopyIcon fontSize="small" />,
|
||||||
onClick: handleDuplicateParallelFromMenu,
|
onClick: handleDuplicateParallelFromMenu,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Duplicate (Series)',
|
label: "Duplicate (Series)",
|
||||||
icon: <CallSplitIcon fontSize="small" />,
|
icon: <CallSplitIcon fontSize="small" />,
|
||||||
onClick: handleDuplicateSeriesFromMenu,
|
onClick: handleDuplicateSeriesFromMenu,
|
||||||
},
|
},
|
||||||
|
|
@ -353,7 +411,7 @@ const TimelineViewInner: React.FC = () => {
|
||||||
{
|
{
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: 'Delete',
|
label: "Delete",
|
||||||
icon: <DeleteIcon fontSize="small" />,
|
icon: <DeleteIcon fontSize="small" />,
|
||||||
onClick: handleDeleteFromMenu,
|
onClick: handleDeleteFromMenu,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue