|
| 1 | +/** |
| 2 | + * CloudScale Code Block Migrator - Admin JS |
| 3 | + */ |
| 4 | +(function () { |
| 5 | + 'use strict'; |
| 6 | + |
| 7 | + var scanBtn = document.getElementById('cs-scan-btn'); |
| 8 | + var migrateAllBtn = document.getElementById('cs-migrate-all-btn'); |
| 9 | + var statusEl = document.getElementById('cs-scan-status'); |
| 10 | + var resultsArea = document.getElementById('cs-results-area'); |
| 11 | + var modal = document.getElementById('cs-preview-modal'); |
| 12 | + var modalTitle = document.getElementById('cs-modal-title'); |
| 13 | + var modalBody = document.getElementById('cs-modal-body'); |
| 14 | + var modalMigrateBtn = document.getElementById('cs-modal-migrate-btn'); |
| 15 | + |
| 16 | + // Bail if the migrate tab elements aren't present |
| 17 | + if (!scanBtn || !modal) return; |
| 18 | + |
| 19 | + var scannedPosts = []; |
| 20 | + |
| 21 | + // ========================================================================= |
| 22 | + // Helpers |
| 23 | + // ========================================================================= |
| 24 | + |
| 25 | + function ajax(action, data, callback) { |
| 26 | + var fd = new FormData(); |
| 27 | + fd.append('action', action); |
| 28 | + fd.append('nonce', csMigrate.nonce); |
| 29 | + if (data) { |
| 30 | + Object.keys(data).forEach(function (k) { |
| 31 | + fd.append(k, data[k]); |
| 32 | + }); |
| 33 | + } |
| 34 | + fetch(csMigrate.ajaxUrl, { method: 'POST', body: fd }) |
| 35 | + .then(function (r) { return r.json(); }) |
| 36 | + .then(function (resp) { |
| 37 | + if (resp.success) { |
| 38 | + callback(null, resp.data); |
| 39 | + } else { |
| 40 | + callback(resp.data || 'Unknown error'); |
| 41 | + } |
| 42 | + }) |
| 43 | + .catch(function (err) { |
| 44 | + callback(err.message || 'Network error'); |
| 45 | + }); |
| 46 | + } |
| 47 | + |
| 48 | + function setStatus(msg, type) { |
| 49 | + statusEl.textContent = msg; |
| 50 | + statusEl.className = 'cs-status' + (type ? ' ' + type : ''); |
| 51 | + } |
| 52 | + |
| 53 | + function escHtml(str) { |
| 54 | + var div = document.createElement('div'); |
| 55 | + div.textContent = str; |
| 56 | + return div.innerHTML; |
| 57 | + } |
| 58 | + |
| 59 | + // ========================================================================= |
| 60 | + // Scan |
| 61 | + // ========================================================================= |
| 62 | + |
| 63 | + function doScan() { |
| 64 | + scanBtn.disabled = true; |
| 65 | + setStatus('Scanning...', ''); |
| 66 | + resultsArea.innerHTML = '<p class="cs-loading"><span class="cs-spinner"></span> Scanning all posts for legacy code blocks...</p>'; |
| 67 | + |
| 68 | + ajax('cs_migrate_scan', {}, function (err, data) { |
| 69 | + scanBtn.disabled = false; |
| 70 | + |
| 71 | + if (err) { |
| 72 | + setStatus('Scan failed: ' + err, 'error'); |
| 73 | + return; |
| 74 | + } |
| 75 | + |
| 76 | + scannedPosts = data.posts; |
| 77 | + |
| 78 | + if (data.total_posts === 0) { |
| 79 | + setStatus('No legacy code blocks found.', 'success'); |
| 80 | + resultsArea.innerHTML = '<p class="cs-migrate-hint">No posts with legacy code blocks were found. Everything is already migrated.</p>'; |
| 81 | + migrateAllBtn.disabled = true; |
| 82 | + return; |
| 83 | + } |
| 84 | + |
| 85 | + setStatus('Found ' + data.total_blocks + ' block(s) across ' + data.total_posts + ' post(s).', 'success'); |
| 86 | + migrateAllBtn.disabled = false; |
| 87 | + renderResults(data); |
| 88 | + }); |
| 89 | + } |
| 90 | + |
| 91 | + function renderResults(data) { |
| 92 | + var html = ''; |
| 93 | + |
| 94 | + // Summary |
| 95 | + html += '<div class="cs-migrate-summary">'; |
| 96 | + html += '<div class="cs-stat"><strong>' + data.total_posts + '</strong>Posts with legacy blocks</div>'; |
| 97 | + html += '<div class="cs-stat"><strong>' + data.total_blocks + '</strong>Total code blocks to migrate</div>'; |
| 98 | + html += '</div>'; |
| 99 | + |
| 100 | + // Table |
| 101 | + html += '<table class="cs-migrate-table">'; |
| 102 | + html += '<thead><tr>'; |
| 103 | + html += '<th>Post</th>'; |
| 104 | + html += '<th style="width:90px;text-align:center;">Blocks</th>'; |
| 105 | + html += '<th style="width:80px;">Status</th>'; |
| 106 | + html += '<th style="width:200px;">Actions</th>'; |
| 107 | + html += '</tr></thead>'; |
| 108 | + html += '<tbody>'; |
| 109 | + |
| 110 | + data.posts.forEach(function (post) { |
| 111 | + html += '<tr id="cs-row-' + post.id + '">'; |
| 112 | + html += '<td>'; |
| 113 | + html += '<div class="cs-post-title"><a href="' + escHtml(post.view_url) + '" target="_blank">' + escHtml(post.title) + '</a></div>'; |
| 114 | + html += '<div class="cs-post-meta">' + escHtml(post.date) + ' · ' + escHtml(post.status) + ' · ID: ' + post.id + '</div>'; |
| 115 | + html += '</td>'; |
| 116 | + html += '<td style="text-align:center;"><span class="cs-block-count">' + post.block_count + '</span></td>'; |
| 117 | + html += '<td class="cs-status-cell">Pending</td>'; |
| 118 | + html += '<td class="cs-actions">'; |
| 119 | + html += '<button class="button button-small cs-preview-btn" data-post-id="' + post.id + '">Preview</button> '; |
| 120 | + html += '<button class="button button-primary button-small cs-single-migrate-btn" data-post-id="' + post.id + '">Migrate</button>'; |
| 121 | + html += '</td>'; |
| 122 | + html += '</tr>'; |
| 123 | + }); |
| 124 | + |
| 125 | + html += '</tbody></table>'; |
| 126 | + |
| 127 | + resultsArea.innerHTML = html; |
| 128 | + |
| 129 | + // Bind preview buttons |
| 130 | + resultsArea.querySelectorAll('.cs-preview-btn').forEach(function (btn) { |
| 131 | + btn.addEventListener('click', function () { |
| 132 | + openPreview(parseInt(this.getAttribute('data-post-id'))); |
| 133 | + }); |
| 134 | + }); |
| 135 | + |
| 136 | + // Bind single migrate buttons |
| 137 | + resultsArea.querySelectorAll('.cs-single-migrate-btn').forEach(function (btn) { |
| 138 | + btn.addEventListener('click', function () { |
| 139 | + migrateSingle(parseInt(this.getAttribute('data-post-id')), this); |
| 140 | + }); |
| 141 | + }); |
| 142 | + } |
| 143 | + |
| 144 | + // ========================================================================= |
| 145 | + // Preview Modal |
| 146 | + // ========================================================================= |
| 147 | + |
| 148 | + function openPreview(postId) { |
| 149 | + modal.style.display = 'flex'; |
| 150 | + modalTitle.textContent = 'Loading preview...'; |
| 151 | + modalBody.innerHTML = '<p class="cs-loading"><span class="cs-spinner"></span> Loading block preview...</p>'; |
| 152 | + modalMigrateBtn.setAttribute('data-post-id', postId); |
| 153 | + |
| 154 | + ajax('cs_migrate_preview', { post_id: postId }, function (err, data) { |
| 155 | + if (err) { |
| 156 | + modalBody.innerHTML = '<p style="color:#d63638;">Error: ' + escHtml(err) + '</p>'; |
| 157 | + return; |
| 158 | + } |
| 159 | + |
| 160 | + modalTitle.textContent = data.title + ' (' + data.block_count + ' block' + (data.block_count !== 1 ? 's' : '') + ')'; |
| 161 | + |
| 162 | + var html = ''; |
| 163 | + data.blocks.forEach(function (block) { |
| 164 | + html += '<div class="cs-preview-block">'; |
| 165 | + html += '<div class="cs-preview-block-header">'; |
| 166 | + html += '<span class="cs-block-num">' + block.index + '</span>'; |
| 167 | + html += '<span class="cs-block-lang">' + escHtml(block.language) + '</span>'; |
| 168 | + html += '<span class="cs-block-lang" style="background:#f3e8ff;color:#7c3aed;">' + escHtml(block.type || 'wp:code') + '</span>'; |
| 169 | + html += '<span class="cs-block-firstline">' + escHtml(block.first_line) + '</span>'; |
| 170 | + html += '</div>'; |
| 171 | + html += '<div class="cs-preview-diff">'; |
| 172 | + html += '<div class="cs-preview-side cs-before">'; |
| 173 | + html += '<div class="cs-preview-side-label">Before (wp:code)</div>'; |
| 174 | + html += '<pre>' + block.original + '</pre>'; |
| 175 | + html += '</div>'; |
| 176 | + html += '<div class="cs-preview-side cs-after">'; |
| 177 | + html += '<div class="cs-preview-side-label">After (cloudscale/code-block)</div>'; |
| 178 | + html += '<pre>' + block.converted + '</pre>'; |
| 179 | + html += '</div>'; |
| 180 | + html += '</div>'; |
| 181 | + html += '</div>'; |
| 182 | + }); |
| 183 | + |
| 184 | + modalBody.innerHTML = html; |
| 185 | + }); |
| 186 | + } |
| 187 | + |
| 188 | + function closeModal() { |
| 189 | + modal.style.display = 'none'; |
| 190 | + } |
| 191 | + |
| 192 | + // Modal close handlers |
| 193 | + modal.querySelector('.cs-modal-backdrop').addEventListener('click', closeModal); |
| 194 | + modal.querySelectorAll('.cs-modal-close, .cs-modal-close-btn').forEach(function (el) { |
| 195 | + el.addEventListener('click', closeModal); |
| 196 | + }); |
| 197 | + |
| 198 | + // Migrate from modal |
| 199 | + modalMigrateBtn.addEventListener('click', function () { |
| 200 | + var postId = parseInt(this.getAttribute('data-post-id')); |
| 201 | + if (!postId) return; |
| 202 | + |
| 203 | + this.disabled = true; |
| 204 | + this.textContent = 'Migrating...'; |
| 205 | + |
| 206 | + ajax('cs_migrate_single', { post_id: postId }, function (err, data) { |
| 207 | + modalMigrateBtn.disabled = false; |
| 208 | + modalMigrateBtn.innerHTML = '<span class="dashicons dashicons-yes-alt"></span> Migrate This Post'; |
| 209 | + |
| 210 | + if (err) { |
| 211 | + alert('Migration failed: ' + err); |
| 212 | + return; |
| 213 | + } |
| 214 | + |
| 215 | + closeModal(); |
| 216 | + markRowMigrated(postId, data.blocks_migrated); |
| 217 | + setStatus(data.message, 'success'); |
| 218 | + }); |
| 219 | + }); |
| 220 | + |
| 221 | + // ========================================================================= |
| 222 | + // Single Migrate (from table button) |
| 223 | + // ========================================================================= |
| 224 | + |
| 225 | + function migrateSingle(postId, btn) { |
| 226 | + if (!confirm('Migrate all code blocks in this post to CloudScale format?')) return; |
| 227 | + |
| 228 | + btn.disabled = true; |
| 229 | + btn.textContent = 'Migrating...'; |
| 230 | + |
| 231 | + ajax('cs_migrate_single', { post_id: postId }, function (err, data) { |
| 232 | + if (err) { |
| 233 | + btn.disabled = false; |
| 234 | + btn.textContent = 'Migrate'; |
| 235 | + alert('Migration failed: ' + err); |
| 236 | + return; |
| 237 | + } |
| 238 | + |
| 239 | + markRowMigrated(postId, data.blocks_migrated); |
| 240 | + setStatus(data.message, 'success'); |
| 241 | + }); |
| 242 | + } |
| 243 | + |
| 244 | + function markRowMigrated(postId, blockCount) { |
| 245 | + var row = document.getElementById('cs-row-' + postId); |
| 246 | + if (!row) return; |
| 247 | + |
| 248 | + row.classList.add('cs-migrated'); |
| 249 | + |
| 250 | + var statusCell = row.querySelector('.cs-status-cell'); |
| 251 | + if (statusCell) { |
| 252 | + statusCell.innerHTML = '<span class="cs-migrated-badge"><span class="dashicons dashicons-yes"></span> Done</span>'; |
| 253 | + } |
| 254 | + |
| 255 | + var actionsCell = row.querySelector('.cs-actions'); |
| 256 | + if (actionsCell) { |
| 257 | + actionsCell.innerHTML = '<a href="' + getViewUrl(postId) + '" target="_blank" class="button button-small">View Post</a>'; |
| 258 | + } |
| 259 | + |
| 260 | + // Check if all rows are migrated |
| 261 | + var pending = document.querySelectorAll('.cs-single-migrate-btn'); |
| 262 | + if (pending.length === 0) { |
| 263 | + migrateAllBtn.disabled = true; |
| 264 | + setStatus('All posts migrated successfully!', 'success'); |
| 265 | + } |
| 266 | + } |
| 267 | + |
| 268 | + function getViewUrl(postId) { |
| 269 | + for (var i = 0; i < scannedPosts.length; i++) { |
| 270 | + if (scannedPosts[i].id === postId) return scannedPosts[i].view_url; |
| 271 | + } |
| 272 | + return '#'; |
| 273 | + } |
| 274 | + |
| 275 | + // ========================================================================= |
| 276 | + // Migrate All |
| 277 | + // ========================================================================= |
| 278 | + |
| 279 | + function doMigrateAll() { |
| 280 | + var pending = document.querySelectorAll('.cs-single-migrate-btn'); |
| 281 | + var count = pending.length; |
| 282 | + |
| 283 | + if (count === 0) { |
| 284 | + alert('No remaining posts to migrate.'); |
| 285 | + return; |
| 286 | + } |
| 287 | + |
| 288 | + if (!confirm('This will migrate ' + count + ' remaining post(s) to CloudScale Code Blocks. Continue?')) { |
| 289 | + return; |
| 290 | + } |
| 291 | + |
| 292 | + migrateAllBtn.disabled = true; |
| 293 | + migrateAllBtn.textContent = 'Migrating all...'; |
| 294 | + setStatus('Migrating all remaining posts...', ''); |
| 295 | + |
| 296 | + ajax('cs_migrate_all', {}, function (err, data) { |
| 297 | + migrateAllBtn.innerHTML = '<span class="dashicons dashicons-update"></span> Migrate All Remaining'; |
| 298 | + |
| 299 | + if (err) { |
| 300 | + migrateAllBtn.disabled = false; |
| 301 | + setStatus('Batch migration failed: ' + err, 'error'); |
| 302 | + return; |
| 303 | + } |
| 304 | + |
| 305 | + setStatus( |
| 306 | + 'Migrated ' + data.migrated_blocks + ' block(s) across ' + data.migrated_posts + ' post(s).', |
| 307 | + 'success' |
| 308 | + ); |
| 309 | + |
| 310 | + // Mark all rows as done |
| 311 | + data.details.forEach(function (detail) { |
| 312 | + var match = detail.match(/^#(\d+)/); |
| 313 | + if (match) { |
| 314 | + markRowMigrated(parseInt(match[1]), 0); |
| 315 | + } |
| 316 | + }); |
| 317 | + |
| 318 | + migrateAllBtn.disabled = true; |
| 319 | + }); |
| 320 | + } |
| 321 | + |
| 322 | + // ========================================================================= |
| 323 | + // Event Bindings |
| 324 | + // ========================================================================= |
| 325 | + |
| 326 | + scanBtn.addEventListener('click', doScan); |
| 327 | + migrateAllBtn.addEventListener('click', doMigrateAll); |
| 328 | + |
| 329 | + // Escape key closes modal |
| 330 | + document.addEventListener('keydown', function (e) { |
| 331 | + if (e.key === 'Escape' && modal.style.display !== 'none') { |
| 332 | + closeModal(); |
| 333 | + } |
| 334 | + }); |
| 335 | + |
| 336 | +})(); |
0 commit comments