// Templates view — filterable table with readiness badges + template detail
import { api } from './api.js';
import { state, setState, updateDerivedState } from './state.js';
import { escHtml, formatDate, formatRelative, debounce, renderFieldIssues, bindFieldIssueToggles } from './utils.js';
import { navigate } from './router.js';
import { bindQuickStartCard, quickStartCardMarkup, shouldShowQuickStart } from './help.js';
// ── Readiness badge ────────────────────────────────────────────────────────
function readiness(t) {
if (t.blockers && t.blockers.length > 0) {
return { key: 'blocked', label: 'Blocked', cls: 'badge-blocked' };
}
if (hasFieldIssues(t)) {
return { key: 'field-caveats', label: 'Caveats', cls: 'badge-caveats' };
}
if (t.status === 'migrated') {
return hasWarnings(t)
? { key: 'migrated-warn', label: 'Migrated', cls: 'badge-migrated' }
: { key: 'migrated', label: 'Migrated', cls: 'badge-migrated' };
}
if (t.status === 'needs_update') {
return { key: 'needs-update', label: 'Needs Update', cls: 'badge-needs-update' };
}
if (hasWarnings(t)) {
return { key: 'caveats', label: 'Caveats', cls: 'badge-caveats' };
}
return { key: 'ready', label: 'Ready', cls: 'badge-ready' };
}
// ── Refresh templates from API ─────────────────────────────────────────────
export async function refreshTemplates() {
if (!state.auth.adobe || !state.auth.docusign) {
setState('templates', []);
setState('templatesError', null);
updateDerivedState();
return;
}
try {
const data = await api.templates.status();
setState('templates', data.templates || []);
setState('templatesError', null);
updateDerivedState();
} catch (e) {
console.warn('refreshTemplates failed:', e.message);
setState('templates', []);
setState('templatesError', e.data?.error || e.message || 'Failed to load templates.');
updateDerivedState();
}
}
// ── Templates list view ────────────────────────────────────────────────────
let _filter = { search: '', status: 'all' };
let _sort = { col: 'name', dir: 'asc' };
export async function renderTemplates() {
const outlet = document.getElementById('router-outlet');
// Fetch if not loaded
if (!state.templates.length && state.auth.adobe && state.auth.docusign) {
outlet.innerHTML = `
`;
await refreshTemplates();
}
_render();
}
function _render() {
const outlet = document.getElementById('router-outlet');
const templates = _applyFilter(state.templates);
const counts = _statusCounts(state.templates);
const anySelected = state.selectedIds.size > 0;
outlet.innerHTML = `
${!state.auth.adobe || !state.auth.docusign ? `
ℹ️
Connect both Adobe Sign and Docusign in the top bar to load templates.
` : ''}
${state.templatesError ? `
❌
Template loading failed: ${escHtml(state.templatesError)}
` : ''}
${shouldShowQuickStart() ? quickStartCardMarkup() : ''}
${state.selectedIds.size} template(s) selected
|
|
${_th('name', 'Template Name')}
${_th('readiness', 'Readiness')}
${_th('warnings', 'Issues')}
${_th('adobe_modified','Last Modified')}
${_th('status', 'DS Status')}
Actions |
${templates.length
? templates.map(t => _templateRow(t)).join('')
: `
📄
${state.templates.length ? 'No templates match your filter' : 'No templates found'}
${state.templates.length ? 'Try clearing the search or filter.' : (state.templatesError ? 'The template load failed. Check the error message above.' : 'Connect Adobe Sign to load templates.')}
|
`
}
`;
_bindEvents();
}
function _filterTab(key, label) {
return ``;
}
function _filterTab2(key, label) {
// readiness-based filters
return ``;
}
function _th(col, label) {
const dir = _sort.col === col ? (_sort.dir === 'asc' ? 'sort-asc' : 'sort-desc') : '';
return `${label} | `;
}
function _templateRow(t) {
const r = readiness(t);
const selected = state.selectedIds.has(t.adobe_id);
const warnCount = (t.warnings || []).length;
const blockCount = (t.blockers || []).length;
const fieldIssueCount = (t.field_issues || []).length;
const issueClass = blockCount > 0 ? 'blocked' : (warnCount > 0 || fieldIssueCount > 0 ? 'has-issues' : 'no-issues');
const issueLabel = blockCount > 0
? `🚫 ${blockCount} blocker${blockCount > 1 ? 's' : ''}`
: (warnCount > 0 || fieldIssueCount > 0
? `⚠ ${warnCount + fieldIssueCount} caveat${warnCount + fieldIssueCount > 1 ? 's' : ''}`
: '✓ Clean');
return `
|
${escHtml(t.name)}
${escHtml(t.adobe_id)}
|
${r.label} |
${issueLabel} |
${formatRelative(t.adobe_modified)} |
${t.docusign_id
? `In Docusign`
: `Not Migrated`}
|
|
`;
}
function _statusCounts(templates) {
return {
total: templates.length,
not_migrated: templates.filter(t => t.status === 'not_migrated').length,
migrated: templates.filter(t => t.status === 'migrated').length,
needs_update: templates.filter(t => t.status === 'needs_update').length,
blocked: templates.filter(t => t.blockers && t.blockers.length > 0).length,
caveats: templates.filter(t => !hasBlockers(t) && (hasWarnings(t) || hasFieldIssues(t))).length,
};
}
function _applyFilter(templates) {
let list = [...templates];
// Text search
if (_filter.search) {
const q = _filter.search.toLowerCase();
list = list.filter(t => t.name.toLowerCase().includes(q));
}
// Status / readiness filter
if (_filter.status !== 'all') {
if (_filter.status === 'blocked') {
list = list.filter(t => hasBlockers(t));
} else if (_filter.status === 'caveats') {
list = list.filter(t => !hasBlockers(t) && (hasWarnings(t) || hasFieldIssues(t)));
} else {
list = list.filter(t => t.status === _filter.status);
}
}
// Sorting
list.sort((a, b) => {
let va = a[_sort.col] || '';
let vb = b[_sort.col] || '';
if (_sort.col === 'readiness') { va = readiness(a).key; vb = readiness(b).key; }
if (_sort.col === 'warnings') { va = totalIssueCount(a); vb = totalIssueCount(b); }
if (typeof va === 'number') return _sort.dir === 'asc' ? va - vb : vb - va;
return _sort.dir === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va));
});
return list;
}
// ── Event wiring ───────────────────────────────────────────────────────────
function _bindEvents() {
bindQuickStartCard(document);
// Search
const searchEl = document.getElementById('template-search');
if (searchEl) {
searchEl.addEventListener('input', debounce(e => {
_filter.search = e.target.value;
_render();
}, 250));
}
// Filter tabs
document.querySelectorAll('.filter-tab').forEach(btn => {
btn.addEventListener('click', () => {
_filter.status = btn.dataset.filter;
_render();
});
});
// Sort headers
document.querySelectorAll('th.sortable').forEach(th => {
th.addEventListener('click', () => {
const col = th.dataset.col;
if (_sort.col === col) {
_sort.dir = _sort.dir === 'asc' ? 'desc' : 'asc';
} else {
_sort.col = col; _sort.dir = 'asc';
}
_render();
});
});
// Checkboxes
document.getElementById('select-all')?.addEventListener('change', e => {
const ids = _applyFilter(state.templates).map(t => t.adobe_id);
if (e.target.checked) { ids.forEach(id => state.selectedIds.add(id)); }
else { ids.forEach(id => state.selectedIds.delete(id)); }
_render();
});
document.querySelectorAll('.row-cb').forEach(cb => {
cb.addEventListener('change', e => {
const id = cb.dataset.id;
if (e.target.checked) state.selectedIds.add(id);
else state.selectedIds.delete(id);
_render();
});
});
// Migrate selected
document.getElementById('btn-migrate-selected')?.addEventListener('click', () => {
_launchMigration([...state.selectedIds]);
});
document.getElementById('btn-clear-selection')?.addEventListener('click', () => {
state.selectedIds.clear();
_render();
});
// Migrate individual
document.querySelectorAll('.btn-migrate-one').forEach(btn => {
btn.addEventListener('click', () => _launchMigration([btn.dataset.id]));
});
// View detail
document.querySelectorAll('.btn-view-detail, .tpl-name-link').forEach(el => {
el.addEventListener('click', () => navigate(`#/templates/${el.dataset.id}`));
});
// Refresh
document.getElementById('btn-refresh-templates')?.addEventListener('click', async () => {
await refreshTemplates();
_render();
});
}
async function _launchMigration(ids) {
if (!ids.length) return;
const { showOptionsModal } = await import('./migration.js');
showOptionsModal(ids);
}
// ── Template detail view ───────────────────────────────────────────────────
export async function renderTemplateDetail(adobeId) {
const outlet = document.getElementById('router-outlet');
const t = state.templates.find(t => t.adobe_id === adobeId);
if (!t) {
outlet.innerHTML = `
`;
return;
}
const r = readiness(t);
const issueCount = totalIssueCount(t);
outlet.innerHTML = `
Overview
Issues ${issueCount > 0
? `${issueCount}` : ''}
Migration History
`;
document.getElementById('detail-migrate-btn')?.addEventListener('click', () => {
import('./migration.js').then(m => m.showOptionsModal([adobeId]));
});
_bindDetailTabs(t);
_renderDetailTab(t, 'overview');
}
function _bindDetailTabs(t) {
document.querySelectorAll('#detail-tabs .tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('#detail-tabs .tab').forEach(x => x.classList.remove('active'));
tab.classList.add('active');
_renderDetailTab(t, tab.dataset.tab);
});
});
}
function _renderDetailTab(t, tabKey) {
const content = document.getElementById('detail-tab-content');
if (tabKey === 'overview') {
content.innerHTML = `
Adobe Sign ID
${escHtml(t.adobe_id)}
Last Modified
${formatDate(t.adobe_modified)}
Migration Status
${t.status.replace('_', ' ')}
${t.docusign_id ? `
Docusign Template ID
${escHtml(t.docusign_id)}
` : ''}
`;
} else if (tabKey === 'issues') {
const blockers = t.blockers || [];
const warnings = t.warnings || [];
const fieldIssues = t.field_issues || [];
if (!blockers.length && !warnings.length && !fieldIssues.length) {
content.innerHTML = `✓No issues found. This template is ready to migrate.
`;
} else {
content.innerHTML = `
ℹ️
This view combines pre-migration validation with field mapping caveats. Field caveats are the same kinds of issues shown after migration.
${blockers.length ? `
${blockers.map(b => `
`).join('')}
` : ''}
${fieldIssues.length ? `
${renderFieldIssues(fieldIssues)}
` : ''}
${warnings.length ? `
${warnings.map(w => `
`).join('')}
` : ''}`;
bindFieldIssueToggles(content);
}
} else if (tabKey === 'history') {
api.migrate.history().then(data => {
const records = (data.history || []).filter(r =>
r.adobe_template_id === t.adobe_id || r.adobe_template_name === t.name
);
if (!records.length) {
content.innerHTML = `ℹ️No migration history for this template yet.
`;
} else {
const rows = [...records].reverse().map(r => {
const fieldIssues = r.field_issues || [];
const hasIssues = fieldIssues.length > 0;
const hasDetail = r.error || (r.blockers||[]).length || (r.warnings||[]).length || hasIssues;
const detailHtml = hasDetail ? `
${(r.blockers||[]).map(b => ` 🚫 ${escHtml(b)} `).join('')}
${(r.warnings||[]).map(w => ` ⚠ ${escHtml(w)} `).join('')}
${r.error ? ` ❌ ${escHtml(r.error)} ` : ''}
${renderFieldIssues(fieldIssues)}
|
` : '';
return `
| ${(r.timestamp||'').slice(0,19).replace('T',' ')} |
${escHtml(r.action||'—')} |
${r.status}
${hasIssues ? 'partial' : ''}
${hasDetail ? '▶ click for details' : ''}
|
${escHtml(r.docusign_template_id||'—')} |
${detailHtml}`;
}).join('');
content.innerHTML = `
| Time | Action | Status | Docusign ID |
${rows}
`;
content.querySelectorAll('.row-expandable').forEach(row => {
row.addEventListener('click', () => {
const next = row.nextElementSibling;
if (next?.classList.contains('row-expanded-content')) {
const open = next.style.display !== 'none';
next.style.display = open ? 'none' : 'table-row';
const hint = row.querySelector('span[style*="text-muted"]');
if (hint) hint.textContent = open ? '▶ click for details' : '▼ hide details';
}
});
});
bindFieldIssueToggles(content);
}
}).catch(() => {
content.innerHTML = `❌Failed to load history.
`;
});
}
}
function hasBlockers(t) {
return (t.blockers || []).length > 0;
}
function hasWarnings(t) {
return (t.warnings || []).length > 0;
}
function hasFieldIssues(t) {
return (t.field_issues || []).length > 0;
}
function totalIssueCount(t) {
return (t.blockers || []).length + (t.warnings || []).length + (t.field_issues || []).length;
}