// Migration workflow: options modal → progress → results view import { api } from './api.js'; import { state, setState } from './state.js'; import { escHtml, formatDateTime, downloadCsv, renderFieldIssues, bindFieldIssueToggles } from './utils.js'; import { navigate } from './router.js'; import { refreshTemplates } from './templates.js'; // ── Helpers ──────────────────────────────────────────────────────────────── const _RESULTS_STORAGE_KEY = 'migrator_last_batch_results'; function getSettings() { try { return JSON.parse(localStorage.getItem('migrator_settings')) || {}; } catch { return {}; } } function persistLastResults(results) { try { sessionStorage.setItem(_RESULTS_STORAGE_KEY, JSON.stringify(results)); } catch { // Best-effort only. } } function loadPersistedResults() { try { const raw = sessionStorage.getItem(_RESULTS_STORAGE_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } function buildResultsFromHistory(history) { if (!history || !history.length) return null; const sorted = [...history].sort((a, b) => String(b.timestamp || '').localeCompare(String(a.timestamp || ''))); const newestTimestamp = sorted[0]?.timestamp; const recentBatch = sorted.filter(item => item.timestamp === newestTimestamp); if (!recentBatch.length) return null; return { status: 'completed', completed_at: newestTimestamp, results: recentBatch, summary: { total: recentBatch.length, success: recentBatch.filter(r => r.status === 'success').length, failed: recentBatch.filter(r => r.status === 'failed' || r.status === 'blocked').length, skipped: recentBatch.filter(r => r.status === 'skipped').length, dry_run: recentBatch.filter(r => r.status === 'dry_run').length, }, recovered_from_history: true, }; } // ── 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 = '❌'; if (r.status === 'blocked' || r.status === 'error' || r.status === 'failed') { const msg = r.error || (r.blockers||[])[0] || 'Migration failed'; let hint = row.querySelector('.progress-template-error'); if (!hint) { hint = document.createElement('div'); hint.className = 'progress-template-error'; row.appendChild(hint); } hint.textContent = msg; } }); }); // Migration done — show "View Results" button const allResults = (jobData.results || []); const failCount = allResults.filter(r => r.status === 'blocked' || r.status === 'error' || r.status === 'failed').length; document.getElementById('prog-label') && (document.getElementById('prog-label').textContent = 'Done!'); if (failCount > 0) { const hint = document.createElement('div'); hint.style.cssText = 'font-size:12px;color:var(--text-muted);margin-top:10px;text-align:center'; hint.textContent = `${failCount} template${failCount > 1 ? 's' : ''} had issues — select View Results for details.`; body.appendChild(hint); } 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' || data.status === 'completed') { setState('lastMigrationResults', data); persistLastResults(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 async function renderResults() { const outlet = document.getElementById('router-outlet'); let results = state.lastMigrationResults || loadPersistedResults(); if (!results) { try { const data = await api.migrate.history(); results = buildResultsFromHistory(data.history || []); } catch { results = null; } } if (results) { setState('lastMigrationResults', results); persistLastResults(results); } if (!results) { outlet.innerHTML = `
📊
No migration results yet
Run a migration from the Templates view to see results here. If templates already exist in DocuSign, use History & Audit to review older runs.
`; 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 = ` ${results.recovered_from_history ? `
ℹ️ These results were recovered from recent migration history after the page state was reset.
` : ''}
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'); }); }); bindFieldIssueToggles(); // 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 issues = r.field_issues || []; const warnings = r.warnings || []; const hasDetail = warnings.length || r.error || issues.length; const icon = r.status === 'success' ? (issues.length ? '⚠️' : r.action === 'created' ? '✅' : r.action === 'dry_run' ? '🔍' : '✏️') : (r.status === 'skipped' ? '⏭' : r.status === 'blocked' ? '🚫' : '❌'); const statusBadge = r.status === 'success' ? `${r.action || 'success'}${issues.length ? 'partial' : ''}` : `${r.status}`; 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))}…` : ''} ${issues.length ? `⚠ ${issues.length} field issue${issues.length > 1 ? 's' : ''}` : ''} ${warnings.length ? `${warnings.length} warning${warnings.length > 1 ? 's' : ''}` : ''}
${hasDetail ? `
${warnings.map(w => `
${escHtml(w)}
`).join('')} ${r.error ? `
${escHtml(r.error)}
` : ''} ${renderFieldIssues(issues)}
` : ''}
`; }