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

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,55 +633,52 @@ const TableEditor = () => {
// Parse CSV/TSV data
const parseData = (text, hasHeaders = true) => {
try {
const result = Papa.parse(text.trim(), {
header: false,
skipEmptyLines: true,
delimiter: text.includes("\t") ? "\t" : ",",
});
const result = Papa.parse(text.trim(), {
header: false,
skipEmptyLines: true,
delimiter: text.includes("\t") ? "\t" : ",",
});
if (result.errors.length > 0) {
throw new Error(result.errors[0].message);
}
const rows = result.data;
if (rows.length === 0) {
throw new Error("No data found");
}
let headers;
let dataRows;
if (hasHeaders && rows.length > 0) {
headers = rows[0].map((header, index) => ({
id: `col_${index}`,
name: header || `Column ${index + 1}`,
type: "text",
}));
dataRows = rows.slice(1);
} else {
headers = rows[0].map((_, index) => ({
id: `col_${index}`,
name: `Column ${index + 1}`,
type: "text",
}));
dataRows = rows;
}
const tableData = dataRows.map((row, index) => {
const rowData = { id: `row_${index}` };
headers.forEach((header, colIndex) => {
rowData[header.id] = row[colIndex] || "";
});
return rowData;
});
setColumns(headers);
setData(tableData);
setError("");
} catch (err) {
setError(`Failed to parse data: ${err.message}`);
if (result.errors.length > 0) {
throw new Error(result.errors[0].message);
}
const rows = result.data;
if (rows.length === 0) {
throw new Error("No data found");
}
let headers;
let dataRows;
if (hasHeaders && rows.length > 0) {
headers = rows[0].map((header, index) => ({
id: `col_${index}`,
name: header || `Column ${index + 1}`,
type: "text",
}));
dataRows = rows.slice(1);
} else {
headers = rows[0].map((_, index) => ({
id: `col_${index}`,
name: `Column ${index + 1}`,
type: "text",
}));
dataRows = rows;
}
const tableData = dataRows.map((row, index) => {
const rowData = { id: `row_${index}` };
headers.forEach((header, colIndex) => {
rowData[header.id] = row[colIndex] || "";
});
return rowData;
});
setColumns(headers);
setData(tableData);
setError("");
return tableData.length; // Return actual row count
};
// Parse SQL data
@@ -941,40 +921,37 @@ const TableEditor = () => {
// Parse JSON data
const parseJsonData = (text) => {
try {
const jsonData = JSON.parse(text);
const jsonData = JSON.parse(text);
if (!Array.isArray(jsonData)) {
throw new Error("JSON must be an array of objects");
}
if (jsonData.length === 0) {
throw new Error("Array is empty");
}
// Extract columns from first object
const firstItem = jsonData[0];
const headers = Object.keys(firstItem).map((key, index) => ({
id: `col_${index}`,
name: key,
type: typeof firstItem[key] === "number" ? "number" : "text",
}));
// Convert to table format
const tableData = jsonData.map((item, index) => {
const rowData = { id: `row_${index}` };
headers.forEach((header) => {
rowData[header.id] = item[header.name] || "";
});
return rowData;
});
setColumns(headers);
setData(tableData);
setError("");
} catch (err) {
setError(`Failed to parse JSON: ${err.message}`);
if (!Array.isArray(jsonData)) {
throw new Error("JSON must be an array of objects");
}
if (jsonData.length === 0) {
throw new Error("Array is empty");
}
// Extract columns from first object
const firstItem = jsonData[0];
const headers = Object.keys(firstItem).map((key, index) => ({
id: `col_${index}`,
name: key,
type: typeof firstItem[key] === "number" ? "number" : "text",
}));
// Convert to table format
const tableData = jsonData.map((item, index) => {
const rowData = { id: `row_${index}` };
headers.forEach((header) => {
rowData[header.id] = item[header.name] || "";
});
return rowData;
});
setColumns(headers);
setData(tableData);
setError("");
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) {
setPasteDataSummary({
format: format,
size: inputText.length,
rows: data.length
});
setPasteCollapsed(true);
setError('');
}
// Collapse input and show summary
setPasteDataSummary({
format: format,
size: inputText.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,56 +2016,72 @@ const TableEditor = () => {
)}
{activeTab === "url" && (
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://api.example.com/data.json or https://example.com/data.csv"
className="tool-input w-full pr-10"
disabled={isLoading}
/>
{url && !isLoading && (
<button
onClick={() => setUrl("")}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
>
<X className="h-4 w-4" />
</button>
)}
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>
<button
onClick={fetchUrlData}
disabled={isLoading || !url.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
>
{isLoading ? "Fetching..." : "Fetch Data"}
</button>
</div>
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
<input
type="checkbox"
checked={useFirstRowAsHeader}
onChange={(e) => setUseFirstRowAsHeader(e.target.checked)}
className="mr-2"
/>
Use first row as column headers (for CSV/TSV)
</label>
</div>
) : (
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://api.example.com/data.json or https://example.com/data.csv"
className="tool-input w-full pr-10"
disabled={isLoading}
/>
{url && !isLoading && (
<button
onClick={() => setUrl("")}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
>
<X className="h-4 w-4" />
</button>
)}
</div>
<button
onClick={fetchUrlData}
disabled={isLoading || !url.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
>
{isLoading ? "Fetching..." : "Fetch Data"}
</button>
</div>
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
<input
type="checkbox"
checked={useFirstRowAsHeader}
onChange={(e) => setUseFirstRowAsHeader(e.target.checked)}
className="mr-2"
/>
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,18 +2129,33 @@ const TableEditor = () => {
)}
{activeTab === "upload" && (
<div className="space-y-3">
<input
type="file"
accept=".csv,.tsv,.json,.sql"
onChange={handleFileUpload}
className="tool-input"
/>
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
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="checkbox"
checked={useFirstRowAsHeader}
onChange={(e) => setUseFirstRowAsHeader(e.target.checked)}
type="file"
accept=".csv,.tsv,.json,.sql"
onChange={handleFileUpload}
className="tool-input"
/>
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
<input
type="checkbox"
checked={useFirstRowAsHeader}
onChange={(e) => setUseFirstRowAsHeader(e.target.checked)}
className="mr-2"
/>
Use first row as column headers (for CSV/TSV)
@@ -2115,7 +2167,8 @@ const TableEditor = () => {
open, edit, and export your files locally.
</p>
</div>
</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>