✨ Enhanced Object Editor with fetch data & mobile improvements
🚀 New Features: - Fetch Data functionality with URL input and error handling - Auto-protocol detection (adds https:// if missing) - Smart content-type handling for various JSON APIs - Perfect for Telegram Bot API, GitHub API, JSONPlaceholder, etc. 📱 Mobile Responsiveness: - Desktop: Clean tab interface for view modes - Mobile: Native select dropdown with emoji icons - StructuredEditor: Horizontal scroll for wide JSON structures - Input data field auto-hides on successful fetch 🐛 Critical Fixes: - Fixed StructuredEditor reinitialization loop issue - Fixed deep nested property/array item deletion - Proper array splicing and object property removal - Internal vs external data change tracking 🎨 UX Improvements: - Loading states during fetch operations - Better error messages and validation - Responsive button layouts and spacing - Enhanced usage tips with fetch examples
This commit is contained in:
@@ -1,13 +1,20 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces } from 'lucide-react';
|
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces } from 'lucide-react';
|
||||||
|
|
||||||
const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||||
const [data, setData] = useState(initialData);
|
const [data, setData] = useState(initialData);
|
||||||
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
|
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
|
||||||
|
const isInternalUpdate = useRef(false);
|
||||||
|
|
||||||
// Update internal data when initialData prop changes
|
// Update internal data when initialData prop changes (but not from internal updates)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('📥 INITIAL DATA CHANGED:', {
|
// Skip update if this change came from internal editor actions
|
||||||
|
if (isInternalUpdate.current) {
|
||||||
|
isInternalUpdate.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📥 EXTERNAL DATA CHANGED:', {
|
||||||
keys: Object.keys(initialData),
|
keys: Object.keys(initialData),
|
||||||
hasData: Object.keys(initialData).length > 0,
|
hasData: Object.keys(initialData).length > 0,
|
||||||
data: initialData
|
data: initialData
|
||||||
@@ -20,7 +27,8 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
|||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
const updateData = (newData) => {
|
const updateData = (newData) => {
|
||||||
console.log('📊 DATA UPDATE:', { keys: Object.keys(newData), totalProps: JSON.stringify(newData).length });
|
console.log('📊 INTERNAL DATA UPDATE:', { keys: Object.keys(newData), totalProps: JSON.stringify(newData).length });
|
||||||
|
isInternalUpdate.current = true; // Mark as internal update
|
||||||
setData(newData);
|
setData(newData);
|
||||||
onDataChange(newData);
|
onDataChange(newData);
|
||||||
};
|
};
|
||||||
@@ -78,22 +86,31 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeProperty = (key, parentPath) => {
|
const removeProperty = (key, parentPath) => {
|
||||||
|
console.log('🗑️ REMOVE PROPERTY:', { key, parentPath });
|
||||||
|
|
||||||
const pathParts = parentPath.split('.');
|
const pathParts = parentPath.split('.');
|
||||||
const newData = { ...data };
|
const newData = { ...data };
|
||||||
let current = newData;
|
let current = newData;
|
||||||
|
|
||||||
|
// Navigate to the parent object/array
|
||||||
for (let i = 1; i < pathParts.length; i++) {
|
for (let i = 1; i < pathParts.length; i++) {
|
||||||
if (i === pathParts.length - 1) {
|
current = current[pathParts[i]];
|
||||||
delete current[key];
|
|
||||||
} else {
|
|
||||||
current = current[pathParts[i]];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathParts.length === 1) {
|
// Delete the property/item from the parent
|
||||||
delete newData[key];
|
if (Array.isArray(current)) {
|
||||||
|
// For arrays, remove by index and reindex
|
||||||
|
current.splice(parseInt(key), 1);
|
||||||
|
} else {
|
||||||
|
// For objects, delete the property
|
||||||
|
delete current[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('🗑️ REMOVE PROPERTY - After:', {
|
||||||
|
parentPath,
|
||||||
|
remainingKeys: Array.isArray(current) ? current.length : Object.keys(current)
|
||||||
|
});
|
||||||
|
|
||||||
updateData(newData);
|
updateData(newData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef, useCallback } from 'react';
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
import { Edit3, Upload, FileText, Map, Table } from 'lucide-react';
|
import { Edit3, Upload, FileText, Map, Table, Globe } from 'lucide-react';
|
||||||
import ToolLayout from '../components/ToolLayout';
|
import ToolLayout from '../components/ToolLayout';
|
||||||
import CopyButton from '../components/CopyButton';
|
import CopyButton from '../components/CopyButton';
|
||||||
import StructuredEditor from '../components/StructuredEditor';
|
import StructuredEditor from '../components/StructuredEditor';
|
||||||
@@ -21,6 +21,9 @@ const ObjectEditor = () => {
|
|||||||
jsonMinified: '',
|
jsonMinified: '',
|
||||||
serialized: ''
|
serialized: ''
|
||||||
});
|
});
|
||||||
|
const [showFetch, setShowFetch] = useState(false);
|
||||||
|
const [fetchUrl, setFetchUrl] = useState('');
|
||||||
|
const [fetching, setFetching] = useState(false);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
// PHP serialize implementation (reused from SerializeTool)
|
// PHP serialize implementation (reused from SerializeTool)
|
||||||
@@ -337,6 +340,67 @@ const ObjectEditor = () => {
|
|||||||
generateOutputs(sample);
|
generateOutputs(sample);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch data from URL
|
||||||
|
const handleFetchData = async () => {
|
||||||
|
if (!fetchUrl.trim()) {
|
||||||
|
setError('Please enter a valid URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFetching(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Add protocol if missing
|
||||||
|
let url = fetchUrl.trim();
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
url = 'https://' + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
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);
|
||||||
|
setShowInput(false); // Hide input on successful fetch
|
||||||
|
setShowFetch(false);
|
||||||
|
} catch {
|
||||||
|
throw new Error('Response is not valid JSON. Content-Type: ' + (contentType || 'unknown'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
setStructuredData(data);
|
||||||
|
generateOutputs(data);
|
||||||
|
setInputText(JSON.stringify(data, null, 2));
|
||||||
|
setInputFormat('JSON');
|
||||||
|
setInputValid(true);
|
||||||
|
setShowInput(false); // Hide input on successful fetch
|
||||||
|
setShowFetch(false);
|
||||||
|
}
|
||||||
|
} 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}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setFetching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize outputs when component mounts or data changes
|
// Initialize outputs when component mounts or data changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
generateOutputs(structuredData);
|
generateOutputs(structuredData);
|
||||||
@@ -374,6 +438,14 @@ const ObjectEditor = () => {
|
|||||||
<span>Load Sample</span>
|
<span>Load Sample</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowFetch(!showFetch)}
|
||||||
|
className="flex items-center space-x-2 tool-button-secondary"
|
||||||
|
>
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
<span>{showFetch ? 'Hide Fetch' : 'Fetch Data'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -383,8 +455,42 @@ const ObjectEditor = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* View Mode Toggle */}
|
{/* Fetch URL Input */}
|
||||||
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
|
{showFetch && (
|
||||||
|
<div className="mb-6 space-y-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Fetch Data from URL
|
||||||
|
</label>
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={fetchUrl}
|
||||||
|
onChange={(e) => setFetchUrl(e.target.value)}
|
||||||
|
placeholder="https://api.telegram.org/bot<token>/getMe"
|
||||||
|
className="tool-input flex-1"
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleFetchData()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleFetchData}
|
||||||
|
disabled={fetching || !fetchUrl.trim()}
|
||||||
|
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
|
||||||
|
fetching || !fetchUrl.trim()
|
||||||
|
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||||
|
: 'bg-primary-600 text-white hover:bg-primary-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
<span>{fetching ? 'Fetching...' : 'Fetch'}</span>
|
||||||
|
</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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* View Mode Toggle - Desktop Tabs */}
|
||||||
|
<div className="hidden sm:flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('visual')}
|
onClick={() => setViewMode('visual')}
|
||||||
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
|
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
|
||||||
@@ -420,6 +526,22 @@ const ObjectEditor = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* View Mode Toggle - Mobile Select */}
|
||||||
|
<div className="sm:hidden mb-6">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
View Mode
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={viewMode}
|
||||||
|
onChange={(e) => setViewMode(e.target.value)}
|
||||||
|
className="tool-input w-full"
|
||||||
|
>
|
||||||
|
<option value="visual">📝 Visual Editor</option>
|
||||||
|
<option value="mindmap">🗺️ Mindmap View</option>
|
||||||
|
<option value="table">📊 Table View</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Input Section */}
|
{/* Input Section */}
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<div className="mb-6 space-y-2">
|
<div className="mb-6 space-y-2">
|
||||||
@@ -460,11 +582,13 @@ const ObjectEditor = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{viewMode === 'visual' && (
|
{viewMode === 'visual' && (
|
||||||
<div className="min-h-96 border border-gray-200 dark:border-gray-700 rounded-lg">
|
<div className="min-h-96 border border-gray-200 dark:border-gray-700 rounded-lg overflow-x-auto">
|
||||||
<StructuredEditor
|
<div className="min-w-max">
|
||||||
initialData={structuredData}
|
<StructuredEditor
|
||||||
onDataChange={handleStructuredDataChange}
|
initialData={structuredData}
|
||||||
/>
|
onDataChange={handleStructuredDataChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -554,6 +678,7 @@ const ObjectEditor = () => {
|
|||||||
<li>• <strong>Table View:</strong> Browse data like Postman - click arrays for horizontal tables, objects for key-value pairs</li>
|
<li>• <strong>Table View:</strong> Browse data like Postman - click arrays for horizontal tables, objects for key-value pairs</li>
|
||||||
<li>• <strong>Navigation:</strong> Use breadcrumbs and Back button to navigate through nested data structures</li>
|
<li>• <strong>Navigation:</strong> Use breadcrumbs and Back button to navigate through nested data structures</li>
|
||||||
<li>• <strong>Input Data:</strong> Paste JSON/PHP serialized data with auto-detection in the input field</li>
|
<li>• <strong>Input Data:</strong> Paste JSON/PHP serialized data with auto-detection in the input field</li>
|
||||||
|
<li>• <strong>Fetch Data:</strong> Load JSON from any URL - perfect for APIs like Telegram Bot, GitHub, JSONPlaceholder</li>
|
||||||
<li>• Import data from files or use the sample data to get started</li>
|
<li>• Import data from files or use the sample data to get started</li>
|
||||||
<li>• Toggle output formats visibility with the "Show/Hide Output Formats" button</li>
|
<li>• Toggle output formats visibility with the "Show/Hide Output Formats" button</li>
|
||||||
<li>• Export your data in any format: JSON pretty, minified, or PHP serialized</li>
|
<li>• Export your data in any format: JSON pretty, minified, or PHP serialized</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user