refactoring
This commit is contained in:
@@ -26,7 +26,8 @@ import { AnimatedEdge } from '@/components/graph/AnimatedEdge'
|
||||
import FlowContext from '@/lib/graph/flowContext'
|
||||
import { useTheme } from '@/lib/themeContext'
|
||||
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
||||
import { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory'
|
||||
import { useCanvasGraph } from '@/app/canvas/useCanvasGraph'
|
||||
import { getExampleGraph } from '@/app/canvas/canvasGraphUtils'
|
||||
import { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu'
|
||||
import { CanvasContextMenuContent } from '@/app/canvas/CanvasContextMenuContent'
|
||||
import { useCanvasConnectionPath } from '@/app/canvas/useCanvasConnectionPath'
|
||||
@@ -48,7 +49,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { FolderOpen, FileStack, CircleDotDashed } from 'lucide-react'
|
||||
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '@/lib/graph/flowUtils'
|
||||
import { getDefaultDataForType, getNextNodeId } from '@/lib/graph/flowUtils'
|
||||
import {
|
||||
getRegisteredNodeTypes,
|
||||
getRegisteredNodeTypeIds,
|
||||
@@ -58,12 +59,7 @@ import {
|
||||
} from '@/lib/graph/nodeRegistry'
|
||||
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
loadGraphFromStorage,
|
||||
saveGraphToStorage,
|
||||
PROJECT_FILE_EXT,
|
||||
PROJECT_VERSION,
|
||||
} from '@/app/pleroma/projectGraphStorage'
|
||||
import { PROJECT_FILE_EXT, PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage'
|
||||
|
||||
const SNAP_GRID: [number, number] = [15, 15]
|
||||
const DUPLICATE_OFFSET = { x: 30, y: 30 }
|
||||
@@ -72,52 +68,6 @@ const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
|
||||
y: Math.round(y / SNAP_GRID[1]) * SNAP_GRID[1],
|
||||
})
|
||||
|
||||
const NODE_GAP = 150
|
||||
const initialNodes: AppNode[] = [
|
||||
{
|
||||
id: 'var_001',
|
||||
position: { x: 50, y: 100 },
|
||||
data: { value: 'Zoe', valueType: 'string' as const },
|
||||
type: 'variable',
|
||||
style: DEFAULT_NODE_STYLE.variable,
|
||||
},
|
||||
{
|
||||
id: 'cfg_001',
|
||||
position: { x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP, y: 50 },
|
||||
data: {
|
||||
plantuml: '@startuml\nactor User\nparticipant "{{ var_001 }}" as R\nUser -> R : loves\n@enduml\n',
|
||||
title: 'config-cfg_001',
|
||||
},
|
||||
type: 'config',
|
||||
style: DEFAULT_NODE_STYLE.config,
|
||||
},
|
||||
{
|
||||
id: 'rnd_001',
|
||||
position: {
|
||||
x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP + DEFAULT_NODE_STYLE.config.width + NODE_GAP,
|
||||
y: 50,
|
||||
},
|
||||
data: {},
|
||||
type: 'render',
|
||||
style: DEFAULT_NODE_STYLE.render,
|
||||
},
|
||||
]
|
||||
|
||||
const initialEdges: AppEdge[] = [
|
||||
{ id: 'e-var_001-cfg_001', source: 'var_001', target: 'cfg_001', type: 'animated' },
|
||||
{ id: 'e-cfg_001-rnd_001', source: 'cfg_001', target: 'rnd_001', type: 'animated' },
|
||||
]
|
||||
|
||||
function getExampleGraph(): { nodes: AppNode[]; edges: AppEdge[] } {
|
||||
return {
|
||||
nodes: initialNodes.map((n) => ({
|
||||
...n,
|
||||
data: n.data && typeof n.data === 'object' ? { ...(n.data as object) } : n.data,
|
||||
})),
|
||||
edges: initialEdges.map((e) => ({ ...e })),
|
||||
}
|
||||
}
|
||||
|
||||
function FlowFitViewOnLoad() {
|
||||
const nodesInitialized = useNodesInitialized()
|
||||
const { fitView } = useReactFlow()
|
||||
@@ -196,21 +146,9 @@ export type CanvasPageProps = {
|
||||
projectId?: string
|
||||
}
|
||||
|
||||
function getInitialGraph(projectId: string | undefined): { nodes: AppNode[]; edges: AppEdge[] } {
|
||||
if (projectId) {
|
||||
const stored = loadGraphFromStorage(projectId)
|
||||
if (stored && (stored.nodes.length > 0 || stored.edges.length > 0)) {
|
||||
return { nodes: stored.nodes as AppNode[], edges: stored.edges as AppEdge[] }
|
||||
}
|
||||
return { nodes: [], edges: [] }
|
||||
}
|
||||
return getExampleGraph()
|
||||
}
|
||||
|
||||
export function CanvasPage({ projectId }: CanvasPageProps) {
|
||||
const { theme } = useTheme()
|
||||
const { showMinimap } = usePlatform()
|
||||
const initialGraph = useMemo(() => getInitialGraph(projectId), [projectId])
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
@@ -225,20 +163,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
||||
canUndo,
|
||||
canRedo,
|
||||
setStateImmediate,
|
||||
} = useGraphStateWithHistory(initialGraph.nodes, initialGraph.edges)
|
||||
|
||||
// Persist graph to localStorage when projectId is set (debounced)
|
||||
const saveTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
React.useEffect(() => {
|
||||
if (!projectId) return
|
||||
const save = () => {
|
||||
saveGraphToStorage(projectId, { version: PROJECT_VERSION, nodes, edges })
|
||||
}
|
||||
saveTimeoutRef.current = setTimeout(save, 500)
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
|
||||
}
|
||||
}, [projectId, nodes, edges])
|
||||
} = useCanvasGraph(projectId)
|
||||
|
||||
const importInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [rfInstance, setRfInstance] = React.useState<unknown>(null)
|
||||
|
||||
66
frontend/src/app/canvas/canvasGraphUtils.ts
Normal file
66
frontend/src/app/canvas/canvasGraphUtils.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Canvas graph: example graph and initial graph from storage.
|
||||
* Used by useCanvasGraph and CanvasPage (e.g. Load example).
|
||||
*/
|
||||
|
||||
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
|
||||
import { DEFAULT_NODE_STYLE } from '@/lib/graph/flowUtils'
|
||||
import { loadGraphFromStorage } from '@/app/pleroma/projectGraphStorage'
|
||||
|
||||
const NODE_GAP = 150
|
||||
|
||||
const EXAMPLE_NODES: AppNode[] = [
|
||||
{
|
||||
id: 'var_001',
|
||||
position: { x: 50, y: 100 },
|
||||
data: { value: 'Zoe', valueType: 'string' as const },
|
||||
type: 'variable',
|
||||
style: DEFAULT_NODE_STYLE.variable,
|
||||
},
|
||||
{
|
||||
id: 'cfg_001',
|
||||
position: { x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP, y: 50 },
|
||||
data: {
|
||||
plantuml: '@startuml\nactor User\nparticipant "{{ var_001 }}" as R\nUser -> R : loves\n@enduml\n',
|
||||
title: 'config-cfg_001',
|
||||
},
|
||||
type: 'config',
|
||||
style: DEFAULT_NODE_STYLE.config,
|
||||
},
|
||||
{
|
||||
id: 'rnd_001',
|
||||
position: {
|
||||
x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP + DEFAULT_NODE_STYLE.config.width + NODE_GAP,
|
||||
y: 50,
|
||||
},
|
||||
data: {},
|
||||
type: 'render',
|
||||
style: DEFAULT_NODE_STYLE.render,
|
||||
},
|
||||
]
|
||||
|
||||
const EXAMPLE_EDGES: AppEdge[] = [
|
||||
{ id: 'e-var_001-cfg_001', source: 'var_001', target: 'cfg_001', type: 'animated' },
|
||||
{ id: 'e-cfg_001-rnd_001', source: 'cfg_001', target: 'rnd_001', type: 'animated' },
|
||||
]
|
||||
|
||||
export function getExampleGraph(): { nodes: AppNode[]; edges: AppEdge[] } {
|
||||
return {
|
||||
nodes: EXAMPLE_NODES.map((n) => ({
|
||||
...n,
|
||||
data: n.data && typeof n.data === 'object' ? { ...(n.data as object) } : n.data,
|
||||
})),
|
||||
edges: EXAMPLE_EDGES.map((e) => ({ ...e })),
|
||||
}
|
||||
}
|
||||
|
||||
export function getInitialGraph(projectId: string | undefined): { nodes: AppNode[]; edges: AppEdge[] } {
|
||||
if (projectId) {
|
||||
const stored = loadGraphFromStorage(projectId)
|
||||
if (stored && (stored.nodes.length > 0 || stored.edges.length > 0)) {
|
||||
return { nodes: stored.nodes as AppNode[], edges: stored.edges as AppEdge[] }
|
||||
}
|
||||
return { nodes: [], edges: [] }
|
||||
}
|
||||
return getExampleGraph()
|
||||
}
|
||||
33
frontend/src/app/canvas/useCanvasGraph.ts
Normal file
33
frontend/src/app/canvas/useCanvasGraph.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Hook for canvas graph state and persistence. Wraps useGraphStateWithHistory
|
||||
* with initial graph from project storage (or example) and debounced save.
|
||||
* Keeps CanvasPage focused on composition and layout.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory'
|
||||
import { getInitialGraph } from '@/app/canvas/canvasGraphUtils'
|
||||
import { saveGraphToStorage, PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage'
|
||||
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
|
||||
|
||||
export type UseCanvasGraphResult = ReturnType<typeof useGraphStateWithHistory>
|
||||
|
||||
export function useCanvasGraph(projectId: string | undefined): UseCanvasGraphResult {
|
||||
const initialGraph = useMemo(() => getInitialGraph(projectId), [projectId])
|
||||
const result = useGraphStateWithHistory(initialGraph.nodes, initialGraph.edges)
|
||||
const { nodes, edges } = result
|
||||
|
||||
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
useEffect(() => {
|
||||
if (!projectId) return
|
||||
const save = () => {
|
||||
saveGraphToStorage(projectId, { version: PROJECT_VERSION, nodes, edges })
|
||||
}
|
||||
saveTimeoutRef.current = setTimeout(save, 500)
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
|
||||
}
|
||||
}, [projectId, nodes, edges])
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user