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,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(/&ZeroWidthSpace;/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)