/* * Copyright (C) 1998-2018 by Northwoods Software Corporation * All Rights Reserved. * * Go DropBox */ import * as go from "../../release/go"; import * as gcs from "./GoCloudStorage"; import {Promise} from "es6-promise"; /** *
Class for saving / loading GoJS Diagram models * 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 Dropbox JS SDK.
* @category Storage */ export class GoDropBox extends gcs.GoCloudStorage { private _dropbox: any; private _menuPath: string; /** * The number of files to display in {@link ui} before loading more */ private static _MIN_FILES_IN_UI = 100; /** * @constructor * @param {go.Diagram|go.Diagram[]} managedDiagrams An array of GoJS Diagrams 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 .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|go.Diagram[], clientId: string, defaultModel?: string, iconsRelativeDirectory?: string) { super(managedDiagrams, defaultModel, clientId); if (window['Dropbox']) { let Dropbox = window['Dropbox']; this._dropbox = new Dropbox({clientId: clientId}); } this.menuPath = ""; this.ui.id = "goDropBoxCustomFilepicker"; this._serviceName = "Dropbox"; } /** * Get the Dropbox client 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/{path}/{to}/{folder}/; i.e. /Public/
* @param {string} numAdditionalFiles Number of files to show in UI, in addition to a static property that can only be modified by changing source code.
* This prevents long wait times while the UI loads if there are a large number of diagram files stored in Dropbox.
* @return {Promise} Returns a Promise which resolves (in {@link save}, {@link load}, or {@link remove}, after action is handled with a
* {@link DiagramFile} representing the saved/loaded/deleted file
*/
public showUI(action: string, path?: string, numAdditionalFiles?: number) {
const storage = this;
const ui = storage.ui;
if (!path) path = "";
if (!numAdditionalFiles) numAdditionalFiles = 0;
if (!storage.dropbox.getAccessToken()) {
storage.authorize(true);
}
storage.dropbox.usersGetCurrentAccount(null).then(function (userData) {
if (userData) {
ui.innerHTML = "Observed " + (GoDropBox._MIN_FILES_IN_UI + numAdditionalFiles) + " files. There may be more diagram files not shown. " + "Click here to search for more.
"; document.getElementById('dropBoxLoadMoreFiles').onclick = function () { storage.showUI(action, storage.menuPath, num); } } // include a link to return to the parent directory if (parentDirectory !== undefined) { let parentDirectoryDisplay: string; if (!parentDirectory) parentDirectoryDisplay = "root"; else parentDirectoryDisplay = parentDirectory; let parentDiv: HTMLDivElement = document.createElement('div'); let parentAnchor: HTMLAnchorElement = document.createElement('a'); parentAnchor.id = 'dropBoxReturnToParentDir'; parentAnchor.text = "Back to " + parentDirectoryDisplay; parentAnchor.onclick = function () { storage.showUI(action, parentDirectory, 0); } parentDiv.appendChild(parentAnchor); filesDiv.appendChild(parentDiv); } if (!document.getElementById(filesDiv.id)) ui.appendChild(filesDiv); // italicize currently open file, if a file is currently open if (storage.currentDiagramFile.id) { var currentFileElement = document.getElementById(storage.currentDiagramFile.id + '-label'); if (currentFileElement) { currentFileElement.style.fontStyle = "italic"; } } // user input div (only for save) if (action === 'Save' && !document.getElementById('userInputDiv')) { let userInputDiv: HTMLElement = document.createElement('div'); userInputDiv.id = 'userInputDiv'; userInputDiv.innerHTML = 'Save Diagram As '; ui.appendChild(userInputDiv); } // user data div if (!document.getElementById('userDataDiv')) { let userDataDiv: HTMLElement = document.createElement('div'); userDataDiv.id = 'userDataDiv'; let userDataSpan: HTMLSpanElement = document.createElement('span'); userDataSpan.textContent = userData.name.display_name + ', ' + userData.email; userDataDiv.appendChild(userDataSpan); let changeAccountAnchor: HTMLAnchorElement = document.createElement('a'); changeAccountAnchor.href = "#"; changeAccountAnchor.id = "dropBoxChangeAccount"; changeAccountAnchor.textContent = "Change Account"; changeAccountAnchor.onclick = function () { storage.authorize(true); // from the authorization page, a user can sign in under a different DropBox account } userDataDiv.appendChild(changeAccountAnchor); ui.appendChild(userDataDiv); } if (!document.getElementById('submitDiv') && !document.getElementById('cancelDiv')) { // buttons let submitDiv: HTMLElement = document.createElement('div'); submitDiv.id = "submitDiv"; let actionButton = document.createElement('button'); actionButton.id = 'actionButton'; actionButton.textContent = action; actionButton.onclick = function () { storage.processUIResult(action); } submitDiv.appendChild(actionButton); ui.appendChild(submitDiv); let cancelDiv: HTMLElement = document.createElement('div'); cancelDiv.id = 'cancelDiv'; let cancelButton = document.createElement('button'); cancelButton.textContent = "Cancel"; cancelButton.id = 'cancelButton'; cancelButton.onclick = function () { storage.hideUI(true); } cancelDiv.appendChild(cancelButton); ui.appendChild(cancelDiv); } }); } }).catch(function (e) { // Bad request: Access token is either expired or malformed. Get another one. if (e.status == 400) { storage.authorize(true); } }); return storage._deferredPromise.promise; // will not resolve until action (save, load, delete) completes } /** * 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: */{path-to-file}/{filename}; i.e. /Public/example.diagram.
* @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:
* /{path-to-file}/{filename}; i.e. /Public/example.diagram.
* @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.
* @return {Promise}
*/
public saveWithUI() {
const storage = this;
return new Promise(function (resolve: Function, reject: Function) {
resolve(storage.showUI('Save', ''));
});
}
/**
* 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.
* @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) {
let 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) {
let 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;
return new Promise(function (resolve, reject) {
resolve(storage.showUI('Load', ''));
});
}
/**
* 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.
* @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) {
let link: string = resp.link;
storage.currentDiagramFile.name = resp.metadata.name;
storage.currentDiagramFile.id = resp.metadata.id;
storage.currentDiagramFile.path = path;
let 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);
let 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;
return new Promise(function (resolve, reject) {
resolve(storage.showUI('Delete', ''));
});
}
/**
* 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.
* @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 };
let 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);
});
}
}