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

155 lines
4.9 KiB
JavaScript

// Shared utility functions
export function escHtml(str) {
return String(str ?? '').replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
})[c]);
}
export function formatDate(iso) {
if (!iso) return '—';
try {
return new Date(iso).toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric'
});
} catch { return iso.slice(0, 10); }
}
export function formatDateTime(iso) {
if (!iso) return '—';
try {
return new Date(iso).toLocaleString('en-US', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
} catch { return iso.slice(0, 19).replace('T', ' '); }
}
export function formatRelative(iso) {
if (!iso) return '—';
const diff = Date.now() - new Date(iso).getTime();
const m = Math.floor(diff / 60000);
if (m < 1) return 'just now';
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h ago`;
const d = Math.floor(h / 24);
if (d < 7) return `${d}d ago`;
return formatDate(iso);
}
export function debounce(fn, ms = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
export function uuid() {
return crypto.randomUUID
? crypto.randomUUID()
: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
// Truncate a string to maxLen chars, appending ellipsis if needed
export function truncate(str, maxLen = 40) {
if (!str) return '';
return str.length > maxLen ? str.slice(0, maxLen - 1) + '…' : str;
}
// First letter of a name for avatar initials
export function initials(name) {
if (!name) return '?';
const parts = name.trim().split(/\s+/);
return parts.length >= 2
? (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
: name.slice(0, 2).toUpperCase();
}
// Download a string as a file
export function downloadText(filename, content, type = 'text/plain') {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename;
document.body.appendChild(a); a.click();
document.body.removeChild(a); URL.revokeObjectURL(url);
}
// Convert array of objects to CSV and download
export function downloadCsv(filename, rows) {
if (!rows.length) return;
const headers = Object.keys(rows[0]);
const csv = [
headers.join(','),
...rows.map(r => headers.map(h => JSON.stringify(r[h] ?? '')).join(','))
].join('\n');
downloadText(filename, csv, 'text/csv');
}
// Shorten a SHA-256 hash for display
export function shortHash(hash, len = 8) {
return hash ? hash.slice(0, len) : '—';
}
// Human-readable labels for field issue codes (mirrors src/models/field_issue.py)
export const FIELD_ISSUE_LABELS = {
CROSS_RECIPIENT_CONDITIONAL: 'Cross-recipient conditional dropped',
UNSUPPORTED_OPERATOR: 'Unsupported condition operator dropped',
HIDE_ACTION: 'Hide condition dropped (no DocuSign equivalent)',
MULTI_PREDICATE: 'Multi-condition logic simplified to first match',
INVALID_PARENT_TAB: 'Conditional parent tab invalid or missing',
FIELD_TYPE_SKIPPED: 'Field type skipped (no DocuSign equivalent)',
PARTIAL_FIELD_TYPE: 'Field type approximated',
};
/**
* Wire click-to-expand on all .field-issue-group elements within root.
* Call this after injecting renderFieldIssues() HTML into the DOM.
*/
export function bindFieldIssueToggles(root = document) {
root.querySelectorAll('.field-issue-group-header').forEach(hdr => {
hdr.addEventListener('click', () => hdr.parentElement.classList.toggle('open'));
});
}
/**
* Render a grouped field-issues section as an HTML string.
* Groups issues by code, shows count + label, expands to field names + messages.
* Returns '' if no issues.
*/
export function renderFieldIssues(issues) {
if (!issues || !issues.length) return '';
// Group by code
const groups = {};
issues.forEach(i => {
if (!groups[i.code]) groups[i.code] = [];
groups[i.code].push(i);
});
const groupHtml = Object.entries(groups).map(([code, items]) => {
const label = FIELD_ISSUE_LABELS[code] || code;
const rows = items.map(i =>
`<div class="field-issue-row">
<span class="field-issue-field">${escHtml(i.field_name)}</span>
<span class="field-issue-msg">${escHtml(i.message)}</span>
</div>`
).join('');
return `
<div class="field-issue-group">
<div class="field-issue-group-header">
<span class="badge badge-amber" style="font-size:10px">${items.length}</span>
${escHtml(label)}
</div>
<div class="field-issue-group-body">${rows}</div>
</div>`;
}).join('');
return `<div class="field-issues-block">${groupHtml}</div>`;
}