Created
April 1, 2025 15:15
-
-
Save dexit/bb9d11f66ba8cded3e5c0e30443de01e to your computer and use it in GitHub Desktop.
Data maper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Advanced Data Mapping API Tool</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src-min-noconflict/ace.min.js" type="text/javascript" charset="utf-8"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jsoneditor.min.js"></script> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/jsoneditor.min.css" rel="stylesheet" type="text/css"> | |
<style> | |
.json-viewer { | |
font-family: 'Courier New', monospace; | |
white-space: pre; | |
padding: 12px; | |
border-radius: 6px; | |
max-height: 300px; | |
overflow-y: auto; | |
background-color: #f8fafc; | |
border: 1px solid #e2e8f0; | |
font-size: 13px; | |
line-height: 1.5; | |
transition: all 0.3s; | |
} | |
.json-viewer.error { | |
background-color: #fef2f2; | |
border-color: #fecaca; | |
} | |
.json-viewer.success { | |
background-color: #f0fdf4; | |
border-color: #bbf7d0; | |
} | |
.draggable-item { | |
cursor: move; | |
transition: all 0.2s; | |
} | |
.draggable-item:hover { | |
transform: translateY(-2px); | |
background-color: #eff6ff !important; | |
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.05); | |
} | |
.drop-zone { | |
min-height: 100px; | |
transition: all 0.3s; | |
} | |
.drop-zone.active { | |
background-color: #f0f9ff; | |
border-color: #7dd3fc; | |
} | |
.field-path { | |
font-family: 'Courier New', monospace; | |
font-size: 12px; | |
color: #4b5563; | |
} | |
.flashing { | |
animation: flash 1s; | |
} | |
@keyframes flash { | |
0% { background-color: inherit; } | |
50% { background-color: #dbeafe; } | |
100% { background-color: inherit; } | |
} | |
.autocomplete-items { | |
position: absolute; | |
border: 1px solid #d4d4d4; | |
border-bottom: none; | |
border-top: none; | |
z-index: 99; | |
top: 100%; | |
left: 0; | |
right: 0; | |
max-height: 200px; | |
overflow-y: auto; | |
background-color: white; | |
} | |
.autocomplete-items div { | |
padding: 8px 12px; | |
cursor: pointer; | |
background-color: #fff; | |
border-bottom: 1px solid #d4d4d4; | |
font-size: 13px; | |
} | |
.autocomplete-items div:hover { | |
background-color: #f1f1f1; | |
} | |
.tooltip { | |
position: relative; | |
display: inline-block; | |
} | |
.tooltip .tooltip-text { | |
visibility: hidden; | |
width: 200px; | |
background-color: #1e293b; | |
color: #fff; | |
text-align: center; | |
padding: 5px; | |
border-radius: 4px; | |
position: absolute; | |
z-index: 1; | |
bottom: 125%; | |
left: 50%; | |
margin-left: -100px; | |
opacity: 0; | |
transition: opacity 0.3s; | |
font-size: 12px; | |
font-weight: normal; | |
} | |
.tooltip:hover .tooltip-text { | |
visibility: visible; | |
opacity: 1; | |
} | |
#editor-source, #editor-target { | |
height: 300px; | |
border-radius: 6px; | |
border: 1px solid #e2e8f0; | |
} | |
.ace_gutter { | |
background-color: #f8fafc !important; | |
} | |
.transformations-panel { | |
scrollbar-width: thin; | |
scrollbar-color: #cbd5e1 #f1f5f9; | |
} | |
.transformations-panel::-webkit-scrollbar { | |
width: 6px; | |
} | |
.transformations-panel::-webkit-scrollbar-track { | |
background: #f1f5f9; | |
} | |
.transformations-panel::-webkit-scrollbar-thumb { | |
background-color: #cbd5e1; | |
border-radius: 6px; | |
} | |
.resizer { | |
background-color: #e2e8f0; | |
height: 100%; | |
width: 5px; | |
cursor: col-resize; | |
transition: background-color 0.2s; | |
} | |
.resizer:hover { | |
background-color: #94a3b8; | |
} | |
@media (max-width: 1024px) { | |
.grid-cols-3 { | |
grid-template-columns: 1fr !important; | |
} | |
.resizable-container { | |
flex-direction: column; | |
} | |
.resizer { | |
width: 100%; | |
height: 5px; | |
cursor: row-resize; | |
} | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50"> | |
<div class="container mx-auto px-4 py-4 sm:py-8"> | |
<!-- Header Section --> | |
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4"> | |
<div> | |
<h1 class="text-2xl sm:text-3xl font-bold text-blue-600"> | |
<i class="fas fa-project-diagram mr-2"></i>Advanced Data Mapper | |
</h1> | |
<p class="text-sm text-gray-600 mt-1">Map, transform & integrate your API data with ease</p> | |
</div> | |
<div class="flex space-x-2"> | |
<button id="load-sample" class="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-2 rounded-lg flex items-center text-sm"> | |
<i class="fas fa-file-alt mr-2"></i> Load Sample | |
</button> | |
<button id="load-config" class="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-2 rounded-lg flex items-center text-sm"> | |
<i class="fas fa-folder-open mr-2"></i> Load Config | |
</button> | |
<button id="save-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center"> | |
<i class="fas fa-save mr-2"></i> Save Mapping | |
</button> | |
</div> | |
</div> | |
<!-- Resizable Source/Target Container --> | |
<div class="resizable-container flex flex-col lg:flex-row bg-white rounded-lg shadow overflow-hidden mb-6"> | |
<!-- Source Panel --> | |
<div class="flex-1 min-w-0 p-4 sm:p-6"> | |
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 gap-2"> | |
<h2 class="text-lg sm:text-xl font-semibold text-gray-800 flex items-center"> | |
<i class="fas fa-database mr-2 text-blue-500"></i>Source Data | |
</h2> | |
<div class="flex space-x-2"> | |
<button id="fetch-source" class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm flex items-center"> | |
<i class="fas fa-sync-alt mr-1"></i> Fetch | |
</button> | |
<button id="clear-source" class="bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded text-sm flex items-center"> | |
<i class="fas fa-trash-alt mr-1"></i> Clear | |
</button> | |
</div> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-sm font-medium text-gray-700 mb-1 flex items-center justify-between"> | |
<span>API Endpoint</span> | |
<div> | |
<button id="test-source" class="text-xs bg-blue-100 text-blue-600 px-2 py-0.5 rounded hover:bg-blue-200 mr-2"> | |
Test Connection | |
</button> | |
<button id="show-source-samples" class="text-xs bg-purple-100 text-purple-600 px-2 py-0.5 rounded hover:bg-purple-200"> | |
<i class="fas fa-lightbulb mr-1"></i> Samples | |
</button> | |
</div> | |
</label> | |
<div class="flex"> | |
<select id="source-method" class="bg-gray-100 border border-gray-300 rounded-l-lg px-3 py-2 text-sm w-20"> | |
<option>GET</option> | |
<option>POST</option> | |
<option>PUT</option> | |
</select> | |
<input id="source-url" type="text" placeholder="https://api.example.com/data" | |
class="flex-1 border border-gray-300 rounded-r-lg px-3 py-2 text-sm" value="https://jsonplaceholder.typicode.com/users"> | |
</div> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Headers</label> | |
<div id="source-headers" class="space-y-2"> | |
<div class="flex gap-2"> | |
<input type="text" placeholder="Header" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm" value="Content-Type"> | |
<input type="text" placeholder="Value" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm" value="application/json"> | |
<button class="add-header bg-blue-100 text-blue-600 px-2 rounded hover:bg-blue-200 h-8 self-center"> | |
<i class="fas fa-plus"></i> | |
</button> | |
</div> | |
</div> | |
</div> | |
<div class="relative mb-4"> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Request Body (for POST/PUT)</label> | |
<div id="editor-source" class="w-full border border-gray-300 rounded"></div> | |
<div class="absolute right-2 bottom-2 flex space-x-1"> | |
<button id="format-source-body" class="bg-gray-100 text-gray-600 p-1 rounded text-xs" title="Format JSON"> | |
<i class="fas fa-align-left"></i> | |
</button> | |
<button id="minify-source-body" class="bg-gray-100 text-gray-600 p-1 rounded text-xs" title="Minify JSON"> | |
<i class="fas fa-compress-alt"></i> | |
</button> | |
</div> | |
</div> | |
<div> | |
<div class="flex justify-between items-center mb-1"> | |
<label class="text-sm font-medium text-gray-700">Response Preview</label> | |
<div> | |
<button id="copy-source-json" class="text-xs text-blue-600 hover:text-blue-800 mr-2"> | |
<i class="fas fa-copy mr-1"></i> Copy | |
</button> | |
<button id="expand-source" class="text-xs text-blue-600 hover:text-blue-800"> | |
<i class="fas fa-expand mr-1"></i> Fullscreen | |
</button> | |
</div> | |
</div> | |
<div id="source-preview" class="json-viewer"> | |
No data fetched yet... | |
</div> | |
<div id="source-editor-container" class="hidden"> | |
<div id="jsoneditor-source" style="height: 400px;"></div> | |
</div> | |
</div> | |
</div> | |
<!-- Resizer --> | |
<div class="resizer" id="resizer"></div> | |
<!-- Target Panel --> | |
<div class="flex-1 min-w-0 p-4 sm:p-6"> | |
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 gap-2"> | |
<h2 class="text-lg sm:text-xl font-semibold text-gray-800 flex items-center"> | |
<i class="fas fa-cloud-upload-alt mr-2 text-green-500"></i>Target Data | |
</h2> | |
<div class="flex space-x-2"> | |
<button id="send-target" class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm flex items-center"> | |
<i class="fas fa-paper-plane mr-1"></i> Send | |
</button> | |
<button id="clear-target" class="bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded text-sm flex items-center"> | |
<i class="fas fa-trash-alt mr-1"></i> Clear | |
</button> | |
</div> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-sm font-medium text-gray-700 mb-1 flex items-center justify-between"> | |
<span>API Endpoint</span> | |
<button id="show-target-samples" class="text-xs bg-purple-100 text-purple-600 px-2 py-0.5 rounded hover:bg-purple-200"> | |
<i class="fas fa-lightbulb mr-1"></i> Samples | |
</button> | |
</label> | |
<div class="flex"> | |
<select id="target-method" class="bg-gray-100 border border-gray-300 rounded-l-lg px-3 py-2 text-sm w-20"> | |
<option>POST</option> | |
<option>PUT</option> | |
<option>PATCH</option> | |
</select> | |
<input id="target-url" type="text" placeholder="https://api.example.com/receive" | |
class="flex-1 border border-gray-300 rounded-r-lg px-3 py-2 text-sm" value="https://api.example.com/users"> | |
</div> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Headers</label> | |
<div id="target-headers" class="space-y-2"> | |
<div class="flex gap-2"> | |
<input type="text" placeholder="Header" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm" value="Content-Type"> | |
<input type="text" placeholder="Value" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm" value="application/json"> | |
<button class="add-header bg-blue-100 text-blue-600 px-2 rounded hover:bg-blue-200 h-8 self-center"> | |
<i class="fas fa-plus"></i> | |
</button> | |
</div> | |
</div> | |
</div> | |
<div> | |
<div class="flex justify-between items-center mb-1"> | |
<label class="text-sm font-medium text-gray-700">Mapped Output</label> | |
<div> | |
<button id="copy-target-json" class="text-xs text-blue-600 hover:text-blue-800 mr-2"> | |
<i class="fas fa-copy mr-1"></i> Copy | |
</button> | |
<button id="expand-target" class="text-xs text-blue-600 hover:text-blue-800"> | |
<i class="fas fa-expand mr-1"></i> Fullscreen | |
</button> | |
</div> | |
</div> | |
<div id="target-preview" class="json-viewer"> | |
No mapped data yet... | |
</div> | |
<div id="target-editor-container" class="hidden"> | |
<div id="jsoneditor-target" style="height: 400px;"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Mapping Panel --> | |
<div class="bg-white rounded-lg shadow p-4 sm:p-6 mb-6"> | |
<h2 class="text-lg sm:text-xl font-semibold text-gray-800 mb-4 flex items-center"> | |
<i class="fas fa-arrows-alt-h mr-2 text-purple-500"></i>Field Mapping | |
</h2> | |
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4"> | |
<!-- Source Fields --> | |
<div class="bg-gray-50 rounded-lg p-4"> | |
<div class="flex justify-between items-center mb-2"> | |
<h3 class="text-sm font-medium text-gray-700">Available Source Fields</h3> | |
<div class="flex space-x-1"> | |
<button id="explore-data" class="text-xs bg-blue-100 text-blue-600 px-2 py-0.5 rounded hover:bg-blue-200"> | |
<i class="fas fa-search mr-1"></i> Explore | |
</button> | |
<button id="refresh-fields" class="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded hover:bg-gray-200"> | |
<i class="fas fa-redo"></i> | |
</button> | |
</div> | |
</div> | |
<div class="h-64 overflow-y-auto transformations-panel" id="source-fields"> | |
<div class="text-gray-500 italic text-sm p-2">Fetch source data to see available fields</div> | |
</div> | |
</div> | |
<!-- Mapping Configuration --> | |
<div class="transformations-panel"> | |
<div class="mb-4"> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Target Field Name</label> | |
<input id="target-field-name" type="text" placeholder="e.g., full_name" | |
class="w-full border border-gray-300 rounded px-3 py-2 text-sm"> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-sm font-medium text-gray-700 mb-1"> | |
Transformation Type | |
<span class="tooltip ml-1"> | |
<i class="fas fa-info-circle text-gray-400"></i> | |
<span class="tooltip-text">Select how you want to transform the source data</span> | |
</span> | |
</label> | |
<select id="transformation-type" class="w-full border border-gray-300 rounded px-3 py-2 text-sm"> | |
<option value="direct">Direct Mapping</option> | |
<option value="concat">Concatenate Fields</option> | |
<option value="date">Date Formatting</option> | |
<option value="math">Mathematical Operation</option> | |
<option value="lookup">Value Lookup</option> | |
<option value="conditional">Conditional Logic</option> | |
<option value="custom">Custom JavaScript</option> | |
</select> | |
</div> | |
<!-- Transformation Settings (dynamic based on type) --> | |
<div id="transformation-settings" class="mb-4 p-3 bg-gray-50 rounded-lg border border-gray-200 h-48 overflow-y-auto"> | |
<p class="text-sm text-center text-gray-500">Select a transformation type to see options</p> | |
</div> | |
<div class="flex flex-col sm:flex-row justify-between gap-2"> | |
<button id="add-rule" class="bg-purple-500 hover:bg-purple-600 text-white px-3 py-2 rounded text-sm flex-grow flex items-center justify-center"> | |
<i class="fas fa-plus-circle mr-2"></i> Add Rule | |
</button> | |
<button id="test-mapping" class="bg-yellow-500 hover:bg-yellow-600 text-white px-3 py-2 rounded text-sm flex-grow flex items-center justify-center"> | |
<i class="fas fa-vial mr-2"></i> Test | |
</button> | |
</div> | |
</div> | |
<!-- Mapping Preview --> | |
<div class="transformations-panel"> | |
<div class="flex justify-between items-center mb-2"> | |
<h3 class="text-sm font-medium text-gray-700">Mapping Rules</h3> | |
<button id="clear-rules" class="text-xs text-red-600 hover:text-red-800"> | |
<i class="fas fa-trash-alt mr-1"></i> Clear All | |
</button> | |
</div> | |
<div id="rules-list" class="h-64 overflow-y-auto border rounded bg-gray-50 p-2 space-y-2"> | |
<div class="text-xs text-gray-500 italic p-2">No mapping rules added yet</div> | |
</div> | |
</div> | |
</div> | |
<!-- Mapping Fields Area --> | |
<div class="mt-4"> | |
<div class="flex justify-between items-center mb-1"> | |
<label class="text-sm font-medium text-gray-700">Source Field Mapping</label> | |
<button id="auto-map" class="text-xs text-blue-600 hover:text-blue-800"> | |
<i class="fas fa-magic mr-1"></i> Auto-Map | |
</button> | |
</div> | |
<div id="mapping-container" class="drop-zone border-2 border-dashed border-gray-300 rounded-lg p-4 mb-2 min-h-[100px]"> | |
<p class="text-gray-500 text-sm text-center">Drag source fields here or click to add</p> | |
</div> | |
<div class="autocomplete relative"> | |
<input id="field-search" type="text" placeholder="Search source fields..." | |
class="w-full border border-gray-300 rounded px-3 py-2 text-sm mt-2"> | |
<div id="autocomplete-results" class="autocomplete-items hidden"></div> | |
</div> | |
</div> | |
</div> | |
<!-- Data Explorer Modal --> | |
<div id="data-explorer-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 hidden"> | |
<div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col"> | |
<div class="flex justify-between items-center border-b px-6 py-4"> | |
<h3 class="text-lg font-semibold">Data Explorer</h3> | |
<button id="close-explorer" class="text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="flex flex-1 overflow-hidden"> | |
<div class="w-1/3 border-r p-4 overflow-y-auto" id="data-tree"> | |
<div class="text-gray-500 italic">Loading data structure...</div> | |
</div> | |
<div class="w-2/3 p-4 overflow-y-auto"> | |
<div class="mb-4"> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Selected Path</label> | |
<div class="flex"> | |
<input id="selected-path" type="text" class="flex-1 bg-gray-100 border border-gray-300 rounded-l-lg px-3 py-2 text-sm" readonly> | |
<button id="copy-path" class="bg-blue-500 text-white px-3 py-2 rounded-r-lg text-sm"> | |
<i class="fas fa-copy mr-1"></i> Copy | |
</button> | |
</div> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Value Preview</label> | |
<div id="path-preview" class="json-viewer bg-gray-50 min-h-24"> | |
Select a node to see its value | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="border-t px-6 py-3 flex justify-end"> | |
<button id="use-selected-path" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg"> | |
Use Selected Path | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Sample Data Modal --> | |
<div id="sample-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 hidden"> | |
<div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col"> | |
<div class="flex justify-between items-center border-b px-6 py-4"> | |
<h3 class="text-lg font-semibold" id="sample-modal-title">Sample Data</h3> | |
<button id="close-sample-modal" class="text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="flex-1 overflow-auto p-6"> | |
<div id="sample-content" class="space-y-4"> | |
<!-- Sample content will be loaded here --> | |
</div> | |
</div> | |
<div class="border-t px-6 py-3 flex justify-end"> | |
<button id="use-sample-data" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg"> | |
Use This Sample | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Initialize editors | |
const editorSource = ace.edit("editor-source"); | |
editorSource.setTheme("ace/theme/chrome"); | |
editorSource.session.setMode("ace/mode/json"); | |
editorSource.setOptions({ | |
fontSize: "12px", | |
showPrintMargin: false, | |
wrap: true | |
}); | |
let sourceEditor = null; | |
let targetEditor = null; | |
let sourceData = null; | |
let extractedFields = []; | |
let mappingRules = []; | |
// Sample data repositories | |
const sampleSourceAPIs = [ | |
{ | |
name: "JSONPlaceholder - Users", | |
url: "https://jsonplaceholder.typicode.com/users", | |
method: "GET", | |
description: "Sample user data with addresses and company info" | |
}, | |
{ | |
name: "JSONPlaceholder - Posts", | |
url: "https://jsonplaceholder.typicode.com/posts", | |
method: "GET", | |
description: "Sample blog post data with user IDs" | |
}, | |
{ | |
name: "JSONPlaceholder - Comments", | |
url: "https://jsonplaceholder.typicode.com/comments", | |
method: "GET", | |
description: "Sample comments with email addresses" | |
}, | |
{ | |
name: "ReqRes - Users", | |
url: "https://reqres.in/api/users", | |
method: "GET", | |
description: "Sample user data with pagination" | |
}, | |
{ | |
name: "Swapi - People", | |
url: "https://swapi.dev/api/people", | |
method: "GET", | |
description: "Star Wars characters data" | |
} | |
]; | |
const sampleTargetAPIs = [ | |
{ | |
name: "User Management API", | |
url: "https://api.example.com/users", | |
method: "POST", | |
description: "Endpoint for creating user records" | |
}, | |
{ | |
name: "CRM Contacts API", | |
url: "https://api.example.com/contacts", | |
method: "POST", | |
description: "Endpoint for adding CRM contacts" | |
}, | |
{ | |
name: "E-commerce Orders API", | |
url: "https://api.example.com/orders", | |
method: "POST", | |
description: "Endpoint for submitting orders" | |
}, | |
{ | |
name: "Analytics Events API", | |
url: "https://api.example.com/events", | |
method: "POST", | |
description: "Endpoint for tracking user events" | |
} | |
]; | |
// Initialize UI components | |
initHeaderButtons(); | |
initDragAndDrop(); | |
initAutocomplete(); | |
initTransformationSettings(); | |
initDataExplorer(); | |
initSampleModal(); | |
initUtilityButtons(); | |
initResizableColumns(); | |
// JSON Editor initialization | |
function initJSONEditors() { | |
if (!sourceEditor) { | |
sourceEditor = new JSONEditor(document.getElementById('jsoneditor-source'), { | |
mode: 'tree', | |
modes: ['code', 'tree', 'preview'], | |
onError: function(err) { | |
alert('JSON Editor Error: ' + err.toString()); | |
}, | |
onChange: function() { | |
try { | |
const json = sourceEditor.get(); | |
updateSourcePreview(json); | |
extractFieldsFromData(json); | |
populateFieldsList(); | |
} catch (e) { | |
console.error('Error parsing JSON:', e); | |
} | |
} | |
}); | |
sourceEditor.set({}); | |
} | |
if (!targetEditor) { | |
targetEditor = new JSONEditor(document.getElementById('jsoneditor-target'), { | |
mode: 'tree', | |
modes: ['code', 'tree', 'preview'], | |
onError: function(err) { | |
alert('JSON Editor Error: ' + err.toString()); | |
}, | |
search: false | |
}); | |
targetEditor.set({}); | |
} | |
} | |
function initResizableColumns() { | |
const resizer = document.getElementById('resizer'); | |
const container = document.querySelector('.resizable-container'); | |
let isResizing = false; | |
let lastX = 0; | |
let leftPanelWidth = 50; // Percentage | |
resizer.addEventListener('mousedown', (e) => { | |
isResizing = true; | |
lastX = e.clientX; | |
document.body.style.cursor = container.classList.contains('flex-col') ? 'row-resize' : 'col-resize'; | |
document.addEventListener('mousemove', handleMouseMove); | |
document.addEventListener('mouseup', () => { | |
isResizing = false; | |
document.body.style.cursor = ''; | |
document.removeEventListener('mousemove', handleMouseMove); | |
}); | |
}); | |
function handleMouseMove(e) { | |
if (!isResizing) return; | |
const panels = container.querySelectorAll('> div'); | |
if (panels.length !== 2) return; | |
const containerRect = container.getBoundingClientRect(); | |
if (container.classList.contains('flex-col')) { | |
// Vertical resizing | |
const dy = e.clientY - lastX; | |
const containerHeight = containerRect.height; | |
const topHeight = panels[0].offsetHeight; | |
const newTopHeight = topHeight + dy; | |
const minHeight = 200; | |
if (newTopHeight > minHeight && (containerHeight - newTopHeight) > minHeight) { | |
panels[0].style.height = newTopHeight + 'px'; | |
lastX = e.clientY; | |
} | |
} else { | |
// Horizontal resizing | |
const dx = e.clientX - lastX; | |
const containerWidth = containerRect.width; | |
const leftWidth = panels[0].offsetWidth; | |
const newLeftWidth = leftWidth + dx; | |
const minWidth = 300; | |
if (newLeftWidth > minWidth && (containerWidth - newLeftWidth) > minWidth) { | |
panels[0].style.width = newLeftWidth + 'px'; | |
lastX = e.clientX; | |
} | |
} | |
} | |
} | |
function initHeaderButtons() { | |
// Add header row buttons | |
document.querySelectorAll('.add-header').forEach(button => { | |
button.addEventListener('click', function() { | |
const headersContainer = this.closest('div').parentElement; | |
const newHeaderRow = document.createElement('div'); | |
newHeaderRow.className = 'flex gap-2'; | |
newHeaderRow.innerHTML = ` | |
<input type="text" placeholder="Header" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"> | |
<input type="text" placeholder="Value" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"> | |
<button class="remove-header bg-red-100 text-red-600 px-2 rounded hover:bg-red-200 h-8 self-center"> | |
<i class="fas fa-minus"></i> | |
</button> | |
`; | |
headersContainer.appendChild(newHeaderRow); | |
// Add event to the new remove button | |
newHeaderRow.querySelector('.remove-header').addEventListener('click', function() { | |
if (headersContainer.children.length > 1) { | |
headersContainer.removeChild(this.parentElement); | |
} | |
}); | |
}); | |
}); | |
} | |
function initDragAndDrop() { | |
// Setup drop zone for mappings | |
const mappingContainer = document.getElementById('mapping-container'); | |
mappingContainer.addEventListener('dragover', function(e) { | |
e.preventDefault(); | |
this.classList.add('active'); | |
e.dataTransfer.dropEffect = 'copy'; | |
}); | |
mappingContainer.addEventListener('dragleave', function() { | |
this.classList.remove('active'); | |
}); | |
mappingContainer.addEventListener('drop', function(e) { | |
e.preventDefault(); | |
this.classList.remove('active'); | |
const fieldName = e.dataTransfer.getData('text/plain'); | |
if (fieldName) { | |
const fieldInfo = extractedFields.find(f => f.path === fieldName); | |
if (fieldInfo) { | |
addMappingField(fieldInfo.path, fieldInfo.type); | |
} | |
} | |
}); | |
mappingContainer.addEventListener('click', function(e) { | |
if (e.target === this || e.target.tagName === 'P') { | |
// Show field selection modal | |
document.getElementById('data-explorer-modal').classList.remove('hidden'); | |
} | |
}); | |
} | |
function initAutocomplete() { | |
const fieldSearch = document.getElementById('field-search'); | |
const autocompleteResults = document.getElementById('autocomplete-results'); | |
fieldSearch.addEventListener('input', function() { | |
const searchTerm = this.value.toLowerCase(); | |
autocompleteResults.innerHTML = ''; | |
if (searchTerm.length < 1) { | |
autocompleteResults.classList.add('hidden'); | |
return; | |
} | |
const matches = extractedFields.filter(field => | |
field.path.toLowerCase().includes(searchTerm) || | |
field.type.toLowerCase().includes(searchTerm) | |
).slice(0, 10); | |
if (matches.length > 0) { | |
matches.forEach(field => { | |
const div = document.createElement('div'); | |
div.innerHTML = ` | |
<div class="flex justify-between"> | |
<span class="font-mono">${field.path}</span> | |
<span class="text-gray-500 text-xs">${field.type}</span> | |
</div> | |
<div class="text-xs text-gray-400 truncate">${field.example || ''}</div> | |
`; | |
div.addEventListener('click', function() { | |
addMappingField(field.path, field.type); | |
fieldSearch.value = ''; | |
autocompleteResults.classList.add('hidden'); | |
}); | |
autocompleteResults.appendChild(div); | |
}); | |
autocompleteResults.classList.remove('hidden'); | |
} else { | |
autocompleteResults.classList.add('hidden'); | |
} | |
}); | |
// Hide autocomplete when clicking elsewhere | |
document.addEventListener('click', function(e) { | |
if (e.target !== fieldSearch) { | |
autocompleteResults.classList.add('hidden'); | |
} | |
}); | |
} | |
function initTransformationSettings() { | |
const transformationType = document.getElementById('transformation-type'); | |
const settingsContainer = document.getElementById('transformation-settings'); | |
transformationType.addEventListener('change', updateTransformationSettings); | |
function updateTransformationSettings() { | |
const type = transformationType.value; | |
let html = ''; | |
switch (type) { | |
case 'direct': | |
html = ` | |
<p class="text-sm text-gray-700 mb-2">Copies the field value directly without transformation.</p> | |
<div class="flex items-center text-sm mt-2"> | |
<input type="checkbox" id="trim-space" class="mr-2"> | |
<label for="trim-space">Trim whitespace</label> | |
</div> | |
<div class="flex items-center text-sm mt-2"> | |
<input type="checkbox" id="null-handling" class="mr-2" checked> | |
<label for="null-handling">Convert null to empty string</label> | |
</div> | |
`; | |
break; | |
case 'concat': | |
html = ` | |
<p class="text-sm text-gray-700 mb-2">Combine multiple fields with a separator.</p> | |
<div class="mb-2"> | |
<label class="block text-xs text-gray-600 mb-1">Separator</label> | |
<select class="w-full border border-gray-300 rounded px-2 py-1 text-sm"> | |
<option value=" ">Space ( )</option> | |
<option value="-">Hyphen (-)</option> | |
<option value="_">Underscore (_)</option> | |
<option value=",">Comma (,)</option> | |
<option value="|">Pipe (|)</option> | |
<option value="/">Slash (/)</option> | |
<option value="custom">Custom...</option> | |
</select> | |
</div> | |
<div class="grid grid-cols-2 gap-4"> | |
<div class="flex items-center text-sm"> | |
<input type="checkbox" id="concat-trim" class="mr-2" checked> | |
<label for="concat-trim">Trim each field</label> | |
</div> | |
<div class="flex items-center text-sm"> | |
<input type="checkbox" id="concat-capitalize" class="mr-2"> | |
<label for="concat-capitalize">Capitalize</label> | |
</div> | |
</div> | |
<div class="flex items-center text-sm mt-2"> | |
<input type="checkbox" id="concat-unique" class="mr-2"> | |
<label for="concat-unique">Remove duplicate values</label> | |
</div> | |
`; | |
break; | |
case 'date': | |
html = ` | |
<p class="text-sm text-gray-700 mb-2">Convert between date formats.</p> | |
<div class="grid grid-cols-2 gap-2 mb-2"> | |
<div> | |
<label class="block text-xs text-gray-600 mb-1">Input Format</label> | |
<select class="w-full border border-gray-300 rounded px-2 py-1 text-sm"> | |
<option value="auto">Autodetect</option> | |
<option value="iso8601">ISO 8601</option> | |
<option value="mm/dd/yyyy">MM/DD/YYYY</option> | |
<option value="dd/mm/yyyy">DD/MM/YYYY</option> | |
<option value="yyyy-mm-dd">YYYY-MM-DD</option> | |
<option value="timestamp_ms">Timestamp (ms)</option> | |
<option value="timestamp_s">Timestamp (s)</option> | |
<option value="custom">Custom...</option> | |
</select> | |
</div> | |
<div> | |
<label class="block text-xs text-gray-600 mb-1">Output Format</label> | |
<select class="w-full border border-gray-300 rounded px-2 py-1 text-sm"> | |
<option value="yyyy-mm-dd">YYYY-MM-DD</option> | |
<option value="mm/dd/yyyy">MM/DD/YYYY</option> | |
<option value="dd/mm/yyyy">DD/MM/YYYY</option> | |
<option value="timestamp_ms">Unix Timestamp (ms)</option> | |
<option value="timestamp_s">Unix Timestamp (s)</option> | |
<option value="iso8601">ISO 8601</option> | |
<option value="rfc2822">RFC 2822</option> | |
<option value="custom">Custom...</option> | |
</select> | |
</div> | |
</div> | |
<div class="grid grid-cols-2 gap-4"> | |
<div class="flex items-center text-sm"> | |
<input type="checkbox" id="date-utc" class="mr-2" checked> | |
<label for="date-utc">Convert to UTC</label> | |
</div> | |
<div class="flex items-center text-sm"> | |
<input type="checkbox" id="date-validate" class="mr-2" checked> | |
<label for="date-validate">Validate dates</label> | |
</div> | |
</div> | |
<div class="mt-2"> | |
<label class="block text-xs text-gray-600 mb-1">On Invalid Date</label> | |
<select class="w-full border border-gray-300 rounded px-2 py-1 text-sm"> | |
<option value="null">Set to null</option> | |
<option value="empty">Set to empty string</option> | |
<option value="original">Keep original value</option> | |
<option value="error">Throw error</option> | |
</select> | |
</div> | |
`; | |
break; | |
case 'math': | |
html = ` | |
<p class="text-sm text-gray-700 mb-2">Perform a mathematical operation.</p> | |
<div class="mb-2"> | |
<label class="block text-xs text-gray-600 mb-1">Operation</label> | |
<select class="w-full border border-gray-300 rounded px-2 py-1 text-sm"> | |
<option value="add">Add (+)</option> | |
<option value="subtract">Subtract (-)</option> | |
<option value="multiply">Multiply (×)</option> | |
<option value="divide">Divide (÷)</option> | |
<option value="round">Round</option> | |
<option value="ceil">Ceiling</option> | |
<option value="floor">Floor</option> | |
<option value="custom">Custom Formula</option> | |
</select> | |
</div> | |
<div class="mb-2"> | |
<label class="block text-xs text-gray-600 mb-1">Operand</label> | |
<div class="flex items-center"> | |
<input type="text" placeholder="Value or field path" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"> | |
<button class="ml-2 text-xs bg-blue-100 text-blue-600 px-2 py-1 rounded hover:bg-blue-200"> | |
<i class="fas fa-plus"></i> Field | |
</button> | |
</div> | |
</div> | |
<div class="grid grid-cols-2 gap-4"> | |
<div class="flex items-center text-sm"> | |
<input type="checkbox" id="math-round" class="mr-2" checked> | |
<label for="math-round">Round to 2 decimals</label> | |
</div> | |
<div class="flex items-center text-sm"> | |
<input type="checkbox" id="math-validate" class="mr-2" checked> | |
<label for="math-validate">Validate numbers</label> | |
</div> | |
</div> | |
<div class="mt-2"> | |
<label class="block text-xs text-gray-600 mb-1">On Invalid Number</label> | |
<select class="w-full border border-gray-300 rounded px-2 py-1 text-sm"> | |
<option value="null">Set to null</option> | |
<option value="zero">Set to zero</option> | |
<option value="original">Keep original value</option> | |
<option value="error">Throw error</option> | |
</select> | |
</div> | |
`; | |
break; | |
case 'lookup': | |
html = ` | |
<p class="text-sm text-gray-700 mb-2">Map values using a lookup table.</p> | |
<div class="mb-2"> | |
<label class="block text-xs text-gray-600 mb-1">Lookup Table</label> | |
<div class="border rounded"> | |
<div class="grid grid-cols-2 gap-0 border-b bg-gray-50"> | |
<div class="p-2 text-xs font-medium">Source Value</div> | |
<div class="p-2 text-xs font-medium">Target Value</div> | |
</div> | |
<div class="lookup-items"> | |
<div class="grid grid-cols-2 gap-0 border-b"> | |
<input type="text" placeholder="Source" class="p-1 border-r text-sm"> | |
<input type="text" placeholder="Target" class="p-1 text-sm"> | |
</div> | |
<div class="grid grid-cols-2 gap-0 border-b"> | |
<input type="text" placeholder="Source" class="p-1 border-r text-sm"> | |
<input type="text" placeholder="Target" class="p-1 text-sm"> | |
</div> | |
</div> | |
</div> | |
<button class="add-lookup-item mt-1 text-xs bg-blue-100 text-blue-600 px-2 py-1 rounded hover:bg-blue-200 flex items-center justify-center w-full"> | |
<i class="fas fa-plus mr-1"></i> Add Item | |
</button> | |
</div> | |
<div class="mb-2"> | |
<label class="block text-xs text-gray-600 mb-1">Matching Type</label> | |
<select class="w-full border border-gray-300 rounded px-2 py-1 text-sm"> | |
<option value="exact">Exact Match</option> | |
<option value="contains">Contains</option> | |
<option value="starts">Starts With</option> | |
<option value="regex">Regular Expression</option> | |
</select> | |
</div> | |
<div class="mb-2"> | |
<label class="block text-xs text-gray-600 mb-1">Default Value</label> | |
<input type="text" placeholder="Used when no match found" class="w-full border border-gray-300 rounded px-2 py-1 text-sm"> | |
</div> | |
<div class="flex items-center text-sm"> | |
<input type="checkbox" id="lookup-case" class="mr-2" checked> | |
<label for="lookup-case">Case Sensitive</label> | |
</div> | |
`; | |
break; | |
case 'conditional': | |
html = ` | |
<p class="text-sm text-gray-700 mb-2">Set value based on conditions.</p> | |
<div class="space-y-2 mb-2" id="condition-items"> | |
<div class="p-2 bg-gray-50 rounded border"> | |
<div class="grid grid-cols-12 gap-2 mb-2"> | |
<select class="col-span-4 border border-gray-300 rounded px-2 py-1 text-sm"> | |
<option>Field Value</option> | |
<option>Constant</option> | |
</select> | |
<select class="col-span-3 border border-gray-300 rounded px-2 py-1 text-sm"> | |
<option>==</option> | |
<option>!=</option> | |
<option>></option> | |
<option>>=</option> | |
<option><</option> | |
<option><=</option> | |
<option>contains</option> | |
</select> | |
<input type="text" class="col-span-5 border border-gray-300 rounded px-2 py-1 text-sm" placeholder="Comparison Value"> | |
</div> | |
<div class="flex items-center justify-between"> | |
<span class="text-xs text-gray-600">THEN set to:</span> | |
<input type="text" class="w-48 border border-gray-300 rounded px-2 py-1 text-sm" placeholder="Value or field"> | |
</div> | |
</div> | |
</div> | |
<button class="add-condition text-xs bg-blue-100 text-blue-600 px-2 py-1 rounded hover:bg-blue-200 w-full flex items-center justify-center"> | |
<i class="fas fa-plus mr-1"></i> Add Condition | |
</button> | |
<div class="mt-2"> | |
<label class="block text-xs text-gray-600 mb-1">Default Value</label> | |
<input type="text" placeholder="Used when no condition matches" class="w-full border border-gray-300 rounded px-2 py-1 text-sm"> | |
</div> | |
`; | |
break; | |
case 'custom': | |
html = ` | |
<p class="text-sm text-gray-700 mb-2">Write custom JavaScript to transform the value.</p> | |
<div class="mb-2"> | |
<div id="ace-custom-transform" style="height: 120px;" class="border border-gray-300 rounded"></div> | |
</div> | |
<div class="grid grid-cols-2 gap-4"> | |
<div class="flex items-center text-sm"> | |
<input type="checkbox" id="custom-async" class="mr-2"> | |
<label for="custom-async">Async function</label> | |
</div> | |
<div class="flex items-center text-sm"> | |
<input type="checkbox" id="custom-debug" class="mr-2" checked> | |
<label for="custom-debug">Debug Mode</label> | |
</div> | |
</div> | |
<div class="mt-2"> | |
<p class="text-xs text-gray-500">Available variables: <span class="font-mono">value</span>, <span class="font-mono">item</span>, <span class="font-mono">index</span>, <span class="font-mono">utils</span></p> | |
</div> | |
`; | |
// Initialize ACE editor for custom transformation | |
setTimeout(() => { | |
const editor = ace.edit("ace-custom-transform"); | |
editor.setTheme("ace/theme/chrome"); | |
editor.session.setMode("ace/mode/javascript"); | |
editor.setOptions({ | |
fontSize: "12px", | |
showPrintMargin: false, | |
highlightActiveLine: false | |
}); | |
editor.setValue("// Custom transformation function\n// Return the transformed value\nreturn value;", -1); | |
}, 50); | |
break; | |
default: | |
html = '<p class="text-sm text-center text-gray-500">Select a transformation type to see options</p>'; | |
} | |
settingsContainer.innerHTML = html; | |
} | |
// Initialize with default settings | |
updateTransformationSettings(); | |
} | |
function initDataExplorer() { | |
const explorerModal = document.getElementById('data-explorer-modal'); | |
const closeExplorer = document.getElementById('close-explorer'); | |
const useSelectedPath = document.getElementById('use-selected-path'); | |
const selectedPath = document.getElementById('selected-path'); | |
const copyPath = document.getElementById('copy-path'); | |
const pathPreview = document.getElementById('path-preview'); | |
const exploreDataBtn = document.getElementById('explore-data'); | |
// Open explorer | |
exploreDataBtn.addEventListener('click', function() { | |
if (!sourceData) { | |
alert('Please fetch source data first'); | |
return; | |
} | |
explorerModal.classList.remove('hidden'); | |
buildDataTree(sourceData); | |
}); | |
// Close explorer | |
closeExplorer.addEventListener('click', function() { | |
explorerModal.classList.add('hidden'); | |
}); | |
// Use selected path | |
useSelectedPath.addEventListener('click', function() { | |
if (selectedPath.value) { | |
// Find the field info | |
const path = selectedPath.value; | |
const fieldInfo = extractedFields.find(f => f.path === path) || | |
{ path: path, type: 'unknown' }; | |
addMappingField(fieldInfo.path, fieldInfo.type); | |
explorerModal.classList.add('hidden'); | |
} | |
}); | |
// Copy path | |
copyPath.addEventListener('click', function() { | |
if (selectedPath.value) { | |
navigator.clipboard.writeText(selectedPath.value); | |
// Show feedback | |
const originalText = this.innerHTML; | |
this.innerHTML = '<i class="fas fa-check mr-1"></i> Copied'; | |
this.className = 'bg-green-500 text-white px-3 py-2 rounded-r-lg text-sm'; | |
setTimeout(() => { | |
this.innerHTML = originalText; | |
this.className = 'bg-blue-500 text-white px-3 py-2 rounded-r-lg text-sm'; | |
}, 2000); | |
} | |
}); | |
// Build the data tree UI | |
function buildDataTree(data, parentPath = '') { | |
const dataTree = document.getElementById('data-tree'); | |
dataTree.innerHTML = ''; | |
if (!data) { | |
dataTree.innerHTML = '<div class="text-gray-500">No data available</div>'; | |
return; | |
} | |
const createTreeItem = (key, value, currentPath) => { | |
const item = document.createElement('div'); | |
item.className = 'tree-item pl-4 mb-1'; | |
const isObject = typeof value === 'object' && value !== null; | |
const isArray = Array.isArray(value); | |
const displayKey = isArray ? `[${key}]` : key; | |
const fullPath = currentPath ? `${currentPath}.${key}` : key; | |
item.innerHTML = ` | |
<div class="flex items-center py-1 cursor-pointer hover:bg-gray-100 rounded" data-path="${fullPath}"> | |
<span class="expand-icon mr-1 text-gray-500 w-4 text-center"> | |
${isObject || isArray ? '<i class="fas fa-caret-right"></i>' : ''} | |
</span> | |
<span class="key font-medium">${displayKey}</span> | |
<span class="type text-xs text-gray-500 ml-2"> | |
${isArray ? 'array' : typeof value} | |
</span> | |
</div> | |
`; | |
const contentDiv = item.querySelector('div'); | |
// Handle click on item to show preview | |
contentDiv.addEventListener('click', function(e) { | |
// Don't propagate if clicking the expand icon | |
if (e.target.classList.contains('expand-icon') || e.target.tagName === 'I') return; | |
selectedPath.value = fullPath; | |
// Show value preview | |
try { | |
let preview = value; | |
if (isObject || isArray) { | |
preview = JSON.stringify(value, null, 2); | |
} | |
pathPreview.textContent = preview; | |
pathPreview.style.color = '#000'; | |
} catch (err) { | |
pathPreview.textContent = 'Cannot display value'; | |
pathPreview.style.color = '#dc2626'; | |
} | |
}); | |
// Handle expand/collapse | |
const expandIcon = contentDiv.querySelector('.expand-icon i'); | |
if (expandIcon) { | |
expandIcon.addEventListener('click', function(e) { | |
e.stopPropagation(); | |
// Check if already expanded | |
const isExpanded = this.classList.contains('fa-caret-down'); | |
if (isExpanded) { | |
// Collapse | |
this.className = 'fas fa-caret-right'; | |
const childItems = item.querySelectorAll('.tree-item'); | |
childItems.forEach(child => child.remove()); | |
} else { | |
// Expand | |
this.className = 'fas fa-caret-down'; | |
if (isArray) { | |
// Display first 5 items for arrays | |
const limitedItems = value.slice(0, 5); | |
limitedItems.forEach((arrayItem, index) => { | |
const childItem = createTreeItem( | |
index, | |
arrayItem, | |
`${fullPath}[${index}]` | |
); | |
item.appendChild(childItem); | |
}); | |
if (value.length > 5) { | |
const moreItem = document.createElement('div'); | |
moreItem.className = 'pl-6 text-xs text-gray-500 italic'; | |
moreItem.textContent = `+ ${value.length - 5} more items...`; | |
item.appendChild(moreItem); | |
} | |
} else if (isObject) { | |
// Display object properties | |
Object.keys(value).forEach(key => { | |
const childItem = createTreeItem( | |
key, | |
value[key], | |
fullPath | |
); | |
item.appendChild(childItem); | |
}); | |
} | |
} | |
}); | |
} | |
return item; | |
}; | |
if (Array.isArray(data)) { | |
// Special handling for root array | |
data.slice(0, 10).forEach((item, index) => { | |
const treeItem = createTreeItem(index, item, parentPath); | |
dataTree.appendChild(treeItem); | |
}); | |
if (data.length > 10) { | |
const moreItem = document.createElement('div'); | |
moreItem.className = 'pl-4 text-xs text-gray-500 italic'; | |
moreItem.textContent = `+ ${data.length - 10} more items...`; | |
dataTree.appendChild(moreItem); | |
} | |
} else if (typeof data === 'object' && data !== null) { | |
// Object handling | |
Object.keys(data).forEach(key => { | |
const treeItem = createTreeItem(key, data[key], parentPath); | |
dataTree.appendChild(treeItem); | |
}); | |
} else { | |
// Primitive value | |
dataTree.appendChild(createTreeItem('value', data, parentPath)); | |
} | |
} | |
} | |
function initSampleModal() { | |
const sampleModal = document.getElementById('sample-modal'); | |
const closeSampleModal = document.getElementById('close-sample-modal'); | |
const showSourceSamples = document.getElementById('show-source-samples'); | |
const showTargetSamples = document.getElementById('show-target-samples'); | |
const useSampleData = document.getElementById('use-sample-data'); | |
showSourceSamples.addEventListener('click', function() { | |
showSampleOptions('Source API Samples', sampleSourceAPIs); | |
}); | |
showTargetSamples.addEventListener('click', function() { | |
showSampleOptions('Target API Samples', sampleTargetAPIs); | |
}); | |
closeSampleModal.addEventListener('click', function() { | |
sampleModal.classList.add('hidden'); | |
}); | |
useSampleData.addEventListener('click', function() { | |
const activeTab = document.querySelector('#sample-content .tab-active'); | |
if (!activeTab) return; | |
const sample = sampleSourceAPIs.concat(sampleTargetAPIs).find(s => s.name === activeTab.textContent); | |
if (!sample) return; | |
if (sampleSourceAPIs.includes(sample)) { | |
// Source sample | |
document.getElementById('source-url').value = sample.url; | |
document.getElementById('source-method').value = sample.method; | |
document.getElementById('fetch-source').click(); | |
} else { | |
// Target sample | |
document.getElementById('target-url').value = sample.url; | |
document.getElementById('target-method').value = sample.method; | |
} | |
sampleModal.classList.add('hidden'); | |
}); | |
function showSampleOptions(title, samples) { | |
document.getElementById('sample-modal-title').textContent = title; | |
const sampleContent = document.getElementById('sample-content'); | |
sampleContent.innerHTML = ''; | |
// Create tabs | |
const tabContainer = document.createElement('div'); | |
tabContainer.className = 'flex border-b'; | |
const contentContainer = document.createElement('div'); | |
contentContainer.className = 'p-4'; | |
samples.forEach((sample, index) => { | |
const tab = document.createElement('button'); | |
tab.className = `px-4 py-2 text-sm font-medium ${index === 0 ? 'tab-active border-b-2 border-blue-500 text-blue-600' : 'text-gray-500 hover:text-gray-700'}`; | |
tab.textContent = sample.name; | |
tab.addEventListener('click', function() { | |
// Update active tab | |
document.querySelectorAll('#sample-content .tab-active').forEach(e => { | |
e.classList.remove('tab-active', 'border-b-2', 'border-blue-500', 'text-blue-600'); | |
e.classList.add('text-gray-500', 'hover:text-gray-700'); | |
}); | |
this.classList.add('tab-active', 'border-b-2', 'border-blue-500', 'text-blue-600'); | |
this.classList.remove('text-gray-500', 'hover:text-gray-700'); | |
// Update content | |
contentContainer.innerHTML = ` | |
<h4 class="font-medium mb-2">${sample.name}</h4> | |
<p class="text-sm text-gray-700 mb-4">${sample.description}</p> | |
<div class="bg-gray-50 p-3 rounded"> | |
<div class="flex items-center mb-1"> | |
<span class="font-mono bg-blue-100 text-blue-800 px-2 py-0.5 rounded text-xs mr-2">${sample.method}</span> | |
<span class="text-sm">${sample.url}</span> | |
</div> | |
</div> | |
`; | |
}); | |
tabContainer.appendChild(tab); | |
}); | |
// Click first tab to populate content | |
if (samples.length > 0) { | |
tabContainer.children[0].click(); | |
} | |
sampleContent.appendChild(tabContainer); | |
sampleContent.appendChild(contentContainer); | |
sampleModal.classList.remove('hidden'); | |
} | |
} | |
function initUtilityButtons() { | |
// Format source body button | |
document.getElementById('format-source-body').addEventListener('click', function() { | |
try { | |
const value = editorSource.getValue(); | |
if (value) { | |
const obj = JSON.parse(value); | |
editorSource.setValue(JSON.stringify(obj, null, 2), -1); | |
} | |
} catch (e) { | |
editorSource.session.setAnnotations([{ | |
row: 0, | |
column: 0, | |
text: e.message, | |
type: "error" | |
}]); | |
} | |
}); | |
// Minify source body button | |
document.getElementById('minify-source-body').addEventListener('click', function() { | |
try { | |
const value = editorSource.getValue(); | |
if (value) { | |
const obj = JSON.parse(value); | |
editorSource.setValue(JSON.stringify(obj), -1); | |
} | |
} catch (e) { | |
editorSource.session.setAnnotations([{ | |
row: 0, | |
column: 0, | |
text: e.message, | |
type: "error" | |
}]); | |
} | |
}); | |
// Copy source JSON | |
document.getElementById('copy-source-json').addEventListener('click', function() { | |
const sourcePreview = document.getElementById('source-preview'); | |
if (sourcePreview.textContent && sourcePreview.textContent !== 'No data fetched yet...') { | |
navigator.clipboard.writeText(sourcePreview.textContent); | |
// Show feedback | |
const originalText = this.innerHTML; | |
this.innerHTML = '<i class="fas fa-check mr-1"></i> Copied'; | |
this.className = 'text-xs text-green-600 hover:text-green-800'; | |
setTimeout(() => { | |
this.innerHTML = originalText; | |
this.className = 'text-xs text-blue-600 hover:text-blue-800'; | |
}, 2000); | |
} | |
}); | |
// Copy target JSON | |
document.getElementById('copy-target-json').addEventListener('click', function() { | |
const targetPreview = document.getElementById('target-preview'); | |
if (targetPreview.textContent && targetPreview.textContent !== 'No mapped data yet...') { | |
navigator.clipboard.writeText(targetPreview.textContent); | |
// Show feedback | |
const originalText = this.innerHTML; | |
this.innerHTML = '<i class="fas fa-check mr-1"></i> Copied'; | |
this.className = 'text-xs text-green-600 hover:text-green-800'; | |
setTimeout(() => { | |
this.innerHTML = originalText; | |
this.className = 'text-xs text-blue-600 hover:text-blue-800'; | |
}, 2000); | |
} | |
}); | |
// Expand source preview | |
document.getElementById('expand-source').addEventListener('click', function() { | |
const preview = document.getElementById('source-preview'); | |
const container = document.getElementById('source-editor-container'); | |
if (container.classList.contains('hidden')) { | |
// Switch to editor | |
preview.classList.add('hidden'); | |
container.classList.remove('hidden'); | |
if (!sourceEditor) { | |
initJSONEditors(); | |
} | |
try { | |
const json = JSON.parse(preview.textContent); | |
sourceEditor.set(json); | |
} catch (e) { | |
console.error('Error parsing JSON:', e); | |
sourceEditor.set({}); | |
} | |
this.innerHTML = '<i class="fas fa-compress mr-1"></i> Collapse'; | |
} else { | |
// Switch back to preview | |
try { | |
const json = sourceEditor.get(); | |
const prettyJson = JSON.stringify(json, null, 2); | |
preview.textContent = prettyJson; | |
preview.className = 'json-viewer success'; | |
extractFieldsFromData(json); | |
populateFieldsList(); | |
sourceData = json; | |
} catch (e) { | |
preview.textContent = 'Error: ' + e.message; | |
preview.className = 'json-viewer error'; | |
} | |
container.classList.add('hidden'); | |
preview.classList.remove('hidden'); | |
this.innerHTML = '<i class="fas fa-expand mr-1"></i> Fullscreen'; | |
} | |
}); | |
// Expand target preview | |
document.getElementById('expand-target').addEventListener('click', function() { | |
const preview = document.getElementById('target-preview'); | |
const container = document.getElementById('target-editor-container'); | |
if (container.classList.contains('hidden')) { | |
// Switch to editor | |
preview.classList.add('hidden'); | |
container.classList.remove('hidden'); | |
if (!targetEditor) { | |
initJSONEditors(); | |
} | |
try { | |
const json = JSON.parse(preview.textContent); | |
targetEditor.set(json); | |
} catch (e) { | |
console.error('Error parsing JSON:', e); | |
targetEditor.set({}); | |
} | |
this.innerHTML = '<i class="fas fa-compress mr-1"></i> Collapse'; | |
} else { | |
// Switch back to preview | |
try { | |
const json = targetEditor.get(); | |
const prettyJson = JSON.stringify(json, null, 2); | |
preview.textContent = prettyJson; | |
preview.className = 'json-viewer success'; | |
} catch (e) { | |
preview.textContent = 'Error: ' + e.message; | |
preview.className = 'json-viewer error'; | |
} | |
container.classList.add('hidden'); | |
preview.classList.remove('hidden'); | |
this.innerHTML = '<i class="fas fa-expand mr-1"></i> Fullscreen'; | |
} | |
}); | |
// Refresh fields | |
document.getElementById('refresh-fields').addEventListener('click', function() { | |
if (sourceData) { | |
extractFieldsFromData(sourceData); | |
populateFieldsList(); | |
// Show feedback | |
this.innerHTML = '<i class="fas fa-redo fa-spin"></i>'; | |
setTimeout(() => { | |
this.innerHTML = '<i class="fas fa-redo"></i>'; | |
}, 1000); | |
} else { | |
alert('No source data to refresh'); | |
} | |
}); | |
// Load config button (example) | |
document.getElementById('load-config').addEventListener('click', function() { | |
alert('In a real implementation, this would open a file dialog to load a saved mapping configuration.'); | |
}); | |
// Clear all rules | |
document.getElementById('clear-rules').addEventListener('click', function() { | |
if (mappingRules.length > 0 && confirm('Are you sure you want to clear all mapping rules?')) { | |
mappingRules = []; | |
updateRulesList(); | |
} | |
}); | |
// Load sample data | |
document.getElementById('load-sample').addEventListener('click', function() { | |
// Load a sample from JSONPlaceholder | |
document.getElementById('source-url').value = 'https://jsonplaceholder.typicode.com/users'; | |
document.getElementById('source-method').value = 'GET'; | |
document.getElementById('fetch-source').click(); | |
// Set target sample | |
document.getElementById('target-url').value = 'https://api.example.com/users'; | |
document.getElementById('target-method').value = 'POST'; | |
// Add some sample mapping rules | |
setTimeout(() => { | |
if (extractedFields.length > 0) { | |
const idField = extractedFields.find(f => f.path.includes('id')); | |
const nameField = extractedFields.find(f => f.path.includes('name')); | |
const emailField = extractedFields.find(f => f.path.includes('email')); | |
if (idField) { | |
document.getElementById('target-field-name').value = 'user_id'; | |
addMappingField(idField.path, idField.type); | |
document.getElementById('transformation-type').value = 'direct'; | |
updateTransformationSettings(); | |
document.getElementById('add-rule').click(); | |
} | |
if (nameField) { | |
document.getElementById('target-field-name').value = 'full_name'; | |
addMappingField(nameField.path, nameField.type); | |
document.getElementById('transformation-type').value = 'direct'; | |
updateTransformationSettings(); | |
document.getElementById('add-rule').click(); | |
} | |
if (emailField) { | |
document.getElementById('target-field-name').value = 'email_address'; | |
addMappingField(emailField.path, emailField.type); | |
document.getElementById('transformation-type').value = 'direct'; | |
updateTransformationSettings(); | |
document.getElementById('add-rule').click(); | |
} | |
} | |
}, 1000); | |
}); | |
// Save mapping button | |
document.getElementById('save-btn').addEventListener('click', function() { | |
if (mappingRules.length === 0) { | |
alert('No mapping rules to save'); | |
return; | |
} | |
const mappingName = prompt('Enter a name for this mapping:'); | |
if (mappingName) { | |
const config = { | |
name: mappingName, | |
source: { | |
url: document.getElementById('source-url').value, | |
method: document.getElementById('source-method').value, | |
headers: getHeaders('source-headers'), | |
body: editorSource.getValue() | |
}, | |
target: { | |
url: document.getElementById('target-url').value, | |
method: document.getElementById('target-method').value, | |
headers: getHeaders('target-headers') | |
}, | |
mappings: mappingRules, | |
createdAt: new Date().toISOString() | |
}; | |
// In a real app, this would save to a file or server | |
console.log('Saving mapping:', config); | |
alert(`Mapping "${mappingName}" saved successfully!`); | |
this.innerHTML = '<i class="fas fa-check mr-2"></i> Saved'; | |
setTimeout(() => { | |
this.innerHTML = '<i class="fas fa-save mr-2"></i> Save Mapping'; | |
}, 2000); | |
} | |
}); | |
function getHeaders(containerId) { | |
const headers = []; | |
const headerElements = document.getElementById(containerId).children; | |
for (let i = 0; i < headerElements.length; i++) { | |
const inputs = headerElements[i].querySelectorAll('input'); | |
if (inputs[0].value && inputs[1].value) { | |
headers.push({ | |
name: inputs[0].value, | |
value: inputs[1].value | |
}); | |
} | |
} | |
return headers; | |
} | |
} | |
function updateSourcePreview(data) { | |
const sourcePreview = document.getElementById('source-preview'); | |
try { | |
const prettyJson = JSON.stringify(data, null, 2); | |
sourcePreview.textContent = prettyJson; | |
sourcePreview.style.color = '#000'; | |
sourcePreview.classList.remove('error'); | |
sourcePreview.classList.add('success'); | |
sourcePreview.classList.add('flashing'); | |
setTimeout(() => sourcePreview.classList.remove('flashing'), 1000); | |
} catch (error) { | |
sourcePreview.textContent = 'Error: ' + error.message; | |
sourcePreview.style.color = '#dc2626'; | |
sourcePreview.classList.add('error'); | |
} | |
} | |
function extractFieldsFromData(data, path = '', fields = []) { | |
if (typeof data === 'object' && data !== null) { | |
for (const key in data) { | |
const currentPath = path ? `${path}.${key}` : key; | |
const value = data[key]; | |
// Skip if the field is already processed (to avoid circular references) | |
if (fields.some(f => f.path === currentPath)) continue; | |
if (Array.isArray(value)) { | |
// For arrays, include the array itself and optionally its items | |
fields.push({ | |
path: currentPath, | |
type: 'array', | |
example: JSON.stringify(value.slice(0, 2)) + (value.length > 2 ? '...' : '') | |
}); | |
// Optionally include first few items | |
if (value.length > 0) { | |
extractFieldsFromData(value[0], `${currentPath}[0]`, fields); | |
} | |
} else if (typeof value === 'object' && value !== null) { | |
fields.push({ | |
path: currentPath, | |
type: 'object', | |
example: '{...}' | |
}); | |
extractFieldsFromData(value, currentPath, fields); | |
} else { | |
// Primitive value | |
fields.push({ | |
path: currentPath, | |
type: typeof value, | |
example: value | |
}); | |
} | |
} | |
} | |
extractedFields = fields; | |
return fields; | |
} | |
function populateFieldsList() { | |
const fieldsContainer = document.getElementById('source-fields'); | |
fieldsContainer.innerHTML = ''; | |
if (extractedFields.length === 0) { | |
fieldsContainer.innerHTML = '<div class="text-gray-500 italic text-sm p-2">No fields found in the data</div>'; | |
return; | |
} | |
extractedFields.forEach(field => { | |
const fieldElement = document.createElement('div'); | |
fieldElement.className = 'draggable-item bg-gradient-to-br from-blue-50 to-gray-50 text-gray-800 px-3 py-2 rounded text-sm cursor-move flex items-center justify-between border border-gray-200 mb-1'; | |
fieldElement.setAttribute('draggable', 'true'); | |
fieldElement.dataset.path = field.path; | |
fieldElement.dataset.type = field.type; | |
fieldElement.innerHTML = ` | |
<div class="overflow-hidden"> | |
<div class="field-path truncate font-medium">${field.path}</div> | |
<div class="text-xs text-gray-500 flex justify-between"> | |
<span class="bg-gray-200 px-1 rounded">${field.type}</span> | |
<span class="truncate ml-2">${String(field.example).substring(0, 30)}${String(field.example).length > 30 ? '...' : ''}</span> | |
</div> | |
</div> | |
<i class="fas fa-arrows-alt ml-2 text-blue-400"></i> | |
`; | |
fieldElement.addEventListener('dragstart', function(e) { | |
e.dataTransfer.setData('text/plain', field.path); | |
e.dataTransfer.effectAllowed = 'copy'; | |
}); | |
fieldElement.addEventListener('click', function() { | |
addMappingField(field.path, field.type); | |
}); | |
fieldsContainer.appendChild(fieldElement); | |
}); | |
} | |
function addMappingField(fieldPath, fieldType) { | |
const mappingContainer = document.getElementById('mapping-container'); | |
// Check if field is already added | |
if (Array.from(mappingContainer.children).some(el => | |
el.tagName !== 'P' && el.dataset.field === fieldPath)) { | |
return; | |
} | |
const mappingItem = document.createElement('div'); | |
mappingItem.className = 'bg-purple-50 border border-purple-100 text-purple-700 px-3 py-2 rounded text-sm mb-2 flex justify-between items-center'; | |
mappingItem.dataset.field = fieldPath; | |
mappingItem.innerHTML = ` | |
<div class="flex items-center"> | |
<span class="font-mono text-xs mr-2 bg-purple-100 px-2 py-0.5 rounded">${fieldType}</span> | |
<span class="truncate max-w-xs" title="${fieldPath}">${fieldPath.split('.').pop()}</span> | |
</div> | |
<div class="flex items-center"> | |
<button class="move-up text-purple-400 hover:text-purple-600 mr-1" title="Move up"> | |
<i class="fas fa-arrow-up text-xs"></i> | |
</button> | |
<button class="move-down text-purple-400 hover:text-purple-600 mr-1" title="Move down"> | |
<i class="fas fa-arrow-down text-xs"></i> | |
</button> | |
<button class="text-purple-400 hover:text-purple-600 mr-1" title="Edit"> | |
<i class="fas fa-edit text-xs"></i> | |
</button> | |
<button class="text-purple-400 hover:text-purple-600" title="Remove"> | |
<i class="fas fa-times text-xs"></i> | |
</button> | |
</div> | |
`; | |
// Move up button | |
mappingItem.querySelector('.move-up').addEventListener('click', function(e) { | |
e.stopPropagation(); | |
const prev = mappingItem.previousElementSibling; | |
if (prev && prev !== mappingContainer.firstChild) { | |
mappingContainer.insertBefore(mappingItem, prev); | |
} | |
}); | |
// Move down button | |
mappingItem.querySelector('.move-down').addEventListener('click', function(e) { | |
e.stopPropagation(); | |
const next = mappingItem.nextElementSibling; | |
if (next) { | |
mappingContainer.insertBefore(next, mappingItem); | |
} | |
}); | |
// Edit button | |
mappingItem.querySelectorAll('button')[2].addEventListener('click', function(e) { | |
e.stopPropagation(); | |
const newPath = prompt('Edit field path:', fieldPath); | |
if (newPath && newPath !== fieldPath) { | |
mappingItem.dataset.field = newPath; | |
mappingItem.querySelector('div > span:last-child').textContent = newPath.split('.').pop(); | |
mappingItem.querySelector('div > span:last-child').title = newPath; | |
} | |
}); | |
// Remove button | |
mappingItem.querySelectorAll('button')[3].addEventListener('click', function() { | |
mappingContainer.removeChild(mappingItem); | |
if (mappingContainer.children.length === 0) { | |
mappingContainer.innerHTML = '<p class="text-gray-500 text-sm text-center">Drag source fields here or click to add</p>'; | |
} | |
}); | |
// Click to edit | |
mappingItem.addEventListener('click', function(e) { | |
if (e.target.tagName !== 'BUTTON') { | |
const newPath = prompt('Edit field path:', fieldPath); | |
if (newPath && newPath !== fieldPath) { | |
mappingItem.dataset.field = newPath; | |
mappingItem.querySelector('div > span:last-child').textContent = newPath.split('.').pop(); | |
mappingItem.querySelector('div > span:last-child').title = newPath; | |
} | |
} | |
}); | |
// Remove the placeholder text if it exists | |
if (mappingContainer.children.length === 1 && mappingContainer.children[0].tagName === 'P') { | |
mappingContainer.innerHTML = ''; | |
} | |
mappingContainer.appendChild(mappingItem); | |
mappingContainer.scrollTop = mappingContainer.scrollHeight; | |
} | |
function findPotentialMatch(targetField, sourceFields) { | |
const simpleTarget = targetField.toLowerCase().replace(/[^a-z0-9]/g, ''); | |
// Try exact match first | |
const exactMatch = sourceFields.find(f => | |
f.path.toLowerCase().replace(/[^a-z0-9]/g, '') === simpleTarget | |
); | |
if (exactMatch) return exactMatch; | |
// Try partial match | |
const partialMatch = sourceFields.find(f => | |
f.path.toLowerCase().replace(/[^a-z0-9]/g, '').includes(simpleTarget) || | |
simpleTarget.includes(f.path.toLowerCase().replace(/[^a-z0-9]/g, '')) | |
); | |
if (partialMatch) return partialMatch; | |
// Try type-based matching | |
if (targetField.toLowerCase().includes('date')) { | |
const dateField = sourceFields.find(f => | |
f.type === 'string' && ( | |
f.path.toLowerCase().includes('date') || | |
f.path.toLowerCase().includes('time') | |
) | |
); | |
if (dateField) return dateField; | |
} else if (targetField.toLowerCase().includes('name')) { | |
const nameField = sourceFields.find(f => | |
f.path.toLowerCase().includes('name') && | |
(f.type === 'string' || f.type === 'object') | |
); | |
if (nameField) return nameField; | |
} else if (targetField.toLowerCase().includes('id')) { | |
const idField = sourceFields.find(f => | |
f.path.toLowerCase().includes('id') && | |
(f.type === 'string' || f.type === 'number') | |
); | |
if (idField) return idField; | |
} | |
// Return first field as fallback | |
return sourceFields.length > 0 ? sourceFields[0] : null; | |
} | |
function applyMappingRules(data) { | |
// Get all mapping items | |
const mappingItems = Array.from(document.getElementById('mapping-container').children) | |
.filter(el => el.tagName !== 'P') | |
.map(el => ({ | |
path: el.dataset.field, | |
type: document.getElementById('transformation-type').value, | |
target: document.getElementById('target-field-name').value || 'unnamed_field' | |
})); | |
if (mappingItems.length === 0) { | |
throw new Error('No mapping rules defined'); | |
} | |
// Process each item in source data | |
let results = []; | |
if (Array.isArray(data.users || data.items || data.results)) { | |
const items = data.users || data.items || data.results; | |
results = items.map((item, index) => { | |
const mappedItem = {}; | |
mappingItems.forEach(mapping => { | |
try { | |
// Get the value from source | |
const value = getValueFromPath(item, mapping.path); | |
// Apply transformation | |
const transformedValue = applyTransformation(value, mapping.type, index, item); | |
// Set to target | |
mappedItem[mapping.target] = transformedValue; | |
} catch (error) { | |
console.error(`Error mapping ${mapping.path}:`, error); | |
mappedItem[mapping.target] = null; | |
} | |
}); | |
return mappedItem; | |
}); | |
} else { | |
// Handle single object mapping | |
const mappedItem = {}; | |
mappingItems.forEach(mapping => { | |
try { | |
// Get the value from source | |
const value = getValueFromPath(data, mapping.path); | |
// Apply transformation | |
const transformedValue = applyTransformation(value, mapping.type, 0, data); | |
// Set to target | |
mappedItem[mapping.target] = transformedValue; | |
} catch (error) { | |
console.error(`Error mapping ${mapping.path}:`, error); | |
mappedItem[mapping.target] = null; | |
} | |
}); | |
results.push(mappedItem); | |
} | |
return results; | |
} | |
function getValueFromPath(obj, path) { | |
if (!path) return undefined; | |
// Handle array indices like "users[0].name" | |
const segments = path.split(/\.|\[|\]/).filter(Boolean); | |
return segments.reduce((acc, segment) => { | |
if (acc === undefined || acc === null) return undefined; | |
return acc[segment]; | |
}, obj); | |
} | |
function applyTransformation(value, type, index, item) { | |
// In a real app, this would use the settings from the transformation-settings container | |
switch (type) { | |
case 'direct': | |
// Direct mapping with optional trimming | |
if (value === null || value === undefined) return ''; | |
if (typeof value === 'string') return value.trim(); | |
return value; | |
case 'concat': | |
// Concatenation of multiple fields with separator | |
if (Array.isArray(value)) { | |
return value.join(' '); | |
} | |
return String(value); | |
case 'date': | |
// Date formatting | |
try { | |
if (typeof value === 'number' || /^\d+$/.test(value)) { | |
// Assuming timestamp | |
const timestamp = typeof value === 'number' ? value : parseInt(value, 10); | |
return new Date(timestamp).toISOString().split('T')[0]; | |
} else { | |
// Try parsing as date string | |
return new Date(value).toISOString().split('T')[0]; | |
} | |
} catch (e) { | |
return ''; | |
} | |
case 'math': | |
// Mathematical operation | |
if (value === null || value === undefined) return 0; | |
const num = typeof value === 'number' ? value : parseFloat(value); | |
if (isNaN(num)) return 0; | |
return Math.round(num * 100) / 100; // Round to 2 decimals | |
case 'lookup': | |
// Value lookup with fallback | |
if (value === 'active') return true; | |
if (value === 'inactive') return false; | |
return value || ''; | |
case 'conditional': | |
// Conditional logic | |
if (value === 'active') return 'ENABLED'; | |
if (value === 'inactive') return 'DISABLED'; | |
return 'UNKNOWN'; | |
case 'custom': | |
// Custom JavaScript transformation | |
try { | |
if (typeof value === 'string') return value.toUpperCase(); | |
if (typeof value === 'number') return value * 2; | |
return value; | |
} catch (e) { | |
console.error('Custom transformation error:', e); | |
return value; | |
} | |
default: | |
return value; | |
} | |
} | |
// Fetch source data button | |
document.getElementById('fetch-source').addEventListener('click', function() { | |
const url = document.getElementById('source-url').value; | |
const method = document.getElementById('source-method').value; | |
const headers = {}; | |
// Set headers from UI | |
const headerElements = document.getElementById('source-headers').children; | |
for (let i = 0; i < headerElements.length; i++) { | |
const inputs = headerElements[i].querySelectorAll('input'); | |
if (inputs[0].value && inputs[1].value) { | |
headers[inputs[0].value] = inputs[1].value; | |
} | |
} | |
// Get body if POST/PUT | |
const body = method === 'GET' ? null : editorSource.getValue(); | |
const sourcePreview = document.getElementById('source-preview'); | |
const targetPreview = document.getElementById('target-preview'); | |
// Reset previews | |
sourcePreview.textContent = 'Fetching data...'; | |
sourcePreview.className = 'json-viewer'; | |
targetPreview.textContent = 'No mapped data yet...'; | |
targetPreview.className = 'json-viewer'; | |
// In a real app, this would make an actual API call | |
this.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Fetching...'; | |
this.disabled = true; | |
// Simulate API call delay | |
setTimeout(() => { | |
try { | |
// For demo purposes, we'll use sample data based on the URL | |
let demoData = {}; | |
if (url.includes('jsonplaceholder.typicode.com/users')) { | |
demoData = [ | |
{ | |
id: 1, | |
name: "Leanne Graham", | |
username: "Bret", | |
email: "[email protected]", | |
address: { | |
street: "Kulas Light", | |
suite: "Apt. 556", | |
city: "Gwenborough", | |
zipcode: "92998-3874", | |
geo: { | |
lat: "-37.3159", | |
lng: "81.1496" | |
} | |
}, | |
phone: "1-770-736-8031 x56442", | |
website: "hildegard.org", | |
company: { | |
name: "Romaguera-Crona", | |
catchPhrase: "Multi-layered client-server neural-net", | |
bs: "harness real-time e-markets" | |
}, | |
status: "active" | |
}, | |
{ | |
id: 2, | |
name: "Ervin Howell", | |
username: "Antonette", | |
email: "[email protected]", | |
address: { | |
street: "Victor Plains", | |
suite: "Suite 879", | |
city: "Wisokyburgh", | |
zipcode: "90566-7771", | |
geo: { | |
lat: "-43.9509", | |
lng: "-34.4618" | |
} | |
}, | |
phone: "010-692-6593 x09125", | |
website: "anastasia.net", | |
company: { | |
name: "Deckow-Crist", | |
catchPhrase: "Proactive didactic contingency", | |
bs: "synergize scalable supply-chains" | |
}, | |
status: "inactive" | |
} | |
]; | |
} else if (url.includes('reqres.in/api/users')) { | |
demoData = { | |
page: 1, | |
per_page: 6, | |
total: 12, | |
total_pages: 2, | |
data: [ | |
{ | |
id: 1, | |
email: "[email protected]", | |
first_name: "George", | |
last_name: "Bluth", | |
avatar: "https://reqres.in/img/faces/1-image.jpg" | |
}, | |
{ | |
id: 2, | |
email: "[email protected]", | |
first_name: "Janet", | |
last_name: "Weaver", | |
avatar: "https://reqres.in/img/faces/2-image.jpg" | |
} | |
] | |
}; | |
} else { | |
// Generic sample data | |
demoData = { | |
id: 123, | |
name: "John Doe", | |
email: "[email protected]", | |
active: true, | |
roles: ["user", "admin"], | |
metadata: { | |
created_at: "2023-01-15T12:00:00Z", | |
last_login: "2023-05-20T08:30:00Z" | |
} | |
}; | |
} | |
sourceData = demoData; | |
const prettyJson = JSON.stringify(demoData, null, 2); | |
sourcePreview.textContent = prettyJson; | |
sourcePreview.style.color = '#000'; | |
sourcePreview.classList.remove('error'); | |
sourcePreview.classList.add('success'); | |
sourcePreview.classList.add('flashing'); | |
setTimeout(() => sourcePreview.classList.remove('flashing'), 1000); | |
extractFieldsFromData(demoData); | |
populateFieldsList(); | |
this.innerHTML = '<i class="fas fa-check mr-1"></i> Fetched'; | |
setTimeout(() => { | |
this.innerHTML = '<i class="fas fa-sync-alt mr-1"></i> Fetch'; | |
this.disabled = false; | |
}, 2000); | |
} catch (error) { | |
sourcePreview.textContent = 'Error: ' + error.message; | |
sourcePreview.style.color = '#dc2626'; | |
sourcePreview.classList.add('error'); | |
this.innerHTML = '<i class="fas fa-exclamation-circle mr-1"></i> Error'; | |
setTimeout(() => { | |
this.innerHTML = '<i class="fas fa-sync-alt mr-1"></i> Fetch'; | |
this.disabled = false; | |
}, 2000); | |
} | |
}, 1500); | |
}); | |
// Clear source data button | |
document.getElementById('clear-source').addEventListener('click', function() { | |
document.getElementById('source-preview').textContent = 'No data fetched yet...'; | |
document.getElementById('source-preview').className = 'json-viewer'; | |
document.getElementById('source-fields').innerHTML = '<div class="text-gray-500 italic text-sm p-2">Fetch source data to see available fields</div>'; | |
sourceData = null; | |
extractedFields = []; | |
document.getElementById('target-preview').textContent = 'No mapped data yet...'; | |
document.getElementById('target-preview').className = 'json-viewer'; | |
}); | |
// Test connection button | |
document.getElementById('test-source').addEventListener('click', function() { | |
const url = document.getElementById('source-url').value; | |
if (!url) { | |
alert('Please enter a URL first'); | |
return; | |
} | |
this.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Testing...'; | |
setTimeout(() => { | |
// Simulate connection test | |
const isSuccess = Math.random() > 0.3; // 70% chance of success | |
if (isSuccess) { | |
this.innerHTML = '<i class="fas fa-check-circle mr-1"></i> Connected'; | |
this.className = 'text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded'; | |
} else { | |
this.innerHTML = '<i class="fas fa-times-circle mr-1"></i> Failed'; | |
this.className = 'text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded'; | |
alert('Connection test failed. Please check the URL and try again.'); | |
} | |
setTimeout(() => { | |
this.innerHTML = 'Test Connection'; | |
this.className = 'text-xs bg-blue-100 text-blue-600 px-2 py-0.5 rounded hover:bg-blue-200'; | |
}, 3000); | |
}, 1500); | |
}); | |
// Auto-map button | |
document.getElementById('auto-map').addEventListener('click', function() { | |
if (!sourceData) { | |
alert('Please fetch source data first'); | |
return; | |
} | |
// Simple auto-mapping logic (in a real app this would be more sophisticated) | |
const mappingContainer = document.getElementById('mapping-container'); | |
mappingContainer.innerHTML = ''; | |
// Suggest mapping based on target field name | |
const targetField = document.getElementById('target-field-name').value; | |
if (targetField) { | |
const sourceField = findPotentialMatch(targetField, extractedFields); | |
if (sourceField) { | |
addMappingField(sourceField.path, sourceField.type); | |
return; | |
} | |
} | |
// If no match found, just add the first field | |
if (extractedFields.length > 0) { | |
const firstField = extractedFields[0]; | |
addMappingField(firstField.path, firstField.type); | |
} else { | |
alert('No fields available to map'); | |
} | |
}); | |
// Test mapping button | |
document.getElementById('test-mapping').addEventListener('click', function() { | |
const targetPreview = document.getElementById('target-preview'); | |
const targetBody = document.getElementById('target-body'); | |
if (!sourceData) { | |
alert('Please fetch source data first'); | |
return; | |
} | |
if (document.getElementById('mapping-container').children.length === 0 || | |
(document.getElementById('mapping-container').children.length === 1 && | |
document.getElementById('mapping-container').children[0].tagName === 'P')) { | |
alert('Please add at least one source field to map'); | |
return; | |
} | |
if (!document.getElementById('target-field-name').value) { | |
alert('Please enter a target field name'); | |
return; | |
} | |
try { | |
// Apply mapping rules to source data | |
const mappedData = { | |
results: applyMappingRules(sourceData) | |
}; | |
const prettyJson = JSON.stringify(mappedData, null, 2); | |
targetPreview.textContent = prettyJson; | |
targetPreview.style.color = '#000'; | |
targetPreview.classList.remove('error'); | |
targetPreview.classList.add('success'); | |
targetPreview.classList.add('flashing'); | |
setTimeout(() => targetPreview.classList.remove('flashing'), 1000); | |
targetBody.value = prettyJson; | |
// Show success message | |
this.innerHTML = '<i class="fas fa-check mr-2"></i> Mapping Successful'; | |
setTimeout(() => { | |
this.innerHTML = '<i class="fas fa-vial mr-2"></i> Test'; | |
}, 2000); | |
} catch (error) { | |
targetPreview.textContent = 'Error: ' + error.message; | |
targetPreview.style.color = '#dc2626'; | |
targetPreview.classList.add('error'); | |
this.innerHTML = '<i class="fas fa-exclamation-circle mr-2"></i> Error'; | |
setTimeout(() => { | |
this.innerHTML = '<i class="fas fa-vial mr-2"></i> Test'; | |
}, 2000); | |
} | |
}); | |
// Send to target button | |
document.getElementById('send-target').addEventListener('click', function() { | |
const targetUrl = document.getElementById('target-url').value; | |
if (!targetUrl) { | |
alert('Please enter a target API endpoint'); | |
return; | |
} | |
if (!sourceData) { | |
alert('Please fetch source data first'); | |
return; | |
} | |
// Get body from preview | |
const requestBody = document.getElementById('target-preview').textContent; | |
if (!requestBody || requestBody === 'No mapped data yet...') { | |
alert('No data to send. Please test the mapping first.'); | |
return; | |
} | |
// Simulate API call | |
this.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Sending...'; | |
setTimeout(() => { | |
// Simulate success/failure | |
const isSuccess = Math.random() > 0.7; // 30% chance of failure | |
if (isSuccess) { | |
alert(`Successfully sent data to: ${targetUrl}`); | |
this.innerHTML = '<i class="fas fa-check mr-1"></i> Sent'; | |
this.className = 'bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm flex items-center'; | |
} else { | |
alert(`Failed to send data to: ${targetUrl}\nPlease check your connection and try again.`); | |
this.innerHTML = '<i class="fas fa-times mr-1"></i> Failed'; | |
this.className = 'bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-sm flex items-center'; | |
} | |
setTimeout(() => { | |
this.innerHTML = '<i class="fas fa-paper-plane mr-1"></i> Send'; | |
this.className = 'bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm flex items-center'; | |
}, 3000); | |
}, 2000); | |
}); | |
// Clear target data button | |
document.getElementById('clear-target').addEventListener('click', function() { | |
document.getElementById('target-preview').textContent = 'No mapped data yet...'; | |
document.getElementById('target-preview').className = 'json-viewer'; | |
}); | |
// Add rule button | |
document.getElementById('add-rule').addEventListener('click', function() { | |
const targetField = document.getElementById('target-field-name').value; | |
if (!targetField) { | |
alert('Please enter a target field name'); | |
return; | |
} | |
const mappingItems = Array.from(document.getElementById('mapping-container').children) | |
.filter(el => el.tagName !== 'P') | |
.map(el => el.dataset.field); | |
if (mappingItems.length === 0) { | |
alert('Please add at least one source field to map'); | |
return; | |
} | |
const transformationType = document.getElementById('transformation-type').value; | |
// Add to rules list | |
const rule = { | |
target: targetField, | |
source: mappingItems, | |
type: transformationType, | |
settings: {} // In a real app, this would capture the settings | |
}; | |
mappingRules.push(rule); | |
updateRulesList(); | |
// Clear mapping container and target field | |
document.getElementById('mapping-container').innerHTML = | |
'<p class="text-gray-500 text-sm text-center">Drag source fields here or click to add</p>'; | |
document.getElementById('target-field-name').value = ''; | |
// Show success | |
this.innerHTML = '<i class="fas fa-check mr-2"></i> Rule Added'; | |
setTimeout(() => { | |
this.innerHTML = '<i class="fas fa-plus-circle mr-2"></i> Add Rule'; | |
}, 2000); | |
}); | |
function updateRulesList() { | |
const rulesList = document.getElementById('rules-list'); | |
rulesList.innerHTML = ''; | |
if (mappingRules.length === 0) { | |
rulesList.innerHTML = '<div class="text-xs text-gray-500 italic p-2">No mapping rules added yet</div>'; | |
return; | |
} | |
mappingRules.forEach((rule, index) => { | |
const ruleItem = document.createElement('div'); | |
ruleItem.className = 'bg-white border rounded p-2 hover:bg-gray-50 mb-1'; | |
ruleItem.dataset.index = index; | |
ruleItem.innerHTML = ` | |
<div class="flex justify-between items-center mb-1"> | |
<span class="font-medium">${rule.target}</span> | |
<div class="flex space-x-1"> | |
<button class="edit-rule text-xs text-blue-600 hover:text-blue-800"> | |
<i class="fas fa-edit"></i> | |
</button> | |
<button class="delete-rule text-xs text-red-600 hover:text-red-800"> | |
<i class="fas fa-trash-alt"></i> | |
</button> | |
</div> | |
</div> | |
<div class="text-xs text-gray-600 flex items-center"> | |
<span class="bg-gray-200 px-2 py-0.5 rounded mr-2">${rule.type}</span> | |
${rule.source.join(', ')} | |
</div> | |
`; | |
ruleItem.querySelector('.edit-rule').addEventListener('click', function(e) { | |
e.stopPropagation(); | |
editRule(index); | |
}); | |
ruleItem.querySelector('.delete-rule').addEventListener('click', function(e) { | |
e.stopPropagation(); | |
deleteRule(index); | |
}); | |
ruleItem.addEventListener('click', function() { | |
editRule(index); | |
}); | |
rulesList.appendChild(ruleItem); | |
}); | |
} | |
function editRule(index) { | |
const rule = mappingRules[index]; | |
if (!rule) return; | |
// Populate form with rule data | |
document.getElementById('target-field-name').value = rule.target; | |
document.getElementById('transformation-type').value = rule.type; | |
// Update transformation settings | |
updateTransformationSettings(); | |
// Clear and add source fields | |
const mappingContainer = document.getElementById('mapping-container'); | |
mappingContainer.innerHTML = ''; | |
rule.source.forEach(field => { | |
const fieldInfo = extractedFields.find(f => f.path === field) || { path: field, type: 'unknown' }; | |
addMappingField(fieldInfo.path, fieldInfo.type); | |
}); | |
// Remove the rule from the list (user can re-add after editing) | |
mappingRules.splice(index, 1); | |
updateRulesList(); | |
// Scroll to mapping panel | |
document.querySelector('.drop-zone').scrollIntoView({ behavior: 'smooth' }); | |
} | |
function deleteRule(index) { | |
if (confirm('Are you sure you want to delete this mapping rule?')) { | |
mappingRules.splice(index, 1); | |
updateRulesList(); | |
} | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment