From e1c74e4a4e66640c057889b1f11331d6211de316 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 21 Sep 2025 16:33:28 +0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=AF=20Complete=20Postman-Style=20Table?= =?UTF-8?q?=20View=20with=20Consistent=20Design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ 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 --- src/components/PostmanTable.js | 341 +++++++++++++++++++++++++++++ src/components/PostmanTreeTable.js | 273 +++++++++++++++++++++++ src/components/StructuredEditor.js | 69 +++++- src/pages/ObjectEditor.js | 89 ++++++-- 4 files changed, 749 insertions(+), 23 deletions(-) create mode 100644 src/components/PostmanTable.js create mode 100644 src/components/PostmanTreeTable.js diff --git a/src/components/PostmanTable.js b/src/components/PostmanTable.js new file mode 100644 index 00000000..a76cb9a8 --- /dev/null +++ b/src/components/PostmanTable.js @@ -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: , + type: 'null' + }; + } + + if (Array.isArray(value)) { + return { + color: 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300', + icon: , + 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: , + type: 'object' + }; + case 'string': + return { + color: 'bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300', + icon: , + type: 'string' + }; + case 'number': + return { + color: 'bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-300', + icon: , + type: 'number' + }; + case 'boolean': + return { + color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300', + icon: , + type: 'boolean' + }; + default: + return { + color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300', + icon: , + 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 ( + + {typeStyle.icon} + {formattedValue} + + ); + }; + + // Get value type + const getValueType = (value) => { + if (value === null) return 'null'; + if (Array.isArray(value)) return 'array'; + return typeof value; + }; + + return ( +
+ {/* Header with Breadcrumb */} +
+
+
+ {currentPath.length > 0 && ( + + )} +
+ {getBreadcrumb().map((part, index) => ( + + {index > 0 && /} + + + ))} +
+
+
+ {isArrayView && `${currentData.length} items`} + {isObjectView && `${Object.keys(currentData).length} properties`} +
+
+
+ + {/* Content */} +
+ {isArrayView ? ( + // Horizontal table for arrays + + + + + {headers.map(header => ( + + ))} + + + + {currentData.map((item, index) => { + const isPrimitiveArray = headers.length === 1 && headers[0] === 'Value'; + return ( + handleRowClick(index)} + className="hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer transition-colors duration-150" + > + + {isPrimitiveArray ? ( + + ) : ( + headers.map(header => ( + + )) + )} + + ); + })} + +
+ # + + {header} +
+ {index} + + {renderValue(item)} + + {renderValue(item?.[header])} +
+ ) : isObjectView ? ( + // Vertical key-value table for objects + + + + + + + + + {Object.entries(currentData).map(([key, value]) => { + const isClickable = typeof value === 'object' && value !== null; + return ( + 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' + }`} + > + + + + ); + })} + +
+ Key + + Value +
+ {key} + + {renderValue(value)} +
+ ) : ( + // Fallback for primitive values +
+
+
+ {formatValue(currentData)} +
+
+ Type: {getValueType(currentData)} +
+
+
+ )} +
+ + {/* Footer */} +
+
+ + {isArrayView ? `Array with ${currentData.length} items` : + isObjectView ? `Object with ${Object.keys(currentData).length} properties` : + `Primitive value (${getValueType(currentData)})`} + + + Path: {currentPath.length === 0 ? 'Root' : currentPath.join(' → ')} + +
+
+
+ ); +}; + +export default PostmanTable; diff --git a/src/components/PostmanTreeTable.js b/src/components/PostmanTreeTable.js new file mode 100644 index 00000000..40c7ca8e --- /dev/null +++ b/src/components/PostmanTreeTable.js @@ -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 ( +
+ {/* Header */} +
+
+

{title}

+
+ {/* Search */} +
+ + 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" + /> +
+ + {/* Type Filter */} +
+ + +
+
+
+
+ + {/* Table */} +
+ + + + + + + + + + + {filteredData.map((item, index) => ( + + {/* Key Column */} + + + {/* Type Column */} + + + {/* Value Column */} + + + {/* Actions Column */} + + + ))} + +
+ Key + + Type + + Value + + Actions +
+
+ {item.isExpandable ? ( + + ) : ( +
+ )} + + {getKeyDisplay(item.key, item.level)} + +
+
+ + {item.type} + + +
+ {formatValue(item.value, item.type)} +
+
+ {!item.isExpandable && ( + + + + )} +
+
+ + {/* Footer */} +
+
+ + Showing {filteredData.length} of {flatData.length} items + + + {Object.keys(data || {}).length} root properties + +
+
+
+ ); +}; + +export default PostmanTreeTable; diff --git a/src/components/StructuredEditor.js b/src/components/StructuredEditor.js index b69851ae..a6fc267e 100644 --- a/src/components/StructuredEditor.js +++ b/src/components/StructuredEditor.js @@ -232,14 +232,67 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => { }; const getTypeIcon = (value) => { - if (value === null) return ; - if (value === undefined) return ?; - if (typeof value === 'string') return ; - if (typeof value === 'number') return ; - if (typeof value === 'boolean') return ; - if (Array.isArray(value)) return ; - if (typeof value === 'object') return ; - return ; + if (value === null) { + return ( + + - + + ); + } + + if (value === undefined) { + return ( + + ? + + ); + } + + if (typeof value === 'string') { + return ( + + + + ); + } + + if (typeof value === 'number') { + return ( + + + + ); + } + + if (typeof value === 'boolean') { + return ( + + + + ); + } + + if (Array.isArray(value)) { + return ( + + + + ); + } + + if (typeof value === 'object') { + return ( + + + + ); + } + + return ( + + + + ); }; const renderValue = (value, key, path, parentPath) => { diff --git a/src/pages/ObjectEditor.js b/src/pages/ObjectEditor.js index d72cb8d5..fa50c1c5 100644 --- a/src/pages/ObjectEditor.js +++ b/src/pages/ObjectEditor.js @@ -1,9 +1,10 @@ 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 CopyButton from '../components/CopyButton'; import StructuredEditor from '../components/StructuredEditor'; import MindmapView from '../components/MindmapView'; +import PostmanTable from '../components/PostmanTable'; const ObjectEditor = () => { console.log(' ObjectEditor component loaded successfully!'); @@ -13,7 +14,8 @@ const ObjectEditor = () => { const [inputFormat, setInputFormat] = useState(''); const [inputValid, setInputValid] = useState(false); 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({ jsonPretty: '', jsonMinified: '', @@ -293,20 +295,43 @@ const ObjectEditor = () => { // Load sample data const loadSample = () => { const sample = { - "user": { - "name": "John Doe", - "age": 30, - "email": "john@example.com", - "preferences": { - "theme": "dark", - "notifications": true + "users": [ + { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "age": 30, + "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": { + "theme": "dark", "language": "en", - "timezone": "UTC" - } + "timezone": "UTC", + "features": { + "notifications": true, + "darkMode": true, + "autoSave": false + } + }, + "tags": ["developer", "javascript", "react", "nodejs"] }; setStructuredData(sample); generateOutputs(sample); @@ -382,6 +407,17 @@ const ObjectEditor = () => { Mindmap View +