fix: codemirror util
This commit is contained in:
@@ -6,9 +6,10 @@ import { markdown } from '@codemirror/lang-markdown'
|
|||||||
import {
|
import {
|
||||||
AbstractNodeProps,
|
AbstractNodeProps,
|
||||||
createAbstractNodeComponent,
|
createAbstractNodeComponent,
|
||||||
|
getConnectedNodesByType,
|
||||||
useAbstractNode,
|
useAbstractNode,
|
||||||
type FlowNode,
|
|
||||||
} from '@/lib/graph/abstractNode'
|
} from '@/lib/graph/abstractNode'
|
||||||
|
import { useCodeMirrorInsert, type CodeMirrorEditorRef } from '@/hooks/useCodeMirrorInsert'
|
||||||
import { useResizeHeight } from '@/hooks/useResizeHeight'
|
import { useResizeHeight } from '@/hooks/useResizeHeight'
|
||||||
import { nunjucksCompletionSource } from '@/lib/nunjucksAutocomplete'
|
import { nunjucksCompletionSource } from '@/lib/nunjucksAutocomplete'
|
||||||
import { plantumlLanguage } from '@/lib/plantumlLanguage'
|
import { plantumlLanguage } from '@/lib/plantumlLanguage'
|
||||||
@@ -60,30 +61,30 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
const content = getConfigContent(data ?? {})
|
const content = getConfigContent(data ?? {})
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { nodes, sourceIds, updateData } = useAbstractNode<ConfigNodeData>(id, data ?? {})
|
const { nodes, sourceIds, updateData } = useAbstractNode<ConfigNodeData>(id, data ?? {})
|
||||||
const editorRef = useRef<unknown>(null)
|
const editorRef = useRef<CodeMirrorEditorRef | null>(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 onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(val: string) => updateData({ content: val, configType: configTypeId }),
|
(val: string) => updateData({ content: val, configType: configTypeId }),
|
||||||
[updateData, 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(
|
const setConfigType = useCallback(
|
||||||
(newTypeId: ConfigTypeId) => {
|
(newTypeId: ConfigTypeId) => {
|
||||||
@@ -96,36 +97,6 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
[configTypeId, data, updateData]
|
[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(
|
const insertExtendsFromNode = useCallback(
|
||||||
(sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
|
(sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
|
||||||
insertAt(`{% extends "${sourceNode.id}" %}\n`, mode)
|
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">
|
<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
|
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={content}
|
value={content}
|
||||||
height={`${editorHeight}px`}
|
height={`${editorHeight}px`}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { javascript } from '@codemirror/lang-javascript'
|
|||||||
import {
|
import {
|
||||||
AbstractNodeProps,
|
AbstractNodeProps,
|
||||||
createAbstractNodeComponent,
|
createAbstractNodeComponent,
|
||||||
|
getConnectedNodesByType,
|
||||||
useAbstractNode,
|
useAbstractNode,
|
||||||
type FlowNode,
|
|
||||||
} from '@/lib/graph/abstractNode'
|
} from '@/lib/graph/abstractNode'
|
||||||
|
import { useCodeMirrorInsert, type CodeMirrorEditorRef } from '@/hooks/useCodeMirrorInsert'
|
||||||
import { useResizeHeight } from '@/hooks/useResizeHeight'
|
import { useResizeHeight } from '@/hooks/useResizeHeight'
|
||||||
import { useTheme } from '@/lib/themeContext'
|
import { useTheme } from '@/lib/themeContext'
|
||||||
import {
|
import {
|
||||||
@@ -36,43 +37,22 @@ function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
const bodyValue = data?.body ?? ''
|
const bodyValue = data?.body ?? ''
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { nodes, sourceIds, updateData } = useAbstractNode<FunctionNodeData>(id, data ?? {})
|
const { nodes, sourceIds, updateData } = useAbstractNode<FunctionNodeData>(id, data ?? {})
|
||||||
const editorRef = useRef<unknown>(null)
|
const editorRef = useRef<CodeMirrorEditorRef | null>(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 onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(val: string) => updateData({ body: val }),
|
(val: string) => updateData({ body: val }),
|
||||||
[updateData]
|
[updateData]
|
||||||
)
|
)
|
||||||
|
const insertAt = useCodeMirrorInsert(editorRef, bodyValue, onChange)
|
||||||
|
|
||||||
const insertAt = useCallback(
|
const connectedVariableNodes = useMemo(
|
||||||
(insertText: string, mode: 'prepend' | 'append' | 'cursor') => {
|
() => getConnectedNodesByType(nodes, sourceIds, 'variable'),
|
||||||
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
|
[nodes, sourceIds]
|
||||||
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 connectedFunctionNodes = useMemo(
|
||||||
|
() => getConnectedNodesByType(nodes, sourceIds, 'function'),
|
||||||
|
[nodes, sourceIds]
|
||||||
|
)
|
||||||
|
const hasConnectedInputs = connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0
|
||||||
|
|
||||||
const insertVariableAtCursor = useCallback(
|
const insertVariableAtCursor = useCallback(
|
||||||
(variableNode: any) => {
|
(variableNode: any) => {
|
||||||
@@ -142,7 +122,6 @@ function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div ref={editorContainerRef as React.RefObject<HTMLDivElement>} 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
|
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={bodyValue}
|
value={bodyValue}
|
||||||
height={`${editorHeight}px`}
|
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[]
|
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
|
// Hook
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user