import React, { memo, useCallback, useContext, useMemo, useRef } from 'react' import { autocompletion } from '@codemirror/autocomplete' import CodeMirror from '@uiw/react-codemirror' import FlowContext from '../../lib/flowContext' import { useResizeHeight } from '../../hooks/useResizeHeight' import { nunjucksCompletionSource } from '../../lib/nunjucksAutocomplete' import { plantumlLanguage } from '../../lib/plantumlLanguage' import { useTheme } from '../../lib/themeContext' import { BaseNode, BaseNodeContent, BaseNodeFooter, BaseNodeHeaderRow, } from './BaseNode' import { Code2, ScrollText, Variable } from 'lucide-react' import { MenubarItem, MenubarSub, MenubarSubContent, MenubarSubTrigger, } from '../ui/menubar' import { InputHandle, OutputHandle } from './NodeHandles' import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators' import { NodeHeaderTitle } from './NodeHeaderTitle' import { NodeMenubar } from './NodeMenubar' type Props = { id: string data: any width?: number height?: number } const DEFAULT_PLANTUML = '@startuml\n\n@enduml\n' export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: Props) { const plantumlValue = data?.plantuml ?? DEFAULT_PLANTUML const { theme } = useTheme() const ctx = useContext(FlowContext) const setNodes = ctx?.setNodes const editorRef = useRef(null) const incomingEdges = ctx?.edges?.filter((e: any) => e.target === id) ?? [] const incomingIds = incomingEdges.map((e: any) => e.source).sort() const connectedConfigNodes = (ctx?.nodes ?? []).filter((n: any) => incomingIds.includes(n.id) && n.type === 'config') const connectedVariableNodes = (ctx?.nodes ?? []).filter((n: any) => incomingIds.includes(n.id) && n.type === 'variable') const connectedFunctionNodes = (ctx?.nodes ?? []).filter((n: any) => incomingIds.includes(n.id) && n.type === 'function') 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, plantuml: val } } : n))) } }, [id, setNodes] ) 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 + plantumlValue) } else { onChange(plantumlValue + insertText) } }, [onChange, plantumlValue] ) const insertExtendsFromNode = useCallback( (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => { insertAt(`{% extends "${sourceNode.id}" %}\n`, mode) }, [insertAt] ) const insertIncludeFromNode = useCallback( (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => { insertAt(`{% include "${sourceNode.id}" %}\n`, mode) }, [insertAt] ) const insertImportFromNode = useCallback( (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => { insertAt(`{% import "${sourceNode.id}" as ${sourceNode.id} %}\n`, mode) }, [insertAt] ) const insertVariableReference = useCallback( (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => { insertAt(`{{ ${sourceNode.id} }}`, mode) }, [insertAt] ) const insertFunctionCall = useCallback( (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => { insertAt(`{{ ${sourceNode.id}() }}`, mode) }, [insertAt] ) const variableIds = useMemo(() => connectedVariableNodes.map((n: any) => n.id), [connectedVariableNodes]) const functionIds = useMemo(() => connectedFunctionNodes.map((n: any) => n.id), [connectedFunctionNodes]) const configTitles = useMemo( () => connectedConfigNodes.map((n: any) => n.data?.title ?? n.id), [connectedConfigNodes], ) const extensions = useMemo( () => [ plantumlLanguage.extension, autocompletion({ override: [nunjucksCompletionSource(variableIds, configTitles, functionIds)], activateOnTyping: true, }), ], [variableIds, functionIds, configTitles], ) const [editorHeight, editorContainerRef] = useResizeHeight(180) const nunjucksTagSnippets = useMemo( () => [ { label: 'Variable {{ }}', snippet: '{{ }}' }, { label: 'if / endif', snippet: '{% if %}\n \n{% endif %}' }, { label: 'for / endfor', snippet: '{% for in %}\n \n{% endfor %}' }, { label: 'set', snippet: '{% set = %}' }, { label: 'block / endblock', snippet: '{% block %}\n \n{% endblock %}' }, { label: 'extends', snippet: '{% extends "" %}' }, { label: 'include', snippet: '{% include "" %}' }, { label: 'import', snippet: '{% import "" as %}' }, { label: 'raw / endraw', snippet: '{% raw %}\n \n{% endraw %}' }, ], [] ) const dimensions = width != null && height != null && width > 0 && height > 0 ? { width, height } : undefined return ( }> } title={} />
{connectedConfigNodes.map((n: any) => ( {n.data?.title ?? n.id} insertExtendsFromNode(n, 'cursor')} > Extend insertIncludeFromNode(n, 'cursor')} > Include insertImportFromNode(n, 'cursor')} > Import ))} {connectedVariableNodes.map((n: any) => ( {n.id} insertVariableReference(n, 'cursor')} > Insert ))} {connectedFunctionNodes.map((n: any) => ( {n.id} insertFunctionCall(n, 'cursor')} > Insert ))} ) : undefined } insertTagsContent={ <> {nunjucksTagSnippets.map(({ label, snippet }) => ( insertAt(snippet, 'cursor')} > {label} ))} } />
{plantumlValue ? `${plantumlValue.length} chars` : 'none'}
) }) ConfigNode.displayName = 'ConfigNode' export default ConfigNode