TrelloClient.js 16 KB

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