refactoring
This commit is contained in:
@@ -31,6 +31,23 @@ This doc summarizes recent improvements and suggested next steps for readability
|
|||||||
- **CanvasContextMenuContent** (`app/canvas/CanvasContextMenuContent.tsx`): context menu content (Create Node grouped by classification, Paste). CanvasPage passes `onCreateNode` and `onPaste`.
|
- **CanvasContextMenuContent** (`app/canvas/CanvasContextMenuContent.tsx`): context menu content (Create Node grouped by classification, Paste). CanvasPage passes `onCreateNode` and `onPaste`.
|
||||||
- **useCanvasConnectionPath** (`app/canvas/useCanvasConnectionPath.ts`): all connection-path state and callbacks (updating/trigger/paused/error node ids, path node ids, start/end update, add/remove paused/error). CanvasPage calls the hook with `edges` and passes the result into FlowContext.
|
- **useCanvasConnectionPath** (`app/canvas/useCanvasConnectionPath.ts`): all connection-path state and callbacks (updating/trigger/paused/error node ids, path node ids, start/end update, add/remove paused/error). CanvasPage calls the hook with `edges` and passes the result into FlowContext.
|
||||||
|
|
||||||
|
### 6. useCanvasGraph and canvas graph utils
|
||||||
|
|
||||||
|
- **canvasGraphUtils.ts**: `getExampleGraph()`, `getInitialGraph(projectId)` (load from storage or return example). Example nodes/edges live here.
|
||||||
|
- **useCanvasGraph(projectId)**: wraps `useGraphStateWithHistory` with initial graph from `getInitialGraph(projectId)` and debounced save to storage when `projectId` is set. CanvasPage uses this instead of inline state + persistence.
|
||||||
|
|
||||||
|
### 7. Config types registry
|
||||||
|
|
||||||
|
- **configTypes.ts**: `configTypeRegistry` (Map), `registerConfigType(type)`, `getConfigTypes()`, `getConfigTypeIds()`, `registerBuiltinConfigTypes()`. Built-in types are in `BUILTIN_CONFIG_TYPES` and registered at app init. `getConfigType(id)` looks up in the registry; `getConfigTypeId(data)` uses registered ids. ConfigNode uses `getConfigTypes()` instead of `CONFIG_TYPES`. New output types can call `registerConfigType()` without editing the core array.
|
||||||
|
|
||||||
|
### 8. Consistent NodeLike / EdgeLike
|
||||||
|
|
||||||
|
- **templateRefs.ts**: `NodeLike` includes `type?: string`; both types documented. Single source of truth for minimal node/edge shape in the graph lib.
|
||||||
|
- **sourceRenderingLogic**: context uses `NodeLike[]` and `EdgeLike[]` from templateRefs.
|
||||||
|
- **renderingSignatures**: imports and re-exports `NodeLike` / `EdgeLike` from templateRefs; no local duplicate types.
|
||||||
|
- **config/renderingLogic**: uses context nodes/edges directly (no casts); `setVarInContext(src: NodeLike)`.
|
||||||
|
- **useRenderingNodeState**: casts to `NodeLike[]` / `EdgeLike[]` when calling `buildSourceSignatures` (types from renderingSignatures).
|
||||||
|
|
||||||
## Design patterns in use
|
## Design patterns in use
|
||||||
|
|
||||||
| Pattern | Where |
|
| Pattern | Where |
|
||||||
@@ -43,8 +60,4 @@ This doc summarizes recent improvements and suggested next steps for readability
|
|||||||
|
|
||||||
## Suggested next steps
|
## Suggested next steps
|
||||||
|
|
||||||
1. **CanvasPage**: Further split optional: e.g. `useCanvasGraph()` for graph state + persistence + connection rules, so the page is mostly composition and layout.
|
- **CanvasPage**: Optional further split (e.g. move connection validation or node/edge change handlers into a hook) if the file grows again.
|
||||||
|
|
||||||
2. **Config types**: If you add more output types (e.g. Mermaid), consider a small registry API (`registerConfigType`, `getConfigType`) instead of a single large `CONFIG_TYPES` array.
|
|
||||||
|
|
||||||
3. **Consistent node shape in lib**: `templateRefs` and `renderingSignatures` use `EdgeLike` / `NodeLike`; standardize where appropriate to reduce casts.
|
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import { AnimatedEdge } from '@/components/graph/AnimatedEdge'
|
|||||||
import FlowContext from '@/lib/graph/flowContext'
|
import FlowContext from '@/lib/graph/flowContext'
|
||||||
import { useTheme } from '@/lib/themeContext'
|
import { useTheme } from '@/lib/themeContext'
|
||||||
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
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 { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu'
|
||||||
import { CanvasContextMenuContent } from '@/app/canvas/CanvasContextMenuContent'
|
import { CanvasContextMenuContent } from '@/app/canvas/CanvasContextMenuContent'
|
||||||
import { useCanvasConnectionPath } from '@/app/canvas/useCanvasConnectionPath'
|
import { useCanvasConnectionPath } from '@/app/canvas/useCanvasConnectionPath'
|
||||||
@@ -48,7 +49,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { FolderOpen, FileStack, CircleDotDashed } from 'lucide-react'
|
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 {
|
import {
|
||||||
getRegisteredNodeTypes,
|
getRegisteredNodeTypes,
|
||||||
getRegisteredNodeTypeIds,
|
getRegisteredNodeTypeIds,
|
||||||
@@ -58,12 +59,7 @@ import {
|
|||||||
} from '@/lib/graph/nodeRegistry'
|
} from '@/lib/graph/nodeRegistry'
|
||||||
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
|
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import {
|
import { PROJECT_FILE_EXT, PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage'
|
||||||
loadGraphFromStorage,
|
|
||||||
saveGraphToStorage,
|
|
||||||
PROJECT_FILE_EXT,
|
|
||||||
PROJECT_VERSION,
|
|
||||||
} from '@/app/pleroma/projectGraphStorage'
|
|
||||||
|
|
||||||
const SNAP_GRID: [number, number] = [15, 15]
|
const SNAP_GRID: [number, number] = [15, 15]
|
||||||
const DUPLICATE_OFFSET = { x: 30, y: 30 }
|
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],
|
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() {
|
function FlowFitViewOnLoad() {
|
||||||
const nodesInitialized = useNodesInitialized()
|
const nodesInitialized = useNodesInitialized()
|
||||||
const { fitView } = useReactFlow()
|
const { fitView } = useReactFlow()
|
||||||
@@ -196,21 +146,9 @@ export type CanvasPageProps = {
|
|||||||
projectId?: string
|
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) {
|
export function CanvasPage({ projectId }: CanvasPageProps) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { showMinimap } = usePlatform()
|
const { showMinimap } = usePlatform()
|
||||||
const initialGraph = useMemo(() => getInitialGraph(projectId), [projectId])
|
|
||||||
const {
|
const {
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
@@ -225,20 +163,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
canUndo,
|
canUndo,
|
||||||
canRedo,
|
canRedo,
|
||||||
setStateImmediate,
|
setStateImmediate,
|
||||||
} = useGraphStateWithHistory(initialGraph.nodes, initialGraph.edges)
|
} = useCanvasGraph(projectId)
|
||||||
|
|
||||||
// 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])
|
|
||||||
|
|
||||||
const importInputRef = useRef<HTMLInputElement | null>(null)
|
const importInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const [rfInstance, setRfInstance] = React.useState<unknown>(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
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import { nunjucksCompletionSource } from '@/lib/nunjucksAutocomplete'
|
|||||||
import { plantumlLanguage } from '@/lib/plantumlLanguage'
|
import { plantumlLanguage } from '@/lib/plantumlLanguage'
|
||||||
import { useTheme } from '@/lib/themeContext'
|
import { useTheme } from '@/lib/themeContext'
|
||||||
import {
|
import {
|
||||||
CONFIG_TYPES,
|
getConfigTypes,
|
||||||
getConfigContent,
|
getConfigContent,
|
||||||
getConfigType,
|
getConfigType,
|
||||||
getConfigTypeId,
|
getConfigTypeId,
|
||||||
@@ -261,7 +261,7 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{CONFIG_TYPES.map((t) => (
|
{getConfigTypes().map((t) => (
|
||||||
<SelectItem key={t.id} value={t.id} className="text-xs">
|
<SelectItem key={t.id} value={t.id} className="text-xs">
|
||||||
{t.label}
|
{t.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -7,10 +7,7 @@
|
|||||||
import nunjucks from 'nunjucks'
|
import nunjucks from 'nunjucks'
|
||||||
import type { ResolvedContentResult, SourceRenderingLogicContext } from '@/lib/graph/rendering'
|
import type { ResolvedContentResult, SourceRenderingLogicContext } from '@/lib/graph/rendering'
|
||||||
import { getConfigContent, getConfigType, getConfigTypeId, type ConfigTypeId } from '@/lib/graph/rendering'
|
import { getConfigContent, getConfigType, getConfigTypeId, type ConfigTypeId } from '@/lib/graph/rendering'
|
||||||
import { isReachable, resolveExtendsRef, getTemplateRefs } from '@/lib/graph/templateRefs'
|
import { isReachable, resolveExtendsRef, getTemplateRefs, type NodeLike } from '@/lib/graph/templateRefs'
|
||||||
|
|
||||||
type Node = { id: string; type?: string; data?: unknown }
|
|
||||||
type Edge = { id: string; source: string; target: string }
|
|
||||||
|
|
||||||
export async function getResolvedContentForConfig(context: SourceRenderingLogicContext): Promise<ResolvedContentResult> {
|
export async function getResolvedContentForConfig(context: SourceRenderingLogicContext): Promise<ResolvedContentResult> {
|
||||||
const { nodes, edges, sourceNodeId, renderNodeId } = context
|
const { nodes, edges, sourceNodeId, renderNodeId } = context
|
||||||
@@ -24,11 +21,11 @@ export async function getResolvedContentForConfig(context: SourceRenderingLogicC
|
|||||||
const configIdsUsed = new Set<string>()
|
const configIdsUsed = new Set<string>()
|
||||||
|
|
||||||
const addConfigAndRefs = (templateName: string, visited = new Set<string>()) => {
|
const addConfigAndRefs = (templateName: string, visited = new Set<string>()) => {
|
||||||
const refId = resolveExtendsRef(nodes as { id: string; data?: unknown }[], templateName)
|
const refId = resolveExtendsRef(nodes, templateName)
|
||||||
if (visited.has(refId)) throw new Error(`Circular reference detected: ${templateName}`)
|
if (visited.has(refId)) throw new Error(`Circular reference detected: ${templateName}`)
|
||||||
const node = nodes.find((n) => n.id === refId && n.type === 'config')
|
const node = nodes.find((n) => n.id === refId && n.type === 'config')
|
||||||
if (!node) throw new Error(`Config not found: ${templateName}`)
|
if (!node) throw new Error(`Config not found: ${templateName}`)
|
||||||
if (refId !== srcId && !isReachable(edges as { source: string; target: string }[], refId, id))
|
if (refId !== srcId && !isReachable(edges, refId, id))
|
||||||
throw new Error(`Referenced config not connected to renderer: ${templateName}`)
|
throw new Error(`Referenced config not connected to renderer: ${templateName}`)
|
||||||
visited.add(refId)
|
visited.add(refId)
|
||||||
configIdsUsed.add(refId)
|
configIdsUsed.add(refId)
|
||||||
@@ -40,10 +37,10 @@ export async function getResolvedContentForConfig(context: SourceRenderingLogicC
|
|||||||
|
|
||||||
const configLoader = {
|
const configLoader = {
|
||||||
getSource: (name: string): { src: string; path: string } | null => {
|
getSource: (name: string): { src: string; path: string } | null => {
|
||||||
const refId = resolveExtendsRef(nodes as { id: string; data?: unknown }[], name)
|
const refId = resolveExtendsRef(nodes, name)
|
||||||
const node = nodes.find((n) => n.id === refId && n.type === 'config')
|
const node = nodes.find((n) => n.id === refId && n.type === 'config')
|
||||||
if (!node) return null
|
if (!node) return null
|
||||||
if (refId !== srcId && !isReachable(edges as { source: string; target: string }[], refId, id))
|
if (refId !== srcId && !isReachable(edges, refId, id))
|
||||||
throw new Error(`Referenced config not connected to renderer: ${name}`)
|
throw new Error(`Referenced config not connected to renderer: ${name}`)
|
||||||
return {
|
return {
|
||||||
src: getConfigContent((node.data ?? undefined) as Record<string, unknown> | undefined),
|
src: getConfigContent((node.data ?? undefined) as Record<string, unknown> | undefined),
|
||||||
@@ -53,7 +50,7 @@ export async function getResolvedContentForConfig(context: SourceRenderingLogicC
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nunjucksContext = Object.create(null) as Record<string, unknown>
|
const nunjucksContext = Object.create(null) as Record<string, unknown>
|
||||||
const setVarInContext = (src: Node) => {
|
const setVarInContext = (src: NodeLike) => {
|
||||||
const v = (src.data as Record<string, unknown>)?.value
|
const v = (src.data as Record<string, unknown>)?.value
|
||||||
const str = v === undefined || v === null ? '' : String(v)
|
const str = v === undefined || v === null ? '' : String(v)
|
||||||
nunjucksContext[src.id] =
|
nunjucksContext[src.id] =
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
processSvgDisplay,
|
processSvgDisplay,
|
||||||
stripTemplateSyntax,
|
stripTemplateSyntax,
|
||||||
} from '@/lib/graph/rendering'
|
} from '@/lib/graph/rendering'
|
||||||
import { buildSourceSignatures } from '@/lib/graph/renderingSignatures'
|
import { buildSourceSignatures, type NodeLike, type EdgeLike } from '@/lib/graph/renderingSignatures'
|
||||||
import type { ConfigTypeId, SourceRenderingLogicContext } from '@/lib/graph/rendering'
|
import type { ConfigTypeId, SourceRenderingLogicContext } from '@/lib/graph/rendering'
|
||||||
|
|
||||||
export type RenderingNodeData = {
|
export type RenderingNodeData = {
|
||||||
@@ -137,13 +137,7 @@ export function useRenderingNodeState(
|
|||||||
: ''
|
: ''
|
||||||
|
|
||||||
const signatures = useMemo(
|
const signatures = useMemo(
|
||||||
() =>
|
() => buildSourceSignatures(nodes as NodeLike[], edges as EdgeLike[], id, incomingIds),
|
||||||
buildSourceSignatures(
|
|
||||||
nodes as { id: string; type?: string; data?: unknown }[],
|
|
||||||
edges as { source: string; target: string }[],
|
|
||||||
id,
|
|
||||||
incomingIds
|
|
||||||
),
|
|
||||||
[nodes, edges, id, incomingIds]
|
[nodes, edges, id, incomingIds]
|
||||||
)
|
)
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -144,7 +144,27 @@ async function renderWireframe(content: string, options?: RenderOptions): Promis
|
|||||||
return svg
|
return svg
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CONFIG_TYPES: ConfigType[] = [
|
/** Registry for config (output) types. Built-in types register in registerBuiltinConfigTypes(). */
|
||||||
|
const configTypeRegistry = new Map<string, ConfigType>()
|
||||||
|
|
||||||
|
export function registerConfigType(type: ConfigType): void {
|
||||||
|
if (configTypeRegistry.has(type.id)) {
|
||||||
|
console.warn(`[configTypes] Overwriting config type: ${type.id}`)
|
||||||
|
}
|
||||||
|
configTypeRegistry.set(type.id, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All registered config types (for type selector, etc.). */
|
||||||
|
export function getConfigTypes(): ConfigType[] {
|
||||||
|
return Array.from(configTypeRegistry.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All registered config type ids. */
|
||||||
|
export function getConfigTypeIds(): string[] {
|
||||||
|
return Array.from(configTypeRegistry.keys())
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILTIN_CONFIG_TYPES: ConfigType[] = [
|
||||||
{
|
{
|
||||||
id: 'plantuml',
|
id: 'plantuml',
|
||||||
label: 'Diagram',
|
label: 'Diagram',
|
||||||
@@ -220,11 +240,17 @@ export const CONFIG_TYPES: ConfigType[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const CONFIG_TYPE_IDS = CONFIG_TYPES.map((t) => t.id)
|
|
||||||
export const DEFAULT_CONFIG_TYPE_ID: ConfigTypeId = 'plantuml'
|
export const DEFAULT_CONFIG_TYPE_ID: ConfigTypeId = 'plantuml'
|
||||||
|
|
||||||
export function getConfigType(id: ConfigTypeId): ConfigType {
|
/** Register built-in config types (plantuml, markdown, wireframe). Call once at app init. */
|
||||||
const t = CONFIG_TYPES.find((c) => c.id === id)
|
export function registerBuiltinConfigTypes(): void {
|
||||||
|
for (const t of BUILTIN_CONFIG_TYPES) {
|
||||||
|
registerConfigType(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigType(id: string): ConfigType {
|
||||||
|
const t = configTypeRegistry.get(id)
|
||||||
if (!t) throw new Error(`Unknown config type: ${id}`)
|
if (!t) throw new Error(`Unknown config type: ${id}`)
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
@@ -264,9 +290,9 @@ export function getConfigContent(data: Record<string, unknown> | undefined): str
|
|||||||
return typeof content === 'string' ? content : ''
|
return typeof content === 'string' ? content : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Config type id from data (default plantuml). */
|
/** Config type id from data (default plantuml). Uses registered ids. */
|
||||||
export function getConfigTypeId(data: Record<string, unknown> | undefined): ConfigTypeId {
|
export function getConfigTypeId(data: Record<string, unknown> | undefined): ConfigTypeId {
|
||||||
if (!data || data.configType == null) return 'plantuml'
|
if (!data || data.configType == null) return 'plantuml'
|
||||||
const id = data.configType
|
const id = String(data.configType)
|
||||||
return CONFIG_TYPE_IDS.includes(id as ConfigTypeId) ? (id as ConfigTypeId) : 'plantuml'
|
return configTypeRegistry.has(id) ? (id as ConfigTypeId) : 'plantuml'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
*
|
*
|
||||||
* ## Implementing a new output type
|
* ## Implementing a new output type
|
||||||
* - Add a config type (or equivalent) fulfilling `IOutputTypeRenderer`.
|
* - Add a config type (or equivalent) fulfilling `IOutputTypeRenderer`.
|
||||||
* - Register in CONFIG_TYPES and use `getConfigType(id)` in the resolve step.
|
* - Call registerConfigType(type) (e.g. from registerBuiltinConfigTypes) and use getConfigType(id) in the resolve step.
|
||||||
*
|
*
|
||||||
* ## Shared helpers
|
* ## Shared helpers
|
||||||
* - templateRefs.ts: isReachable, resolveExtendsRef, getTemplateRefs (Nunjucks extends/include/import).
|
* - templateRefs.ts: isReachable, resolveExtendsRef, getTemplateRefs (Nunjucks extends/include/import).
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Pure functions to build "connected node ids" and source signatures for the
|
* Pure functions to build "connected node ids" and source signatures for the
|
||||||
* rendering pipeline. Used by useRenderingNodeState so the hook stays thin and
|
* rendering pipeline. Used by useRenderingNodeState so the hook stays thin and
|
||||||
* this logic is testable in isolation.
|
* this logic is testable in isolation. Uses NodeLike/EdgeLike from templateRefs.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getConfigContent } from '@/lib/graph/rendering'
|
import { getConfigContent } from '@/lib/graph/rendering'
|
||||||
import { isReachable, resolveExtendsRef, getTemplateRefs } from '@/lib/graph/templateRefs'
|
import { isReachable, resolveExtendsRef, getTemplateRefs, type NodeLike, type EdgeLike } from '@/lib/graph/templateRefs'
|
||||||
|
|
||||||
export type NodeLike = { id: string; type?: string; data?: unknown }
|
export type { NodeLike, EdgeLike } from '@/lib/graph/templateRefs'
|
||||||
export type EdgeLike = { source: string; target: string }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collects all node ids that affect the render: direct incoming nodes plus
|
* Collects all node ids that affect the render: direct incoming nodes plus
|
||||||
|
|||||||
@@ -7,17 +7,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ConfigTypeId } from './configTypes'
|
import type { ConfigTypeId } from './configTypes'
|
||||||
|
import type { NodeLike, EdgeLike } from './templateRefs'
|
||||||
export type { OutputMenuDescriptor, OutputMenuItemDescriptor, OutputMenuAction } from './configTypes'
|
export type { OutputMenuDescriptor, OutputMenuItemDescriptor, OutputMenuAction } from './configTypes'
|
||||||
|
|
||||||
/** Context passed into the resolve step (graph, source id, callbacks). See {@link IRenderingContext}. */
|
/** Context passed into the resolve step (graph, source id, callbacks). See {@link IRenderingContext}. */
|
||||||
export type SourceRenderingLogicContext = {
|
export type SourceRenderingLogicContext = {
|
||||||
nodes: { id: string; type?: string; data?: unknown }[]
|
nodes: NodeLike[]
|
||||||
edges: { id: string; source: string; target: string }[]
|
edges: EdgeLike[]
|
||||||
sourceNodeId: string
|
sourceNodeId: string
|
||||||
renderNodeId: string
|
renderNodeId: string
|
||||||
viewportWidth?: number
|
viewportWidth?: number
|
||||||
viewportHeight?: number
|
viewportHeight?: number
|
||||||
setNodes?: (updater: (nodes: { id: string; type?: string; data?: unknown }[]) => { id: string; type?: string; data?: unknown }[]) => void
|
setNodes?: (updater: (nodes: NodeLike[]) => NodeLike[]) => void
|
||||||
aiConnection?: unknown
|
aiConnection?: unknown
|
||||||
onStreamingStart?: () => void
|
onStreamingStart?: () => void
|
||||||
onStreamingChunk?: (chunk: string) => void
|
onStreamingChunk?: (chunk: string) => void
|
||||||
|
|||||||
@@ -4,8 +4,11 @@
|
|||||||
* so template parsing and "connected configs" stay in one place.
|
* so template parsing and "connected configs" stay in one place.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** Minimal edge shape for graph helpers (reachability, path, signatures). */
|
||||||
export type EdgeLike = { source: string; target: string }
|
export type EdgeLike = { source: string; target: string }
|
||||||
export type NodeLike = { id: string; data?: unknown }
|
|
||||||
|
/** Minimal node shape for graph helpers (resolve refs, signatures). */
|
||||||
|
export type NodeLike = { id: string; type?: string; data?: unknown }
|
||||||
|
|
||||||
/** BFS: is target reachable from start following directed edges? */
|
/** BFS: is target reachable from start following directed edges? */
|
||||||
export function isReachable(
|
export function isReachable(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
|
|||||||
import { Toaster } from 'sonner'
|
import { Toaster } from 'sonner'
|
||||||
import { ThemeProvider } from './lib/themeContext'
|
import { ThemeProvider } from './lib/themeContext'
|
||||||
import { registerBuiltinNodes } from './lib/graph/registerBuiltinNodes'
|
import { registerBuiltinNodes } from './lib/graph/registerBuiltinNodes'
|
||||||
|
import { registerBuiltinConfigTypes } from './lib/graph/configTypes'
|
||||||
import { KosmosPage } from './app/kosmos/KosmosPage'
|
import { KosmosPage } from './app/kosmos/KosmosPage'
|
||||||
import { ProjectsPage } from './app/pleroma/PleromaPage'
|
import { ProjectsPage } from './app/pleroma/PleromaPage'
|
||||||
import { KeromaPage } from './app/keroma/KeromaPage'
|
import { KeromaPage } from './app/keroma/KeromaPage'
|
||||||
@@ -11,6 +12,7 @@ import { CanvasRoute } from './app/canvas/CanvasRoute'
|
|||||||
import './styles.css'
|
import './styles.css'
|
||||||
import '@xyflow/react/dist/style.css'
|
import '@xyflow/react/dist/style.css'
|
||||||
|
|
||||||
|
registerBuiltinConfigTypes()
|
||||||
registerBuiltinNodes()
|
registerBuiltinNodes()
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
|||||||
Reference in New Issue
Block a user