import { useState, useEffect, useRef } from 'react' import { Box, Button, useMediaQuery } from '@mui/material' import { Save, FormatLineSpacing, Cancel, WrapText, Notes } from '@mui/icons-material' import { useI18nStore } from '../stores/i18n.store' import { useCommonStore } from '../stores/common.store' import { useRedisStateStore } from '../stores/redis-state.store' import { useSettingsStore } from '../stores/settings.store' import { useThemeStore } from '../stores/theme.store' import { isDarkTheme } from '../themes' import P3xrDialog from '../components/P3xrDialog' interface Props { open: boolean value: string hideFormatSave?: boolean onClose: (result?: { obj: string } | null) => void } export default function JsonEditorDialog({ open, value, hideFormatSave, onClose }: Props) { const strings = useI18nStore(s => s.strings) const { generalHandleError } = useCommonStore() const isReadonly = useRedisStateStore(s => s.connection)?.readonly === true const jsonFormat = useSettingsStore(s => s.jsonFormat) const themeKey = useThemeStore(s => s.themeKey) const isWide = useMediaQuery('(min-width: 960px)') const [isJson, setIsJson] = useState(false) const [lineWrap, setLineWrap] = useState(true) const editorRef = useRef(null) const viewRef = useRef(null) const wrapRef = useRef(null) const EditorViewRef = useRef(null) // Init CodeMirror when dialog opens — delay to ensure DOM is ready useEffect(() => { if (!open) return let obj: any try { obj = JSON.parse(value); setIsJson(true) } catch { setIsJson(false); return } const doc = JSON.stringify(obj, null, jsonFormat || 2) let view: any let cancelled = false const initEditor = async () => { // Wait for DOM to be ready while (!editorRef.current && !cancelled) { await new Promise(r => setTimeout(r, 50)) } if (cancelled || !editorRef.current) return const { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, highlightActiveLine, rectangularSelection, crosshairCursor } = await import('@codemirror/view') const { EditorState, Compartment } = await import('@codemirror/state') const { json } = await import('@codemirror/lang-json') const { defaultKeymap, history, historyKeymap } = await import('@codemirror/commands') const { bracketMatching, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting, defaultHighlightStyle } = await import('@codemirror/language') const { closeBrackets, closeBracketsKeymap } = await import('@codemirror/autocomplete') const { searchKeymap, highlightSelectionMatches } = await import('@codemirror/search') const { lintKeymap } = await import('@codemirror/lint') let themeExt: any if (isDarkTheme(themeKey)) { const { oneDark } = await import('@codemirror/theme-one-dark') themeExt = oneDark } else { const { githubLight } = await import('@uiw/codemirror-theme-github') themeExt = githubLight } const wrapCompartment = new Compartment() wrapRef.current = wrapCompartment EditorViewRef.current = EditorView view = new EditorView({ state: EditorState.create({ doc, extensions: [ lineNumbers(), highlightActiveLineGutter(), highlightSpecialChars(), history(), foldGutter(), drawSelection(), indentOnInput(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), bracketMatching(), closeBrackets(), rectangularSelection(), crosshairCursor(), highlightActiveLine(), highlightSelectionMatches(), keymap.of([ ...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...lintKeymap, ]), json(), themeExt, EditorView.theme({ '.cm-scroller': { 'overflow-x': 'scroll', 'scrollbar-width': 'auto' }, '.cm-scroller::-webkit-scrollbar': { height: '12px', display: 'block' }, '.cm-scroller::-webkit-scrollbar-track': { background: 'rgba(128,128,128,0.1)' }, '.cm-scroller::-webkit-scrollbar-thumb': { background: 'rgba(128,128,128,0.4)', 'border-radius': '6px' }, '.cm-scroller::-webkit-scrollbar-thumb:hover': { background: 'rgba(128,128,128,0.6)' }, }), wrapCompartment.of(EditorView.lineWrapping), EditorState.readOnly.of(isReadonly), ], }), parent: editorRef.current!, }) viewRef.current = view } initEditor() return () => { cancelled = true if (viewRef.current) { viewRef.current.destroy(); viewRef.current = null } } }, [open, value, themeKey]) const toggleWrap = () => { setLineWrap(prev => { const next = !prev if (viewRef.current && wrapRef.current && EditorViewRef.current) { viewRef.current.dispatch({ effects: wrapRef.current.reconfigure(next ? EditorViewRef.current.lineWrapping : []), }) } return next }) } const save = (format: boolean) => { try { const text = viewRef.current.state.doc.toString() const parsed = JSON.parse(text) const result = JSON.stringify(parsed, null, format ? (jsonFormat || 2) : 0) onClose({ obj: result }) } catch (e) { generalHandleError(e) } } if (!open) return null const minHeight = isWide ? `${Math.max(10, window.innerHeight - 100)}px` : '100%' return ( onClose(null)} contentPadding={!isJson} width="90vw" height="90vh" title={ {strings?.intention?.jsonViewEditor} } actions={ <> {isJson && !isReadonly && ( <> {!hideFormatSave && ( )} )} }> {isJson ? ( ) : ( {strings?.label?.jsonViewNotParsable} )} ) }