NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Freshdesk Ticket Note Agent // @version 0.1.0 // @description A userscript that adds agent assignment to note // @license MIT // @author Jim Cheung // @namespace https://github.com/jinhucheung // @include https://*.freshdesk.com/* // @run-at document-idle // @grant none // @icon https://raw.githubusercontent.com/jinhucheung/freshdesk-userscripts/main/images/freshdesk.ico // @updateURL https://raw.githubusercontent.com/jinhucheung/freshdesk-userscripts/main/freshdesk-ticket-note-agent.user.js // @downloadURL https://raw.githubusercontent.com/jinhucheung/freshdesk-userscripts/main/freshdesk-ticket-note-agent.user.js // ==/UserScript== (() => { 'use strict' // constants const cssSelectors = { basicDropdown: '#ember-basic-dropdown-wormhole', ticketProperties: '.__module-tickets__ticket-details__properties', noteAgentField: '#ticket-note-agent-field', noteAgentInput: '#ticket-note-agent-input', noteAgentDropdown: '#ticket-note-agent-dropdown-wormhole', noteAgentDropdownOptionList: '#ticket-note-agent-dropdown-option-list', noteAgentNotFound: '#ticket-note-agent-not-found', noteEmailField: '[data-test-notify-to]', noteEmailSelectField: '[data-test-notify-to] .ticket-action__email > .ember-view', propertyAgentLabel: '[data-test-id="agent"] .label-field' } // data let dropdownObserver = null // entry document.body.addEventListener('click', e => { if (!isTicketPath() || !getTicket()) { removeObserver() return } if (isTargetOf('addNote', e.target)) { setTimeout(renderNoteAgentField, 0) addObserver() } if (isTargetOf('sendNote', e.target)) { assignResponder() } if (!isTargetOf('noteAgent', e.target)) { removeNoteAgentDropdown() } }) window.addEventListener('resize', () => { if (!isTicketPath() && !getTicket()) return locateNoteAgentDropdown() }) // elements function createNoteAgentField(name, value) { const element = document.createElement('div') element.id = 'ticket-note-agent-field' element.className = 'ticket-action__fields can-edit' element.innerHTML = ` <div class="ticket-action__field ticket-action__label text__infotext mt-11">${name}: </div> <div class="ticket-action__field mt-10 ml-10" style="width: 300px;"> <input id="ticket-note-agent-input" data-group="note-agent" value="${value || ' -- '}" style="border: none; box-shadow: none; outline: none; width: 100%; border-radius: 4px; padding: 4px 8px;" /> </div> ` return element } function createNoteAgentDropdown(ticket, agents) { const element = document.createElement('div') element.id = 'ticket-note-agent-dropdown-wormhole' element.innerHTML = ` <div class="dropdown-content ember-basic-dropdown-content ember-power-select-dropdown ember-view ember-basic-dropdown-content--left ember-basic-dropdown-content--above ember-basic-dropdown--transitioned-in" style="max-height: 300px; width: 300px; overflow: auto;" > <ul class="ember-power-select-options ember-view" id="ticket-note-agent-dropdown-option-list"> <li class="ember-power-select-option" role="option"> -- </li> ${agents && agents.map(agent => { return ` <li class="ember-power-select-option" data-id="${agent.id}" data-email="${agent.email}" aria-selected="${agent.id == ticket.responderId}" aria-current="${agent.id == ticket.responderId}" role="option" >${agent.name}</li> ` }).join('')} </ul> </div> ` return element } function createNoteAgentNotFound() { const element = document.createElement('li') element.id = 'not-found' element.innerHTML = 'No results found' element.style.padding = '7px 30px 7px 8px' return element } function renderNoteAgentField() { if (getNoteAgentField()) return const noteEmailField = getNoteEmailField() if (!noteEmailField) return const responder = getResponder() const propertyAgentLabel = getPropertyAgentLabel() const noteAgentField = createNoteAgentField(propertyAgentLabel.title, responder.name) noteEmailField.parentNode.insertBefore(noteAgentField, noteEmailField) observeNoteAgentField() } function renderNoteAgentDropdown() { if (getNoteAgentDropdown()) return const dropdown = createNoteAgentDropdown(getTicket(), getAgents()) document.body.appendChild(dropdown) locateNoteAgentDropdown() observeNoteAgentDropdown() } function locateNoteAgentDropdown() { const dropdown = getNoteAgentDropdown() if (!dropdown) return const dropdownContent = dropdown.querySelector('.dropdown-content') if (!dropdownContent) return const noteAgentInput = getNoteAgentInput() if (!noteAgentInput) return const noteAgentInputRect = noteAgentInput.getBoundingClientRect() dropdownContent.style.top = `${noteAgentInputRect.top + window.scrollY + 35}px` dropdownContent.style.left = `${noteAgentInputRect.left + window.scrollX}px` } function removeNoteAgentDropdown() { const dropdown = getNoteAgentDropdown() dropdown && dropdown.remove() } function filterNoteAgentOptions(value) { const list = getNoteAgentDropdownOptionList() if (!list) return true const options = list.querySelectorAll('li') if (!options) return true const formattedValue = (value || '').toLowerCase() let resultCount = options.length options.forEach(option => { const email = (option.dataset.email || '').toLowerCase() if (!formattedValue || option.innerHTML.trim().toLowerCase().indexOf(formattedValue) >= 0 || email.indexOf(formattedValue) >= 0) { option.style.display = 'block' } else { option.style.display = 'none' resultCount-- } }) return resultCount > 0 } function observeNoteAgentField() { const noteAgentInput = getNoteAgentInput() if (!noteAgentInput) return noteAgentInput.addEventListener('focus', () => { noteAgentInput.style.boxShadow = '0 0 0 2px #2c5cc5' noteAgentInput.select() }) noteAgentInput.addEventListener('blur', () => { noteAgentInput.style.boxShadow = '' }) noteAgentInput.addEventListener('click', () => { renderNoteAgentDropdown() }) noteAgentInput.addEventListener('keyup', () => { setTimeout(() => { const hasResult = filterNoteAgentOptions(noteAgentInput.value) const notFound = getNoteAgentNotFound() if (hasResult) { notFound && notFound.remove() } else if (!notFound) { const optionList = getNoteAgentDropdownOptionList() optionList && optionList.appendChild(createNoteAgentNotFound()) } }, 500) }) } function observeNoteAgentDropdown() { const optionList = getNoteAgentDropdownOptionList() optionList && optionList.addEventListener('click', e => { if (e.target.getAttribute('role') === 'option') { setResponder(e.target.dataset.id) } }) } function addObserver() { observeTicket() observeDropdown() } function removeObserver() { dropdownObserver && dropdownObserver.disconnect() dropdownObserver = null } function observeTicket() { const ticket = getTicket() ticket && ticket.addObserver('responderId', handleResponderChanged) } function observeDropdown() { if (dropdownObserver) return const basicDropdown = getBasicDropdown() if (!basicDropdown) return dropdownObserver = new MutationObserver(() => { setTimeout(() => { basicDropdown.innerHTML && removeNoteAgentDropdown() }, 0) }) dropdownObserver.observe(basicDropdown, { childList: true, subtree: true }) } function handleResponderChanged() { const responder = getResponder() const noteAgentInput = getNoteAgentInput() if (noteAgentInput) noteAgentInput.value = responder.name || ' -- ' setEmailTo(responder) } function getBasicDropdown() { return document.querySelector(cssSelectors.basicDropdown) } function getNoteAgentField() { return document.querySelector(cssSelectors.noteAgentField) } function getNoteAgentInput() { return document.querySelector(cssSelectors.noteAgentInput) } function getNoteAgentDropdown() { return document.querySelector(cssSelectors.noteAgentDropdown) } function getNoteAgentDropdownOptionList() { return document.querySelector(cssSelectors.noteAgentDropdownOptionList) } function getNoteAgentNotFound() { return document.querySelector(cssSelectors.noteAgentNotFound) } function getNoteEmailField() { return document.querySelector(cssSelectors.noteEmailField) } function getPropertyAgentLabel() { return document.querySelector(cssSelectors.propertyAgentLabel) } // methods function getApp() { const apps = window.Ember.A(window.Ember.Namespace.NAMESPACES) return apps[apps.length - 1] } function getPath() { return getApp().__container__.lookup('controller:application').get('currentPath') } function getTicket() { const node = document.querySelector(cssSelectors.ticketProperties) if (!(node && node.id)) return return getApp().__container__.lookup('-view-registry:main')[node.id].ticket } function getEmailTo() { const node = document.querySelector(cssSelectors.noteEmailSelectField) if (!node && node.id) return return getApp().__container__.lookup('-view-registry:main')[node.id] } function getAccount() { const ticket = getTicket() return ticket && ticket.currentAccount } function getAgents() { const account = getAccount() return account && account.agents && account.agents.currentState.map(agent => agent.__data.contact) } function getResponder() { const ticket = getTicket() if (!ticket || !ticket.responderId) return {} const agents = getAgents() if (!agents) return {} const responder = agents.find(agent => agent.id == ticket.responderId) return responder || {} } function getMeta() { const account = getAccount() return account && account.meta } function setEmailTo(responder) { const emailTo = getEmailTo() if (!emailTo && emailTo.options) return const selects = [] const option = emailTo.options.content.find(option => option.id == responder.id) if (option && option._record) selects.push(option._record) emailTo.set('selected', selects) } function setResponder(id) { const ticket = getTicket() ticket && ticket.set('responderId', parseInt(id)) } function assignResponder() { const responder = getResponder() responder && updateResponder(responder.id) } function isTicketPath() { return getPath() === 'helpdesk.tickets.show' } function isTargetOf(name, target) { switch (name) { case 'addNote': // [add note] button return target.dataset.testEmailActionBtn === 'note' case 'sendNote': // [Add note] button / [Add note and set as xxx] button return target.id === 'send-and-set' || (target.getAttribute('class') || '').indexOf('send-and-set-item') >= 0 case 'noteAgent': // [Agent] field in note box return target.dataset.group === 'note-agent' default: return false } } // api async function updateResponder(id) { const formattedId = id && parseInt(id) || null return fetch(`/api/_/tickets/${getTicket().id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getMeta().csrf_token, }, body: JSON.stringify({ responder_id: formattedId }) }) .then(response => response.json()) .catch(console.error) } })()