refactor nodes
This commit is contained in:
174
src/App.tsx
174
src/App.tsx
@@ -15,11 +15,7 @@ import {
|
||||
type NodeChange,
|
||||
type EdgeChange,
|
||||
} from '@xyflow/react'
|
||||
import ConfigNode from './components/graph/ConfigNode'
|
||||
import FunctionNode from './components/graph/FunctionNode'
|
||||
import RenderingNode from './components/graph/RenderingNode'
|
||||
import VariableNode from './components/graph/VariableNode'
|
||||
import { AnimatedEdge } from './components/graph/AnimatedEdge'
|
||||
import { AnimatedEdge } from './components/base/AnimatedEdge'
|
||||
import FlowContext from './lib/flowContext'
|
||||
import { useTheme } from './lib/themeContext'
|
||||
import { useGraphStateWithHistory } from './hooks/useGraphStateWithHistory'
|
||||
@@ -35,8 +31,14 @@ import {
|
||||
ContextMenuGroup
|
||||
} from "@/components/ui/context-menu"
|
||||
import { AppMenubar } from '@/components/AppMenubar'
|
||||
import { ClipboardPaste, Code2, ScrollText, Sparkles, Variable } from 'lucide-react'
|
||||
import { ClipboardPaste } from 'lucide-react'
|
||||
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from './lib/flowUtils'
|
||||
import {
|
||||
getRegisteredNodeTypes,
|
||||
getRegisteredNodeTypeIds,
|
||||
getDefaultStyle,
|
||||
isConnectionAllowed,
|
||||
} from './lib/nodeRegistry'
|
||||
|
||||
const SNAP_GRID: [number, number] = [15, 15]
|
||||
const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
|
||||
@@ -125,7 +127,7 @@ export default function App() {
|
||||
)
|
||||
|
||||
const nodeTypes = React.useMemo(
|
||||
() => ({ config: ConfigNode, render: RenderingNode, variable: VariableNode, function: FunctionNode }),
|
||||
() => Object.fromEntries(getRegisteredNodeTypes().map((r) => [r.id, r.component])),
|
||||
[]
|
||||
)
|
||||
|
||||
@@ -153,11 +155,12 @@ export default function App() {
|
||||
const sourceType = sourceNode?.type
|
||||
const targetType = targetNode?.type
|
||||
if (!sourceType || !targetType) return false
|
||||
if (connection.source === connection.target) return false
|
||||
if (targetType === 'variable') return false
|
||||
if (targetType === 'render' && (sourceType === 'variable' || sourceType === 'function')) return false
|
||||
if (sourceType === 'render') return false
|
||||
return true
|
||||
return isConnectionAllowed(
|
||||
sourceType,
|
||||
targetType,
|
||||
connection.source,
|
||||
connection.target
|
||||
)
|
||||
},
|
||||
[nodes]
|
||||
)
|
||||
@@ -294,22 +297,17 @@ export default function App() {
|
||||
(type: string) => {
|
||||
const position = getMenuPosition()
|
||||
if (position == null) return
|
||||
const typeMap: Record<string, Node['type']> = {
|
||||
config: 'config',
|
||||
render: 'render',
|
||||
variable: 'variable',
|
||||
function: 'function',
|
||||
}
|
||||
const nodeType = typeMap[type] ?? 'config'
|
||||
const nodeType = type as Node['type']
|
||||
setNodes((nds) => {
|
||||
const newId = getNextNodeId(nodeType, nds.map((n) => n.id))
|
||||
const dataMap = getDefaultDataForType(nodeType, newId)
|
||||
const style = getDefaultStyle(nodeType)
|
||||
const newNode: Node = {
|
||||
id: newId,
|
||||
type: nodeType,
|
||||
position: { x: position.x, y: position.y },
|
||||
data: dataMap,
|
||||
style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config,
|
||||
style,
|
||||
}
|
||||
return nds.concat(newNode)
|
||||
})
|
||||
@@ -319,8 +317,6 @@ export default function App() {
|
||||
[getMenuPosition, setNodes]
|
||||
)
|
||||
|
||||
const VALID_NODE_TYPES = ['config', 'render', 'variable', 'function'] as const
|
||||
|
||||
const pasteNode = React.useCallback(async () => {
|
||||
const position = getMenuPosition()
|
||||
if (position == null) return
|
||||
@@ -328,17 +324,19 @@ export default function App() {
|
||||
const text = await navigator.clipboard?.readText()
|
||||
if (!text) return
|
||||
const raw = JSON.parse(text) as { id?: string; type?: string; data?: any; position?: { x: number; y: number }; style?: any }
|
||||
if (!raw || typeof raw.type !== 'string' || !VALID_NODE_TYPES.includes(raw.type as any)) return
|
||||
const validIds = getRegisteredNodeTypeIds()
|
||||
if (!raw || typeof raw.type !== 'string' || !validIds.includes(raw.type)) return
|
||||
setNodes((nds) => {
|
||||
const newId = getNextNodeId(raw.type as Node['type'], nds.map((n) => n.id))
|
||||
const data = raw.data != null && typeof raw.data === 'object' ? { ...raw.data } : {}
|
||||
if (raw.type === 'config' && data.title != null) data.title = `config-${newId}`
|
||||
const style = getDefaultStyle(raw.type)
|
||||
const newNode: Node = {
|
||||
id: newId,
|
||||
type: raw.type as Node['type'],
|
||||
position: { x: position.x, y: position.y },
|
||||
data,
|
||||
style: DEFAULT_NODE_STYLE[raw.type] ?? DEFAULT_NODE_STYLE.config,
|
||||
style,
|
||||
}
|
||||
return nds.concat(newNode)
|
||||
})
|
||||
@@ -387,75 +385,67 @@ export default function App() {
|
||||
/>
|
||||
<div className="flex-1 min-h-0 relative flex flex-col">
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<FlowContext.Provider value={flowContextValue}>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div className="flex-1 min-h-0 w-full" onWheelCapture={onWheelCapture}>
|
||||
<ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
isValidConnection={isValidConnection}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
colorMode={theme as ColorMode}
|
||||
snapToGrid
|
||||
snapGrid={SNAP_GRID}
|
||||
fitView
|
||||
onInit={onInit}
|
||||
nodeDragThreshold={1}
|
||||
>
|
||||
<Background variant="dots" gap={20} />
|
||||
<Controls />
|
||||
<MiniMap />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<FlowContext.Provider value={flowContextValue}>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div className="flex-1 min-h-0 w-full" onWheelCapture={onWheelCapture}>
|
||||
<ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
isValidConnection={isValidConnection}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
colorMode={theme as ColorMode}
|
||||
snapToGrid
|
||||
snapGrid={SNAP_GRID}
|
||||
fitView
|
||||
onInit={onInit}
|
||||
nodeDragThreshold={1}
|
||||
>
|
||||
<Background variant="dots" gap={20} />
|
||||
<Controls />
|
||||
<MiniMap />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
|
||||
<ContextMenuContent className="w-48">
|
||||
<ContextMenuGroup>
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger>Create Node</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent className="w-44">
|
||||
<ContextMenuGroup>
|
||||
<ContextMenuItem onSelect={() => createNode('config')}>
|
||||
<ScrollText className="mr-2 h-4 w-4" />
|
||||
Config
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onSelect={() => createNode('render')}>
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
Renderer
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator></ContextMenuSeparator>
|
||||
<ContextMenuItem onSelect={() => createNode('variable')}>
|
||||
<Variable className="mr-2 h-4 w-4" />
|
||||
Variable
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onSelect={() => createNode('function')}>
|
||||
<Code2 className="mr-2 h-4 w-4" />
|
||||
Function
|
||||
</ContextMenuItem>
|
||||
</ContextMenuGroup>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onSelect={() => pasteNode()}>
|
||||
<ClipboardPaste className="mr-2 h-4 w-4" />
|
||||
Paste
|
||||
</ContextMenuItem>
|
||||
</ContextMenuGroup>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</FlowContext.Provider>
|
||||
<ContextMenuContent className="w-48">
|
||||
<ContextMenuGroup>
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger>Create Node</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent className="w-44">
|
||||
<ContextMenuGroup>
|
||||
{getRegisteredNodeTypes().map((desc, index) => (
|
||||
<React.Fragment key={desc.id}>
|
||||
{index === 2 ? <ContextMenuSeparator /> : null}
|
||||
<ContextMenuItem onSelect={() => createNode(desc.id)}>
|
||||
{desc.menuIcon}
|
||||
{desc.menuLabel}
|
||||
</ContextMenuItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ContextMenuGroup>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onSelect={() => pasteNode()}>
|
||||
<ClipboardPaste className="mr-2 h-4 w-4" />
|
||||
Paste
|
||||
</ContextMenuItem>
|
||||
</ContextMenuGroup>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</FlowContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
|
||||
Reference in New Issue
Block a user