1/** 2 * Public JavaScript for Odoo Helpdesk Integration 3 */ 4(function($) { 5 'use strict'; 6 7 // Form handling 8 $(document).ready(function() { 9 const $form = $('#odoo-helpdesk-form'); 10 11 // If form not found, exit 12 if (!$form.length) { 13 return; 14 } 15 16 const filesCollection = []; 17 const $fileInput = $('#odoo-file-upload'); 18 const $fileList = $('#odoo-file-list'); 19 const $submitBtn = $('#odoo-form-submit'); 20 const $successMsg = $('#odoo-form-success'); 21 const $errorMsg = $('#odoo-form-error'); 22 const $loadingIndicator = $('#odoo-form-loading'); 23 24 // File selection handling 25 $fileInput.on('change', function() { 26 // Get the newly selected files 27 const newFiles = Array.from(this.files); 28 29 // Add new files to collection 30 newFiles.forEach(file => { 31 // Check if file with same name already exists in collection 32 const exists = filesCollection.some(f => f.name === file.name); 33 if (!exists) { 34 filesCollection.push(file); 35 } 36 }); 37 38 // Clear the file input so the next selection doesn't replace these files 39 $(this).val(''); 40 41 // Update the file list display 42 updateFileList(); 43 }); 44 45 // Validate file size and type 46 function validateFile(file) { 47 // Check file size (convert MB to bytes) 48 const maxSizeBytes = parseInt(odooHelpdeskForm.messages.maxFileSize || 5) * 1024 * 1024; 49 if (file.size > maxSizeBytes) { 50 return { 51 valid: false, 52 message: odooHelpdeskForm.messages.fileTooBig 53 }; 54 } 55 56 // Check file type 57 const allowedTypes = [ 58 'image/jpeg', 59 'image/jpg', 60 'image/png', 61 'image/gif', 62 'image/bmp', 63 'image/webp', 64 'image/tiff', 65 'image/svg+xml', 66 'image/heic', 67 'image/heif' 68 ]; 69 70 if (allowedTypes.indexOf(file.type) === -1) { 71 return { 72 valid: false, 73 message: odooHelpdeskForm.messages.invalidFileType 74 }; 75 } 76 77 return { valid: true }; 78 } 79 80 // Update file list display 81 function updateFileList() { 82 $fileList.empty(); 83 84 if (filesCollection.length === 0) { 85 return; 86 } 87 88 let allValid = true; 89 90 for (let i = 0; i < filesCollection.length; i++) { 91 const file = filesCollection[i]; 92 const validation = validateFile(file); 93 const $item = $('<div>', { 94 class: 'odoo-file-item' + (validation.valid ? '' : ' invalid') 95 }); 96 97 $item.append( 98 $('<span>', { class: 'file-name', text: file.name }), 99 $('<span>', { 100 class: 'file-size', 101 text: formatFileSize(file.size) 102 }) 103 ); 104 105 // Add a remove button for each file 106 const $removeBtn = $('<button>', { 107 type: 'button', 108 class: 'odoo-file-remove', 109 text: 'x', 110 'data-index': i, 111 click: function() { 112 const index = $(this).data('index'); 113 filesCollection.splice(index, 1); 114 updateFileList(); 115 } 116 }); 117 118 $item.append($removeBtn); 119 120 if (!validation.valid) { 121 $item.append( 122 $('<span>', { 123 class: 'file-error', 124 text: validation.message 125 }) 126 ); 127 allValid = false; 128 } 129
130 $fileList.append($item); 131 } 132 133 // Disable submit if any files are invalid 134 $submitBtn.prop('disabled', !allValid); 135 } 136 137 // Format file size for display 138 function formatFileSize(bytes) { 139 if (bytes === 0) return '0 Bytes'; 140 141 const k = 1024; 142 const sizes = ['Bytes', 'KB', 'MB']; 143 const i = Math.floor(Math.log(bytes) / Math.log(k)); 144 145 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 146 } 147 148 // Custom form validation 149 function validateForm() { 150 let isValid = true; 151 let firstInvalid = null; 152 153 // Check required fields 154 $form.find('[required]').each(function() { 155 const $field = $(this); 156 const $formGroup = $field.closest('.odoo-form-group'); 157 const $errorMessage = $formGroup.find('.validation-error'); 158 159 // Remove any existing error message 160 if ($errorMessage.length) { 161 $errorMessage.remove(); 162 } 163 164 // Check if field is empty 165 if (!$field.val().trim()) { 166 isValid = false; 167 168 // Create and add error message 169 const fieldName = $field.attr('aria-labelledby') 170 ? $('#' + $field.attr('aria-labelledby')).text().replace('*', '').trim() 171 : 'This field'; 172 173 const $error = $('<div>', { 174 'class': 'validation-error', 175 'id': $field.attr('id') + '-error', 176 'text': fieldName + ' is required.', 177 'role': 'alert' 178 }); 179 180 $formGroup.append($error); 181 $field.attr('aria-invalid', 'true') 182 .attr('aria-describedby', $field.attr('id') + '-error'); 183 184 if (!firstInvalid) { 185 firstInvalid = $field; 186 } 187 } else { 188 $field.removeAttr('aria-invalid') 189 .removeAttr('aria-describedby'); 190 } 191 }); 192 193 // Check if any category is selected 194 if ($('input[name="category"]:checked').length === 0) { 195 isValid = false; 196 const $radioGroup = $('.odoo-radio-group'); 197 198 // Remove any existing error message 199 $radioGroup.find('.validation-error').remove(); 200 201 // Create and add error message 202 const $error = $('<div>', { 203 'class': 'validation-error', 204 'id': 'category-error', 205 'text': 'Please select a support category.', 206 'role': 'alert' 207 }); 208 209 $radioGroup.append($error); 210 $radioGroup.attr('aria-describedby', 'category-error'); 211 212 if (!firstInvalid) { 213 firstInvalid = $('input[name="category"]').first(); 214 } 215 } else { 216 $('.odoo-radio-group').removeAttr('aria-describedby'); 217 } 218 219 // Focus the first invalid field 220 if (firstInvalid) { 221 firstInvalid.focus(); 222 } 223 224 return isValid; 225 } 226 227 // Form submission 228 $form.on('submit', function(e) { 229 e.preventDefault(); 230 231 // Hide previous messages 232 $successMsg.hide(); 233 $errorMsg.hide(); 234 235 // Validate form 236 if (!$form[0].checkValidity()) { 237 $form[0].reportValidity(); 238 return; 239 } 240 241 // Check reCAPTCHA if enabled 242 if (odooHelpdeskForm.recaptchaEnabled) { 243 const recaptchaResponse = grecaptcha.getResponse(); 244 if (recaptchaResponse.length === 0) {
245 $errorMsg.text(odooHelpdeskForm.messages.recaptchaError).show(); 246 return; 247 } 248 } 249 250 // Show loading indicator 251 $loadingIndicator.show(); 252 $submitBtn.prop('disabled', true); 253 254 // Prepare form data 255 const formData = new FormData($form[0]); 256 formData.append('action', 'submit_odoo_ticket'); 257 formData.append('odoo_helpdesk_nonce', odooHelpdeskForm.nonce); 258 259 // Submit form data via AJAX 260 $.ajax({ 261 url: odooHelpdeskForm.ajaxUrl, 262 type: 'POST', 263 data: formData, 264 processData: false, 265 contentType: false, 266 success: function(response) { 267 if (response.success) { 268 // Handle successful ticket creation 269 handleSuccessfulSubmission(response.data); 270 } else { 271 // Handle error 272 $errorMsg.text(response.data.message || odooHelpdeskForm.messages.error).show(); 273 $submitBtn.prop('disabled', false); 274 // Reset recaptcha if enabled 275 if (odooHelpdeskForm.recaptchaEnabled) { 276 grecaptcha.reset(); 277 } 278 } 279 }, 280 error: function() { 281 $errorMsg.text(odooHelpdeskForm.messages.error).show(); 282 $submitBtn.prop('disabled', false); 283 // Reset recaptcha if enabled 284 if (odooHelpdeskForm.recaptchaEnabled) { 285 grecaptcha.reset(); 286 } 287 }, 288 complete: function() { 289 $loadingIndicator.hide(); 290 } 291 }); 292 }); 293 294 // Display error message 295 function showError(message) { 296 $errorMsg.text(message).show(); 297 $errorMsg.focus(); // Focus for screen readers 298 } 299 300 // Handle successful form submission and file uploads 301 function handleSuccessfulSubmission(data) { 302 const ticketId = data.ticket_id; 303 304 // If no files to upload, show success message immediately 305 if (filesCollection.length === 0) { 306 showSuccessMessage(); 307 return; 308 } 309 310 // Upload files one by one 311 let uploadedCount = 0; 312 let failedUploads = 0; 313 314 const uploadNextFile = function(index) { 315 if (index >= filesCollection.length) { 316 // All files processed 317 if (failedUploads > 0) { 318 $successMsg.text( 319 odooHelpdeskForm.messages.success + ' ' + 320 failedUploads + ' file(s) could not be uploaded.' 321 ).show(); 322 } else { 323 showSuccessMessage(); 324 } 325 return; 326 } 327 328 const file = filesCollection[index]; 329 const fileData = new FormData(); 330 fileData.append('action', 'upload_odoo_attachment'); 331 fileData.append('odoo_helpdesk_nonce', odooHelpdeskForm.nonce); 332 fileData.append('ticket_id', ticketId); 333 fileData.append('file', file); 334 335 $.ajax({
336 url: odooHelpdeskForm.ajaxUrl, 337 type: 'POST', 338 data: fileData, 339 processData: false, 340 contentType: false, 341 success: function(response) { 342 if (response.success) { 343 uploadedCount++; 344 } else { 345 failedUploads++; 346 } 347 }, 348 error: function() { 349 failedUploads++; 350 }, 351 complete: function() { 352 // Process next file 353 uploadNextFile(index + 1); 354 } 355 }); 356 }; 357 358 // Start uploading files 359 uploadNextFile(0); 360 } 361 362 // Show success message and reset form 363 function showSuccessMessage() { 364 $form.hide(); 365 $successMsg.text(odooHelpdeskForm.messages.success).show(); 366 $form[0].reset(); 367 $fileList.empty(); 368 filesCollection.length = 0; 369 370 // Reset recaptcha if enabled 371 if (odooHelpdeskForm.recaptchaEnabled && typeof grecaptcha !== 'undefined') { 372 grecaptcha.reset(); 373 } 374 } 375 376 // Add CSS for validation errors 377 $('<style>') 378 .text(` 379 .validation-error { 380 color: #d63638; 381 font-size: 0.9em; 382 margin-top: 5px; 383 } 384 [aria-invalid="true"] { 385 border-color: #d63638; 386 } 387 `) 388 .appendTo('head'); 389 }); 390 391})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.