feat: enhance connection validation and node creation logic, improve type handling in various components
This commit is contained in:
@@ -217,9 +217,12 @@ export function CanvasPage({ projectId: _projectId }: CanvasPageProps) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const isValidConnection = useCallback(
|
const isValidConnection = useCallback(
|
||||||
(connection: Connection) => {
|
(connection: Connection | AppEdge) => {
|
||||||
const sourceNode = nodes.find((n) => n.id === connection.source)
|
const src = 'source' in connection ? connection.source : undefined
|
||||||
const targetNode = nodes.find((n) => n.id === connection.target)
|
const tgt = 'target' in connection ? connection.target : undefined
|
||||||
|
if (typeof src !== 'string' || typeof tgt !== 'string') return false
|
||||||
|
const sourceNode = nodes.find((n) => n.id === src)
|
||||||
|
const targetNode = nodes.find((n) => n.id === tgt)
|
||||||
const sourceType = sourceNode?.type
|
const sourceType = sourceNode?.type
|
||||||
const targetType = targetNode?.type
|
const targetType = targetNode?.type
|
||||||
if (!sourceType || !targetType) return false
|
if (!sourceType || !targetType) return false
|
||||||
@@ -227,13 +230,16 @@ export function CanvasPage({ projectId: _projectId }: CanvasPageProps) {
|
|||||||
const targetData = targetNode?.data as { configType?: string } | undefined
|
const targetData = targetNode?.data as { configType?: string } | undefined
|
||||||
if (targetData?.configType !== 'markdown') return false
|
if (targetData?.configType !== 'markdown') return false
|
||||||
}
|
}
|
||||||
return isConnectionAllowed(sourceType, targetType, connection.source, connection.target)
|
return isConnectionAllowed(sourceType, targetType, src, tgt)
|
||||||
},
|
},
|
||||||
[nodes]
|
[nodes]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onConnectStart = useCallback(
|
const onConnectStart = useCallback(
|
||||||
(_: React.MouseEvent | React.TouchEvent, params: { nodeId?: string | null; handleId?: string | null; handleType?: string | null }) => {
|
(
|
||||||
|
_: React.MouseEvent<Element> | React.TouchEvent<Element> | MouseEvent | TouchEvent,
|
||||||
|
params: { nodeId?: string | null; handleId?: string | null; handleType?: string | null }
|
||||||
|
) => {
|
||||||
if (params.handleType !== 'source' || !params.nodeId) {
|
if (params.handleType !== 'source' || !params.nodeId) {
|
||||||
setConnectionFrom(null)
|
setConnectionFrom(null)
|
||||||
return
|
return
|
||||||
@@ -371,14 +377,14 @@ export function CanvasPage({ projectId: _projectId }: CanvasPageProps) {
|
|||||||
const createNode = useCallback(
|
const createNode = useCallback(
|
||||||
(type: string) => {
|
(type: string) => {
|
||||||
const position = getMenuPosition()
|
const position = getMenuPosition()
|
||||||
if (position == null) return
|
if (position == null || typeof type !== 'string') return
|
||||||
const nodeType = type as Node['type']
|
const existingIds = nodesRef.current.map((n) => n.id).filter((id): id is string => id != null)
|
||||||
const newId = getNextNodeId(nodeType, nodesRef.current.map((n) => n.id))
|
const newId = getNextNodeId(type, existingIds)
|
||||||
const dataMap = getDefaultDataForType(nodeType, newId)
|
const dataMap = getDefaultDataForType(type, newId)
|
||||||
const style = getDefaultStyle(nodeType)
|
const style = getDefaultStyle(type)
|
||||||
const newNode: Node = {
|
const newNode: Node = {
|
||||||
id: newId,
|
id: newId,
|
||||||
type: nodeType,
|
type: type as Node['type'],
|
||||||
position: { x: position.x, y: position.y },
|
position: { x: position.x, y: position.y },
|
||||||
data: dataMap,
|
data: dataMap,
|
||||||
style,
|
style,
|
||||||
@@ -416,14 +422,16 @@ export function CanvasPage({ projectId: _projectId }: CanvasPageProps) {
|
|||||||
const raw = JSON.parse(text) as { id?: string; type?: string; data?: Record<string, unknown>; position?: { x: number; y: number }; style?: unknown }
|
const raw = JSON.parse(text) as { id?: string; type?: string; data?: Record<string, unknown>; position?: { x: number; y: number }; style?: unknown }
|
||||||
const validIds = getRegisteredNodeTypeIds()
|
const validIds = getRegisteredNodeTypeIds()
|
||||||
if (!raw || typeof raw.type !== 'string' || !validIds.includes(raw.type)) return
|
if (!raw || typeof raw.type !== 'string' || !validIds.includes(raw.type)) return
|
||||||
|
const nodeType = raw.type
|
||||||
setNodes((nds) => {
|
setNodes((nds) => {
|
||||||
const newId = getNextNodeId(raw.type as Node['type'], nds.map((n) => n.id))
|
const existingIds = nds.map((n) => n.id).filter((id): id is string => id != null)
|
||||||
|
const newId = getNextNodeId(nodeType, existingIds)
|
||||||
const data = raw.data != null && typeof raw.data === 'object' ? { ...raw.data } : {}
|
const data = raw.data != null && typeof raw.data === 'object' ? { ...raw.data } : {}
|
||||||
if (raw.type === 'config' && data && 'title' in data) data.title = `config-${newId}`
|
if (nodeType === 'config' && data && 'title' in data) data.title = `config-${newId}`
|
||||||
const style = getDefaultStyle(raw.type)
|
const style = getDefaultStyle(nodeType)
|
||||||
const newNode: Node = {
|
const newNode: Node = {
|
||||||
id: newId,
|
id: newId,
|
||||||
type: raw.type as Node['type'],
|
type: nodeType as Node['type'],
|
||||||
position: { x: position.x, y: position.y },
|
position: { x: position.x, y: position.y },
|
||||||
data,
|
data,
|
||||||
style,
|
style,
|
||||||
@@ -560,7 +568,8 @@ export function CanvasPage({ projectId: _projectId }: CanvasPageProps) {
|
|||||||
nodesConnectable
|
nodesConnectable
|
||||||
elementsSelectable
|
elementsSelectable
|
||||||
>
|
>
|
||||||
<Background variant="dots" gap={20} />
|
{/* BackgroundVariant from @xyflow/system expects enum; 'dots' is valid at runtime */}
|
||||||
|
<Background variant={'dots' as React.ComponentProps<typeof Background>['variant']} gap={20} />
|
||||||
<div role="group" aria-label="Canvas controls: zoom and fit view">
|
<div role="group" aria-label="Canvas controls: zoom and fit view">
|
||||||
<Controls />
|
<Controls />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function AnimatedEdge({
|
|||||||
const nodes = ctx?.nodes ?? []
|
const nodes = ctx?.nodes ?? []
|
||||||
const targetNode = useMemo(() => nodes.find((n: any) => n.id === target), [nodes, target])
|
const targetNode = useMemo(() => nodes.find((n: any) => n.id === target), [nodes, target])
|
||||||
const derivedLabel = useMemo(
|
const derivedLabel = useMemo(
|
||||||
() => getConnectionLabelForTarget(targetNode?.type),
|
() => (targetNode?.type != null ? getConnectionLabelForTarget(targetNode.type) : undefined),
|
||||||
[targetNode?.type]
|
[targetNode?.type]
|
||||||
)
|
)
|
||||||
const label = labelProp ?? derivedLabel
|
const label = labelProp ?? derivedLabel
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { ComponentProps } from "react";
|
|
||||||
import { Handle, type HandleProps } from "@xyflow/react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type BaseHandleProps = HandleProps;
|
|
||||||
|
|
||||||
export function BaseHandle({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComponentProps<typeof Handle>) {
|
|
||||||
return (
|
|
||||||
<Handle
|
|
||||||
{...props}
|
|
||||||
className={cn(
|
|
||||||
"dark:border-secondary dark:bg-secondary h-[11px] w-[11px] rounded-full border border-slate-300 bg-slate-100 transition",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Handle>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -34,6 +34,7 @@ export function FlowKeyboardShortcuts() {
|
|||||||
}
|
}
|
||||||
const validIds = getRegisteredNodeTypeIds()
|
const validIds = getRegisteredNodeTypeIds()
|
||||||
if (!raw || typeof raw.type !== 'string' || !validIds.includes(raw.type)) return
|
if (!raw || typeof raw.type !== 'string' || !validIds.includes(raw.type)) return
|
||||||
|
const nodeType = raw.type
|
||||||
const pane = document.querySelector('.react-flow__viewport')
|
const pane = document.querySelector('.react-flow__viewport')
|
||||||
const rect = pane?.getBoundingClientRect()
|
const rect = pane?.getBoundingClientRect()
|
||||||
const center = rect
|
const center = rect
|
||||||
@@ -41,13 +42,14 @@ export function FlowKeyboardShortcuts() {
|
|||||||
: { x: window.innerWidth / 2, y: window.innerHeight / 2 }
|
: { x: window.innerWidth / 2, y: window.innerHeight / 2 }
|
||||||
const position = screenToFlowPosition(center)
|
const position = screenToFlowPosition(center)
|
||||||
setNodes((nds: Node[]) => {
|
setNodes((nds: Node[]) => {
|
||||||
const newId = getNextNodeId(raw.type, nds.map((n) => n.id))
|
const existingIds = nds.map((n) => n.id).filter((id): id is string => id != null)
|
||||||
|
const newId = getNextNodeId(nodeType, existingIds)
|
||||||
const data: Record<string, unknown> =
|
const data: Record<string, unknown> =
|
||||||
raw.data != null && typeof raw.data === 'object'
|
raw.data != null && typeof raw.data === 'object'
|
||||||
? { ...raw.data }
|
? { ...raw.data }
|
||||||
: (getDefaultDataForType(raw.type, newId) as Record<string, unknown>)
|
: (getDefaultDataForType(nodeType, newId) as Record<string, unknown>)
|
||||||
if (raw.type === 'config') data.title = `config-${newId}`
|
if (nodeType === 'config') data.title = `config-${newId}`
|
||||||
const style = getDefaultStyle(raw.type)
|
const style = getDefaultStyle(nodeType)
|
||||||
const newNode: Node = {
|
const newNode: Node = {
|
||||||
id: newId,
|
id: newId,
|
||||||
type: raw.type as Node['type'],
|
type: raw.type as Node['type'],
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function InputHandle({ id, nodeId }: NodeHandleProps) {
|
|||||||
isConnecting &&
|
isConnecting &&
|
||||||
isValidConnection?.({
|
isValidConnection?.({
|
||||||
source: connectionFrom!.nodeId,
|
source: connectionFrom!.nodeId,
|
||||||
sourceHandle: connectionFrom!.sourceHandle ?? undefined,
|
sourceHandle: connectionFrom!.sourceHandle ?? null,
|
||||||
target: nodeId!,
|
target: nodeId!,
|
||||||
targetHandle: id,
|
targetHandle: id,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||||
import FlowContext from '../../lib/flowContext'
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import type { AppNode } from '../../lib/nodeTypes'
|
||||||
import { replaceNodeIdInGraph } from '../../lib/flowUtils'
|
import { replaceNodeIdInGraph } from '../../lib/flowUtils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -41,7 +42,7 @@ export function NodeHeaderTitle({ nodeId, displayTitle }: Props) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { nodes: nextNodes, edges: nextEdges } = replaceNodeIdInGraph(nodes, edges, nodeId, newId)
|
const { nodes: nextNodes, edges: nextEdges } = replaceNodeIdInGraph(nodes, edges, nodeId, newId)
|
||||||
setNodes(nextNodes)
|
setNodes(nextNodes as AppNode[])
|
||||||
setEdges(nextEdges)
|
setEdges(nextEdges)
|
||||||
setRenamingNodeId(null)
|
setRenamingNodeId(null)
|
||||||
}, [nodeId, inputValue, nodes, edges, setNodes, setEdges, setRenamingNodeId])
|
}, [nodeId, inputValue, nodes, edges, setNodes, setEdges, setRenamingNodeId])
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent, inputsMenuCon
|
|||||||
data: typeof node.data === 'object' && node.data !== null ? { ...node.data } : node.data,
|
data: typeof node.data === 'object' && node.data !== null ? { ...node.data } : node.data,
|
||||||
style: getDefaultStyle(nodeType),
|
style: getDefaultStyle(nodeType),
|
||||||
}
|
}
|
||||||
if (newNode.data?.title && nodeType === 'config') newNode.data.title = `${newId}`
|
if (nodeType === 'config' && newNode.data && typeof newNode.data === 'object' && 'title' in newNode.data) {
|
||||||
|
(newNode.data as { title: string }).title = `${newId}`
|
||||||
|
}
|
||||||
return nds.concat(newNode)
|
return nds.concat(newNode)
|
||||||
})
|
})
|
||||||
}, [node, nodeType, setNodes])
|
}, [node, nodeType, setNodes])
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
|
|||||||
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
|
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
|
||||||
import { NodeMenubar } from '../base/NodeMenubar'
|
import { NodeMenubar } from '../base/NodeMenubar'
|
||||||
|
|
||||||
export type ConfigNodeData = { configType?: ConfigTypeId; content?: string; title?: string }
|
export type ConfigNodeData = { configType?: ConfigTypeId; content?: string; title?: string; /** @deprecated use content */ plantuml?: string }
|
||||||
|
|
||||||
type Props = AbstractNodeProps<ConfigNodeData>
|
type Props = AbstractNodeProps<ConfigNodeData>
|
||||||
|
|
||||||
@@ -324,7 +324,7 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref={editorContainerRef} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input">
|
<div ref={editorContainerRef as React.RefObject<HTMLDivElement>} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
// @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
|
// @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div ref={editorContainerRef} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input">
|
<div ref={editorContainerRef as React.RefObject<HTMLDivElement>} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
// @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
|
// @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
const incomingIds = sourceIds
|
const incomingIds = sourceIds
|
||||||
const srcId = incomingIds.length > 0 ? incomingIds[0] : null
|
const srcId = incomingIds.length > 0 ? incomingIds[0] : null
|
||||||
const srcNode = nodes.find((n: any) => n.id === srcId)
|
const srcNode = nodes.find((n: any) => n.id === srcId)
|
||||||
const configTypeId = srcNode?.type === 'config' ? getConfigTypeId(srcNode.data) : 'plantuml'
|
const configTypeId = srcNode?.type === 'config' ? getConfigTypeId((srcNode.data ?? undefined) as Record<string, unknown> | undefined) : 'plantuml'
|
||||||
const sourceContent = srcNode?.type === 'config' ? getConfigContent(srcNode.data) : ''
|
const sourceContent = srcNode?.type === 'config' ? getConfigContent((srcNode.data ?? undefined) as Record<string, unknown> | undefined) : ''
|
||||||
const srcData = srcNode?.data ?? {}
|
const srcData = srcNode?.data ?? {}
|
||||||
|
|
||||||
/** Set of node IDs that can affect this render node (configs in the chain + variables/functions feeding them) */
|
/** Set of node IDs that can affect this render node (configs in the chain + variables/functions feeding them) */
|
||||||
@@ -94,7 +94,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
if (!node) return
|
if (!node) return
|
||||||
visited.add(nodeId)
|
visited.add(nodeId)
|
||||||
out.add(nodeId)
|
out.add(nodeId)
|
||||||
const content = getConfigContent(node.data)
|
const content = getConfigContent((node.data ?? undefined) as Record<string, unknown> | undefined)
|
||||||
for (const ref of getTemplateRefs(content)) {
|
for (const ref of getTemplateRefs(content)) {
|
||||||
const refId = resolveRef(ref)
|
const refId = resolveRef(ref)
|
||||||
if (refId && nodes.some((n: any) => n.id === refId && n.type === 'config') && isReachable(refId, id))
|
if (refId && nodes.some((n: any) => n.id === refId && n.type === 'config') && isReachable(refId, id))
|
||||||
@@ -223,7 +223,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
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)
|
||||||
const content = getConfigContent(node.data)
|
const content = getConfigContent((node.data ?? undefined) as Record<string, unknown> | undefined)
|
||||||
for (const ref of getTemplateRefs(content)) addConfigAndRefs(ref, visited)
|
for (const ref of getTemplateRefs(content)) addConfigAndRefs(ref, visited)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
if (refId !== srcId && !isReachable(refId, id))
|
if (refId !== srcId && !isReachable(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),
|
src: getConfigContent((node.data ?? undefined) as Record<string, unknown> | undefined),
|
||||||
path: name,
|
path: name,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -338,7 +338,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
for (const fid of functionIdsToRegister) {
|
for (const fid of functionIdsToRegister) {
|
||||||
const src = nodes.find((n: any) => n.id === fid)
|
const src = nodes.find((n: any) => n.id === fid)
|
||||||
if (!src || src.type !== 'function') continue
|
if (!src || src.type !== 'function') continue
|
||||||
const body = src.data?.body ?? 'return args[0];'
|
const body = (src.data as { body?: string } | undefined)?.body ?? 'return args[0];'
|
||||||
const parsed = parseFunctionSignature(body)
|
const parsed = parseFunctionSignature(body)
|
||||||
const connectedVarIds = new Set<string>(functionConnectedVariableIds[fid] ?? [])
|
const connectedVarIds = new Set<string>(functionConnectedVariableIds[fid] ?? [])
|
||||||
const connectedFuncIds = functionConnectedFunctionIds[fid] ?? []
|
const connectedFuncIds = functionConnectedFunctionIds[fid] ?? []
|
||||||
@@ -432,7 +432,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
env.render(srcId!, nunjucksContext, async (nunjucksErr: Error | null, afterNunjucks: string) => {
|
env.render(srcId!, nunjucksContext, async (nunjucksErr: Error | null, afterNunjucks: string) => {
|
||||||
if (cancelled || thisRunId !== runIdRef.current) return
|
if (cancelled || thisRunId !== runIdRef.current) return
|
||||||
if (nunjucksErr) {
|
if (nunjucksErr) {
|
||||||
setSvgContent(null)
|
setRenderedContent(null)
|
||||||
setError({ kind: 'render', message: `Nunjucks: ${nunjucksErr.message}` })
|
setError({ kind: 'render', message: `Nunjucks: ${nunjucksErr.message}` })
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
@@ -677,10 +677,10 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
</EmptyContent>
|
</EmptyContent>
|
||||||
</Empty>
|
</Empty>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
srcData?.renderError ? (
|
(srcData as { renderError?: (err: { kind: string; message: string }) => React.ReactNode; errorHtml?: string })?.renderError ? (
|
||||||
srcData.renderError(error)
|
(srcData as { renderError: (err: { kind: string; message: string }) => React.ReactNode }).renderError(error)
|
||||||
) : srcData?.errorHtml ? (
|
) : (srcData as { errorHtml?: string })?.errorHtml ? (
|
||||||
<div className="p-3 text-xs text-red-700 dark:text-red-400" dangerouslySetInnerHTML={{ __html: String(srcData.errorHtml) }} />
|
<div className="p-3 text-xs text-red-700 dark:text-red-400" dangerouslySetInnerHTML={{ __html: String((srcData as { errorHtml: string }).errorHtml) }} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2 p-3">
|
<div className="flex flex-col gap-2 p-3">
|
||||||
<p className="text-xs text-red-700 dark:text-red-400">{error.message}</p>
|
<p className="text-xs text-red-700 dark:text-red-400">{error.message}</p>
|
||||||
@@ -706,7 +706,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
minScale={0.2}
|
minScale={0.2}
|
||||||
maxScale={4}
|
maxScale={4}
|
||||||
centerOnInit
|
centerOnInit
|
||||||
onInit={(ref) => ref?.centerView(1, 0, 0)}
|
onInit={(ref) => ref?.centerView(1, 200, 'easeOut')}
|
||||||
panning={{ disabled: true }}
|
panning={{ disabled: true }}
|
||||||
wheel={{ disabled: true }}
|
wheel={{ disabled: true }}
|
||||||
doubleClick={{ disabled: true }}
|
doubleClick={{ disabled: true }}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import React, { useCallback, useContext, useMemo } from 'react'
|
import React, { useCallback, useContext, useMemo } from 'react'
|
||||||
import FlowContext from './flowContext'
|
import FlowContext from './flowContext'
|
||||||
import { nodePropsAreEqual } from './flowUtils'
|
import { nodePropsAreEqual } from './flowUtils'
|
||||||
|
import type { AppNode } from './nodeTypes'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
@@ -74,10 +75,10 @@ export function useAbstractNode<TData = Record<string, unknown>>(
|
|||||||
const updateData = useCallback(
|
const updateData = useCallback(
|
||||||
(partial: Partial<TData>) => {
|
(partial: Partial<TData>) => {
|
||||||
if (!setNodes) return
|
if (!setNodes) return
|
||||||
setNodes((nds: FlowNode[]) =>
|
setNodes((nds: AppNode[]) =>
|
||||||
nds.map((n) =>
|
nds.map((n) =>
|
||||||
n.id === id ? { ...n, data: { ...(n.data as object), ...partial } } : n
|
n.id === id ? { ...n, data: { ...(n.data as object), ...partial } } : n
|
||||||
)
|
) as AppNode[]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[id, setNodes]
|
[id, setNodes]
|
||||||
@@ -105,7 +106,7 @@ export function useAbstractNode<TData = Record<string, unknown>>(
|
|||||||
data,
|
data,
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
setNodes: setNodes ?? (() => {}),
|
setNodes: ((setNodes ?? (() => {})) as AbstractNodeContext<TData>['setNodes']),
|
||||||
setEdges: setEdges ?? (() => {}),
|
setEdges: setEdges ?? (() => {}),
|
||||||
updateData,
|
updateData,
|
||||||
incomingEdges,
|
incomingEdges,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
getIdPrefix,
|
getIdPrefix,
|
||||||
getDefaultDataForType as getDefaultDataFromRegistry,
|
getDefaultDataForType as getDefaultDataFromRegistry,
|
||||||
getResetDataForType as getResetDataFromRegistry,
|
getResetDataForType as getResetDataFromRegistry,
|
||||||
getDefaultStyle,
|
|
||||||
} from './nodeRegistry'
|
} from './nodeRegistry'
|
||||||
|
|
||||||
export function nodePropsAreEqual<P extends { id?: string; data?: any; width?: number; height?: number; selected?: boolean }>(
|
export function nodePropsAreEqual<P extends { id?: string; data?: any; width?: number; height?: number; selected?: boolean }>(
|
||||||
@@ -24,14 +23,6 @@ export function nodePropsAreEqual<P extends { id?: string; data?: any; width?: n
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use getIdPrefix from nodeRegistry for new code. Kept for compatibility. */
|
|
||||||
export const PREFIX_BY_TYPE: Record<string, string> = {
|
|
||||||
config: 'cfg_',
|
|
||||||
render: 'rnd_',
|
|
||||||
variable: 'var_',
|
|
||||||
function: 'fn_',
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Next node id for type: prefix + 3-digit increasing number (001, 002, …). Uses nodeRegistry for prefix when available. */
|
/** Next node id for type: prefix + 3-digit increasing number (001, 002, …). Uses nodeRegistry for prefix when available. */
|
||||||
export function getNextNodeId(type: string, existingIds: string[]): string {
|
export function getNextNodeId(type: string, existingIds: string[]): string {
|
||||||
const prefix = getIdPrefix(type)
|
const prefix = getIdPrefix(type)
|
||||||
@@ -91,7 +82,3 @@ export function getResetDataForType(type: string, nodeId?: string): any {
|
|||||||
return getResetDataFromRegistry(type, nodeId)
|
return getResetDataFromRegistry(type, nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Default style for a type. Uses nodeRegistry when type is registered. */
|
|
||||||
export function getDefaultStyleForType(type: string): { width: number; height: number } {
|
|
||||||
return getDefaultStyle(type)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { StreamLanguage } from '@codemirror/language'
|
import { StreamLanguage } from '@codemirror/language'
|
||||||
|
|
||||||
/** Nunjucks block comment {# ... #} */
|
/** Nunjucks block comment {# ... #} */
|
||||||
function tokenNunjucksComment(stream: { match: (re: RegExp) => string | null; next: () => string; eol: () => boolean }) {
|
function tokenNunjucksComment(stream: { match: (re: RegExp) => unknown; next: () => string | void; eol: () => boolean }) {
|
||||||
if (stream.match(/^\{#/)) {
|
if (stream.match(/^\{#/)) {
|
||||||
while (!stream.eol()) {
|
while (!stream.eol()) {
|
||||||
if (stream.match(/#\}/)) return 'comment'
|
if (stream.match(/#\}/)) return 'comment'
|
||||||
@@ -13,7 +13,7 @@ function tokenNunjucksComment(stream: { match: (re: RegExp) => string | null; ne
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Nunjucks variable {{ ... }} or tag {% ... %} - tokenize the whole block */
|
/** Nunjucks variable {{ ... }} or tag {% ... %} - tokenize the whole block */
|
||||||
function tokenNunjucksBlock(stream: { match: (re: RegExp) => string | null; next: () => string; eol: () => boolean }) {
|
function tokenNunjucksBlock(stream: { match: (re: RegExp) => unknown; next: () => string | void; eol: () => boolean }) {
|
||||||
if (stream.match(/^\{\{/)) {
|
if (stream.match(/^\{\{/)) {
|
||||||
while (!stream.eol()) {
|
while (!stream.eol()) {
|
||||||
if (stream.match(/\}\}/)) return 'variableName.special'
|
if (stream.match(/\}\}/)) return 'variableName.special'
|
||||||
|
|||||||
18
frontend/src/nunjucks.d.ts
vendored
Normal file
18
frontend/src/nunjucks.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
declare module 'nunjucks' {
|
||||||
|
export interface Loader {
|
||||||
|
getSource(name: string): { src: string; path: string } | null
|
||||||
|
}
|
||||||
|
export interface Environment {
|
||||||
|
render(name: string, context: Record<string, unknown>, callback: (err: Error | null, res: string) => void): void
|
||||||
|
addFilter(name: string, fn: (...args: unknown[]) => void, async?: boolean): void
|
||||||
|
getFilter(name: string): (...args: unknown[]) => void
|
||||||
|
}
|
||||||
|
export class Environment {
|
||||||
|
constructor(loaders?: Loader[], opts?: { autoescape?: boolean })
|
||||||
|
render(name: string, context: Record<string, unknown>, callback: (err: Error | null, res: string) => void): void
|
||||||
|
addFilter(name: string, fn: (...args: unknown[]) => void, async?: boolean): void
|
||||||
|
getFilter(name: string): (...args: unknown[]) => void
|
||||||
|
}
|
||||||
|
const nunjucks: { Environment: typeof Environment }
|
||||||
|
export default nunjucks
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user