1/** 2 * Copyright (c) 2006-2021, JGraph Ltd 3 * Copyright (c) 2006-2021, draw.io AG 4 */ 5 6//Add a closure to hide the class private variables without changing the code a lot 7(function () 8{ 9 10var _token = null; 11 12window.NotionClient = function(editorUi) 13{ 14 DrawioClient.call(this, editorUi, 'notionAuthInfo'); 15}; 16 17// Extends DrawioClient 18mxUtils.extend(NotionClient, DrawioClient); 19 20/** 21 * 22 */ 23NotionClient.prototype.extension = '.drawio'; 24 25NotionClient.prototype.xmlField = 'draw.io XML'; 26 27/** 28 * 29 */ 30NotionClient.prototype.baseUrl = window.NOTION_API_URL || 'https://app.diagrams.net/notion-api'; 31 32 33NotionClient.prototype.getTitle = function (props) 34{ 35 var obj, key; 36 37 for (var field in props) 38 { 39 if (props[field].type == 'title') 40 { 41 key = field; 42 obj = props[field]; 43 break; 44 } 45 } 46 47 return {title: this.getTitleVal(obj), key: key}; 48}; 49 50NotionClient.prototype.getTitleVal = function (obj) 51{ 52 if (typeof obj.title === 'string') 53 { 54 return obj.title; 55 } 56 else 57 { 58 var title = []; 59 60 for (var i = 0; i < obj.title.length; i++) 61 { 62 title.push(obj.title[i].text.content) 63 } 64 65 return title.join(' '); 66 } 67}; 68 69NotionClient.prototype.authenticate = function(success, error, failOnAuth) 70{ 71 var acceptAuthResponse = true; 72 73 var errFn = mxUtils.bind(this, function() 74 { 75 if (acceptAuthResponse) 76 { 77 acceptAuthResponse = false; 78 error({message: mxResources.get('accessDenied'), retry: mxUtils.bind(this, function() 79 { 80 this.ui.hideDialog(); 81 auth(); 82 })}); 83 } 84 }); 85 86 var auth = mxUtils.bind(this, function() 87 { 88 acceptAuthResponse = true; 89 90 this.ui.showAuthDialog(this, true, mxUtils.bind(this, function(remember, authSuccess) 91 { 92 var tokenDlg = new FilenameDialog(this.ui, '', 93 mxResources.get('ok'), mxUtils.bind(this, function(token) 94 { 95 //check token is valid 96 _token = token; 97 98 //TODO use any simpler request if one becomes available 99 this.executeRequest('/v1/databases', null, 'GET', mxUtils.bind(this, function() 100 { 101 this.executeRequest('/setToken', null, 'GET', mxUtils.bind(this, function() 102 { 103 acceptAuthResponse = false; 104 105 if (remember) 106 { 107 _token = null; 108 } 109 110 if (authSuccess != null) 111 { 112 authSuccess(); 113 } 114 115 success(); 116 }), errFn, failOnAuth); 117 }), errFn, failOnAuth); 118 }), mxResources.get('notionToken'), function(token) 119 { 120 return token != null && token.length > 0; 121 }, null, 'https://developers.notion.com/docs/getting-started#step-1-create-an-integration'); 122 123 this.ui.showDialog(tokenDlg.container, 300, 80, true, true); 124 tokenDlg.init(); 125 }), errFn); 126 }); 127 128 auth(); 129}; 130 131/** 132 * Checks if the client is authorized and calls the next step. 133 */ 134NotionClient.prototype.executeRequest = function(url, data, method, success, error, failOnAuth) 135{ 136 var doExecute = mxUtils.bind(this, function() 137 { 138 var acceptResponse = true; 139 140 var timeoutThread = window.setTimeout(mxUtils.bind(this, function() 141 { 142 acceptResponse = false; 143 error({code: App.ERROR_TIMEOUT, retry: doExecute}); 144 }), this.ui.timeout); 145 146 var req = new mxXmlRequest(this.baseUrl + url, data, method); 147 148 req.withCredentials = true; 149 150 req.setRequestHeaders = mxUtils.bind(this, function(request, params) 151 { 152 if (_token != null) 153 { 154 request.setRequestHeader('Authorization', 'Bearer ' + _token); 155 } 156 157 request.setRequestHeader('Notion-Version', '2021-05-13'); 158 request.setRequestHeader('Content-Type', 'application/json'); 159 }); 160 161 req.send(mxUtils.bind(this, function(req) 162 { 163 window.clearTimeout(timeoutThread); 164 165 if (acceptResponse) 166 { 167 // 404 (file not found) is a valid response for checkExists 168 if (req.getStatus() >= 200 && req.getStatus() <= 299) 169 { 170 if (this.user == null) 171 { 172 this.setUser(new DrawioUser('notion', null, 'Notion')); 173 } 174 175 success(JSON.parse(req.getText())); 176 } 177 else if (!failOnAuth && (req.getStatus() === 401 || req.getStatus() === 400)) 178 { 179 this.setUser(null); 180 failOnAuth = true; 181 //Authorize again using the refresh token 182 this.authenticate(function() 183 { 184 doExecute(); 185 }, error, failOnAuth); 186 } 187 else 188 { 189 error(this.parseRequestText(req)); 190 } 191 } 192 }), mxUtils.bind(this, function(err) 193 { 194 window.clearTimeout(timeoutThread); 195 196 if (acceptResponse) 197 { 198 error(err); 199 } 200 })); 201 }); 202 203 doExecute(); 204}; 205 206/** 207 * 208 */ 209NotionClient.prototype.getLibrary = function(id, success, error) 210{ 211 this.getFile(id, success, error, false, true); 212}; 213 214/** 215 * 216 */ 217NotionClient.prototype.getFile = function(id, success, error, denyConvert, asLibrary) 218{ 219 asLibrary = (asLibrary != null) ? asLibrary : false; 220 221 this.executeRequest('/v1/pages/' + encodeURIComponent(id), null, 'GET', mxUtils.bind(this, function(fileInfo) 222 { 223 try 224 { 225 var xmlParts = fileInfo.properties[this.xmlField].rich_text, xml = ''; 226 var fileNameObj = this.getTitle(fileInfo.properties); 227 228 for (var i = 0; i < xmlParts.length; i++) 229 { 230 xml += xmlParts[i].text.content; 231 } 232 233 var meta = {id: id, name: fileNameObj.title, nameField: fileNameObj.key}; 234 235 if (asLibrary) 236 { 237 success(new NotionLibrary(this.ui, xml, meta)); 238 } 239 else 240 { 241 success(new NotionFile(this.ui, xml, meta)); 242 } 243 } 244 catch(e) 245 { 246 if (error != null) 247 { 248 error(e); 249 } 250 else 251 { 252 throw e; 253 } 254 } 255 }), error); 256}; 257 258/** 259 * 260 */ 261NotionClient.prototype.insertLibrary = function(filename, data, success, error, folderObj) 262{ 263 this.insertFile(filename, data, success, error, true, folderObj); 264}; 265 266/** 267 * 268 */ 269NotionClient.prototype.insertFile = function(filename, data, success, error, asLibrary, folderObj) 270{ 271 asLibrary = (asLibrary != null) ? asLibrary : false; 272 var folderId, nameField; 273 274 var startSave = mxUtils.bind(this, function() 275 { 276 this.checkExists(folderId, filename, nameField, true, mxUtils.bind(this, function(checked, currentId) 277 { 278 if (checked) 279 { 280 this.writeFile(currentId? '/v1/pages/' + encodeURIComponent(currentId) : '/v1/pages', 281 currentId? null : folderId, filename, nameField, data, currentId? 'PATCH' : 'POST', 282 mxUtils.bind(this, function(fileInfo) 283 { 284 var fileNameObj = this.getTitle(fileInfo.properties); 285 var meta = {id: fileInfo.id, name: fileNameObj.title, nameField: fileNameObj.key}; 286 287 if (asLibrary) 288 { 289 success(new NotionLibrary(this.ui, data, meta)); 290 } 291 else 292 { 293 success(new NotionFile(this.ui, data, meta)); 294 } 295 }), error); 296 } 297 else 298 { 299 error(); 300 } 301 })); 302 }); 303 304 if (typeof folderObj === 'object') 305 { 306 nameField = this.getTitle(folderObj.schema.properties).key; 307 folderId = folderObj.id; 308 309 if (!folderObj.drawioReady) 310 { 311 folderObj.schema.properties[this.xmlField] = { 312 name: this.xmlField, 313 type: 'rich_text', 314 rich_text: {} 315 }; 316 317 this.executeRequest('/v1/databases/' + encodeURIComponent(folderObj.id), JSON.stringify({ 318 title: folderObj.schema.title, 319 properties: folderObj.schema.properties 320 }), 'PATCH', startSave, error); 321 } 322 else 323 { 324 startSave(); 325 } 326 } 327 else 328 { 329 error(); //This shouldn't happen! 330 } 331}; 332 333/** 334 * 335 */ 336NotionClient.prototype.checkExists = function(parentId, filename, nameField, askReplace, fn) 337{ 338 this.executeRequest('/v1/databases/' + encodeURIComponent(parentId) + '/query', JSON.stringify({ 339 filter: { 340 property: nameField, 341 text: { 342 equals: filename 343 } 344 } 345 }), 'POST', mxUtils.bind(this, function(resp) 346 { 347 if (resp.results.length == 0) 348 { 349 fn(true); 350 } 351 else 352 { 353 if (askReplace) 354 { 355 this.ui.spinner.stop(); 356 357 this.ui.confirm(mxResources.get('replaceIt', [filename]), function() 358 { 359 fn(true, resp.results[0].id); 360 }, function() 361 { 362 fn(false); 363 }); 364 } 365 else 366 { 367 this.ui.spinner.stop(); 368 369 this.ui.showError(mxResources.get('error'), mxResources.get('fileExists'), mxResources.get('ok'), function() 370 { 371 fn(false); 372 }); 373 } 374 } 375 }), function(req) 376 { 377 fn(false); 378 }); 379}; 380 381/** 382 * 383 */ 384NotionClient.prototype.saveFile = function(file, success, error) 385{ 386 try 387 { 388 var data = file.getData(); 389 390 this.writeFile('/v1/pages/' + file.getId(), null, file.getTitle(), file.getNameField(), 391 data, 'PATCH', mxUtils.bind(this, function(resp) 392 { 393 success(resp, data); 394 }), error); 395 } 396 catch (e) 397 { 398 error(e); 399 } 400}; 401 402/** 403 * 404 */ 405NotionClient.prototype.writeFile = function(url, folderId, filename, nameField, data, method, success, error) 406{ 407 try 408 { 409 if (url != null && data != null) 410 { 411 //Notion has a limit on rich-text pages of 200KB 412 if (data.length > 200000 /*200KB*/) 413 { 414 error({message: mxResources.get('drawingTooLarge') + ' (' + 415 this.ui.formatFileSize(data.length) + ' / 200 KB)'}); 416 417 return; 418 } 419 420 var richTxt = [] 421 var parts = Math.ceil(data.length / 2000); 422 423 for (var i = 0; i < parts; i++) 424 { 425 richTxt.push({ 426 text: { 427 content: data.substr(i * 2000, 2000) 428 } 429 }); 430 } 431 432 var reqBody = { 433 properties: {} 434 }; 435 436 reqBody.properties[nameField] = { 437 title: [{ 438 text: { 439 content: filename 440 } 441 }] 442 }; 443 444 reqBody.properties[this.xmlField] = { 445 rich_text: richTxt 446 }; 447 448 if (folderId) 449 { 450 reqBody['parent'] = { database_id: folderId }; 451 } 452 453 this.executeRequest(url, JSON.stringify(reqBody), method, success, error); 454 } 455 else 456 { 457 error({message: mxResources.get('unknownError')}); 458 } 459 } 460 catch (e) 461 { 462 error(e); 463 } 464}; 465 466/** 467 * 468 */ 469NotionClient.prototype.parseRequestText = function(req) 470{ 471 var result = {message: mxResources.get('unknownError')}; 472 473 try 474 { 475 result = JSON.parse(req.getText()); 476 } 477 catch (e) 478 { 479 // ignore 480 } 481 482 return result; 483}; 484 485/** 486 * Checks if the client is authorized and calls the next step. 487 */ 488NotionClient.prototype.pickLibrary = function(fn) 489{ 490 this.pickFile(fn); 491}; 492 493/** 494 * Checks if the client is authorized and calls the next step. 495 */ 496NotionClient.prototype.pickFolder = function(fn) 497{ 498 this.showNotionDialog(false, fn); 499}; 500 501NotionClient.prototype.pickFile = function(fn) 502{ 503 fn = (fn != null) ? fn : mxUtils.bind(this, function(id) 504 { 505 this.ui.loadFile('N' + encodeURIComponent(id)); 506 }); 507 508 this.showNotionDialog(true, fn); 509}; 510 511/** 512 * 513 */ 514NotionClient.prototype.showNotionDialog = function(showFiles, fn) 515{ 516 var itemId, itemName; 517 518 var content = document.createElement('div'); 519 content.style.whiteSpace = 'nowrap'; 520 content.style.overflow = 'hidden'; 521 content.style.height = '304px'; 522 523 var hd = document.createElement('h3'); 524 mxUtils.write(hd, mxResources.get((showFiles) ? 'officeSelDiag' : 'selectDB')); 525 hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px'; 526 content.appendChild(hd); 527 528 var div = document.createElement('div'); 529 div.style.whiteSpace = 'nowrap'; 530 div.style.border = '1px solid lightgray'; 531 div.style.boxSizing = 'border-box'; 532 div.style.padding = '4px'; 533 div.style.overflow = 'auto'; 534 div.style.lineHeight = '1.2em'; 535 div.style.height = '274px'; 536 content.appendChild(div); 537 538 var listItem = document.createElement('div'); 539 listItem.style.textOverflow = 'ellipsis'; 540 listItem.style.boxSizing = 'border-box'; 541 listItem.style.overflow = 'hidden'; 542 listItem.style.padding = '4px'; 543 listItem.style.width = '100%'; 544 545 var dlg = new CustomDialog(this.ui, content, mxUtils.bind(this, function() 546 { 547 fn(itemId); 548 })); 549 this.ui.showDialog(dlg.container, 420, 380, true, true); 550 551 if (showFiles) 552 { 553 dlg.okButton.parentNode.removeChild(dlg.okButton); 554 } 555 556 var createLink = mxUtils.bind(this, function(label, exec, padding, underline) 557 { 558 var link = document.createElement('a'); 559 link.setAttribute('title', label); 560 link.style.cursor = 'pointer'; 561 mxUtils.write(link, label); 562 mxEvent.addListener(link, 'click', exec); 563 564 if (underline) 565 { 566 link.style.textDecoration = 'underline'; 567 } 568 569 if (padding != null) 570 { 571 var temp = listItem.cloneNode(); 572 temp.style.padding = padding; 573 temp.appendChild(link); 574 575 link = temp; 576 } 577 578 return link; 579 }); 580 581 var updatePathInfo = mxUtils.bind(this, function() 582 { 583 var dbInfo = document.createElement('div'); 584 dbInfo.style.marginBottom = '8px'; 585 586 dbInfo.appendChild(createLink(itemName, mxUtils.bind(this, function() 587 { 588 itemId = null; 589 selectDB(); 590 }), null, true)); 591 592 div.appendChild(dbInfo); 593 }); 594 595 var error = mxUtils.bind(this, function(err) 596 { 597 // Pass a dummy notFoundMessage to bypass special handling 598 this.ui.handleError(err, null, mxUtils.bind(this, function() 599 { 600 this.ui.spinner.stop(); 601 602 if (this.getUser() != null) 603 { 604 itemId = null; 605 606 selectDB(); 607 } 608 else 609 { 610 this.ui.hideDialog(); 611 } 612 }), null, {}); 613 }); 614 615 // Adds paging for DBs and diagrams (DB pages) 616 var nextPageDiv = null; 617 var scrollFn = null; 618 var pageSize = 100; 619 620 var selectFile = mxUtils.bind(this, function(nextCursor) 621 { 622 if (nextCursor == null) 623 { 624 div.innerHTML = ''; 625 } 626 627 this.ui.spinner.spin(div, mxResources.get('loading')); 628 dlg.okButton.removeAttribute('disabled'); 629 630 if (scrollFn != null) 631 { 632 mxEvent.removeListener(div, 'scroll', scrollFn); 633 scrollFn = null; 634 } 635 636 if (nextPageDiv != null && nextPageDiv.parentNode != null) 637 { 638 nextPageDiv.parentNode.removeChild(nextPageDiv); 639 } 640 641 nextPageDiv = document.createElement('a'); 642 nextPageDiv.style.display = 'block'; 643 nextPageDiv.style.cursor = 'pointer'; 644 mxUtils.write(nextPageDiv, mxResources.get('more') + '...'); 645 646 var nextPage = mxUtils.bind(this, function() 647 { 648 selectFile(nextCursor); 649 }); 650 651 mxEvent.addListener(nextPageDiv, 'click', nextPage); 652 653 var reqBody = { 654 page_size: pageSize 655 }; 656 657 if (nextCursor != null) 658 { 659 reqBody.start_cursor = nextCursor; 660 } 661 662 this.executeRequest('/v1/databases/' + encodeURIComponent(itemId) + '/query', 663 JSON.stringify(reqBody), 'POST', mxUtils.bind(this, function(resp) 664 { 665 this.ui.spinner.stop(); 666 667 if (nextCursor == null) 668 { 669 updatePathInfo(); 670 671 div.appendChild(createLink('../ [Up]', mxUtils.bind(this, function() 672 { 673 itemId = null; 674 selectDB(); 675 }), '4px')); 676 } 677 678 var files = resp.results; 679 680 if (files == null || files.length == 0) 681 { 682 mxUtils.write(div, mxResources.get('noFiles')); 683 } 684 else 685 { 686 var gray = true; 687 688 for (var i = 0; i < files.length; i++) 689 { 690 (mxUtils.bind(this, function(file, idx) 691 { 692 var temp = listItem.cloneNode(); 693 temp.style.backgroundColor = (gray) ? 694 ((Editor.isDarkMode()) ? '#000000' : '#eeeeee') : ''; 695 gray = !gray; 696 697 var typeImg = document.createElement('img'); 698 typeImg.src = IMAGE_PATH + '/file.png'; 699 typeImg.setAttribute('align', 'absmiddle'); 700 typeImg.style.marginRight = '4px'; 701 typeImg.style.marginTop = '-4px'; 702 typeImg.width = 20; 703 temp.appendChild(typeImg); 704 705 temp.appendChild(createLink(this.getTitle(file.properties).title, mxUtils.bind(this, function() 706 { 707 this.ui.hideDialog(); 708 fn(file.id); 709 }))); 710 711 div.appendChild(temp); 712 }))(files[i], i); 713 } 714 715 if (resp.has_more) 716 { 717 nextCursor = resp.next_cursor; 718 719 div.appendChild(nextPageDiv); 720 721 scrollFn = function() 722 { 723 if (div.scrollTop >= div.scrollHeight - div.offsetHeight) 724 { 725 nextPage(); 726 } 727 }; 728 729 mxEvent.addListener(div, 'scroll', scrollFn); 730 } 731 } 732 }), error, true); 733 }); 734 735 var selectDB = mxUtils.bind(this, function(nextCursor) 736 { 737 if (nextCursor == null) 738 { 739 div.innerHTML = ''; 740 } 741 742 dlg.okButton.setAttribute('disabled', 'disabled'); 743 this.ui.spinner.spin(div, mxResources.get('loading')); 744 745 if (scrollFn != null) 746 { 747 mxEvent.removeListener(div, 'scroll', scrollFn); 748 } 749 750 if (nextPageDiv != null && nextPageDiv.parentNode != null) 751 { 752 nextPageDiv.parentNode.removeChild(nextPageDiv); 753 } 754 755 nextPageDiv = document.createElement('a'); 756 nextPageDiv.style.display = 'block'; 757 nextPageDiv.style.cursor = 'pointer'; 758 mxUtils.write(nextPageDiv, mxResources.get('more') + '...'); 759 760 var nextPage = mxUtils.bind(this, function() 761 { 762 selectDB(nextCursor); 763 }); 764 765 mxEvent.addListener(nextPageDiv, 'click', nextPage); 766 767 this.executeRequest('/v1/databases?page_size=' + pageSize + (nextCursor != null? '&start_cursor=' + nextCursor : ''), 768 null, 'GET', mxUtils.bind(this, function(resp) 769 { 770 this.ui.spinner.stop(); 771 var dbs = resp.results; 772 var count = 0; 773 774 if (dbs == null || dbs.length == 0) 775 { 776 mxUtils.write(div, mxResources.get('noDBs')); 777 } 778 else 779 { 780 for (var i = 0; i < dbs.length; i++) 781 { 782 var drawioReady = dbs[i].properties[this.xmlField] && 783 dbs[i].properties[this.xmlField].type == 'rich_text'; 784 785 //Filter DBs when opening a file 786 if (showFiles && !drawioReady) continue; 787 788 (mxUtils.bind(this, function(db, idx, drawioReady) 789 { 790 var temp = listItem.cloneNode(); 791 temp.style.backgroundColor = (idx % 2 == 0) ? 792 ((Editor.isDarkMode()) ? '#000000' : '#eeeeee') : ''; 793 794 temp.appendChild(createLink(this.getTitleVal(db), mxUtils.bind(this, function() 795 { 796 itemId = db.id; 797 itemName = this.getTitleVal(db); 798 799 if (showFiles) 800 { 801 selectFile(); 802 } 803 else 804 { 805 this.ui.hideDialog(); 806 fn({id: itemId, drawioReady: drawioReady, schema: db}); 807 } 808 }))); 809 810 div.appendChild(temp); 811 }))(dbs[i], i, drawioReady); 812 813 count++; 814 } 815 } 816 817 if (resp.has_more) 818 { 819 nextCursor = resp.next_cursor; 820 821 if (count == 0) 822 { 823 nextPage(); 824 return; 825 } 826 827 div.appendChild(nextPageDiv); 828 829 scrollFn = function() 830 { 831 if (div.scrollTop >= div.scrollHeight - div.offsetHeight) 832 { 833 nextPage(); 834 } 835 }; 836 837 mxEvent.addListener(div, 'scroll', scrollFn); 838 } 839 else if (count == 0 && div.innerHTML == '') 840 { 841 mxUtils.write(div, mxResources.get('noDBs')); 842 } 843 }), error); 844 }); 845 846 selectDB(); 847}; 848 849/** 850 * 851 */ 852NotionClient.prototype.logout = function() 853{ 854 //Send to server to clear token cookie 855 this.executeRequest('/removeToken', null, 'GET', function(){}, function(){}); 856 this.setUser(null); 857 _token = null; 858}; 859 860})();