Skip to content

Instantly share code, notes, and snippets.

@dexit
Created April 1, 2025 15:15
Show Gist options
  • Save dexit/bb9d11f66ba8cded3e5c0e30443de01e to your computer and use it in GitHub Desktop.
Save dexit/bb9d11f66ba8cded3e5c0e30443de01e to your computer and use it in GitHub Desktop.
Data maper
<!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>&gt;</option>
<option>&gt;=</option>
<option>&lt;</option>
<option>&lt;=</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