feat: add node help system and registry for extensible node types
- Implemented a help system for different node types (config, render, variable, function) with detailed usage instructions. - Created a node registry to manage node types, including registration, retrieval, and validation of connections between nodes. - Defined central node and edge types for the application to streamline state management. - Added Nunjucks autocomplete functionality to enhance user experience in template editing. - Developed a syntax highlighting parser for PlantUML and Nunjucks within the CodeMirror editor. - Registered built-in node types at application startup, including their default configurations and help entries. - Introduced a theme context provider to manage light/dark mode preferences across the application. - Created utility functions for class name management using clsx and tailwind-merge. - Set up Tailwind CSS for styling with custom themes and responsive design. - Configured Vite for development with proxy settings for backend API calls and Kroki diagram service.
This commit is contained in:
117
frontend/src/hooks/useGraphStateWithHistory.ts
Normal file
117
frontend/src/hooks/useGraphStateWithHistory.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import type { AppNode, AppEdge } from '@/lib/nodeTypes'
|
||||
|
||||
export type GraphState = { nodes: AppNode[]; edges: AppEdge[] }
|
||||
|
||||
function cloneState(state: GraphState): GraphState {
|
||||
return {
|
||||
nodes: state.nodes.map((n) => ({ ...n, data: n.data && typeof n.data === 'object' ? { ...n.data } : n.data })),
|
||||
edges: state.edges.map((e) => ({ ...e })),
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_HISTORY = 100
|
||||
|
||||
export function useGraphStateWithHistory(initialNodes: AppNode[], initialEdges: AppEdge[]) {
|
||||
const [nodes, setNodesState] = useState<AppNode[]>(initialNodes)
|
||||
const [edges, setEdgesState] = useState<AppEdge[]>(initialEdges)
|
||||
const [historySizes, setHistorySizes] = useState({ past: 0, future: 0 })
|
||||
|
||||
const pastRef = useRef<GraphState[]>([])
|
||||
const futureRef = useRef<GraphState[]>([])
|
||||
const preDragRef = useRef<GraphState | null>(null)
|
||||
const nodesRef = useRef(nodes)
|
||||
const edgesRef = useRef(edges)
|
||||
nodesRef.current = nodes
|
||||
edgesRef.current = edges
|
||||
|
||||
const pushToPast = useCallback((state: GraphState) => {
|
||||
pastRef.current = pastRef.current.slice(-(MAX_HISTORY - 1))
|
||||
pastRef.current.push(cloneState(state))
|
||||
futureRef.current = []
|
||||
setHistorySizes({ past: pastRef.current.length, future: 0 })
|
||||
}, [])
|
||||
|
||||
const setNodes = useCallback((updater: AppNode[] | ((prev: AppNode[]) => AppNode[])) => {
|
||||
pushToPast({ nodes: nodesRef.current, edges: edgesRef.current })
|
||||
setNodesState(typeof updater === 'function' ? updater : () => updater)
|
||||
}, [pushToPast])
|
||||
|
||||
const setEdges = useCallback((updater: AppEdge[] | ((prev: AppEdge[]) => AppEdge[])) => {
|
||||
pushToPast({ nodes: nodesRef.current, edges: edgesRef.current })
|
||||
setEdgesState(typeof updater === 'function' ? updater : () => updater)
|
||||
}, [pushToPast])
|
||||
|
||||
const setNodesSilent = useCallback((updater: AppNode[] | ((prev: AppNode[]) => AppNode[])) => {
|
||||
setNodesState(typeof updater === 'function' ? updater : () => updater)
|
||||
}, [])
|
||||
|
||||
const setEdgesSilent = useCallback((updater: AppEdge[] | ((prev: AppEdge[]) => AppEdge[])) => {
|
||||
setEdgesState(typeof updater === 'function' ? updater : () => updater)
|
||||
}, [])
|
||||
|
||||
const applyGraph = useCallback((updater: (state: GraphState) => GraphState) => {
|
||||
pushToPast({ nodes: nodesRef.current, edges: edgesRef.current })
|
||||
const next = updater({ nodes: nodesRef.current, edges: edgesRef.current })
|
||||
setNodesState(next.nodes)
|
||||
setEdgesState(next.edges)
|
||||
}, [pushToPast])
|
||||
|
||||
const saveForDragEnd = useCallback(() => {
|
||||
preDragRef.current = cloneState({ nodes: nodesRef.current, edges: edgesRef.current })
|
||||
}, [])
|
||||
|
||||
const commitDragEnd = useCallback(() => {
|
||||
if (preDragRef.current) {
|
||||
pastRef.current = pastRef.current.slice(-(MAX_HISTORY - 1))
|
||||
pastRef.current.push(preDragRef.current)
|
||||
futureRef.current = []
|
||||
preDragRef.current = null
|
||||
setHistorySizes({ past: pastRef.current.length, future: 0 })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const undo = useCallback(() => {
|
||||
if (pastRef.current.length === 0) return
|
||||
const prev = pastRef.current.pop()!
|
||||
futureRef.current.push(cloneState({ nodes: nodesRef.current, edges: edgesRef.current }))
|
||||
setNodesState(prev.nodes)
|
||||
setEdgesState(prev.edges)
|
||||
setHistorySizes({ past: pastRef.current.length, future: futureRef.current.length })
|
||||
}, [])
|
||||
|
||||
const redo = useCallback(() => {
|
||||
if (futureRef.current.length === 0) return
|
||||
const next = futureRef.current.pop()!
|
||||
pastRef.current.push(cloneState({ nodes: nodesRef.current, edges: edgesRef.current }))
|
||||
setNodesState(next.nodes)
|
||||
setEdgesState(next.edges)
|
||||
setHistorySizes({ past: pastRef.current.length, future: futureRef.current.length })
|
||||
}, [])
|
||||
|
||||
const setStateImmediate = useCallback((state: GraphState) => {
|
||||
setNodesState(state.nodes)
|
||||
setEdgesState(state.edges)
|
||||
pastRef.current = []
|
||||
futureRef.current = []
|
||||
preDragRef.current = null
|
||||
setHistorySizes({ past: 0, future: 0 })
|
||||
}, [])
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
setEdges,
|
||||
setNodesSilent,
|
||||
setEdgesSilent,
|
||||
applyGraph,
|
||||
saveForDragEnd,
|
||||
commitDragEnd,
|
||||
undo,
|
||||
redo,
|
||||
canUndo: historySizes.past > 0,
|
||||
canRedo: historySizes.future > 0,
|
||||
setStateImmediate,
|
||||
}
|
||||
}
|
||||
27
frontend/src/hooks/useResizeHeight.ts
Normal file
27
frontend/src/hooks/useResizeHeight.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Returns the height of the observed element, updating when it resizes (e.g. node resize).
|
||||
* Used to give CodeMirror and other components an explicit height that tracks their container.
|
||||
*/
|
||||
export function useResizeHeight(defaultHeight: number): [number, React.RefObject<HTMLDivElement | null>] {
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
const [height, setHeight] = useState(defaultHeight)
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
const entry = entries[0]
|
||||
if (entry?.contentRect.height != null && entry.contentRect.height > 0) {
|
||||
setHeight(entry.contentRect.height)
|
||||
}
|
||||
})
|
||||
ro.observe(el)
|
||||
setHeight(el.getBoundingClientRect().height)
|
||||
return () => ro.disconnect()
|
||||
}, [])
|
||||
|
||||
return [height, ref]
|
||||
}
|
||||
Reference in New Issue
Block a user