(function() { const apiBase = '/api/flexible'; let state = { fileType: '', targetTable: '', q: '', skip: 0, limit: 50, total: 0, hasKeys: [], }; function q(id) { return document.getElementById(id); } function formatPreviewHtml(obj, term) { // Returns sanitized HTML with clickable keys try { const payload = obj && obj.unmapped && typeof obj.unmapped === 'object' ? obj.unmapped : obj; const keys = Object.keys(payload || {}).slice(0, 5); const segments = keys.map((k) => { const safeKey = window.htmlSanitizer.escape(String(k)); const valueStr = String(payload[k]).slice(0, 60); const valueHtml = term && term.trim().length > 0 ? highlight(valueStr, term) : window.htmlSanitizer.escape(valueStr); return `: ${valueHtml}`; }); return segments.join(', '); } catch (_) { return ''; } } function escapeRegExp(str) { return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function highlight(text, term) { if (!term) return window.htmlSanitizer.escape(text); const pattern = new RegExp(escapeRegExp(term), 'ig'); const escaped = window.htmlSanitizer.escape(text); // Replace on the escaped string to avoid breaking HTML return escaped.replace(pattern, (m) => `${window.htmlSanitizer.escape(m)}`); } async function loadOptions() { try { const res = await window.http.wrappedFetch(`${apiBase}/options`); if (!res.ok) throw await window.http.toError(res, 'Failed to load options'); const data = await res.json(); const fileSel = q('filterFileType'); const tableSel = q('filterTargetTable'); // Clear existing except first fileSel.length = 1; tableSel.length = 1; (data.file_types || []).forEach(v => { const opt = document.createElement('option'); opt.value = v; opt.textContent = v; fileSel.appendChild(opt); }); (data.target_tables || []).forEach(v => { const opt = document.createElement('option'); opt.value = v; opt.textContent = v; tableSel.appendChild(opt); }); } catch (e) { alert(window.http.formatAlert(e, 'Error loading options')); } } async function loadRows() { try { const params = new URLSearchParams(); if (state.fileType) params.set('file_type', state.fileType); if (state.targetTable) params.set('target_table', state.targetTable); if (state.q) params.set('q', state.q); if (Array.isArray(state.hasKeys)) { state.hasKeys.forEach((k) => { if (k && String(k).trim().length > 0) params.append('has_keys', String(k).trim()); }); } params.set('skip', String(state.skip)); params.set('limit', String(state.limit)); const res = await window.http.wrappedFetch(`${apiBase}/imports?${params.toString()}`); if (!res.ok) throw await window.http.toError(res, 'Failed to load flexible imports'); const data = await res.json(); state.total = data.total || 0; renderRows(data.items || []); renderMeta(); renderKeyChips(); } catch (e) { alert(window.http.formatAlert(e, 'Error loading flexible imports')); } } function renderRows(items) { const tbody = q('flexibleRows'); tbody.innerHTML = ''; items.forEach(item => { const tr = document.createElement('tr'); tr.className = 'hover:bg-neutral-50 dark:hover:bg-neutral-700/40 cursor-pointer'; tr.innerHTML = ` ${item.id} ${window.htmlSanitizer.escape(item.file_type || '')} ${window.htmlSanitizer.escape(item.target_table || '')} ${window.htmlSanitizer.escape((item.primary_key_field || '') + (item.primary_key_value ? '=' + item.primary_key_value : ''))} `; // Set sanitized highlighted preview const previewCell = tr.querySelector('.previewCell'); const previewHtml = formatPreviewHtml(item.extra_data || {}, state.q); window.setSafeHTML(previewCell, previewHtml); // Bind click on keys to add filters previewCell.querySelectorAll('.key-link').forEach((btn) => { btn.addEventListener('click', (ev) => { ev.stopPropagation(); const key = btn.getAttribute('data-key') || ''; addKeyFilter(key); }); }); // Row click opens modal tr.addEventListener('click', (ev) => { // Ignore clicks on the export button inside the row const target = ev.target.closest('button[data-action="export"]'); if (target) return; openDetailModal(item); }); // Export button handler tr.querySelector('button[data-action="export"]').addEventListener('click', (ev) => { ev.stopPropagation(); exportSingleRow(item.id); }); tbody.appendChild(tr); }); } function renderMeta() { const start = state.total === 0 ? 0 : state.skip + 1; const end = Math.min(state.skip + state.limit, state.total); q('rowsMeta').textContent = `Showing ${start}-${end} of ${state.total}`; q('prevPageBtn').disabled = state.skip === 0; q('nextPageBtn').disabled = state.skip + state.limit >= state.total; } function applyFilters() { state.fileType = q('filterFileType').value || ''; state.targetTable = q('filterTargetTable').value || ''; state.q = (q('quickSearch').value || '').trim(); state.skip = 0; loadRows(); } function addKeyFilter(key) { const k = String(key || '').trim(); if (!k) return; if (!Array.isArray(state.hasKeys)) state.hasKeys = []; if (!state.hasKeys.includes(k)) { state.hasKeys.push(k); state.skip = 0; loadRows(); } } function removeKeyFilter(key) { const k = String(key || '').trim(); if (!k) return; state.hasKeys = (state.hasKeys || []).filter((x) => x !== k); state.skip = 0; loadRows(); } function clearKeyFilters() { if ((state.hasKeys || []).length === 0) return; state.hasKeys = []; state.skip = 0; loadRows(); } function renderKeyChips() { const container = q('keyChipsContainer'); const chipsWrap = q('keyChips'); const clearBtn = q('clearKeyChips'); if (!container || !chipsWrap) return; chipsWrap.innerHTML = ''; const keys = state.hasKeys || []; if (keys.length === 0) { container.classList.add('hidden'); } else { container.classList.remove('hidden'); keys.forEach((k) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-primary-50 text-primary-700 border border-primary-200 hover:bg-primary-100 dark:bg-primary-900/30 dark:text-primary-200 dark:border-primary-800'; btn.setAttribute('data-chip-key', k); btn.innerHTML = `${window.htmlSanitizer.escape(k)} `; btn.addEventListener('click', (ev) => { ev.stopPropagation(); removeKeyFilter(k); }); chipsWrap.appendChild(btn); }); } if (clearBtn) { clearBtn.onclick = (ev) => { ev.preventDefault(); clearKeyFilters(); }; } } async function exportCsv() { try { const params = new URLSearchParams(); if (state.fileType) params.set('file_type', state.fileType); if (state.targetTable) params.set('target_table', state.targetTable); if (Array.isArray(state.hasKeys)) { state.hasKeys.forEach((k) => { if (k && String(k).trim().length > 0) params.append('has_keys', String(k).trim()); }); } const url = `${apiBase}/export?${params.toString()}`; const res = await window.http.wrappedFetch(url); if (!res.ok) throw await window.http.toError(res, 'Export failed'); const blob = await res.blob(); const a = document.createElement('a'); const objectUrl = URL.createObjectURL(blob); a.href = objectUrl; a.download = 'flexible_unmapped.csv'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(objectUrl), 1000); } catch (e) { alert(window.http.formatAlert(e, 'Error exporting CSV')); } } async function exportSingleRow(rowId) { try { const res = await window.http.wrappedFetch(`${apiBase}/export/${rowId}`); if (!res.ok) throw await window.http.toError(res, 'Export failed'); const blob = await res.blob(); const a = document.createElement('a'); const objectUrl = URL.createObjectURL(blob); a.href = objectUrl; a.download = `flexible_row_${rowId}.csv`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(objectUrl), 1000); } catch (e) { alert(window.http.formatAlert(e, 'Error exporting row CSV')); } } function openDetailModal(item) { // Populate fields q('detailRowId').textContent = `#${item.id}`; q('detailFileType').textContent = item.file_type || ''; q('detailTargetTable').textContent = item.target_table || ''; q('detailPkField').textContent = item.primary_key_field || ''; q('detailPkValue').textContent = item.primary_key_value || ''; try { const pretty = JSON.stringify(item.extra_data || {}, null, 2); q('detailJson').textContent = pretty; } catch (_) { q('detailJson').textContent = ''; } const exportBtn = q('detailExportBtn'); exportBtn.onclick = () => exportSingleRow(item.id); openModal('flexibleDetailModal'); } function bindEvents() { q('applyFiltersBtn').addEventListener('click', applyFilters); q('exportCsvBtn').addEventListener('click', exportCsv); const clearBtn = q('clearKeyChips'); if (clearBtn) clearBtn.addEventListener('click', (ev) => { ev.preventDefault(); clearKeyFilters(); }); // Quick search with debounce const searchInput = q('quickSearch'); let searchTimer = null; searchInput.addEventListener('input', () => { const value = searchInput.value || ''; clearTimeout(searchTimer); searchTimer = setTimeout(() => { state.q = value.trim(); state.skip = 0; loadRows(); }, 300); }); searchInput.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); clearTimeout(searchTimer); state.q = (searchInput.value || '').trim(); state.skip = 0; loadRows(); } }); q('prevPageBtn').addEventListener('click', () => { state.skip = Math.max(0, state.skip - state.limit); loadRows(); }); q('nextPageBtn').addEventListener('click', () => { if (state.skip + state.limit < state.total) { state.skip += state.limit; loadRows(); } }); } document.addEventListener('DOMContentLoaded', () => { bindEvents(); loadOptions().then(loadRows); }); })();