From 35b2e9d53875801f98cc0776bed17dd7dd7b56e1 Mon Sep 17 00:00:00 2001 From: dtoro Date: Thu, 12 Mar 2026 20:23:39 +0100 Subject: [PATCH] fix: codemirror util --- .../components/nodes/config/ConfigNode.tsx | 74 ++++++------------- .../nodes/function/FunctionNode.tsx | 45 +++-------- frontend/src/hooks/useCodeMirrorInsert.ts | 51 +++++++++++++ frontend/src/lib/graph/abstractNode.ts | 9 +++ 4 files changed, 94 insertions(+), 85 deletions(-) create mode 100644 frontend/src/hooks/useCodeMirrorInsert.ts diff --git a/frontend/src/components/nodes/config/ConfigNode.tsx b/frontend/src/components/nodes/config/ConfigNode.tsx index 5459e55..bd0e0b6 100644 --- a/frontend/src/components/nodes/config/ConfigNode.tsx +++ b/frontend/src/components/nodes/config/ConfigNode.tsx @@ -6,9 +6,10 @@ import { markdown } from '@codemirror/lang-markdown' import { AbstractNodeProps, createAbstractNodeComponent, + getConnectedNodesByType, useAbstractNode, - type FlowNode, } from '@/lib/graph/abstractNode' +import { useCodeMirrorInsert, type CodeMirrorEditorRef } from '@/hooks/useCodeMirrorInsert' import { useResizeHeight } from '@/hooks/useResizeHeight' import { nunjucksCompletionSource } from '@/lib/nunjucksAutocomplete' import { plantumlLanguage } from '@/lib/plantumlLanguage' @@ -60,30 +61,30 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) { const content = getConfigContent(data ?? {}) const { theme } = useTheme() const { nodes, sourceIds, updateData } = useAbstractNode(id, data ?? {}) - const editorRef = useRef(null) - - const connectedConfigNodes = useMemo( - () => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'config'), - [nodes, sourceIds] - ) - const connectedVariableNodes = useMemo( - () => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'variable'), - [nodes, sourceIds] - ) - const connectedFunctionNodes = useMemo( - () => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'function'), - [nodes, sourceIds] - ) - const connectedDataNodes = useMemo( - () => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'data'), - [nodes, sourceIds] - ) - const hasDependencies = connectedConfigNodes.length > 0 || connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0 || connectedDataNodes.length > 0 - + const editorRef = useRef(null) const onChange = useCallback( (val: string) => updateData({ content: val, configType: configTypeId }), [updateData, configTypeId] ) + const insertAt = useCodeMirrorInsert(editorRef, content, onChange) + + const connectedConfigNodes = useMemo( + () => getConnectedNodesByType(nodes, sourceIds, 'config'), + [nodes, sourceIds] + ) + const connectedVariableNodes = useMemo( + () => getConnectedNodesByType(nodes, sourceIds, 'variable'), + [nodes, sourceIds] + ) + const connectedFunctionNodes = useMemo( + () => getConnectedNodesByType(nodes, sourceIds, 'function'), + [nodes, sourceIds] + ) + const connectedDataNodes = useMemo( + () => getConnectedNodesByType(nodes, sourceIds, 'data'), + [nodes, sourceIds] + ) + const hasDependencies = connectedConfigNodes.length > 0 || connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0 || connectedDataNodes.length > 0 const setConfigType = useCallback( (newTypeId: ConfigTypeId) => { @@ -96,36 +97,6 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) { [configTypeId, data, updateData] ) - const insertAt = useCallback( - (insertText: string, mode: 'prepend' | 'append' | 'cursor') => { - const ref = editorRef.current as { view: { state: { doc: { length: number; toString(): string }; selection: { main: { from: number } } }; dispatch: (arg: { changes: { from: number; to: number; insert: string } }) => void } } | null - if (ref?.view) { - const view = ref.view - const doc = view.state.doc - const len = doc.length - let from: number - if (mode === 'prepend') { - from = 0 - } else if (mode === 'append') { - from = len - } else { - const main = view.state.selection.main - from = main.from - } - view.dispatch({ changes: { from, to: from, insert: insertText } }) - const newVal = view.state.doc.toString() - onChange(newVal) - return - } - if (mode === 'prepend') { - onChange(insertText + content) - } else { - onChange(content + insertText) - } - }, - [onChange, content] - ) - const insertExtendsFromNode = useCallback( (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => { insertAt(`{% extends "${sourceNode.id}" %}\n`, mode) @@ -357,7 +328,6 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input"> (id, data ?? {}) - const editorRef = useRef(null) - - const connectedVariableNodes = useMemo( - () => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'variable'), - [nodes, sourceIds] - ) - const connectedFunctionNodes = useMemo( - () => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'function'), - [nodes, sourceIds] - ) - const hasConnectedInputs = connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0 - + const editorRef = useRef(null) const onChange = useCallback( (val: string) => updateData({ body: val }), [updateData] ) + const insertAt = useCodeMirrorInsert(editorRef, bodyValue, onChange) - const insertAt = useCallback( - (insertText: string, mode: 'prepend' | 'append' | 'cursor') => { - const ref = editorRef.current as { view: { state: { doc: { length: number }; selection: { main: { from: number } } }; dispatch: (arg: { changes: { from: number; to: number; insert: string } }) => void } } | null - if (ref?.view) { - const view = ref.view - const doc = view.state.doc - const len = doc.length - let from: number - if (mode === 'prepend') from = 0 - else if (mode === 'append') from = len - else from = view.state.selection.main.from - view.dispatch({ changes: { from, to: from, insert: insertText } }) - onChange(view.state.doc.toString()) - return - } - if (mode === 'prepend') onChange(insertText + bodyValue) - else onChange(bodyValue + insertText) - }, - [onChange, bodyValue] + const connectedVariableNodes = useMemo( + () => getConnectedNodesByType(nodes, sourceIds, 'variable'), + [nodes, sourceIds] ) + const connectedFunctionNodes = useMemo( + () => getConnectedNodesByType(nodes, sourceIds, 'function'), + [nodes, sourceIds] + ) + const hasConnectedInputs = connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0 const insertVariableAtCursor = useCallback( (variableNode: any) => { @@ -142,7 +122,6 @@ function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input"> . + */ +export type CodeMirrorEditorRef = { + view: { + state: { + doc: { length: number; toString(): string } + selection: { main: { from: number } } + } + dispatch: (arg: { changes: { from: number; to: number; insert: string } }) => void + } +} + +export type InsertPosition = 'prepend' | 'append' | 'cursor' + +/** + * Returns a stable insertAt(insertText, mode) that inserts text into the CodeMirror + * editor at the given position (prepend, append, or cursor), then calls onChange + * with the new content. Falls back to string concatenation when the editor ref + * is not yet mounted. + */ +export function useCodeMirrorInsert( + editorRef: React.RefObject, + currentContent: string, + onChange: (value: string) => void +): (insertText: string, mode: InsertPosition) => void { + return useCallback( + (insertText: string, mode: InsertPosition) => { + const ref = editorRef.current + if (ref?.view) { + const view = ref.view + const doc = view.state.doc + const len = doc.length + const from = + mode === 'prepend' ? 0 : mode === 'append' ? len : view.state.selection.main.from + view.dispatch({ changes: { from, to: from, insert: insertText } }) + onChange(view.state.doc.toString()) + return + } + if (mode === 'prepend') { + onChange(insertText + currentContent) + } else { + onChange(currentContent + insertText) + } + }, + [currentContent, onChange] + ) +} diff --git a/frontend/src/lib/graph/abstractNode.ts b/frontend/src/lib/graph/abstractNode.ts index 386f008..d847577 100644 --- a/frontend/src/lib/graph/abstractNode.ts +++ b/frontend/src/lib/graph/abstractNode.ts @@ -59,6 +59,15 @@ export type AbstractNodeContext> = { targetIds: string[] } +/** Returns nodes that are connected to this node (in sourceIds) and have the given type. */ +export function getConnectedNodesByType( + nodes: FlowNode[], + sourceIds: string[], + type: string +): T[] { + return nodes.filter((n) => sourceIds.includes(n.id) && n.type === type) as T[] +} + // --------------------------------------------------------------------------- // Hook // ---------------------------------------------------------------------------