feat: enhance connection validation and node creation logic, improve type handling in various components
This commit is contained in:
@@ -27,7 +27,7 @@ export function AnimatedEdge({
|
||||
const nodes = ctx?.nodes ?? []
|
||||
const targetNode = useMemo(() => nodes.find((n: any) => n.id === target), [nodes, target])
|
||||
const derivedLabel = useMemo(
|
||||
() => getConnectionLabelForTarget(targetNode?.type),
|
||||
() => (targetNode?.type != null ? getConnectionLabelForTarget(targetNode.type) : undefined),
|
||||
[targetNode?.type]
|
||||
)
|
||||
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()
|
||||
if (!raw || typeof raw.type !== 'string' || !validIds.includes(raw.type)) return
|
||||
const nodeType = raw.type
|
||||
const pane = document.querySelector('.react-flow__viewport')
|
||||
const rect = pane?.getBoundingClientRect()
|
||||
const center = rect
|
||||
@@ -41,13 +42,14 @@ export function FlowKeyboardShortcuts() {
|
||||
: { x: window.innerWidth / 2, y: window.innerHeight / 2 }
|
||||
const position = screenToFlowPosition(center)
|
||||
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> =
|
||||
raw.data != null && typeof raw.data === 'object'
|
||||
? { ...raw.data }
|
||||
: (getDefaultDataForType(raw.type, newId) as Record<string, unknown>)
|
||||
if (raw.type === 'config') data.title = `config-${newId}`
|
||||
const style = getDefaultStyle(raw.type)
|
||||
: (getDefaultDataForType(nodeType, newId) as Record<string, unknown>)
|
||||
if (nodeType === 'config') data.title = `config-${newId}`
|
||||
const style = getDefaultStyle(nodeType)
|
||||
const newNode: Node = {
|
||||
id: newId,
|
||||
type: raw.type as Node['type'],
|
||||
|
||||
@@ -19,7 +19,7 @@ export function InputHandle({ id, nodeId }: NodeHandleProps) {
|
||||
isConnecting &&
|
||||
isValidConnection?.({
|
||||
source: connectionFrom!.nodeId,
|
||||
sourceHandle: connectionFrom!.sourceHandle ?? undefined,
|
||||
sourceHandle: connectionFrom!.sourceHandle ?? null,
|
||||
target: nodeId!,
|
||||
targetHandle: id,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||
import FlowContext from '../../lib/flowContext'
|
||||
import type { AppNode } from '../../lib/nodeTypes'
|
||||
import { replaceNodeIdInGraph } from '../../lib/flowUtils'
|
||||
|
||||
type Props = {
|
||||
@@ -41,7 +42,7 @@ export function NodeHeaderTitle({ nodeId, displayTitle }: Props) {
|
||||
return
|
||||
}
|
||||
const { nodes: nextNodes, edges: nextEdges } = replaceNodeIdInGraph(nodes, edges, nodeId, newId)
|
||||
setNodes(nextNodes)
|
||||
setNodes(nextNodes as AppNode[])
|
||||
setEdges(nextEdges)
|
||||
setRenamingNodeId(null)
|
||||
}, [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,
|
||||
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)
|
||||
})
|
||||
}, [node, nodeType, setNodes])
|
||||
|
||||
@@ -43,7 +43,7 @@ import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
|
||||
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
|
||||
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>
|
||||
|
||||
@@ -324,7 +324,7 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
/>
|
||||
</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
|
||||
// @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
|
||||
ref={editorRef}
|
||||
|
||||
@@ -131,7 +131,7 @@ function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
}
|
||||
/>
|
||||
</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
|
||||
// @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
|
||||
ref={editorRef}
|
||||
|
||||
@@ -51,8 +51,8 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const incomingIds = sourceIds
|
||||
const srcId = incomingIds.length > 0 ? incomingIds[0] : null
|
||||
const srcNode = nodes.find((n: any) => n.id === srcId)
|
||||
const configTypeId = srcNode?.type === 'config' ? getConfigTypeId(srcNode.data) : 'plantuml'
|
||||
const sourceContent = srcNode?.type === 'config' ? getConfigContent(srcNode.data) : ''
|
||||
const configTypeId = srcNode?.type === 'config' ? getConfigTypeId((srcNode.data ?? undefined) as Record<string, unknown> | undefined) : 'plantuml'
|
||||
const sourceContent = srcNode?.type === 'config' ? getConfigContent((srcNode.data ?? undefined) as Record<string, unknown> | undefined) : ''
|
||||
const srcData = srcNode?.data ?? {}
|
||||
|
||||
/** 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
|
||||
visited.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)) {
|
||||
const refId = resolveRef(ref)
|
||||
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}`)
|
||||
visited.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)
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
if (refId !== srcId && !isReachable(refId, id))
|
||||
throw new Error(`Referenced config not connected to renderer: ${name}`)
|
||||
return {
|
||||
src: getConfigContent(node.data),
|
||||
src: getConfigContent((node.data ?? undefined) as Record<string, unknown> | undefined),
|
||||
path: name,
|
||||
}
|
||||
},
|
||||
@@ -338,7 +338,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
for (const fid of functionIdsToRegister) {
|
||||
const src = nodes.find((n: any) => n.id === fid)
|
||||
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 connectedVarIds = new Set<string>(functionConnectedVariableIds[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) => {
|
||||
if (cancelled || thisRunId !== runIdRef.current) return
|
||||
if (nunjucksErr) {
|
||||
setSvgContent(null)
|
||||
setRenderedContent(null)
|
||||
setError({ kind: 'render', message: `Nunjucks: ${nunjucksErr.message}` })
|
||||
setLoading(false)
|
||||
return
|
||||
@@ -677,10 +677,10 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
) : error ? (
|
||||
srcData?.renderError ? (
|
||||
srcData.renderError(error)
|
||||
) : srcData?.errorHtml ? (
|
||||
<div className="p-3 text-xs text-red-700 dark:text-red-400" dangerouslySetInnerHTML={{ __html: String(srcData.errorHtml) }} />
|
||||
(srcData as { renderError?: (err: { kind: string; message: string }) => React.ReactNode; errorHtml?: string })?.renderError ? (
|
||||
(srcData as { renderError: (err: { kind: string; message: string }) => React.ReactNode }).renderError(error)
|
||||
) : (srcData as { errorHtml?: string })?.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">
|
||||
<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}
|
||||
maxScale={4}
|
||||
centerOnInit
|
||||
onInit={(ref) => ref?.centerView(1, 0, 0)}
|
||||
onInit={(ref) => ref?.centerView(1, 200, 'easeOut')}
|
||||
panning={{ disabled: true }}
|
||||
wheel={{ disabled: true }}
|
||||
doubleClick={{ disabled: true }}
|
||||
|
||||
Reference in New Issue
Block a user