unclean SPARC
This commit is contained in:
@@ -0,0 +1,788 @@
|
||||
class TemplatesEditorView extends EICDomContent {
|
||||
|
||||
constructor(privileges) {
|
||||
super('/templates', privileges)
|
||||
Object.assign(this, app.helpers.activeAttributes)
|
||||
Object.assign(this, app.helpers.basicDialogs)
|
||||
}
|
||||
|
||||
DOMContentLoaded(options) {
|
||||
for (let model in options.models) this[model] = options.models[model]
|
||||
this.components = ui.eicfy(this.el)
|
||||
|
||||
this.templateId = options.id
|
||||
this.decodedHtml = null;
|
||||
this.decodedAltText = null;
|
||||
|
||||
(new Tab()).addTabs(this.findAll('menu li'), this.findAll('.template-field'));
|
||||
|
||||
this.setupRefs(this.components)
|
||||
|
||||
this.checkAccessAndLoadTemplate()
|
||||
}
|
||||
|
||||
async checkAccessAndLoadTemplate() {
|
||||
const tplEditorContainer = this.find('.tplEditor')
|
||||
|
||||
try {
|
||||
const result = await this.templates.getReadableFolders()
|
||||
this.authorizedPaths = result.authorizedPaths || []
|
||||
|
||||
if (this.authorizedPaths.length === 0) {
|
||||
tplEditorContainer.innerHTML = `
|
||||
<div style="padding: 2rem; text-align: center;">
|
||||
<h2 style="color:#b00020;">Access Denied</h2>
|
||||
<p>You are not authorized to access the template editor.</p>
|
||||
</div>
|
||||
`
|
||||
return
|
||||
}
|
||||
this.loadTemplate()
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error checking template privileges:", err)
|
||||
tplEditorContainer.innerHTML = `
|
||||
<div style="padding: 2rem; text-align: center;">
|
||||
<h2 style="color:#b00020;">Error</h2>
|
||||
<p>Unable to check permissions due to a system error.</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
async loadTemplate() {
|
||||
const fieldSubj = this.find('.template-field.subject .content')
|
||||
this.showLoading(fieldSubj)
|
||||
const tplEditorContainer = this.find('.tplEditor')
|
||||
this.gridHistory = new DataGrid(this.find('.tpl-history'), {
|
||||
headers: [{ label: 'Date', type: 'markup', sortable: true }],
|
||||
actions: []
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await this.templates.getReadableFolders()
|
||||
this.authorizedPaths = result.authorizedPaths || []
|
||||
|
||||
if (this.authorizedPaths.length === 0) {
|
||||
tplEditorContainer.innerHTML = `
|
||||
<div style="padding: 2rem; text-align: center;">
|
||||
<h2 style="color:#b00020;">Access Denied</h2>
|
||||
<p>You are not authorized to access the template editor.</p>
|
||||
</div>
|
||||
`
|
||||
return
|
||||
}
|
||||
|
||||
const currentTemplate = await this.templates.get(this.templateId)
|
||||
|
||||
if (!currentTemplate) {
|
||||
ui.growl.append('Template not found. Please select a valid template.')
|
||||
return
|
||||
}
|
||||
|
||||
const templatePath = currentTemplate.path || '/'
|
||||
const isAuthorized = this.authorizedPaths.some(authPath => templatePath.startsWith(authPath))
|
||||
|
||||
if (!isAuthorized) {
|
||||
tplEditorContainer.innerHTML = `
|
||||
<div style="padding: 2rem; text-align: center;">
|
||||
<h2 style="color:#b00020;">Access Denied</h2>
|
||||
<p>You do not have permission to access this template.</p>
|
||||
</div>
|
||||
`
|
||||
return
|
||||
}
|
||||
|
||||
this.editedTemplate = new TemplatesModel(currentTemplate)
|
||||
const templateStatus = currentTemplate.status
|
||||
|
||||
if (templateStatus === 'submitted' || templateStatus === 'prod') {
|
||||
const tplFields = this.findAll('.template-field, .tpl-inputMeta')
|
||||
tplFields.forEach(field => {
|
||||
if (field.tagName === 'INPUT' || field.tagName === 'TEXTAREA') {
|
||||
field.disabled = true
|
||||
} else {
|
||||
field.setAttribute('contenteditable', 'false')
|
||||
}
|
||||
field.classList.add('disabled')
|
||||
})
|
||||
}
|
||||
|
||||
if (this.templates.hasPrivilege('edit')) {
|
||||
this.toggleButton('onTestSend', ['draft', 'submitted'].includes(templateStatus))
|
||||
this.toggleButton('saveHtml', ['draft', 'created'].includes(templateStatus))
|
||||
this.toggleButton('onRequestPub', templateStatus === 'draft')
|
||||
this.toggleButton('onUploadImage', ['draft', 'created'].includes(templateStatus))
|
||||
this.toggleButton('onPathSearch', ['draft', 'created'].includes(templateStatus))
|
||||
}
|
||||
|
||||
if (this.templates.hasPrivilege('approved')) {
|
||||
this.toggleButton('onApproved', templateStatus === 'submitted')
|
||||
this.toggleButton('onRejected', templateStatus === 'submitted')
|
||||
}
|
||||
|
||||
this.currentPath = templatePath
|
||||
this.find('.tplPath').textContent = this.currentPath
|
||||
this.decodeHtmlBin(currentTemplate.html, currentTemplate.meta)
|
||||
this.find('.tplName').value = currentTemplate.name || ' '
|
||||
|
||||
const currentDate = new Date()
|
||||
const oneYearLater = new Date()
|
||||
oneYearLater.setFullYear(currentDate.getFullYear() + 1)
|
||||
|
||||
const startDate = currentTemplate.startDate ? new Date(currentTemplate.startDate) : currentDate
|
||||
const endDate = currentTemplate.endDate ? new Date(currentTemplate.endDate) : oneYearLater
|
||||
|
||||
this.find('.tpl-formattedDateStart').value = this.formatDateToDDMMYYYYHHMM(startDate)
|
||||
this.find('.tpl-formattedDateEnd').value = this.formatDateToDDMMYYYYHHMM(endDate)
|
||||
|
||||
const headerContent = this.extractSection(this.decodedHtml, 'header')
|
||||
const mainContent = this.extractSection(this.decodedHtml, 'main')
|
||||
const footerContent = this.extractSection(this.decodedHtml, 'footer')
|
||||
|
||||
this.tplSubj = this.find('.template-field.subject')
|
||||
this.tplHeader = this.find('.template-field.header')
|
||||
this.tplMain = this.find('.template-field.main')
|
||||
this.tplFooter = this.find('.template-field.footer')
|
||||
this.tplAltText = this.find('.template-field.altText')
|
||||
|
||||
this.tplSubj.innerHTML = this.replaceMustaches(currentTemplate.meta.subject || ' ')
|
||||
this.tplHeader.innerHTML = this.replaceMustaches(headerContent || ' ')
|
||||
this.tplMain.innerHTML = this.replaceMustaches(mainContent || ' ')
|
||||
this.tplFooter.innerHTML = this.replaceMustaches(footerContent || ' ')
|
||||
this.tplAltText.innerText = this.decodedAltText || ' '
|
||||
|
||||
this.updatePreview()
|
||||
|
||||
const tplEditor = this.find('.tplEditor')
|
||||
const images = await this.templates.getImages()
|
||||
const tokens = await this.templates.getTechDDTokens(currentTemplate)
|
||||
const tId = this.templateId
|
||||
|
||||
this.htmlEditor = new HtmlEditor(tplEditor, this, {
|
||||
images,
|
||||
tokens,
|
||||
templateStatus,
|
||||
tId,
|
||||
externalHandlers: {
|
||||
onTestSend: this.onTestSend.bind(this),
|
||||
onUploadImage: this.onUploadImage.bind(this, templateStatus),
|
||||
onRequestPub: async () => {
|
||||
if (this.isDialogOpen) return
|
||||
this.isDialogOpen = true
|
||||
|
||||
await this.confirmDialog({
|
||||
title: 'Request review ?',
|
||||
message: `You're about to request a review for this template.<br>
|
||||
During the review process, the template will no longer be editable.
|
||||
<div class="prompts">
|
||||
<label small>Remarks or comments for the reviewer:</label>
|
||||
<textarea name="comments" eictextarea maxlen="500" cols="50"/></textarea>
|
||||
</div>`,
|
||||
promptsClass: 'prompts',
|
||||
okLabel: 'Request review',
|
||||
severity: 'warning',
|
||||
okSeverity: 'primary',
|
||||
okPromise: async (data) => {
|
||||
try {
|
||||
await this.saveHtml('submitted', data.comments)
|
||||
await this.loadTemplate()
|
||||
return true
|
||||
} finally {
|
||||
this.isDialogOpen = false
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
this.isDialogOpen = false
|
||||
})
|
||||
},
|
||||
onApproved: async () => {
|
||||
if (this.isDialogOpen) return
|
||||
this.isDialogOpen = true
|
||||
|
||||
await this.confirmDialog({
|
||||
title: 'Template approval ?',
|
||||
message: `Approve this template ?<br>
|
||||
Remarks or comments for the reviewer:
|
||||
<div class="prompts">
|
||||
<textarea name="comments" eictextarea maxlen="500" cols="50"/></textarea>
|
||||
</div>`,
|
||||
promptsClass: 'prompts',
|
||||
okLabel: 'Approve',
|
||||
severity: 'warning',
|
||||
okSeverity: 'primary',
|
||||
okPromise: async (data) => {
|
||||
try {
|
||||
await this.saveHtml('prod', data.comments)
|
||||
await this.loadTemplate()
|
||||
return true
|
||||
} finally {
|
||||
this.isDialogOpen = false
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
this.isDialogOpen = false
|
||||
})
|
||||
},
|
||||
onRejected: async () => {
|
||||
if (this.isDialogOpen) return
|
||||
this.isDialogOpen = true
|
||||
|
||||
await this.confirmDialog({
|
||||
title: 'Template rejection ?',
|
||||
message: `FeedBack<br>
|
||||
<div class="prompts">
|
||||
<textarea name="comments" eictextarea maxlen="500" cols="50"/></textarea>
|
||||
</div>
|
||||
`,
|
||||
promptsClass: 'prompts',
|
||||
okLabel: 'Reject',
|
||||
severity: 'warning',
|
||||
okSeverity: 'primary',
|
||||
okPromise: async (data) => {
|
||||
try {
|
||||
await this.saveHtml('draft', data.comments)
|
||||
this.loadTemplate()
|
||||
return true
|
||||
} finally {
|
||||
this.isDialogOpen = false
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
this.isDialogOpen = false
|
||||
})
|
||||
},
|
||||
onPathSearch: this.onPathSearch.bind(this),
|
||||
onStartDateClicked: this.onStartDateClicked.bind(this),
|
||||
onEndDateClicked: this.onEndDateClicked.bind(this),
|
||||
saveHtml: this.saveHtml.bind(this, templateStatus)
|
||||
}
|
||||
})
|
||||
|
||||
// Re-attach tab listeners after reload
|
||||
if (typeof Tab === 'function') {
|
||||
(new Tab()).addTabs(this.findAll('menu li'), this.findAll('.template-field'))
|
||||
}
|
||||
|
||||
// Restore the last active tab if available (with a short delay to ensure listeners are attached)
|
||||
if (typeof this._lastActiveTabIndex === 'number' && this._lastActiveTabIndex >= 0) {
|
||||
const tabs = Array.from(this.findAll('menu li'))
|
||||
if (tabs[this._lastActiveTabIndex]) {
|
||||
setTimeout(() => {
|
||||
tabs[this._lastActiveTabIndex].click()
|
||||
// Force toolbar rebuild for the restored tab
|
||||
const fieldTypes = ['subject', 'header', 'main', 'footer', 'altText', 'preview']
|
||||
const fieldType = fieldTypes[this._lastActiveTabIndex] || 'subject'
|
||||
if (this.htmlEditor && typeof this.htmlEditor.buildToolButtons === 'function') {
|
||||
this.htmlEditor.buildToolButtons(fieldType)
|
||||
}
|
||||
}, 150)
|
||||
}
|
||||
}
|
||||
|
||||
this.gridHistory.clear()
|
||||
const history = currentTemplate.statusHistory.sort((a, b) => new Date(b.dateTime) - new Date(a.dateTime))
|
||||
|
||||
history.forEach(item => {
|
||||
const statusColor = {
|
||||
draft: 'info',
|
||||
created: 'secondary',
|
||||
submitted: 'warning',
|
||||
archived: 'primary',
|
||||
prod: 'success'
|
||||
}[item.value] || 'danger'
|
||||
|
||||
const oldestDate = new Date(item.dateTime).toLocaleDateString('fr-FR')
|
||||
const fullName = `<span xsmall class="tpl-fullName">By ${item.changedBy.firstname} ${item.changedBy.lastname}</span>`
|
||||
const status = `<span class="tpl-status" ${statusColor}>${item.value}</span>`
|
||||
const comment = item.changedBy.comment?.trim()
|
||||
? `<span xsmall info class="tpl-comment"><u>comment:</u> ${item.changedBy.comment}</span>`
|
||||
: ''
|
||||
|
||||
const details = `
|
||||
<div eiccard collapsable collapsed>
|
||||
<span xsmall class="tpl-details">${oldestDate} - ${status}</span>
|
||||
<div class="collapsible-content">
|
||||
${fullName}
|
||||
${comment}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
const row = this.gridHistory.addRow(item.id, [details])
|
||||
row.addEventListener('click', () => {
|
||||
const content = row.querySelector('.collapsible-content')
|
||||
content.style.display = content.style.display === 'none' || content.style.display === '' ? 'block' : 'none'
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error loading template:", error)
|
||||
tplEditorContainer.innerHTML = `
|
||||
<div style="padding: 2rem; text-align: center;">
|
||||
<h2 style="color:#b00020;">Error</h2>
|
||||
<p>Unable to load the template due to a system error.</p>
|
||||
</div>
|
||||
`
|
||||
} finally {
|
||||
this.hideLoading(fieldSubj)
|
||||
}
|
||||
const toggleBtn = document.querySelector('.toggle-history')
|
||||
const historyPanel = document.querySelector('.tpl-history')
|
||||
|
||||
toggleBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
historyPanel.classList.toggle('collapsed')
|
||||
|
||||
if (historyPanel.classList.contains('collapsed')) {
|
||||
toggleBtn.classList.remove('icon-close')
|
||||
toggleBtn.classList.add('icon-history')
|
||||
} else {
|
||||
toggleBtn.classList.remove('icon-history')
|
||||
toggleBtn.classList.add('icon-close')
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!historyPanel.contains(e.target) && !toggleBtn.contains(e.target)) {
|
||||
historyPanel.classList.add('collapsed')
|
||||
toggleBtn.classList.remove('icon-close')
|
||||
toggleBtn.classList.add('icon-history')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updatePreview() {
|
||||
if (!this.tplHeader || !this.tplMain || !this.tplFooter) return
|
||||
|
||||
const previewContent = `
|
||||
<div class="tpl-preview-block">${this.tplHeader.innerHTML}</div>
|
||||
<div class="tpl-preview-block">${this.tplMain.innerHTML}</div>
|
||||
<div class="tpl-preview-block">${this.tplFooter.innerHTML}</div>
|
||||
`
|
||||
this.find('.template-field.preview .content').innerHTML = previewContent
|
||||
}
|
||||
|
||||
onStartDateClicked() {
|
||||
const startDateInput = this.find('.tplStartDate');
|
||||
startDateInput.showPicker();
|
||||
this.syncDateInputs(this.find('.tpl-formattedDateStart'), this.find('.tplStartDate'));
|
||||
}
|
||||
|
||||
onEndDateClicked() {
|
||||
const endDateInput = this.find('.tplEndDate');
|
||||
endDateInput.showPicker();
|
||||
this.syncDateInputs(this.find('.tpl-formattedDateEnd'), this.find('.tplEndDate'));
|
||||
}
|
||||
|
||||
syncDateInputs(textInput, dateInput) {
|
||||
dateInput.addEventListener('input', () => {
|
||||
textInput.value = this.formatDateToDDMMYYYYHHMM(dateInput.value);
|
||||
});
|
||||
|
||||
textInput.addEventListener('click', () => {
|
||||
dateInput.showPicker();
|
||||
});
|
||||
}
|
||||
|
||||
formatDateToDDMMYYYYHHMM(dateString) {
|
||||
const date = new Date(dateString);
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false // 24h format
|
||||
});
|
||||
const formattedDate = dateFormatter.format(date);
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
reformatDateToISO(dateInput) {
|
||||
let dateObj;
|
||||
|
||||
if (typeof dateInput === 'string') {
|
||||
const parts = dateInput.split(/[\s/:]/);
|
||||
if (parts.length >= 5) {
|
||||
const [day, month, year, hours, minutes] = parts;
|
||||
dateObj = new Date(`${year}-${month}-${day}T${hours}:${minutes}:00`);
|
||||
} else {
|
||||
console.error('Invalid date format:', dateInput);
|
||||
return '';
|
||||
}
|
||||
} else if (dateInput instanceof Date) {
|
||||
dateObj = dateInput;
|
||||
} else {
|
||||
console.error('Expected a Date object or valid date string but received:', dateInput);
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isNaN(dateObj.getTime())) {
|
||||
console.error('Invalid Date object:', dateObj);
|
||||
return '';
|
||||
}
|
||||
|
||||
const day = String(dateObj.getDate()).padStart(2, '0')
|
||||
const month = String(dateObj.getMonth() + 1).padStart(2, '0')
|
||||
const year = dateObj.getFullYear()
|
||||
|
||||
const hours = String(dateObj.getHours()).padStart(2, '0')
|
||||
const minutes = String(dateObj.getMinutes()).padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}:00.000Z`
|
||||
}
|
||||
|
||||
|
||||
toggleButton(trigger, isVisible) {
|
||||
const button = this.components.find(c => c.el.dataset.trigger === trigger)
|
||||
if (button) {
|
||||
requestAnimationFrame(() => {
|
||||
button.el.classList.toggle('hidden', !isVisible)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async onTestSend() {
|
||||
const button = this.find('.tpl-sendTest')
|
||||
|
||||
// Deactivate button and add spinner
|
||||
this.showLoading(button)
|
||||
try {
|
||||
let result = await this.openDialog(
|
||||
await this.loadContent(
|
||||
'comms/templates/dialogs/TemplatesMailTestDialog',
|
||||
{ title: `Send a test mail...` }
|
||||
)
|
||||
)
|
||||
if (result) {
|
||||
await this.templates.testMail(this.templateId, result.email)
|
||||
ui.growl.append('Test mail sent successfully', 'success')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error on dialog UploadImage loading :", error)
|
||||
ui.growl.append('Error sending test mail', 'error')
|
||||
} finally {
|
||||
// Reactivate button
|
||||
this.hideLoading(button, '<span>Test mail</span>')
|
||||
}
|
||||
}
|
||||
|
||||
async onUploadImage(templateStatus) {
|
||||
const button = this.find('.he-uplImg');
|
||||
const tabs = Array.from(this.findAll('menu li'))
|
||||
const activeTabIndex = tabs.findIndex(tab => tab.classList.contains('active'))
|
||||
this._lastActiveTabIndex = activeTabIndex
|
||||
|
||||
this.showLoading(button)
|
||||
|
||||
let dialogInstance = null
|
||||
|
||||
try {
|
||||
dialogInstance = await this.openDialog(
|
||||
await this.loadContent(
|
||||
'comms/templates/dialogs/TemplatesUplImgDialog',
|
||||
{ title: 'Upload your own image...' }
|
||||
)
|
||||
)
|
||||
|
||||
if (!dialogInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
if (dialogInstance.showDialogSpinner) dialogInstance.showDialogSpinner(true)
|
||||
|
||||
const response = await this.templates.saveImage(dialogInstance)
|
||||
|
||||
if (response?.success) {
|
||||
ui.growl.append('Image saved successfully', 'success')
|
||||
await this.saveHtml(templateStatus)
|
||||
} else {
|
||||
ui.growl.append('Error saving image', 'error')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error on dialog UploadImage loading:", error)
|
||||
|
||||
} finally {
|
||||
// Always hide the spinner and close dialog if needed
|
||||
if (dialogInstance?.showDialogSpinner) {
|
||||
dialogInstance.showDialogSpinner(false)
|
||||
}
|
||||
if (dialogInstance?.cancel) {
|
||||
dialogInstance.cancel()
|
||||
}
|
||||
this.hideLoading(button, '<span>Upload image</span>')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Decod html binary64 fron DB
|
||||
decodeHtmlBin(html, meta) {
|
||||
this.decodedHtml = html ? this.decodeBase64ToUTF8(html) : '';
|
||||
this.decodedAltText = meta.altText ? this.decodeBase64ToUTF8(meta.altText) : ''
|
||||
}
|
||||
decodeBase64ToUTF8(base64Str) {
|
||||
const binaryString = atob(base64Str)
|
||||
const uint8Array = new Uint8Array(binaryString.length)
|
||||
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
uint8Array[i] = binaryString.charCodeAt(i)
|
||||
}
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
return decoder.decode(uint8Array)
|
||||
}
|
||||
|
||||
async onPathSearch() {
|
||||
const button = this.find('.tpl-btn-path')
|
||||
this.showLoading(button)
|
||||
try {
|
||||
let result = await this.templates.getReadableFolders()
|
||||
|
||||
if (!result || typeof result !== 'object') {
|
||||
console.error("getReadableFolders returned invalid result", result)
|
||||
return
|
||||
}
|
||||
|
||||
let { templates, authorizedPaths, foldersWithFiles, rights } = result
|
||||
|
||||
if (!Array.isArray(authorizedPaths)) authorizedPaths = []
|
||||
if (!authorizedPaths.includes('/templates')) authorizedPaths.unshift('/templates')
|
||||
this.templates.ffs.authorizedPaths = authorizedPaths
|
||||
this.templates.ffs.foldersWithFiles = foldersWithFiles || []
|
||||
this.templates.ffs.rights = rights || []
|
||||
|
||||
if (!templates?.templates) {
|
||||
templates = { templates }
|
||||
}
|
||||
|
||||
const fileList = Array.isArray(templates.templates) ? templates.templates : []
|
||||
this.templates.ffs.loadStructure(templates || {}, fileList)
|
||||
this.templates.ffs.currentPath = '/templates'
|
||||
this.templates.ffs.authorizedPaths = authorizedPaths
|
||||
this.currentPath = this.find('.tplPath').textContent
|
||||
|
||||
let dialogResult = await this.openDialog(
|
||||
await this.loadContent(
|
||||
'templates/Ffs/dialogs/FileBrowserDialog',
|
||||
{ title: `Select a path...` },
|
||||
{
|
||||
ffs: this.templates.ffs,
|
||||
editable: false,
|
||||
startPath: this.currentPath,
|
||||
canManage: true,
|
||||
templates: this.templates,
|
||||
context: 'templates'
|
||||
}
|
||||
)
|
||||
)
|
||||
if (dialogResult) {
|
||||
this.editedTemplate.path = dialogResult
|
||||
this.find('.tplPath').textContent = dialogResult
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error on dialog PathSearch loading :", error)
|
||||
} finally {
|
||||
// Reactivate button
|
||||
this.hideLoading(button)
|
||||
}
|
||||
}
|
||||
|
||||
extractSection(html, section) {
|
||||
const regex = new RegExp(`<${section}.*?>([\\s\\S]*?)<\\/${section}>`, 'i')
|
||||
const match = html.match(regex)
|
||||
const extractedText = match ? match[1].trim() : ''
|
||||
const normStr = this.normalizeText(extractedText)
|
||||
return normStr
|
||||
}
|
||||
//Normalize special char. from Word copy/paste
|
||||
normalizeText(str) {
|
||||
return str
|
||||
.replace(/[\u2018\u2019]/g, "'") //Replace ‘ ’ by '
|
||||
.replace(/[\u201C\u201D]/g, '"') // Replace “ ” by "
|
||||
.replace(/\u00A0/g, ' ') // Replace space
|
||||
}
|
||||
toBase64UTF8(str) {
|
||||
const encoder = new TextEncoder()
|
||||
const uint8Array = encoder.encode(str)
|
||||
let binaryString = ''
|
||||
uint8Array.forEach(byte => {
|
||||
binaryString += String.fromCharCode(byte)
|
||||
});
|
||||
return btoa(binaryString)
|
||||
}
|
||||
fromBase64UTF8(base64) {
|
||||
const binaryString = atob(base64)
|
||||
const uint8Array = new Uint8Array([...binaryString].map(char => char.charCodeAt(0)))
|
||||
const decoder = new TextDecoder()
|
||||
return decoder.decode(uint8Array)
|
||||
}
|
||||
|
||||
async saveHtml(tplStatus, comment = null) {
|
||||
const button = this.find('.tpl-save')
|
||||
// Deactivate button and add spinner
|
||||
this.showLoading(button)
|
||||
// Save the current active tab index before reload
|
||||
const tabs = Array.from(this.findAll('menu li'))
|
||||
const activeTabIndex = tabs.findIndex(tab => tab.classList.contains('active'))
|
||||
this._lastActiveTabIndex = activeTabIndex
|
||||
try {
|
||||
const newTplStatus = (tplStatus === 'created') ? 'draft' : tplStatus
|
||||
const tplName = this.find('.tplName').value
|
||||
const tplPath = this.find('.tplPath').textContent
|
||||
|
||||
const tplStartDate = this.reformatDateToISO(this.find('.tpl-formattedDateStart').value)
|
||||
const tplEndDate = this.reformatDateToISO(this.find('.tpl-formattedDateEnd').value)
|
||||
|
||||
const curYear = new Date().getFullYear()
|
||||
|
||||
const tplType = 'mail'
|
||||
|
||||
const path = (tplPath ? (tplPath.startsWith('/') ? tplPath : '/' + tplPath) : '/Common/' + curYear)
|
||||
|
||||
// keep all inputs content
|
||||
const tplSubject = this.cleanInvisibleChars(this.restoreMustaches(this.find('.template-field.subject').innerHTML))
|
||||
const headerContent = this.cleanInvisibleChars(this.restoreMustaches(this.find('.template-field.header').innerHTML))
|
||||
const mainContent = this.cleanInvisibleChars(this.restoreMustaches(this.find('.template-field.main').innerHTML))
|
||||
const footerContent = this.cleanInvisibleChars(this.restoreMustaches(this.find('.template-field.footer').innerHTML))
|
||||
const altTextContent = this.toBase64UTF8(this.find('.template-field.altText').innerText)
|
||||
|
||||
const newHtml = `<header>${headerContent}</header>
|
||||
<main>${mainContent}</main>
|
||||
<footer>${footerContent}</footer>`
|
||||
|
||||
this.updatePreview()
|
||||
|
||||
const refImgIds = this.extractImageIds(newHtml)
|
||||
|
||||
const data = {
|
||||
id: this.templateId,
|
||||
type: 'templateInfo',
|
||||
name: tplName,
|
||||
path: path,
|
||||
html: this.toBase64UTF8(newHtml),
|
||||
status: newTplStatus,
|
||||
meta: {
|
||||
subject: tplSubject,
|
||||
type: tplType,
|
||||
altText: altTextContent,
|
||||
},
|
||||
statusHistory: {
|
||||
changedBy: {
|
||||
euLoginId: '',
|
||||
mail: '',
|
||||
role: '',
|
||||
comment: comment || '',
|
||||
},
|
||||
dateTime: '',
|
||||
value: newTplStatus
|
||||
},
|
||||
startDate: tplStartDate,
|
||||
endDate: tplEndDate,
|
||||
imgRefs: refImgIds || '',
|
||||
comment: comment || '',
|
||||
}
|
||||
const response = await this.editedTemplate.save(data)
|
||||
|
||||
if (response.success) {
|
||||
ui.growl.append('Template saved successfully', 'success')
|
||||
} else {
|
||||
ui.growl.append('Error saving template', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving template :", error)
|
||||
} finally {
|
||||
// Reactivate button
|
||||
this.hideLoading(button, '<span>Save</span>')
|
||||
}
|
||||
}
|
||||
|
||||
cleanInvisibleChars(str) {
|
||||
return str
|
||||
.replace(/\u200B/g, '') // Unicode invisibles chars
|
||||
.replace(/​/gi, '') // HTML entity for zero-width space
|
||||
.normalize('NFC') // Normalisation Unicode
|
||||
.replace(/[\u200B-\u200D\uFEFF]/g, '') // zero-width & BOM
|
||||
.replace(/​/gi, '') // HTML entity
|
||||
|
||||
.replace(/[\u2018\u2019\u02BC]/g, "'") // smart apostrophes → '
|
||||
.replace(/[\u201C\u201D\u00AB\u00BB]/g, '"') // smart quotes → "
|
||||
.replace(/[\u2013\u2014\u2212]/g, '-') // dashes → -
|
||||
.replace(/\u2026/g, '...') // ellipsis → ...
|
||||
|
||||
.replace(/[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g, ' ') // weird spaces → normal space
|
||||
|
||||
.replace(/[\u0000-\u001F\u007F]/g, '') // control chars
|
||||
|
||||
.replace(/\s{2,}/g, ' ') // double spaces
|
||||
.replace(/[ \t]+\n/g, '\n') // strip trailing space before newlines
|
||||
.replace(/\n{3,}/g, '\n\n') // max 2 line breaks
|
||||
.trim()
|
||||
}
|
||||
|
||||
extractImageIds(htmlContent) {
|
||||
const tempElement = document.createElement('div')
|
||||
tempElement.innerHTML = htmlContent
|
||||
const imgElements = tempElement.querySelectorAll('img')
|
||||
const ids = [];
|
||||
imgElements.forEach(img => {
|
||||
if (img.id) {
|
||||
ids.push(img.id)
|
||||
}
|
||||
})
|
||||
return ids.join(',')
|
||||
}
|
||||
|
||||
async onDecision(status) {
|
||||
if (status === 'prod') {
|
||||
let result = await this.confirmDialog({
|
||||
title: 'APPROVE this template ?',
|
||||
message: `<p>This approval will be final.</p>
|
||||
<p>Are you sure?</p>`,
|
||||
okLabel: 'Approve',
|
||||
severity: 'warning',
|
||||
okPromise: () => this.saveHtml(status, '')
|
||||
})
|
||||
if (result) {
|
||||
ui.growl.append("Template approved !", 'success')
|
||||
} else {
|
||||
ui.growl.append("Error approval!", 'error')
|
||||
}
|
||||
|
||||
} else {
|
||||
let result = await this.openDialog(
|
||||
await this.loadContent(
|
||||
'comms/templates/dialogs/TemplatesDecisionDialog',
|
||||
{ title: `Comment your decision...` }
|
||||
)
|
||||
)
|
||||
if (result) {
|
||||
await this.saveHtml(status, result.comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async addImage(data) {
|
||||
const response = await this.editedTemplate.saveImage(data)
|
||||
|
||||
if (response.success) {
|
||||
ui.growl.append('Image saved successfully', 'success')
|
||||
this.loadTemplate(this.templateId)
|
||||
} else {
|
||||
ui.growl.append('Error saving image', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
//Manage tokens mustaches
|
||||
replaceMustaches(html) {
|
||||
return html.replace(/\{\{(.*?)\}\}/g, function (match, p1) {
|
||||
return `<span class="tpl-token" eicchip="" success="" data-original="${p1.trim()}">${p1.trim()}</span>`
|
||||
});
|
||||
}
|
||||
restoreMustaches(html) {
|
||||
return html.replace(/<span class="tpl-token"[^>]*data-original="(.*?)"[^>]*>.*?<\/span>/g, '{{$1}}')
|
||||
}
|
||||
}
|
||||
|
||||
app.registerClass('TemplatesEditorView', TemplatesEditorView);
|
||||
|
||||
Reference in New Issue
Block a user