diff --git a/package-lock.json b/package-lock.json index e1ef2db4..d97ec5c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", @@ -2174,6 +2175,20 @@ "@lezer/json": "^1.0.0" } }, + "node_modules/@codemirror/lang-sql": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.11.3", "license": "MIT", diff --git a/package.json b/package.json index e348d800..e82f643a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", diff --git a/public/data/commits.json b/public/data/commits.json index 0d91c398..6fcf2bcb 100644 --- a/public/data/commits.json +++ b/public/data/commits.json @@ -1,5 +1,16 @@ { "changelog": [ + { + "date": "2025-10-15", + "changes": [ + { + "datetime": "2025-10-15T22:32:00+07:00", + "type": "enhancement", + "title": "Object Editor Preview Mode & Mobile Optimizations", + "description": "Added Preview/Edit mode toggle to Object Editor's Tree View with read-only preview mode for better data visibility. Implemented clickable nested data values in preview mode - no need to switch to edit mode to explore JSON/serialized structures. Fixed mobile responsiveness for all data load notices across Object and Table editors. Improved Table Editor sticky header positioning and export section layout on mobile. Moved horizontal overflow handling from ObjectEditor to StructuredEditor component for consistent behavior in main view and nested modals. Updated TableEditor object modal footer layout and changed 'Apply Changes' to 'Save Changes' for UX consistency." + } + ] + }, { "date": "2025-10-14", "changes": [ diff --git a/src/components/CodeMirrorEditor.js b/src/components/CodeMirrorEditor.js index 40c51b1a..dd37e5dc 100644 --- a/src/components/CodeMirrorEditor.js +++ b/src/components/CodeMirrorEditor.js @@ -1,8 +1,9 @@ -import React, { useRef, useEffect, useState } from 'react'; +import React, { useRef, useEffect, useState, useCallback } from 'react'; import { EditorView } from '@codemirror/view'; import { EditorState } from '@codemirror/state'; import { basicSetup } from 'codemirror'; import { json } from '@codemirror/lang-json'; +import { sql } from '@codemirror/lang-sql'; import { oneDark } from '@codemirror/theme-one-dark'; import { Maximize2, Minimize2 } from 'lucide-react'; @@ -13,7 +14,8 @@ const CodeMirrorEditor = ({ className = '', language = 'json', maxLines = 12, - showToggle = true + showToggle = true, + cardRef = null // Reference to the card header for scroll target }) => { const editorRef = useRef(null); const viewRef = useRef(null); @@ -38,13 +40,26 @@ const CodeMirrorEditor = ({ return () => observer.disconnect(); }, []); + // Detect if content is single-line (minified) + const isSingleLine = (value || '').split('\n').length === 1; + // Initialize editor only once useEffect(() => { if (!editorRef.current || viewRef.current) return; + // Language extension + let langExtension = []; + if (language === 'json') { + langExtension = [json()]; + } else if (language === 'sql') { + langExtension = [sql()]; + } + const extensions = [ basicSetup, - language === 'json' ? json() : [], + ...langExtension, + // Enable line wrapping for single-line content + ...(isSingleLine ? [EditorView.lineWrapping] : []), EditorView.theme({ '&': { fontSize: '14px', @@ -58,6 +73,13 @@ const CodeMirrorEditor = ({ }, '.cm-editor': { borderRadius: '6px', + }, + '.cm-scroller': { + overflowY: 'auto', + height: '100%', + }, + '.cm-line': { + wordBreak: isSingleLine ? 'break-word' : 'normal', } }), EditorView.updateListener.of((update) => { @@ -80,6 +102,25 @@ const CodeMirrorEditor = ({ viewRef.current = view; + // Apply styles immediately after editor creation + setTimeout(() => { + const editorElement = editorRef.current?.querySelector('.cm-editor'); + const scrollerElement = editorRef.current?.querySelector('.cm-scroller'); + + if (editorElement) { + editorElement.style.height = '350px'; + editorElement.style.maxHeight = '350px'; + } + + if (scrollerElement) { + scrollerElement.style.overflowY = 'auto'; + scrollerElement.style.overflowX = isSingleLine ? 'hidden' : 'auto'; + scrollerElement.style.height = '100%'; + } + + // No manual wrapping needed - EditorView.lineWrapping handles it + }, 0); + return () => { if (viewRef.current) { viewRef.current.destroy(); @@ -87,13 +128,15 @@ const CodeMirrorEditor = ({ } }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isDark]); // Only recreate on theme change + }, [isDark, isSingleLine]); // Recreate when theme or line count changes - // Handle height changes without recreating editor - useEffect(() => { - if (!viewRef.current) return; + // Apply overflow and height styles + const applyEditorStyles = useCallback(() => { + if (!viewRef.current || !editorRef.current) return; + + const editorElement = editorRef.current.querySelector('.cm-editor'); + const scrollerElement = editorRef.current.querySelector('.cm-scroller'); - const editorElement = editorRef.current?.querySelector('.cm-editor'); if (editorElement) { if (isExpanded) { editorElement.style.height = 'auto'; @@ -103,8 +146,19 @@ const CodeMirrorEditor = ({ editorElement.style.maxHeight = '350px'; } } + + // Handle scrolling + if (scrollerElement) { + scrollerElement.style.overflowY = isExpanded ? 'visible' : 'auto'; + scrollerElement.style.height = '100%'; + } }, [isExpanded]); + // Apply styles on mount, expand/collapse, and content changes + useEffect(() => { + applyEditorStyles(); + }, [applyEditorStyles]); + // Update content when value changes externally useEffect(() => { if (viewRef.current && value !== viewRef.current.state.doc.toString()) { @@ -116,8 +170,11 @@ const CodeMirrorEditor = ({ } }); viewRef.current.dispatch(transaction); + + // Re-apply styles after content change (e.g., tab switch) + setTimeout(() => applyEditorStyles(), 10); } - }, [value]); + }, [value, applyEditorStyles]); return (
@@ -132,7 +189,12 @@ const CodeMirrorEditor = ({ onClick={() => { setIsExpanded(!isExpanded); setTimeout(() => { - window.scrollTo({ top: 0, behavior: 'smooth' }); + // Scroll to card header if cardRef is provided, otherwise scroll to top + if (cardRef?.current) { + cardRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } else { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } }, 50); }} className="absolute bottom-2 right-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-600 shadow-sm z-10" diff --git a/src/components/StructuredEditor.js b/src/components/StructuredEditor.js index 31513ef3..8e447687 100644 --- a/src/components/StructuredEditor.js +++ b/src/components/StructuredEditor.js @@ -1,13 +1,17 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces, Edit3, X } from 'lucide-react'; +import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces, Edit3, X, Eye, Pencil } from 'lucide-react'; -const StructuredEditor = ({ onDataChange, initialData = {} }) => { +const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyProp = false }) => { const [data, setData] = useState(initialData); const [expandedNodes, setExpandedNodes] = useState(new Set(['root'])); const [fieldTypes, setFieldTypes] = useState({}); // Track intended types for fields const isInternalUpdate = useRef(false); const [nestedEditModal, setNestedEditModal] = useState(null); // { path, value, type: 'json' | 'serialized' } const [nestedData, setNestedData] = useState(null); + const [editMode, setEditMode] = useState(false); // Internal edit mode state + + // Use internal editMode if readOnlyProp is not explicitly set, otherwise use prop + const readOnly = readOnlyProp !== false ? readOnlyProp : !editMode; // Update internal data when initialData prop changes (but not from internal updates) useEffect(() => { @@ -533,7 +537,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => { {canExpand && ( - - {value.toString()} - + {readOnly ? ( + + {value.toString()} + + ) : ( + <> + + + {value.toString()} + + + )}
) : (
- {(fieldTypes[path] === 'longtext' || (typeof value === 'string' && value.includes('\n'))) ? ( -