-
@@ -1070,26 +1133,27 @@ const ObjectEditor = () => {
{
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'}
{
- 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'}
@@ -1098,23 +1162,26 @@ const ObjectEditor = () => {
{activeExportTab === 'php' && (
-
{
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'}
{
@@ -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'}
diff --git a/src/pages/TableEditor.js b/src/pages/TableEditor.js
index 6f52a198..e46b2dbc 100644
--- a/src/pages/TableEditor.js
+++ b/src/pages/TableEditor.js
@@ -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" && (
-
-
-
-
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 && (
-
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"
- >
-
-
- )}
+ urlDataSummary ? (
+
+
+
+ ✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.rows} rows)
+
+ setUrlDataSummary(null)}
+ className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
+ >
+ Fetch New URL ▼
+
-
- {isLoading ? "Fetching..." : "Fetch Data"}
-
-
- setUseFirstRowAsHeader(e.target.checked)}
- className="mr-2"
- />
- Use first row as column headers (for CSV/TSV)
-
-
+ ) : (
+
+
+
+ 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 && (
+ 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"
+ >
+
+
+ )}
+
+
+ {isLoading ? "Fetching..." : "Fetch Data"}
+
+
+
+ setUseFirstRowAsHeader(e.target.checked)}
+ className="mr-2"
+ />
+ Use first row as column headers (for CSV/TSV)
+
+
+ )
)}
{activeTab === "paste" && (
pasteCollapsed ? (
-
-
+
+
✓ Data loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.rows} rows)
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 ▼
@@ -2092,18 +2129,33 @@ const TableEditor = () => {
)}
{activeTab === "upload" && (
-
+
+ )
)}
)}
@@ -2288,7 +2341,7 @@ const TableEditor = () => {
style={{ maxWidth: '100%' }}
>
-
+
{
{/* Export Header - Collapsible */}
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"
>
-
+
Export Results
@@ -2744,12 +2798,14 @@ const TableEditor = () => {
{exportTab === "json" && (
-
@@ -2776,22 +2832,28 @@ const TableEditor = () => {
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'}
+ 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'}
@@ -2800,31 +2862,39 @@ const TableEditor = () => {
{exportTab === "csv" && (
-
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'}
+ 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'}
@@ -2832,31 +2902,39 @@ const TableEditor = () => {
{exportTab === "tsv" && (
-
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'}
+ 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'}
@@ -2907,12 +2985,14 @@ const TableEditor = () => {
-
{/* Intelligent Schema Analysis */}
@@ -3046,22 +3126,28 @@ const TableEditor = () => {
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'}
+ 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'}
@@ -3570,10 +3656,26 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
Row {modal.rowIndex} • Column: {modal.columnName} • Format:{" "}
{modal.format.type.replace("_", " ")}
+ {/* Format info */}
+
+
+ {isValid ? "✓ Valid" : "✗ Invalid"}{" "}
+ {modal.format.type.replace("_", " ")}
+
+ {isValid &&
+ structuredData &&
+ typeof structuredData === "object" && (
+
+ {" • "}{Object.keys(structuredData).length} properties
+
+ )}
+
@@ -3633,23 +3735,11 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
{/* Footer */}
-
-
-
- {isValid ? "✓ Valid" : "✗ Invalid"}{" "}
- {modal.format.type.replace("_", " ")}
-
- {isValid &&
- structuredData &&
- typeof structuredData === "object" && (
-
- {Object.keys(structuredData).length} properties
-
- )}
-
-
+
+
+
+ {/* Buttons */}
+
{
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