// Migration workflow: options modal → progress → results view import { api } from './api.js'; import { state, setState } from './state.js'; import { escHtml, formatDateTime, downloadCsv } from './utils.js'; import { navigate } from './router.js'; import { refreshTemplates } from './templates.js'; // ── Helpers ──────────────────────────────────────────────────────────────── function getSettings() { try { return JSON.parse(localStorage.getItem('migrator_settings')) || {}; } catch { return {}; } } // ── Options modal ────────────────────────────────────────────────────────── export function showOptionsModal(ids) { if (!ids || !ids.length) return; const settings = getSettings(); const names = ids.map(id => { const t = state.templates.find(t => t.adobe_id === id); return t ? t.name : id; }); const existing = document.getElementById('migration-modal'); if (existing) existing.remove(); const wrapper = document.createElement('div'); wrapper.id = 'migration-modal'; wrapper.innerHTML = ` `; document.body.appendChild(wrapper); // Wire toggles wrapper.querySelectorAll('.toggle').forEach(btn => { if (settings.defaultOverwrite && btn.id === 'opt-overwrite') btn.classList.add('on'); btn.addEventListener('click', () => { btn.classList.toggle('on'); btn.setAttribute('aria-checked', btn.classList.contains('on')); }); }); document.getElementById('mm-close').onclick = () => wrapper.remove(); document.getElementById('mm-cancel').onclick = () => wrapper.remove(); document.getElementById('mm-run').onclick = () => _startMigration(ids, wrapper); } // ── Start migration ──────────────────────────────────────────────────────── async function _startMigration(ids, wrapper) { const dryRun = document.getElementById('opt-dry-run')?.classList.contains('on') || false; const overwrite = document.getElementById('opt-overwrite')?.classList.contains('on') || false; const includeDocs = !(document.getElementById('opt-include-docs')?.classList.contains('on') === false); const folder = document.getElementById('opt-folder')?.value.trim() || undefined; // Replace modal body with progress view const body = wrapper.querySelector('.modal-body'); const footer = wrapper.querySelector('.modal-footer'); wrapper.querySelector('.modal-title').textContent = dryRun ? 'Dry Run' : 'Migrating…'; footer.innerHTML = ''; // hide footer during migration const names = ids.map(id => { const t = state.templates.find(t => t.adobe_id === id); return { id, name: t ? t.name : id }; }); body.innerHTML = `
Starting… 0 / ${ids.length}
${names.map(n => `
${escHtml(n.name)}
`).join('')}
`; try { const jobData = await api.migrate.batch({ source_template_ids: ids, target_folder: folder, options: { dry_run: dryRun, overwrite_if_exists: overwrite, include_documents: includeDocs }, }); const jobId = jobData.job_id; await pollJob(jobId, (progress) => { const { completed, total } = progress.progress || { completed: 0, total: ids.length }; const pct = total > 0 ? Math.round(completed / total * 100) : 0; document.getElementById('prog-bar') && (document.getElementById('prog-bar').style.width = pct + '%'); document.getElementById('prog-count')&& (document.getElementById('prog-count').textContent = `${completed} / ${total}`); document.getElementById('prog-label')&& (document.getElementById('prog-label').textContent = `Migrating… ${pct}%`); // Update per-template icons as results come in (progress.results || []).forEach(r => { const row = document.getElementById(`prog-row-${r.adobe_template_id}`); if (!row) return; const statusEl = row.querySelector('.progress-template-status'); if (!statusEl) return; statusEl.className = 'progress-template-status'; if (r.status === 'success') statusEl.textContent = r.action === 'created' ? '✅' : r.action === 'dry_run' ? '🔍' : '✏️'; else if (r.status === 'skipped') statusEl.textContent = '⏭'; else if (r.status === 'blocked') statusEl.textContent = '🚫'; else statusEl.textContent = '❌'; }); }); // Migration done — show "View Results" button document.getElementById('prog-label') && (document.getElementById('prog-label').textContent = 'Done!'); footer.innerHTML = ` `; document.getElementById('mm-close-done').onclick = () => wrapper.remove(); document.getElementById('mm-view-results').onclick = () => { wrapper.remove(); navigate('#/results'); }; // Refresh template list await refreshTemplates(); state.selectedIds.clear(); } catch (err) { body.innerHTML += `
Migration failed: ${escHtml(err.message)}
`; footer.innerHTML = ``; document.getElementById('mm-err-close').onclick = () => wrapper.remove(); } } // ── Poll batch job ───────────────────────────────────────────────────────── export async function pollJob(jobId, onProgress) { const POLL_MS = 2000; const MAX_WAIT = 300000; // 5 minutes const started = Date.now(); return new Promise((resolve, reject) => { const tick = async () => { try { const data = await api.migrate.batchStatus(jobId); if (onProgress) onProgress(data); if (data.status === 'done' || data.status === 'complete') { setState('lastMigrationResults', data); resolve(data); } else if (data.status === 'failed') { reject(new Error('Migration job failed')); } else if (Date.now() - started > MAX_WAIT) { reject(new Error('Migration timed out')); } else { setTimeout(tick, POLL_MS); } } catch (e) { reject(e); } }; tick(); }); } // ── Results view ─────────────────────────────────────────────────────────── export function renderResults() { const outlet = document.getElementById('router-outlet'); const results = state.lastMigrationResults; if (!results) { outlet.innerHTML = `
📊
No migration results yet
Run a migration from the Templates view to see results here.
`; return; } const templateResults = results.results || []; const summary = { created: templateResults.filter(r => r.action === 'created').length, updated: templateResults.filter(r => r.action === 'updated').length, skipped: templateResults.filter(r => r.status === 'skipped').length, blocked: templateResults.filter(r => r.status === 'blocked').length, errors: templateResults.filter(r => r.status === 'error').length, dry_run: templateResults.filter(r => r.status === 'dry_run').length, }; const migratedIds = templateResults .filter(r => r.status === 'success') .map(r => r.adobe_template_id); outlet.innerHTML = `
Created
${summary.created}
Updated
${summary.updated}
Skipped
${summary.skipped}
Blocked
${summary.blocked}
Errors
${summary.errors}
${summary.dry_run ? `
Dry Run
${summary.dry_run}
` : ''}
Per-Template Results
${templateResults.map(r => _resultRow(r)).join('')}
← Back to Templates
`; // Expand/collapse result rows document.querySelectorAll('.result-header').forEach(hdr => { hdr.addEventListener('click', () => { hdr.parentElement.classList.toggle('open'); }); }); // Export CSV document.getElementById('btn-export-results')?.addEventListener('click', () => { downloadCsv('migration-results.csv', templateResults.map(r => ({ name: r.adobe_template_name || r.adobe_template_id, adobe_id: r.adobe_template_id, docusign_id: r.docusign_template_id || '', status: r.status, action: r.action || '', warnings: (r.warnings || []).join('; '), }))); }); // Verify button document.getElementById('btn-verify-results')?.addEventListener('click', () => { import('./verification.js').then(m => { setState('verifyIds', migratedIds); navigate('#/verify'); }); }); } function _resultRow(r) { const icon = r.status === 'success' ? (r.action === 'created' ? '✅' : r.action === 'dry_run' ? '🔍' : '✏️') : (r.status === 'skipped' ? '⏭' : r.status === 'blocked' ? '🚫' : '❌'); const statusBadge = r.status === 'success' ? `${r.action || 'success'}` : `${r.status}`; const warnings = r.warnings || []; return `
${icon} ${escHtml(r.adobe_template_name || r.adobe_template_id)} ${statusBadge} ${r.docusign_template_id ? `DS: ${escHtml(r.docusign_template_id.slice(0,8))}…` : ''} ${warnings.length ? `⚠ ${warnings.length} warning${warnings.length > 1 ? 's' : ''}` : ''}
${warnings.length || r.error ? `
${warnings.map(w => `
${escHtml(w)}
`).join('')} ${r.error ? `
${escHtml(r.error)}
` : ''}
` : ''}
`; }