class TemplatesManagerView extends EICDomContent { constructor(privileges) { super('/templates', privileges) Object.assign(this, app.helpers.basicDialogs) Object.assign(this, app.helpers.activeAttributes) Object.assign(this, app.helpers.validators) this.selectedTemplate = null this.decodedHtml = null this.decodedAltText = null } DOMContentLoaded(options) { for (let model in options.models) this[model] = options.models[model] this.components = ui.eicfy(this.el) this.gridTemplates = new DataGrid(this.find('.library [eicdatagrid]'), { headers: [ { label: 'Name', filter: 'text', sortable: true }, { label: 'Author', type: 'markup', filter: 'text', sortable: true }, { label: 'Delete', type: 'button' }, { label: 'Status', type: 'markup' }, { label: 'Actions', type: 'markup' }, ], actions: [] }) this.gridTemplates.onRowClick = (row, event) => { const target = event.target const isButton = target.closest('.template-actions button') if (!isButton) { this.onTemplateSelect(row) } } this.preview = new Card(this.find('.preview')) this.previewContent = this.find('.preview .html').attachShadow({ mode: 'open' }) this.setupTriggers(this.components) this.setupRefs(this.components) this.setupStatusButtons() this.components.searchCriteria.value = [app.User.identity.uuid] } async refreshTpl() { this.gridTemplates.loading = true try { const result = await this.templates.getReadableFolders() this.authorizedPaths = result.authorizedPaths || [] if (this.authorizedPaths.length === 0) { this.output('searchResults', `

Access Denied

You're not authorized to access the templates dashboard

`) return } const tpls = await this.templates.search( this.components.searchCriteria.value, this.getSelectedStatus() ) if (tpls?.message === 'NO_ACCESS') { this.output('searchResults', `

Access Denied

You're not authorized to view templates

`) return } this.fillTemplates(tpls) } catch (error) { this.output('searchResults', `

Unexpected Error

An error occurred while loading templates. Please try again later

`) console.error(error) } finally { this.gridTemplates.loading = false } } onSearchChange(component, event) { event.stopPropagation() event.preventDefault() this.refreshTpl() } setupStatusButtons() { this.statusButtons = {} for (let status in this.templates.getStatusLabel()) { this.statusButtons[status] = new Button(null, { rounded: true, size: 'xsmall', severity: 'info', label: `${this.templates.getStatusLabel()[status]}` }) this.statusButtons[status].click = this.onStatusChange.bind(this, status) this.find('.tpl-filters').append(this.statusButtons[status].el) } } onStatusChange(status, event) { event.stopPropagation() event.preventDefault() this.statusButtons[status].severity = this.statusButtons[status].severity === 'info' ? 'secondary' : 'info' this.refreshTpl() } getSelectedStatus() { let selected = Object.keys(this.statusButtons).filter(status => this.statusButtons[status].severity === 'info' ) return selected.length > 0 ? selected : Object.keys(this.statusButtons) } async onPathBrowse() { const button = this.find('.tpl-btn-path') this.showLoading(button) let result = await this.templates.getReadableFolders() let { templates, authorizedPaths } = result if (!Array.isArray(authorizedPaths)) { authorizedPaths = [] } if (!authorizedPaths.includes('/templates')) authorizedPaths.unshift('/templates') this.templates.ffs.authorizedPaths = authorizedPaths if (!templates.templates) { templates = { templates } } this.templates.ffs.loadStructure(templates || {}, []) this.templates.ffs.currentPath = '/templates' this.templates.ffs.authorizedPaths = authorizedPaths let dialogResult = await this.openDialog( await this.loadContent( 'templates/Ffs/dialogs/FileBrowserDialog', { title: `Select a path...` }, { ffs: this.templates.ffs, templates: this.templates.templates, editable: false, canManage: false, startPath: '/templates', } ) ) if (dialogResult) { const criteria = this.components.searchCriteria.value.filter(item => (!item.startsWith('/'))) this.components.searchCriteria.value = [...criteria, dialogResult] } this.hideLoading(button) } setupStatusButtons() { this.statusButtons = {} this.statusCounters = {} for (let status in this.templates.getStatusLabel()) { const label = this.templates.getStatusLabel()[status] // Crée le bouton avec label const button = new Button(null, { rounded: true, size: 'xsmall', severity: 'info', label: `${label}` }) // Create the badge for the count const badge = new Badge(null, { size: 'xxsmall' }) badge.value = '0' button.el.append(badge.el) // Store references to the button and badge this.statusButtons[status] = button this.statusCounters[status] = badge this.statusButtons[status].click = this.onStatusChange.bind(this, status) this.find('.tpl-filters').append(button.el) } } updateStatusCounts(counts) { for (let status in this.templates.getStatusLabel()) { const count = counts[status] || 0 if (this.statusCounters[status]) { this.statusCounters[status].value = count.toString() } } } fillTemplates(list) { this.gridTemplates.clear() this.gridTemplates.loading = true let statusCounts = {} for (let item of list) { statusCounts[item.status] = (statusCounts[item.status] || 0) + 1 let statusBadge switch (item.status) { case 'draft': statusBadge = 'info'; break case 'created': statusBadge = 'secondary'; break case 'submitted': statusBadge = 'warning'; break case 'archived': statusBadge = 'primary'; break case 'prod': statusBadge = 'success'; break default: statusBadge = 'danger' } let changeBy = '' let oldest = item.statusHistory.find(() => true) if (oldest) changeBy = oldest.changedBy let oldestDate = new Date(oldest.dateTime).toLocaleDateString('fr-FR') let fullName = changeBy.firstname + ' ' + changeBy.lastname let statusLabel = item.status === 'prod' ? 'approved' : item.status const dirs = item.path.split('/').filter(item => item) const pathChips = dirs.map((dir, idx) => { const path = dirs.slice(0, idx + 1).join('/') return (`${dir}`) }).join('') let tplInfos = `` let tplTitle = `
${item.name}
last action: ${oldestDate}
by: ${fullName}
` let deleteB = '' if (item.status === 'created' || item.status === 'draft') { deleteB = `
` } let tplActions = `
${ item.status !== 'prod' ? `` : '' }
` let row = this.gridTemplates.addRow(item.id, [pathChips, tplTitle, tplInfos, deleteB, tplActions]) row.dataset.type = 'file' let previewButton = row.querySelector('[data-trigger="onTemplateSelect"]') if (previewButton) { previewButton.addEventListener('click', (event) => { event.stopPropagation() this.onTemplateSelect(row) }) } let editButton = row.querySelector('[data-trigger="onTemplateEdit"]') if (editButton) { editButton.addEventListener('click', (event) => { event.stopPropagation() this.onTemplateEdit(item.id) }) } let cloneButton = row.querySelector('[data-trigger="onTemplateClone"]') if (cloneButton) { cloneButton.addEventListener('click', (event) => { event.stopPropagation() this.onTemplateClone(item.id, event.currentTarget) }) } let deleteButton = row.querySelector('[data-trigger="onDeleteTemplate"]') if (deleteButton) { deleteButton.addEventListener('click', (event) => { event.stopPropagation() this.onDeleteTemplate(item.id, item.name) }) } } this.updateStatusCounts(statusCounts) this.gridTemplates.loading = false } async onTemplateSelect(row) { const currentId = row.dataset.id if (row.dataset.type == 'file') { this.find('.preview').removeAttribute('disabled') this.preview.loading = true let data = await this.templates.get(currentId) if (data) { let rawHtml = data.html || '' let html = this.replaceMustaches(this.decodeBase64Utf8(rawHtml)) html = html.replace(/[\u200B-\u200D\uFEFF]/g, '') html = html .replace(/\u200B/g, '') .replace(/​/gi, '') this.previewContent.innerHTML = `\n${html}` const images = this.previewContent.querySelectorAll('img') images.forEach((img) => { img.style.maxWidth = '100%' img.style.height = 'auto' }) this.findAll('.templatesList .row.selected').forEach(r => r.classList.remove('selected')) row.classList.add('selected') this.find('.preview').removeAttribute('disabled') this.selectedTemplate = data row.scrollIntoView({ behavior: 'smooth', block: 'start' }) } else { this.find('.preview').removeAttribute('disabled') ui.growl.append('Template not found', 'danger') } this.preview.loading = false } else if (row.dataset.type == 'folder') this.onFolderSelect(currentId) } async onDeleteTemplate(tplId, tplName) { if (!this.templates.hasPrivilege('delete')) { ui.growl.append("You do not have permissions to delete this template.", 'danger') return } let result = await this.confirmDialog({ title: 'DELETE template ?', message: `

You are about to permanently delete the template "${tplName}".

Are you sure?

`, okLabel: 'Delete', severity: 'danger', okPromise: () => this.templates.delete(tplId) }) if (result) { ui.growl.append("Template deleted !", 'success') this.refreshTpl() } else { ui.growl.append("Error deleting Template!", 'error') } } onFolderSelect(path) { this.templates.ffs.changeDir(path) this.refreshTpl() } async onTemplateClone(tId, button) { if (!button) return button.classList.add('spin') button.disabled = true try { const originalTpl = await this.templates.get(tId) if (!originalTpl) { ui.growl.append('Original template not found', 'danger') return } const responseCreate = await this.templates.save({ status: 'created' }) if (responseCreate.success) { const newId = responseCreate.payload.id const responseClone = await this.createAndSaveNewTemplate(newId, originalTpl) if (responseClone.success) { app.Router.route(`/templates/${newId}`) } else { ui.growl.append('Error cloning template', 'error') } } else { ui.growl.append('Error creating template', 'error') } } catch (error) { console.error('Error cloning template:', error) ui.growl.append('Error cloning template', 'error') } finally { button.classList.remove('spin') button.disabled = false } } async onTemplateEdit(tId) { const button = this.find(`[data-id="${tId}"][data-trigger="onTemplateEdit"]`) if (!button) return button.classList.add('spin') button.disabled = true try { app.Router.route(`/templates/${tId}`, { mode: 'edit', ffsData: this.templates.ffs }) } catch (error) { console.error('Error opening template:', error) ui.growl.append('Error opening template', 'error') } finally { button.classList.remove('spin') button.disabled = false } } /*Manage template record for new template @status = created */ async onTemplateCreate() { const button = this.find('.tpl-createButton'); if (!button) return button.disabled = true; button.classList.add('loading') button.innerHTML = ' Creating...' const responseCreate = await this.templates.save({ status: 'created' }) if (responseCreate.success) { this.newId = responseCreate.payload.id const templateStatus = 'created' const tplDate = new Date().toISOString() const path = this.authorizedPaths?.[0] const tplName = 'New template' const html = btoa('

new template

') const altText = '' const subject = 'New template' const tplData = { id: this.newId, type: 'templateInfo', name: tplName, path: path, html: html, status: templateStatus, meta: { subject: subject, type: '', altText: altText, }, statusHistory: { changedBy: { euLoginId: app.User.identity.uuid, mail: app.User.identity.mail, role: app.User.identity.role, }, dateTime: tplDate, value: templateStatus, }, startDate: tplDate, endDate: tplDate, } const response = await this.templates.save(tplData) if (response.success) { app.Router.route(`/templates/${this.newId}`) button.disabled = false button.classList.remove('loading') button.innerHTML = 'Create New Template' } else { ui.growl.append('Error creating template', 'error') } } } /*Manage template record for clone @tplData */ async createAndSaveNewTemplate(newId, tplData = {}) { const templateStatus = 'draft' const tplDate = new Date().toISOString() const path = tplData.path || this.authorizedPaths?.[0] const tplName = tplData.name || ' template' const html = tplData.html || btoa('

CLONE template

') const altText = tplData.meta?.altText || '' const subject = tplData.meta?.subject || '' const data = { id: newId, type: 'templateInfo', name: 'CLONE-' + tplName, path: path, html: html, status: templateStatus, meta: { subject: 'CLONE-' + subject, type: '', altText: altText, }, statusHistory: { changedBy: { euLoginId: '', mail: '', role: '', }, dateTime: '', value: templateStatus, }, startDate: tplDate, endDate: tplDate, } const response = await this.templates.save(data) if (response.success) { return { success: true, id: response.payload.recId } } else { ui.growl.append('Error creating template', 'error') } } getEarliestDateTime(statusHistory) { if (!Array.isArray(statusHistory)) return null return statusHistory.reduce((earliest, entry) => { const entryDate = new Date(entry.dateTime) return !earliest || entryDate < earliest ? entryDate : earliest }, null) } onPreviewClose() { this.find('.preview').setAttribute('disabled', '') } onTemplatesDisplay(event) { let button = event._el switch (button.dataset.value) { case 'thumbs': this.find('.library').classList.add('thumbs') break default: this.find('.library').classList.remove('thumbs') } } /** * replaceMustaches : replaces {{mustache}} with a span with class tpl-token * @param {string} html : the html to process * @returns {string} : the processed html */ replaceMustaches(html) { return html.replace(/\{\{(.*?)\}\}/g, function (match, p1) { return `${p1.trim()}` }) } decodeBase64Utf8(str) { return decodeURIComponent( Array.from(atob(str)).map(c => '%' + c.charCodeAt(0).toString(16).padStart(2, '0') ).join('') ) } } app.registerClass('TemplatesManagerView', TemplatesManagerView)