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
+190
View File
@@ -0,0 +1,190 @@
<style>
.support > header {
background: url('/app/assets/images/cards/support.jpg');
background-position-y: center;
}
.support > header h2 {
white-space: normal !important;
margin-right: calc(var(--eicui-base-spacing-4xl) * 3);
width: auto;
display: grid !important;
}
.support > header h2 span {
background: #0000006e;
padding: var(--eicui-base-spacing-2xs);
margin: 0px 220px 0 0;
width: 100%;
box-sizing: border-box;
}
.support-tickets .inspector.loading > section {
min-height: 240px;
}
.support-tickets .inspector.loading::after {
content: "";
display: block;
position: absolute;
width: 100%;
height: 100%;
background: #ffffff9c;
font-family: 'glyphs' !important;
opacity: 0.4;
}
.support-tickets .inspector.loading > section::after {
content: "\\e981";
animation: spin 1s infinite linear;
font-family: 'glyphs' !important;
width: min-content;
height: min-content;
color: var(--eicui-base-color-primary);
font-size: var(--app-font-size-2xl);
z-index: 2;
position: absolute;
top: 50%;
left: 50%;
margin: auto;
}
.support-tickets {
resize: vertical;
height: 42vh;
overflow: hidden;
padding-bottom: var(--eicui-base-spacing-l) !important;
}
.support-tickets .list {
height: 100%;
overflow: hidden;
}
.support-tickets .list [eicdatagrid] {
resize: horizontal;
min-width: 350px;
height: 100%;
display: grid;
}
.support-tickets .list .row { grid-template-columns: 2fr 100px 5em 2em; }
.support-tickets .list .dataset .row { grid-template-columns: 1fr 90px; }
.support-tickets .list .dataset .cell:nth-child(3),.support-tickets .cell:nth-child(4) { text-align: center;}
.support-tickets .list .dataset .cell:nth-child(2) {
grid-column: 1 / 3;
font-weight: bold;
}
.support-tickets .list .dataset .cell:nth-child(4) {
grid-row: 2;
grid-column: 2;
text-align: right;
}
.support-tickets .list .dataset .cell:nth-child(3) {
grid-column: 1;
font-size: smaller;
text-align: left;
}
.support-tickets .inspector {
height: 100%;
display: grid;
}
.support-tickets .inspector .sender { padding: var(--eicui-base-spacing-l) var(--eicui-base-spacing-xs) var(--eicui-base-spacing-xs) var(--eicui-base-spacing-xs); }
.support-tickets .inspector .subject {
background: var(--app-color-white);
padding: var(--eicui-base-spacing-2xs) var(--eicui-base-spacing-xs);
font-weight: bold;
color: var(--app-color-primary);
background: var(--eicui-base-color-grey-5);
}
.support-tickets .inspector .message {
background: var(--eicui-base-color-grey-5);
padding: var(--eicui-base-spacing-m);
}
.support-tickets .inspector .attachments {
display: flex;
flex-direction: row;
flex-wrap: wrap;
background: var(--eicui-base-color-grey-5);
}
.support-tickets .inspector .attachments button { margin: var(--eicui-base-spacing-xs); }
.support-console .row .cell:nth-child(3) { text-align: center;}
.console-error-detail .row { grid-template-columns: 8vw auto; }
.console-error-detail .row .cell:nth-child(2) { font-weight: bold; }
.support-console .row .cell, .console-error-detail .row .cell {
white-space: normal;
text-overflow: clip;
}
.console-error-detail .row .cell .trace{
max-height: 20vw!important;
overflow: auto!important;
}
.support-console .row .cell span.error { color: var(--app-color-danger); }
.support-console .row .cell span.warning { color: var(--app-color-warning); }
.console-error-detail .row .cell span.error { color: var(--app-color-danger); }
.console-error-detail .row .cell span.warning { color: var(--app-color-warning); }
.console-error-detail .row .cell .traceline, .console-error-detail .row .cell .msgdata {
font-family: monospace;
font-size: var(--eicui-base-font-size-s);
color: var(--eicui-base-color-white);
padding: .2em 1em;
border-radius: 5px;
margin-bottom: 1px;
width: fit-content;
}
.console-error-detail .row .cell .traceline {
background-color:var(--eicui-base-color-info-100);
float: right;
clear: both;
}
.console-error-detail .row .cell .msgdata { background-color:var(--eicui-base-color-success-100); }
</style>
<article eiccard media class="support">
<header>
<h1>Help & Support</h1>
<h2 class="cols-2 right">
<span medium>If you require assistance, have suggestions for improvements, or need to report technical issues and bugs, please feel free to contact us by clicking on the "Ask for Support" button, located above. You can also contact us at <a href="mailto:eic@support.eismea.eu" title="contact support"><span accent>eic@support.eismea.eu</span></a>.</span>
<button eicbutton accent large class="create">Ask for support</button>
</h2>
<div class="actions cols-1 right">
</div>
</header>
<section>
<div eicalert info >
For any inquiries regarding EIC Accelerator short proposal submission, we recommend checking out our <a href="https://eic.ec.europa.eu/eic-accelerator-application-platform-frequently-asked-questions_en#short-proposal-step-1" title="FAQ" target="_blank">FAQ section</a> first.</br>
</div>
<div class="cols-2 left support-tickets">
<div class="list">
<div eicdatagrid></div>
</div>
<article eiccard class="inspector">
<header>
<h1></h1>
<h2></h2>
</header>
<section></section>
<footer>
<div class="cols-1 right cta">
<button eicbutton class="reply" small primary>reply</button>
</div>
<form class="reply-form">
<textarea eictextarea name="body" data-path="" class="required"></textarea>
<select eicselect multiple editable name="attachments" data-type="ignore" placeholder="Click here to add a file" hint="You may not attach more than 5Mb of files"></select>
<div class="cols-1 right">
<div class="cols-2">
<button eicbutton class="cancel" small secondary>Cancel</button>
<button eicbutton class="send" data-id="" small primary>Send</button>
</div>
</div>
</form>
</footer>
</article>
</div>
<article class="support-console" eiccard collapsable collapsed>
<header>
<h1>Interface error log</h1>
<h2>Technical errors we spotted while you were using the platform</h2>
</header>
<section>
<div eicdatagrid footer="hidden"></div>
</section>
</article>
</section>
</article>
+358
View File
@@ -0,0 +1,358 @@
class SupportView extends EICDomContent {
TICKET_STATUSES = [
{id: 0, label: '', severity: ''},
{id: 1, label: 'new', severity: 'info'},
{id: 2, label: 'opened', severity: 'warning'},
{id: 3, label: 'opened', severity: 'warning'},
{id: 4, label: 'closed', severity: 'success'},
{id: 5, label: '(state5)', severity: 'danger'},
{id: 6, label: '(state6)', severity: 'danger'},
{id: 7, label: 'opened', severity: 'warning'},
]
constructor() {
super();
Object.assign(this, app.helpers.basicDialogs)
}
DOMContentLoaded(options) {
this.components = ui.eicfy(this.el)
for(let model in options.models) this[model] = options.models[model];
this.gridTickets = new DataGrid(this.find('.support-tickets .list [eicdatagrid]'), {
headers: [
{ label: 'Ticket', filter: 'text', sortable: true},
{ label: 'Modified', type: 'dateTime', sortable: true},
{ label: 'Status', type: 'markup', filter: 'list', sortable: true},
]
});
this.btTicketsRefresh = new Button(null, {icon: 'icon-refresh', severity: 'primary', rounded: true, size: 'xsmall', hint: 'Refresh the list'})
this.btTicketsRefresh.addEventListener('click', this.refreshTicketsList.bind(this));
this.gridTickets.el.querySelector('header .cell.actions').append(this.btTicketsRefresh.el);
this.gridTickets.onRowClick = this.onTicketSelect.bind(this);
this.gridConsole = new DataGrid(this.find('.support-console [eicdatagrid]'), {
headers: [
{ label: 'Time', filter: 'text', sortable: true},
{ label: 'Level', filter: 'list', sortable: true},
{ label: 'Error', filter: 'text', sortable: true},
{ label: 'Url', filter: 'text', sortable: true},
],
actions: [],
height: '30vh'
});
this.gridConsole.onRowClick = this.showErrDetails.bind(this)
this.ticketDetail = this.find('.support-tickets .inspector');
ui.hide(this.ticketDetail);
this.btReply = this.find('.support-tickets .inspector footer .cta button');
this.btReply.addEventListener('click', this.onTicketReplyOpen.bind(this))
this.attachments = this.components.find(item => item.el.name == 'attachments')
this.attachments.activeEditor = BinaryFileContentSelector;
this.replyForm = new Form(this.find('.support-tickets .inspector footer .reply-form'));
ui.hide(this.replyForm.el);
this.replyForm.el.querySelector('button.send').addEventListener('click', this.onTicketReply.bind(this))
this.replyForm.el.querySelector('button.cancel').addEventListener('click', this.onTicketReplyCancel.bind(this))
this.btIssueNew = this.components.find(item => item.el.classList.contains('create'));
this.btIssueNew.addEventListener('click', this.onTicketCreate.bind(this));
this.refreshConsole();
this.refreshTicketsList()
}
DOMContentFocused() {
if(!this.refreshTo) this.refreshTo = setTimeout(this.checkUp2Date.bind(this),500)
this.refreshConsole();
}
DOMContentBlured() {
if(this.refreshTo) {
clearTimeout(this.refreshTo)
this.refreshTo = null
}
}
refreshTicketsList(event=null){
if(event){
event.stopPropagation();
event.preventDefault();
}
this.gridTickets.loading = true;
this.tickets.list()
.then(this.refreshTickets.bind(this));
}
refreshTickets(payload) {
this.gridTickets.loading = false;
this.gridTickets.clear();
let tickets = payload.reverse();
if(tickets.length > 0) {
for(let ticket of tickets) {
let status = this.TICKET_STATUSES.find(item => item.id == (ticket.state.id || 0))
this.gridTickets.addRow(ticket.id, [
`${ticket.title} (#${ticket.number})`,
ticket.state.lastActivity,
`<span eicchip xsmall ${status.severity}>${status.label}</span>`,
], true);
}
this.gridTickets.filter()
this.tickets.get(tickets[0].id)
.then(this.refreshInspector.bind(this));
}
}
clearInspector() {
ui.show(this.ticketDetail, 'grid');
this.ticketDetail.classList.remove('loading');
this.ticketDetail.querySelector('header h1').innerHTML = ''
this.ticketDetail.querySelector('header h2').innerHTML = ''
this.ticketDetail.querySelector('section').innerHTML = '';
this.replyForm.el.querySelector('textarea').innerHTML = '';
this.attachments.clear();
}
freezeInspector() {
ui.show(this.ticketDetail, 'grid');
this.ticketDetail.classList.add('loading');
}
refreshInspector(payload) {
let ticket = payload.ticket;
let articles = payload.articles.reverse();
this.clearInspector();
let content = this.ticketDetail.querySelector('section');
this.ticketDetail.querySelector('header h1').innerHTML = `${ticket.title} (#${ticket.number})`;
this.ticketDetail.querySelector('header h2').innerHTML = ticket.typeofissue || 'General issue';
this.replyForm.el.querySelector('button.send').dataset.id = ticket.id;
let lastDate = null;
for(let article of articles) {
let message = article.body
.split("\n").join('<br/>')
.split(' -> ').join(' &#8594; ')
let currentDate = ui.format.date( article.created )
if(lastDate != currentDate ) {
content.append(ui.create(`<div class="center" small>${currentDate}</div>`))
lastDate = currentDate;
}
switch(article.sender) {
case 'Agent':
case 'System':
content.append(ui.create(`<div class="cols-1 left sender"><span eicchip primary xsmall><i class="icon-user"></i><span>EIC support wrote</span></span></div>`))
break;
case 'Customer':
content.append(ui.create(`<div class="cols-1 right sender"><span eicchip success xsmall><i class="icon-user"></i><span>You wrote</span></span></div>`))
break;
}
let thread = ui.create(`<div class="content"><div class="subject">${article.subject || ''}</div><div class="message">${message}</div><div class="attachments"></div></div>`)
content.append(thread)
for(let file of article.attachments) {
let button = new Button(null, {
label: file.filename,
severity: 'info',
size: 'xsmall',
rounded: true,
icon: 'icon-attachment'
});
button.el.dataset.ticket = ticket.id;
button.el.dataset.article = article.id;
button.el.dataset.attachment = file.id;
button.el.addEventListener('click', this.onFileOpen.bind(this));
thread.querySelector('.attachments').append(button.el)
}
}
}
refreshConsole() {
this.gridConsole.clear();
for(let err of app.latestErrors) {
this.gridConsole.addRow(err.errId,
[ err.timestamp,
`<span class="${err.level.toLowerCase()}">${err.level}</span>`,
err.message,
err.url,
]
);
}
}
checkUp2Date() {
let mustRefresh = false
if(app.latestErrors.lengh != this.gridConsole.length) mustRefresh=true
else {
for(let err of app.latestErrors) {
if(!this.gridConsole.getRowById(err.errId)) { // err not in grid (the opposite should not happen)
mustRefresh = true
break
}
}
}
if(mustRefresh) this.refreshConsole()
this.refreshTo = setTimeout(this.checkUp2Date.bind(this),500)
}
onFileOpen(event) {
event.stopPropagation();
event.preventDefault();
let ticketId = event.currentTarget.dataset.ticket;
let articleId = event.currentTarget.dataset.article;
let attachmentId = event.currentTarget.dataset.attachment;
this.tickets.download(ticketId, articleId, attachmentId);
}
onTicketSelect(target) {
this.closeReplyForm();
this.freezeInspector();
this.tickets.get(target.dataset.id)
.then(this.refreshInspector.bind(this));
}
async onTicketCreate(event) {
event.stopPropagation();
event.preventDefault();
let ticket = await this.openDialog(
await this.loadContent(
'common/support/dialogs/SupportIssueFormDialog',
{ title: 'Report an issue' },
{
mode: 'create',
models: { tickets: this.tickets }
}
)
);
if(ticket) {
this.tickets.list()
.then(this.refreshTickets.bind(this));
}
}
closeReplyForm() {
ui.show(this.ticketDetail.querySelector('footer .cta'), 'grid');
ui.hide(this.ticketDetail.querySelector('footer .reply-form'));
}
openReplyForm() {
ui.hide(this.ticketDetail.querySelector('footer .cta'));
ui.show(this.ticketDetail.querySelector('footer .reply-form'), 'grid');
}
onTicketReplyOpen(event) {
event.stopPropagation();
event.preventDefault();
this.openReplyForm();
}
async onTicketReply(event) {
event.stopPropagation();
event.preventDefault();
let ticketId = event.currentTarget.dataset.id;
if(this.replyForm.validate()) {
let payload = this.replyForm.value;
payload.attachments = this.attachments.selectedItems.map(item => { return { filename: item.label, "mime-type": item.type, data: item.content} })
let size = JSON.stringify(payload).length
let limit = 1024 * 1024 * 5;
if(size < limit) {
this.tickets.update(ticketId, payload)
.then( this.onReplySuccess.bind(this, ticketId))
} else {
ui.growl.append('Your message is exceeding the allowed 5Mb size limit. Please consider removing files or reduce file size', 'danger')
}
}
}
onReplySuccess(id, payload) {
this.replyForm.el.querySelector('textarea').value = '';
this.attachments.clear();
this.closeReplyForm();
this.tickets.list().then( this.refreshTickets.bind(this));
this.tickets.get(id).then(this.refreshInspector.bind(this));
}
onTicketReplyCancel(event) {
event.stopPropagation();
event.preventDefault();
this.closeReplyForm();
}
async showErrDetails(row, event) {
let err = app.latestErrors.find(err=>err.errId==row.dataset['id'])
if(!err) return
let trace = '<div class="trace">'
for(let step of err.stacktrace) {
if(step[2].length<2) continue
trace += `<div class="traceline">${step[0]} @ ${step[1]} ${step[2].join(':')}</div>`
}
trace += '</div>'
let messageData = ''
if(Array.isArray(err.messageData)) {
messageData = '<div class="msgdata">'+err.messageData.map(x=>JSON.stringify(x)).join('</div><div class="msgdata">')+'</div>'
} else if(err.messageData) messageData = '<div class="msgdata">'+err.messageData+'</div>'
let result = await this.confirmDialog({
title: 'Error Details',
message: `<div eicdatagrid class="console-error-detail" footer="hidden" header="hidden"></div>`,
cancelLabel: 'Close',
okLabel: '',
severity: 'primary',
muted: true,
okPromise:() => { },
contentLoaded: (el => {
el.style['max-width'] = '80vw'
el.style['max-height'] = '50vw'
this.gridEntry = new DataGrid(el.querySelector('.support-entryGrid'), {
headers: [
{ label: ''},
{ label: ''}
],
height: 'auto'
});
this.gridEntry.addRow('time', ['Time', err.timestamp])
this.gridEntry.addRow('level', ['Level', `<span class="${err.level.toLowerCase()}">${err.level}</span>`])
this.gridEntry.addRow('message', ['Message', `<span class="${err.level.toLowerCase()}">${err.message}</span><br>${messageData}`])
this.gridEntry.addRow('user', ['User UID', err.user.identity.uuid])
this.gridEntry.addRow('user', ['User Roles', err.user.roles.join(', ')])
this.gridEntry.addRow('url', ['Url', err.url])
this.gridEntry.addRow('stacktrace', ['Trace', trace])
}).bind(this),
})
}
}
app.registerClass('SupportView',SupportView);
@@ -0,0 +1,23 @@
<style>
.support-issue-form {
min-width: 50vw;
min-height: 50vh;
}
.support-issue-form [name="body"] {
min-height: 180px;
}
</style>
<div class="support-issue-form">
<label xsmall>Subject</label>
<input eicinput type="text" name="title" data-path="" value="" class="required" placeholder="Please describe shortly your issue" />
<label xsmall>Type of inquiry</label>
<select eicselect name="typeofissue" tabindex="" data-path="" class="required" placeholder="Please select a category">
<option value=""></option>
<option value="question">I have a general question</option>
<option value="accessRequest">I cannot connect to the platform</option>
<option value="bug">I am facing a technical issue</option>
</select>
<label xsmall>Description</label>
<textarea eictextarea name="body" data-path="article" class="required" placeholder="Please provide as many information as possible"></textarea>
<select eicselect multiple editable name="attachments" data-type="ignore" placeholder="Click here to add a file" hint="You may not attach more than 5Mb of files"></select>
</div>
@@ -0,0 +1,61 @@
class SupportIssueFormDialog extends EICDialogContent {
actions = [
{
label: 'Cancel',
onclick: this.cancel.bind(this),
severity: 'secondary',
role: 'cancel'
},
{
label: 'Send',
onclick: this.send.bind(this),
severity: 'primary',
role: 'send',
disabled: false
}
]
DOMContentLoaded(options) {
this.mode = options.mode || 'create';
this.ticketId = options.ticketId || 0;
for(let model in options.models) this[model] = options.models[model];
this.components = ui.eicfy(this.el)
this.form = new Form(this.el);
this.attachments = this.components.find(item => item.el.name == 'attachments')
this.attachments.activeEditor = BinaryFileContentSelector;
}
send() {
if(this.form.validate()) {
let payload = this.form.value;
payload.article.attachments = this.attachments.selectedItems.map(item => { return { filename: item.label, "mime-type": item.type, data: item.content} })
let size = JSON.stringify(payload).length
let limit = 1024 * 1024 * 5;
if(size < limit) {
this.tickets.create(payload)
.then( this.success.bind(this));
} else {
ui.growl.append('Your message is exceeding the allowed 5Mb size limit. Please consider removing files or reduce file size', 'danger')
}
}
}
success(payload) {
this.commit(payload)
ui.growl.append(`We'll do our best to respond to your message promptly.</br>
Thank you for your interest and feedback, we look forward to help you out!`, 'success')
}
}
app.registerClass('SupportIssueFormDialog',SupportIssueFormDialog);