TrelloClient.js 16 KB

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