work on import
This commit is contained in:
@@ -9,14 +9,15 @@ class ImportManager {
|
||||
this.batchFileCount = 0;
|
||||
this.currentImportId = null;
|
||||
this.pollInterval = null;
|
||||
this.bulkFiles = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadSupportedTables();
|
||||
await this.loadImportStatus();
|
||||
this.setupEventListeners();
|
||||
this.addInitialBatchFile();
|
||||
}
|
||||
|
||||
async loadSupportedTables() {
|
||||
@@ -53,13 +54,26 @@ class ImportManager {
|
||||
this.onTableChange(e.target.value);
|
||||
});
|
||||
|
||||
// Batch import buttons
|
||||
document.getElementById('addBatchFile').addEventListener('click', () => {
|
||||
this.addBatchFile();
|
||||
// Bulk file upload
|
||||
document.getElementById('bulkFiles').addEventListener('change', (e) => {
|
||||
this.handleBulkFileSelection(e);
|
||||
});
|
||||
|
||||
document.getElementById('batchImportBtn').addEventListener('click', () => {
|
||||
this.handleBatchImport();
|
||||
document.getElementById('autoMapBtn').addEventListener('click', () => {
|
||||
this.autoMapTables();
|
||||
});
|
||||
|
||||
document.getElementById('clearAllBtn').addEventListener('click', () => {
|
||||
this.clearAllFiles();
|
||||
});
|
||||
|
||||
document.getElementById('bulkUploadBtn').addEventListener('click', () => {
|
||||
this.handleBulkUpload();
|
||||
});
|
||||
|
||||
// Status refresh button
|
||||
document.getElementById('refreshStatusBtn').addEventListener('click', () => {
|
||||
this.loadImportStatus();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,98 +223,6 @@ class ImportManager {
|
||||
}
|
||||
}
|
||||
|
||||
addInitialBatchFile() {
|
||||
this.addBatchFile();
|
||||
}
|
||||
|
||||
addBatchFile() {
|
||||
this.batchFileCount++;
|
||||
const container = document.getElementById('batchFiles');
|
||||
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex space-x-3 items-center';
|
||||
fileDiv.id = `batchFile${this.batchFileCount}`;
|
||||
|
||||
fileDiv.innerHTML = `
|
||||
<select name="batch_table_${this.batchFileCount}" class="flex-none w-40 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">-- Select Table --</option>
|
||||
<option value="rolodex">ROLODEX</option>
|
||||
<option value="phone">PHONE</option>
|
||||
<option value="files">FILES</option>
|
||||
<option value="ledger">LEDGER</option>
|
||||
<option value="qdros">QDROS</option>
|
||||
</select>
|
||||
<input type="file" name="batch_file_${this.batchFileCount}" accept=".csv"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<button type="button" onclick="importManager.removeBatchFile(${this.batchFileCount})"
|
||||
class="flex-none px-3 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500">
|
||||
Remove
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
removeBatchFile(fileId) {
|
||||
const fileDiv = document.getElementById(`batchFile${fileId}`);
|
||||
if (fileDiv) {
|
||||
fileDiv.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async handleBatchImport() {
|
||||
const batchFiles = document.getElementById('batchFiles');
|
||||
const fileDivs = batchFiles.children;
|
||||
|
||||
const formData = new FormData();
|
||||
const tableNames = [];
|
||||
let hasFiles = false;
|
||||
|
||||
for (let i = 0; i < fileDivs.length; i++) {
|
||||
const div = fileDivs[i];
|
||||
const tableSelect = div.querySelector('select');
|
||||
const fileInput = div.querySelector('input[type="file"]');
|
||||
|
||||
if (tableSelect.value && fileInput.files[0]) {
|
||||
tableNames.push(tableSelect.value);
|
||||
formData.append('files', fileInput.files[0]);
|
||||
hasFiles = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasFiles) {
|
||||
window.alerts.error('Please select at least one table and file');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add table names to form data
|
||||
tableNames.forEach(name => {
|
||||
formData.append('table_names', name);
|
||||
});
|
||||
|
||||
// Show progress
|
||||
this.showProgress();
|
||||
|
||||
try {
|
||||
const response = await window.http.wrappedFetch('/api/admin/import/batch', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
this.currentImportIds = result.import_ids;
|
||||
this.updateProgress(`Batch import started for ${result.total_files} files`, 'info');
|
||||
this.startBatchPolling();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
this.updateProgress(`Batch import failed: ${error.detail}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Batch import error:', error);
|
||||
this.updateProgress('Batch import failed: Network error', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showProgress() {
|
||||
document.getElementById('importProgress').classList.remove('hidden');
|
||||
@@ -359,6 +281,10 @@ class ImportManager {
|
||||
clearInterval(this.pollInterval);
|
||||
this.updateProgress('Import completed successfully!', 'success');
|
||||
this.showResults(status.result);
|
||||
// Refresh status after successful import
|
||||
setTimeout(() => {
|
||||
this.loadImportStatus();
|
||||
}, 1000);
|
||||
} else if (status.status === 'FAILED') {
|
||||
clearInterval(this.pollInterval);
|
||||
this.updateProgress(`Import failed: ${status.error || 'Unknown error'}`, 'error');
|
||||
@@ -405,6 +331,11 @@ class ImportManager {
|
||||
const message = anyFailed ? 'Batch import completed with some failures' : 'Batch import completed successfully!';
|
||||
const type = anyFailed ? 'warning' : 'success';
|
||||
this.updateProgress(message, type);
|
||||
|
||||
// Refresh the import status after batch completion
|
||||
setTimeout(() => {
|
||||
this.loadImportStatus();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,6 +395,361 @@ class ImportManager {
|
||||
resultsDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
handleBulkFileSelection(event) {
|
||||
const files = Array.from(event.target.files);
|
||||
console.log('Selected files:', files);
|
||||
|
||||
this.bulkFiles = files.map(file => {
|
||||
const suggestion = this.suggestTableType(file.name);
|
||||
return {
|
||||
file: file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
suggested_table: suggestion || 'skip' // Default to skip for unknown files
|
||||
};
|
||||
});
|
||||
|
||||
this.renderBulkFiles();
|
||||
this.updateBulkControls();
|
||||
}
|
||||
|
||||
suggestTableType(filename) {
|
||||
const name = filename.toUpperCase().replace('.CSV', '');
|
||||
const mapping = {
|
||||
// Core supported tables
|
||||
'ROLODEX': 'rolodex',
|
||||
'ROLEX_V': 'rolodex', // ROLEX_V variant
|
||||
'PHONE': 'phone',
|
||||
'FILES': 'files',
|
||||
'FILES_R': 'files',
|
||||
'FILES_V': 'files',
|
||||
'LEDGER': 'ledger',
|
||||
'QDROS': 'qdros',
|
||||
// Lookup and reference tables
|
||||
'GRUPLKUP': 'gruplkup',
|
||||
'PLANINFO': 'planinfo',
|
||||
'RVARLKUP': 'rvarlkup',
|
||||
'FVARLKUP': 'fvarlkup',
|
||||
'TRNSLKUP': 'trnslkup',
|
||||
'EMPLOYEE': 'employee',
|
||||
'FILETYPE': 'filetype',
|
||||
'TRNSTYPE': 'trnstype',
|
||||
'TRNSACTN': 'trnsactn',
|
||||
'FILENOTS': 'filenots',
|
||||
'SETUP': 'setup',
|
||||
'PENSIONS': 'pensions',
|
||||
'PAYMENTS': 'payments',
|
||||
'DEPOSITS': 'deposits',
|
||||
// Form tables
|
||||
'NUMBERAL': 'numberal',
|
||||
'INX_LKUP': 'inx_lkup',
|
||||
'FORM_LST': 'form_lst',
|
||||
'FORM_INX': 'form_inx',
|
||||
'LIFETABL': 'lifetabl',
|
||||
// Pension tables
|
||||
'MARRIAGE': 'marriage',
|
||||
'DEATH': 'death',
|
||||
'SEPARATE': 'separate',
|
||||
'SCHEDULE': 'schedule'
|
||||
};
|
||||
return mapping[name] || '';
|
||||
}
|
||||
|
||||
renderBulkFiles() {
|
||||
const filesDisplay = document.getElementById('bulkFilesDisplay');
|
||||
const filesList = document.getElementById('bulkFilesList');
|
||||
const fileCount = document.getElementById('bulkFileCount');
|
||||
|
||||
fileCount.textContent = this.bulkFiles.length;
|
||||
|
||||
if (this.bulkFiles.length === 0) {
|
||||
filesDisplay.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
filesDisplay.classList.remove('hidden');
|
||||
|
||||
let html = '';
|
||||
this.bulkFiles.forEach((fileObj, index) => {
|
||||
const sizeKB = Math.round(fileObj.size / 1024);
|
||||
|
||||
html += `
|
||||
<div class="flex items-center justify-between p-3 bg-white dark:bg-neutral-700 rounded-lg border border-neutral-200 dark:border-neutral-600">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i class="fa-solid fa-file-csv text-primary-600"></i>
|
||||
<div>
|
||||
<div class="font-medium text-neutral-900 dark:text-neutral-100">
|
||||
${fileObj.name}
|
||||
</div>
|
||||
<p class="text-sm text-neutral-500 dark:text-neutral-400">${sizeKB} KB</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<select onchange="importManager.updateBulkFileTableMapping(${index}, this.value)"
|
||||
class="px-3 py-1 border border-neutral-300 dark:border-neutral-600 rounded-md text-sm bg-white dark:bg-neutral-700 text-neutral-900 dark:text-neutral-100">
|
||||
<option value="">-- Select Table --</option>
|
||||
<optgroup label="Core Tables">
|
||||
<option value="rolodex" ${fileObj.suggested_table === 'rolodex' ? 'selected' : ''}>ROLODEX (Contacts)</option>
|
||||
<option value="phone" ${fileObj.suggested_table === 'phone' ? 'selected' : ''}>PHONE (Phone Numbers)</option>
|
||||
<option value="files" ${fileObj.suggested_table === 'files' ? 'selected' : ''}>FILES (Case Files)</option>
|
||||
<option value="ledger" ${fileObj.suggested_table === 'ledger' ? 'selected' : ''}>LEDGER (Financial)</option>
|
||||
<option value="qdros" ${fileObj.suggested_table === 'qdros' ? 'selected' : ''}>QDROS (Documents)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Lookup Tables">
|
||||
<option value="gruplkup" ${fileObj.suggested_table === 'gruplkup' ? 'selected' : ''}>GRUPLKUP</option>
|
||||
<option value="employee" ${fileObj.suggested_table === 'employee' ? 'selected' : ''}>EMPLOYEE</option>
|
||||
<option value="filetype" ${fileObj.suggested_table === 'filetype' ? 'selected' : ''}>FILETYPE</option>
|
||||
<option value="trnstype" ${fileObj.suggested_table === 'trnstype' ? 'selected' : ''}>TRNSTYPE</option>
|
||||
<option value="trnslkup" ${fileObj.suggested_table === 'trnslkup' ? 'selected' : ''}>TRNSLKUP</option>
|
||||
<option value="rvarlkup" ${fileObj.suggested_table === 'rvarlkup' ? 'selected' : ''}>RVARLKUP</option>
|
||||
<option value="fvarlkup" ${fileObj.suggested_table === 'fvarlkup' ? 'selected' : ''}>FVARLKUP</option>
|
||||
</optgroup>
|
||||
<optgroup label="Financial Tables">
|
||||
<option value="payments" ${fileObj.suggested_table === 'payments' ? 'selected' : ''}>PAYMENTS</option>
|
||||
<option value="deposits" ${fileObj.suggested_table === 'deposits' ? 'selected' : ''}>DEPOSITS</option>
|
||||
<option value="trnsactn" ${fileObj.suggested_table === 'trnsactn' ? 'selected' : ''}>TRNSACTN</option>
|
||||
</optgroup>
|
||||
<optgroup label="Forms & Documents">
|
||||
<option value="numberal" ${fileObj.suggested_table === 'numberal' ? 'selected' : ''}>NUMBERAL</option>
|
||||
<option value="inx_lkup" ${fileObj.suggested_table === 'inx_lkup' ? 'selected' : ''}>INX_LKUP</option>
|
||||
<option value="form_lst" ${fileObj.suggested_table === 'form_lst' ? 'selected' : ''}>FORM_LST</option>
|
||||
<option value="form_inx" ${fileObj.suggested_table === 'form_inx' ? 'selected' : ''}>FORM_INX</option>
|
||||
<option value="lifetabl" ${fileObj.suggested_table === 'lifetabl' ? 'selected' : ''}>LIFETABL</option>
|
||||
</optgroup>
|
||||
<optgroup label="Pension Tables">
|
||||
<option value="pensions" ${fileObj.suggested_table === 'pensions' ? 'selected' : ''}>PENSIONS</option>
|
||||
<option value="marriage" ${fileObj.suggested_table === 'marriage' ? 'selected' : ''}>MARRIAGE</option>
|
||||
<option value="death" ${fileObj.suggested_table === 'death' ? 'selected' : ''}>DEATH</option>
|
||||
<option value="separate" ${fileObj.suggested_table === 'separate' ? 'selected' : ''}>SEPARATE</option>
|
||||
<option value="schedule" ${fileObj.suggested_table === 'schedule' ? 'selected' : ''}>SCHEDULE</option>
|
||||
</optgroup>
|
||||
<optgroup label="Configuration">
|
||||
<option value="setup" ${fileObj.suggested_table === 'setup' ? 'selected' : ''}>SETUP</option>
|
||||
<option value="planinfo" ${fileObj.suggested_table === 'planinfo' ? 'selected' : ''}>PLANINFO</option>
|
||||
<option value="filenots" ${fileObj.suggested_table === 'filenots' ? 'selected' : ''}>FILENOTS</option>
|
||||
</optgroup>
|
||||
<optgroup label="Other">
|
||||
<option value="skip" ${fileObj.suggested_table === 'skip' ? 'selected' : ''}>⚠️ SKIP (Don't Import)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<button onclick="importManager.removeBulkFile(${index})"
|
||||
class="px-2 py-1 bg-red-600 text-white rounded-md hover:bg-red-700 text-sm">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
filesList.innerHTML = html;
|
||||
}
|
||||
|
||||
updateBulkFileTableMapping(index, tableType) {
|
||||
if (this.bulkFiles[index]) {
|
||||
this.bulkFiles[index].suggested_table = tableType;
|
||||
}
|
||||
this.updateBulkControls();
|
||||
}
|
||||
|
||||
removeBulkFile(index) {
|
||||
this.bulkFiles.splice(index, 1);
|
||||
this.renderBulkFiles();
|
||||
this.updateBulkControls();
|
||||
}
|
||||
|
||||
autoMapTables() {
|
||||
this.bulkFiles.forEach(fileObj => {
|
||||
if (!fileObj.suggested_table || fileObj.suggested_table === '') {
|
||||
const suggestion = this.suggestTableType(fileObj.name);
|
||||
// If no mapping found, default to 'skip' for unknown files
|
||||
fileObj.suggested_table = suggestion || 'skip';
|
||||
}
|
||||
});
|
||||
this.renderBulkFiles();
|
||||
this.updateBulkControls();
|
||||
}
|
||||
|
||||
clearAllFiles() {
|
||||
this.bulkFiles = [];
|
||||
document.getElementById('bulkFiles').value = '';
|
||||
this.renderBulkFiles();
|
||||
this.updateBulkControls();
|
||||
}
|
||||
|
||||
updateBulkControls() {
|
||||
const autoMapBtn = document.getElementById('autoMapBtn');
|
||||
const clearAllBtn = document.getElementById('clearAllBtn');
|
||||
const uploadBtn = document.getElementById('bulkUploadBtn');
|
||||
|
||||
const hasFiles = this.bulkFiles.length > 0;
|
||||
const allMapped = this.bulkFiles.every(f => f.suggested_table && f.suggested_table !== '');
|
||||
|
||||
autoMapBtn.disabled = !hasFiles;
|
||||
clearAllBtn.disabled = !hasFiles;
|
||||
uploadBtn.disabled = !hasFiles || !allMapped;
|
||||
|
||||
if (hasFiles) {
|
||||
const mappedCount = this.bulkFiles.filter(f => f.suggested_table && f.suggested_table !== '').length;
|
||||
const importCount = this.bulkFiles.filter(f => f.suggested_table && f.suggested_table !== 'skip').length;
|
||||
uploadBtn.innerHTML = `
|
||||
<i class="fa-solid fa-cloud-upload"></i>
|
||||
<span>Upload & Import ${importCount} Files (${mappedCount} mapped)</span>
|
||||
`;
|
||||
} else {
|
||||
uploadBtn.innerHTML = `
|
||||
<i class="fa-solid fa-cloud-upload"></i>
|
||||
<span>Upload & Import All</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async handleBulkUpload() {
|
||||
if (this.bulkFiles.length === 0) {
|
||||
window.alerts.error('Please select files to upload');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate all files have table mappings
|
||||
const unmappedFiles = this.bulkFiles.filter(f => !f.suggested_table || f.suggested_table === '');
|
||||
if (unmappedFiles.length > 0) {
|
||||
window.alerts.error(`Please select table types for: ${unmappedFiles.map(f => f.name).join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out files marked as 'skip'
|
||||
const filesToImport = this.bulkFiles.filter(f => f.suggested_table !== 'skip');
|
||||
|
||||
if (filesToImport.length === 0) {
|
||||
window.alerts.error('No files selected for import (all marked as skip)');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Starting bulk upload:', filesToImport);
|
||||
|
||||
// Show progress
|
||||
this.showProgress();
|
||||
|
||||
try {
|
||||
// Use the existing batch endpoint that handles FormData
|
||||
const formData = new FormData();
|
||||
|
||||
// Add only files that are not marked as 'skip'
|
||||
filesToImport.forEach(fileObj => {
|
||||
formData.append('files', fileObj.file);
|
||||
});
|
||||
|
||||
// Add corresponding table names
|
||||
filesToImport.forEach(fileObj => {
|
||||
formData.append('table_names', fileObj.suggested_table);
|
||||
});
|
||||
|
||||
const response = await window.http.wrappedFetch('/api/admin/import/batch', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
this.currentImportIds = result.import_ids;
|
||||
this.updateProgress(`Bulk upload started for ${result.total_files} files`, 'info');
|
||||
this.startBatchPolling();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
this.updateProgress(`Bulk upload failed: ${error.detail}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Bulk upload error:', error);
|
||||
this.updateProgress('Bulk upload failed: Network error', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async loadImportStatus() {
|
||||
try {
|
||||
console.log('Loading import status...');
|
||||
const response = await window.http.wrappedFetch('/api/admin/import/status');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.displayImportStatus(data);
|
||||
console.log('Import status loaded:', data);
|
||||
} else {
|
||||
console.error('Failed to load import status, status:', response.status);
|
||||
window.alerts.error('Failed to load import status');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load import status:', error);
|
||||
window.alerts.error('Failed to load import status');
|
||||
}
|
||||
}
|
||||
|
||||
displayImportStatus(data) {
|
||||
const summary = data.summary;
|
||||
const categories = data.categories;
|
||||
|
||||
// Update summary stats
|
||||
document.getElementById('totalTables').textContent = summary.total_tables;
|
||||
document.getElementById('importedTables').textContent = summary.imported_tables;
|
||||
document.getElementById('emptyTables').textContent = summary.empty_tables;
|
||||
document.getElementById('missingTables').textContent = summary.missing_tables;
|
||||
document.getElementById('totalRows').textContent = summary.total_rows.toLocaleString();
|
||||
document.getElementById('completionPercentage').textContent = `${summary.completion_percentage}%`;
|
||||
|
||||
// Update progress bar
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
progressBar.style.width = `${summary.completion_percentage}%`;
|
||||
|
||||
// Display categories
|
||||
const categoryContainer = document.getElementById('statusByCategory');
|
||||
categoryContainer.innerHTML = '';
|
||||
|
||||
Object.keys(categories).forEach(categoryName => {
|
||||
const tables = categories[categoryName];
|
||||
const categoryDiv = document.createElement('div');
|
||||
categoryDiv.className = 'bg-neutral-50 dark:bg-neutral-900 rounded-lg p-4';
|
||||
|
||||
const importedCount = tables.filter(t => t.imported).length;
|
||||
const totalCount = tables.length;
|
||||
|
||||
categoryDiv.innerHTML = `
|
||||
<h3 class="font-semibold text-neutral-900 dark:text-neutral-100 mb-3 flex items-center justify-between">
|
||||
<span>${categoryName} Tables</span>
|
||||
<span class="text-sm font-normal text-neutral-600 dark:text-neutral-400">${importedCount}/${totalCount} imported</span>
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
${tables.map(table => `
|
||||
<div class="flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-neutral-200 dark:border-neutral-600">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
${table.imported ?
|
||||
'<i class="fa-solid fa-check-circle text-green-600"></i>' :
|
||||
table.exists ?
|
||||
'<i class="fa-solid fa-exclamation-circle text-yellow-600"></i>' :
|
||||
'<i class="fa-solid fa-times-circle text-red-600"></i>'
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-neutral-900 dark:text-neutral-100">${table.display_name}</div>
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
${table.imported ?
|
||||
`${table.row_count.toLocaleString()} rows` :
|
||||
table.exists ?
|
||||
'Empty table' :
|
||||
'Not created'
|
||||
}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-400 dark:text-neutral-500">
|
||||
Expected: ${table.expected_files.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
categoryContainer.appendChild(categoryDiv);
|
||||
});
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
|
||||
Reference in New Issue
Block a user