🎯 Complete Postman-Style Table View with Consistent Design
✨ New Features: - Authentic Postman table experience with horizontal arrays & vertical objects - Click rows to drill down with breadcrumb navigation - Smart data detection (arrays → tables, objects → key-value, primitives → display) - Clickable breadcrumb buttons for easy navigation - Back button for seamless exploration 🎨 Consistent Visual Design: - Unified type notation across Visual Editor, Mindmap, and Table views - Color-coded pills with icons for all data types: 🔵 Objects (blue) 🟢 Arrays (green) 🟣 Strings (purple) 🟠 Numbers (orange) 🟡 Booleans (yellow) ⚫ Null (gray) - Button-style breadcrumb vs circular data pills for clear distinction - Full dark mode support throughout 🔧 UX Improvements: - Output formats hidden by default with 'See Data Outputs' toggle - Left-aligned toggle button matching existing UI - Preserved PostmanTreeTable as asset for hierarchical view - Enhanced sample data with realistic array/object structure - Professional styling matching Postman's interface 🚀 Technical Implementation: - Dynamic column generation from array data - Path-based navigation with state management - Type-aware rendering with consistent icons - Responsive design for all screen sizes - Clean component architecture
This commit is contained in:
341
src/components/PostmanTable.js
Normal file
341
src/components/PostmanTable.js
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { ChevronLeft, Braces, List, Type, Hash, ToggleLeft, Minus } from 'lucide-react';
|
||||||
|
|
||||||
|
const PostmanTable = ({ data, title = "JSON Data" }) => {
|
||||||
|
const [currentPath, setCurrentPath] = useState([]);
|
||||||
|
const [selectedRowIndex, setSelectedRowIndex] = useState(null);
|
||||||
|
|
||||||
|
// Get current data based on path
|
||||||
|
const getCurrentData = () => {
|
||||||
|
let current = data;
|
||||||
|
for (const pathSegment of currentPath) {
|
||||||
|
if (current && typeof current === 'object') {
|
||||||
|
current = current[pathSegment];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentData = getCurrentData();
|
||||||
|
|
||||||
|
// Check if current data is an array for horizontal table display
|
||||||
|
const isArrayView = Array.isArray(currentData);
|
||||||
|
const isObjectView = currentData && typeof currentData === 'object' && !Array.isArray(currentData);
|
||||||
|
|
||||||
|
// Generate table headers for array view
|
||||||
|
const getArrayHeaders = () => {
|
||||||
|
if (!isArrayView || currentData.length === 0) return [];
|
||||||
|
|
||||||
|
// Check if array contains objects or primitives
|
||||||
|
const firstItem = currentData[0];
|
||||||
|
if (firstItem && typeof firstItem === 'object' && !Array.isArray(firstItem)) {
|
||||||
|
// Array of objects - get all possible keys
|
||||||
|
const headers = new Set();
|
||||||
|
currentData.forEach(item => {
|
||||||
|
if (item && typeof item === 'object') {
|
||||||
|
Object.keys(item).forEach(key => headers.add(key));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(headers);
|
||||||
|
} else {
|
||||||
|
// Array of primitives - use "Value" as header
|
||||||
|
return ['Value'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = getArrayHeaders();
|
||||||
|
|
||||||
|
// Handle row click - navigate to item details
|
||||||
|
const handleRowClick = (index, key = null) => {
|
||||||
|
if (isArrayView) {
|
||||||
|
setSelectedRowIndex(index);
|
||||||
|
setCurrentPath([...currentPath, index]);
|
||||||
|
} else if (isObjectView && key) {
|
||||||
|
// Navigate into object property
|
||||||
|
setCurrentPath([...currentPath, key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle back navigation
|
||||||
|
const handleBack = () => {
|
||||||
|
if (currentPath.length > 0) {
|
||||||
|
const newPath = currentPath.slice(0, -1);
|
||||||
|
setCurrentPath(newPath);
|
||||||
|
setSelectedRowIndex(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle breadcrumb navigation
|
||||||
|
const handleBreadcrumbClick = (index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
// Click on "Root" - go to root
|
||||||
|
setCurrentPath([]);
|
||||||
|
} else {
|
||||||
|
// Click on specific breadcrumb - navigate to that level
|
||||||
|
// Adjust for the "Root" prefix in breadcrumb
|
||||||
|
const newPath = currentPath.slice(0, index);
|
||||||
|
setCurrentPath(newPath);
|
||||||
|
}
|
||||||
|
setSelectedRowIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate breadcrumb
|
||||||
|
const getBreadcrumb = () => {
|
||||||
|
const parts = ['Root'];
|
||||||
|
currentPath.forEach((segment, index) => {
|
||||||
|
if (typeof segment === 'number') {
|
||||||
|
parts.push(segment.toString());
|
||||||
|
} else {
|
||||||
|
parts.push(segment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parts;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format value for display
|
||||||
|
const formatValue = (value) => {
|
||||||
|
if (value === null) return 'null';
|
||||||
|
if (value === undefined) return 'undefined';
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get type-specific styling and icon
|
||||||
|
const getTypeStyle = (value) => {
|
||||||
|
if (value === null) {
|
||||||
|
return {
|
||||||
|
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300',
|
||||||
|
icon: <Minus className="h-3 w-3" />,
|
||||||
|
type: 'null'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return {
|
||||||
|
color: 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300',
|
||||||
|
icon: <List className="h-3 w-3" />,
|
||||||
|
type: 'array'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (typeof value) {
|
||||||
|
case 'object':
|
||||||
|
return {
|
||||||
|
color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300',
|
||||||
|
icon: <Braces className="h-3 w-3" />,
|
||||||
|
type: 'object'
|
||||||
|
};
|
||||||
|
case 'string':
|
||||||
|
return {
|
||||||
|
color: 'bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300',
|
||||||
|
icon: <Type className="h-3 w-3" />,
|
||||||
|
type: 'string'
|
||||||
|
};
|
||||||
|
case 'number':
|
||||||
|
return {
|
||||||
|
color: 'bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-300',
|
||||||
|
icon: <Hash className="h-3 w-3" />,
|
||||||
|
type: 'number'
|
||||||
|
};
|
||||||
|
case 'boolean':
|
||||||
|
return {
|
||||||
|
color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300',
|
||||||
|
icon: <ToggleLeft className="h-3 w-3" />,
|
||||||
|
type: 'boolean'
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300',
|
||||||
|
icon: <Minus className="h-3 w-3" />,
|
||||||
|
type: 'unknown'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if value is a complex object (not a primitive)
|
||||||
|
const isComplexValue = (value) => {
|
||||||
|
return value !== null && typeof value === 'object';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render value with appropriate styling
|
||||||
|
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}`}>
|
||||||
|
{typeStyle.icon}
|
||||||
|
<span>{formattedValue}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get value type
|
||||||
|
const getValueType = (value) => {
|
||||||
|
if (value === null) return 'null';
|
||||||
|
if (Array.isArray(value)) return 'array';
|
||||||
|
return typeof value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg 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">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{currentPath.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className="flex items-center space-x-1 text-blue-600 hover:text-blue-800 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
<span className="text-sm">Back</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center space-x-1 text-sm">
|
||||||
|
{getBreadcrumb().map((part, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{index > 0 && <span className="text-gray-400 dark:text-gray-500">/</span>}
|
||||||
|
<button
|
||||||
|
onClick={() => handleBreadcrumbClick(index)}
|
||||||
|
className={`px-2 py-1 rounded transition-colors ${
|
||||||
|
index === getBreadcrumb().length - 1
|
||||||
|
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 font-semibold cursor-default'
|
||||||
|
: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
disabled={index === getBreadcrumb().length - 1}
|
||||||
|
>
|
||||||
|
{part}
|
||||||
|
</button>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{isArrayView && `${currentData.length} items`}
|
||||||
|
{isObjectView && `${Object.keys(currentData).length} properties`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="overflow-auto max-h-96">
|
||||||
|
{isArrayView ? (
|
||||||
|
// Horizontal table for arrays
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0">
|
||||||
|
<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">
|
||||||
|
#
|
||||||
|
</th>
|
||||||
|
{headers.map(header => (
|
||||||
|
<th key={header} className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
{header}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{currentData.map((item, index) => {
|
||||||
|
const isPrimitiveArray = headers.length === 1 && headers[0] === 'Value';
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={index}
|
||||||
|
onClick={() => handleRowClick(index)}
|
||||||
|
className="hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer transition-colors duration-150"
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 text-sm font-mono text-gray-500 dark:text-gray-400">
|
||||||
|
{index}
|
||||||
|
</td>
|
||||||
|
{isPrimitiveArray ? (
|
||||||
|
<td className="px-4 py-3 text-sm">
|
||||||
|
{renderValue(item)}
|
||||||
|
</td>
|
||||||
|
) : (
|
||||||
|
headers.map(header => (
|
||||||
|
<td key={header} className="px-4 py-3 text-sm">
|
||||||
|
{renderValue(item?.[header])}
|
||||||
|
</td>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : isObjectView ? (
|
||||||
|
// Vertical key-value table for objects
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0">
|
||||||
|
<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
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{Object.entries(currentData).map(([key, value]) => {
|
||||||
|
const isClickable = typeof value === 'object' && value !== null;
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={key}
|
||||||
|
onClick={() => isClickable && handleRowClick(null, key)}
|
||||||
|
className={`transition-colors duration-150 ${
|
||||||
|
isClickable
|
||||||
|
? 'hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer'
|
||||||
|
: 'hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 text-sm font-mono font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{key}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm">
|
||||||
|
{renderValue(value)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
|
// 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>
|
||||||
|
<div className="text-sm mt-2">
|
||||||
|
Type: {getValueType(currentData)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-700 px-4 py-2 border-t border-gray-200 dark:border-gray-600">
|
||||||
|
<div className="flex items-center justify-between text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<span>
|
||||||
|
{isArrayView ? `Array with ${currentData.length} items` :
|
||||||
|
isObjectView ? `Object with ${Object.keys(currentData).length} properties` :
|
||||||
|
`Primitive value (${getValueType(currentData)})`}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Path: {currentPath.length === 0 ? 'Root' : currentPath.join(' → ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostmanTable;
|
||||||
273
src/components/PostmanTreeTable.js
Normal file
273
src/components/PostmanTreeTable.js
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { ChevronRight, ChevronDown, Copy, Search, Filter } from 'lucide-react';
|
||||||
|
import CopyButton from './CopyButton';
|
||||||
|
|
||||||
|
const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
|
||||||
|
const [expandedPaths, setExpandedPaths] = useState(new Set());
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [filterType, setFilterType] = useState('all');
|
||||||
|
|
||||||
|
// Flatten the data structure for table display
|
||||||
|
const flattenData = (obj, path = '', level = 0) => {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
if (obj === null || obj === undefined) {
|
||||||
|
return [{
|
||||||
|
key: path || 'root',
|
||||||
|
value: obj,
|
||||||
|
type: obj === null ? 'null' : 'undefined',
|
||||||
|
level,
|
||||||
|
path,
|
||||||
|
hasChildren: false
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj !== 'object') {
|
||||||
|
return [{
|
||||||
|
key: path || 'root',
|
||||||
|
value: obj,
|
||||||
|
type: typeof obj,
|
||||||
|
level,
|
||||||
|
path,
|
||||||
|
hasChildren: false
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
const arrayItem = {
|
||||||
|
key: path || 'root',
|
||||||
|
value: `Array(${obj.length})`,
|
||||||
|
type: 'array',
|
||||||
|
level,
|
||||||
|
path,
|
||||||
|
hasChildren: obj.length > 0,
|
||||||
|
isExpandable: true
|
||||||
|
};
|
||||||
|
result.push(arrayItem);
|
||||||
|
|
||||||
|
if (expandedPaths.has(path)) {
|
||||||
|
obj.forEach((item, index) => {
|
||||||
|
const itemPath = path ? `${path}[${index}]` : `[${index}]`;
|
||||||
|
result.push(...flattenData(item, itemPath, level + 1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const keys = Object.keys(obj);
|
||||||
|
const objectItem = {
|
||||||
|
key: path || 'root',
|
||||||
|
value: `Object(${keys.length})`,
|
||||||
|
type: 'object',
|
||||||
|
level,
|
||||||
|
path,
|
||||||
|
hasChildren: keys.length > 0,
|
||||||
|
isExpandable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (path) result.push(objectItem);
|
||||||
|
|
||||||
|
if (!path || expandedPaths.has(path)) {
|
||||||
|
keys.forEach(key => {
|
||||||
|
const keyPath = path ? `${path}.${key}` : key;
|
||||||
|
result.push(...flattenData(obj[key], keyPath, path ? level + 1 : level));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleExpanded = (path) => {
|
||||||
|
const newExpanded = new Set(expandedPaths);
|
||||||
|
if (newExpanded.has(path)) {
|
||||||
|
newExpanded.delete(path);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(path);
|
||||||
|
}
|
||||||
|
setExpandedPaths(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flatData = useMemo(() => flattenData(data), [data, expandedPaths]);
|
||||||
|
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
return flatData.filter(item => {
|
||||||
|
// Search filter
|
||||||
|
if (searchTerm) {
|
||||||
|
const searchLower = searchTerm.toLowerCase();
|
||||||
|
const keyMatch = item.key.toLowerCase().includes(searchLower);
|
||||||
|
const valueMatch = String(item.value).toLowerCase().includes(searchLower);
|
||||||
|
if (!keyMatch && !valueMatch) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type filter
|
||||||
|
if (filterType !== 'all' && item.type !== filterType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [flatData, searchTerm, filterType]);
|
||||||
|
|
||||||
|
const getTypeColor = (type) => {
|
||||||
|
const colors = {
|
||||||
|
string: 'bg-green-100 text-green-800',
|
||||||
|
number: 'bg-blue-100 text-blue-800',
|
||||||
|
boolean: 'bg-purple-100 text-purple-800',
|
||||||
|
object: 'bg-orange-100 text-orange-800',
|
||||||
|
array: 'bg-red-100 text-red-800',
|
||||||
|
null: 'bg-gray-100 text-gray-800',
|
||||||
|
undefined: 'bg-gray-100 text-gray-800'
|
||||||
|
};
|
||||||
|
return colors[type] || 'bg-gray-100 text-gray-800';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatValue = (value, type) => {
|
||||||
|
if (type === 'string') return `"${value}"`;
|
||||||
|
if (type === 'null') return 'null';
|
||||||
|
if (type === 'undefined') return 'undefined';
|
||||||
|
if (type === 'boolean') return value ? 'true' : 'false';
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getKeyDisplay = (key, level) => {
|
||||||
|
const parts = key.split(/[.\[\]]+/).filter(Boolean);
|
||||||
|
return parts[parts.length - 1] || key;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search keys or values..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10 pr-4 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Type Filter */}
|
||||||
|
<div className="relative">
|
||||||
|
<Filter className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
|
<select
|
||||||
|
value={filterType}
|
||||||
|
onChange={(e) => setFilterType(e.target.value)}
|
||||||
|
className="pl-10 pr-8 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent appearance-none bg-white"
|
||||||
|
>
|
||||||
|
<option value="all">All Types</option>
|
||||||
|
<option value="string">String</option>
|
||||||
|
<option value="number">Number</option>
|
||||||
|
<option value="boolean">Boolean</option>
|
||||||
|
<option value="object">Object</option>
|
||||||
|
<option value="array">Array</option>
|
||||||
|
<option value="null">Null</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="overflow-auto max-h-96">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-gray-50 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Key
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-16">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{filteredData.map((item, index) => (
|
||||||
|
<tr
|
||||||
|
key={`${item.path}-${index}`}
|
||||||
|
className="hover:bg-gray-50 transition-colors duration-150"
|
||||||
|
>
|
||||||
|
{/* Key Column */}
|
||||||
|
<td className="px-4 py-3 text-sm">
|
||||||
|
<div
|
||||||
|
className="flex items-center"
|
||||||
|
style={{ paddingLeft: `${item.level * 20}px` }}
|
||||||
|
>
|
||||||
|
{item.isExpandable ? (
|
||||||
|
<button
|
||||||
|
onClick={() => toggleExpanded(item.path)}
|
||||||
|
className="mr-2 p-1 hover:bg-gray-200 rounded transition-colors"
|
||||||
|
>
|
||||||
|
{expandedPaths.has(item.path) ? (
|
||||||
|
<ChevronDown className="w-4 h-4 text-gray-600" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-4 h-4 text-gray-600" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="w-6 mr-2" />
|
||||||
|
)}
|
||||||
|
<span className="font-mono text-gray-900">
|
||||||
|
{getKeyDisplay(item.key, item.level)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Type Column */}
|
||||||
|
<td className="px-4 py-3 text-sm">
|
||||||
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getTypeColor(item.type)}`}>
|
||||||
|
{item.type}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Value Column */}
|
||||||
|
<td className="px-4 py-3 text-sm">
|
||||||
|
<div className="font-mono text-gray-900 max-w-xs truncate">
|
||||||
|
{formatValue(item.value, item.type)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Actions Column */}
|
||||||
|
<td className="px-4 py-3 text-sm">
|
||||||
|
{!item.isExpandable && (
|
||||||
|
<CopyButton
|
||||||
|
text={String(item.value)}
|
||||||
|
className="p-1 hover:bg-gray-200 rounded transition-colors"
|
||||||
|
>
|
||||||
|
<Copy className="w-4 h-4 text-gray-600" />
|
||||||
|
</CopyButton>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="bg-gray-50 px-4 py-2 border-t border-gray-200">
|
||||||
|
<div className="flex items-center justify-between text-sm text-gray-600">
|
||||||
|
<span>
|
||||||
|
Showing {filteredData.length} of {flatData.length} items
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{Object.keys(data || {}).length} root properties
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostmanTreeTable;
|
||||||
@@ -232,14 +232,67 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTypeIcon = (value) => {
|
const getTypeIcon = (value) => {
|
||||||
if (value === null) return <span className="text-gray-500">∅</span>;
|
if (value === null) {
|
||||||
if (value === undefined) return <span className="text-gray-400">?</span>;
|
return (
|
||||||
if (typeof value === 'string') return <Type className="h-4 w-4 text-green-600" />;
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300">
|
||||||
if (typeof value === 'number') return <Hash className="h-4 w-4 text-blue-600" />;
|
<span className="text-xs font-medium">-</span>
|
||||||
if (typeof value === 'boolean') return <ToggleLeft className="h-4 w-4 text-purple-600" />;
|
</span>
|
||||||
if (Array.isArray(value)) return <List className="h-4 w-4 text-orange-600" />;
|
);
|
||||||
if (typeof value === 'object') return <Braces className="h-4 w-4 text-red-600" />;
|
}
|
||||||
return <Type className="h-4 w-4 text-gray-600" />;
|
|
||||||
|
if (value === undefined) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300">
|
||||||
|
<span className="text-xs font-medium">?</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300">
|
||||||
|
<Type className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-300">
|
||||||
|
<Hash className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300">
|
||||||
|
<ToggleLeft className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300">
|
||||||
|
<List className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300">
|
||||||
|
<Braces className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300">
|
||||||
|
<Type className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderValue = (value, key, path, parentPath) => {
|
const renderValue = (value, key, path, parentPath) => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useState, useRef, useCallback } from 'react';
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
import { Edit3, Upload, FileText, Map } from 'lucide-react';
|
import { Edit3, Upload, FileText, Map, Table } 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';
|
||||||
import MindmapView from '../components/MindmapView';
|
import MindmapView from '../components/MindmapView';
|
||||||
|
import PostmanTable from '../components/PostmanTable';
|
||||||
|
|
||||||
const ObjectEditor = () => {
|
const ObjectEditor = () => {
|
||||||
console.log(' ObjectEditor component loaded successfully!');
|
console.log(' ObjectEditor component loaded successfully!');
|
||||||
@@ -13,7 +14,8 @@ const ObjectEditor = () => {
|
|||||||
const [inputFormat, setInputFormat] = useState('');
|
const [inputFormat, setInputFormat] = useState('');
|
||||||
const [inputValid, setInputValid] = useState(false);
|
const [inputValid, setInputValid] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [viewMode, setViewMode] = useState('visual'); // 'visual', 'mindmap'
|
const [viewMode, setViewMode] = useState('visual'); // 'visual', 'mindmap', 'table'
|
||||||
|
const [showOutputs, setShowOutputs] = useState(false);
|
||||||
const [outputs, setOutputs] = useState({
|
const [outputs, setOutputs] = useState({
|
||||||
jsonPretty: '',
|
jsonPretty: '',
|
||||||
jsonMinified: '',
|
jsonMinified: '',
|
||||||
@@ -293,20 +295,43 @@ const ObjectEditor = () => {
|
|||||||
// Load sample data
|
// Load sample data
|
||||||
const loadSample = () => {
|
const loadSample = () => {
|
||||||
const sample = {
|
const sample = {
|
||||||
"user": {
|
"users": [
|
||||||
"name": "John Doe",
|
{
|
||||||
"age": 30,
|
"id": 1,
|
||||||
"email": "john@example.com",
|
"name": "John Doe",
|
||||||
"preferences": {
|
"email": "john@example.com",
|
||||||
"theme": "dark",
|
"age": 30,
|
||||||
"notifications": true
|
"role": "admin",
|
||||||
|
"active": true
|
||||||
},
|
},
|
||||||
"tags": ["developer", "javascript", "react"]
|
{
|
||||||
},
|
"id": 2,
|
||||||
|
"name": "Jane Smith",
|
||||||
|
"email": "jane@example.com",
|
||||||
|
"age": 28,
|
||||||
|
"role": "user",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Bob Wilson",
|
||||||
|
"email": "bob@example.com",
|
||||||
|
"age": 35,
|
||||||
|
"role": "moderator",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"theme": "dark",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"timezone": "UTC"
|
"timezone": "UTC",
|
||||||
}
|
"features": {
|
||||||
|
"notifications": true,
|
||||||
|
"darkMode": true,
|
||||||
|
"autoSave": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["developer", "javascript", "react", "nodejs"]
|
||||||
};
|
};
|
||||||
setStructuredData(sample);
|
setStructuredData(sample);
|
||||||
generateOutputs(sample);
|
generateOutputs(sample);
|
||||||
@@ -382,6 +407,17 @@ const ObjectEditor = () => {
|
|||||||
<Map className="h-4 w-4" />
|
<Map className="h-4 w-4" />
|
||||||
<span>Mindmap View</span>
|
<span>Mindmap View</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('table')}
|
||||||
|
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
|
||||||
|
viewMode === 'table'
|
||||||
|
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
|
||||||
|
: 'text-gray-600 dark:text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Table className="h-4 w-4" />
|
||||||
|
<span>Table View</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input Section */}
|
{/* Input Section */}
|
||||||
@@ -420,6 +456,7 @@ const ObjectEditor = () => {
|
|||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||||
{viewMode === 'visual' && 'Visual Editor'}
|
{viewMode === 'visual' && 'Visual Editor'}
|
||||||
{viewMode === 'mindmap' && 'Mindmap Visualization'}
|
{viewMode === 'mindmap' && 'Mindmap Visualization'}
|
||||||
|
{viewMode === 'table' && 'Table View'}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{viewMode === 'visual' && (
|
{viewMode === 'visual' && (
|
||||||
@@ -435,10 +472,29 @@ const ObjectEditor = () => {
|
|||||||
<MindmapView data={structuredData} />
|
<MindmapView data={structuredData} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{viewMode === 'table' && (
|
||||||
|
<PostmanTable
|
||||||
|
data={structuredData}
|
||||||
|
title="JSON Data Structure"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle Output Button */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowOutputs(!showOutputs)}
|
||||||
|
className="flex items-center space-x-2 tool-button-secondary"
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
<span>{showOutputs ? 'Hide Data Outputs' : 'See Data Outputs'}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Output Actions */}
|
{/* Output Actions */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
{showOutputs && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
{/* JSON Pretty */}
|
{/* JSON Pretty */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
@@ -487,6 +543,7 @@ const ObjectEditor = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Usage Tips */}
|
{/* Usage Tips */}
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
|
||||||
@@ -494,10 +551,12 @@ const ObjectEditor = () => {
|
|||||||
<ul className="text-blue-700 dark:text-blue-300 text-sm space-y-1">
|
<ul className="text-blue-700 dark:text-blue-300 text-sm space-y-1">
|
||||||
<li>• <strong>Visual Editor:</strong> Create and modify object structures with forms</li>
|
<li>• <strong>Visual Editor:</strong> Create and modify object structures with forms</li>
|
||||||
<li>• <strong>Mindmap View:</strong> Visualize complex JSON structures as interactive diagrams</li>
|
<li>• <strong>Mindmap View:</strong> Visualize complex JSON structures as interactive diagrams</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>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>• 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>• 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>
|
||||||
<li>• Use copy buttons to quickly copy any output format</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</ToolLayout>
|
</ToolLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user