refactor node

This commit is contained in:
2026-03-09 14:11:33 +01:00
parent 24efc597b2
commit 9565a425b6
5 changed files with 230 additions and 134 deletions

View File

@@ -1,10 +1,14 @@
import React, { memo, useCallback, useContext, useMemo, useRef } from 'react'
import React, { useCallback, useMemo, useRef } from 'react'
import { autocompletion } from '@codemirror/autocomplete'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { markdown } from '@codemirror/lang-markdown'
import FlowContext from '../../lib/flowContext'
import { nodePropsAreEqual } from '../../lib/flowUtils'
import {
AbstractNodeProps,
createAbstractNodeComponent,
useAbstractNode,
type FlowNode,
} from '../../lib/abstractNode'
import { useResizeHeight } from '../../hooks/useResizeHeight'
import { nunjucksCompletionSource } from '../../lib/nunjucksAutocomplete'
import { plantumlLanguage } from '../../lib/plantumlLanguage'
@@ -39,73 +43,46 @@ import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
import { NodeMenubar } from '../base/NodeMenubar'
type Props = {
id: string
data: any
width?: number
height?: number
}
export type ConfigNodeData = { configType?: ConfigTypeId; content?: string; title?: string }
export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: Props) {
const configTypeId = getConfigTypeId(data)
type Props = AbstractNodeProps<ConfigNodeData>
function ConfigNodeComponent({ id, data, width, height }: Props) {
const configTypeId = getConfigTypeId(data ?? {})
const configType = getConfigType(configTypeId)
const content = getConfigContent(data)
const content = getConfigContent(data ?? {})
const { theme } = useTheme()
const ctx = useContext(FlowContext)
const setNodes = ctx?.setNodes
const { nodes, sourceIds, updateData } = useAbstractNode<ConfigNodeData>(id, data ?? {})
const editorRef = useRef<unknown>(null)
const edges = ctx?.edges ?? []
const nodes = ctx?.nodes ?? []
const incomingEdges = useMemo(() => edges.filter((e: any) => e.target === id), [edges, id])
const incomingIds = useMemo(() => incomingEdges.map((e: any) => e.source).sort(), [incomingEdges])
const connectedConfigNodes = useMemo(
() => (nodes as any[]).filter((n: any) => incomingIds.includes(n.id) && n.type === 'config'),
[nodes, incomingIds]
() => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'config'),
[nodes, sourceIds]
)
const connectedVariableNodes = useMemo(
() => (nodes as any[]).filter((n: any) => incomingIds.includes(n.id) && n.type === 'variable'),
[nodes, incomingIds]
() => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'variable'),
[nodes, sourceIds]
)
const connectedFunctionNodes = useMemo(
() => (nodes as any[]).filter((n: any) => incomingIds.includes(n.id) && n.type === 'function'),
[nodes, incomingIds]
() => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'function'),
[nodes, sourceIds]
)
const hasDependencies = connectedConfigNodes.length > 0 || connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0
const onChange = useCallback(
(val: string) => {
if (setNodes) {
setNodes((nds: any[]) =>
nds.map((n) =>
n.id === id ? { ...n, data: { ...n.data, content: val, configType: configTypeId } } : n
)
)
}
},
[id, setNodes, configTypeId]
(val: string) => updateData({ content: val, configType: configTypeId }),
[updateData, configTypeId]
)
const setConfigType = useCallback(
(newTypeId: ConfigTypeId) => {
if (!setNodes || newTypeId === configTypeId) return
setNodes((nds: any[]) =>
nds.map((n) =>
n.id === id
? {
...n,
data: {
...n.data,
configType: newTypeId,
content: getConfigContent(n.data) ?? '',
},
}
: n
)
)
if (newTypeId === configTypeId) return
updateData({
configType: newTypeId,
content: getConfigContent({ ...data, configType: newTypeId }) ?? '',
})
},
[id, setNodes, configTypeId]
[configTypeId, data, updateData]
)
const insertAt = useCallback(
@@ -369,8 +346,11 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
</BaseNodeFooter>
</BaseNode>
)
}, nodePropsAreEqual)
}
ConfigNode.displayName = 'ConfigNode'
export const ConfigNode = createAbstractNodeComponent<ConfigNodeData>(
'ConfigNode',
ConfigNodeComponent
)
export default ConfigNode

