adobe-to-docusign-migrator/web/static/app.js

324 lines
11 KiB
JavaScript

// Adobe Sign → DocuSign Migrator — frontend app
// Vanilla JS, no build step.
const $ = id => document.getElementById(id);
let statusTemplates = []; // [{adobe_id, name, status, docusign_id, ...}]
let dsTemplates = []; // [{id, name, lastModified}]
let authState = { adobe: false, docusign: false };
// ── Init ────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', async () => {
await refreshAuth();
await refreshTemplates();
await refreshHistory();
$('btn-migrate').addEventListener('click', onMigrate);
$('btn-refresh').addEventListener('click', async () => {
await refreshTemplates();
await refreshHistory();
});
});
// ── Auth ─────────────────────────────────────────────────────────────────────
async function refreshAuth() {
const resp = await fetch('/api/auth/status');
authState = await resp.json();
renderAuthBar();
}
function renderAuthBar() {
// Adobe: manual paste flow
const adobeEl = $('badge-adobe');
adobeEl.textContent = authState.adobe ? '✓ Adobe Sign' : 'Connect Adobe Sign';
adobeEl.className = 'auth-badge' + (authState.adobe ? ' connected' : '');
adobeEl.onclick = authState.adobe
? () => disconnectPlatform('adobe')
: () => startAdobeAuth();
// DocuSign: JWT grant from .env — no browser sign-in needed
const dsEl = $('badge-docusign');
dsEl.textContent = authState.docusign ? '✓ DocuSign' : 'Connect DocuSign';
dsEl.className = 'auth-badge' + (authState.docusign ? ' connected' : '');
dsEl.onclick = authState.docusign
? () => disconnectPlatform('docusign')
: () => connectDocusign();
}
async function disconnectPlatform(platform) {
await fetch(`/api/auth/${platform}/disconnect`);
authState[platform] = false;
renderAuthBar();
await refreshTemplates();
}
async function connectDocusign() {
const dsEl = $('badge-docusign');
dsEl.textContent = 'Connecting…';
const resp = await fetch('/api/auth/docusign/connect');
const data = await resp.json();
if (data.connected) {
authState.docusign = true;
renderAuthBar();
await refreshTemplates();
} else {
dsEl.textContent = 'Connect DocuSign';
setStatus('DocuSign error: ' + (data.error || 'unknown'));
}
}
// Adobe Sign uses the same manual-paste flow as the CLI:
// 1. Open auth URL in new tab
// 2. User authorizes → lands on failed https://localhost:8080/callback page
// 3. User copies that URL, pastes it into the dialog here
// 4. We POST it to /api/auth/adobe/exchange
async function startAdobeAuth() {
const resp = await fetch('/api/auth/adobe/url');
const { url } = await resp.json();
showAdobeDialog(url);
}
function showAdobeDialog(authUrl) {
// Remove any existing dialog
const existing = $('adobe-auth-dialog');
if (existing) existing.remove();
const dialog = document.createElement('div');
dialog.id = 'adobe-auth-dialog';
dialog.innerHTML = `
<div class="dialog-backdrop"></div>
<div class="dialog-box">
<h2>Connect Adobe Sign</h2>
<ol>
<li><a href="${escHtml(authUrl)}" target="_blank" rel="noopener" id="adobe-auth-link">Click here to authorize in Adobe Sign</a></li>
<li>After authorizing, your browser will show a page that fails to load — that's expected.</li>
<li>Copy the full URL from the address bar and paste it below.</li>
</ol>
<input type="text" id="adobe-redirect-input" placeholder="https://localhost:8080/callback?code=…" />
<div class="dialog-error" id="dialog-error"></div>
<div class="dialog-actions">
<button id="btn-submit-code">Connect</button>
<button id="btn-cancel-dialog" class="btn-secondary">Cancel</button>
</div>
</div>
`;
document.body.appendChild(dialog);
$('btn-cancel-dialog').onclick = () => dialog.remove();
$('btn-submit-code').onclick = () => submitAdobeCode(dialog);
// Also handle Enter key
$('adobe-redirect-input').addEventListener('keydown', e => {
if (e.key === 'Enter') submitAdobeCode(dialog);
});
}
async function submitAdobeCode(dialog) {
const url = $('adobe-redirect-input').value.trim();
if (!url) return;
$('btn-submit-code').disabled = true;
$('btn-submit-code').textContent = 'Connecting…';
$('dialog-error').textContent = '';
try {
const resp = await fetch('/api/auth/adobe/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ redirect_url: url }),
});
const data = await resp.json();
if (!resp.ok || data.error) {
$('dialog-error').textContent = data.error || 'Connection failed.';
$('btn-submit-code').disabled = false;
$('btn-submit-code').textContent = 'Connect';
return;
}
dialog.remove();
authState.adobe = true;
renderAuthBar();
await refreshTemplates();
} catch (e) {
$('dialog-error').textContent = 'Error: ' + e.message;
$('btn-submit-code').disabled = false;
$('btn-submit-code').textContent = 'Connect';
}
}
// ── Templates ────────────────────────────────────────────────────────────────
async function refreshTemplates() {
renderAdobeList([]);
renderDsList([]);
if (!authState.adobe || !authState.docusign) {
setStatus(authState.adobe || authState.docusign
? 'Connect both platforms to see migration status.'
: 'Connect Adobe Sign and DocuSign to get started.');
$('btn-migrate').disabled = true;
return;
}
setStatus('Loading templates…');
try {
const [statusResp, dsResp] = await Promise.all([
fetch('/api/templates/status'),
fetch('/api/templates/docusign'),
]);
statusTemplates = (await statusResp.json()).templates || [];
dsTemplates = (await dsResp.json()).templates || [];
renderAdobeList(statusTemplates);
renderDsList(dsTemplates);
setStatus(`${statusTemplates.length} Adobe template(s) loaded.`);
} catch (e) {
setStatus('Error loading templates: ' + e.message);
}
}
function renderAdobeList(items) {
const ul = $('adobe-list');
if (!items.length) {
ul.innerHTML = '<li class="empty-msg">No templates found.</li>';
return;
}
ul.innerHTML = items.map(t => `
<li class="template-item" data-id="${t.adobe_id}">
<input type="checkbox" data-id="${t.adobe_id}" />
<span class="template-name">${escHtml(t.name)}</span>
<span class="badge badge-${t.status}">${statusLabel(t.status)}</span>
<span class="template-spinner" id="spin-${t.adobe_id}"></span>
</li>
`).join('');
ul.querySelectorAll('.template-item').forEach(li => {
li.addEventListener('click', e => {
if (e.target.type === 'checkbox') return;
const cb = li.querySelector('input[type=checkbox]');
cb.checked = !cb.checked;
li.classList.toggle('selected', cb.checked);
updateMigrateButton();
});
li.querySelector('input').addEventListener('change', () => {
li.classList.toggle('selected', li.querySelector('input').checked);
updateMigrateButton();
});
});
}
function renderDsList(items) {
const ul = $('ds-list');
if (!items.length) {
ul.innerHTML = '<li class="empty-msg">No templates found.</li>';
return;
}
ul.innerHTML = items.map(t => `
<li class="template-item">
<span class="template-name">${escHtml(t.name)}</span>
<span style="font-size:11px;color:#999">${(t.lastModified || '').slice(0, 10)}</span>
</li>
`).join('');
}
function updateMigrateButton() {
const checked = document.querySelectorAll('#adobe-list input[type=checkbox]:checked');
$('btn-migrate').disabled = checked.length === 0;
}
// ── Migration ─────────────────────────────────────────────────────────────────
async function onMigrate() {
const checked = [...document.querySelectorAll('#adobe-list input[type=checkbox]:checked')];
const ids = checked.map(cb => cb.dataset.id);
if (!ids.length) return;
$('btn-migrate').disabled = true;
setStatus(`Migrating ${ids.length} template(s)…`);
ids.forEach(id => {
const spin = $('spin-' + id);
if (spin) spin.textContent = '⏳';
});
try {
const resp = await fetch('/api/migrate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ adobe_template_ids: ids }),
});
const data = await resp.json();
let successCount = 0;
(data.results || []).forEach(r => {
const spin = $('spin-' + r.adobe_template_id);
if (r.status === 'success') {
successCount++;
if (spin) spin.textContent = r.action === 'updated' ? '✏️' : '✅';
} else {
if (spin) spin.textContent = '❌';
}
});
setStatus(`Done: ${successCount}/${ids.length} succeeded.`);
await refreshTemplates();
await refreshHistory();
} catch (e) {
setStatus('Migration error: ' + e.message);
}
}
// ── History ───────────────────────────────────────────────────────────────────
async function refreshHistory() {
try {
const resp = await fetch('/api/migrate/history');
const { history } = await resp.json();
renderHistory(history || []);
} catch {
renderHistory([]);
}
}
function renderHistory(records) {
const tbody = $('history-tbody');
if (!records.length) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-msg">No migrations yet.</td></tr>';
return;
}
tbody.innerHTML = [...records].reverse().slice(0, 50).map(r => `
<tr>
<td>${(r.timestamp || '').replace('T', ' ').slice(0, 19)}</td>
<td>${escHtml(r.adobe_template_name || r.adobe_template_id || '')}</td>
<td>${escHtml(r.docusign_template_id || '—')}</td>
<td>${escHtml(r.action || '—')}</td>
<td>
<span class="badge ${r.status === 'success' ? 'badge-migrated' : 'badge-not_migrated'}">
${r.status}
</span>
</td>
</tr>
`).join('');
}
// ── Utilities ─────────────────────────────────────────────────────────────────
function setStatus(msg) { $('status-msg').textContent = msg; }
function statusLabel(s) {
return { not_migrated: 'Not Migrated', migrated: 'Migrated', needs_update: 'Needs Update' }[s] || s;
}
function escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}