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
+40
View File
@@ -0,0 +1,40 @@
<style>
.video-guide > header {
background: url('/app/assets/images/cards/eic_purple_banner.png');
background-position: top;
background-size: contain !important;
background-repeat: no-repeat;
background-color: #5336a3;
border-bottom: 4px solid white !important;
}
.video-guide .list {
display: grid;
grid-gap: var(--eicui-base-spacing-m);
grid-template-columns: repeat(auto-fill, 240px);
}
.video-guide .list section{
text-align: center;
}
.video-guide .list img{
height: 130px;
}
.video-guide .videoStage{
text-align: center;
}
.video-guide video{
max-height: 70vh;
}
</style>
<article eiccard media class="video-guide">
<header>
<h1>${guideMeta.title}</h1>
</header>
<section class="guides">
<div class="videoStage">
<video class="video-guide" controls class="EmbedVideo" width="95%">
<source src="/app/assets/videos/guides/${guideMeta.video}" type="${guideMeta.videoType}">
</video>
</div>
</section>
</article>
+10
View File
@@ -0,0 +1,10 @@
class GuideView extends EICDomContent {
constructor() { super(); }
DOMContentLoaded(options) {
ui.eicfy(this.el)
}
}
app.registerClass('GuideView', GuideView);
+36
View File
@@ -0,0 +1,36 @@
<style>
.welcome-guides > header {
background: url('/app/assets/images/cards/eic_purple_banner.png');
background-position: top;
background-size: contain !important;
background-repeat: no-repeat;
background-color: #5336a3;
border-bottom: 4px solid white !important;
}
.welcome-guides .list {
display: grid;
grid-gap: var(--eicui-base-spacing-m);
grid-template-columns: repeat(auto-fill, 240px);
}
.welcome-guides .list section{
text-align: center;
}
.welcome-guides .list img{
height: 130px;
}
</style>
<article eiccard media class="welcome-guides">
<header>
<h1>Welcome guides</h1>
</header>
<section class="guides">
<div class="list">
</div>
</section>
</article>
+43
View File
@@ -0,0 +1,43 @@
class GuidesListView extends EICDomContent {
constructor() { super(); }
DOMContentLoaded(options) {
ui.eicfy(this.el)
this.guides = options.guides;
this.thumbs = this.find('.guides .list');
this.fillGuides()
}
fillGuides(){
if((app.Assets.Store.json.videoGuides) && app.Assets.Store.json.videoGuides.guides){
let guidesList = app.Assets.Store.json.videoGuides.guides
for(let item of guidesList) {
let tile = ui.create(`<article eiccard>
<header>
<h1>${item.title || ''}</h1>
</header>
<section>
<img src="/app/assets/videos/guides/thumbnails/${item.thumbnail}">
</section>
<footer></footer>
</article>`);
let button = new Button(null, {label:'watch', severity: 'primary', size: 'xsmall'});
button.el.addEventListener('click', this.onGuideWatch.bind(this, item.id))
tile.querySelector('footer').append(button.el)
this.thumbs.append(tile);
}
}
}
onGuideWatch(guideID, event){
event.stopPropagation();
event.preventDefault();
app.Router.route(`/about/guide/${guideID}`, { });
}
}
app.registerClass('GuidesListView', GuidesListView);
+22
View File
@@ -0,0 +1,22 @@
<style>
.helpdesk > header {
background: url('/app/assets/images/cards/eic_purple_banner.png');
background-position: top;
background-size: contain !important;
background-repeat: no-repeat;
background-color: #5336a3;
border-bottom: 4px solid white !important;
}
</style>
<article eiccard media class="helpdesk">
<header>
<h1>Help Desk</h1>
</header>
<section>
<p>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.</p>
<p>If you need support, have suggestions of improvements, or need to report technical issues and bugs, feel free to reach out to us at <a href="mailto:eic@support.eismea.eu" title="Email for support">eic@support.eismea.eu</a> . </p>
<p>We'll do our best to respond to your message promptly.</p>
<p>Thank you for your interest and feedback, we look forward to help you out!</p>
</section>
</article>
+9
View File
@@ -0,0 +1,9 @@
class HelpDeskView extends EICDomContent {
DOMContentLoaded() {
ui.eicfy(this.el)
}
}
app.registerClass('HelpDeskView', HelpDeskView);
+40
View File
@@ -0,0 +1,40 @@
<style>
.disclaimer > header {
background: url('/app/assets/images/cards/eic_purple_banner.png');
background-position: top;
background-size: contain !important;
background-repeat: no-repeat;
background-color: #5336a3;
border-bottom: 4px solid white !important;
}
.disclaimer > section {
padding: var(--eicui-base-spacing-xxl);
background: var(--app-color-primary);
color: var(--app-color-white) !important;
}
.disclaimer a { color: var(--app-color-white) !important; line-height: 2; }
.disclaimer h2 a { color: var(--app-color-white) !important; line-height: 1; }
.disclaimer .ec-logo {
display: grid;
align-items: center;
}
.disclaimer hr {
opacity: 0.5;
}
</style>
<article eiccard media class="disclaimer">
<header>
<h1>Legal notice</h1>
</header>
<section>
<div>
<ul nonbulleted>
<li><a target="_blank" href="https://eic.ec.europa.eu//history-eic_en">About the EIC</a></li>
<li><a target="_blank" href="https://eismea.ec.europa.eu/">European Innovation Council and Small and Medium-sized Enterprises Executive Agency (EISMEA)</a></li>
<li><a target="_blank" href="https://eic.eismea.eu/community/sites/default/files/data_protection_notice.pdf">Data protection notice</a></li>
<li><a target="_blank" href="https://eic.eismea.eu/community/sites/default/files/cookies.pdf">Cookies</a></li>
</ul>
</div>
</section>
</article>
+9
View File
@@ -0,0 +1,9 @@
class disclaimerView extends EICDomContent {
DOMContentLoaded() {
ui.eicfy(this.el)
}
}
app.registerClass('disclaimerView', disclaimerView);
@@ -0,0 +1,39 @@
<style>
.onboarding.applicant { max-width: 50vw; }
.onboarding.applicant .legalname { font-weight: bold; }
</style>
<div class="onboarding applicant">
<section class="search">
<p>In order to apply, your organisation must be registered withe the European Commission.<br>
Please provide the PIC (Participant Identification Code) of the organisation you are representing.</p>
<div class="cols-2 right top">
<input eicinput type="search" class="required validate-number" data-path="query" hint="The PIC number of a company is at least 9 digit long number" placeholder="Enter a PIC number and press enter..." />
<button eicbutton primary icon="icon-search" class="search"></button>
</div>
</section>
<section class="result">
</section>
<section class="how-to">
If your organisation is not registered yet,
you may apply for the PIC using the<br>
<a href="https://ec.europa.eu/info/funding-tenders/opportunities/portal/participant/register" target="_blank">Participant Register in the</a>
<a href="https://ec.europa.eu/info/funding-tenders/opportunities/portal/participant/register" target="_blank">Funding & Tenders Portal</a>
</section>
<section class="unregistered-pic">
<input type="hidden" data-path="pic" value="" />
<alert eicalert info>This organisation is not registered in the platform.<br/>By continuing the registration process, you will become administrator of this organisation.<br/>The organisation administrator will be notified by email and in charge of accepting or denying any later user registration request on this organisation.</alert>
<section>
<input eiccheckbox type="checkbox" data-path="optin" data-type="ignore" class="required" label="Hereby, I confirm that I hold the legal right to administrate this organisation" value="yes" />
</section>
</section>
<section class="registered-pic">
<input type="hidden" data-path="pic" value="" />
<alert eicalert info>This organisation has already been registered by another user of the platform. In order to complete the registration, an access request will be sent tho the current administrator of the organisation. Once your request validated, you will get access to your organisation.</alert>
<section>
<textarea name="justification" eictextarea class=""
placeholder="You may provide an explaination to the administrator about your access request"></textarea>
<input eiccheckbox type="checkbox" data-path="optin" data-type="ignore" class="required" label="Hereby, I confirm that I have the legal right to access to this organisation" value="yes" />
</section>
</section>
</div>
@@ -0,0 +1,152 @@
class onboardingApplicantDialog extends EICDialogContent {
actions = [
{
label: 'Back to profiles',
onclick: this.cancel.bind(this),
severity: 'secondary',
role: 'cancel'
},
{
label: 'Register to that organisation',
onclick: this.register.bind(this),
severity: 'success',
role: 'register',
disabled: true
}
]
status = '';
DOMContentLoaded(options) {
for(let model in options.models) this[model] = options.models[model];
if(options.cancelLabel) this.actions.find(o => o.role == 'cancel').label = options.cancelLabel;
this.profile = options.profile;
let components = ui.eicfy(this.el);
this.searchbar = this.find('.search');
this.result = this.find('.result');
this.howTo = new Form(this.find('.how-to'));
this.unregistered = new Form(this.find('.unregistered-pic'));
this.registered = new Form(this.find('.registered-pic'));
this.query = components.find(component => component.el.dataset.path == 'query');
this.query.onQuery = this.onSearch.bind(this);
this.searchButton = components.find(component => component.el.classList.contains('search'));
this.searchButton.el.addEventListener('click', this.onSearch.bind(this));
ui.hide(this.result);
ui.hide(this.registered.el);
ui.hide(this.unregistered.el);
}
onSearch() {
let pic = this.query.value.trim();
if(!pic.match(/^\d{8,}$/)) return;
this.searchButton.loading = true;
ui.hide(this.result);
this.user.searchOrganisation(pic)
.then(this.onResult.bind(this), this.onEmptyResult.bind(this));
}
onResult(payload) {
ui.show(this.result);
this.howTo.hide()
let organisation = payload[0]
this.searchButton.loading = false;
this.isRegistered = organisation.existsInMl
let status = organisation.existsInMl ? 'warning': 'success';
let message = organisation.existsInMl ?
'This company is already registered in the EIC plaform':
'This company is not registered yet the EIC platform';
this.result.innerHTML = `<div class="company" ${status} class="icon-success">
<div><span class="legalname" primary>${organisation.legalName}</span></div>
<div class="cols-3 top">
<div><label>PIC</label><span class="pic">${organisation.pic}</span></div>
<div><label>Country</label><span class="domain">${organisation.country || '(unknown)'}</span></div>
<div><label>Location</label><span class="domain">${organisation.city || '(unknown)'}</span></div>
</div>
</div>
<div eicalert ${status} ${organisation.existsInMl ? 'warning' : 'success' }>${message}</div>`;
this.pic = payload[0].pic;
this.legalname = payload[0].legalName;
this.status = 'selecting'
this.actions.find(o => o.role == 'register').button.disabled = false;
this.actions.find(o => o.role == 'register').button.label = this.isRegistered ? 'Ask to join this organisation' : 'Register to that organisation'
}
onEmptyResult() {
ui.show(this.result);
this.searchButton.loading = false;
this.result.innerHTML = `<div eicalert danger>Sorry, this PIC is invalid</div>`;
this.status = 'selecting';
this.actions.find(o => o.role == 'register').button.disabled = true;
}
onSelect() {
this.findAll('[data-path="pic"]').forEach((item) => item.value = this.query.value )
ui.hide(this.searchbar);
ui.hide(this.result);
this.form = !this.isRegistered ? this.unregistered: this.registered;
ui.show(this.form.el);
this.status = 'completing';
}
register() {
switch(this.status) {
case 'selecting':
this.onSelect();
break;
case 'completing':
if(this.form.validate()) {
this.actions.find(o => o.role == 'register').button.loading = true;
this.actions.find(o => o.role == 'cancel').button.disabled = true;
if(!this.isRegistered) {
this.user.registerOrganisationAccess(this.form.value)
.then( this.onRegisterSuccess.bind(this), this.onFailed.bind(this))
} else {
this.user.requestOrganisationAccess(this.form.value)
.then( this.onRequestSuccess.bind(this), this.onFailed.bind(this))
}
}
break;
}
}
onRegisterSuccess() {
this.actions.find(o => o.role == 'register').button.loading = false;
this.actions.find(o => o.role == 'cancel').button.disabled = false;
this.commit({ request: 'register', pic: this.pic, legalname: this.legalname });
ui.growl.append(`
You have successfully registered to the organisation "${this.legalname}".<br>
You will now be redirected to your dashboard...`,
'success',5000)
setTimeout(()=>{
app.User.logout('/applicant')
}, 1000)
}
async onRequestSuccess() {
this.actions.find(o => o.role == 'register').button.loading = false;
this.actions.find(o => o.role == 'cancel').button.disabled = false;
this.commit({ request: 'join', pic: this.pic, legalname: this.legalname });
}
onFailed() {
this.actions.find(o => o.role == 'register').button.loading = false;
this.actions.find(o => o.role == 'cancel').button.disabled = false;
ui.growl.append(`There was a technical issue trying to join you to the organisation "${this.legalname}".<br>
Please retry again later.`,
'danger',10000)
this.commit(false);
}
}
app.registerClass('onboardingApplicantDialog',onboardingApplicantDialog);
@@ -0,0 +1,39 @@
<style>
.onboarding {}
.onboarding .profiles {
display: grid;
width: auto;
justify-content: center;
grid-auto-flow: column;
grid-gap: var(--app-font-size-m);
}
.onboarding .profiles > article { width: 240px; }
.onboarding, .onboarding .profiles > article h1, .onboarding .profiles > article p { text-align: center; }
.onboarding .exit {
text-align: center;
padding: var(--eicui-base-spacing-m) 0;
}
.onboarding .logout {
margin: 1vw auto;
}
</style>
<div class="onboarding">
<div class="exit">
<div>You are the user: <b>${app.User.identity.uuid}</b></div>
<div>(${app.User.identity.email})</div>
<div class="logout"><a href="#">Sign in with as a different user ?</a></div>
</div>
<div class="profiles">
<article eiccard>
<header>
<h1>I am an Applicant</h1>
</header>
<section>
<p small>I would like to start a short proposal for the EIC accelerator</p>
</section>
<footer>
<div class="cols-1 "><button eicbutton small primary data-type="Applicant">Proceed with that profile</button></div>
</footer>
</article>
</div>
</div>
@@ -0,0 +1,31 @@
class onboardingLandingDialog extends EICDialogContent {
actions = [
]
constructor(options) { super(options); }
DOMContentLoaded(options) {
ui.eicfy(this.el);
let buttons = this.findAll('.profiles button');
for(let button of buttons) button.addEventListener('click', this.onProfileSelect.bind(this));
this.logoutLink = this.find('.onboarding .logout a');
this.logoutLink.addEventListener('click', (event) => {
event.preventDefault()
event.stopPropagation()
app.User.logout()
this.commit()
});
}
onProfileSelect(event) {
event.preventDefault();
event.stopPropagation();
let profile = event.currentTarget.dataset.type;
this.commit(profile);
}
}
app.registerClass('onboardingLandingDialog',onboardingLandingDialog);
@@ -0,0 +1,9 @@
class onboardingLandingView extends EICDomContent {
DOMContentLoaded() {
ui.eicfy(this.el)
}
}
app.registerClass('onboardingLandingView', onboardingLandingView);
@@ -0,0 +1,6 @@
<style>
</style>
<div eicalert success>
Your request to join the organisation "${legalname}" is submited.<br>
You will be notified by email as soon as your access is granted.
</div>
@@ -0,0 +1,21 @@
class onboardingRequestSuccessDialog extends EICDialogContent {
actions = [
{
label: 'Ok',
onclick: this.cancel.bind(this),
severity: 'secondary',
role: 'cancel'
},
]
constructor(options) { super(options); }
DOMContentLoaded() {
ui.eicfy(this.el)
}
}
app.registerClass('onboardingRequestSuccessDialog', onboardingRequestSuccessDialog);
@@ -0,0 +1,14 @@
<article eiccard>
<header>
<h1>Welcome to the EIC!</h1>
<h2></h2>
</header>
<section>
<p>It appears we could not identify any matching profile in our platform</p>
<p>Please, tell us about yor profile:</p>
<ul>
<li><a href="/onboarding/applicant"></a>I am an applicant</a></li>
<li><a href="https://ec.europa.eu">Sorry, wrong place</a></li>
</ul>
</section>
</article>
@@ -0,0 +1,9 @@
class onboardingLandingView extends EICDomContent {
DOMContentLoaded() {
ui.eicfy(this.el)
}
}
app.registerClass('onboardingLandingView', onboardingLandingView);
@@ -0,0 +1,3 @@
<div>
<span eicalert danger>All your preference settings will be discarded. This operation cannot be undone.</span>
</div>
@@ -0,0 +1,29 @@
class ProfilePreferencesResetDialog extends EICDialogContent {
actions = [
{
label: 'Cancel',
onclick: this.cancel.bind(this),
severity: 'secondary',
role: 'cancel'
},
{
label: 'Reset',
onclick: this.reset.bind(this),
severity: 'danger',
role: 'reset'
}
]
cancel() {
this.commit(false);
}
reset() {
app.User.preferences = {};
app.User.savePreferences();
this.commit(true);
}
}
app.registerClass('ProfilePreferencesResetDialog',ProfilePreferencesResetDialog);
@@ -0,0 +1,51 @@
<style>
.my-profile > header {
background: url('/app/assets/images/cards/userprefs.jpg');
}
.my-profile .preferences [eicdatagrid] .row {
grid-template-columns: 2fr 1fr;
}
</style>
<article eiccard media class="my-profile">
<header>
<h1>My Profile</h1>
</header>
<section>
<div class="tabs-extended">
<section>
<menu eictab>
<li>User Informations</li>
<li>Preferences</li>
</menu>
</section>
</div>
<article eiccard class="tab-content">
<section>
<div class="cols-2">
<article class="identity" eiccard collapsable>
<header><h1>User informations</h1></header>
<section >
<div eicdatagrid footer="hidden" header="hidden"></div>
</section>
</article>
<article class="roles" eiccard collapsable>
<header><h1>User Roles</h1></header>
<section >
<div eicdatagrid footer="hidden" header="hidden"></div>
</section>
</article>
</div>
</section>
</article>
<article eiccard class="tab-content preferences">
<section>
<div eicdatagrid></div>
</section>
<footer>
<div class="actions cols-1 right">
<button eicbutton small danger class="reset">Reset preferences...</button>
</div>
</footer>
</article>
</section>
</article>
+57
View File
@@ -0,0 +1,57 @@
class myProfileView extends EICDomContent {
DOMContentLoaded() {
ui.eicfy(this.el)
this.tabs = new Tab();
this.tabs.addTabs(this.findAll('menu li'), this.findAll('.tab-content'))
this.gridIdentity = new DataGrid(this.find('.identity [eicdatagrid]'), {
headers: [ { label: ''}, { label: ''} ]
});
this.gridIdentity.addRow('', [ 'First name',app.User.identity.firstname ]);
this.gridIdentity.addRow('', [ 'Last name',app.User.identity.lastname ]);
this.gridIdentity.addRow('', [ 'Email',app.User.identity.email ]);
this.gridIdentity.addRow('', [ 'EU Login',app.User.identity.uuid ]);
this.gridRoles = new DataGrid(this.find('.roles [eicdatagrid]'), {
headers: [ { label: 'Role name', filter: null, sortable: true} ]
});
for(let role of app.User.roles) { this.gridRoles.addRow(role, [ role ]); }
this.gridPreferences = new DataGrid(this.find('.preferences [eicdatagrid]'), {
headers: [
{ label: 'Key', filter: 'text', sortable: true},
{ label: 'Value', filter: 'text', sortable: false},
]
});
this.scanPreferences(app.User.preferences, '')
let resetButton = new Button(this.find('.preferences button.reset'));
resetButton.click = this.onPreferencesReset.bind(this);
}
scanPreferences(tree, path) {
for(const key in tree) {
let item = tree[key];
let newPath = `${path}${ path ? '.': ''}${key}`;
if(typeof item == 'object')
this.scanPreferences(item, newPath)
else
this.gridPreferences.addRow(key, [ newPath, item ] );
}
}
async onPreferencesReset() {
let resetted = await this.openDialog(
await this.loadContent(
'common/profile/dialogs/ProfilePreferencesResetDialog',
{ title: 'Reset your preferences' }, {}
)
);
if(resetted) { this.gridPreferences.clear(); }
}
}
app.registerClass('myProfileView',myProfileView);
+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);