feat: Object Editor Preview Mode & Mobile Optimizations

Major Enhancements:
- Added Preview/Edit mode toggle to StructuredEditor component
  * Preview mode: Read-only view with full text visibility
  * Edit mode: Full editing capabilities with all controls
  * Toggle positioned below title, responsive on mobile
  * Works in both main ObjectEditor view and nested modals

- Clickable nested data in Preview mode
  * JSON/serialized values are blue and clickable
  * Opens modal directly without switching to Edit mode
  * Hover effects and tooltips for better UX
  * No longer need edit mode just to explore structure

Mobile Responsiveness Improvements:
- Fixed all data load notices in ObjectEditor (URL, Paste, Open tabs)
- Fixed all data load notices in TableEditor (URL, Paste, Open tabs)
- Notices now stack vertically on mobile with proper spacing
- Added break-words for long text, whitespace-nowrap for buttons
- Dark mode colors added for better visibility

Table Editor Fixes:
- Fixed sticky header showing row underneath (top-[-1px])
- Made Export section header mobile responsive
- Updated object modal footer layout:
  * Format info and properties combined on single line
  * Buttons moved to separate row below
  * Changed 'Apply Changes' to 'Save Changes' for consistency

StructuredEditor Improvements:
- Moved overflow-x handling from ObjectEditor to StructuredEditor
- Now works consistently in main view and nested modals
- Long strings scroll horizontally everywhere
- 'Add Property' button hidden in Preview mode
- Improved chevron colors for dark mode visibility

Technical Changes:
- StructuredEditor now manages its own editMode state
- readOnly prop can still be passed from parent if needed
- Proper conditional rendering for all UI elements
- Consistent mobile-first responsive design patterns
This commit is contained in:
dwindown
2025-10-15 22:40:57 +07:00
parent f6c19e855d
commit df0fb5d22a
9 changed files with 929 additions and 600 deletions

15
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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": [

View File

