import { translate } from '../utils/i18nHelpers.js'; /** * Escape HTML entities in a string to prevent injection when interpolating into innerHTML. * @param {string} str - The string to escape * @returns {string} - The escaped string */ function _escapeHtml(str) { if (str === null || str === undefined) return ''; const div = document.createElement('div'); div.textContent = String(str); return div.innerHTML.replace(/"/g, '"').replace(/'/g, '''); } /** * Show the sidecar migration summary modal after a migration completes. * Mirrors the Metadata Fetch Summary / Batch Download Summary lifecycle: the * modal element is appended directly to document.body and removed on close; * it is not registered with ModalManager. * @param {Object} options - Summary options * @param {Object} options.result - Migration result payload from /api/lm/sidecars/migrate * @param {Function} options.onClose - Callback invoked after the modal is dismissed * @param {Function} [options.onOpenLocation] - Callback for the "Open Folder" action; the modal stays open */ export function showSidecarMigrationSummary({ result, onClose, onOpenLocation }) { const errorCount = result.error_count || 0; const errors = Array.isArray(result.errors) ? result.errors : []; const moved = result.moved || 0; const modelsMoved = result.models_moved || 0; const modelsTotal = result.models_total || 0; const skipped = result.skipped || 0; const conflicts = result.conflicts || 0; const showLocation = result.direction !== 'to_alongside' && !!result.sidecar_root; // 3-state summary header semantics (mirrors DownloadBatchSummaryModal) let headerState; let headerIcon; let headerText; if (errorCount > 0) { headerState = 'warning'; headerIcon = 'fa-exclamation-circle'; headerText = translate('modals.sidecarMigrationResult.completedWithErrors', { count: errorCount }, 'Completed with ' + errorCount + ' error(s)'); } else { headerState = 'success'; headerIcon = 'fa-check-circle'; headerText = translate('modals.sidecarMigrationResult.completedSuccessfully', {}, 'Migration completed successfully'); } const failureRows = errors.map((entry, i) => { const name = entry?.model || 'Unknown'; const error = entry?.error ? String(entry.error) : 'Unknown error'; return ` ${i + 1} ${_escapeHtml(name)} ${_escapeHtml(error)} `; }).join(''); const modalHtml = ` `; const existing = document.getElementById('sidecarMigrationSummaryModal'); if (existing) existing.remove(); const container = document.createElement('div'); container.innerHTML = modalHtml; const modal = container.firstElementChild; document.body.appendChild(modal); const close = () => { document.removeEventListener('keydown', handleEscape, true); modal.remove(); if (typeof onClose === 'function') { onClose(); } }; // Capture phase + stopPropagation so ESC never reaches the settings // modal's own ESC handler underneath. const handleEscape = (event) => { if (event.key === 'Escape') { event.stopPropagation(); event.preventDefault(); close(); } }; document.addEventListener('keydown', handleEscape, true); modal.addEventListener('click', (e) => { const actionEl = e.target.closest('[data-action]'); const action = actionEl?.dataset.action; if (!action) return; e.preventDefault(); switch (action) { case 'close-modal': close(); break; case 'open-sidecar-location': // Keep the modal open; only trigger the file-manager action. if (typeof onOpenLocation === 'function') { onOpenLocation(); } break; } }); return modal; }