324 lines
11 KiB
JavaScript
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|