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, `${status.label}`, ], 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('
') .split(' -> ').join(' → ') let currentDate = ui.format.date( article.created ) if(lastDate != currentDate ) { content.append(ui.create(`
${currentDate}
`)) lastDate = currentDate; } switch(article.sender) { case 'Agent': case 'System': content.append(ui.create(`
EIC support wrote
`)) break; case 'Customer': content.append(ui.create(`
You wrote
`)) break; } let thread = ui.create(`
${article.subject || ''}
${message}
`) 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, `${err.level}`, 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 = '
' for(let step of err.stacktrace) { if(step[2].length<2) continue trace += `
${step[0]} @ ${step[1]} ${step[2].join(':')}
` } trace += '
' let messageData = '' if(Array.isArray(err.messageData)) { messageData = '
'+err.messageData.map(x=>JSON.stringify(x)).join('
')+'
' } else if(err.messageData) messageData = '
'+err.messageData+'
' let result = await this.confirmDialog({ title: 'Error Details', message: `
`, 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', `${err.level}`]) this.gridEntry.addRow('message', ['Message', `${err.message}
${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);