View File

@@ -1,8 +1,12 @@
import React, { memo, useCallback, useContext, useMemo, useRef } from 'react'
import React, { useCallback, useMemo, useRef } from 'react'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import FlowContext from '../../lib/flowContext'
import { nodePropsAreEqual } from '../../lib/flowUtils'
import {
AbstractNodeProps,
createAbstractNodeComponent,
useAbstractNode,
type FlowNode,
} from '../../lib/abstractNode'
import { useResizeHeight } from '../../hooks/useResizeHeight'
import { useTheme } from '../../lib/themeContext'
import {
@@ -19,45 +23,29 @@ import { MenubarItem, MenubarShortcut } from '../ui/menubar'
import { Kbd } from '../ui/kbd'
import { Code2, Variable } from 'lucide-react'
type Props = {
id: string
data: { body?: string }
width?: number
height?: number
}
export type FunctionNodeData = { body?: string }
export const FunctionNode = memo(function FunctionNode({ id, data, width, height }: Props) {
type Props = AbstractNodeProps<FunctionNodeData>
function FunctionNodeComponent({ id, data, width, height }: Props) {
const bodyValue = data?.body ?? ''
const { theme } = useTheme()
const ctx = useContext(FlowContext)
const setNodes = ctx?.setNodes
const { nodes, sourceIds, updateData } = useAbstractNode<FunctionNodeData>(id, data ?? {})
const editorRef = useRef<unknown>(null)
const edges = ctx?.edges ?? []
const nodes = ctx?.nodes ?? []
const incomingEdges = useMemo(() => edges.filter((e: any) => e.target === id), [edges, id])
const incomingIds = useMemo(() => incomingEdges.map((e: any) => e.source).sort(), [incomingEdges])
const connectedVariableNodes = useMemo(
() => (nodes as any[]).filter((n: any) => incomingIds.includes(n.id) && n.type === 'variable'),
[nodes, incomingIds]
() => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'variable'),
[nodes, sourceIds]
)
const connectedFunctionNodes = useMemo(
() => (nodes as any[]).filter((n: any) => incomingIds.includes(n.id) && n.type === 'function'),
[nodes, incomingIds]
() => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'function'),
[nodes, sourceIds]
)
const hasConnectedVariables = connectedVariableNodes.length > 0
const hasConnectedFunctions = connectedFunctionNodes.length > 0
const hasConnectedInputs = hasConnectedVariables || hasConnectedFunctions
const hasConnectedInputs = connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0
const onChange = useCallback(
(val: string) => {
if (setNodes) {
setNodes((nds: any[]) =>
nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, body: val } } : n))
)
}
},
[id, setNodes]
(val: string) => updateData({ body: val }),
[updateData]
)
const insertAt = useCallback(
@@ -165,8 +153,11 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
</BaseNodeFooter>
</BaseNode>
)
}, nodePropsAreEqual)
}
FunctionNode.displayName = 'FunctionNode'
export const FunctionNode = createAbstractNodeComponent<FunctionNodeData>(
'FunctionNode',
FunctionNodeComponent
)
export default FunctionNode

View File

