| 
 | 1 | +/* eslint-disable no-empty */  | 
 | 2 | +/* eslint-disable @typescript-eslint/no-explicit-any */  | 
 | 3 | +import React, { FC, useEffect, useMemo, useRef, useState } from 'react'  | 
 | 4 | +import { Form } from 'antd'  | 
 | 5 | +import Editor from '@monaco-editor/react'  | 
 | 6 | +import type * as monaco from 'monaco-editor'  | 
 | 7 | +import { TFormName, TPersistedControls } from 'localTypes/form'  | 
 | 8 | +import * as yaml from 'yaml'  | 
 | 9 | +import { useOnValuesChangeCallback } from '../../organisms/BlackholeForm/context'  | 
 | 10 | + | 
 | 11 | +export const FormInlineYamlEditor: FC<{  | 
 | 12 | +  path: TFormName  | 
 | 13 | +  persistedControls: TPersistedControls  | 
 | 14 | +  externalValue?: unknown  | 
 | 15 | +}> = ({ path, persistedControls, externalValue }) => {  | 
 | 16 | +  const form = Form.useFormInstance()  | 
 | 17 | +  const onValuesChange = useOnValuesChangeCallback()  | 
 | 18 | + | 
 | 19 | +  const [yamlText, setYamlText] = useState<string>('')  | 
 | 20 | +  const monacoRef = useRef<typeof monaco | null>(null)  | 
 | 21 | +  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)  | 
 | 22 | +  const isLocalEditRef = useRef<boolean>(false)  | 
 | 23 | +  const clearLocalEditTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)  | 
 | 24 | +  const isFocusedRef = useRef<boolean>(false)  | 
 | 25 | + | 
 | 26 | +  // focus is tracked via Monaco events for reliability during IME/composition  | 
 | 27 | + | 
 | 28 | +  const modelUri = useMemo(() => {  | 
 | 29 | +    const encoded = encodeURIComponent(JSON.stringify(path))  | 
 | 30 | +    return `inmemory://openapi-ui/unknown/${encoded}.yaml`  | 
 | 31 | +  }, [path])  | 
 | 32 | + | 
 | 33 | +  // Watch specific field for external updates (e.g., main YAML editor)  | 
 | 34 | +  const watchedValue = Form.useWatch(path as any, form)  | 
 | 35 | +  useEffect(() => {  | 
 | 36 | +    if (isLocalEditRef.current) return  | 
 | 37 | +    if (isFocusedRef.current) return  | 
 | 38 | +    const value = watchedValue  | 
 | 39 | +    const isEmptyObj = value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0  | 
 | 40 | +    const next = value == null || isEmptyObj ? '' : yaml.stringify(value) ?? ''  | 
 | 41 | +    setYamlText(prev => (prev === next ? prev : next))  | 
 | 42 | +  }, [watchedValue])  | 
 | 43 | + | 
 | 44 | +  // Force-sync from externalValue prop (provided by parent Form.Item with shouldUpdate)  | 
 | 45 | +  useEffect(() => {  | 
 | 46 | +    if (isLocalEditRef.current) return  | 
 | 47 | +    if (isFocusedRef.current) return  | 
 | 48 | +    const value = externalValue  | 
 | 49 | +    const isEmptyObj = value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0  | 
 | 50 | +    const next = value == null || isEmptyObj ? '' : yaml.stringify(value) ?? ''  | 
 | 51 | +    setYamlText(prev => (prev === next ? prev : next))  | 
 | 52 | +  }, [externalValue])  | 
 | 53 | + | 
 | 54 | +  // Keep model tokenization and content in sync when not locally editing or focused  | 
 | 55 | +  useEffect(() => {  | 
 | 56 | +    if (isLocalEditRef.current) return  | 
 | 57 | +    if (isFocusedRef.current) return  | 
 | 58 | +    const ed = editorRef.current  | 
 | 59 | +    const m = monacoRef.current  | 
 | 60 | +    if (!ed || !m) return  | 
 | 61 | +    const uri = m.Uri.parse(modelUri)  | 
 | 62 | +    let model = ed.getModel() || m.editor.getModel(uri)  | 
 | 63 | +    if (!model) {  | 
 | 64 | +      model = m.editor.createModel(yamlText ?? '', 'yaml', uri)  | 
 | 65 | +    }  | 
 | 66 | +    if (model) {  | 
 | 67 | +      m.editor.setModelLanguage(model, 'yaml')  | 
 | 68 | +      const current = model.getValue()  | 
 | 69 | +      if ((yamlText ?? '') !== current) {  | 
 | 70 | +        model.setValue(yamlText ?? '')  | 
 | 71 | +      }  | 
 | 72 | +    }  | 
 | 73 | +  }, [yamlText, modelUri])  | 
 | 74 | + | 
 | 75 | +  return (  | 
 | 76 | +    <div style={{ height: 140 }}>  | 
 | 77 | +      <Editor  | 
 | 78 | +        language="yaml"  | 
 | 79 | +        path={modelUri}  | 
 | 80 | +        keepCurrentModel  | 
 | 81 | +        width="100%"  | 
 | 82 | +        height="100%"  | 
 | 83 | +        defaultValue={yamlText ?? ''}  | 
 | 84 | +        onMount={(editor, m) => {  | 
 | 85 | +          editorRef.current = editor  | 
 | 86 | +          monacoRef.current = m  | 
 | 87 | +          // initialize focus state  | 
 | 88 | +          try {  | 
 | 89 | +            isFocusedRef.current = !!editor.hasTextFocus?.()  | 
 | 90 | +          } catch {  | 
 | 91 | +            isFocusedRef.current = false  | 
 | 92 | +          }  | 
 | 93 | +          editor.onDidFocusEditorText(() => {  | 
 | 94 | +            isFocusedRef.current = true  | 
 | 95 | +          })  | 
 | 96 | +          editor.onDidBlurEditorText(() => {  | 
 | 97 | +            isFocusedRef.current = false  | 
 | 98 | +          })  | 
 | 99 | +          const uri = m.Uri.parse(modelUri)  | 
 | 100 | +          let model = editor.getModel() || m.editor.getModel(uri)  | 
 | 101 | +          if (!model) {  | 
 | 102 | +            model = m.editor.createModel(yamlText ?? '', 'yaml', uri)  | 
 | 103 | +          }  | 
 | 104 | +          if (model) {  | 
 | 105 | +            const ensureYaml = () => {  | 
 | 106 | +              try {  | 
 | 107 | +                const mm = monacoRef.current  | 
 | 108 | +                const ee = editorRef.current  | 
 | 109 | +                if (!mm || !ee) return  | 
 | 110 | +                const u = mm.Uri.parse(modelUri)  | 
 | 111 | +                const mdl = ee.getModel() || mm.editor.getModel(u)  | 
 | 112 | +                if (!mdl) return  | 
 | 113 | +                const lang = (mdl as any).getLanguageId?.() || (mdl as any).getModeId?.()  | 
 | 114 | +                if (lang !== 'yaml') {  | 
 | 115 | +                  mm.editor.setModelLanguage(mdl, 'yaml')  | 
 | 116 | +                }  | 
 | 117 | +              } catch {}  | 
 | 118 | +            }  | 
 | 119 | +            // Initial apply and a few retries to cover lazy language loading/layout timing  | 
 | 120 | +            ensureYaml()  | 
 | 121 | +            try {  | 
 | 122 | +              requestAnimationFrame(() => ensureYaml())  | 
 | 123 | +            } catch {}  | 
 | 124 | +            setTimeout(() => ensureYaml(), 50)  | 
 | 125 | +            setTimeout(() => ensureYaml(), 200)  | 
 | 126 | +          }  | 
 | 127 | +        }}  | 
 | 128 | +        onValidate={() => {  | 
 | 129 | +          // Re-apply model language to restore tokenization after value changes from outside  | 
 | 130 | +          const m = monacoRef.current  | 
 | 131 | +          const ed = editorRef.current  | 
 | 132 | +          if (!m || !ed) return  | 
 | 133 | +          const uri = m.Uri.parse(modelUri)  | 
 | 134 | +          const model = ed.getModel() || m.editor.getModel(uri)  | 
 | 135 | +          if (model) {  | 
 | 136 | +            m.editor.setModelLanguage(model, 'yaml')  | 
 | 137 | +          }  | 
 | 138 | +        }}  | 
 | 139 | +        onChange={value => {  | 
 | 140 | +          const nextText = value || ''  | 
 | 141 | +          // Debounce local flag longer to avoid flicker with fast inputs and async BFF roundtrip  | 
 | 142 | +          isLocalEditRef.current = true  | 
 | 143 | +          if (clearLocalEditTimeoutRef.current) clearTimeout(clearLocalEditTimeoutRef.current)  | 
 | 144 | +          setYamlText(nextText)  | 
 | 145 | +          try {  | 
 | 146 | +            const parsed = yaml.parse(nextText || '')  | 
 | 147 | +            // Normalize empty content/null/empty object to an empty object value  | 
 | 148 | +            let nextValue: any  | 
 | 149 | +            if (!nextText.trim()) {  | 
 | 150 | +              nextValue = {}  | 
 | 151 | +            } else if (parsed === null) {  | 
 | 152 | +              nextValue = {}  | 
 | 153 | +            } else if (typeof parsed === 'object' && !Array.isArray(parsed) && Object.keys(parsed).length === 0) {  | 
 | 154 | +              nextValue = {}  | 
 | 155 | +            } else {  | 
 | 156 | +              nextValue = parsed  | 
 | 157 | +            }  | 
 | 158 | +            form.setFieldValue(path as any, nextValue)  | 
 | 159 | +            // Mark persisted to prevent cleanup from removing empty object branch  | 
 | 160 | +            if (!persistedControls.persistedKeys.some(k => JSON.stringify(k) === JSON.stringify(path))) {  | 
 | 161 | +              persistedControls.onPersistMark(path, 'obj')  | 
 | 162 | +            }  | 
 | 163 | +            onValuesChange?.()  | 
 | 164 | +          } catch {  | 
 | 165 | +            // ignore parse errors while typing  | 
 | 166 | +          }  | 
 | 167 | +          clearLocalEditTimeoutRef.current = setTimeout(() => {  | 
 | 168 | +            isLocalEditRef.current = false  | 
 | 169 | +          }, 600)  | 
 | 170 | +        }}  | 
 | 171 | +        options={{  | 
 | 172 | +          minimap: { enabled: false },  | 
 | 173 | +          lineNumbers: 'off',  | 
 | 174 | +          glyphMargin: false,  | 
 | 175 | +          folding: false,  | 
 | 176 | +          lineDecorationsWidth: 0,  | 
 | 177 | +          lineNumbersMinChars: 0,  | 
 | 178 | +          renderLineHighlight: 'none',  | 
 | 179 | +          scrollbar: { vertical: 'hidden', horizontal: 'auto' },  | 
 | 180 | +          overviewRulerLanes: 0,  | 
 | 181 | +          wordWrap: 'on',  | 
 | 182 | +        }}  | 
 | 183 | +      />  | 
 | 184 | +    </div>  | 
 | 185 | +  )  | 
 | 186 | +}  | 
0 commit comments