1/** 2 * Freshdesk ticket plugin. Drag tickets into the diagram. Tickets are 3 * updated on file open, page select and via Extras, Update Tickets. 4 * 5 * Drag freshdesk tickets into the diagram. Domain must match deskDomain.freshdesk.com. 6 * 7 * Use #C to configure the client as follows: 8 * 9 * https://www.draw.io/?p=tickets#C%7B"ticketsConfig"%3A %7B"deskApiKey"%3A"YOUR_API_KEY"%2C"deskDomain"%3A"YOUR_DOMAIN"%7D%7D 10 * 11 * Use an additional "open" variable in the config JSON to open a file after parsing as follows: 12 * 13 * ...#_TICKETS%7B"ticketsConfig"%3A %7B"deskApiKey"%3A"YOUR_API_KEY"%2C"deskDomain"%3A"YOUR_DOMAIN"%7D%2C"open"%3A"ID_WITH_PREFIX"%7D 14 * 15 * Required JSON parameters: 16 * - deskApiKey=api_key (see user profile) 17 * - deskDomain=subdomain (subdomain.freshdesk.com) 18 * 19 * Optional JSON parameters: 20 * - deskStatus: Lookup for status codes (code => string) 21 * - deskTypes: Lookup for ticket types (string => story, task, subTask, feature, 22 * bug, techTask, epic, improvement, fault, change, access, purchase or itHelp) 23 * 24 * The current configuration is stored in localStorage under ".tickets-config". Use 25 * https://jgraph.github.io/drawio-tools/tools/convert.html for URI encoding. 26 */ 27Draw.loadPlugin(function(ui) 28{ 29 var config = null; 30 var deskDomain = null; 31 var deskApiKey = null; 32 var graph = ui.editor.graph; 33 34 var deskPriority = {'1': 'minor', '2': 'major', 35 '3': 'critical', '4': 'blocker'}; 36 var deskTypes = {'Question': 'story', 'Incident': 'techTask', 'Problem': 'fault', 37 'Feature Request': 'feature', 'Lead': 'purchase'}; 38 var deskStatus = {'2': 'Open', '3': 'Pending', '4': 'Resolved', '5': 'Closed', 39 '6': 'Waiting on Customer', '7': 'Waiting on Third Party', 40 '8': 'Resolved Internally'}; 41 var deskStatusWidth = {}; 42 43 function configure() 44 { 45 deskDomain = 'https://' + config.deskDomain + '.freshdesk.com'; 46 deskApiKey = config.deskApiKey; 47 48 deskTypes = config.deskTypes || deskTypes; 49 deskStatus = config.deskStatus || deskStatus; 50 deskStatusWidth = {}; 51 52 // Precomputes text widths for custom ticket status 53 var div = document.createElement('div'); 54 div.style.fontFamily = 'Arial,Helvetica'; 55 div.style.visibility = 'hidden'; 56 div.style.position = 'absolute'; 57 div.style.fontSize = '11px'; 58 59 document.body.appendChild(div); 60 61 for (var key in deskStatus) 62 { 63 div.innerHTML = ''; 64 mxUtils.write(div, deskStatus[key]); 65 deskStatusWidth[key] = div.clientWidth + 4; 66 } 67 68 document.body.removeChild(div); 69 }; 70 71 if (window.location.hash != null && window.location.hash.substring(0, 9) == '#_TICKETS') 72 { 73 try 74 { 75 var temp = JSON.parse(decodeURIComponent( 76 window.location.hash.substring(9))); 77 78 if (temp != null && temp.ticketsConfig != null) 79 { 80 config = temp.ticketsConfig; 81 configure(); 82 ui.fileLoaded(new LocalFile(ui, ui.emptyDiagramXml, this.defaultFilename, true)); 83 ui.editor.setStatus('Drag tickets from <a href="' + deskDomain + 84 '/a/tickets/filters/all_tickets" target="_blank">' + 85 deskDomain + '</a>'); 86 } 87 } 88 catch (e) 89 { 90 console.error(e); 91 } 92 } 93 94 function isDeskLink(link) 95 { 96 if (deskDomain != null) 97 { 98 var dl = deskDomain.length; 99 100 return config != null && link.substring(0, dl) == deskDomain && 101 (link.substring(dl, dl + 18) == '/helpdesk/tickets/' || 102 link.substring(dl, dl + 11) == '/a/tickets/'); 103 } 104 else 105 { 106 return false; 107 } 108 }; 109 110 function getIdForDeskLink(link) 111 { 112 return link.substring(link.lastIndexOf('/') + 1); 113 }; 114 115 function getDeskTicket(id, fn) 116 { 117 var xhr = new XMLHttpRequest(); 118 xhr.open('GET', deskDomain + '/api/v2/tickets/' + id); 119 xhr.setRequestHeader('Authorization', 'Basic ' + btoa(deskApiKey + ':x')); 120 121 xhr.onload = function () 122 { 123 if (xhr.status >= 200 && xhr.status <= 299) 124 { 125 fn(JSON.parse(xhr.responseText), xhr); 126 } 127 else 128 { 129 fn(null, xhr); 130 } 131 }; 132 133 xhr.onerror = function () 134 { 135 fn(null, xhr); 136 }; 137 138 xhr.send(); 139 }; 140 141 function updateStyle(cell, ticket) 142 { 143 var type = (ticket.type != null) ? deskTypes[ticket.type] : 'bug'; 144 var status = deskStatus[ticket.status] || 'Unknown'; 145 var priority = deskPriority[ticket.priority]; 146 var sw = deskStatusWidth[ticket.status]; 147 var prev = cell.style; 148 149 cell.style = mxUtils.setStyle(cell.style, 'issueType', type); 150 cell.style = mxUtils.setStyle(cell.style, 'issueStatus', status); 151 cell.style = mxUtils.setStyle(cell.style, 'issueStatusWidth', sw); 152 cell.style = mxUtils.setStyle(cell.style, 'issuePriority', priority); 153 154 return prev != cell.style; 155 }; 156 157 function shortString(s, max) 158 { 159 if (s.length > max) 160 { 161 return s.substring(0, max) + '...'; 162 } 163 else 164 { 165 return s; 166 } 167 } 168 169 function updateData(cell, ticket) 170 { 171 var changed = false; 172 173 function setAttr(key, value) 174 { 175 var prev = cell.value.getAttribute(key); 176 value = value || ''; 177 178 if (prev != value) 179 { 180 cell.value.setAttribute(key, value); 181 182 return true; 183 } 184 else 185 { 186 return false; 187 } 188 }; 189 190 changed = setAttr('abstract', shortString(ticket.description_text, 600)) | 191 setAttr('email_config_id', ticket.email_config_id) | 192 setAttr('requester_id', ticket.requester_id) | 193 setAttr('group_id', ticket.group_id) | 194 setAttr('created_at', ticket.created_at) | 195 setAttr('updated_at', ticket.updated_at) | 196 setAttr('due_by', ticket.due_by) | 197 setAttr('tags', ticket.tags.join(' ')); 198 199 for (var key in ticket.custom_fields) 200 { 201 changed = changed | setAttr(key, ticket.custom_fields[key]); 202 } 203 204 return changed; 205 }; 206 207 function updateTickets(spin) 208 { 209 if (config != null && (!spin || ui.spinner.spin(document.body, mxResources.get('loading') + '...'))) 210 { 211 var validate = false; 212 var pending = 0; 213 214 graph.view.states.visit(function(id, state) 215 { 216 var link = graph.getLinkForCell(state.cell); 217 218 if (link != null && isDeskLink(link)) 219 { 220 var id = getIdForDeskLink(link); 221 pending++; 222 223 getDeskTicket(id, function(ticket, req) 224 { 225 pending--; 226 227 if (ticket != null) 228 { 229 // Expression must execute both calls 230 if (updateStyle(state.cell, ticket) | 231 updateData(state.cell, ticket)) 232 { 233 graph.view.invalidate(state.cell, true, false); 234 state.style = null; 235 validate = true; 236 } 237 } 238 239 if (pending == 0) 240 { 241 if (spin) 242 { 243 ui.spinner.stop(); 244 } 245 246 if (validate) 247 { 248 graph.view.validate(); 249 } 250 } 251 }) 252 } 253 }); 254 255 if (spin && pending == 0) 256 { 257 ui.spinner.stop(); 258 } 259 } 260 }; 261 262 function getCellForLink(link) 263 { 264 for (var key in graph.view.states.map) 265 { 266 var cell = graph.view.states.map[key].cell; 267 268 if (link == graph.getLinkForCell(cell)) 269 { 270 return cell; 271 } 272 } 273 }; 274 275 // Adds resource for action 276 mxResources.parse('updateTickets=Update Tickets...'); 277 278 // Adds action 279 ui.actions.addAction('updateTickets', function() 280 { 281 updateTickets(true); 282 }); 283 284 // Updates tickets in opened files 285 ui.editor.addListener('fileLoaded', function() 286 { 287 updateTickets(false); 288 }); 289 290 // Updates tickets when page changes 291 ui.editor.addListener('pageSelected', function() 292 { 293 updateTickets(false); 294 }); 295 296 // Adds menu item 297 var menu = ui.menus.get('extras'); 298 var oldFunct = menu.funct; 299 300 menu.funct = function(menu, parent) 301 { 302 oldFunct.apply(this, arguments); 303 304 ui.menus.addMenuItems(menu, ['-', 'updateTickets'], parent); 305 }; 306 307 // Intercepts ticket URLs 308 var uiInsertTextAt = ui.insertTextAt; 309 310 ui.insertTextAt = function(text, dx, dy, html, asImage, crop, resizeImages) 311 { 312 if (isDeskLink(text)) 313 { 314 var cell = getCellForLink(text); 315 316 if (cell != null) 317 { 318 // Selects existing ticket with same link 319 graph.setSelectionCell(cell); 320 graph.scrollCellToVisible(graph.getSelectionCell()); 321 } 322 else if (ui.spinner.spin(document.body, mxResources.get('loading') + '...')) 323 { 324 // Creates new shape 325 var id = getIdForDeskLink(text); 326 327 getDeskTicket(id, function(ticket, req) 328 { 329 ui.spinner.stop(); 330 331 if (ticket != null) 332 { 333 var cell = null; 334 335 graph.getModel().beginUpdate(); 336 try 337 { 338 cell = graph.insertVertex(graph.getDefaultParent(), null, 339 '%title%\n\n<b>Updated:</b> %updated_at% ' + 340 '(<a href="' + deskDomain + '/contacts/%requester_id%">From</a>)', 341 graph.snap(dx), graph.snap(dy), 200, 50, 342 'html=1;whiteSpace=wrap;overflow=hidden;shape=mxgraph.atlassian.issue;' + 343 'fontSize=12;verticalAlign=top;align=left;spacingTop=25;' + 344 'strokeColor=#A8ADB0;fillColor=#EEEEEE;backgroundOutline=1;'); 345 346 graph.setLinkForCell(cell, text); 347 cell.value.setAttribute('title', shortString(ticket.subject, 40)); 348 cell.value.setAttribute('subject', ticket.subject); 349 cell.value.setAttribute('placeholders', '1'); 350 cell.value.setAttribute('ticket_id', id); 351 updateData(cell, ticket); 352 updateStyle(cell, ticket); 353 354 // Adds ticket ID label 355 var label1 = new mxCell('%ticket_id%', new mxGeometry(0, 0, 60, 20), 356 'strokeColor=none;fillColor=none;part=1;resizable=0;align=left;' + 357 'autosize=1;points=[];deletable=0;editable=0;connectable=0;'); 358 graph.setAttributeForCell(label1, 'placeholders', '1'); 359 label1.geometry.relative = true; 360 label1.geometry.offset = new mxPoint(20, 0); 361 label1.vertex = true; 362 cell.insert(label1); 363 364 graph.updateCellSize(cell); 365 cell.geometry.width = Math.max(220, cell.geometry.width); 366 cell.geometry.height += 10; 367 } 368 finally 369 { 370 graph.getModel().endUpdate(); 371 } 372 373 graph.setSelectionCell(cell); 374 } 375 else 376 { 377 var err = req.status 378 379 try 380 { 381 err = JSON.parse(req.responseText); 382 } 383 catch (e) 384 { 385 // ignore 386 } 387 388 ui.handleError({message: err.message}); 389 } 390 }); 391 } 392 393 return null; 394 } 395 else 396 { 397 return uiInsertTextAt.apply(this, arguments); 398 } 399 }; 400}); 401