unclean SPARC

This commit is contained in:
STEINNI
2025-08-27 07:03:09 +00:00
commit f308460931
430 changed files with 54426 additions and 0 deletions
@@ -0,0 +1,17 @@
<style>
.dups-dialog li.row{ grid-template-columns: 1fr 2fr 5rem;}
.dups-dialog .cell ul{ list-style: none; }
.dups-dialog section.dups section{max-height: 50vh;}
</style>
<div class="dups-dialog">
<section class="dups">
<article eiccard eiccard>
<header>Duplicate emails found across your sources:</header>
<section>
<div data-output="dupsGrid"></div>
</section>
</article>
</section>
</div>
@@ -0,0 +1,47 @@
class MailingDuplicatesDialog extends EICDialogContent {
actions = [
{
label: 'Done',
onclick: this.cancel.bind(this),
severity: 'primary',
role: 'cancel'
}
]
constructor(...args) {
super(...args)
Object.assign(this, app.helpers.activeAttributes)
}
DOMContentLoaded(options) {
let components = ui.eicfy(this.el)
this.setupRefs(components)
this.dupesGrid = new DataGrid(this.outputs.dupsGrid, {
headers: [
{label: 'Email'},
{label: 'Sources'},
{label: 'Row numbers'},
],
actions:[],
})
this.fillDupsGrid(options.dupes)
}
fillDupsGrid(dupes){
let idx=0
for(let email in dupes){
let sources = dupes[email].map( item =>item.sourceName).join('</li><li>')
let rownbs = dupes[email].map( item =>item.rownb+1).join('</li><li>')
let row = this.dupesGrid.addRow(idx, [
email, // expecting source import datestamp
`<ul><li>${sources}</li></ul>`,
`<ul><li>${rownbs}</li></ul>`,
], true)
idx++
}
}
}
app.registerClass('MailingDuplicatesDialog',MailingDuplicatesDialog)
@@ -0,0 +1,15 @@
<style>
.import-dialog{ width: 50vw; }
</style>
<div class="import-dialog">
<section class="import">
<label>Drop or select your Excel file...</label>
<div eicfileupload class="browseBtn"></div>
<article eiccard eiccard collapsable collapsed hidden class="report">
<header><span large class="report-title"></span></header>
<section class="report-details">
</section>
</article>
</section>
</div>
@@ -0,0 +1,251 @@
class MailingExclusionDialog extends EICDialogContent {
actions = [
{
label: 'Cancel',
onclick: this.cancel.bind(this),
severity: 'secondary',
role: 'cancel'
},
{
label: 'Import & Exclude them',
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.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 = `Found ${result.recipientsList.length} emails to use as Excusion list!`
let headersList = ''
if(result.availableColumns.some(item => item.hint)){
headersList = result.availableColumns.map(item=>item.hint).join(', ')
}
this.reportDetails.innerHTML = `
<ul>
<li>Column used for the emails: <span success>${result.emailColName}</span></li>
<li><span success>${(result.rejectedRows.length>0) ? result.rejectedRows.length : 'No'}</span> ${ (result.rejectedRows.length==1) ? 'row was' : 'rows where' } rejected because column ${result.emailColName} did not contain an email.</li>
<li>Removed <span success>${result.nbDups}</span> emails that ${result.nbDups.length>1 ? 'where duplicates' : 'was a duplicate'}
</li>
</ul>
`
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<colsCount.length; idx++){ if(typeof(colsCount[idx])!='number') colsCount[idx]=0 }
// find winning column
let emailCol = colsCount.reduce((acc,val, 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 = 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.saveExclusion(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('MailingExclusionDialog',MailingExclusionDialog);
@@ -0,0 +1,75 @@
<style>
.fetch-dialog{ width: 78vw; }
.fetch-dialog .icon-spinner{ top:0; right:1em; }
.fetch-dialog div.field{ margin: .5em 0 0.5em 0; }
.fetch-dialog article.searchCriteria{ max-height:70vh; min-height: 50vh; }
.fetch-dialog article.sourceName header h1, .fetch-dialog article.searchType header h1,
.fetch-dialog article.searchCriteria header h1 {color: var(--eicui-base-color-primary-100);}
.fetch-dialog div.criteria-output{grid-template-columns: 3fr 2fr;}
.fetch-dialog .searchOutput ul{list-style: none; font-size: var(--eicui-base-font-size-s);}
.fetch-dialog header h1 span.icon-info{margin-left: 1em;}
.fetch-dialog .searchCriteria div.cols-2.field-label{grid-template-columns: auto 2em;justify-content: flex-start;}
.fetch-dialog menu.searchCriteriaGroups li { white-space: collapse; width: 8em; }
.fetch-dialog [data-output="resultLegend"] alert {border-width: 1em;}
.fetch-dialog .searchOutput button.collapser{
min-height: calc(var(--eicui-base-spacing-l) + var(--eicui-base-spacing-2xs));
min-width: calc(var(--eicui-base-spacing-l) + var(--eicui-base-spacing-2xs));
}
.fetch-dialog .search-criteria{ grid-template-columns: 1fr 8em; }
.fetch-dialog .sample-refresh{ grid-template-columns: 1fr 1em; }
.fetch-dialog [data-trigger="onRefreshSample"]{ position: absolute; top: 0; right: 1em; }
.fetch-dialog [data-output="resultLegend"] li { padding-top: var(--eicui-base-font-size-2xs); }
.fetch-dialog [data-output="resultLegend"] li .cols-2{ grid-template-columns:1.5em auto; }
.fetch-dialog [data-output="resultLegend"] .color-dot{border-radius: 1em;width: 1.5em; height: 1.5em; display: inline-block; vertical-align: middle; }
.fetch-dialog [data-output="resultLegend"] i{white-space: nowrap; }
.fetch-dialog article.probe-results ul li div.cols-2{ grid-template-columns: 10em auto; }
.fetch-dialog article.probe-results header{ grid-template-columns: auto 4em; }
.fetch-dialog article.probe-results header button {width: 2em; margin-left: 0.2em;}
.fetch-dialog article.probe-results ul li b { background: var(--eicui-base-color-info-25); padding: .2em; margin: .1em 0; }
</style>
<div class="fetch-dialog">
<section>
<div class="cols-2">
<article eiccard class="sourceName">
<header>
<h1>Name for this source in your mailing:</h1>
</header>
<section>
<input eicinput type="text" name="sourceName" placeholder="People in science projects" data-ref="sourceName" data-change="onSourceName">
</section>
</article>
<article eiccard class="searchType">
<header data-async="searchType">
<h1>Type of search</h1>
</header>
<section>
<select eicselect name="searchType" data-ref="searchType" data-change="onsearchType">
<option></option>
</select>
</section>
</article>
</div>
<div class="cols-2 criteria-output">
<article eiccard class="searchCriteria">
<header data-async="searchCriteria">
<h1>Search criteria</h1>
</header>
<section data-output="searchCriteria">
</section>
</article>
<article eiccard class="searchOutput">
<header data-async="searchOutput">
<div class="cols-2 search-criteria">
<h1>Search results</h1>
<button eicbutton small primary data-ref="searchBtn" data-trigger="onFiltersChange" disabled>Search</button>
</div>
</header>
<section data-output="searchOutput">
</section>
</article>
</div>
</section>
</div>
@@ -0,0 +1,349 @@
class MailingFetchDialog extends EICDialogContent {
actions = [
{
label: 'Cancel',
onclick: this.cancel.bind(this),
severity: 'secondary',
role: 'cancel'
},
{
label: 'Use these recipients',
onclick: this.add.bind(this),
severity: 'primary',
role: 'add',
disabled: true
}
]
constructor(...args) {
super(...args)
Object.assign(this, app.helpers.activeAttributes)
this.currentFilters = []
this.currentQCId = null
this.currentNbResults = 0
this.baseColors = ['success','primary','danger','warning','accent','secondary']
}
DOMContentLoaded(options) {
this.contactsModel = options.models.contacts
this.mailingsModel = options.models.mailings
this.mid = options.mid
const components = ui.eicfy(this.el)
this.setupTriggers(components)
this.setupRefs(components)
this.components.searchType.disabled = true
this.setAsyncLoading(true, ['searchType'])
this.contactsModel.list().then(this.fillQCSelector.bind(this))
}
fillQCSelector(qcList){
for(let query of qcList){
this.components.searchType.el.append(ui.create(
`<option value="${query.id}">${query.title}</option>`
))
}
this.components.searchType.disabled = false
this.setAsyncLoading(false, ['searchType'])
}
onsearchType(component, event){
this.setAsyncLoading(true, ['searchCriteria'])
this.components.searchBtn.disabled = true
this.contactsModel.get(component.value).then(this.buildForm.bind(this))
}
onSourceName(component, event){ this.changeAddBtnState() }
buildForm(query){
this.fieldNames = {}
this.fieldColors = {}
this.output('searchCriteria','')
const tabsMenu = ui.create(`
<menu eictab vertical class="searchCriteriaGroups" xsmall>
${query.group.map(queryGroup => `
<li>
${queryGroup.title}
</li>
`).join('\n')}
</menu>
`)
const tabsDiv = ui.create(`<div class="cols-2 left"></div>`)
this.outputs.searchCriteria.append(tabsDiv)
for(let queryGroup of query.group){
const groupEl = ui.create(`
<article eiccard="" class="tab-content searchCriteriaGroups">
<header>
<h1>${queryGroup.title}
${queryGroup.description ? `<span info="" class="icon-info" title="${queryGroup.description.replace('"','&quot;').replace(/\n/g, ' ')}"></span>`: ''}
</h1>
</header>
<section>
</section>
</article>
`)
const sectionEl = groupEl.querySelector('section')
let colIdx = 0
for(let field of queryGroup.field){
this.fieldNames[field.id] = field.title
this.fieldColors[field.id] = {
severity: this.baseColors[colIdx % this.baseColors.length],
brightness: 100+(20*Math.floor(colIdx/this.baseColors.length))
}
colIdx++
const el = ui.create(`
<div class="cols-2 field">
<div class="cols-2 field-label">
<label title="${field.description ? field.description.replace('"','&quot;'):''}">${field.title}</label>
${field.description ? `<span info="" class="icon-info" title="${field.description.replace('"','&quot;').replace(/\n/g, ' ')}"></span>` : ''}
</div>
</div>
`)
if(!field.type) field.type='text'
switch(field.type){
case 'list':
const selector = ui.create(`<select eicselect multiple name="${field.id}" lookup></select>`)
for(let value of field.value){
selector.append(ui.create(
`<option value="${value}">${value}</option>`
))
}
el.append(selector)
break
case 'date':
el.append(ui.create(`<input eicinput type="date" name="${field.id}">`))
break
default:
el.append(ui.create(`<select eicselect name="${field.id}" editable data-type="array" placeholder="One or several search values (hit enter after each)">`))
}
sectionEl.append(el)
}
tabsDiv.append(groupEl)
}
tabsDiv.prepend(tabsMenu)
const groupsMenu = new Tab(tabsMenu)
groupsMenu.addTabs(tabsDiv.querySelectorAll('menu.searchCriteriaGroups li'), tabsDiv.querySelectorAll('article.searchCriteriaGroups')); //Array.from(this.findAll('menu.searchCriteriaGroups li')).reverse()
const components = ui.eicfy(this.outputs.searchCriteria)
this.setAsyncLoading(false, ['searchCriteria'])
this.filtersForm = new Form(this.outputs.searchCriteria)
this.setupTriggers(components)
this.components.searchBtn.disabled = false
}
onFiltersChange(component, event){
this.currentQCId = this.components.searchType.value
const formValues = this.filtersForm.value
let fieldValues = Object.keys(formValues).map(k => ({id: k, value: formValues[k]}))
this.currentFilters = fieldValues.filter(item => item.value.length>0)
//this.setAsyncLoading(true, ['searchOutput'])
this.components.searchBtn.loading = true
this.contactsModel.getSample(this.currentQCId, this.currentFilters).then(
this.fillSearchOutput.bind(this),
() => this.components.searchBtn.loading = false
//this.setAsyncLoading(false, ['searchOutput'])
)
}
fillSearchOutput(results){
this.components.searchBtn.loading = false
// this.setAsyncLoading(false, ['searchOutput'])
this.output('searchOutput', `
<alert eicalert small muted info>Total recipients: <b>${results.nbRows}</b></alert>
<article eiccard collapsable>
<header eicalert small muted info><h1>Quick Probe</h1></header>
<section>
Email (or part of it)
<input eicinput="" type="search" name="emailProbe" data-ref="probeSearch"/>
<article eiccard class="probe-results"></article>
</section>
</article>
<article eiccard collapsable>
<header eicalert small muted info><h1>Results statistics</h1></header>
<section>
<ul data-output="countersList"></ul>
</section>
</article>
`)
this.outputs.searchOutput.querySelectorAll('[eiccard]').forEach(el => { new Card(el) })
this.outputs.searchOutput.querySelectorAll('[eicbutton]').forEach(el => { new Button(el) })
const probeSearch = new InputSearch(this.outputs.searchOutput.querySelector('[eicinput][type="search"]'))
probeSearch.onQuery = this.onProbe.bind(this,probeSearch)
this.fillCountersLegend(results.counters)
this.currentNbResults = results.nbRows
this.changeAddBtnState()
}
changeAddBtnState(){
if((this.currentNbResults>0) && (this.components.sourceName.value.length>3)) this.actions.find(o => o.role == 'add').button.disabled = false
else this.actions.find(o => o.role == 'add').button.disabled = true
}
onRefreshSample(component){
this.components.sampleRefreshBtn.loading = true
this.contactsModel.getSample(this.currentQCId, this.currentFilters).then(
(results) => {
const el = this.outputs.searchOutput.querySelector('.sample-list')
el.innerHTML = results.rows.map(row => ('<li>'+row.email+'</li>')).join('')
this.components.sampleRefreshBtn.loading = false
},
() => this.components.sampleRefreshBtn.loading = false
)
}
onProbe(component){
component.loading = true
this.setAsyncLoading(false, ['quickprobe'])
this.outputs.searchOutput.querySelector('article.probe-results').innerHTML = ''
this.contactsModel.probe(this.currentQCId, this.currentFilters, component.value).then((payload) =>{
this.fillProbeResults(payload)
component.loading = false
},
() => { component.loading = false }
)
}
fillProbeResults(results){
this.lastProbeResults = results
this.lastProbeIndex = 0
if(results.probeMatchesNumber==0){
this.outputs.searchOutput.querySelector('article.probe-results').innerHTML = `<header>No email found containing that text.</header>`
return
}
this.showProbeResult()
}
showProbeResult(){
this.outputs.searchOutput.querySelector('article.probe-results').innerHTML = ''
this.outputs.searchOutput.querySelector('article.probe-results').append(ui.create(`
<header>
<div>
<span>Found ${this.lastProbeResults.probeMatchesNumber} matching</span>
<span xsmall data-output="probeIndex"></span>
</div>
<span class="next-prev"></span>
</header>
`))
this.outputs.searchOutput.querySelector('article.probe-results').append(ui.create(`<section></section>`))
this.probeMaxIndex = this.lastProbeResults.probeMatchesNumber<20 ? this.lastProbeResults.probeMatchesNumber-1 : 19
this.components.prevProbeShow = new Button(null, {
label:'&lt;',
size: 'xxsmall',
onclick: () => {
this.lastProbeIndex = (this.lastProbeIndex>0) ? this.lastProbeIndex-1 : this.probeMaxIndex
this.showOneProbeMatch()
}
});
this.outputs.searchOutput.querySelector('.next-prev').append(this.components.prevProbeShow.el)
this.components.nextProbeShow = new Button(null, {
label:'&gt;',
size: 'xxsmall',
onclick: () => {
this.lastProbeIndex = (this.lastProbeIndex<(this.probeMaxIndex)) ? this.lastProbeIndex+1 : 0
this.showOneProbeMatch()
}
});
this.outputs.searchOutput.querySelector('.next-prev').append(this.components.nextProbeShow.el)
this.showOneProbeMatch()
}
showOneProbeMatch(){
const row = this.lastProbeResults.probeMatches[this.lastProbeIndex]
let html='<ul>'
for(const key of Object.keys(row)){
html += `<li><div class='cols-2'>
<b>${row[key].title}</b>
<i>${Array.isArray(row[key].value) ? row[key].value.join(', ') : row[key].value}</i>
</div></li>`
}
html += '</ul>'
this.outputs.searchOutput.querySelector('article.probe-results section').innerHTML = ''
this.outputs.searchOutput.querySelector('article.probe-results section').append(ui.create(html))
this.outputs.searchOutput.querySelector('[data-output="probeIndex"]').innerHTML = ` (Showing ${this.lastProbeIndex+1}/${this.probeMaxIndex+1})`
}
fillCountersLegend(counters){
const el = this.outputs.searchOutput.querySelector('[data-output="countersList"]')
el.innerHTML = ''
for(let counterName in counters) {
const liel = ui.create(`
<li>
<article eiccard collapsable collapsed>
<header><div>${counterName} <span eicchip xxsmall>${counters[counterName].nbUnique} unique</span></div></header>
<section>
<div class="cols-2 resultCounter">
<ul data-output="resultLegend"></ul>
<div data-output="resultPie"></div>
</div>
</section>
</alert>
</li>
`)
el.append(liel)
const ulel = liel.querySelector('[data-output="resultLegend"]')
let colIdx = 0
const pieData = []
for(let value in counters[counterName].counters) {
const severity = this.baseColors[colIdx % this.baseColors.length]
const brightness = 100-(20*Math.floor(colIdx/this.baseColors.length))
ulel.append(ui.create(`
<li><div class="cols-2">
<div class="color-dot" style="background: var(--eicui-base-color-${(severity!='secondary') ? severity : 'grey'}-100);filter:brightness(${brightness}%");></div>
<div>
<b>${value}:</b>
<i>${ (100*counters[counterName].counters[value]/counters[counterName].total).toFixed(1) }% (${counters[counterName].counters[value]})</i>
</div>
</div>
</li>
`))
pieData.push({
value: counters[counterName].counters[value],
severity: severity,
brightness: `${brightness}%`,
})
colIdx++
}
new PieTreeChart(liel.querySelector('[data-output="resultPie"]'), {
title: '',
height: 200,
data: pieData,
angularGap: 1,
innerRadius: 0,
outerRadius: 50
})
}
el.querySelectorAll('[eiccard]').forEach(el => { new Card(el) })
}
async add() {
this.actions.find(o => o.role == 'add').button.loading = true
this.currentQCId = this.components.searchType.value
const formValues = this.filtersForm.value
let fieldValues = Object.keys(formValues).map(k => ({id: k, value: formValues[k]}))
this.currentFilters = fieldValues.filter(item => item.value.length>0)
await this.mailingsModel.saveImport(this.mid, {
sourceName: this.components.sourceName.value,
sourceType: 'Marklogic',
data: null,
availableColumns: null,
query_id: this.currentQCId,
query_params: this.currentFilters,
})
this.commit(true)
}
onFailed() {
this.abort()
}
}
app.registerClass('MailingFetchDialog',MailingFetchDialog);
@@ -0,0 +1,23 @@
<style>
.import-dialog{ width: 50vw; }
</style>
<div class="import-dialog">
<section class="import">
<label>Drop or select your Excel file...</label>
<div eicfileupload class="browseBtn"></div>
<article eiccard eiccard>
<section>
<div class="cols-2 switches">
Remove duplicates
<input eicinput type="toggler" name="removeDups" primary="" labelleft="no" labelright="yes" xsmall="" value="yes" aria-enabled="true" classOff="greyed" class="remove-dups"/>
</div>
</section>
</article>
<article eiccard eiccard collapsable collapsed hidden class="report">
<header><span large class="report-title"></span></header>
<section class="report-details">
</section>
</article>
</section>
</div>
@@ -0,0 +1,260 @@
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 = `
<ul>
<li>Column used for the emails: <span success>${result.emailColName}</span></li>
<li><span success>${(result.rejectedRows.length>0) ? result.rejectedRows.length : 'No'}</span> ${ (result.rejectedRows.length==1) ? 'row was' : 'rows where' } rejected because column ${result.emailColName} did not contain an email.</li>
<li> ${(this.removeDups.value=='yes') ?
`Removed <span success>${result.nbDups}</span> emails that ${result.nbDups.length>1 ? 'where duplicates' : 'was a duplicate'}`:
`<span danger >I detected ${result.nbDups} duplicate${result.nbDups.length>1 ? 's' : ''} !</span> (which I did not remove)`}
</li>
<li><span ${headersList ? 'success': 'warning'}>${headersList ? 'Potential headers row: ': 'None headers found.'}</span>${headersList}</li>
</ul>
`
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<colsCount.length; idx++){ if(typeof(colsCount[idx])!='number') colsCount[idx]=0 }
// find winning column
let emailCol = colsCount.reduce((acc,val, 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);
@@ -0,0 +1,10 @@
<style>
.mapping-dialog{ width: 50vw; }
</style>
<div class="mapping-dialog">
<label>Select a column for<span eicchip info xsmall>${token}</span></label>
<select eicselect name="column" small></select>
<div eicalert warning small data-output="warning"></div>
<div eicdatagrid class="sample-grid" footer="hidden"></div>
</div>
@@ -0,0 +1,129 @@
class MailingMappingDialog extends EICDialogContent {
actions = [
{
label: 'Cancel',
onclick: this.cancel.bind(this),
severity: 'secondary',
role: 'cancel'
},
{
label: 'Link',
onclick: this.link.bind(this),
severity: 'primary',
role: 'link',
disabled: false
}
]
DOMContentLoaded(options) {
this.model = options.model
this.token = options.token
this.sourceId = options.sourceId
this.currentMailing = options.currentMailing
this.oneMapping = this.currentMailing.mappings.find(item => item.sourceId==options.sourceId) || { mappings: {} }
this.oneImport = this.currentMailing.imports.find(item => item.sourceId == options.sourceId)
let value = this.oneMapping.mappings[this.token] ? this.oneMapping.mappings[this.token].value : null
let components = ui.eicfy(this.el)
this.warning = this.find('[data-output="warning"]')
ui.hide(this.warning)
this.selector = components.find(component => component.el.name == 'column')
for(let column of this.oneImport.availableColumns ) {
let opt = `<option value="${column.value}">Column ${XLSX.utils.encode_col(column.value)}`
if(column.hint) {
opt += ` (${(column.hint.length<51) ? column.hint : column.hint.slice(0,30)+'..…'})`
}
opt +='</option>'
this.selector.el.append(ui.create(opt))
}
this.sampleGrid = new DataGrid(this.find('.sample-grid'), {
headers: [ {label: 'Sample data'} ],
height: '20vh'
})
this.selector.value = value
this.selector.change(this.onColChange.bind(this));
this.onColChange()
this.btResample = new Button(null, {icon: 'icon-refresh', rounded: true, size: 'xsmall', hint: 'Resample'})
this.btResample.click = this.onColChange.bind(this)
this.sampleGrid.el.querySelector('header .cell.actions').append(this.btResample.el)
}
onColChange(event){
const size = Math.min(10, this.oneImport.dataCount)
const colIdx = this.selector.value
let warnings = []
this.sampleGrid.clear()
if(this.selector.value){
for(let[idx, row] of this._makeSample(this.oneImport.data).entries()) {
let value = String(row[colIdx] || '').trim() || `<span eicchip xsmall danger>Empty !</span>`
this.sampleGrid.addRow( idx, [ value ] )
}
if(this.oneImport.data.some(row => (row[colIdx].trim()==''))){
warnings.push('<div>Some value in this column are empty !</div>')
}
if(this.oneImport.availableColumns.find(c => c.value == colIdx).isEmail) {
warnings.push('<div>Column used as recipients email address !</div>')
}
let tokens = []
for(let token in this.oneMapping.mappings) {
if(this.oneMapping.mappings[token].value == colIdx && token != this.token) tokens.push(token)
}
if(tokens.length > 0) {
warnings.push(`<div>Column already mapped to ${tokens.join(', ')} !</div>`)
}
}
this.warning.innerHTML = ''
ui.hide(this.warning)
if(warnings.length > 0) {
ui.show(this.warning)
this.warning.innerHTML = warnings.join('')
}
}
_makeSample(arr){
let sample = []
let noDups = []
for(let i=0; i<Math.min(10, arr.length); i++){
let rowNb=Math.floor(Math.random() * arr.length)
if(noDups.includes(rowNb)){
i--
} else {
sample.push(arr[rowNb])
noDups.push(rowNb)
}
}
return(sample)
}
async link() {
const colIdx = parseInt(this.selector.value)
if(!Number.isNaN(colIdx)) {
this.actions.find(o => o.role == 'link').button.loading = true
let curmap = this.currentMailing.mappings.find(item => item.sourceId==this.sourceId )
curmap.mappings[this.token] = { value: colIdx, hint: this.oneImport.availableColumns[colIdx].hint }
await this.model.save(this.currentMailing)
this.commit(true)
} else {
this.commit(false)
}
}
}
app.registerClass('MailingMappingDialog',MailingMappingDialog);
@@ -0,0 +1,74 @@
<style>
.template-selection .cols-2.left{ grid-template-columns: 400px auto; }
.template-selection .file-browser{ height: 500px; }
.template-selection .preview { height: 75vh; }
.template-selection .file-browser .dataset {
min-width: 30vw;
max-width: 40vw;
height: 60vh;
}
.template-selection .file-browser [eicdatagrid] .row { grid-template-columns: 60px auto 80px; }
.template-selection .preview .empty:after {
background-color: var();
position: absolute;
top: 0;
left: 0;
}
.template-selection .preview .mail-subject {
padding: var(--eicui-base-spacing-s);
border: 1px solid var(--eicui-base-color-grey-10);
margin: var(--eicui-base-spacing-xs) 0 var(--eicui-base-spacing-m) 0;
}
.template-selection .preview .mail-content {
min-width: 40vw;
width: 1fr;
height: 50vh;
overflow: auto;
border: 1px solid var(--eicui-base-color-grey-10);
}
.template-selection .confirmation {
display: none;
}
.template-selection .file-browser [eicdatagrid] .dataset .row .cell:nth-child(2),
.template-selection .file-browser [eicdatagrid] .dataset .row .cell:nth-child(4) {
text-align: center;
}
.template-selection [eicdatagrid] .row.selected{
background-color: var(--eicui-base-color-info);
color: white;
}
.template-selection [eicdatagrid] .row.selected .ffs.icon-new { color:white }
.template-selection [data-output="templateWarning"]{ display:none; }
</style>
<div class="template-selection">
<div class="cols-2 left">
<article eiccard class="file-browser">
<header>
<h1>Templates</h1>
<h2 data-output="path"></h2>
</header>
<section>
<div eicdatagrid header="hidden" footer="hidden"></div>
</section>
</article>
<article eiccard class="preview">
<header>
<h1 data-output="template"></h1>
<div eicalert warning data-output="templateWarning"></div>
</header>
<section class="empty">
<label>Subject</label>
<div class="mail-subject"></div>
<label>Message</label>
<div class="mail-content html"></div>
</section>
</article>
</div>
<div class="confirmation">
<div class="cols-2 left">
<input eicinput type="checkbox" value="yes">
<span>Yes, replace previous content with this template</span>
</div>
</div>
</div>
@@ -0,0 +1,187 @@
/**
* Todos
* - complete change() mmethod when template selected
*/
class MailingTemplatePreviewDialog extends EICDialogContent {
actions = [
{
label: 'Cancel',
onclick: this.cancel.bind(this),
severity: 'secondary',
role: 'cancel'
},
{
label: 'Use',
onclick: this.change.bind(this),
severity: 'primary',
role: 'change',
disabled: true
}
]
constructor(...args) {
super(...args)
Object.assign(this, app.helpers.validators)
Object.assign(this, app.helpers.activeAttributes)
}
DOMContentLoaded(options) {
this.templateModel = options.templateModel
this.mailingModel = options.mailingModel
this.currentMailing = options.currentMailing
this.currentTemplateInfo = {...this.currentMailing.template}
this.templateStatus = options.status
const components = ui.eicfy(this.el)
this.setupTriggers(components)
this.setupRefs(components)
this.fileBrowser = new DataGrid(this.find('.file-browser [eicdatagrid]'), {
headers: [
{label: '', filter: null, sortable:false},
{label: 'name', filter: 'text', sortable:true},
],
})
this.fileBrowser.onRowClick = this.onPathChange.bind(this)
this.fileBrowser.loading = true
this.preview = new Card(this.find('.preview'))
this.shadowRoot = this.find('.mail-content.html').attachShadow({ mode: 'open' })
this.refreshFolder(true)
}
showPath(path){
this.currentPath = path
const dirs = path.split('/').filter(item=>item)
const pathChips = dirs.map((dir, idx) => {
const path = dirs.slice(0, idx+1).join('/')
return(`<span eicchip xxsmall info data-path="/${path}">${dir}</span>`)
}).join('')
this.output('path', pathChips)
}
fillFolder(templates) {
this.fileBrowser.clear()
this.fileBrowser.loading = false
this.showPath(this.templateModel.ffs.currentPath)
for(let folder of this.templateModel.ffs.getFolders()){
let row = this.fileBrowser.addRow('./'+folder.name,
[
`<span warning xsmall class="ffs icon-folder"></span>`,
folder.name,
])
row.dataset.type = 'folder'
}
for(let template of templates){
if(template.path == this.templateModel.ffs.currentPath){ // Important on 1st display whne we havent't CD yet
let row = this.fileBrowser.addRow(template.id, [
`<span info xsmall class="ffs icon-new"></span>`,
template.name.length>35 ? template.name.slice(0,35)+'...' : template.name
])
row.dataset.type = 'file'
}
}
this.fileBrowser.loading = false
if(this.currentTemplateInfo && this.currentTemplateInfo.id) this.onFileSelect(this.fileBrowser.getRowById(this.currentTemplateInfo.id))
}
async refreshFolder(pathToTemplate){
this.fileBrowser.clear()
this.fileBrowser.loading = true
let templates
if(pathToTemplate){ // called from DOMContentLoaded => position to current templat, otherwise, let user move around)
templates = await this.templateModel.search([this.currentTemplateInfo.path || '/'], this.templateStatus )
this.templateModel.ffs.changeDir(this.currentTemplateInfo.path || '/')
} else {
templates = await this.templateModel.search([this.templateModel.ffs.currentPath], this.templateStatus )
}
this.fillFolder(templates)
}
onPathChange(row){
if(row.dataset.type=='file') {
this.onFileSelect(row)
} else if(row.dataset.type=='folder') {
this.onFolderSelect(row.dataset.id)
}
}
async onFileSelect(row){
if(!row) return
this.preview.loading = true
this.fileBrowser.rows().forEach(el => el.classList.remove('selected'))
this.currentTemplateInfo = await this.templateModel.get(row.dataset.id)
if(this.currentTemplateInfo.id){
this.currentTokens = this.templateModel.getTokens(this.currentTemplateInfo)
row.classList.add('selected')
this.refreshTemplate(this.currentTemplateInfo)
}
this.preview.loading = false
}
onFolderSelect(path){
this.templateModel.ffs.changeDir(path)
this.refreshFolder(false)
}
refreshTemplate(templateInfo){
const filterText = (text) => {
let element = document.createElement('div')
element.textContent = text
text = element.innerHTML
text = text.length > 150 ? text.slice(0, 100) + '...' : text
return text
}
this.output('template', templateInfo.name.length>35 ? templateInfo.name.slice(0,35)+'...' : templateInfo.name)
this.outputs['template'].setAttribute('title', templateInfo.name)
let subject
if((!templateInfo.meta.subject) || (!templateInfo.meta.subject.trim())){
this.output('templateWarning', 'This template has no subject and cannot be used here !')
ui.show(this.outputs['templateWarning'])
subject = ''
this.actions.find(o => o.role == 'change').button.disabled = true
} else {
ui.hide(this.outputs['templateWarning'])
subject = filterText(templateInfo.meta.subject || '')
this.find('.mail-subject').innerHTML = `${subject.replace(this.validators.mustacheTokenRegex,'<span class="token" eicchip success>$1</span>')}`
this.actions.find(o => o.role == 'change').button.disabled = false
}
let html = atob(templateInfo.html).replace(this.validators.mustacheTokenRegex,'<span class="token" eicchip success>$1</span>')
this.shadowRoot.innerHTML = `
<style>
span.token[eicchip]{
border-radius: 60px !important;
background-color: var(--eicui-base-color-success-100);
color: var(--eicui-base-color-white);
font-size: 0.75rem;
padding: var(--eicui-base-spacing-2xs) var(--eicui-base-spacing-s);
min-height: var(--eicui-base-spacing-xl);
display: inline-grid;
grid-template-columns: ;
min-content min-content min-content;
min-height: 0;
align-items: center;
margin: 1px var(--eicui-base-spacing-2xs);
pointer-events: all;
white-space: nowrap;
position: relative;
text-shadow: none;
}
</style>
${html}`
}
async change() {
this.currentMailing.template = { id: this.currentTemplateInfo.id }
this.actions.find(o => o.role == 'change').button.loading = true
await this.mailingModel.save(this.currentMailing)
this.commit(true)
}
}
app.registerClass('MailingTemplatePreviewDialog',MailingTemplatePreviewDialog);
@@ -0,0 +1,16 @@
<div class="test-dialog">
<section class="test">
<article eiccard eiccard>
<header>Give a valid email address for the recipient of this test :</header>
<section class="test-email">
<input eicinput type="text" name="email" eicinput value="" class="mail-address">
</section>
</article>
<article eiccard eiccard class="tokens-form">
<header>Give values for variables :</header>
<section class="test-tokens">
</section>
</article>
</section>
</div>
@@ -0,0 +1,89 @@
class MailingTestDialog extends EICDialogContent {
actions = [
{
label: 'Cancel',
onclick: this.cancel.bind(this),
severity: 'secondary',
role: 'cancel',
},
{
label: 'Send test',
onclick: this.send.bind(this),
severity: 'primary',
role: 'send',
disabled: true
}
]
constructor(...args) {
super(...args)
Object.assign(this, app.helpers.validators)
}
DOMContentLoaded(options) {
this.model = options.model
let components = ui.eicfy(this.el)
this.form = new Form(this.find('.test-dialog'));
this.mailInput = components.find(item => item.el.classList.contains('mail-address'))
this.mailInput.el.addEventListener('keyup', this.checkEmail.bind(this))
this.mailInput.el.addEventListener('focus', this.checkEmail.bind(this))
this.mailInput.el.addEventListener('blur', this.checkEmail.bind(this))
this.mailInput.value = app.User.identity.email
this.tokensForm = components.find(item => item.el.classList.contains('tokens-form'))
if(options.tokens.length>0){
ui.show(this.tokensForm.el, 'flex')
this.fillTokenForm(options.tokens)
} else {
ui.hide(this.tokensForm.el)
}
if(this.validators.isValidEmail(this.mailInput.value)) this.actions[1].disabled = false
}
fillTokenForm(tokens){
let markup = `<ul>
<li>
<div class="cols-2">
<label>Token</label>
<label>Value</label>
</div>
</li>`
tokens.forEach(token => {
markup += `<li>
<div class="cols-2">
<div><span eicchip small success>${token}</span></div>
<input eicinput type="text" name="${token}" data-path="tokenValues" eicinput value="" class="token-value">
</div>
</li>
`
})
markup += `</ul>`
this.tokensForm.el.innerHTML = markup
}
checkEmail(event){
if(event) {
event.preventDefault()
event.stopPropagation()
}
if(this.validators.isValidEmail(this.mailInput.value)){
this.actions.find(o => o.role == 'send').button.disabled = false
} else {
this.actions.find(o => o.role == 'send').button.disabled = true
}
}
send() {
if(!this.validators.isValidEmail(this.mailInput.value)) return
let data = this.form.value
console.log(data)
this.commit(data)
}
onFailed() {
this.abort()
}
}
app.registerClass('MailingTestDialog',MailingTestDialog);