import React, { memo, useCallback, useContext, useEffect, useRef, useState } from 'react' import Editor, { loader } from '@monaco-editor/react' import FlowContext from '../../lib/flowContext' import { BaseNode, BaseNodeContent, BaseNodeFooter, BaseNodeHeader, BaseNodeHeaderTitle, } from './BaseNode' import { GitBranchPlus, Pencil } from 'lucide-react' import { Menubar, MenubarContent, MenubarItem, MenubarLabel, MenubarMenu, MenubarSeparator, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, } from '../ui/menubar' import { InputHandle, OutputHandle } from './NodeHandles' // Ensure Monaco can load its web workers under Vite by pointing to a CDN loader.config({ paths: { vs: 'https://unpkg.com/monaco-editor@latest/min/vs' } }) type Props = { id: string data: any } export const ConfigNode = memo(function ConfigNode({ id, data }: Props) { const [value, setValue] = useState(data?.yaml ?? '# Enter YAML here\n') const ctx = useContext(FlowContext) const setNodes = ctx?.setNodes const storedYaml = ctx?.nodes?.find((n: any) => n.id === id)?.data?.yaml ?? '' const editorRef = useRef(null) const monacoRef = 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 hasDependencies = connectedConfigNodes.length > 0 || connectedVariableNodes.length > 0 useEffect(() => { if (setNodes) { setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, yaml: value } } : n))) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const onChange = useCallback( (val?: string) => { const v = val ?? '' setValue(v) // eslint-disable-next-line no-console console.debug('ConfigNode:onChange', id, v.substring(0, 60)) if (setNodes) { setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, yaml: v } } : n))) } }, [id, setNodes] ) const handleEditorMount = useCallback((editor: any, monaco: any) => { editorRef.current = editor monacoRef.current = monaco // Prevent React Flow and other global handlers from interfering with typing try { editor.onKeyDown((e: any) => { if (e?.browserEvent) { e.browserEvent.stopPropagation() } }) editor.onMouseDown((e: any) => { if (e?.event) { e.event.stopPropagation() } }) } catch { // ignore if Monaco internals change } }, []) const insertIncludeFromNode = useCallback( (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => { const ref = `${sourceNode.id}.yaml` const includeText = `!include ${ref}\n` const editor = editorRef.current const monaco = monacoRef.current try { if (editor && monaco) { const model = editor.getModel() const lineCount = model.getLineCount() const selection = editor.getSelection() let range if (mode === 'prepend') { range = new monaco.Range(1, 1, 1, 1) } else if (mode === 'append') { range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1) } else if (selection) { range = new monaco.Range(selection.startLineNumber, selection.startColumn, selection.startLineNumber, selection.startColumn) } else { range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1) } editor.executeEdits('insert-include', [{ range, text: includeText, forceMoveMarkers: true }]) const newVal = editor.getModel().getValue() onChange(newVal) return } if (mode === 'prepend') { onChange(includeText + value) } else { onChange(value + '\n' + includeText) } } catch (err) { // eslint-disable-next-line no-console console.error('Insert include failed', err) } }, [onChange, value] ) const insertVariableReference = useCallback( (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => { const insertText = `\${${sourceNode.id}}` const editor = editorRef.current const monaco = monacoRef.current try { if (editor && monaco) { const model = editor.getModel() const lineCount = model.getLineCount() const selection = editor.getSelection() let range if (mode === 'prepend') { range = new monaco.Range(1, 1, 1, 1) } else if (mode === 'append') { range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1) } else if (selection) { range = new monaco.Range(selection.startLineNumber, selection.startColumn, selection.startLineNumber, selection.startColumn) } else { range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1) } editor.executeEdits('insert-var', [{ range, text: insertText, forceMoveMarkers: true }]) const newVal = editor.getModel().getValue() onChange(newVal) return } if (mode === 'prepend') { onChange(insertText + value) } else { onChange(value + insertText) } } catch (err) { // eslint-disable-next-line no-console console.error('Insert variable reference failed', err) } }, [onChange, value] ) return ( {id}.yml {hasDependencies && (
{connectedConfigNodes.map((n: any) => ( {n.data?.title ?? n.id} insertIncludeFromNode(n, 'prepend')} > Prepend include insertIncludeFromNode(n, 'append')} > Append include insertIncludeFromNode(n, 'cursor')} > Insert at cursor ))} {connectedVariableNodes.map((n: any) => ( {n.id} insertVariableReference(n, 'cursor')} > Insert at cursor ))}
)}
{storedYaml ? `${storedYaml.length} chars` : 'none'}
) }) ConfigNode.displayName = 'ConfigNode' export default ConfigNode