TrelloClient.js 16 KB

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