@@ -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 (
<div className={`relative ${className}`}>
@@ -132,7 +189,12 @@ const CodeMirrorEditor = ({
onClick={() => {
setIsExpanded(!isExpanded);
setTimeout(() => {
// 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"

View File

@@ -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 && (
<button
onClick={() => toggleNode(path)}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-gray-700 dark:text-gray-300"
>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
@@ -558,6 +562,11 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
// Object properties: icon + editable key + colon (compact)
<>
{getTypeIcon(value)}
{readOnly ? (
<span className="px-2 py-1 text-sm text-gray-900 dark:text-gray-100 font-mono">
{key}
</span>
) : (
<input
type="text"
defaultValue={key}
@@ -576,13 +585,20 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
placeholder="Property name"
style={{width: '120px'}} // Fixed width for consistency
/>
<span className="text-gray-500 hidden sm:inline">:</span>
)}
<span className="text-gray-500 inline">:</span>
</>
)}
{!canExpand ? (
typeof value === 'boolean' ? (
<div className="flex-1 flex items-center space-x-2">
{readOnly ? (
<span className="text-sm text-gray-900 dark:text-gray-100 font-mono">
{value.toString()}
</span>
) : (
<>
<button
onClick={() => updateValue((!value).toString(), path)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
@@ -598,9 +614,27 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
<span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
{value.toString()}
</span>
</>
)}
</div>
) : (
<div className="flex-1 flex items-center gap-2">
{readOnly ? (
typeof value === 'string' && detectNestedData(value) ? (
<span
onClick={() => openNestedEditor(value, path)}
className="px-2 py-1 text-sm text-blue-600 dark:text-blue-400 font-mono break-all cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded transition-colors"
title={`Click to view nested ${detectNestedData(value).type} data`}
>
{getDisplayValue(value)}
</span>
) : (
<span className="px-2 py-1 text-sm text-gray-900 dark:text-gray-100 font-mono break-all">
{getDisplayValue(value)}
</span>
)
) : (
<>
{(fieldTypes[path] === 'longtext' || (typeof value === 'string' && value.includes('\n'))) ? (
<textarea
value={getDisplayValue(value)}
@@ -627,6 +661,8 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
<Edit3 className="h-4 w-4" />
</button>
)}
</>
)}
</div>
)
) : (
@@ -635,6 +671,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
</span>
)}
{!readOnly && (
<div className="flex items-center space-x-2 sm:space-x-2">
<select
value={
@@ -667,6 +704,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
<Minus className="h-4 w-4" />
</button>
</div>
)}
</div>
</div>
@@ -677,6 +715,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
{value.map((item, index) =>
renderValue(item, index.toString(), `${path}.${index}`, path)
)}
{!readOnly && (
<button
onClick={() => addArrayItem(value, path)}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
@@ -684,12 +723,14 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
<Plus className="h-4 w-4" />
<span>Add Item</span>
</button>
)}
</>
) : (
<>
{Object.entries(value).map(([k, v]) =>
renderValue(v, k, `${path}.${k}`, path)
)}
{!readOnly && (
<button
onClick={() => addProperty(value, path)}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
@@ -697,6 +738,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
<Plus className="h-4 w-4" />
<span>Add Property</span>
</button>
)}
</>
)}
</div>
@@ -708,10 +750,42 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
return (
<div className="min-h-96 w-full">
<div className="mb-4">
<div className="flex flex-col gap-3 mb-3">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
{/* Mode Toggle - Below title on mobile, inline on desktop */}
{readOnlyProp === false && (
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm w-fit">
<button
onClick={() => setEditMode(false)}
className={`flex items-center gap-2 px-3 py-1.5 text-xs font-medium transition-colors ${
!editMode
? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
<Eye className="h-3.5 w-3.5" />
<span>Preview</span>
</button>
<button
onClick={() => setEditMode(true)}
className={`flex items-center gap-2 px-3 py-1.5 text-xs font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${
editMode
? 'bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-300'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
<Pencil className="h-3.5 w-3.5" />
<span>Edit</span>
</button>
</div>
)}
</div>
</div>
<div className="overflow-x-hidden">
<div className="w-full overflow-hidden">
<div className="w-full overflow-x-auto">
<div className="min-w-max">
{Object.keys(data).length === 0 ? (
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
<Braces className="h-12 w-12 mx-auto mb-2 opacity-50" />
@@ -724,6 +798,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
)}
{/* Root level Add Property button */}
{!readOnly && (
<button
onClick={() => addProperty(data, 'root')}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
@@ -731,6 +806,9 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
<Plus className="h-4 w-4" />
<span>Add Property</span>
</button>
)}
</div>
</div>
</div>
{/* Nested Data Editor Modal */}
@@ -749,7 +827,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
</div>
<button
onClick={closeNestedEditor}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded self-start"
>
<X className="h-5 w-5" />
</button>

View File

@@ -50,7 +50,7 @@ const Home = () => {
<span className="animate-pulse">_</span>
</div>
<h1 className="text-5xl hidden md:text-7xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent mb-6">
<h1 className="text-5xl md:text-7xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent mb-6">
{SITE_CONFIG.title}
</h1>
@@ -222,6 +222,7 @@ const Home = () => {
</div>
</div>
</div>
</div>
</>
);
};

View File

@@ -844,7 +844,7 @@ const InvoiceEditor = () => {
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://api.telegram.org/bot<token>/getMe"
placeholder="https://your-url.com/invoice/your-invoice"
className="tool-input w-full"
onKeyPress={(e) => e.key === 'Enter' && handleUrlFetch()}
/>
@@ -858,7 +858,7 @@ const InvoiceEditor = () => {
</button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.
Enter any URL that returns exported JSON data from your previous invoice work.
</p>
</div>
)}

View File

@@ -4,33 +4,10 @@ import ToolLayout from '../components/ToolLayout';
import StructuredEditor from '../components/StructuredEditor';
import MindmapView from '../components/MindmapView';
import PostmanTable from '../components/PostmanTable';
import CodeEditor from '../components/CodeEditor';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
// Hook to detect dark mode
const useDarkMode = () => {
const [isDark, setIsDark] = useState(() => {
return document.documentElement.classList.contains('dark');
});
useEffect(() => {
const observer = new MutationObserver(() => {
setIsDark(document.documentElement.classList.contains('dark'));
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
return isDark;
};
const ObjectEditor = () => {
const isDark = useDarkMode();
const exportCardRef = useRef(null);
const [structuredData, setStructuredData] = useState({});
// Sync structured data to localStorage for navigation guard
@@ -58,9 +35,53 @@ const ObjectEditor = () => {
const fileInputRef = useRef(null);
const [pasteCollapsed, setPasteCollapsed] = useState(false);
const [pasteDataSummary, setPasteDataSummary] = useState(null);
const [urlDataSummary, setUrlDataSummary] = useState(null);
const [fileDataSummary, setFileDataSummary] = useState(null);
const [outputExpanded, setOutputExpanded] = useState(false);
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
// Button feedback states
const [copiedButton, setCopiedButton] = useState(null);
const [downloadedButton, setDownloadedButton] = useState(null);
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error("Failed to copy:", err);
}
};
// Export functions
const getExportData = (format) => {
if (Object.keys(structuredData).length === 0) return "";
switch (format) {
case "json":
// Convert data to use column names instead of column IDs
// Use Object.fromEntries with columns.map to preserve column order
const jsonData = structuredData;
return jsonFormat === "pretty"
? JSON.stringify(jsonData, null, 2)
: JSON.stringify(jsonData);
default:
return "";
}
};
const downloadFile = (content, filename, mimeType) => {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Helper function to check if user has data that would be lost
const hasUserData = () => {
return Object.keys(structuredData).length > 0;
@@ -549,29 +570,39 @@ const ObjectEditor = () => {
// Handle file import (auto-load, same as Table/Invoice Editor)
const handleFileImport = (event) => {
const file = event.target.files[0];
if (file) {
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
const detection = detectInputFormat(content);
if (detection.valid) {
setStructuredData(detection.data);
setCreateNewCompleted(true);
generateOutputs(detection.data);
setInputText(content);
setInputFormat(detection.format);
setInputValid(true);
setError('');
setCreateNewCompleted(true);
// Set file data summary
setFileDataSummary({
format: detection.format,
size: content.length,
properties: Object.keys(detection.data).length,
filename: file.name
});
// Stay on Open tab - don't switch to paste
} else {
setError(detection.error || 'Invalid format. Please enter valid JSON or PHP serialized data.');
}
} catch (err) {
setError('Failed to read file: ' + err.message);
setError(detection.error || 'Invalid file format');
setFileDataSummary(null);
}
};
reader.readAsText(file);
}
};
// Fetch data from URL
const handleFetchData = async () => {
if (!fetchUrl.trim()) {
@@ -596,36 +627,38 @@ const ObjectEditor = () => {
}
const contentType = response.headers.get('content-type');
const text = await response.text();
let data;
if (!contentType || !contentType.includes('application/json')) {
// Try to parse as JSON anyway, some APIs don't set correct content-type
const text = await response.text();
try {
const data = JSON.parse(text);
setStructuredData(data);
generateOutputs(data);
setInputText(JSON.stringify(data, null, 2));
setInputFormat('JSON');
setInputValid(true);
setCreateNewCompleted(true);
data = JSON.parse(text);
} catch {
throw new Error('Response is not valid JSON. Content-Type: ' + (contentType || 'unknown'));
}
} else {
const data = await response.json();
data = JSON.parse(text);
}
setStructuredData(data);
generateOutputs(data);
setInputText(JSON.stringify(data, null, 2));
setInputFormat('JSON');
setInputValid(true);
setCreateNewCompleted(true);
}
// Set URL data summary
setUrlDataSummary({
format: 'JSON',
size: text.length,
properties: Object.keys(data).length,
url: url
});
} catch (err) {
console.error('Fetch error:', err);
if (err.name === 'TypeError' && err.message.includes('fetch')) {
setError('Network error: Unable to fetch data. Check the URL and try again.');
} else {
setError(`Fetch failed: ${err.message}`);
}
setError(`Failed to fetch data: ${err.message}`);
setUrlDataSummary(null);
} finally {
setFetching(false);
}
@@ -779,6 +812,21 @@ const ObjectEditor = () => {
{/* URL Tab Content */}
{activeTab === 'url' && (
urlDataSummary ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.properties} {urlDataSummary.properties === 1 ? 'property' : 'properties'})
</span>
<button
onClick={() => setUrlDataSummary(null)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Fetch New URL ▼
</button>
</div>
</div>
) : (
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
@@ -803,19 +851,20 @@ const ObjectEditor = () => {
Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.
</p>
</div>
)
)}
{/* Paste Tab Content */}
{activeTab === 'paste' && (
pasteCollapsed ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex items-center justify-between">
<span className="text-sm text-green-700 dark:text-green-300">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ Object loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.properties} {pasteDataSummary.properties === 1 ? 'property' : 'properties'})
</span>
<button
onClick={() => setPasteCollapsed(false)}
className="text-sm text-blue-600 hover:underline"
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Edit Input ▼
</button>
@@ -868,6 +917,21 @@ const ObjectEditor = () => {
{/* Open Tab Content */}
{activeTab === 'open' && (
fileDataSummary ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ File loaded: {fileDataSummary.format} ({fileDataSummary.size.toLocaleString()} chars, {fileDataSummary.properties} {fileDataSummary.properties === 1 ? 'property' : 'properties'}) - {fileDataSummary.filename}
</span>
<button
onClick={() => setFileDataSummary(null)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Upload New File ▼
</button>
</div>
</div>
) : (
<div className="space-y-3">
<input
ref={fileInputRef}
@@ -882,6 +946,7 @@ const ObjectEditor = () => {
</p>
</div>
</div>
)
)}
</div>
)}
@@ -900,7 +965,7 @@ const ObjectEditor = () => {
Object Editor
</h3>
{/* View Mode Tabs - Moved to right */}
{/* View Mode Tabs */}
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<button
onClick={() => setViewMode('visual')}
@@ -911,7 +976,7 @@ const ObjectEditor = () => {
}`}
>
<Edit3 className="h-4 w-4" />
<span className="hidden sm:inline">Visual Editor</span>
<span className="hidden sm:inline">Tree</span>
</button>
<button
onClick={() => setViewMode('mindmap')}
@@ -922,7 +987,7 @@ const ObjectEditor = () => {
}`}
>
<Workflow className="h-4 w-4" />
<span className="hidden sm:inline">Mindmap View</span>
<span className="hidden sm:inline">Mindmap</span>
</button>
<button
onClick={() => setViewMode('table')}
@@ -933,7 +998,7 @@ const ObjectEditor = () => {
}`}
>
<Table className="h-4 w-4" />
<span className="hidden sm:inline">Table View</span>
<span className="hidden sm:inline">Table</span>
</button>
</div>
</div>
@@ -954,16 +1019,12 @@ const ObjectEditor = () => {
) : (
<>
{viewMode === 'visual' && (
<div className="w-full overflow-hidden">
<div className="w-full overflow-x-auto p-4">
<div className="min-w-max">
<div className="p-4">
<StructuredEditor
initialData={structuredData}
onDataChange={handleStructuredDataChange}
/>
</div>
</div>
</div>
)}
{viewMode === 'mindmap' && (
@@ -987,6 +1048,7 @@ const ObjectEditor = () => {
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
{/* Export Header - Collapsible */}
<div
ref={exportCardRef}
onClick={() => setOutputExpanded(!outputExpanded)}
className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
@@ -1035,13 +1097,14 @@ const ObjectEditor = () => {
<div className="p-4">
{activeExportTab === 'json' && (
<div className="space-y-3">
<CodeEditor
<CodeMirrorEditor
value={jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}')}
language="json"
readOnly={true}
height="300px"
maxLines={12}
showToggle={true}
className="w-full"
theme={isDark ? 'dark' : 'light'}
cardRef={exportCardRef}
/>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -1070,26 +1133,27 @@ const ObjectEditor = () => {
<button
onClick={() => {
const content = jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}');
navigator.clipboard.writeText(content);
copyToClipboard(content);
setCopiedButton('json');
setTimeout(() => setCopiedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded transition-colors"
>
Copy
{copiedButton === 'json' ? '✓ Copied!' : 'Copy'}
</button>
<button
onClick={() => {
const content = jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}');
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'object-data.json';
a.click();
URL.revokeObjectURL(url);
downloadFile(
getExportData("json"),
"object-data.json",
"application/json",
);
setDownloadedButton('json');
setTimeout(() => setDownloadedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded transition-colors"
>
Download
{downloadedButton === 'json' ? '✓ Downloaded!' : 'Download'}
</button>
</div>
</div>
@@ -1098,23 +1162,26 @@ const ObjectEditor = () => {
{activeExportTab === 'php' && (
<div className="space-y-3">
<CodeEditor
<CodeMirrorEditor
value={outputs.serialized || 'a:0:{}'}
language="javascript"
readOnly={true}
height="300px"
maxLines={12}
showToggle={true}
className="w-full"
theme={isDark ? 'dark' : 'light'}
cardRef={exportCardRef}
/>
<div className="flex justify-end gap-2">
<button
onClick={() => {
const content = outputs.serialized || 'a:0:{}';
navigator.clipboard.writeText(content);
copyToClipboard(content);
setCopiedButton('php');
setTimeout(() => setCopiedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded transition-colors"
>
Copy
{copiedButton === 'php' ? '✓ Copied!' : 'Copy'}
</button>
<button
onClick={() => {
@@ -1124,12 +1191,16 @@ const ObjectEditor = () => {
const a = document.createElement('a');
a.href = url;
a.download = 'object-data.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
setDownloadedButton('php');
setTimeout(() => setDownloadedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded transition-colors"
>
Download
{downloadedButton === 'php' ? '✓ Downloaded!' : 'Download'}
</button>
</div>
</div>

View File

@@ -1,35 +1,12 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { Plus, Upload, FileText, Globe, Download, X, Table, Trash2, Database, Braces, Code, Eye, Minimize2, Maximize2, Search, ArrowUpDown, AlertTriangle, Edit3, ChevronUp, ChevronDown } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CodeEditor from '../components/CodeEditor';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
import StructuredEditor from "../components/StructuredEditor";
import Papa from "papaparse";
// Hook to detect dark mode
const useDarkMode = () => {
const [isDark, setIsDark] = useState(() => {
return document.documentElement.classList.contains('dark');
});
useEffect(() => {
const observer = new MutationObserver(() => {
setIsDark(document.documentElement.classList.contains('dark'));
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
return isDark;
};
const TableEditor = () => {
const isDark = useDarkMode();
const exportCardRef = useRef(null);
const [data, setData] = useState([]);
const [columns, setColumns] = useState([]);
@@ -69,6 +46,8 @@ const TableEditor = () => {
const [createNewCompleted, setCreateNewCompleted] = useState(false); // Track if user completed Create New step
const [pasteCollapsed, setPasteCollapsed] = useState(false); // Track if paste input is collapsed
const [pasteDataSummary, setPasteDataSummary] = useState(null); // Summary of pasted data
const [urlDataSummary, setUrlDataSummary] = useState(null); // Summary of URL fetched data
const [fileDataSummary, setFileDataSummary] = useState(null); // Summary of file uploaded data
const [exportExpanded, setExportExpanded] = useState(false); // Track if export section is expanded
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false); // Track if usage tips is expanded
@@ -76,6 +55,10 @@ const TableEditor = () => {
const [sqlTableName, setSqlTableName] = useState(""); // Table name for SQL export
const [sqlPrimaryKey, setSqlPrimaryKey] = useState(""); // Primary key column for SQL export
// Button feedback states
const [copiedButton, setCopiedButton] = useState(null);
const [downloadedButton, setDownloadedButton] = useState(null);
// Helper function to check if user has data that would be lost
const hasUserData = () => {
// Check if there are multiple tables (imported data)
@@ -650,7 +633,6 @@ const TableEditor = () => {
// Parse CSV/TSV data
const parseData = (text, hasHeaders = true) => {
try {
const result = Papa.parse(text.trim(), {
header: false,
skipEmptyLines: true,
@@ -696,9 +678,7 @@ const TableEditor = () => {
setColumns(headers);
setData(tableData);
setError("");
} catch (err) {
setError(`Failed to parse data: ${err.message}`);
}
return tableData.length; // Return actual row count
};
// Parse SQL data
@@ -941,7 +921,6 @@ const TableEditor = () => {
// Parse JSON data
const parseJsonData = (text) => {
try {
const jsonData = JSON.parse(text);
if (!Array.isArray(jsonData)) {
@@ -972,9 +951,7 @@ const TableEditor = () => {
setColumns(headers);
setData(tableData);
setError("");
} catch (err) {
setError(`Failed to parse JSON: ${err.message}`);
}
return tableData.length; // Return row count for summary
};
// Handle text input
@@ -987,15 +964,14 @@ const TableEditor = () => {
const trimmed = inputText.trim();
let format = '';
let success = false;
let rowCount = 0;
try {
// Try to detect format
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
// JSON array
parseJsonData(trimmed);
rowCount = parseJsonData(trimmed);
format = 'JSON';
success = true;
} else if (
trimmed.toLowerCase().includes("insert into") &&
trimmed.toLowerCase().includes("values")
@@ -1003,24 +979,25 @@ const TableEditor = () => {
// SQL INSERT statements
parseSqlData(trimmed);
format = 'SQL';
success = true;
// Get row count from state after parse
rowCount = data.length;
} else {
// CSV/TSV
parseData(trimmed, useFirstRowAsHeader);
format = trimmed.includes('\t') ? 'TSV' : 'CSV';
success = true;
// Get row count from state after parse
rowCount = data.length;
}
// If successful, collapse input and show summary
if (success && data.length > 0) {
// Collapse input and show summary
setPasteDataSummary({
format: format,
size: inputText.length,
rows: data.length
rows: rowCount || data.length // Use rowCount if available, fallback to data.length
});
setPasteCollapsed(true);
setCreateNewCompleted(true);
setError('');
}
} catch (err) {
// Keep input expanded on error
setPasteCollapsed(false);
@@ -1047,14 +1024,28 @@ const TableEditor = () => {
const contentType = response.headers.get("content-type") || "";
const text = await response.text();
let format = '';
let rowCount = 0;
if (contentType.includes("application/json") || url.includes(".json")) {
parseJsonData(text);
rowCount = parseJsonData(text);
format = 'JSON';
} else {
parseData(text, useFirstRowAsHeader);
rowCount = parseData(text, useFirstRowAsHeader);
format = text.includes('\t') ? 'TSV' : 'CSV';
}
// Set summary for URL fetch
setUrlDataSummary({
format: format,
size: text.length,
rows: rowCount,
url: url.trim()
});
setCreateNewCompleted(true);
} catch (err) {
setError(`Failed to fetch data: ${err.message}`);
setUrlDataSummary(null);
} finally {
setIsLoading(false);
}
@@ -1136,17 +1127,47 @@ const TableEditor = () => {
reader.onload = (e) => {
try {
const content = e.target.result;
let format = '';
let rowCount = 0;
// Check if it's SQL file for multi-table support
if (file.name.toLowerCase().endsWith(".sql")) {
initializeTablesFromSQL(content, file.name);
format = 'SQL';
// For SQL, we need to wait for state update, use a timeout
setTimeout(() => {
setFileDataSummary({
format: format,
size: content.length,
rows: data.length,
filename: file.name
});
}, 100);
} else if (file.name.toLowerCase().endsWith(".json")) {
rowCount = parseJsonData(content);
format = 'JSON';
setFileDataSummary({
format: format,
size: content.length,
rows: rowCount,
filename: file.name
});
} else {
// Fallback to single-table parsing
parseData(content);
rowCount = parseData(content, useFirstRowAsHeader);
format = file.name.toLowerCase().endsWith(".tsv") ? 'TSV' : 'CSV';
setFileDataSummary({
format: format,
size: content.length,
rows: rowCount,
filename: file.name
});
}
setCreateNewCompleted(true);
} catch (err) {
console.error("❌ File upload error:", err);
setError("Failed to read file: " + err.message);
setFileDataSummary(null);
} finally {
setIsLoading(false);
}
@@ -1995,6 +2016,21 @@ const TableEditor = () => {
)}
{activeTab === "url" && (
urlDataSummary ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.rows} rows)
</span>
<button
onClick={() => setUrlDataSummary(null)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Fetch New URL ▼
</button>
</div>
</div>
) : (
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
@@ -2033,18 +2069,19 @@ const TableEditor = () => {
Use first row as column headers (for CSV/TSV)
</label>
</div>
)
)}
{activeTab === "paste" && (
pasteCollapsed ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex items-center justify-between">
<span className="text-sm text-green-700 dark:text-green-300">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ Data loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.rows} rows)
</span>
<button
onClick={() => setPasteCollapsed(false)}
className="text-sm text-blue-600 hover:underline"
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Edit Input ▼
</button>
@@ -2092,6 +2129,21 @@ const TableEditor = () => {
)}
{activeTab === "upload" && (
fileDataSummary ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ File loaded: {fileDataSummary.format} ({fileDataSummary.size.toLocaleString()} chars, {fileDataSummary.rows} rows) - {fileDataSummary.filename}
</span>
<button
onClick={() => setFileDataSummary(null)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Upload New File ▼
</button>
</div>
</div>
) : (
<div className="space-y-3">
<input
type="file"
@@ -2116,6 +2168,7 @@ const TableEditor = () => {
</p>
</div>
</div>
)
)}
</div>
)}
@@ -2288,7 +2341,7 @@ const TableEditor = () => {
style={{ maxWidth: '100%' }}
>
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-[-1px] z-10">
<tr>
<th
className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border-r border-gray-200 dark:border-gray-600 ${
@@ -2663,10 +2716,11 @@ const TableEditor = () => {
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
{/* Export Header - Collapsible */}
<div
ref={exportCardRef}
onClick={() => setExportExpanded(!exportExpanded)}
className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Download className="h-5 w-5 text-green-600 dark:text-green-400" />
Export Results
@@ -2744,12 +2798,14 @@ const TableEditor = () => {
<div className="p-4">
{exportTab === "json" && (
<div className="space-y-3">
<CodeEditor
<CodeMirrorEditor
value={getExportData("json")}
language="json"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
maxLines={12}
showToggle={true}
className="w-full"
cardRef={exportCardRef}
/>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -2776,22 +2832,28 @@ const TableEditor = () => {
</div>
<div className="flex gap-2">
<button
onClick={() => copyToClipboard(getExportData("json"))}
onClick={() => {
copyToClipboard(getExportData("json"));
setCopiedButton('json');
setTimeout(() => setCopiedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded transition-colors"
>
Copy
{copiedButton === 'json' ? '✓ Copied!' : 'Copy'}
</button>
<button
onClick={() =>
onClick={() => {
downloadFile(
getExportData("json"),
"table-data.json",
"application/json",
)
}
);
setDownloadedButton('json');
setTimeout(() => setDownloadedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded transition-colors"
>
Download
{downloadedButton === 'json' ? '✓ Downloaded!' : 'Download'}
</button>
</div>
</div>
@@ -2800,31 +2862,39 @@ const TableEditor = () => {
{exportTab === "csv" && (
<div className="space-y-3">
<CodeEditor
<CodeMirrorEditor
value={getExportData("csv")}
language="javascript"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
maxLines={12}
showToggle={true}
className="w-full"
cardRef={exportCardRef}
/>
<div className="flex justify-end gap-2">
<button
onClick={() => copyToClipboard(getExportData("csv"))}
onClick={() => {
copyToClipboard(getExportData("csv"));
setCopiedButton('csv');
setTimeout(() => setCopiedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded transition-colors"
>
Copy
{copiedButton === 'csv' ? '✓ Copied!' : 'Copy'}
</button>
<button
onClick={() =>
onClick={() => {
downloadFile(
getExportData("csv"),
"table-data.csv",
"text/csv",
)
}
);
setDownloadedButton('csv');
setTimeout(() => setDownloadedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded transition-colors"
>
Download
{downloadedButton === 'csv' ? '✓ Downloaded!' : 'Download'}
</button>
</div>
</div>
@@ -2832,31 +2902,39 @@ const TableEditor = () => {
{exportTab === "tsv" && (
<div className="space-y-3">
<CodeEditor
<CodeMirrorEditor
value={getExportData("tsv")}
language="javascript"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
maxLines={12}
showToggle={true}
className="w-full"
cardRef={exportCardRef}
/>
<div className="flex justify-end gap-2">
<button
onClick={() => copyToClipboard(getExportData("tsv"))}
onClick={() => {
copyToClipboard(getExportData("tsv"));
setCopiedButton('tsv');
setTimeout(() => setCopiedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded transition-colors"
>
Copy
{copiedButton === 'tsv' ? '✓ Copied!' : 'Copy'}
</button>
<button
onClick={() =>
onClick={() => {
downloadFile(
getExportData("tsv"),
"table-data.tsv",
"text/tab-separated-values",
)
}
);
setDownloadedButton('tsv');
setTimeout(() => setDownloadedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded transition-colors"
>
Download
{downloadedButton === 'tsv' ? '✓ Downloaded!' : 'Download'}
</button>
</div>
</div>
@@ -2907,12 +2985,14 @@ const TableEditor = () => {
</div>
</div>
<CodeEditor
<CodeMirrorEditor
value={getExportData("sql")}
language="javascript"
language="sql"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
maxLines={12}
showToggle={true}
className="w-full"
cardRef={exportCardRef}
/>
{/* Intelligent Schema Analysis */}
@@ -3046,22 +3126,28 @@ const TableEditor = () => {
<div className="flex justify-end gap-2">
<button
onClick={() => copyToClipboard(getExportData("sql"))}
onClick={() => {
copyToClipboard(getExportData("sql"));
setCopiedButton('sql');
setTimeout(() => setCopiedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded transition-colors"
>
Copy
{copiedButton === 'sql' ? '✓ Copied!' : 'Copy'}
</button>
<button
onClick={() =>
onClick={() => {
downloadFile(
getExportData("sql"),
`${sqlTableName || currentTable || originalFileName || "database"}.sql`,
`${sqlTableName || "database"}.sql`,
"application/sql",
)
}
);
setDownloadedButton('sql');
setTimeout(() => setDownloadedButton(null), 2000);
}}
className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded transition-colors"
>
Download
{downloadedButton === 'sql' ? '✓ Downloaded!' : 'Download'}
</button>
</div>
</div>
@@ -3570,10 +3656,26 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
Row {modal.rowIndex} Column: {modal.columnName} Format:{" "}
{modal.format.type.replace("_", " ")}
</p>
{/* Format info */}
<div className="text-sm">
<span
className={`${isValid ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}`}
>
{isValid ? "✓ Valid" : "✗ Invalid"}{" "}
{modal.format.type.replace("_", " ")}
</span>
{isValid &&
structuredData &&
typeof structuredData === "object" && (
<span className="text-gray-600 dark:text-gray-400">
{" • "}{Object.keys(structuredData).length} properties
</span>
)}
</div>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 self-start"
>
<X className="h-6 w-6" />
</button>
@@ -3633,23 +3735,11 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
{/* Footer */}
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700 flex-shrink-0">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<span
className={`text-sm ${isValid ? "text-green-600" : "text-red-600"}`}
>
{isValid ? "✓ Valid" : "✗ Invalid"}{" "}
{modal.format.type.replace("_", " ")}
</span>
{isValid &&
structuredData &&
typeof structuredData === "object" && (
<span className="text-sm text-gray-600 dark:text-gray-400">
{Object.keys(structuredData).length} properties
</span>
)}
</div>
<div className="flex space-x-3">
<div className="flex flex-col gap-3">
{/* Buttons */}
<div className="flex justify-end space-x-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-500 transition-colors"
@@ -3661,7 +3751,7 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
disabled={!isValid}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 rounded-md transition-colors"
>
Apply Changes
Save Changes
</button>
</div>
</div>