/* * Copyright (C) 1998-2018 by Northwoods Software Corporation * All Rights Reserved. * * GoCloudStorage.js */ import * as go from "../../release/go"; import {Promise} from "es6-promise"; /** * A simple interface containing basic information about a diagram saved to a storage service. Guarantees the existence of file id, name, and path. *

DiagramFiles are used as a minimal representation of files stored in data. {@link GoCloudStorage.currentDiagramFile}. * {@link GoCloudStorage.save}, {@link GoCloudStorage.load}, {@link GoCloudStorage.remove}, and {@link GoCloudStorage.create} all return Promises which resolve with * DiagramFiles containing minimal data about the saved / loaded / removed / created file in storage.

* @category Storage */ export interface DiagramFile { /** * The storage-given ID of the diagram file. This is usually a lengthy alphanumeric string. *

Note: In the case of files saved to / loaded from Local Storage with {@link GoLocalStorage},ID is the same as the name of the file. */ id: string; /** * The name of the diagram file in storage. This is assigned by the user during {@link GoCloudStorage.save}. */ name: string; /** * The path of the diagram file in storage. Rules for valid path syntax by subclass: *

*/ path: string; /** * @private * @hidden * token is sometimes necesary for {@link GoOneDrive}. This is an access token given by the * Microsoft OneDrive Filepicker allowing read / write on a specific drive file. * No other {@link GoCloudStorage} subclasses use it. It is unlikely one will use this field without editing source code. */ token?: string, /** * @private * @hidden * parentReference is sometimes necesary for {@link GoOneDrive}. It is the * parent information * of a Microsoft OneDrive item, if the item has a parent. * No other {@link GoCloudStorage} subclasses use it. It is unlikely one will use this field without editing source code. */ parentReference?: Object } /** * @private * @hidden * A simple interface acting as a wrapper for a Promise that gurantees the existence of the promise field. * DeferredPromise is important in that it allows for a Promise to be returned by one function and resolved within another. * You may call .resolve and .reject on this field as you may a standard ES6 Promise. After resolving / rejecting * deferredPromise.promise, it is recommended you reset it by calling {@link GoCloudStorage.makeDeferredPromise}. Example:

* * ```js * * // function a returns the "promise" field of deferredPromise * function a () { * return gcs.deferredPromise.promise * } * * // function b resolves the "promise" field of deferredPromise and resets it * function b () { * gcs.deferredPromise.promise.resolve("Promise resolved"); // resolve * gcs.deferredPromise.promise = gcs.makeDeferredPromise(); // reset * } * * a(); // return deferredPromise.promise * b(); // b is called after a (and before anything else can resolve deferredPromise.promise), so b resolves deferredPromise.promise * * ``` */ export interface DeferredPromise { promise: any; // this really isn't "any", it's an ES6 Promise, but declaring it as Promise removes .resolve() functionality??? } /** * An abstract class for storing GoJS Diagram * models in various cloud storage services. *

GoCloudStorage is never used on its own. Its subclasses can be used to manage diagram model storage programatically, * or any subset of GoCloudStorage subclasses can be bundled and used graphically with the {@link GoCloudStorageManager}.

*

Note: With the exception of {@link GoLocalStorage}, all GoCloudStorage subclasses must be used in pages served * on a web server.

* @category Storage */ export class GoCloudStorage { private _managedDiagrams: go.Diagram[]; private _clientId: string; private _isAutoSaving: boolean; private _currentDiagramFile: DiagramFile; private _ui: HTMLElement; private _defaultModel: string; /** * @private * @hidden */ protected _serviceName: string; /** * @private * @hidden */ protected _deferredPromise: DeferredPromise; private _iconsRelativeDirectory: string; /** * @constructor * @param {go.Diagram|go.Diagram[]} managedDiagrams An array of GoJS Diagrams whose model(s) will be saved to * / loaded from a cloud storage service. Can also be a single Diagram. * @param {string} clientId The client ID of the cloud storage application to use (given by the cloud storage service to developer). Not needed for all subclasses. * @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 GoCloudStorage exists, in which * the storage service brand icons can be found. The default value is "../goCloudStorageIcons/". */ constructor(managedDiagrams: go.Diagram|go.Diagram[], defaultModel?: string, clientId?: string, iconsRelativeDirectory?: string) { if (managedDiagrams instanceof go.Diagram) managedDiagrams = [managedDiagrams]; this._managedDiagrams = managedDiagrams; this._currentDiagramFile = { name: null, id: null, path: null }; this._isAutoSaving = true; if (defaultModel) this._defaultModel = defaultModel; if (clientId) this._clientId = clientId; else clientId = null; this._iconsRelativeDirectory = (!!iconsRelativeDirectory) ? iconsRelativeDirectory : "../goCloudStorageIcons/"; const menu = document.createElement('div'); menu.className = 'goCustomFilepicker'; menu.style.visibility = 'hidden'; document.getElementsByTagName('body')[0].appendChild(menu); this._ui = menu; this._deferredPromise = { promise: this.makeDeferredPromise() }; // enable autosaving capability function addAutoSave(d: go.Diagram) { d.addModelChangedListener(function (e: go.ChangedEvent) { if (e.isTransactionFinished && storage.isAutoSaving && e.oldValue !== "") { if (storage.currentDiagramFile.name) { storage.save(); } } }); } const d = this.managedDiagrams; const storage = this; if (d instanceof go.Diagram) { addAutoSave(d); } else for (let i = 0; i < d.length; i++) { addAutoSave(d[i]); } } /** * Get / set the GoJS Diagrams associated with this instance of GoCloudStorage. * Set with a parameter during construction. * @function. * @return {go.Diagram[]} */ get managedDiagrams(): go.Diagram[] { return this._managedDiagrams } set managedDiagrams(value: go.Diagram[]) { this._managedDiagrams = value } /** * Get / set the defaultModel data for the app used by an instance of GoCloudStorage. defaultModel is used when creating new diagrams. See {@link create}. * @function. * @return {string} */ get defaultModel(): string { return this._defaultModel } set defaultModel(value: string) { this._defaultModel = value } /** * Get / set iconsRelativeDirectory, the directory path relative to the page in which this instance of GoCloudStorage exists, in which * the storage service brand icons can be found. The default value is "../goCloudStorageIcons/". * @function. * @return {string} */ get iconsRelativeDirectory(): string { return this._iconsRelativeDirectory } set iconsRelativeDirectory(value: string) { this._iconsRelativeDirectory = value } /** * Get the clientId for the app using the cloud storage service. This is usually given by the cloud storage provider's dev console or similar. * Set with a parameter during construction. * @function. * @return {string} */ get clientId(): string { return this._clientId } /** * Get or set the currently open {@link DiagramFile}. By default, currentDiagramFile is set when a file is loaded from storage, saved to storage * (if saved to a different path from the currentDiagramFile.path), or deleted from storage (if the deleted file is the currently open one). * The default value is a {@link DiagramFile} with null id, name, and path values. * @function. * @return {Object} */ get currentDiagramFile(): DiagramFile { return this._currentDiagramFile } set currentDiagramFile(value: DiagramFile) { this._currentDiagramFile = value } /** * Get or set isAutoSaving property. If true, the {@link managedDiagrams} will be saved to storage after every * Transaction (only if {@link currentDiagramFile} holds a non-null path value). * Additionally, if isAutoSaving is true, users will be prompted to save newly created diagrams when created with {@link create}. * The default value for isAutoSaving is true. * @function. * @return {boolean} */ get isAutoSaving(): boolean { return this._isAutoSaving } set isAutoSaving(value: boolean) { this._isAutoSaving = value } /** * Get the name of the cloud storage service being used; i.e. "Dropbox" * @function. * @return {string} */ get serviceName(): string { return this._serviceName }; /** * Get the UI element associated with this instance of GoCloudStorage. This is a custom filepicker window for {@link GoDropBox} and * {@link GoLocalStorage}. It is a save prompt for {@link GoOneDrive} and {@link GoGoogleDrive} (both these classes use third party * UI for storage navigation, provided by Microsoft and Google, respectively). The UI element is created during construction. * @function. * @return {HTMLElement} */ get ui(): HTMLElement { return this._ui } /** *

Get / set deferredPromise property. This is a special property that is simply an Object with a "promise" field, which contains an * ES6 Promise. DeferredPromise is important in that it allows for a Promise to be * returned by one function and resolved within another. You may call .resolve and .reject on this field as you may * a standard ES6 Promise. After resolving / rejecting deferredPromise.promise, it is recommended you reset it by calling {@link makeDeferredPromise}. Example:

* * ```js * * // function a returns the "promise" field of deferredPromise * function a () { * return gcs.deferredPromise.promise * } * * // function b resolves the "promise" field of deferredPromise and resets it * function b () { * gcs.deferredPromise.promise.resolve("Promise resolved"); // resolve * gcs.deferredPromise.promise = gcs.makeDeferredPromise(); // reset * } * * a(); // return deferredPromise.promise * b(); // b is called after a (and before anything else can resolve deferredPromise.promise), so b resolves deferredPromise.promise * * ``` * @function. * @return {Object} * get deferredPromise(): DeferredPromise { return this._deferredPromise } set deferredPromise(value: DeferredPromise) { this._deferredPromise = value }*/ /** *

Explicitly authorize a currently-signed in user of the storage service to use the application associated with this * instance of GoCloudStorage (via {@link clientId}. If no currently signed-in user exists, prompt user to sign into their account, then authorize that account.

*

Note: Authorization does not occur (and is not possible or necessary) with {@link GoLocalStorage}. Instead, * {@link GoLocalStorage.authorize} ensures localStorage exists in the browser.

* @param {boolean} refreshToken Whether to get a new access token (true) or try to find / use an existing one. Exact behavior varies * from subclass to subclass. See: * * @return {Promise} Returns a Promise that resolves with a boolean stating whether authorization was succesful (true) or failed (false). */ public authorize(refreshToken: boolean = false) { return new Promise(function(resolve: Function, reject: Function){ console.error("authorize not implemented"); reject(false); }); } /** * @private * @hidden * Returns a new {@link DeferredPromise}. Use this method to reset {@link deferredPromise}.promise property after deferredPromise.promise has been resolved. For example: * * ```js * * // function a returns the "promise" field of deferredPromise * function a () { * return gcs.deferredPromise.promise * } * * // function b resolves the "promise" field of deferredPromise and resets it * function b () { * gcs.deferredPromise.promise.resolve("Promise resolved"); // resolve * gcs.deferredPromise.promise = gcs.makeDeferredPromise(); // reset * } * * a(); // return deferredPromise.promise * b(); // b is called after a (and before anything else can resolve deferredPromise.promise), so b resolves deferredPromise.promise * * ``` * @return {Promise} */ public makeDeferredPromise(): Promise { let res: Function, rej: Function; let promise: any = new Promise((resolve: Function, reject: Function) => { res = resolve; rej = reject; }); promise.resolve = res; promise.reject = rej; return promise; } /** * Get information about the currently logged in user. This information varies from subclass to subclass. For more info, see: *
    *
  • {@link GoLocalStorage.getUserInfo}
  • *
  • {@link GoDropBox.getUserInfo}
  • *
  • {@link GoGoogleDrive.getUserInfo}
  • *
  • {@link GoOneDrive.getUserInfo}
  • *
* @return {Promise} Returns a Promise that resolves with information about the currently logged in user */ public getUserInfo() { return new Promise(function(resolve: Function, reject: Function){ reject("getUserInfo not implemented"); }); } /** * Hide the {@link ui} element associated with this instance of GoCloudStorage. Used in some UI element onclicks. * @param {boolean} isActionCanceled If action (save, delete, load) is canceled, resolve Promise (returned previously in * {@link showUI}) with a 'Canceled' notification. Default value is false. */ public hideUI(isActionCanceled: boolean = false) { const storage = this; storage.ui.style.visibility = 'hidden'; if (isActionCanceled) { var action = document.getElementById('actionButton').innerHTML; storage._deferredPromise.promise.resolve(action + ' canceled by user'); storage._deferredPromise.promise = storage.makeDeferredPromise(); } } /** * Check whether a file exists at a given path. * @param {string} path A valid filepath. What is meant by this varies from subclass to subclass. Rules for valid filepaths by subclass: *
    *
  • {@link GoLocalStorage}: Just the filename (the key in local storage); i.e. example.diagram
  • *
  • {@link GoDropBox}: /{path-to-file}/{filename}; i.e. /Public/example.diagram
  • *
  • {@link GoGoogleDrive}: Use Google Drive-given file IDs. Parameter is still called 'path' in GoGoogleDrive methods to preserve system nomenclature.
  • *
  • {@link GoOneDrive}: /drive/root:/{path-to-file}/{filename}; i.e. /drive/root:/Documents/example.diagram
  • *
* @return {Promise} Returns a Promise that resolves with a boolean stating whether a file exists at a given path. */ public checkFileExists(path: string) { return new Promise(function(resolve: Function, reject: Function){ reject("checkFileExists not implemented"); }); } /** * Get the file at a given path in storage. The exact file data given varies from storage service * to storage service, though all include name, path, id data. The exact syntax of these data fields may vary. * See subclass-specific documentation for more details. *
    *
  • {@link GoLocalStorage.getFile}
  • *
  • {@link GoDropBox.getFile}
  • *
  • {@link GoGoogleDrive.getFile}
  • *
  • {@link GoOneDrive.getFile}
  • *
* @param {string} path A valid filepath. What is meant by this varies from subclass to subclass. Rules for valid filepaths by subclass: *
    *
  • {@link GoLocalStorage}: Just the filename (the key in local storage); i.e. example.diagram
  • *
  • {@link GoDropBox}: /{path-to-file}/{filename}; i.e. /Public/example.diagram
  • *
  • {@link GoGoogleDrive}: Use Google Drive-given file IDs. Parameter is still called 'path' in GoGoogleDrive methods to preserve system nomenclature.
  • *
  • {@link GoOneDrive}: /drive/root:/{path-to-file}/{filename}; i.e. /drive/root:/Documents/example.diagram
  • *
* @return {Promise} Return a Promise that resolves with the file at a given path in storage(or null). */ public getFile(path: string) { return new Promise(function(resolve: Function, reject: Function){ throw Error("getFile not implemented"); }); } /** * Show the {@link ui} element associated with this instance of GoCloudStorage. * @param {string} action Clarify what action is being done after file selection. Acceptable values: Save, Delete, Load * @return {Promise} Returns a Promise that resolves (in {@link save}, {@link load}, or {@link remove} with an {@link DiagramFile} * representing the saved/loaded/deleted file */ public showUI(action: string) { return new Promise(function(resolve: Function, reject: Function){ throw Error("showUI not implemented"); }); } /** * Set each of {@link managedDiagrams}' .model to {@link defaultModel} (if defaultModel is null, * each model will be set to a new GraphLinksModel by default). * If {@link isAutoSaving} is true and no path parameter is supplied, users will be immediately prompted to save their new diagrams to cloud storage. * @param {string} path If a valid filepath is supplied, save each of {@link managedDiagrams}' model data to this path. No UI of any sort appears. * What is meant by "valid filepath" varies from subclass to subclass. Rules for valid filepaths by subclass: *
    *
  • {@link GoLocalStorage}: Just the filename (the key in local storage); i.e. example.diagram
  • *
  • {@link GoDropBox}: /{path-to-file}/{filename}; i.e. /Public/example.diagram
  • *
  • {@link GoGoogleDrive}: Use Google Drive-given file IDs. Parameter is still called 'path' in GoGoogleDrive methods to preserve system nomenclature.
  • *
  • {@link GoOneDrive}: /drive/root:/{path-to-file}/{filename}; i.e. /drive/root:/Documents/example.diagram
  • *
* @return {boolean} Returns a Promise that resolves with a {@link DiagramFile} representing the newly created file. */ public create(path?: string) { const storage = this; const diagrams = storage.managedDiagrams; return new Promise(function (resolve: Function, reject: Function) { storage.currentDiagramFile = { name: null, id: null, path: null }; function makeNewDiagram(d: go.Diagram) { if (storage.defaultModel) d.model = go.Model.fromJson(JSON.parse(storage.defaultModel)); else d.model = new go.GraphLinksModel; } if (storage.managedDiagrams instanceof go.Diagram) { makeNewDiagram(storage.managedDiagrams); } else for (let i = 0; i < storage.managedDiagrams.length; i++) { makeNewDiagram(storage.managedDiagrams[i]); } if (storage.isAutoSaving) { if (path) { resolve(storage.save(path)); } else resolve(storage.saveWithUI()); } else resolve("New diagram created.") // no prompt to save }); } /** * @private * @hidden * Returns the data to save to storage. This is a string representation of a JSON-like object. * Keys are the div IDs of the diagrams being saved. Values are the model.toJson() values for those diagrams. * @return {string} */ protected makeSaveFile() { let item: string = '{\n'; const storage = this; if (storage.managedDiagrams.length == 0) return; for (let i = 0; i < storage.managedDiagrams.length; i++) { let diagram: go.Diagram = storage.managedDiagrams[i]; let div: string = diagram.div.id; item += '"' + div + '"' + ': ' + diagram.model.toJson(); if (i+1 !== storage.managedDiagrams.length) item += ',\n'; } item += '\n}'; return item; } /** * @private * @hidden * Loads all models in a saved file to their respective diagrams */ protected loadFromFileContents(fileContents: string) { const storage = this; let models = JSON.parse(fileContents); for (let divId in models) { let model = models[divId]; let div: HTMLDivElement = (document.getElementById(divId)); let diagram: go.Diagram = go.Diagram.fromDiv(div); if (diagram) { diagram.model = go.Model.fromJson(JSON.stringify(model)); } else { throw Error("No Diagram on page is associated with a div with id " + divId); } } } /** * Save the current diagram's model data to cloud storage with the {@link ui} for this class. * @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file. */ public saveWithUI() { return new Promise(function(resolve: Function, reject: Function){ reject("saveWithUI not implemented"); }); } /** * Save {@link managedDiagrams}' model data to storage. 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 storage corresponding to currentDiagramFile.path with current managedDiagrams' model data. *

Rules for valid filepaths by subclass:

*
    *
  • {@link GoLocalStorage}: Just the filename (the key in local storage); i.e. example.diagram
  • *
  • {@link GoDropBox}: /{path-to-file}/{filename}; i.e. /Public/example.diagram
  • *
  • {@link GoGoogleDrive}: Use Google Drive-given file IDs. Parameter is still called 'path' in GoGoogleDrive methods to preserve system nomenclature.
  • *
  • {@link GoOneDrive}: /drive/root:/{path-to-file}/{filename}; i.e. /drive/root:/Documents/example.diagram
  • *
* @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file */ public save(path?: string) { return new Promise(function(resolve: Function, reject: Function){ reject("save not implemented"); }); } /** * Load diagram model data from a given cloud storage-specific file path into {@link managedDiagrams}. * @param {string} path A valid filepath. What is meant by this varies from subclass to subclass. Rules for valid filepaths by subclass: *
    *
  • {@link GoLocalStorage}: Just the filename (the key in local storage); i.e. example.diagram
  • *
  • {@link GoDropBox}: /{path-to-file}/{filename}; i.e. /Public/example.diagram
  • *
  • {@link GoGoogleDrive}: Use Google Drive-given file IDs. Parameter is still called 'path' in GoGoogleDrive methods to preserve system nomenclature.
  • *
  • {@link GoOneDrive}: /drive/root:/{path-to-file}/{filename}; i.e. /drive/root:/Documents/example.diagram
  • *
* @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} */ public load(path: string) { return new Promise(function(resolve: Function, reject: Function){ reject("load not implemented"); }); } /** * Load diagram model data from cloud storage into {@link managedDiagrams} using the {@link ui} for this class. * @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the loaded file */ public loadWithUI() { return new Promise(function(resolve: Function, reject: Function){ reject("loadWithUI not implemented"); }); } /** * Remove a file containing diagram model data at a given cloud storage-specific file path. * @param {string} path A valid filepath. What is meant by this varies from subclass to subclass. Rules for valid filepaths by subclass: *
    *
  • {@link GoLocalStorage}: Just the filename (the key in local storage); i.e. example.diagram
  • *
  • {@link GoDropBox}: /{path-to-file}/{filename}; i.e. /Public/example.diagram
  • *
  • {@link GoGoogleDrive}: Use Google Drive-given file IDs. Parameter is still called 'path' in GoGoogleDrive methods to preserve system nomenclature.
  • *
  • {@link GoOneDrive}: /drive/root:/{path-to-file}/{filename}; i.e. /drive/root:/Documents/example.diagram
  • *
* @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file. */ public remove(path: string) { return new Promise(function(resolve: Function, reject: Function){ reject("remove not implemented"); }); } /** * Remove a given diagram from cloud storage using the {@link ui} for this class. * @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file */ public removeWithUI() { return new Promise(function(resolve: Function, reject: Function){ reject("removeWithUI not implemented"); }); } }