fix: codemirror util
This commit is contained in:
@@ -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<ConfigNodeData>(id, data ?? {})
|
||||
const editorRef = useRef<unknown>(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<CodeMirrorEditorRef | null>(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) {
|
||||
|
||||
<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}
|
||||
value={content}
|
||||
height={`${editorHeight}px`}
|
||||
|
||||
@@ -4,9 +4,10 @@ import { javascript } from '@codemirror/lang-javascript'
|
||||
import {
|
||||
AbstractNodeProps,
|
||||
createAbstractNodeComponent,
|
||||
getConnectedNodesByType,
|
||||
useAbstractNode,
|
||||
type FlowNode,
|
||||
} from '@/lib/graph/abstractNode'
|
||||
import { useCodeMirrorInsert, type CodeMirrorEditorRef } from '@/hooks/useCodeMirrorInsert'
|
||||
import { useResizeHeight } from '@/hooks/useResizeHeight'
|
||||
import { useTheme } from '@/lib/themeContext'
|
||||
import {
|
||||
@@ -36,43 +37,22 @@ function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const bodyValue = data?.body ?? ''
|
||||
const { theme } = useTheme()
|
||||
const { nodes, sourceIds, updateData } = useAbstractNode<FunctionNodeData>(id, data ?? {})
|
||||
const editorRef = useRef<unknown>(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<CodeMirrorEditorRef | null>(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) {
|
||||
</div>
|
||||
<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}
|
||||
value={bodyValue}
|
||||
height={`${editorHeight}px`}
|
||||
|
||||
51
frontend/src/hooks/useCodeMirrorInsert.ts
Normal file
51
frontend/src/hooks/useCodeMirrorInsert.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
/**
|
||||
* Ref shape for @uiw/react-codemirror (view/state not in package types).
|
||||
* Use with: ref={editorRef} and type editorRef as React.MutableRefObject<CodeMirrorEditorRef | null>.
|
||||
*/
|
||||
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<CodeMirrorEditorRef | null>,
|
||||
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]
|
||||
)
|
||||
}
|
||||
@@ -59,6 +59,15 @@ export type AbstractNodeContext<TData = Record<string, unknown>> = {
|
||||
targetIds: string[]
|
||||
}
|
||||
|
||||
/** Returns nodes that are connected to this node (in sourceIds) and have the given type. */
|
||||
export function getConnectedNodesByType<T extends FlowNode = FlowNode>(
|
||||
nodes: FlowNode[],
|
||||
sourceIds: string[],
|
||||
type: string
|
||||
): T[] {
|
||||
return nodes.filter((n) => sourceIds.includes(n.id) && n.type === type) as T[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user