1/** 2 * Copyright (c) 2006-2017, JGraph Ltd 3 * Copyright (c) 2006-2017, Gaudenz Alder 4 */ 5TrelloClient = function(editorUi) 6{ 7 DrawioClient.call(this, editorUi, 'tauth'); 8 Trello.setKey(this.key); 9}; 10 11// Extends DrawioClient 12mxUtils.extend(TrelloClient, DrawioClient); 13 14TrelloClient.prototype.key = (window.location.hostname == 'test.draw.io') ? 15 'e73615c79cf7e381aef91c85936e9553' : 'e73615c79cf7e381aef91c85936e9553'; 16 17TrelloClient.prototype.baseUrl = 'https://api.trello.com/1/'; 18 19TrelloClient.prototype.SEPARATOR = '|$|'; 20 21/** 22 * Maximum attachment size of Trello. 23 */ 24TrelloClient.prototype.maxFileSize = 10000000 /*10MB*/; 25 26/** 27 * Default extension for new files. 28 */ 29TrelloClient.prototype.extension = '.xml'; //TODO export to png 30 31/** 32 * Authorizes the client, used with methods that can be called without a user click and popup blockers will interfere 33 * Show the AuthDialog to work around the popup blockers if the file is opened directly 34 */ 35TrelloClient.prototype.authenticate = function(fn, error, force) 36{ 37 if (force) 38 { 39 this.logout(); 40 } 41 42 var callback = mxUtils.bind(this, function(remember, success) 43 { 44 Trello.authorize( 45 { 46 type: 'popup', 47 name: 'draw.io', 48 scope: 49 { 50 read: 'true', 51 write: 'true' 52 }, 53 expiration: remember ? 'never' : '1hour', 54 success: function() 55 { 56 if (success != null) 57 { 58 success(); 59 } 60 61 fn(); 62 }, 63 error: function() 64 { 65 if (success != null) 66 { 67 success(); 68 } 69 70 if (error != null) 71 { 72 error(mxResources.get('loggedOut')); 73 } 74 } 75 }); 76 }); 77 78 if (this.isAuthorized()) 79 { 80 callback(true); 81 } 82 else 83 { 84 this.ui.showAuthDialog(this, true, callback); 85 } 86} 87 88/** 89 * 90 */ 91TrelloClient.prototype.getLibrary = function(id, success, error) 92{ 93 this.getFile(id, success, error, false, true); 94}; 95 96/** 97 * 98 */ 99TrelloClient.prototype.getFile = function(id, success, error, denyConvert, asLibrary) 100{ 101 //In getFile only, we 102 asLibrary = (asLibrary != null) ? asLibrary : false; 103 104 var callback = mxUtils.bind(this, function() 105 { 106 var ids = id.split(this.SEPARATOR); 107 var acceptResponse = true; 108 109 var timeoutThread = window.setTimeout(mxUtils.bind(this, function() 110 { 111 acceptResponse = false; 112 error({code: App.ERROR_TIMEOUT, retry: callback}) 113 }), this.ui.timeout); 114 115 Trello.cards.get(ids[0] + '/attachments/' + ids[1], mxUtils.bind(this, function(meta) 116 { 117 window.clearTimeout(timeoutThread); 118 119 if (acceptResponse) 120 { 121 var binary = /\.png$/i.test(meta.name); 122 var headers = { 123 Authorization: 'OAuth oauth_consumer_key="' + Trello.key() + '", oauth_token="' + Trello.token() + '"' 124 }; 125 126 // TODO Trello doesn't allow CORS requests to load attachments. Confirm that 127 // and make sure that only a proxy technique can work! 128 // Handles .vsdx, Gliffy and PNG+XML files by creating a temporary file 129 if (/\.v(dx|sdx?)$/i.test(meta.name) || /\.gliffy$/i.test(meta.name) || 130 (!this.ui.useCanvasForExport && binary)) 131 { 132 this.ui.convertFile(PROXY_URL + '?url=' + encodeURIComponent(meta.url), meta.name, meta.mimeType, 133 this.extension, success, error, null, headers); 134 } 135 else 136 { 137 acceptResponse = true; 138 139 timeoutThread = window.setTimeout(mxUtils.bind(this, function() 140 { 141 acceptResponse = false; 142 error({code: App.ERROR_TIMEOUT}) 143 }), this.ui.timeout); 144 145 this.ui.editor.loadUrl(PROXY_URL + '?url=' + encodeURIComponent(meta.url), mxUtils.bind(this, function(data) 146 { 147 window.clearTimeout(timeoutThread); 148 149 if (acceptResponse) 150 { 151 //keep our id which includes the cardId 152 meta.compoundId = id; 153 154 var index = (binary) ? data.lastIndexOf(',') : -1; 155 156 if (index > 0) 157 { 158 var xml = this.ui.extractGraphModelFromPng(data); 159 160 if (xml != null && xml.length > 0) 161 { 162 data = xml; 163 } 164 else 165 { 166 // TODO: Import PNG 167 } 168 } 169 170 if (asLibrary) 171 { 172 success(new TrelloLibrary(this.ui, data, meta)); 173 } 174 else 175 { 176 success(new TrelloFile(this.ui, data, meta)); 177 } 178 } 179 }), mxUtils.bind(this, function(err, req) 180 { 181 window.clearTimeout(timeoutThread); 182 183 if (acceptResponse) 184 { 185 if (req.status == 401) 186 { 187 this.authenticate(callback, error, true); 188 } 189 else 190 { 191 error(); 192 } 193 } 194 }), binary || (meta.mimeType != null && 195 meta.mimeType.substring(0, 6) == 'image/'), null, null, null, headers); 196 } 197 } 198 }), mxUtils.bind(this, function(err) 199 { 200 window.clearTimeout(timeoutThread); 201 202 if (acceptResponse) 203 { 204 if (err != null && err.status == 401) 205 { 206 this.authenticate(callback, error, true); 207 } 208 else 209 { 210 error(); 211 } 212 } 213 })); 214 }); 215 216 this.authenticate(callback, error); 217}; 218 219/** 220 * 221 */ 222TrelloClient.prototype.insertLibrary = function(filename, data, success, error, cardId) 223{ 224 this.insertFile(filename, data, success, error, true, cardId); 225}; 226 227/** 228 * 229 */ 230TrelloClient.prototype.insertFile = function(filename, data, success, error, asLibrary, cardId) 231{ 232 asLibrary = (asLibrary != null) ? asLibrary : false; 233 234 var callback = mxUtils.bind(this, function() 235 { 236 var fn = mxUtils.bind(this, function(fileData) 237 { 238 this.writeFile(filename, fileData, cardId, mxUtils.bind(this, function(meta) 239 { 240 if (asLibrary) 241 { 242 success(new TrelloLibrary(this.ui, data, meta)); 243 } 244 else 245 { 246 success(new TrelloFile(this.ui, data, meta)); 247 } 248 }), error); 249 }); 250 251 if (this.ui.useCanvasForExport && /(\.png)$/i.test(filename)) 252 { 253 this.ui.getEmbeddedPng(mxUtils.bind(this, function(pngData) 254 { 255 fn(this.ui.base64ToBlob(pngData, 'image/png')); 256 }), error, data); 257 } 258 else 259 { 260 fn(data); 261 } 262 }); 263 264 this.authenticate(callback, error); 265}; 266 267/** 268 * 269 */ 270TrelloClient.prototype.saveFile = function(file, success, error) 271{ 272 // write the file first (with the same name), then delete the old file 273 // so that nothing is lost if something goes wrong with deleting 274 var ids = file.meta.compoundId.split(this.SEPARATOR); 275 276 var fn = mxUtils.bind(this, function(data) 277 { 278 this.writeFile(file.meta.name, data, ids[0], function(meta) 279 { 280 Trello.del('cards/' + ids[0] + '/attachments/' + ids[1], mxUtils.bind(this, function() 281 { 282 success(meta); 283 }), mxUtils.bind(this, function(err) 284 { 285 if (err != null && err.status == 401) 286 { 287 // KNOWN: Does not wait for popup to close for callback 288 this.authenticate(callback, error, true); 289 } 290 else 291 { 292 error(); 293 } 294 })); 295 }, error); 296 }); 297 298 var callback = mxUtils.bind(this, function() 299 { 300 if (this.ui.useCanvasForExport && /(\.png)$/i.test(file.meta.name)) 301 { 302 this.ui.getEmbeddedPng(mxUtils.bind(this, function(data) 303 { 304 fn(this.ui.base64ToBlob(data, 'image/png')); 305 }), error, (this.ui.getCurrentFile() != file) ? file.getData() : null); 306 } 307 else 308 { 309 fn(file.getData()); 310 } 311 }); 312 313 this.authenticate(callback, error); 314}; 315 316/** 317 * 318 */ 319TrelloClient.prototype.writeFile = function(filename, data, cardId, success, error) 320{ 321 if (filename != null && data != null) 322 { 323 if (data.length >= this.maxFileSize) 324 { 325 error({message: mxResources.get('drawingTooLarge') + ' (' + 326 this.ui.formatFileSize(data.length) + ' / 10 MB)'}); 327 328 return; 329 } 330 331 var fn = mxUtils.bind(this, function() 332 { 333 var acceptResponse = true; 334 335 var timeoutThread = window.setTimeout(mxUtils.bind(this, function() 336 { 337 acceptResponse = false; 338 error({code: App.ERROR_TIMEOUT, retry: fn}); 339 }), this.ui.timeout); 340 341 var formData = new FormData(); 342 formData.append('key', Trello.key()); 343 formData.append('token', Trello.token()); 344 formData.append('file', typeof data === 'string' ? new Blob([data]) : data, filename); 345 formData.append('name', filename); 346 347 var request = new XMLHttpRequest(); 348 request.responseType = 'json'; 349 350 request.onreadystatechange = mxUtils.bind(this, function() 351 { 352 if (request.readyState === 4) 353 { 354 window.clearTimeout(timeoutThread); 355 356 if (acceptResponse) 357 { 358 if (request.status == 200) 359 { 360 var fileMeta = request.response; 361 fileMeta.compoundId = cardId + this.SEPARATOR + fileMeta.id 362 success(fileMeta); 363 } 364 else if (request.status == 401) 365 { 366 this.authenticate(fn, error, true); 367 } 368 else 369 { 370 error(); 371 } 372 } 373 } 374 }); 375 376 request.open('POST', this.baseUrl + 'cards/' + cardId + '/attachments'); 377 request.send(formData); 378 }); 379 380 this.authenticate(fn, error); 381 } 382 else 383 { 384 error({message: mxResources.get('unknownError')}); 385 } 386}; 387 388/** 389 * Checks if the client is authorized and calls the next step. 390 */ 391TrelloClient.prototype.pickLibrary = function(fn) 392{ 393 this.pickFile(fn); 394}; 395 396/** 397 * 398 */ 399TrelloClient.prototype.pickFolder = function(fn) 400{ 401 this.authenticate(mxUtils.bind(this, function() 402 { 403 // show file select 404 this.showTrelloDialog(false, fn); 405 }), mxUtils.bind(this, function(e) 406 { 407 this.ui.showError(mxResources.get('error'), e); 408 })); 409}; 410 411/** 412 * Checks if the client is authorized and calls the next step. 413 */ 414TrelloClient.prototype.pickFile = function(fn, returnObject) 415{ 416 fn = (fn != null) ? fn : mxUtils.bind(this, function(id) 417 { 418 this.ui.loadFile('T' + encodeURIComponent(id)); 419 }); 420 421 this.authenticate(mxUtils.bind(this, function() 422 { 423 // show file select 424 this.showTrelloDialog(true, fn); 425 }), mxUtils.bind(this, function(e) 426 { 427 this.ui.showError(mxResources.get('error'), e, mxResources.get('ok')); 428 })); 429}; 430 431 432/** 433 * 434 */ 435TrelloClient.prototype.showTrelloDialog = function(showFiles, fn) 436{ 437 var cardId = null; 438 var filter = '@me'; 439 var linkCounter = 0; 440 441 var content = document.createElement('div'); 442 content.style.whiteSpace = 'nowrap'; 443 content.style.overflow = 'hidden'; 444 content.style.height = '224px'; 445 446 var hd = document.createElement('h3'); 447 mxUtils.write(hd, showFiles? mxResources.get('selectFile') : mxResources.get('selectCard')); 448 hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px'; 449 content.appendChild(hd); 450 451 var div = document.createElement('div'); 452 div.style.whiteSpace = 'nowrap'; 453 div.style.overflow = 'auto'; 454 div.style.height = '194px'; 455 content.appendChild(div); 456 457 var dlg = new CustomDialog(this.ui, content); 458 this.ui.showDialog(dlg.container, 340, 290, true, true); 459 460 dlg.okButton.parentNode.removeChild(dlg.okButton); 461 462 var createLink = mxUtils.bind(this, function(label, fn, preview) 463 { 464 linkCounter++; 465 var div = document.createElement('div'); 466 div.style = 'width:100%;text-overflow:ellipsis;overflow:hidden;vertical-align:middle;' + 467 'padding:2px 0 2px 0;background:' + (linkCounter % 2 == 0? 468 ((Editor.isDarkMode()) ? '#000' : '#eee') : 469 ((Editor.isDarkMode()) ? '' : '#fff')); 470 var link = document.createElement('a'); 471 link.style.cursor = 'pointer'; 472 473 if (preview != null) 474 { 475 var img = document.createElement('img'); 476 img.src = preview.url; 477 img.width = preview.width; 478 img.height= preview.height; 479 img.style= "border: 1px solid black;margin:5px;vertical-align:middle" 480 link.appendChild(img); 481 } 482 483 mxUtils.write(link, label); 484 mxEvent.addListener(link, 'click', fn); 485 486 div.appendChild(link); 487 488 return div; 489 }); 490 491 var error = mxUtils.bind(this, function(err) 492 { 493 this.ui.handleError(err, null, mxUtils.bind(this, function() 494 { 495 this.ui.spinner.stop(); 496 this.ui.hideDialog(); 497 })); 498 }); 499 500 var selectAtt = mxUtils.bind(this, function() 501 { 502 linkCounter = 0; 503 div.innerHTML = ''; 504 this.ui.spinner.spin(div, mxResources.get('loading')); 505 506 var callback = mxUtils.bind(this, function() 507 { 508 Trello.cards.get(cardId + '/attachments', {fields: 'id,name,previews'}, mxUtils.bind(this, function(data) 509 { 510 this.ui.spinner.stop(); 511 var files = data; 512 div.appendChild(createLink('../ [Up]', mxUtils.bind(this, function() 513 { 514 selectCard(); 515 }))); 516 mxUtils.br(div); 517 518 if (files == null || files.length == 0) 519 { 520 mxUtils.write(div, mxResources.get('noFiles')); 521 } 522 else 523 { 524 var listFiles = mxUtils.bind(this, function() 525 { 526 for (var i = 0; i < files.length; i++) 527 { 528 (mxUtils.bind(this, function(file) 529 { 530 div.appendChild(createLink(file.name, mxUtils.bind(this, function() 531 { 532 this.ui.hideDialog(); 533 fn(cardId + this.SEPARATOR + file.id); 534 }), file.previews != null? file.previews[0] : null)); 535 }))(files[i]); 536 } 537 }); 538 539 listFiles(); 540 } 541 }), 542 mxUtils.bind(this, function(req) 543 { 544 if (req.status == 401) 545 { 546 this.authenticate(callback, error, true); 547 } 548 else if (error != null) 549 { 550 error(req); 551 } 552 })); 553 }); 554 555 callback(); 556 }); 557 558 // Adds paging for cards (files limited to 1000 by API) 559 var pageSize = 100; 560 var nextPageDiv = null; 561 var scrollFn = null; 562 563 var selectCard = mxUtils.bind(this, function(page) 564 { 565 if (page == null) 566 { 567 linkCounter = 0; 568 div.innerHTML = ''; 569 page = 1; 570 } 571 572 this.ui.spinner.spin(div, mxResources.get('loading')); 573 574 if (nextPageDiv != null && nextPageDiv.parentNode != null) 575 { 576 nextPageDiv.parentNode.removeChild(nextPageDiv); 577 } 578 579 nextPageDiv = document.createElement('a'); 580 nextPageDiv.style.display = 'block'; 581 nextPageDiv.style.cursor = 'pointer'; 582 mxUtils.write(nextPageDiv, mxResources.get('more') + '...'); 583 584 var nextPage = mxUtils.bind(this, function() 585 { 586 mxEvent.removeListener(div, 'scroll', scrollFn); 587 selectCard(page + 1); 588 }); 589 590 mxEvent.addListener(nextPageDiv, 'click', nextPage); 591 592 var callback = mxUtils.bind(this, function() 593 { 594 Trello.get('search', { 595 'query': (mxUtils.trim(filter) == '') ? 'is:open' : filter, 596 'cards_limit': pageSize, 597 'cards_page': page-1 598 }, 599 mxUtils.bind(this, function(data) 600 { 601 this.ui.spinner.stop(); 602 var cards = (data != null) ? data.cards : null; 603 604 if (cards == null || cards.length == 0) 605 { 606 mxUtils.write(div, mxResources.get('noFiles')); 607 } 608 else 609 { 610 if (page == 1) 611 { 612 div.appendChild(createLink(mxResources.get('filterCards') + '...', mxUtils.bind(this, function() 613 { 614 var dlg = new FilenameDialog(this.ui, filter, mxResources.get('ok'), mxUtils.bind(this, function(value) 615 { 616 if (value != null) 617 { 618 filter = value; 619 selectCard(); 620 } 621 }), mxResources.get('filterCards'), null, null, 'http://help.trello.com/article/808-searching-for-cards-all-boards'); 622 this.ui.showDialog(dlg.container, 300, 80, true, false); 623 dlg.init(); 624 }))); 625 626 mxUtils.br(div); 627 } 628 629 for (var i = 0; i < cards.length; i++) 630 { 631 (mxUtils.bind(this, function(card) 632 { 633 div.appendChild(createLink(card.name, mxUtils.bind(this, function() 634 { 635 if (showFiles) 636 { 637 cardId = card.id; 638 selectAtt(); 639 } 640 else 641 { 642 this.ui.hideDialog(); 643 fn(card.id); 644 } 645 }))); 646 }))(cards[i]); 647 } 648 649 if (cards.length == pageSize) 650 { 651 div.appendChild(nextPageDiv); 652 653 scrollFn = function() 654 { 655 if (div.scrollTop >= div.scrollHeight - div.offsetHeight) 656 { 657 nextPage(); 658 } 659 }; 660 661 mxEvent.addListener(div, 'scroll', scrollFn); 662 } 663 } 664 }), 665 mxUtils.bind(this, function(req) 666 { 667 if (req.status == 401) 668 { 669 this.authenticate(callback, error, true); 670 } 671 else if (error != null) 672 { 673 error({message: req.responseText}); 674 } 675 })); 676 }); 677 678 callback(); 679 }); 680 681 selectCard(); 682}; 683 684/** 685 * Checks if the client is authorized 686 */ 687TrelloClient.prototype.isAuthorized = function() 688{ 689 //TODO this may break if Trello client.js is changed 690 try 691 { 692 return localStorage['trello_token'] != null; //Trello.authorized(); doesn't work unless authorize is called first 693 } 694 catch (e) 695 { 696 // ignores access denied 697 } 698 699 return false; 700}; 701 702 703/** 704 * Logout and deauthorize the user. 705 */ 706TrelloClient.prototype.logout = function() 707{ 708 localStorage.removeItem('trello_token'); 709 Trello.deauthorize(); 710}; 711