@@ -1,6 +1,10 @@
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import nunjucks from 'nunjucks'
import FlowContext from '../../lib/flowContext'
import {
AbstractNodeProps,
createAbstractNodeComponent,
useAbstractNode,
} from '../../lib/abstractNode'
import { getConfigContent, getConfigType, getConfigTypeId } from '../../lib/configTypes'
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia } from '../ui/empty'
import {
@@ -9,7 +13,7 @@ import {
BaseNodeFooter,
BaseNodeHeaderRow,
} from '../base/BaseNode'
import { getDefaultDataForType, getNextNodeId, nodePropsAreEqual } from '../../lib/flowUtils'
import { getDefaultDataForType, getNextNodeId } from '../../lib/flowUtils'
import { getDefaultStyle } from '../../lib/nodeRegistry'
import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
@@ -19,27 +23,20 @@ import { MenubarItem, MenubarSeparator, MenubarSub, MenubarSubContent, MenubarSu
import { Sparkles } from 'lucide-react'
import { InputHandle } from '../base/NodeHandles'
type Props = {
id: string
data?: any
style?: React.CSSProperties
}
export type RenderingNodeData = Record<string, unknown>
export const RenderingNode = memo(function RenderingNode({ id, width, height }: Props) {
type Props = AbstractNodeProps<RenderingNodeData>
function RenderingNodeComponent({ id, width, height }: Props) {
const [renderedContent, setRenderedContent] = useState<string | null>(null)
const [error, setError] = useState<null | { kind: string; message: string }>(null)
const [loading, setLoading] = useState(false)
const runIdRef = useRef(0)
const loadingStartedAtRef = useRef<number | null>(null)
const minLoadingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const ctx = useContext(FlowContext)
const nodes = ctx?.nodes ?? []
const edges = ctx?.edges ?? []
const setNodes = ctx?.setNodes
const setEdges = ctx?.setEdges
const { nodes, edges, setNodes, setEdges, sourceIds } = useAbstractNode<RenderingNodeData>(id, {})
const incomingEdges = useMemo(() => edges.filter((e: any) => e.target === id), [edges, id])
const incomingIds = useMemo(() => incomingEdges.map((e: any) => e.source).sort(), [incomingEdges])
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'
@@ -619,8 +616,11 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
</BaseNode>
</NodeStatusIndicator>
)
}, nodePropsAreEqual)
}
RenderingNode.displayName = 'RenderingNode'
export const RenderingNode = createAbstractNodeComponent<RenderingNodeData>(
'RenderingNode',
RenderingNodeComponent
)
export default RenderingNode

View File

@@ -1,6 +1,9 @@
import React, { memo, useCallback, useContext } from 'react'
import FlowContext from '../../lib/flowContext'
import { nodePropsAreEqual } from '../../lib/flowUtils'
import React, { useCallback } from 'react'
import {
AbstractNodeProps,
createAbstractNodeComponent,
useAbstractNode,
} from '../../lib/abstractNode'
import {
BaseNode,
BaseNodeContent,
@@ -16,16 +19,15 @@ import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
import { OutputHandle } from '../base/NodeHandles'
import { Variable } from 'lucide-react'
type ValueType = 'string' | 'number' | 'boolean'
export type ValueType = 'string' | 'number' | 'boolean'
type Props = {
id: string
data: {
value?: string | number | boolean
valueType?: ValueType
}
export type VariableNodeData = {
value?: string | number | boolean
valueType?: ValueType
}
type Props = AbstractNodeProps<VariableNodeData>
const DEFAULT_BY_TYPE: Record<ValueType, string | number | boolean> = {
string: '',
number: 0,
@@ -45,26 +47,13 @@ function coerceValue(raw: string, valueType: ValueType): string | number | boole
}
}
export const VariableNode = memo(function VariableNode({ id, data }: Props) {
const ctx = useContext(FlowContext)
const setNodes = ctx?.setNodes
function VariableNodeComponent({ id, data }: Props) {
const { updateData } = useAbstractNode<VariableNodeData>(id, data ?? {})
const valueType: ValueType = data?.valueType ?? 'string'
const value = data?.value ?? DEFAULT_BY_TYPE[valueType]
const displayValue = typeof value === 'string' ? value : String(value)
const updateData = useCallback(
(updates: { value?: string | number | boolean; valueType?: ValueType }) => {
if (!setNodes) return
setNodes((nds: any[]) =>
nds.map((n) =>
n.id === id ? { ...n, data: { ...n.data, ...updates } } : n
)
)
},
[id, setNodes]
)
const onTypeChange = useCallback(
(nextType: string) => {
const type = nextType as ValueType
@@ -85,9 +74,7 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) {
)
const onBooleanChange = useCallback(
(checked: boolean) => {
updateData({ value: checked })
},
(checked: boolean) => updateData({ value: checked }),
[updateData]
)
@@ -141,8 +128,11 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) {
</BaseNodeFooter>
</BaseNode>
)
}, nodePropsAreEqual)
}
VariableNode.displayName = 'VariableNode'
export const VariableNode = createAbstractNodeComponent<VariableNodeData>(
'VariableNode',
VariableNodeComponent
)
export default VariableNode