class MailingImportDialog extends EICDialogContent { actions = [ { label: 'Cancel', onclick: this.cancel.bind(this), severity: 'secondary', role: 'cancel' }, { label: 'Import recipients', onclick: this.add.bind(this), severity: 'primary', role: 'add', disabled: true } ] constructor(...args) { super(...args) Object.assign(this, app.helpers.validators) } DOMContentLoaded(options) { this.model = options.model this.mid = options.mid let components = ui.eicfy(this.el) this.fileUpload = new FileUpload(this.find('.import-dialog .browseBtn'), { uploadUrl: '#', uploadMethod: '', submitLabel:'Analyse file', allowedExtensions: ['xls','xlsx'], allowedMimes:['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel'], allowDrop: true, maxFiles: 1, minFiles: 1, dndLabel: 'Drop your recipents Excel list here !', noUpload: true, }); this.fileUpload.onFileAdded = this.onFileAdded.bind(this) this.fileUpload.onFileRemoved = this.onFileRemoved.bind(this) this.fileUpload.onWrongFileType = this.onWrongFileType.bind(this) this.report = this.find('.import-dialog .report') this.reportTitle = this.find('.import-dialog .report-title') this.reportDetails = this.find('.import-dialog .report-details') this.removeDups = components.find(item => (item.el.classList.contains('remove-dups'))) this.recipientsToImport = {} } onFileAdded(fileIdx, file){ let reader = new FileReader() reader.onload = this.verifyData.bind(this, reader, file) this.fileUpload.browseBtn.loading = true reader.readAsArrayBuffer(file) } onWrongFileType(file){ ui.show(this.report) this.fileUpload.browseBtn.loading = false // because stopping the loading reactivates the button by default (god's will) if(this.fileUpload.filesList.length==0) this.fileUpload.browseBtn.disabled = false else this.fileUpload.browseBtn.disabled = true this.reportTitle.classList.add('error') this.reportTitle.innerHTML = `Could not import file !` this.reportDetails.innerHTML = `Wrong file type (${file.type})` } verifyData(reader, file){ this.fileUpload.browseBtn.loading = false // because stopping the loading reactivates the button by default (god's will) if(this.fileUpload.filesList.length==0) this.fileUpload.browseBtn.disabled = false else this.fileUpload.browseBtn.disabled = true let error let workbook = null let recipientsArray = null let recipientsList = null try{ workbook = XLSX.read(reader.result) } catch(e) { console.warn('Trying to parse Excel file:',e) error = 'Error parsing this as Excel file !' } if((!error) && workbook){ let first_sheet = workbook.Sheets[workbook.SheetNames[0]] try{ recipientsArray = XLSX.utils.sheet_to_json(first_sheet, { header: 1 }) } catch(e) { recipientsArray = null error = 'Error parsing this as Excel sheet !' } if(recipientsArray && (!first_sheet['!margins'])) { recipientsArray = null error = 'Could not parse this as Excel file !' } } let result = {} if(recipientsArray){ result = this.getEmailRows(recipientsArray, file) } ui.show(this.report) if(result.recipientsList){ this.reportTitle.classList.remove('error') this.reportTitle.innerHTML = `Ready to import ${result.recipientsList.length} recipients !` let headersList = '' if(result.availableColumns.some(item => item.hint)){ headersList = result.availableColumns.map(item=>item.hint).join(', ') } this.reportDetails.innerHTML = ` ` this.recipientsToImport = result this.recipientsToImport.sourceName = file.name this.recipientsToImport.sourceType = 'Excel' } else { this.reportTitle.classList.add('error') this.reportTitle.innerHTML = error this.reportDetails.innerHTML = `` this.recipientsToImport = {} } if(this.fileUpload.filesList.length==0) this.actions.find(o => o.role == 'add').button.disabled = true else this.actions.find(o => o.role == 'add').button.disabled = false } getEmailRows(recipientsArray, file){ if((!Array.isArray(recipientsArray)) || (recipientsArray.length<1)){ return({ err: 'The parsed Excel is not an array or has no rows !', recipientsList:[], importedData:[], rejectedRows: [], nbDups: 0, emailColIdx:null, emailColName:null, availableColumns: null, }) } //const mailRFC5322Regex = /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/ //Spot the column where there are the most email-addresses let colsCount = [] for(let row of recipientsArray){ let colIdx = 0 for(let cell of row){ if(typeof(cell) === null) cell = '' if(typeof(cell)=='string'){ if(typeof(colsCount[colIdx])=='undefined') colsCount[colIdx] = 0 if(this.validators.isValidEmail(cell)) colsCount[colIdx]++ } colIdx++ } } // cleanup empty slots => don't use "in", or "of" ! for(let idx=0; idx(val>colsCount[acc]?idx:acc),0) // Now that we know where to look, remove rows without email in that col let cleanRecipients = [] let rejectedRows = [] let firstValidRow = null let rowIdx = 1 for(let row of recipientsArray){ let cell = row[emailCol] if(typeof(cell)!='string'){ rejectedRows.push(rowIdx) } else { if(this.validators.isValidEmail(cell)){ cleanRecipients.push(row) if(!firstValidRow) firstValidRow = rowIdx } else rejectedRows.push(rowIdx) } rowIdx++ } // The row before the first valid email row might be headers let availableColumns if(firstValidRow>1){ let headersRow = recipientsArray[firstValidRow-2] // rowIdx & firstValidRow start at 1 => row before the first valid is -2 in recipientsArray availableColumns = headersRow.map( (item, idx) => ({value: idx, hint: ((item.length > 150) ? item.slice(0, 20) + '...' : item ), isEmail: (idx==emailCol) }) ) } else { availableColumns = recipientsArray[firstValidRow].map( (item, idx) => ({value: idx, hint: '', isEmail: (idx==emailCol) }) ) } let nodups=[] const uniqueRecipients = cleanRecipients.filter(item=>{ if(!nodups.includes(item[emailCol])) { nodups.push(item[emailCol]); return(true) } return(false) }) let finalRecipients if(this.removeDups.value=='yes'){ finalRecipients = uniqueRecipients } else { finalRecipients = cleanRecipients } let recipientsList=[] for(let recipient of finalRecipients) { recipientsList.push({ sourceType: "Excel", sourceName: file.name, email:recipient[emailCol], meta:{}, //Empty mappings }) } return({ err: false, recipientsList: recipientsList, importedData: finalRecipients, rejectedRows: rejectedRows, nbDups: cleanRecipients.length - uniqueRecipients.length, emailColIdx: emailCol, emailColName: XLSX.utils.encode_col(emailCol), availableColumns: availableColumns, }) } onFileRemoved(fileIdx, file){ if(this.fileUpload.filesList.length==0){ // normally the case as maxFiles=1 this.reportTitle.innerHTML = '' this.reportDetails.innerHTML = '' ui.hide(this.report) this.recipientsToImport = {} this.actions.find(o => o.role == 'add').button.disabled = true } else { this.actions.find(o => o.role == 'add').button.disabled = false } } async add() { this.actions.find(o => o.role == 'add').button.loading = true this.recipientsToImport.importDate = (new Date()).toISOString() await this.model.saveImport(this.mid, { sourceName: this.recipientsToImport.sourceName, sourceType: this.recipientsToImport.sourceType, data: this.recipientsToImport.importedData, availableColumns: this.recipientsToImport.availableColumns, }) this.commit(true) } onFailed() { this.abort() } } app.registerClass('MailingImportDialog',MailingImportDialog);