Improve ObjectEditor and PostmanTable UI/UX

- Enhanced JSON parsing with smart error handling for trailing commas
- Fixed StructuredEditor array handling - array indices now non-editable
- Improved PostmanTable styling: removed border radius, solid thead background
- Enhanced icon spacing and alignment for better visual hierarchy
- Added copy feedback system with green check icons (2500ms duration)
- Restructured MindmapView node layout with dedicated action column
- Added HTML rendering toggle for PostmanTable similar to MindmapView
- Implemented consistent copy functionality across components
- Removed debug console.log statements for cleaner codebase
This commit is contained in:
dwindown
2025-09-24 14:05:10 +07:00
parent 57655410ab
commit 21d0406ece
6 changed files with 306 additions and 201 deletions

View File

@@ -1,8 +1,10 @@
import React, { useState } from 'react';
import { ChevronLeft, Braces, List, Type, Hash, ToggleLeft, Minus } from 'lucide-react';
import { ChevronLeft, Braces, List, Type, Hash, ToggleLeft, Minus, Eye, Code, Copy, Check } from 'lucide-react';
const PostmanTable = ({ data, title = "JSON Data" }) => {
const [currentPath, setCurrentPath] = useState([]);
const [renderHtml, setRenderHtml] = useState(true);
const [copiedItems, setCopiedItems] = useState(new Set());
// Get current data based on path
const getCurrentData = () => {
@@ -44,6 +46,49 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
const headers = getArrayHeaders();
// Check if value contains HTML
const isHtmlContent = (value) => {
return value && typeof value === 'string' &&
(value.includes('<') && value.includes('>')) &&
/<[^>]+>/.test(value);
};
// Copy value to clipboard
const copyToClipboard = async (value, itemId) => {
try {
const textValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
await navigator.clipboard.writeText(textValue);
// Show feedback
setCopiedItems(prev => new Set([...prev, itemId]));
setTimeout(() => {
setCopiedItems(prev => {
const newSet = new Set(prev);
newSet.delete(itemId);
return newSet;
});
}, 2500);
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
// Show feedback for fallback too
setCopiedItems(prev => new Set([...prev, itemId]));
setTimeout(() => {
setCopiedItems(prev => {
const newSet = new Set(prev);
newSet.delete(itemId);
return newSet;
});
}, 2500);
}
};
// Handle row click - navigate to item details
const handleRowClick = (index, key = null) => {
if (isArrayView) {
@@ -87,11 +132,32 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
return parts;
};
// Format value for display
// Format value for display in table (truncated)
const formatValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'string') return value;
if (typeof value === 'string') {
// Truncate long strings for table display
if (value.length > 100) {
return value.substring(0, 100) + '...';
}
// Replace newlines with spaces for single-line display
return value.replace(/\n/g, ' ').replace(/\s+/g, ' ');
}
if (typeof value === 'boolean') return value.toString();
if (typeof value === 'number') return value.toString();
if (typeof value === 'object') {
if (Array.isArray(value)) return `Array(${value.length})`;
return `Object(${Object.keys(value).length})`;
}
return String(value);
};
// Format value for display in details view (full text)
const formatFullValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'string') return value; // Show full string without truncation
if (typeof value === 'boolean') return value.toString();
if (typeof value === 'number') return value.toString();
if (typeof value === 'object') {
@@ -154,19 +220,69 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
};
// Render value with appropriate styling
// Render value with appropriate styling (for table view)
const renderValue = (value) => {
const typeStyle = getTypeStyle(value);
const formattedValue = formatValue(value);
return (
<span className={`inline-flex items-center space-x-1 px-2 py-1 rounded-full text-xs font-medium ${typeStyle.color}`}>
<span className={`inline-flex items-center space-x-1 px-2 py-1 rounded text-xs font-medium ${typeStyle.color}`}>
{typeStyle.icon}
<span>{formattedValue}</span>
</span>
);
};
// Render value with full text (for detail view)
const renderFullValue = (value) => {
const typeStyle = getTypeStyle(value);
const formattedValue = formatFullValue(value);
const hasHtml = isHtmlContent(value);
return (
<div className="relative">
<span className={`inline-flex items-start space-x-2 px-2 py-1 rounded text-xs font-medium ${typeStyle.color}`}>
<span className="flex-shrink-0 mt-0.5">{typeStyle.icon}</span>
<span className="whitespace-pre-wrap break-words flex-1">
{hasHtml && renderHtml ? (
<div dangerouslySetInnerHTML={{ __html: String(value) }} />
) : (
formattedValue
)}
</span>
</span>
{/* HTML Toggle Buttons */}
{hasHtml && (
<div className="absolute -top-1 -right-1 flex">
<button
onClick={() => setRenderHtml(true)}
className={`px-1.5 py-0.5 text-xs rounded-l ${
renderHtml
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
}`}
title="Render HTML"
>
<Eye className="h-3 w-3" />
</button>
<button
onClick={() => setRenderHtml(false)}
className={`px-1.5 py-0.5 text-xs rounded-r ${
!renderHtml
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
}`}
title="Show Raw HTML"
>
<Code className="h-3 w-3" />
</button>
</div>
)}
</div>
);
};
// Get value type
const getValueType = (value) => {
if (value === null) return 'null';
@@ -175,7 +291,7 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
};
return (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Header with Breadcrumb */}
<div className="bg-gray-50 dark:bg-gray-700 px-4 py-3 border-b border-gray-200 dark:border-gray-600">
<div className="flex items-center justify-between">
@@ -220,7 +336,7 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
{isArrayView ? (
// Horizontal table for arrays
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 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 w-12">
#
@@ -263,7 +379,7 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
) : isObjectView ? (
// Vertical key-value table for objects
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 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 w-1/3">
Key
@@ -271,6 +387,9 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Value
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-16">
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@@ -290,7 +409,23 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
{key}
</td>
<td className="px-4 py-3 text-sm">
{renderValue(value)}
{renderFullValue(value)}
</td>
<td className="px-4 py-3 text-sm">
<button
onClick={(e) => {
e.stopPropagation(); // Prevent row click
copyToClipboard(value, `${currentPath.join('.')}.${key}`);
}}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors"
title={copiedItems.has(`${currentPath.join('.')}.${key}`) ? "Copied!" : "Copy value"}
>
{copiedItems.has(`${currentPath.join('.')}.${key}`) ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3 text-gray-500 dark:text-gray-400" />
)}
</button>
</td>
</tr>
);
@@ -301,8 +436,8 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
// Fallback for primitive values
<div className="p-4">
<div className="text-center text-gray-500 dark:text-gray-400">
<div className="text-lg font-mono text-gray-900 dark:text-gray-100">
{formatValue(currentData)}
<div className="text-lg font-mono text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words">
{formatFullValue(currentData)}
</div>
<div className="text-sm mt-2">
Type: {getValueType(currentData)}