Have you ever wished your APEX reports were as flexible as moving JIRA tickets across your board?
Well, now they can be! In this post, I’ll walk you through creating a drag-and-drop column reordering and hiding system that exports exactly what you see into a polished PDF. Fancy? Yes. Complex? Not anymore.
While this blog post explains the concepts and code step by step, the YouTube video gives you a complete visual walkthrough. You'll see how each method works in a real APEX application, how to test it live, and understand the logic better with practical examples. It's perfect if you prefer learning by seeing it in action!
🧩 Problem Statement
We needed a way to:
- Rearrange report columns dynamically
- Hide/show specific columns as needed
- Export the visible columns only in the same order to a PDF
- All without refreshing the page or jumping into configuration popups
✅ Solution Overview
We will:
- Create a RESTful GET endpoint to fetch data
- Use SortableJS to manage drag-and-drop zones
- Render the table dynamically with JavaScript
- Export visible columns into a PDF using
jsPDF
andautoTable
🔧 Step 1: Create the RESTful Service
Module: Reorder Columns
Resource Template: /resourceTemplate
GET SQL:
SELECT
SEQUENCE_ORDER AS "Sequence Order",
STATUS_ID AS "Status Id",
PRIORITY AS "Priority",
ID,
PROJECT_SPRINT_ID AS "Project Sprint ID",
PROJECT_UNIQUE_ID AS "Project Unique ID",
SHORT_TITLE AS "Short Title",
DESCRIPTION AS "Description",
ESTIMATION,
ASSIGNED_TO,
REPORTER
FROM TMS_TASKS
💡 Step 2: Required Libraries
https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js
https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.28/jspdf.plugin.autotable.min.js
🎨 Step 3: Drag-and-Drop CSS Styling
This CSS makes your chips and dropzones look 🔥🔥.dropzone {
display: flex;
gap: 10px;
flex-wrap: wrap;
padding: 10px;
margin-bottom: 15px;
border: 2px dashed #ccc;
border-radius: 10px;
}
#column-dropzone {
background: #f4f6f8;
}
#hidden-dropzone {
background: #eee6e4;
}
.draggable-chip {
color: white;
padding: 6px 12px;
border-radius: 20px;
font-weight: bold;
text-transform: uppercase;
cursor: grab;
user-select: none;
transition: transform 0.2s;
}
#column-dropzone .draggable-chip {
background: linear-gradient(to right, #6dd5ed, #2193b0);
}
#hidden-dropzone .draggable-chip {
background: linear-gradient(to right, #f2709c, #ff9472);
}
.draggable-chip:hover {
transform: scale(1.05);
}
🧠Step 4: JavaScript Logic – Brains of the Operation
This JavaScript handles the rendering, dragging, hiding, reordering, and exporting. Don't forget to replace the 'your restful url here' with your actuat GET endpoint:
let originalData = [];
let columnOrder = [];
let hiddenColumns = [];
function createChip(col) {
const chip = document.createElement('div');
chip.className = 'draggable-chip';
chip.dataset.column = col;
chip.textContent = col.toUpperCase();
return chip;
}
function renderDropzone() {
const visibleZone = document.getElementById('column-dropzone');
const hiddenZone = document.getElementById('hidden-dropzone');
if (!visibleZone || !hiddenZone) return;
visibleZone.innerHTML = '';
hiddenZone.innerHTML = '';
columnOrder.forEach(col => visibleZone.appendChild(createChip(col)));
hiddenColumns.forEach(col => hiddenZone.appendChild(createChip(col)));
makeSortable(visibleZone, hiddenZone);
}
function makeSortable(visibleZone, hiddenZone) {
const commonOptions = {
group: 'columns',
animation: 150,
onEnd: () => {
columnOrder = Array.from(visibleZone.children).map(chip => chip.dataset.column);
hiddenColumns = Array.from(hiddenZone.children).map(chip => chip.dataset.column);
renderTable();
}
};
Sortable.create(visibleZone, commonOptions);
Sortable.create(hiddenZone, commonOptions);
}
function renderTable() {
const table = document.getElementById('dynamicTable');
if (!table) return;
const thead = table.querySelector('thead tr');
const tbody = table.querySelector('tbody');
if (!thead || !tbody) return;
thead.innerHTML = '';
tbody.innerHTML = '';
columnOrder.forEach(col => {
const th = document.createElement('th');
th.textContent = col.toUpperCase();
thead.appendChild(th);
});
originalData.forEach(row => {
const tr = document.createElement('tr');
columnOrder.forEach(col => {
const td = document.createElement('td');
td.textContent = row[col] ?? '';
tr.appendChild(td);
});
tbody.appendChild(tr);
});
}
async function fetchData() {
try {
const response = await fetch('your restful url here');
const json = await response.json();
originalData = json.items;
if (originalData.length > 0) {
const allCols = Object.keys(originalData[0]);
columnOrder = [...allCols];
hiddenColumns = [];
renderDropzone();
renderTable();
}
} catch (err) {
console.error('Error fetching data:', err);
}
}
document.getElementById('downloadBtn')?.addEventListener('click', async function () {
await new Promise(resolve => setTimeout(resolve, 0));
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ orientation: 'landscape' });
const tableData = originalData.map(row => columnOrder.map(col => row[col] ?? ''));
const tableHeader = columnOrder.map(c => c.toUpperCase());
const columnStyles = {};
columnOrder.forEach((_, i) => {
columnStyles[i] = {
cellWidth: 'wrap',
overflow: 'linebreak'
};
});
doc.autoTable({
head: [tableHeader],
body: tableData,
styles: {
fontSize: 8,
overflow: 'linebreak',
cellPadding: 2,
textColor: 20
},
headStyles: {
fillColor: [40, 40, 40],
textColor: 255,
fontStyle: 'bold'
},
columnStyles: columnStyles,
startY: 20,
margin: { top: 20, left: 10, right: 10 },
tableWidth: 'auto'
});
doc.save('column-order-report.pdf');
});
fetchData();
📦 Step 5: HTML Markup
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<div style="flex: 1;">
<h3>Visible Columns</h3>
<div id="column-dropzone" class="dropzone"></div>
</div>
<div style="flex: 1;">
<h3>Hidden Columns</h3>
<div id="hidden-dropzone" class="dropzone"></div>
</div>
</div>
<input type="button" id="downloadBtn" class="t-Button t-Button--hot" value="Download PDF"/>
<table id="dynamicTable">
<thead><tr></tr></thead>
<tbody></tbody>
</table>
🚀 Wrapping Up
You’ve now got a smart, snappy, and modern interface that empowers your users to control how they view and export data. Feels good, right? 😄
More content like this coming soon at Into The Oracle Verse!
data:post.title

Written by
Published on June 08, 2025
No comments: