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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user