Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 125 additions & 9 deletions js/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ $(document).ready(function () {
// Event handler for file input change
$('#input-uploadxml-file').on('change', function (event) {
const file = event.target.files[0];
if (file) {
if (isXmlFile(file)) {
handleXmlFile(file);
} else if (file) {
showUploadStatus(translateWithFallback('modals.upload.invalidFileType', 'Please upload an XML file.'), 'danger');
}
});

Expand All @@ -38,19 +40,68 @@ $(document).ready(function () {
dropZone.removeClass('border-primary');

const file = event.originalEvent.dataTransfer.files[0];
if (file && file.type === 'text/xml' || file.name.endsWith('.xml')) {
if (isXmlFile(file)) {
handleXmlFile(file);
} else {
showUploadStatus('Please upload an XML file.', 'danger');
showUploadStatus(translateWithFallback('modals.upload.invalidFileType', 'Please upload an XML file.'), 'danger');
}
});

// Reset modal state when closed
$('#modal-uploadxml').on('hidden.bs.modal', function () {
$('#input-uploadxml-file').val('');
setUploadLoadingState(false);
$('#xml-upload-status').addClass('d-none');
$('#panel-uploadxml-dropfile').removeClass('border-primary');
});
});

/**
* Checks whether a file is a valid XML file by type or extension
* @param {File|undefined} file - The file to check
* @returns {boolean} True if the file is an XML file
*/
function isXmlFile(file) {
if (!file) return false;
if (file.type === 'text/xml' || file.type === 'application/xml') return true;
return !!(file.name && file.name.endsWith('.xml'));
}

/**
* Returns a translated string with a safe fallback
* @param {string} key - The i18n key
* @param {string} fallback - Fallback string if translation is unavailable
* @returns {string}
*/
function translateWithFallback(key, fallback) {
const translate = (window.elmo && typeof window.elmo.translate === 'function')
? window.elmo.translate
: null;
return (translate && translate(key)) || fallback;
}

/**
* Builds the toast/fallback message for a given file name and type
* @param {string} fileName - The uploaded file name
* @param {string} type - 'success' or 'danger'
* @returns {string}
*/
function buildUploadMessage(fileName, type) {
if (type === 'success') {
const successText = translateWithFallback('modals.upload.successToast', 'successfully loaded');
return fileName + ' ' + successText;
}
const errorText = translateWithFallback('modals.upload.errorToast', 'Error loading file');
return errorText + ': ' + fileName;
}

/**
* Handles the uploaded XML file
* @param {File} file - The uploaded XML file
*/
function handleXmlFile(file) {
setUploadLoadingState(true);

const reader = new FileReader();

reader.onload = async function (event) {
Expand All @@ -65,25 +116,90 @@ function handleXmlFile(file) {
// Load XML data into form
await loadXmlToForm(xmlDoc);

// Close modal and show success message
// Close modal and show success toast
setUploadLoadingState(false);
$('#modal-uploadxml').modal('hide');
showUploadStatus('XML file successfully loaded', 'success');
showUploadToast(file.name, 'success');

} catch (error) {
console.error('Error:', error);
showUploadStatus('Error processing XML file: ' + error.message, 'danger');
setUploadLoadingState(false);
showUploadStatus(translateWithFallback('modals.upload.errorProcessing', 'Error processing XML file') + ': ' + error.message, 'danger');
}
};

reader.onerror = function () {
showUploadStatus('Error reading file', 'danger');
setUploadLoadingState(false);
showUploadStatus(translateWithFallback('modals.upload.errorReading', 'Error reading file'), 'danger');
};

reader.readAsText(file);
}

/**
* Shows upload status message
* Toggles loading state in the upload modal
* @param {boolean} loading - Whether the loading state should be active
*/
function setUploadLoadingState(loading) {
const fileInput = $('#input-uploadxml-file');
const dropZone = $('#panel-uploadxml-dropfile');
const spinner = $('#upload-spinner-overlay');

if (loading) {
fileInput.prop('disabled', true);
dropZone.addClass('pe-none opacity-50');
spinner.removeClass('d-none');
$('#xml-upload-status').addClass('d-none').text('');
} else {
fileInput.prop('disabled', false);
dropZone.removeClass('pe-none opacity-50');
spinner.addClass('d-none');
}
}

/**
* Shows a Bootstrap toast notification after upload
* @param {string} fileName - The name of the uploaded file
* @param {string} type - 'success' or 'danger'
*/
function showUploadToast(fileName, type) {
const toastEl = document.getElementById('toast-upload-feedback');
if (!toastEl) {
showUploadStatus(buildUploadMessage(fileName, type), type === 'success' ? 'success' : 'danger');
return;
}

if (!window.bootstrap || !window.bootstrap.Toast) {
showUploadStatus(buildUploadMessage(fileName, type), type === 'success' ? 'success' : 'danger');
return;
}

const messageEl = document.getElementById('toast-upload-feedback-message');
const iconEl = document.getElementById('toast-upload-feedback-icon');

if (!messageEl || !iconEl) {
showUploadStatus(buildUploadMessage(fileName, type), type === 'success' ? 'success' : 'danger');
return;
}

toastEl.classList.remove('text-bg-success', 'text-bg-danger');

if (type === 'success') {
toastEl.classList.add('text-bg-success');
iconEl.className = 'bi bi-check-circle-fill me-2';
} else {
toastEl.classList.add('text-bg-danger');
iconEl.className = 'bi bi-exclamation-triangle-fill me-2';
}

messageEl.textContent = buildUploadMessage(fileName, type);

const toast = new window.bootstrap.Toast(toastEl, { delay: 5000 });
toast.show();
}

/**
* Shows upload status message inside the modal
* @param {string} message - The message to display
* @param {string} type - Bootstrap alert type (success, danger, etc.)
*/
Expand All @@ -102,5 +218,5 @@ function showUploadStatus(message, type) {

// Export for testing
if (typeof module !== 'undefined' && module.exports) {
module.exports = { handleXmlFile, showUploadStatus };
module.exports = { handleXmlFile, showUploadStatus, setUploadLoadingState, showUploadToast, isXmlFile, translateWithFallback, buildUploadMessage };
}
8 changes: 7 additions & 1 deletion lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,13 @@
},
"upload": {
"title": "XML-Datei auswählen:",
"dropZoneInfo": "XML-Datei hier ablegen"
"dropZoneInfo": "XML-Datei hier ablegen",
"loading": "Datei wird verarbeitet...",
"successToast": "erfolgreich geladen",
"errorToast": "Fehler beim Laden der Datei",
"invalidFileType": "Bitte laden Sie eine XML-Datei hoch.",
"errorProcessing": "Fehler beim Verarbeiten der XML-Datei",
"errorReading": "Fehler beim Lesen der Datei"
},
"changelog": {
"title": "Änderungsprotokoll",
Expand Down
8 changes: 7 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,13 @@
},
"upload": {
"title": "Select XML file:",
"dropZoneInfo": "Drop XML file here"
"dropZoneInfo": "Drop XML file here",
"loading": "Processing file...",
"successToast": "successfully loaded",
"errorToast": "Error loading file",
"invalidFileType": "Please upload an XML file.",
"errorProcessing": "Error processing XML file",
"errorReading": "Error reading file"
},
"changelog": {
"title": "Changelog",
Expand Down
8 changes: 7 additions & 1 deletion lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,13 @@
},
"upload": {
"title": "Sélectionner un fichier XML :",
"dropZoneInfo": "Déposer le fichier XML ici"
"dropZoneInfo": "Déposer le fichier XML ici",
"loading": "Traitement du fichier...",
"successToast": "chargé avec succès",
"errorToast": "Erreur lors du chargement du fichier",
"invalidFileType": "Veuillez télécharger un fichier XML.",
"errorProcessing": "Erreur lors du traitement du fichier XML",
"errorReading": "Erreur lors de la lecture du fichier"
},
"changelog": {
"title": "Journal des modifications",
Expand Down
20 changes: 20 additions & 0 deletions modals.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ <h5 class="modal-title" id="modal-uploadxml-label">XML hochladen</h5>
<div class="drop-zone mb-3 p-5 border rounded text-center" id="panel-uploadxml-dropfile">
<p class="mb-0" data-translate="modals.upload.dropZoneInfo">Drop XML file here</p>
</div>
<div id="upload-spinner-overlay" class="d-none text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden" data-translate="modals.upload.loading">Processing file...</span>
</div>
<p class="mt-2 text-muted" aria-hidden="true" data-translate="modals.upload.loading">Processing file...</p>
</div>
<div class="alert alert-info d-none" id="xml-upload-status"></div>
</div>
</div>
Expand Down Expand Up @@ -384,4 +390,18 @@ <h2 class="modal-title fs-5" id="label-feedbackmodal" data-translate="modals.fee
</div>
</div>
</div>
</div>
<!-- Upload Feedback Toast -->
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1090;" id="toast-upload-container">
<div id="toast-upload-feedback" class="toast align-items-center border-0" role="alert"
aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="toast-upload-feedback-body">
<i id="toast-upload-feedback-icon" class="bi me-2" aria-hidden="true"></i>
<span id="toast-upload-feedback-message"></span>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto"
data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
Loading
Loading