UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

673 lines (608 loc) 31.7 kB
/* * Copyright (C) 1998-2020 by Northwoods Software Corporation * All Rights Reserved. * * Go DropBox */ // import { Promise } from 'es6-promise'; import * as go from 'gojs'; import * as gcs from './GoCloudStorage.js'; /** * Class for saving / loading GoJS {@link Model}s to / from Dropbox. * As with all {@link GoCloudStorage} subclasses (with the exception of {@link GoLocalStorage}, any page using GoDropBox must be served on a web server. * * **Note**: Any page using GoDropBox must include a script tag with a reference to the <a href="https://cdnjs.com/libraries/dropbox.js/">Dropbox JS SDK</a>. * @category Storage */ export class GoDropBox extends gcs.GoCloudStorage { private _dropbox: any; private _menuPath: string; private _options: any; /** * @constructor * @param {go.Diagram|go.Diagram[]} managedDiagrams An array of GoJS {@link Diagram}s whose model(s) will be saved to / loaded from Dropbox. * Can also be a single Diagram. * @param {string} clientId The client ID of the application in use (given in Dropbox Developer's Console) * @param {string} defaultModel String representation of the default model data for new diagrams. If this is null, * default new diagrams will be empty. Usually a value given by calling {@link Model#toJson} on a GoJS Diagram's Model. * @param {string} iconsRelativeDirectory The directory path relative to the page in which this instance of GoDropBox exists, in which * the storage service brand icons can be found. The default value is "../goCloudStorageIcons/". */ constructor(managedDiagrams: go.Diagram | Array<go.Diagram>, clientId: string, defaultModel?: string, iconsRelativeDirectory?: string) { super(managedDiagrams, defaultModel, clientId, iconsRelativeDirectory); if (window['Dropbox']) { const Dropbox = window['Dropbox']; this._dropbox = new Dropbox({ clientId: clientId }); } this.menuPath = ''; this.ui.id = 'goDropBoxCustomFilepicker'; this._serviceName = 'Dropbox'; this._className = 'GoDropBox'; this._options = { // Required. Called when a user selects an item in the Chooser. success: function(files) { alert("Here's the file link: " + files[0].link); }, // Optional. Called when the user closes the dialog without selecting a file // and does not include any parameters. cancel: function() { }, // Optional. "preview" (default) is a preview link to the document for sharing, // "direct" is an expiring link to download the contents of the file. For more // information about link types, see Link types below. linkType: 'direct', // or "direct" // Optional. A value of false (default) limits selection to a single file, while // true enables multiple file selection. multiselect: false, // or true // Optional. This is a list of file extensions. If specified, the user will // only be able to select files with these extensions. You may also specify // file types, such as "video" or "images" in the list. For more information, // see File types below. By default, all extensions are allowed. extensions: ['.pdf', '.doc', '.docx', '.diagram'], // Optional. A value of false (default) limits selection to files, // while true allows the user to select both folders and files. // You cannot specify `linkType: "direct"` when using `folderselect: true`. folderselect: false // or true }; } /** * Get the <a href="https://github.com/dropbox/dropbox-sdk-js">Dropbox client</a> instance associated with this instance of GoDropBox * (via {@link #clientId}). Set during {@link #authorize}. * @function. * @return {any} */ get dropbox(): any { return this._dropbox; } /** * Get / set currently open Dropnpx path in custom filepicker {@link #ui}. Default value is the empty string, which corresponds to the * currently signed in user's Drobox account's root path. Set when a user clicks on a folder in the custom ui menu by invoking anchor onclick values. * These onclick values are set when the Dropbox directory at the current menuPath is displayed with {@link #showUI}. * @function. * @return {string} */ get menuPath(): string { return this._menuPath; } set menuPath(value: string) { this._menuPath = value; } /** * Check if there is a signed in Dropbox user who has authorized the application linked to this instance of GoDropBox (via {@link #clientId}). * If not, prompt user to sign in / authenticate their Dropbox account. * @param {boolean} refreshToken Whether to get a new acess token (triggers a page redirect) (true) or try to find / use the * one in the browser window URI (no redirect) (false) * @return {Promise<boolean>} Returns a Promise that resolves with a boolean stating whether authorization was succesful (true) or failed (false) */ public authorize(refreshToken: boolean = false) { const storage = this; return new Promise(function(resolve: Function, reject: Function) { // First, check if we're explicitly being told to refresh token (redirect to login screen) if (refreshToken) { storage.maybeSaveAppState(); // redirect to the Dropbox sign in page for authentication const authUrl: string = storage.dropbox.getAuthenticationUrl(window.location.href); window.location.href = authUrl; resolve(false); } else if (!storage.dropbox.getAccessToken()) { // Then, check if there is no access token set on our Dropbox instance... // if no redirect, check if there's an db_id and access_token in the current uri if (storage.getAccessTokenFromUrl()) { storage.dropbox.setAccessToken(storage.getAccessTokenFromUrl()); resolve(true); } else { storage.maybeSaveAppState(); // if not, redirect to get an access_token from Dropbox login screen const authUrl: string = storage.dropbox.getAuthenticationUrl(window.location.href); window.location.href = authUrl; resolve(false); } } // load in diagrams' models from before the auth flow started (preserve prior app state) storage.maybeLoadAppState(); // If not explicitly redirecting and we have an access token already, we're already authenticated resolve(true); }); } private getAccessTokenFromUrl() { const accessToken = window.location.hash.substring(window.location.hash.indexOf('=') + 1, window.location.hash.indexOf('&')); return !!accessToken ? accessToken : null; } /** * Attempt to preserve the app state in local storage * This is usually called before a redirect (usually auth flow), so when we return to the app page, we can have the same model data */ private maybeSaveAppState() { const storage = this; try { // temp save the current diagram model data to local storage, if possible (preserver prior app state) // This will be loaded back in after the auth process (which happens in another window) const item: string = storage.makeSaveFile(); window.localStorage.setItem('gdb-' + storage.clientId, item); } catch (e) { throw new Error('Local storage not supported; diagrams model data will not be preserved during Dropboc authentication.'); } } /** * Attempt to load the previous app state from local storage * This is usually called after a redirect from another page (usually auth flow), * so when we return to the app page, we can have the same model data as before */ private maybeLoadAppState() { const storage = this; try { const fileContents: string = window.localStorage.getItem('gdb-' + storage.clientId); storage.loadFromFileContents(fileContents); localStorage.removeItem('gdb-' + storage.clientId); } catch (e) { } } /** * Sign out the currently signed in Dropbox user * Note: Since this redirects the app page, unsaved diagram model data will be lost after calling this */ public signOut() { const storage = this; const dbx = storage.dropbox; // if (!dbx.getAccessToken()) return; storage.maybeSaveAppState(); dbx.setAccessToken(null); dbx.authTokenRevoke(); /*.then(function(response) { // the access token for `dbx` has been revoked console.log("got authTokenRevoke response:"); console.log(response); // this should fail now: dbx.usersGetCurrentAccount() .then(function(response) { console.log("got usersGetCurrentAccount response:"); console.log(response); }) .catch(function(error) { console.log("got usersGetCurrentAccount error:"); console.log(error); }); }) .catch(function(error) { console.log("got authTokenRevoke error:"); console.log(error); });*/ // window.location.href = window.location.href.substr(0, window.location.href.indexOf('#')); } /* No longer used??? public testAuth() { const xhr: XMLHttpRequest = new XMLHttpRequest(); const link: string = 'https://www.dropbox.com/oauth2/authorize'; xhr.open('GET', link, true); xhr.setRequestHeader('response_type', 'code'); xhr.setRequestHeader('client_id', this.clientId); xhr.onload = function () { if (xhr.readyState === 4 && (xhr.status === 200)) { // tslint:disable-next-line:no-console console.log(xhr.response); } else { throw new Error(xhr.response); // failed to load } }; // end xhr onload xhr.send(); }*/ /** * Get information about the currently logged in Dropbox user. Some properties of particular note include: * - country * - email * - account_id * - name * - abbreviated_name * - display_name * - given_name * - surname * @return {Promise} Returns a Promise that resolves with information about the currently logged in Dropbox user */ public getUserInfo() { const storage = this; return new Promise(function(resolve, reject) { // Case: No access token in URI if (!storage.dropbox.getAccessToken() && window.location.hash.indexOf('access_token') === -1) { storage.authorize(true); } else if (!storage.dropbox.getAccessToken() && window.location.hash.indexOf('access_token') === 1) { storage.authorize(false); } storage.dropbox.usersGetCurrentAccount(null).then(function(userData) { resolve(userData); }).catch(function(e) { // Case: storage.dropbox.access_token has expired or become malformed; get another access token if (e.status === 400) { storage.authorize(true); } }); }); } public showUI() { const storage = this; const ui = storage.ui; ui.innerHTML = ''; // clear div ui.style.visibility = 'visible'; ui.innerHTML = "<img class='icons' src='" + storage.iconsRelativeDirectory + "dropBox.png'></img><strong>Save Diagram As</strong><hr></hr>"; // user input div const userInputDiv: HTMLElement = document.createElement('div'); userInputDiv.id = 'userInputDiv'; userInputDiv.innerHTML += '<input id="gdb-userInput" placeholder="Enter filename"></input>'; ui.appendChild(userInputDiv); const submitDiv: HTMLElement = document.createElement('div'); submitDiv.id = 'submitDiv'; const actionButton = document.createElement('button'); actionButton.id = 'actionButton'; actionButton.textContent = 'Save'; actionButton.onclick = function() { const input: HTMLInputElement = (document.getElementById('gdb-userInput')) as HTMLInputElement; const val: string = input.value; if (val !== '' && val !== undefined && val != null) { ui.style.visibility = 'hidden'; storage.saveWithUI(val); } }; submitDiv.appendChild(actionButton); ui.appendChild(submitDiv); const cancelDiv: HTMLElement = document.createElement('div'); cancelDiv.id = 'cancelDiv'; const cancelButton = document.createElement('button'); cancelButton.id = 'cancelButton'; cancelButton.textContent = 'Cancel'; cancelButton.onclick = function() { storage.hideUI(true); }; cancelDiv.appendChild(cancelButton); ui.appendChild(cancelDiv); return storage._deferredPromise.promise; } /** * Hide the custom GoDropBox filepicker {@link #ui}; nullify {@link #menuPath}. * @param {boolean} isActionCanceled If action (Save, Delete, Load) is cancelled, resolve the Promise returned in {@link #showUI} with a 'Canceled' notification. */ public hideUI(isActionCanceled?: boolean) { const storage = this; storage.menuPath = ''; super.hideUI(isActionCanceled); } /** * @private * @hidden * Process the result of pressing the action (Save, Delete, Load) button on the custom GoDropBox filepicker {@link #ui}. * @param {string} action The action that must be done. Acceptable values: * - Save * - Delete * - Load */ public processUIResult(action: string) { const storage = this; /** * Get the selected file (in menu's) Dropbox filepath * @return {string} The selected file's Dropbox filepath */ function getSelectedFilepath() { const radios = document.getElementsByName('dropBoxFile'); let selectedFile = null; for (let i = 0; i < radios.length; i++) { if ((radios[i] as HTMLInputElement).checked) { selectedFile = radios[i].getAttribute('data'); } } return selectedFile; } const filePath: string = getSelectedFilepath(); switch (action) { case 'Save': { if (storage.menuPath || storage.menuPath === '') { let name: string = (document.getElementById('userInput') as HTMLInputElement).value; if (name) { if (name.indexOf('.diagram') === -1) name += '.diagram'; storage.save(storage.menuPath + '/' + name); } else { // handle bad save name // tslint:disable-next-line:no-console console.log('Proposed file name is not valid'); // placeholder handler } } break; } case 'Load': { storage.load(filePath); break; } case 'Delete': { storage.remove(filePath); break; } } storage.hideUI(); } /** * Check whether a file exists in user's Dropbox at a given path. * @param {string} path A valid Dropbox filepath. Path syntax is `/{path-to-file}/{filename}`; i.e. `/Public/example.diagram`. * Alternatively, this may be a valid Dropbox file ID. * @return {Promise} Returns a Promise that resolves with a boolean stating whether a file exists in user's Dropbox at a given path */ public checkFileExists(path: string) { const storage = this; if (path.indexOf('.diagram') === -1) path += '.diagram'; return new Promise(function(resolve: Function, reject: Function) { storage.dropbox.filesGetMetadata({ path: path }).then(function(resp) { if (resp) resolve(true); }).catch(function(err) { resolve(false); }); }); } /** * Get the Dropbox file reference object at a given path. Properties of particular note include: * - name: The name of the file in DropBox * - id: The DropBox-given file ID * - path_diplay: A lower-case version of the path this file is stored at in DropBox * - .tag: A tag denoting the type of this file. Common values are "file" and "folder". * * **Note:** The first three elements in the above list are requisite for creating valid {@link DiagramFile}s. * @param {string} path A valid Dropbox filepath. Path syntax is `/{path-to-file}/{filename}`; i.e. `/Public/example.diagram`. * Alternatively, this may be a valid Dropbox file ID. * @return {Promise} Returns a Promise that resolves with a Dropbox file reference object at a given path */ public getFile(path: string) { const storage = this; if (path.indexOf('.diagram') === -1) path += '.diagram'; return storage.dropbox.filesGetMetadata({ path: path }).then(function(resp) { if (resp) return resp; }).catch(function(err) { return null; }); } /** * Save the current {@link #managedDiagrams} model data to Dropbox with the filepicker {@link #ui}. Returns a Promise that resolves with a * {@link DiagramFile} representing the saved file. * @param {string} filename Optional: The name to save data to Dropbox under. If this is not provided, you will be prompted for a filename * @return {Promise} */ public saveWithUI(filename?: string) { const storage = this; if (filename === undefined || filename == null) { // let filename: string = prompt("GIMME A NAME"); // storage.saveWithUI(filename); return new Promise(function(resolve, reject) { resolve(storage.showUI()); }); } else { if (filename.length < 8) { filename += '.diagram'; } else { const lastEight: string = filename.substring(filename.length - 8, filename.length); if (lastEight !== '.diagram') { filename += '.diagram'; } } return new Promise(function(resolve, reject) { storage._options.success = function(resp) { const a = 3; // find the file that was just saved // look at all files with "filename" in title // find most recent of those const savedFile: any = null; storage.dropbox.filesListFolder({ path: '', recursive: true }).then(function(r) { const files = r.entries; const possibleFiles = []; /*for (let i in files) { var file = files[i]; //var fname = filename.replace(/.diagram([^_]*)$/,'$1'); //console.log(fname); if (file.filename.indexOf(fname) != -1 && file.filename.indexOf(".diagram") != -1) { possibleFiles.push(file); } }*/ // find most recently modified (saved) const latestestDate: Date = new Date(-8400000); let latestFile = null; // for (let i in files) { for (let i = 0; i < files.length; i++) { const file = files[i]; let dateModified = new Date(file.server_modified); if (dateModified != null && dateModified !== undefined && dateModified instanceof Date) { if (dateModified > latestestDate) { dateModified = latestestDate; latestFile = file; } } } // resolve Promises // tslint:disable-next-line:no-shadowed-variable const savedFile: gcs.DiagramFile = { name: latestFile.name, path: latestFile.path_lower, id: latestFile.id }; storage.currentDiagramFile = savedFile; resolve(savedFile); storage._deferredPromise.promise.resolve(savedFile); storage._deferredPromise.promise = storage.makeDeferredPromise(); }); }; function makeTextFile(text) { const data = new Blob([text], { type: 'text/plain' }); let uri = ''; uri = window.URL.createObjectURL(data); return uri; } const dataURI = 'data:text/html,' + encodeURIComponent(storage.makeSaveFile()); const Dropbox = window['Dropbox']; Dropbox.save(dataURI, filename, storage._options); }); } // end if filename exists case } /** * Save {@link #managedDiagrams}' model data to Dropbox. If path is supplied save to that path. If no path is supplied but {@link #currentDiagramFile} has non-null, * valid properties, update saved diagram file content at the path in Dropbox corresponding to currentDiagramFile.path with current managedDiagrams' model data. * If no path is supplied and currentDiagramFile is null or has null properties, this calls {@link #saveWithUI}. * @param {string} path A valid Dropbox filepath to save current diagram model to. Path syntax is `/{path-to-file}/{filename}`; * i.e. `/Public/example.diagram`. * Alternatively, this may be a valid Dropbox file ID. * @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file. */ public save(path?: string) { const storage = this; return new Promise(function(resolve, reject) { if (path) { // save as storage.dropbox.filesUpload({ contents: storage.makeSaveFile(), path: path, autorename: true, // instead of overwriting, save to a different name (i.e. test.diagram -> test(1).diagram) mode: { '.tag': 'add' }, mute: false }).then(function(resp) { const savedFile: gcs.DiagramFile = { name: resp.name, id: resp.id, path: resp.path_lower }; storage.currentDiagramFile = savedFile; resolve(savedFile); // used if saveDiagramAs was called without UI // if saveAs has been called in processUIResult, need to resolve / reset the Deferred Promise instance variable storage._deferredPromise.promise.resolve(savedFile); storage._deferredPromise.promise = storage.makeDeferredPromise(); }).catch(function(e) { // Bad request: Access token is either expired or malformed. Get another one. if (e.status === 400) { storage.authorize(true); } }); } else if (storage.currentDiagramFile.path) { // save path = storage.currentDiagramFile.path; storage.dropbox.filesUpload({ contents: storage.makeSaveFile(), path: path, autorename: false, mode: { '.tag': 'overwrite' }, mute: true }).then(function(resp) { const savedFile: Object = { name: resp.name, id: resp.id, path: resp.path_lower }; resolve(savedFile); }).catch(function(e) { // Bad request: Access token is either expired or malformed. Get another one. if (e.status === 400) { storage.authorize(true); } }); } else { resolve(storage.saveWithUI()); // throw Error("Cannot save file to Dropbox with path " + path); } }); } /** * Load the contents of a saved diagram from Dropbox using the custom filepicker {@link #ui}. * @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the loaded file. */ public loadWithUI() { const storage = this; storage._options.success = function(r) { const file = r[0]; // get the file path storage.dropbox.filesGetMetadata({ path: file.id }).then(function(resp) { const path: string = resp.path_display; storage.load(path); }); }; const Dropbox = window['Dropbox']; Dropbox.choose(storage._options); return storage._deferredPromise.promise; // will not resolve until action (save, load, delete) completes } /** * Load the contents of a saved diagram from Dropbox. * @param {string} path A valid Dropbox filepath to load diagram model data from. Path syntax is `/{path-to-file}/{filename}`; * i.e. `/Public/example.diagram`. * Alternatively, this may be a valid Dropbox file ID. * @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the loaded file */ public load(path: string) { const storage = this; return new Promise(function(resolve, reject) { if (path) { storage.dropbox.filesGetTemporaryLink({ path: path }).then(function(resp) { const link: string = resp.link; storage.currentDiagramFile.name = resp.metadata.name; storage.currentDiagramFile.id = resp.metadata.id; storage.currentDiagramFile.path = path; const xhr: XMLHttpRequest = new XMLHttpRequest(); xhr.open('GET', link, true); xhr.setRequestHeader('Authorization', 'Bearer ' + storage.dropbox.getAccessToken()); xhr.onload = function() { if (xhr.readyState === 4 && (xhr.status === 200)) { storage.loadFromFileContents(xhr.response); const loadedFile: gcs.DiagramFile = { name: resp.metadata.name, id: resp.metadata.id, path: resp.metadata.path_lower }; resolve(loadedFile); // used if loadDiagram was called without UI // if loadDiagram has been called in processUIResult, need to resolve / reset the Deferred Promise instance variable storage._deferredPromise.promise.resolve(loadedFile); storage._deferredPromise.promise = storage.makeDeferredPromise(); } else { throw Error('Cannot load file from Dropbox with path ' + path); // failed to load } }; // end xhr onload xhr.send(); }).catch(function(e) { // Bad request: Access token is either expired or malformed. Get another one. if (e.status === 400) { storage.authorize(true); } }); } else throw Error('Cannot load file from Dropbox with path ' + path); }); } /** * Delete a chosen diagram file from Dropbox using the custom filepicker {@link #ui}. * @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file. */ public removeWithUI() { const storage = this; storage._options.success = function(r) { const file = r[0]; // get the file path storage.dropbox.filesGetMetadata({ path: file.id }).then(function(resp) { const path: string = resp.path_display; storage.remove(path); }); }; const Dropbox = window['Dropbox']; Dropbox.choose(storage._options); return storage._deferredPromise.promise; // will not resolve until action (save, load, delete) completes } /** * Delete a given diagram file from Dropbox. * @param {string} path A valid Dropbox filepath to delete diagram model data from. Path syntax is * `/{path-to-file}/{filename}`; i.e. `/Public/example.diagram`. * Alternatively, this may be a valid Dropbox file ID. * @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file. */ public remove(path: string) { const storage = this; return new Promise(function(resolve, reject) { if (path) { storage.dropbox.filesDelete({ path: path }).then(function(resp) { if (storage.currentDiagramFile && storage.currentDiagramFile['id'] === resp['id']) storage.currentDiagramFile = { name: null, path: null, id: null }; const deletedFile: gcs.DiagramFile = { name: resp.name, id: resp['id'], path: resp.path_lower }; resolve(deletedFile); // used if deleteDiagram was called without UI // if deleteDiagram has been called in processUIResult, need to resolve / reset the Deferred Promise instance variable storage._deferredPromise.promise.resolve(deletedFile); storage._deferredPromise.promise = storage.makeDeferredPromise(); }).catch(function(e) { // Bad request: Access token is either expired or malformed. Get another one. if (e.status === 400) { storage.authorize(true); } }); } else throw Error('Cannot delete file from Dropbox with path ' + path); }); } }