refactoring
This commit is contained in:
@@ -21,6 +21,16 @@ This doc summarizes recent improvements and suggested next steps for readability
|
|||||||
|
|
||||||
- **rendering.ts** documents the 3-step pipeline, how to add a source/output type, and points to `templateRefs.ts` for shared helpers.
|
- **rendering.ts** documents the 3-step pipeline, how to add a source/output type, and points to `templateRefs.ts` for shared helpers.
|
||||||
|
|
||||||
|
### 4. Rendering signatures (pure module)
|
||||||
|
|
||||||
|
- **Added** `lib/graph/renderingSignatures.ts`: `buildConnectedNodeIds`, `buildSourceSignatures` (connectedNodeIds + all five signatures + sourceSignature).
|
||||||
|
- **Refactored** `useRenderingNodeState` to call `buildSourceSignatures` in a single useMemo; hook is shorter and signature logic is testable in isolation.
|
||||||
|
|
||||||
|
### 5. Canvas split
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
## Design patterns in use
|
## Design patterns in use
|
||||||
|
|
||||||
| Pattern | Where |
|
| Pattern | Where |
|
||||||
@@ -33,15 +43,8 @@ This doc summarizes recent improvements and suggested next steps for readability
|
|||||||
|
|
||||||
## Suggested next steps
|
## Suggested next steps
|
||||||
|
|
||||||
1. **CanvasPage** (~950 lines): Split into smaller units, e.g.:
|
1. **CanvasPage**: Further split optional: e.g. `useCanvasGraph()` for graph state + persistence + connection rules, so the page is mostly composition and layout.
|
||||||
- `useCanvasGraph()` or similar for graph state and connection rules.
|
|
||||||
- A dedicated component for the context menu (add node, paste, etc.).
|
|
||||||
- Keeps CanvasPage as composition + layout.
|
|
||||||
|
|
||||||
2. **useRenderingNodeState**: Consider extracting:
|
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.
|
||||||
- Signature building (config/edges/variables/functions/data) into a pure function or small module, e.g. `buildSourceSignatures(nodes, edges, id, incomingIds, getConfigContent)`.
|
|
||||||
- Makes the hook easier to read and the logic testable in isolation.
|
|
||||||
|
|
||||||
3. **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, so extensions can register without editing the core list.
|
3. **Consistent node shape in lib**: `templateRefs` and `renderingSignatures` use `EdgeLike` / `NodeLike`; standardize where appropriate to reduce casts.
|
||||||
|
|
||||||
4. **Consistent node shape in lib**: `templateRefs` uses `EdgeLike` / `NodeLike`; other graph code uses inline `{ source, target }` or `nodes as ...`. You could standardize on the same minimal types where appropriate to reduce casts.
|
|
||||||
|
|||||||
62
frontend/src/app/canvas/CanvasContextMenuContent.tsx
Normal file
62
frontend/src/app/canvas/CanvasContextMenuContent.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Context menu content for the canvas: Create Node (grouped by classification) and Paste.
|
||||||
|
* Used inside CanvasPage so the menu structure lives in one place.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
} from '@/components/ui/context-menu'
|
||||||
|
import { ClipboardPaste } from 'lucide-react'
|
||||||
|
import { getRegisteredNodeTypesGroupedByClassification } from '@/lib/graph/nodeRegistry'
|
||||||
|
|
||||||
|
export type CanvasContextMenuContentProps = {
|
||||||
|
onCreateNode: (type: string) => void
|
||||||
|
onPaste: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanvasContextMenuContent({ onCreateNode, onPaste }: CanvasContextMenuContentProps) {
|
||||||
|
const groups = getRegisteredNodeTypesGroupedByClassification()
|
||||||
|
return (
|
||||||
|
<ContextMenuContent className="w-48" aria-label="Canvas menu: create node or paste">
|
||||||
|
<ContextMenuGroup>
|
||||||
|
<ContextMenuSub>
|
||||||
|
<ContextMenuSubTrigger>Create Node</ContextMenuSubTrigger>
|
||||||
|
<ContextMenuSubContent className="w-44">
|
||||||
|
{groups.map(
|
||||||
|
(group, groupIndex) =>
|
||||||
|
group.types.length > 0 && (
|
||||||
|
<React.Fragment key={group.classification}>
|
||||||
|
{groupIndex > 0 && <ContextMenuSeparator />}
|
||||||
|
<ContextMenuGroup>
|
||||||
|
<ContextMenuLabel className="text-muted-foreground">
|
||||||
|
{group.label}
|
||||||
|
</ContextMenuLabel>
|
||||||
|
{group.types.map((desc) => (
|
||||||
|
<ContextMenuItem key={desc.id} onSelect={() => onCreateNode(desc.id)}>
|
||||||
|
{desc.menuIcon}
|
||||||
|
{desc.menuLabel}
|
||||||
|
</ContextMenuItem>
|
||||||
|
))}
|
||||||
|
</ContextMenuGroup>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ContextMenuSubContent>
|
||||||
|
</ContextMenuSub>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
<ContextMenuItem onSelect={onPaste}>
|
||||||
|
<ClipboardPaste className="mr-2 h-4 w-4" />
|
||||||
|
Paste
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuGroup>
|
||||||
|
</ContextMenuContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -27,18 +27,9 @@ 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 { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory'
|
||||||
import {
|
import { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu'
|
||||||
ContextMenu,
|
import { CanvasContextMenuContent } from '@/app/canvas/CanvasContextMenuContent'
|
||||||
ContextMenuContent,
|
import { useCanvasConnectionPath } from '@/app/canvas/useCanvasConnectionPath'
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuLabel,
|
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuSub,
|
|
||||||
ContextMenuSubContent,
|
|
||||||
ContextMenuSubTrigger,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
ContextMenuGroup,
|
|
||||||
} from '@/components/ui/context-menu'
|
|
||||||
import { CanvasMenubar } from '@/app/canvas/CanvasMenubar'
|
import { CanvasMenubar } from '@/app/canvas/CanvasMenubar'
|
||||||
import { createContextualNode } from '@/app/canvas/ContextualZoomNode'
|
import { createContextualNode } from '@/app/canvas/ContextualZoomNode'
|
||||||
import { FlowKeyboardShortcuts } from '@/components/graph/FlowKeyboardShortcuts'
|
import { FlowKeyboardShortcuts } from '@/components/graph/FlowKeyboardShortcuts'
|
||||||
@@ -56,17 +47,15 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { ClipboardPaste, FolderOpen, FileStack, CircleDotDashed } from 'lucide-react'
|
import { FolderOpen, FileStack, CircleDotDashed } from 'lucide-react'
|
||||||
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '@/lib/graph/flowUtils'
|
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '@/lib/graph/flowUtils'
|
||||||
import {
|
import {
|
||||||
getRegisteredNodeTypes,
|
getRegisteredNodeTypes,
|
||||||
getRegisteredNodeTypesGroupedByClassification,
|
|
||||||
getRegisteredNodeTypeIds,
|
getRegisteredNodeTypeIds,
|
||||||
getDefaultStyle,
|
getDefaultStyle,
|
||||||
getNodeType,
|
getNodeType,
|
||||||
isConnectionAllowed,
|
isConnectionAllowed,
|
||||||
} from '@/lib/graph/nodeRegistry'
|
} from '@/lib/graph/nodeRegistry'
|
||||||
import { getPathNodeIds, getPausedSegmentNodeIds } from '@/lib/graph/graphPath'
|
|
||||||
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 {
|
||||||
@@ -78,8 +67,6 @@ import {
|
|||||||
|
|
||||||
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 }
|
||||||
/** Minimum time (ms) the connection ant trail runs when a path update is in progress. */
|
|
||||||
const CONNECTION_PATH_UPDATE_MIN_MS = 1500
|
|
||||||
const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
|
const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
|
||||||
x: Math.round(x / SNAP_GRID[0]) * SNAP_GRID[0],
|
x: Math.round(x / SNAP_GRID[0]) * SNAP_GRID[0],
|
||||||
y: Math.round(y / SNAP_GRID[1]) * SNAP_GRID[1],
|
y: Math.round(y / SNAP_GRID[1]) * SNAP_GRID[1],
|
||||||
@@ -267,82 +254,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
const [isSelecting, setIsSelecting] = React.useState(false)
|
const [isSelecting, setIsSelecting] = React.useState(false)
|
||||||
const [ariaAnnouncement, setAriaAnnouncement] = React.useState<string | null>(null)
|
const [ariaAnnouncement, setAriaAnnouncement] = React.useState<string | null>(null)
|
||||||
const [fullscreenNodeId, setFullscreenNodeId] = React.useState<string | null>(null)
|
const [fullscreenNodeId, setFullscreenNodeId] = React.useState<string | null>(null)
|
||||||
const [connectionPathUpdatingNodeIds, setConnectionPathUpdatingNodeIds] = React.useState<string[]>([])
|
const connectionPath = useCanvasConnectionPath(edges)
|
||||||
const [connectionPathTriggerNodeIds, setConnectionPathTriggerNodeIds] = React.useState<string[]>([])
|
|
||||||
const pathUpdateNodeIdsRef = useRef<Set<string>>(new Set())
|
|
||||||
const pathUpdateStartTimeRef = useRef<number | null>(null)
|
|
||||||
const pathUpdateEndTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
||||||
|
|
||||||
const [connectionPathPausedNodeIds, setConnectionPathPausedNodeIds] = React.useState<string[]>([])
|
|
||||||
const [connectionPathErrorNodeIds, setConnectionPathErrorNodeIds] = React.useState<string[]>([])
|
|
||||||
const connectionPathPausedNodeIdsRef = useRef<string[]>([])
|
|
||||||
connectionPathPausedNodeIdsRef.current = connectionPathPausedNodeIds
|
|
||||||
|
|
||||||
const clearPathUpdateSession = useCallback(() => {
|
|
||||||
setConnectionPathUpdatingNodeIds([])
|
|
||||||
if (connectionPathPausedNodeIdsRef.current.length === 0) {
|
|
||||||
setConnectionPathTriggerNodeIds([])
|
|
||||||
setConnectionPathPausedNodeIds([])
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const startConnectionPathUpdate = useCallback((nodeId: string) => {
|
|
||||||
const ref = pathUpdateNodeIdsRef.current
|
|
||||||
ref.add(nodeId)
|
|
||||||
if (ref.size === 1) {
|
|
||||||
pathUpdateStartTimeRef.current = Date.now()
|
|
||||||
if (pathUpdateEndTimeoutRef.current != null) {
|
|
||||||
clearTimeout(pathUpdateEndTimeoutRef.current)
|
|
||||||
pathUpdateEndTimeoutRef.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setConnectionPathUpdatingNodeIds(Array.from(ref))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const endConnectionPathUpdate = useCallback((nodeId: string) => {
|
|
||||||
const ref = pathUpdateNodeIdsRef.current
|
|
||||||
ref.delete(nodeId)
|
|
||||||
if (ref.size > 0) {
|
|
||||||
setConnectionPathUpdatingNodeIds(Array.from(ref))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const startedAt = pathUpdateStartTimeRef.current ?? 0
|
|
||||||
const elapsed = Date.now() - startedAt
|
|
||||||
const remaining = Math.max(0, CONNECTION_PATH_UPDATE_MIN_MS - elapsed)
|
|
||||||
if (remaining === 0) {
|
|
||||||
clearPathUpdateSession()
|
|
||||||
} else {
|
|
||||||
pathUpdateEndTimeoutRef.current = setTimeout(() => {
|
|
||||||
pathUpdateEndTimeoutRef.current = null
|
|
||||||
clearPathUpdateSession()
|
|
||||||
}, remaining)
|
|
||||||
}
|
|
||||||
}, [clearPathUpdateSession])
|
|
||||||
|
|
||||||
const pathTriggerBatchRef = useRef<Set<string>>(new Set())
|
|
||||||
const pathTriggerScheduledRef = useRef(false)
|
|
||||||
const addConnectionPathTrigger = useCallback((nodeId: string) => {
|
|
||||||
pathTriggerBatchRef.current.add(nodeId)
|
|
||||||
if (pathTriggerScheduledRef.current) return
|
|
||||||
pathTriggerScheduledRef.current = true
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
pathTriggerScheduledRef.current = false
|
|
||||||
const batch = new Set(pathTriggerBatchRef.current)
|
|
||||||
pathTriggerBatchRef.current = new Set()
|
|
||||||
if (batch.size === 0) return
|
|
||||||
setConnectionPathTriggerNodeIds((prev) => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
batch.forEach((id) => next.add(id))
|
|
||||||
return next.size === prev.length && prev.every((id) => next.has(id)) ? prev : Array.from(next)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
React.useEffect(() => () => {
|
|
||||||
if (pathUpdateEndTimeoutRef.current != null) {
|
|
||||||
clearTimeout(pathUpdateEndTimeoutRef.current)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const nodesRef = useRef(nodes)
|
const nodesRef = useRef(nodes)
|
||||||
nodesRef.current = nodes
|
nodesRef.current = nodes
|
||||||
@@ -551,50 +463,6 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
flowActionsRef.current?.pasteAtViewportCenter?.()
|
flowActionsRef.current?.pasteAtViewportCenter?.()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const connectionPathNodeIds = useMemo(
|
|
||||||
() =>
|
|
||||||
getPathNodeIds(
|
|
||||||
edges,
|
|
||||||
connectionPathUpdatingNodeIds,
|
|
||||||
connectionPathTriggerNodeIds,
|
|
||||||
connectionPathPausedNodeIds
|
|
||||||
),
|
|
||||||
[edges, connectionPathUpdatingNodeIds, connectionPathTriggerNodeIds, connectionPathPausedNodeIds]
|
|
||||||
)
|
|
||||||
|
|
||||||
const connectionPathPausedSegmentNodeIds = useMemo(
|
|
||||||
() =>
|
|
||||||
getPausedSegmentNodeIds(
|
|
||||||
edges,
|
|
||||||
connectionPathNodeIds,
|
|
||||||
connectionPathTriggerNodeIds,
|
|
||||||
connectionPathPausedNodeIds
|
|
||||||
),
|
|
||||||
[edges, connectionPathNodeIds, connectionPathTriggerNodeIds, connectionPathPausedNodeIds]
|
|
||||||
)
|
|
||||||
|
|
||||||
const connectionPathActiveSegmentNodeIds = useMemo(() => {
|
|
||||||
const active = new Set(connectionPathNodeIds)
|
|
||||||
connectionPathPausedSegmentNodeIds.forEach((id) => active.delete(id))
|
|
||||||
return active
|
|
||||||
}, [connectionPathNodeIds, connectionPathPausedSegmentNodeIds])
|
|
||||||
|
|
||||||
const addConnectionPathPausedNode = useCallback((nodeId: string) => {
|
|
||||||
setConnectionPathPausedNodeIds((prev) => (prev.includes(nodeId) ? prev : [...prev, nodeId]))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const removeConnectionPathPausedNode = useCallback((nodeId: string) => {
|
|
||||||
setConnectionPathPausedNodeIds((prev) => prev.filter((id) => id !== nodeId))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const addConnectionPathError = useCallback((nodeId: string) => {
|
|
||||||
setConnectionPathErrorNodeIds((prev) => (prev.includes(nodeId) ? prev : [...prev, nodeId]))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const removeConnectionPathError = useCallback((nodeId: string) => {
|
|
||||||
setConnectionPathErrorNodeIds((prev) => prev.filter((id) => id !== nodeId))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const flowContextValue = useMemo(
|
const flowContextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
nodes,
|
nodes,
|
||||||
@@ -609,20 +477,20 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
flowActionsRef,
|
flowActionsRef,
|
||||||
fullscreenNodeId,
|
fullscreenNodeId,
|
||||||
setFullscreenNodeId,
|
setFullscreenNodeId,
|
||||||
connectionPathUpdatingNodeIds,
|
connectionPathUpdatingNodeIds: connectionPath.connectionPathUpdatingNodeIds,
|
||||||
connectionPathTriggerNodeIds,
|
connectionPathTriggerNodeIds: connectionPath.connectionPathTriggerNodeIds,
|
||||||
addConnectionPathTrigger,
|
addConnectionPathTrigger: connectionPath.addConnectionPathTrigger,
|
||||||
connectionPathNodeIds,
|
connectionPathNodeIds: connectionPath.connectionPathNodeIds,
|
||||||
connectionPathPausedSegmentNodeIds,
|
connectionPathPausedSegmentNodeIds: connectionPath.connectionPathPausedSegmentNodeIds,
|
||||||
connectionPathActiveSegmentNodeIds,
|
connectionPathActiveSegmentNodeIds: connectionPath.connectionPathActiveSegmentNodeIds,
|
||||||
connectionPathPausedNodeIds,
|
connectionPathPausedNodeIds: connectionPath.connectionPathPausedNodeIds,
|
||||||
addConnectionPathPausedNode,
|
addConnectionPathPausedNode: connectionPath.addConnectionPathPausedNode,
|
||||||
removeConnectionPathPausedNode,
|
removeConnectionPathPausedNode: connectionPath.removeConnectionPathPausedNode,
|
||||||
connectionPathErrorNodeIds,
|
connectionPathErrorNodeIds: connectionPath.connectionPathErrorNodeIds,
|
||||||
addConnectionPathError,
|
addConnectionPathError: connectionPath.addConnectionPathError,
|
||||||
removeConnectionPathError,
|
removeConnectionPathError: connectionPath.removeConnectionPathError,
|
||||||
startConnectionPathUpdate,
|
startConnectionPathUpdate: connectionPath.startConnectionPathUpdate,
|
||||||
endConnectionPathUpdate,
|
endConnectionPathUpdate: connectionPath.endConnectionPathUpdate,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
nodes,
|
nodes,
|
||||||
@@ -637,20 +505,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
flowActionsRef,
|
flowActionsRef,
|
||||||
fullscreenNodeId,
|
fullscreenNodeId,
|
||||||
setFullscreenNodeId,
|
setFullscreenNodeId,
|
||||||
connectionPathUpdatingNodeIds,
|
connectionPath,
|
||||||
connectionPathTriggerNodeIds,
|
|
||||||
addConnectionPathTrigger,
|
|
||||||
connectionPathNodeIds,
|
|
||||||
connectionPathPausedSegmentNodeIds,
|
|
||||||
connectionPathActiveSegmentNodeIds,
|
|
||||||
connectionPathPausedNodeIds,
|
|
||||||
addConnectionPathPausedNode,
|
|
||||||
removeConnectionPathPausedNode,
|
|
||||||
connectionPathErrorNodeIds,
|
|
||||||
addConnectionPathError,
|
|
||||||
removeConnectionPathError,
|
|
||||||
startConnectionPathUpdate,
|
|
||||||
endConnectionPathUpdate,
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -900,39 +755,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent className="w-48" aria-label="Canvas menu: create node or paste">
|
<CanvasContextMenuContent onCreateNode={createNode} onPaste={pasteNode} />
|
||||||
<ContextMenuGroup>
|
|
||||||
<ContextMenuSub>
|
|
||||||
<ContextMenuSubTrigger>Create Node</ContextMenuSubTrigger>
|
|
||||||
<ContextMenuSubContent className="w-44">
|
|
||||||
{getRegisteredNodeTypesGroupedByClassification().map(
|
|
||||||
(group, groupIndex) =>
|
|
||||||
group.types.length > 0 && (
|
|
||||||
<React.Fragment key={group.classification}>
|
|
||||||
{groupIndex > 0 && <ContextMenuSeparator />}
|
|
||||||
<ContextMenuGroup>
|
|
||||||
<ContextMenuLabel className="text-muted-foreground">
|
|
||||||
{group.label}
|
|
||||||
</ContextMenuLabel>
|
|
||||||
{group.types.map((desc) => (
|
|
||||||
<ContextMenuItem key={desc.id} onSelect={() => createNode(desc.id)}>
|
|
||||||
{desc.menuIcon}
|
|
||||||
{desc.menuLabel}
|
|
||||||
</ContextMenuItem>
|
|
||||||
))}
|
|
||||||
</ContextMenuGroup>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ContextMenuSubContent>
|
|
||||||
</ContextMenuSub>
|
|
||||||
<ContextMenuSeparator />
|
|
||||||
<ContextMenuItem onSelect={() => pasteNode()}>
|
|
||||||
<ClipboardPaste className="mr-2 h-4 w-4" />
|
|
||||||
Paste
|
|
||||||
</ContextMenuItem>
|
|
||||||
</ContextMenuGroup>
|
|
||||||
</ContextMenuContent>
|
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
{fullscreenNodeId && (
|
{fullscreenNodeId && (
|
||||||
<FullscreenNodeOverlay
|
<FullscreenNodeOverlay
|
||||||
|
|||||||
174
frontend/src/app/canvas/useCanvasConnectionPath.ts
Normal file
174
frontend/src/app/canvas/useCanvasConnectionPath.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* Hook that holds all connection-path state and callbacks for the canvas.
|
||||||
|
* Used to show the "ant trail" along the path of an update (e.g. config → agent → render).
|
||||||
|
* Extracted from CanvasPage to keep the page focused on composition.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { getPathNodeIds, getPausedSegmentNodeIds } from '@/lib/graph/graphPath'
|
||||||
|
|
||||||
|
export type EdgeLike = { source: string; target: string }
|
||||||
|
|
||||||
|
/** Minimum time (ms) the connection ant trail runs when a path update is in progress. */
|
||||||
|
const CONNECTION_PATH_UPDATE_MIN_MS = 1500
|
||||||
|
|
||||||
|
export type UseCanvasConnectionPathResult = {
|
||||||
|
connectionPathUpdatingNodeIds: string[]
|
||||||
|
connectionPathTriggerNodeIds: string[]
|
||||||
|
connectionPathPausedNodeIds: string[]
|
||||||
|
connectionPathErrorNodeIds: string[]
|
||||||
|
connectionPathNodeIds: Set<string>
|
||||||
|
connectionPathPausedSegmentNodeIds: Set<string>
|
||||||
|
connectionPathActiveSegmentNodeIds: Set<string>
|
||||||
|
startConnectionPathUpdate: (nodeId: string) => void
|
||||||
|
endConnectionPathUpdate: (nodeId: string) => void
|
||||||
|
addConnectionPathTrigger: (nodeId: string) => void
|
||||||
|
addConnectionPathPausedNode: (nodeId: string) => void
|
||||||
|
removeConnectionPathPausedNode: (nodeId: string) => void
|
||||||
|
addConnectionPathError: (nodeId: string) => void
|
||||||
|
removeConnectionPathError: (nodeId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCanvasConnectionPath(edges: EdgeLike[]): UseCanvasConnectionPathResult {
|
||||||
|
const [connectionPathUpdatingNodeIds, setConnectionPathUpdatingNodeIds] = useState<string[]>([])
|
||||||
|
const [connectionPathTriggerNodeIds, setConnectionPathTriggerNodeIds] = useState<string[]>([])
|
||||||
|
const [connectionPathPausedNodeIds, setConnectionPathPausedNodeIds] = useState<string[]>([])
|
||||||
|
const [connectionPathErrorNodeIds, setConnectionPathErrorNodeIds] = useState<string[]>([])
|
||||||
|
|
||||||
|
const pathUpdateNodeIdsRef = useRef<Set<string>>(new Set())
|
||||||
|
const pathUpdateStartTimeRef = useRef<number | null>(null)
|
||||||
|
const pathUpdateEndTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
const connectionPathPausedNodeIdsRef = useRef<string[]>([])
|
||||||
|
connectionPathPausedNodeIdsRef.current = connectionPathPausedNodeIds
|
||||||
|
|
||||||
|
const pathTriggerBatchRef = useRef<Set<string>>(new Set())
|
||||||
|
const pathTriggerScheduledRef = useRef(false)
|
||||||
|
|
||||||
|
const clearPathUpdateSession = useCallback(() => {
|
||||||
|
setConnectionPathUpdatingNodeIds([])
|
||||||
|
if (connectionPathPausedNodeIdsRef.current.length === 0) {
|
||||||
|
setConnectionPathTriggerNodeIds([])
|
||||||
|
setConnectionPathPausedNodeIds([])
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const startConnectionPathUpdate = useCallback((nodeId: string) => {
|
||||||
|
const ref = pathUpdateNodeIdsRef.current
|
||||||
|
ref.add(nodeId)
|
||||||
|
if (ref.size === 1) {
|
||||||
|
pathUpdateStartTimeRef.current = Date.now()
|
||||||
|
if (pathUpdateEndTimeoutRef.current != null) {
|
||||||
|
clearTimeout(pathUpdateEndTimeoutRef.current)
|
||||||
|
pathUpdateEndTimeoutRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setConnectionPathUpdatingNodeIds(Array.from(ref))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const endConnectionPathUpdate = useCallback((nodeId: string) => {
|
||||||
|
const ref = pathUpdateNodeIdsRef.current
|
||||||
|
ref.delete(nodeId)
|
||||||
|
if (ref.size > 0) {
|
||||||
|
setConnectionPathUpdatingNodeIds(Array.from(ref))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const startedAt = pathUpdateStartTimeRef.current ?? 0
|
||||||
|
const elapsed = Date.now() - startedAt
|
||||||
|
const remaining = Math.max(0, CONNECTION_PATH_UPDATE_MIN_MS - elapsed)
|
||||||
|
if (remaining === 0) {
|
||||||
|
clearPathUpdateSession()
|
||||||
|
} else {
|
||||||
|
pathUpdateEndTimeoutRef.current = setTimeout(() => {
|
||||||
|
pathUpdateEndTimeoutRef.current = null
|
||||||
|
clearPathUpdateSession()
|
||||||
|
}, remaining)
|
||||||
|
}
|
||||||
|
}, [clearPathUpdateSession])
|
||||||
|
|
||||||
|
const addConnectionPathTrigger = useCallback((nodeId: string) => {
|
||||||
|
pathTriggerBatchRef.current.add(nodeId)
|
||||||
|
if (pathTriggerScheduledRef.current) return
|
||||||
|
pathTriggerScheduledRef.current = true
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
pathTriggerScheduledRef.current = false
|
||||||
|
const batch = new Set(pathTriggerBatchRef.current)
|
||||||
|
pathTriggerBatchRef.current = new Set()
|
||||||
|
if (batch.size === 0) return
|
||||||
|
setConnectionPathTriggerNodeIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
batch.forEach((id) => next.add(id))
|
||||||
|
return next.size === prev.length && prev.every((id) => next.has(id)) ? prev : Array.from(next)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (pathUpdateEndTimeoutRef.current != null) {
|
||||||
|
clearTimeout(pathUpdateEndTimeoutRef.current)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const connectionPathNodeIds = useMemo(
|
||||||
|
() =>
|
||||||
|
getPathNodeIds(
|
||||||
|
edges,
|
||||||
|
connectionPathUpdatingNodeIds,
|
||||||
|
connectionPathTriggerNodeIds,
|
||||||
|
connectionPathPausedNodeIds
|
||||||
|
),
|
||||||
|
[edges, connectionPathUpdatingNodeIds, connectionPathTriggerNodeIds, connectionPathPausedNodeIds]
|
||||||
|
)
|
||||||
|
|
||||||
|
const connectionPathPausedSegmentNodeIds = useMemo(
|
||||||
|
() =>
|
||||||
|
getPausedSegmentNodeIds(
|
||||||
|
edges,
|
||||||
|
connectionPathNodeIds,
|
||||||
|
connectionPathTriggerNodeIds,
|
||||||
|
connectionPathPausedNodeIds
|
||||||
|
),
|
||||||
|
[edges, connectionPathNodeIds, connectionPathTriggerNodeIds, connectionPathPausedNodeIds]
|
||||||
|
)
|
||||||
|
|
||||||
|
const connectionPathActiveSegmentNodeIds = useMemo(() => {
|
||||||
|
const active = new Set(connectionPathNodeIds)
|
||||||
|
connectionPathPausedSegmentNodeIds.forEach((id) => active.delete(id))
|
||||||
|
return active
|
||||||
|
}, [connectionPathNodeIds, connectionPathPausedSegmentNodeIds])
|
||||||
|
|
||||||
|
const addConnectionPathPausedNode = useCallback((nodeId: string) => {
|
||||||
|
setConnectionPathPausedNodeIds((prev) => (prev.includes(nodeId) ? prev : [...prev, nodeId]))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const removeConnectionPathPausedNode = useCallback((nodeId: string) => {
|
||||||
|
setConnectionPathPausedNodeIds((prev) => prev.filter((id) => id !== nodeId))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const addConnectionPathError = useCallback((nodeId: string) => {
|
||||||
|
setConnectionPathErrorNodeIds((prev) => (prev.includes(nodeId) ? prev : [...prev, nodeId]))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const removeConnectionPathError = useCallback((nodeId: string) => {
|
||||||
|
setConnectionPathErrorNodeIds((prev) => prev.filter((id) => id !== nodeId))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionPathUpdatingNodeIds,
|
||||||
|
connectionPathTriggerNodeIds,
|
||||||
|
connectionPathPausedNodeIds,
|
||||||
|
connectionPathErrorNodeIds,
|
||||||
|
connectionPathNodeIds,
|
||||||
|
connectionPathPausedSegmentNodeIds,
|
||||||
|
connectionPathActiveSegmentNodeIds,
|
||||||
|
startConnectionPathUpdate,
|
||||||
|
endConnectionPathUpdate,
|
||||||
|
addConnectionPathTrigger,
|
||||||
|
addConnectionPathPausedNode,
|
||||||
|
removeConnectionPathPausedNode,
|
||||||
|
addConnectionPathError,
|
||||||
|
removeConnectionPathError,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
processSvgDisplay,
|
processSvgDisplay,
|
||||||
stripTemplateSyntax,
|
stripTemplateSyntax,
|
||||||
} from '@/lib/graph/rendering'
|
} from '@/lib/graph/rendering'
|
||||||
import { isReachable, resolveExtendsRef, getTemplateRefs } from '@/lib/graph/templateRefs'
|
import { buildSourceSignatures } 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 = {
|
||||||
@@ -136,101 +136,25 @@ export function useRenderingNodeState(
|
|||||||
? agentOutputMarkdown
|
? agentOutputMarkdown
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
const connectedNodeIds = useMemo(() => {
|
const signatures = useMemo(
|
||||||
const edgeList = edges as { source: string; target: string }[]
|
|
||||||
const nodeList = nodes as { id: string; type?: string; data?: unknown }[]
|
|
||||||
const out = new Set<string>()
|
|
||||||
const addConfigRefs = (nodeId: string, visited: Set<string>) => {
|
|
||||||
if (visited.has(nodeId)) return
|
|
||||||
const node = nodeList.find((n) => n.id === nodeId && n.type === 'config')
|
|
||||||
if (!node) return
|
|
||||||
visited.add(nodeId)
|
|
||||||
out.add(nodeId)
|
|
||||||
const content = getConfigContent((node.data ?? undefined) as Record<string, unknown> | undefined)
|
|
||||||
for (const ref of getTemplateRefs(content)) {
|
|
||||||
const refId = resolveExtendsRef(nodeList, ref)
|
|
||||||
if (
|
|
||||||
refId &&
|
|
||||||
nodeList.some((n) => n.id === refId && n.type === 'config') &&
|
|
||||||
isReachable(edgeList, refId, id)
|
|
||||||
) {
|
|
||||||
addConfigRefs(refId, visited)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const configVisited = new Set<string>()
|
|
||||||
for (const nid of incomingIds) {
|
|
||||||
const node = nodeList.find((n) => n.id === nid)
|
|
||||||
if (node?.type === 'config') addConfigRefs(nid, configVisited)
|
|
||||||
else out.add(nid)
|
|
||||||
}
|
|
||||||
for (const e of edgeList) {
|
|
||||||
if (out.has(e.target)) out.add(e.source)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}, [nodes, edges, id, incomingIds])
|
|
||||||
|
|
||||||
const configSignature = useMemo(
|
|
||||||
() =>
|
() =>
|
||||||
(nodes as { id: string; type?: string; data?: unknown }[])
|
buildSourceSignatures(
|
||||||
.filter((n) => n.type === 'config' && connectedNodeIds.has(n.id))
|
nodes as { id: string; type?: string; data?: unknown }[],
|
||||||
.map((n) => `${n.id}:${(n.data as { title?: string })?.title ?? ''}:${getConfigContent(n.data as Record<string, unknown>)}`)
|
edges as { source: string; target: string }[],
|
||||||
.sort()
|
id,
|
||||||
.join('|'),
|
incomingIds
|
||||||
[nodes, connectedNodeIds]
|
),
|
||||||
|
[nodes, edges, id, incomingIds]
|
||||||
)
|
)
|
||||||
const edgesSignature = useMemo(
|
const {
|
||||||
() =>
|
connectedNodeIds,
|
||||||
(edges as { source: string; target: string }[])
|
|
||||||
.filter(
|
|
||||||
(e) =>
|
|
||||||
connectedNodeIds.has(e.source) &&
|
|
||||||
(connectedNodeIds.has(e.target) || e.target === id)
|
|
||||||
)
|
|
||||||
.map((e) => `${e.source}->${e.target}`)
|
|
||||||
.sort()
|
|
||||||
.join('|'),
|
|
||||||
[edges, connectedNodeIds, id]
|
|
||||||
)
|
|
||||||
const variablesSignature = useMemo(
|
|
||||||
() =>
|
|
||||||
(nodes as { id: string; type?: string; data?: { value?: unknown } }[])
|
|
||||||
.filter((n) => n.type === 'variable' && connectedNodeIds.has(n.id))
|
|
||||||
.map((n) => `${n.id}:${n.data?.value}`)
|
|
||||||
.sort()
|
|
||||||
.join('|'),
|
|
||||||
[nodes, connectedNodeIds]
|
|
||||||
)
|
|
||||||
const functionsSignature = useMemo(
|
|
||||||
() =>
|
|
||||||
(nodes as { id: string; type?: string; data?: { body?: string } }[])
|
|
||||||
.filter((n) => n.type === 'function' && connectedNodeIds.has(n.id))
|
|
||||||
.map((n) => `${n.id}:${n.data?.body ?? ''}`)
|
|
||||||
.sort()
|
|
||||||
.join('|'),
|
|
||||||
[nodes, connectedNodeIds]
|
|
||||||
)
|
|
||||||
const dataSignature = useMemo(
|
|
||||||
() =>
|
|
||||||
(nodes as { id: string; type?: string; data?: { rows?: unknown[]; hiddenColumns?: unknown[] } }[])
|
|
||||||
.filter((n) => n.type === 'data' && connectedNodeIds.has(n.id))
|
|
||||||
.map((n) => `${n.id}:${JSON.stringify(n.data?.rows ?? [])}:${JSON.stringify(n.data?.hiddenColumns ?? [])}`)
|
|
||||||
.sort()
|
|
||||||
.join('|'),
|
|
||||||
[nodes, connectedNodeIds]
|
|
||||||
)
|
|
||||||
|
|
||||||
const sourceSignature = useMemo(
|
|
||||||
() =>
|
|
||||||
JSON.stringify({
|
|
||||||
configSignature,
|
configSignature,
|
||||||
edgesSignature,
|
edgesSignature,
|
||||||
variablesSignature,
|
variablesSignature,
|
||||||
functionsSignature,
|
functionsSignature,
|
||||||
dataSignature,
|
dataSignature,
|
||||||
}),
|
sourceSignature,
|
||||||
[configSignature, edgesSignature, variablesSignature, functionsSignature, dataSignature]
|
} = signatures
|
||||||
)
|
|
||||||
|
|
||||||
const lastRunSourceSignature = data?.lastRunSourceSignature
|
const lastRunSourceSignature = data?.lastRunSourceSignature
|
||||||
|
|
||||||
|
|||||||
142
frontend/src/lib/graph/renderingSignatures.ts
Normal file
142
frontend/src/lib/graph/renderingSignatures.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Pure functions to build "connected node ids" and source signatures for the
|
||||||
|
* rendering pipeline. Used by useRenderingNodeState so the hook stays thin and
|
||||||
|
* this logic is testable in isolation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getConfigContent } from '@/lib/graph/rendering'
|
||||||
|
import { isReachable, resolveExtendsRef, getTemplateRefs } from '@/lib/graph/templateRefs'
|
||||||
|
|
||||||
|
export type NodeLike = { id: string; type?: string; data?: unknown }
|
||||||
|
export type EdgeLike = { source: string; target: string }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects all node ids that affect the render: direct incoming nodes plus
|
||||||
|
* config nodes reached via template refs (extends/include/import) that are
|
||||||
|
* reachable to the render node.
|
||||||
|
*/
|
||||||
|
export function buildConnectedNodeIds(
|
||||||
|
nodes: NodeLike[],
|
||||||
|
edges: EdgeLike[],
|
||||||
|
renderNodeId: string,
|
||||||
|
incomingIds: string[],
|
||||||
|
getContent: (data: Record<string, unknown> | undefined) => string = getConfigContent
|
||||||
|
): Set<string> {
|
||||||
|
const out = new Set<string>()
|
||||||
|
const addConfigRefs = (nodeId: string, visited: Set<string>) => {
|
||||||
|
if (visited.has(nodeId)) return
|
||||||
|
const node = nodes.find((n) => n.id === nodeId && n.type === 'config')
|
||||||
|
if (!node) return
|
||||||
|
visited.add(nodeId)
|
||||||
|
out.add(nodeId)
|
||||||
|
const content = getContent((node.data ?? undefined) as Record<string, unknown> | undefined)
|
||||||
|
for (const ref of getTemplateRefs(content)) {
|
||||||
|
const refId = resolveExtendsRef(nodes, ref)
|
||||||
|
if (
|
||||||
|
refId &&
|
||||||
|
nodes.some((n) => n.id === refId && n.type === 'config') &&
|
||||||
|
isReachable(edges, refId, renderNodeId)
|
||||||
|
) {
|
||||||
|
addConfigRefs(refId, visited)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const configVisited = new Set<string>()
|
||||||
|
for (const nid of incomingIds) {
|
||||||
|
const node = nodes.find((n) => n.id === nid)
|
||||||
|
if (node?.type === 'config') addConfigRefs(nid, configVisited)
|
||||||
|
else out.add(nid)
|
||||||
|
}
|
||||||
|
for (const e of edges) {
|
||||||
|
if (out.has(e.target)) out.add(e.source)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SourceSignatures = {
|
||||||
|
connectedNodeIds: Set<string>
|
||||||
|
configSignature: string
|
||||||
|
edgesSignature: string
|
||||||
|
variablesSignature: string
|
||||||
|
functionsSignature: string
|
||||||
|
dataSignature: string
|
||||||
|
sourceSignature: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds connected node set and all signatures used to decide when to re-run
|
||||||
|
* the resolve → render pipeline.
|
||||||
|
*/
|
||||||
|
export function buildSourceSignatures(
|
||||||
|
nodes: NodeLike[],
|
||||||
|
edges: EdgeLike[],
|
||||||
|
renderNodeId: string,
|
||||||
|
incomingIds: string[],
|
||||||
|
getContent: (data: Record<string, unknown> | undefined) => string = getConfigContent
|
||||||
|
): SourceSignatures {
|
||||||
|
const connectedNodeIds = buildConnectedNodeIds(
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
renderNodeId,
|
||||||
|
incomingIds,
|
||||||
|
getContent
|
||||||
|
)
|
||||||
|
|
||||||
|
const configSignature = nodes
|
||||||
|
.filter((n) => n.type === 'config' && connectedNodeIds.has(n.id))
|
||||||
|
.map(
|
||||||
|
(n) =>
|
||||||
|
`${n.id}:${(n.data as { title?: string })?.title ?? ''}:${getContent(n.data as Record<string, unknown>)}`
|
||||||
|
)
|
||||||
|
.sort()
|
||||||
|
.join('|')
|
||||||
|
|
||||||
|
const edgesSignature = edges
|
||||||
|
.filter(
|
||||||
|
(e) =>
|
||||||
|
connectedNodeIds.has(e.source) &&
|
||||||
|
(connectedNodeIds.has(e.target) || e.target === renderNodeId)
|
||||||
|
)
|
||||||
|
.map((e) => `${e.source}->${e.target}`)
|
||||||
|
.sort()
|
||||||
|
.join('|')
|
||||||
|
|
||||||
|
const variablesSignature = nodes
|
||||||
|
.filter((n) => n.type === 'variable' && connectedNodeIds.has(n.id))
|
||||||
|
.map((n) => `${n.id}:${(n.data as { value?: unknown })?.value}`)
|
||||||
|
.sort()
|
||||||
|
.join('|')
|
||||||
|
|
||||||
|
const functionsSignature = nodes
|
||||||
|
.filter((n) => n.type === 'function' && connectedNodeIds.has(n.id))
|
||||||
|
.map((n) => `${n.id}:${(n.data as { body?: string })?.body ?? ''}`)
|
||||||
|
.sort()
|
||||||
|
.join('|')
|
||||||
|
|
||||||
|
const dataSignature = nodes
|
||||||
|
.filter((n) => n.type === 'data' && connectedNodeIds.has(n.id))
|
||||||
|
.map(
|
||||||
|
(n) =>
|
||||||
|
`${n.id}:${JSON.stringify((n.data as { rows?: unknown[] })?.rows ?? [])}:${JSON.stringify((n.data as { hiddenColumns?: unknown[] })?.hiddenColumns ?? [])}`
|
||||||
|
)
|
||||||
|
.sort()
|
||||||
|
.join('|')
|
||||||
|
|
||||||
|
const sourceSignature = JSON.stringify({
|
||||||
|
configSignature,
|
||||||
|
edgesSignature,
|
||||||
|
variablesSignature,
|
||||||
|
functionsSignature,
|
||||||
|
dataSignature,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectedNodeIds,
|
||||||
|
configSignature,
|
||||||
|
edgesSignature,
|
||||||
|
variablesSignature,
|
||||||
|
functionsSignature,
|
||||||
|
dataSignature,
|
||||||
|
sourceSignature,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user