add redo and undo
This commit is contained in:
146
src/App.tsx
146
src/App.tsx
@@ -6,14 +6,14 @@ import {
|
||||
Background,
|
||||
MiniMap,
|
||||
addEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
type Node,
|
||||
type Edge,
|
||||
type Connection,
|
||||
type ColorMode,
|
||||
type NodeChange,
|
||||
type EdgeChange,
|
||||
} from '@xyflow/react'
|
||||
import ConfigNode from './components/graph/ConfigNode'
|
||||
import FunctionNode from './components/graph/FunctionNode'
|
||||
@@ -22,6 +22,7 @@ import VariableNode from './components/graph/VariableNode'
|
||||
import { AnimatedEdge } from './components/graph/AnimatedEdge'
|
||||
import FlowContext from './lib/flowContext'
|
||||
import { useTheme } from './lib/themeContext'
|
||||
import { useGraphStateWithHistory } from './hooks/useGraphStateWithHistory'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
ContextMenuTrigger,
|
||||
ContextMenuGroup
|
||||
} from "@/components/ui/context-menu"
|
||||
import { AppMenubar } from '@/components/AppMenubar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ClipboardPaste, Code2, Moon, ScrollText, Sparkles, Sun, Variable } from 'lucide-react'
|
||||
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from './lib/flowUtils'
|
||||
@@ -64,10 +66,27 @@ const initialEdges: Edge[] = [
|
||||
{ id: 'e-cfg_001-rnd_001', source: 'cfg_001', target: 'rnd_001', type: 'animated' },
|
||||
]
|
||||
|
||||
const PROJECT_FILE_EXT = '.zui.json'
|
||||
|
||||
export default function App() {
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const [nodes, setNodes, onNodesChangeBase] = useNodesState(initialNodes)
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
setEdges,
|
||||
setNodesSilent,
|
||||
applyGraph,
|
||||
saveForDragEnd,
|
||||
commitDragEnd,
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
setStateImmediate,
|
||||
} = useGraphStateWithHistory(initialNodes, initialEdges)
|
||||
|
||||
const importInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
||||
const [renamingNodeId, setRenamingNodeId] = React.useState<string | null>(null)
|
||||
const [connectionFrom, setConnectionFrom] = React.useState<{ nodeId: string; sourceHandle?: string } | null>(null)
|
||||
@@ -98,12 +117,12 @@ export default function App() {
|
||||
rafRef.current = null
|
||||
const toApply = pendingChangesRef.current.splice(0, pendingChangesRef.current.length)
|
||||
if (toApply.length > 0) {
|
||||
setNodes((nds) => applyNodeChanges(toApply, nds))
|
||||
setNodesSilent((nds) => applyNodeChanges(toApply, nds))
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
[setNodes]
|
||||
[setNodesSilent]
|
||||
)
|
||||
|
||||
const nodeTypes = React.useMemo(
|
||||
@@ -115,6 +134,14 @@ export default function App() {
|
||||
|
||||
const defaultEdgeOptions = React.useMemo(() => ({ type: 'animated' as const }), [])
|
||||
|
||||
const onEdgesChange = useCallback(
|
||||
(changes: EdgeChange<Edge>[]) => {
|
||||
if (changes.length === 0) return
|
||||
setEdges((eds) => applyEdgeChanges(changes, eds))
|
||||
},
|
||||
[setEdges]
|
||||
)
|
||||
|
||||
const onConnect = React.useCallback(
|
||||
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
|
||||
[setEdges]
|
||||
@@ -155,6 +182,50 @@ export default function App() {
|
||||
setRfInstance(instance)
|
||||
}, [])
|
||||
|
||||
const onNodeDragStart = useCallback(() => {
|
||||
saveForDragEnd()
|
||||
}, [saveForDragEnd])
|
||||
|
||||
const onNodeDragStop = useCallback(() => {
|
||||
commitDragEnd()
|
||||
}, [commitDragEnd])
|
||||
|
||||
const handleExportProject = useCallback(() => {
|
||||
const state = { nodes, edges }
|
||||
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `project${PROJECT_FILE_EXT}`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}, [nodes, edges])
|
||||
|
||||
const handleImportProject = useCallback(() => {
|
||||
importInputRef.current?.click()
|
||||
}, [])
|
||||
|
||||
const onImportFileChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
e.target.value = ''
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const text = reader.result as string
|
||||
const state = JSON.parse(text) as { nodes?: Node[]; edges?: Edge[] }
|
||||
if (!state || !Array.isArray(state.nodes) || !Array.isArray(state.edges)) return
|
||||
setStateImmediate({ nodes: state.nodes, edges: state.edges })
|
||||
} catch {
|
||||
// Invalid JSON
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
},
|
||||
[setStateImmediate]
|
||||
)
|
||||
|
||||
const flowContextValue = useMemo(
|
||||
() => ({
|
||||
nodes,
|
||||
@@ -270,35 +341,58 @@ export default function App() {
|
||||
}
|
||||
}, [getMenuPosition, setNodes])
|
||||
|
||||
const deleteNode = React.useCallback((id: string | undefined) => {
|
||||
if (!id) return
|
||||
setNodes((nds) => nds.filter((n) => n.id !== id))
|
||||
setEdges((eds) => eds.filter((e) => e.source !== id && e.target !== id))
|
||||
setContextTarget(null)
|
||||
}, [setNodes, setEdges])
|
||||
const deleteNode = React.useCallback(
|
||||
(id: string | undefined) => {
|
||||
if (!id) return
|
||||
applyGraph(({ nodes: nds, edges: eds }) => ({
|
||||
nodes: nds.filter((n) => n.id !== id),
|
||||
edges: eds.filter((e) => e.source !== id && e.target !== id),
|
||||
}))
|
||||
setContextTarget(null)
|
||||
},
|
||||
[applyGraph]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="reactflow-wrapper"
|
||||
className="reactflow-wrapper flex flex-col h-full"
|
||||
ref={wrapperRef}
|
||||
onContextMenuCapture={onContextMenuCapture}
|
||||
onContextMenu={onCanvasContextMenu}
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="absolute top-3 right-3 z-10 nodrag nopan"
|
||||
onClick={toggleTheme}
|
||||
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</Button>
|
||||
<input
|
||||
ref={importInputRef}
|
||||
type="file"
|
||||
accept=".json,.zui.json,application/json"
|
||||
className="hidden"
|
||||
onChange={onImportFileChange}
|
||||
aria-hidden
|
||||
/>
|
||||
<AppMenubar
|
||||
onImport={handleImportProject}
|
||||
onExport={handleExportProject}
|
||||
undo={undo}
|
||||
redo={redo}
|
||||
canUndo={canUndo}
|
||||
canRedo={canRedo}
|
||||
/>
|
||||
<div className="flex-1 min-h-0 relative flex flex-col">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="absolute top-3 right-3 z-10 nodrag nopan"
|
||||
onClick={toggleTheme}
|
||||
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</Button>
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<FlowContext.Provider value={flowContextValue}>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<div className="flex-1 min-h-0 w-full">
|
||||
<ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
@@ -308,6 +402,8 @@ export default function App() {
|
||||
onConnect={onConnect}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
isValidConnection={isValidConnection}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
@@ -361,6 +457,8 @@ export default function App() {
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</FlowContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user