fix styles and menu
This commit is contained in:
121
src/App.tsx
121
src/App.tsx
@@ -18,7 +18,14 @@ import CustomNode from './components/CustomNode'
|
||||
import ConfigNode from './components/ConfigNode'
|
||||
import RenderingNode from './components/RenderingNode'
|
||||
import FlowContext from './lib/flowContext'
|
||||
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuGroup,
|
||||
ContextMenuItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu"
|
||||
// Generate short unique node ids when missing
|
||||
const genId = () => `node_${Math.random().toString(36).slice(2, 9)}`
|
||||
|
||||
@@ -45,7 +52,8 @@ export default function App() {
|
||||
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
||||
|
||||
const wrapperRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const [contextMenu, setContextMenu] = React.useState<null | { x: number; y: number; clientX: number; clientY: number }>(null)
|
||||
const lastClickRef = React.useRef<{ clientX: number; clientY: number } | null>(null)
|
||||
const [contextTarget, setContextTarget] = React.useState<null | { type: 'canvas' | 'node'; nodeId?: string; clientX: number; clientY: number }>(null)
|
||||
|
||||
const nodeTypes = React.useMemo(
|
||||
() => ({ custom: CustomNode, config: ConfigNode, render: RenderingNode }),
|
||||
@@ -61,26 +69,31 @@ export default function App() {
|
||||
setRfInstance(instance)
|
||||
}, [])
|
||||
|
||||
const hideMenu = React.useCallback(() => setContextMenu(null), [])
|
||||
|
||||
const onCanvasContextMenu = React.useCallback((ev: React.MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
const target = ev.target as HTMLElement
|
||||
// don't show menu when right-clicking nodes or handles
|
||||
if (target.closest('.react-flow__node') || target.closest('.react-flow__handle')) return
|
||||
const rect = wrapperRef.current?.getBoundingClientRect()
|
||||
const nodeEl = target.closest('.react-flow__node') as HTMLElement | null
|
||||
const handleEl = target.closest('.react-flow__handle') as HTMLElement | null
|
||||
const clientX = ev.clientX
|
||||
const clientY = ev.clientY
|
||||
const x = rect ? clientX - rect.left : ev.clientX
|
||||
const y = rect ? clientY - rect.top : ev.clientY
|
||||
setContextMenu({ x, y, clientX, clientY })
|
||||
|
||||
lastClickRef.current = { clientX, clientY }
|
||||
|
||||
if (nodeEl && !handleEl) {
|
||||
const nodeId = nodeEl.dataset?.id || nodeEl.getAttribute('data-id') || (nodeEl.id && nodeEl.id.startsWith('reactflow__node-') ? nodeEl.id.replace('reactflow__node-', '') : undefined)
|
||||
setContextTarget({ type: 'node', nodeId, clientX, clientY })
|
||||
return
|
||||
}
|
||||
|
||||
setContextTarget({ type: 'canvas', clientX, clientY })
|
||||
}, [])
|
||||
|
||||
const createNode = React.useCallback(
|
||||
(type: string) => {
|
||||
if (!rfInstance) return
|
||||
if (!contextMenu) return
|
||||
const { clientX, clientY } = contextMenu
|
||||
const click = contextTarget ?? lastClickRef.current
|
||||
const clientX = click?.clientX ?? window.innerWidth / 2
|
||||
const clientY = click?.clientY ?? window.innerHeight / 2
|
||||
const rect = wrapperRef.current?.getBoundingClientRect()
|
||||
const point = rect ? { x: clientX - rect.left, y: clientY - rect.top } : { x: clientX, y: clientY }
|
||||
let position = point
|
||||
@@ -99,52 +112,60 @@ export default function App() {
|
||||
data: type === 'config' ? { yaml: '# Enter YAML here\n', title: `config-${id}` } : {},
|
||||
}
|
||||
setNodes((nds) => nds.concat(newNode))
|
||||
hideMenu()
|
||||
lastClickRef.current = null
|
||||
setContextTarget(null)
|
||||
},
|
||||
[rfInstance, contextMenu, hideMenu, setNodes]
|
||||
[rfInstance, 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])
|
||||
|
||||
return (
|
||||
<div className="reactflow-wrapper" ref={wrapperRef} onContextMenu={onCanvasContextMenu} style={{ position: 'relative' }}>
|
||||
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
onInit={onInit}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<MiniMap />
|
||||
</ReactFlow>
|
||||
|
||||
{contextMenu && (
|
||||
<div
|
||||
className="absolute bg-white border rounded shadow-md text-sm"
|
||||
style={{ left: contextMenu.x, top: contextMenu.y, zIndex: 9999 }}
|
||||
onMouseLeave={hideMenu}
|
||||
>
|
||||
<div className="p-2">
|
||||
<div className="text-xs text-gray-700 mb-2">Create node</div>
|
||||
<div className="space-y-1">
|
||||
<button className="w-full text-left px-2 py-1 hover:bg-gray-100" onClick={() => createNode('config')}>
|
||||
Configuration Node
|
||||
</button>
|
||||
<button className="w-full text-left px-2 py-1 hover:bg-gray-100" onClick={() => createNode('render')}>
|
||||
Rendering Node
|
||||
</button>
|
||||
<button className="w-full text-left px-2 py-1 hover:bg-gray-100" onClick={() => createNode('custom')}>
|
||||
Custom Node
|
||||
</button>
|
||||
</div>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
onInit={onInit}
|
||||
>
|
||||
<Background />
|
||||
<Controls />ß
|
||||
<MiniMap />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ContextMenuTrigger>
|
||||
|
||||
<ContextMenuContent>
|
||||
{contextTarget?.type === 'node' ? (
|
||||
<ContextMenuItem onSelect={() => deleteNode(contextTarget.nodeId)}>Delete</ContextMenuItem>
|
||||
) : (
|
||||
<>
|
||||
<ContextMenuGroup>
|
||||
<ContextMenuLabel>New Node</ContextMenuLabel>
|
||||
<ContextMenuItem onSelect={() => createNode('config')}>Config</ContextMenuItem>
|
||||
<ContextMenuItem onSelect={() => createNode('render')}>Renderer</ContextMenuItem>
|
||||
<ContextMenuItem onSelect={() => createNode('custom')}>Custom</ContextMenuItem>
|
||||
</ContextMenuGroup>
|
||||
</>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</FlowContext.Provider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user