unclean SPARC
This commit is contained in:
@@ -0,0 +1,561 @@
|
||||
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', `<div style="padding: 2rem; text-align: center;">
|
||||
<h2 style="color:#b00020;">Access Denied</h2>
|
||||
<p>You're not authorized to access the templates dashboard</p>
|
||||
</div>`)
|
||||
return
|
||||
}
|
||||
|
||||
const tpls = await this.templates.search(
|
||||
this.components.searchCriteria.value,
|
||||
this.getSelectedStatus()
|
||||
)
|
||||
|
||||
if (tpls?.message === 'NO_ACCESS') {
|
||||
this.output('searchResults', `<div style="padding: 2rem; text-align: center;">
|
||||
<h2 style="color:#b00020;">Access Denied</h2>
|
||||
<p>You're not authorized to view templates</p>
|
||||
</div>`)
|
||||
return
|
||||
}
|
||||
this.fillTemplates(tpls)
|
||||
|
||||
} catch (error) {
|
||||
this.output('searchResults', `<div style="padding: 2rem; text-align: center;">
|
||||
<h2 style="color:#b00020;">Unexpected Error</h2>
|
||||
<p>An error occurred while loading templates. Please try again later</p>
|
||||
</div>`)
|
||||
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: `<span>${this.templates.getStatusLabel()[status]}</span>` })
|
||||
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: `<span>${label}</span>`
|
||||
})
|
||||
|
||||
// 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 (`<span eicchip xxsmall info data-path="/${path}">${dir}</span>`)
|
||||
}).join('')
|
||||
|
||||
let tplInfos = `<span eicchip ${statusBadge} xsmall outline><label>${statusLabel}</label></span>`
|
||||
let tplTitle = `<div>${item.name}<br/><span xsmall class-"tplInfos">last action: ${oldestDate}<br/>by: ${fullName}</span></div>`
|
||||
let deleteB = ''
|
||||
if (item.status === 'created' || item.status === 'draft') {
|
||||
deleteB = `<div><button eicbutton class="icon-bin delete-tpl" xsmall rounded danger type="button" data-id="${item.id}" data-trigger="onDeleteTemplate"></button><div>`
|
||||
}
|
||||
let tplActions = `
|
||||
<div class="template-actions">
|
||||
${
|
||||
item.status !== 'prod'
|
||||
? `<button eicbutton class="open-btn icon-pen" xsmall rounded primary data-id="${item.id}" type="button" data-trigger="onTemplateEdit" title="Edit this template"></button>`
|
||||
: ''
|
||||
}
|
||||
<button eicbutton class="preview-btn icon-copy" xsmall rounded data-id="${item.id}" type="button" data-trigger="onTemplateClone" title="Clone this template"></button>
|
||||
</div>
|
||||
`
|
||||
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 = `<style>
|
||||
span.tpl-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>\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: `<p>You are about to permanently delete the template "<b>${tplName}</b>".</p>
|
||||
<p>Are you sure?</p>`,
|
||||
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 = '<i class="icon-spinner spin"></i> 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('<p>new template</p>')
|
||||
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 = '<span>Create New Template</span>'
|
||||
}
|
||||
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('<p>CLONE template</p>')
|
||||
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 `<span class="tpl-token" eicchip="" success="" data-original="${p1.trim()}">${p1.trim()}</span>`
|
||||
})
|
||||
}
|
||||
|
||||
decodeBase64Utf8(str) {
|
||||
return decodeURIComponent(
|
||||
Array.from(atob(str)).map(c =>
|
||||
'%' + c.charCodeAt(0).toString(16).padStart(2, '0')
|
||||
).join('')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
app.registerClass('TemplatesManagerView', TemplatesManagerView)
|
||||
|
||||
Reference in New Issue
Block a user