'use client'; import { useEffect, useState, useRef } from 'react'; import YAML from 'yaml'; import Editor, { OnMount } from '@monaco-editor/react'; import type { editor } from 'monaco-editor'; import { useTheme } from '@/components/ThemeProvider'; type Props = { config: T; setConfig: (value: any, key?: string) => void; transformOnParse?: (parsed: any) => any; }; const yamlConfig: YAML.DocumentOptions & YAML.SchemaOptions & YAML.ParseOptions & YAML.CreateNodeOptions & YAML.ToStringOptions = { indent: 2, lineWidth: 999999999999, defaultStringType: 'QUOTE_DOUBLE', defaultKeyType: 'PLAIN', directives: true, }; function toYaml(obj: any): string { const doc = new YAML.Document(obj, yamlConfig); YAML.visit(doc, { Scalar(_key, node) { if (typeof node.value === 'string' && node.value.includes('\n')) { node.type = YAML.Scalar.BLOCK_LITERAL; } }, }); return doc.toString(yamlConfig); } export default function AdvancedConfigEditor({ config, setConfig, transformOnParse }: Props) { const { theme } = useTheme(); const [editorValue, setEditorValue] = useState(''); const [hasError, setHasError] = useState(false); const lastConfigUpdateStringRef = useRef(''); const editorRef = useRef(null); const monacoRef = useRef(null); // Track if the editor has been mounted const isEditorMounted = useRef(false); // Handler for editor mounting const handleEditorDidMount: OnMount = (editor, monaco) => { editorRef.current = editor; monacoRef.current = monaco; isEditorMounted.current = true; // Initial content setup try { const yamlContent = toYaml(config); setEditorValue(yamlContent); lastConfigUpdateStringRef.current = JSON.stringify(config); } catch (e) { console.warn(e); } }; useEffect(() => { const lastUpdate = lastConfigUpdateStringRef.current; const currentUpdate = JSON.stringify(config); // Skip if no changes or editor not yet mounted if (lastUpdate === currentUpdate || !isEditorMounted.current) { return; } try { // Preserve cursor position and selection const editor = editorRef.current; if (editor) { // Save current editor state const position = editor.getPosition(); const selection = editor.getSelection(); const scrollTop = editor.getScrollTop(); // Update content const yamlContent = toYaml(config); // Only update if the content is actually different if (yamlContent !== editor.getValue()) { // Set value directly on the editor model instead of using React state editor.getModel()?.setValue(yamlContent); // Restore cursor position and selection if (position) editor.setPosition(position); if (selection) editor.setSelection(selection); editor.setScrollTop(scrollTop); } lastConfigUpdateStringRef.current = currentUpdate; } } catch (e) { console.warn(e); } }, [config]); const setMarkers = (errors: { message: string; line: number }[]) => { const monaco = monacoRef.current; const model = editorRef.current?.getModel(); if (!monaco || !model) return; const markers = errors.map(err => ({ severity: monaco.MarkerSeverity.Error, message: err.message, startLineNumber: err.line, startColumn: 1, endLineNumber: err.line, endColumn: model.getLineMaxColumn(err.line), })); monaco.editor.setModelMarkers(model, 'yaml', markers); }; const handleChange = (value: string | undefined) => { if (value === undefined) return; try { let parsed = YAML.parse(value); setHasError(false); setMarkers([]); // Don't update config if the change came from the editor itself // to avoid a circular update loop if (JSON.stringify(parsed) !== lastConfigUpdateStringRef.current) { if (transformOnParse) { parsed = transformOnParse(parsed); } lastConfigUpdateStringRef.current = JSON.stringify(parsed); setConfig(parsed); } } catch (e: any) { setHasError(true); const line = e?.linePos?.[0]?.line ?? e?.linePos?.line ?? 1; setMarkers([{ message: e?.message ?? 'Invalid YAML', line }]); } }; return (
{hasError && (
)}
); }