/** * Freshdesk ticket plugin. Drag tickets into the diagram. Tickets are * updated on file open, page select and via Extras, Update Tickets. * * Drag freshdesk tickets into the diagram. Domain must match deskDomain.freshdesk.com. * * Use #C to configure the client as follows: * * https://www.draw.io/?p=tickets#C%7B"ticketsConfig"%3A %7B"deskApiKey"%3A"YOUR_API_KEY"%2C"deskDomain"%3A"YOUR_DOMAIN"%7D%7D * * Use an additional "open" variable in the config JSON to open a file after parsing as follows: * * ...#_TICKETS%7B"ticketsConfig"%3A %7B"deskApiKey"%3A"YOUR_API_KEY"%2C"deskDomain"%3A"YOUR_DOMAIN"%7D%2C"open"%3A"ID_WITH_PREFIX"%7D * * Required JSON parameters: * - deskApiKey=api_key (see user profile) * - deskDomain=subdomain (subdomain.freshdesk.com) * * Optional JSON parameters: * - deskStatus: Lookup for status codes (code => string) * - deskTypes: Lookup for ticket types (string => story, task, subTask, feature, * bug, techTask, epic, improvement, fault, change, access, purchase or itHelp) * * The current configuration is stored in localStorage under ".tickets-config". Use * https://jgraph.github.io/drawio-tools/tools/convert.html for URI encoding. */ Draw.loadPlugin(function(ui) { var config = null; var deskDomain = null; var deskApiKey = null; var graph = ui.editor.graph; var deskPriority = {'1': 'minor', '2': 'major', '3': 'critical', '4': 'blocker'}; var deskTypes = {'Question': 'story', 'Incident': 'techTask', 'Problem': 'fault', 'Feature Request': 'feature', 'Lead': 'purchase'}; var deskStatus = {'2': 'Open', '3': 'Pending', '4': 'Resolved', '5': 'Closed', '6': 'Waiting on Customer', '7': 'Waiting on Third Party', '8': 'Resolved Internally'}; var deskStatusWidth = {}; function configure() { deskDomain = 'https://' + config.deskDomain + '.freshdesk.com'; deskApiKey = config.deskApiKey; deskTypes = config.deskTypes || deskTypes; deskStatus = config.deskStatus || deskStatus; deskStatusWidth = {}; // Precomputes text widths for custom ticket status var div = document.createElement('div'); div.style.fontFamily = 'Arial,Helvetica'; div.style.visibility = 'hidden'; div.style.position = 'absolute'; div.style.fontSize = '11px'; document.body.appendChild(div); for (var key in deskStatus) { div.innerHTML = ''; mxUtils.write(div, deskStatus[key]); deskStatusWidth[key] = div.clientWidth + 4; } document.body.removeChild(div); }; if (window.location.hash != null && window.location.hash.substring(0, 9) == '#_TICKETS') { try { var temp = JSON.parse(decodeURIComponent( window.location.hash.substring(9))); if (temp != null && temp.ticketsConfig != null) { config = temp.ticketsConfig; configure(); ui.fileLoaded(new LocalFile(ui, ui.emptyDiagramXml, this.defaultFilename, true)); ui.editor.setStatus('Drag tickets from ' + deskDomain + ''); } } catch (e) { console.error(e); } } function isDeskLink(link) { if (deskDomain != null) { var dl = deskDomain.length; return config != null && link.substring(0, dl) == deskDomain && (link.substring(dl, dl + 18) == '/helpdesk/tickets/' || link.substring(dl, dl + 11) == '/a/tickets/'); } else { return false; } }; function getIdForDeskLink(link) { return link.substring(link.lastIndexOf('/') + 1); }; function getDeskTicket(id, fn) { var xhr = new XMLHttpRequest(); xhr.open('GET', deskDomain + '/api/v2/tickets/' + id); xhr.setRequestHeader('Authorization', 'Basic ' + btoa(deskApiKey + ':x')); xhr.onload = function () { if (xhr.status >= 200 && xhr.status <= 299) { fn(JSON.parse(xhr.responseText), xhr); } else { fn(null, xhr); } }; xhr.onerror = function () { fn(null, xhr); }; xhr.send(); }; function updateStyle(cell, ticket) { var type = (ticket.type != null) ? deskTypes[ticket.type] : 'bug'; var status = deskStatus[ticket.status] || 'Unknown'; var priority = deskPriority[ticket.priority]; var sw = deskStatusWidth[ticket.status]; var prev = cell.style; cell.style = mxUtils.setStyle(cell.style, 'issueType', type); cell.style = mxUtils.setStyle(cell.style, 'issueStatus', status); cell.style = mxUtils.setStyle(cell.style, 'issueStatusWidth', sw); cell.style = mxUtils.setStyle(cell.style, 'issuePriority', priority); return prev != cell.style; }; function shortString(s, max) { if (s.length > max) { return s.substring(0, max) + '...'; } else { return s; } } function updateData(cell, ticket) { var changed = false; function setAttr(key, value) { var prev = cell.value.getAttribute(key); value = value || ''; if (prev != value) { cell.value.setAttribute(key, value); return true; } else { return false; } }; changed = setAttr('abstract', shortString(ticket.description_text, 600)) | setAttr('email_config_id', ticket.email_config_id) | setAttr('requester_id', ticket.requester_id) | setAttr('group_id', ticket.group_id) | setAttr('created_at', ticket.created_at) | setAttr('updated_at', ticket.updated_at) | setAttr('due_by', ticket.due_by) | setAttr('tags', ticket.tags.join(' ')); for (var key in ticket.custom_fields) { changed = changed | setAttr(key, ticket.custom_fields[key]); } return changed; }; function updateTickets(spin) { if (config != null && (!spin || ui.spinner.spin(document.body, mxResources.get('loading') + '...'))) { var validate = false; var pending = 0; graph.view.states.visit(function(id, state) { var link = graph.getLinkForCell(state.cell); if (link != null && isDeskLink(link)) { var id = getIdForDeskLink(link); pending++; getDeskTicket(id, function(ticket, req) { pending--; if (ticket != null) { // Expression must execute both calls if (updateStyle(state.cell, ticket) | updateData(state.cell, ticket)) { graph.view.invalidate(state.cell, true, false); state.style = null; validate = true; } } if (pending == 0) { if (spin) { ui.spinner.stop(); } if (validate) { graph.view.validate(); } } }) } }); if (spin && pending == 0) { ui.spinner.stop(); } } }; function getCellForLink(link) { for (var key in graph.view.states.map) { var cell = graph.view.states.map[key].cell; if (link == graph.getLinkForCell(cell)) { return cell; } } }; // Adds resource for action mxResources.parse('updateTickets=Update Tickets...'); // Adds action ui.actions.addAction('updateTickets', function() { updateTickets(true); }); // Updates tickets in opened files ui.editor.addListener('fileLoaded', function() { updateTickets(false); }); // Updates tickets when page changes ui.editor.addListener('pageSelected', function() { updateTickets(false); }); // Adds menu item var menu = ui.menus.get('extras'); var oldFunct = menu.funct; menu.funct = function(menu, parent) { oldFunct.apply(this, arguments); ui.menus.addMenuItems(menu, ['-', 'updateTickets'], parent); }; // Intercepts ticket URLs var uiInsertTextAt = ui.insertTextAt; ui.insertTextAt = function(text, dx, dy, html, asImage, crop, resizeImages) { if (isDeskLink(text)) { var cell = getCellForLink(text); if (cell != null) { // Selects existing ticket with same link graph.setSelectionCell(cell); graph.scrollCellToVisible(graph.getSelectionCell()); } else if (ui.spinner.spin(document.body, mxResources.get('loading') + '...')) { // Creates new shape var id = getIdForDeskLink(text); getDeskTicket(id, function(ticket, req) { ui.spinner.stop(); if (ticket != null) { var cell = null; graph.getModel().beginUpdate(); try { cell = graph.insertVertex(graph.getDefaultParent(), null, '%title%\n\nUpdated: %updated_at% ' + '(From)', graph.snap(dx), graph.snap(dy), 200, 50, 'html=1;whiteSpace=wrap;overflow=hidden;shape=mxgraph.atlassian.issue;' + 'fontSize=12;verticalAlign=top;align=left;spacingTop=25;' + 'strokeColor=#A8ADB0;fillColor=#EEEEEE;backgroundOutline=1;'); graph.setLinkForCell(cell, text); cell.value.setAttribute('title', shortString(ticket.subject, 40)); cell.value.setAttribute('subject', ticket.subject); cell.value.setAttribute('placeholders', '1'); cell.value.setAttribute('ticket_id', id); updateData(cell, ticket); updateStyle(cell, ticket); // Adds ticket ID label var label1 = new mxCell('%ticket_id%', new mxGeometry(0, 0, 60, 20), 'strokeColor=none;fillColor=none;part=1;resizable=0;align=left;' + 'autosize=1;points=[];deletable=0;editable=0;connectable=0;'); graph.setAttributeForCell(label1, 'placeholders', '1'); label1.geometry.relative = true; label1.geometry.offset = new mxPoint(20, 0); label1.vertex = true; cell.insert(label1); graph.updateCellSize(cell); cell.geometry.width = Math.max(220, cell.geometry.width); cell.geometry.height += 10; } finally { graph.getModel().endUpdate(); } graph.setSelectionCell(cell); } else { var err = req.status try { err = JSON.parse(req.responseText); } catch (e) { // ignore } ui.handleError({message: err.message}); } }); } return null; } else { return uiInsertTextAt.apply(this, arguments); } }; });