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:
dwindown
2025-09-21 17:08:20 +07:00
parent e1c74e4a4e
commit f2163c9814
2 changed files with 161 additions and 19 deletions

View File

@@ -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';
const StructuredEditor = ({ onDataChange, initialData = {} }) => {
const [data, setData] = useState(initialData);
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(() => {
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),
hasData: Object.keys(initialData).length > 0,
data: initialData
@@ -20,7 +27,8 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
}, [initialData]);
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);
onDataChange(newData);
};
@@ -78,21 +86,30 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
};
const removeProperty = (key, parentPath) => {
console.log('🗑️ REMOVE PROPERTY:', { key, parentPath });
const pathParts = parentPath.split('.');
const newData = { ...data };
let current = newData;
// Navigate to the parent object/array
for (let i = 1; i < pathParts.length; i++) {
if (i === pathParts.length - 1) {
delete current[key];
} else {
current = current[pathParts[i]];
}
// Delete the property/item from the parent
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];
}
if (pathParts.length === 1) {
delete newData[key];
}
console.log('🗑️ REMOVE PROPERTY - After:', {
parentPath,
remainingKeys: Array.isArray(current) ? current.length : Object.keys(current)
});
updateData(newData);
};

View File

@@ -1,5 +1,5 @@
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 CopyButton from '../components/CopyButton';
import StructuredEditor from '../components/StructuredEditor';
@@ -21,6 +21,9 @@ const ObjectEditor = () => {
jsonMinified: '',
serialized: ''
});
const [showFetch, setShowFetch] = useState(false);
const [fetchUrl, setFetchUrl] = useState('');
const [fetching, setFetching] = useState(false);
const fileInputRef = useRef(null);
// PHP serialize implementation (reused from SerializeTool)
@@ -337,6 +340,67 @@ const ObjectEditor = () => {
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
React.useEffect(() => {
generateOutputs(structuredData);
@@ -374,6 +438,14 @@ const ObjectEditor = () => {
<span>Load Sample</span>
</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
ref={fileInputRef}
type="file"
@@ -383,8 +455,42 @@ const ObjectEditor = () => {
/>
</div>
{/* View Mode Toggle */}
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
{/* Fetch URL Input */}
{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
onClick={() => setViewMode('visual')}
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
@@ -420,6 +526,22 @@ const ObjectEditor = () => {
</button>
</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 */}
{showInput && (
<div className="mb-6 space-y-2">
@@ -460,12 +582,14 @@ const ObjectEditor = () => {
</h3>
{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">
<div className="min-w-max">
<StructuredEditor
initialData={structuredData}
onDataChange={handleStructuredDataChange}
/>
</div>
</div>
)}
{viewMode === 'mindmap' && (
@@ -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>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>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> 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>