feat: 后端全量更新 - 含所有本次需求

- Contract.php: 返回合约账户余额(balance_contract)
- My.php: 地址管理增加BTC/ETH
- AppContract.php: 一键平仓(closeall)
- AppProxy.php: 代理专属注册链接 + 分级权限(L1/L2)
- site.php: 手续费减半(0.018→0.009)
- agent_permission_setup.sql: 代理权限SQL
- crypto_news_crawler.py: 新闻自动采集脚本
This commit is contained in:
li
2026-03-30 20:16:32 +08:00
commit 1b24994e74
6721 changed files with 1308571 additions and 0 deletions
+651
View File
@@ -0,0 +1,651 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* GoCloudStorage.js
*/
// import { Promise } from 'es6-promise';
import * as go from 'gojs';
/**
* 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:
* - {@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`
*/
path: string;
/**
* @private
* @hidden
* token is sometimes necesary for {@link GoOneDrive}. This is an access token given by the
* <a href="https://dev.onedrive.com/sdk/js-v72/js-picker-overview.htm">
* Microsoft OneDrive Filepicker</a> 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
* <a href="https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/resources/itemreference"> parent information</a>
* 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<any> removes .resolve() functionality???
}
/**
* An abstract class for storing GoJS {@link Model}s 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: Array<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 _className: string;
/**
* @private
* @hidden
*/
protected _deferredPromise: DeferredPromise;
private _iconsRelativeDirectory: string;
/**
* @constructor
* @param {go.Diagram|go.Diagram[]} managedDiagrams An array of GoJS {@link Diagram}s 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 {@link Model#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 | Array<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 (clientId) this._clientId = clientId;
else clientId = null;
// if defaultModel does not begin with "{", try using that as the iconsRelativeDirectory
if (defaultModel) {
const firstChar: string = defaultModel.trim().charAt(0);
if (defaultModel && firstChar === '{') this._defaultModel = defaultModel;
if (firstChar !== '{' && !iconsRelativeDirectory) this._iconsRelativeDirectory = defaultModel;
else this._iconsRelativeDirectory = (!!iconsRelativeDirectory) ? iconsRelativeDirectory : '../goCloudStorageIcons/';
} else {
this._defaultModel = null;
this._iconsRelativeDirectory = (!!iconsRelativeDirectory) ? iconsRelativeDirectory : '../goCloudStorageIcons/';
}
// make sure iconsRelativeDirectory has a trailing '/'
const lastChar = this._iconsRelativeDirectory.charAt(this._iconsRelativeDirectory.length - 1);
if (lastChar !== '/') {
this._iconsRelativeDirectory += '/';
}
const menu = document.createElement('div');
menu.className = 'goCustomFilepicker';
menu.style.visibility = 'hidden';
// TODO -- this assumes the document has a body element, is this OK??
document.getElementsByTagName('body')[0].appendChild(menu);
this._ui = menu;
this._deferredPromise = { promise: this.makeDeferredPromise() };
// enable autosaving capability
// tslint:disable-next-line:no-shadowed-variable
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 {@link Diagram}s associated with this instance of GoCloudStorage.
* Set with a parameter during construction.
* @function.
* @return {go.Diagram[]}
*/
get managedDiagrams(): Array<go.Diagram> { return this._managedDiagrams; }
set managedDiagrams(value: Array<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
* {@link 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 name of the class; i.e. "GoDropbox"
* @function.
* @return {string}
*/
get className(): string { return this._className; }
/**
* 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; }
/**
* 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:
* - {@link GoLocalStorage#authorize}
* - {@link GoDropBox#authorize}
* - {@link GoGoogleDrive#authorize}
* - {@link GoOneDrive#authorize}
* @return {Promise<any>} Returns a Promise that resolves with a boolean stating whether authorization was succesful (true) or failed (false).
*/
public authorize(refreshToken: boolean = false): Promise<any> {
return new Promise(function(resolve: Function, reject: Function) {
reject('authorize not implemented');
});
}
/**
* @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<Object>}
*/
public makeDeferredPromise(): Promise<Object> {
let res: Function; let rej: Function;
const 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 GoDropBox#getUserInfo}
* - {@link GoGoogleDrive#getUserInfo}
* - {@link GoOneDrive#getUserInfo}
* @return {Promise<any>} 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) {
const action: string = 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<boolean>} 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<any>} 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 {@link 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 Optional. 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`
* @param {boolean} saveBefore Optional. If true, the user will be prompted to save their current diagram(s) before GoCloudStorage makes a new one.
* The default value is false.
* @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the newly created file.
*/
public create(path?: string, saveBefore: boolean = false) {
const storage = this;
function makeNewDiagram(d: go.Diagram) {
if (storage.defaultModel) d.model = go.Model.fromJson(JSON.parse(storage.defaultModel));
else d.model = new go.GraphLinksModel();
}
return new Promise(function(resolve: Function, reject: Function) {
// TODO -- offer the chance for user to save their current diagram
if (saveBefore) {
storage.promptUserToSaveBeforeNew().then(function(resp) {
if (resp) {
storage.saveWithUI().then(function(diagramFile) {
storage.currentDiagramFile = { name: null, id: null, path: null };
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
});
} else {
storage.currentDiagramFile = { name: null, id: null, path: null };
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
}
});
}
if (!saveBefore) {
storage.currentDiagramFile = { name: null, id: null, path: null };
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 promptUserToSaveBeforeNew() {
return new Promise(function(resolve: Function, reject: Function) {
// Remove any prior window like this
const d = document.getElementById('gcs-save-before-new');
if (d) {
document.body.removeChild(d);
}
const div = document.createElement('div');
div.id = 'gcs-save-before-new';
const p = document.createElement('p');
p.innerText = 'Save current diagram(s) before creating a new file?';
const yb = document.createElement('button');
yb.innerText = 'Yes';
const nb = document.createElement('button');
nb.innerText = 'No';
yb.onclick = function() {
document.body.removeChild(div);
resolve(true);
};
nb.onclick = function() {
document.body.removeChild(div);
resolve(false);
};
nb.style['float'] = 'right';
div.style['font-family'] = 'Arial, Helvetica, sans-serif';
div.style['width'] = '400px';
div.style['top'] = '25%';
div.style['left'] = '40%';
div.style['position'] = 'absolute';
div.style['border'] = '1px solid black';
div.style['padding'] = '10px';
div.style['box-shadow'] = '10px 10px 5px #888888';
div.style['background'] = 'white';
div.style['z-index'] = '100';
div.appendChild(p);
div.appendChild(yb);
div.appendChild(nb);
document.body.appendChild(div);
});
}
/**
* @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++) {
const diagram: go.Diagram = storage.managedDiagrams[i];
const div: string = diagram.div.id;
const _model: string = diagram.model.toJson();
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 models = JSON.parse(fileContents);
for (const divId in models) {
const model = models[divId];
const div: HTMLDivElement = document.getElementById(divId) as HTMLDivElement;
const diagram: any = 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');
});
}
}
@@ -0,0 +1,381 @@
/**
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* Go Cloud Storage Manager
*/
// import { Promise } from 'es6-promise';
import * as go from 'gojs';
import * as gcs from './GoCloudStorage.js';
/**
* Class for easily saving / loading GoJS {@link Model}s to / from a user-defined set of Cloud Storage Services with a pre-defined UI.
*
* GoCloudStorageManager holds a set of {@link GoCloudStorage} subclass instances ({@link #storages}) to manage. When one is selected from the
* storage selection {@link #menu}, it becomes the {@link #currentStorage} property, which is used to save / load / delete / create files.
* @category Storage
*/
export class GoCloudStorageManager {
private _storages: go.Set<gcs.GoCloudStorage>;
private _currentStorage: gcs.GoCloudStorage;
private _menu: HTMLElement;
private _deferredPromise: gcs.DeferredPromise;
private _iconsRelativeDirectory: string;
/**
* @constructor
* @param {go.Set<gcs.GoCloudStorage>} storages Contains valid instances of {@link GoCloudStorage} subclasses. Use at most one instance of each subclass.
* @param {string} iconsRelativeDirectory The directory path relative to the page in which this instance of GoCloudStorageManager exists, in which
* the storage service brand icons can be found. The default value is "../goCloudStorageIcons/".
* **Note:** If this parameter is supplied, it is used as for the "iconsRelativeDirectory" constructor parameter for each instance
* this instance of GoCloudStorageManager manages in {@link #storages}.
*/
constructor(storages: go.Set<gcs.GoCloudStorage> | Array<gcs.GoCloudStorage>, iconsRelativeDirectory?: string) {
if (storages instanceof Array) {
const storagesSet = new go.Set<gcs.GoCloudStorage>();
for (let i = 0; i < storages.length; i++) {
if (!(storages[i] instanceof gcs.GoCloudStorage)) {
throw new Error("Cannot create GoCloudStorageManager; provided 'storages' parameter elements are not all of type GoCloudStorage");
} else {
storagesSet.add(storages[i]);
}
}
storages = storagesSet;
}
if (!(storages instanceof go.Set) || !storages) throw Error("Cannot create GoCloudStorageManager with provided 'storages' parameter");
const storageManager = this;
storageManager._storages = storages;
storageManager._currentStorage = storages.first();
const menu = document.createElement('div');
menu.id = 'goCloudStorageManagerMenu';
storageManager._menu = menu;
storageManager._deferredPromise = { promise: gcs.GoCloudStorage.prototype.makeDeferredPromise() };
storageManager._iconsRelativeDirectory = (!!iconsRelativeDirectory) ? iconsRelativeDirectory : '../goCloudStorageIcons/';
if (iconsRelativeDirectory) {
storageManager._storages.iterator.each(function(storage) {
storage.iconsRelativeDirectory = iconsRelativeDirectory;
});
}
// if href includes a certain string, we just authenticated DropBox, so use GoDropBox as "currentStorage"
if (window.location.href.indexOf('account_id=dbid') !== -1) {
storages.iterator.each(function(storage) {
if (storage.className === 'GoDropBox') {
storageManager._currentStorage = storage;
// this will force a load of the diagram data that existed before the Dropbox auth flow
storageManager.currentStorage.authorize();
}
});
}
// append the menu ui to the document
document.getElementsByTagName('body')[0].appendChild(storageManager.menu);
}
/**
* Get storages ({@link GoCloudStorage} subclass instances) managed by an instance of GoCloudStorageManager. At most, there should be only one instance of each subclass.
* This is set with a parameter during construction.
* @function.
* @return {go.Set<gcs.GoCloudStorage>}
*/
get storages(): go.Set<gcs.GoCloudStorage> { return this._storages; }
/**
* Get / set iconsRelativeDirectory, the directory path relative to the page in which this instance of GoCloudStorageManager 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 GoCloudStorageManager menu, from which a user chooses which storage service for this instance of GoCloudStorageManager to actively manage (see {@link #currentStorage}).
* This is created (as a blank div) during construction. Its contents are populated during {@link #selectStorageService}.
* @function.
* @return {HTMLElement}
*/
get menu(): HTMLElement { return this._menu; }
/**
* Get / set the {@link GoCloudStorage} subclass this instance of GoCloudStorageManager is actively managing.
* @function.
* @return {gcs.GoCloudStorage}
*/
get currentStorage() { return this._currentStorage; }
set currentStorage(value: gcs.GoCloudStorage) { this._currentStorage = value; }
/**
* Creates a new diagram with {@link #currentStorage}'s default model data (see {@link GoCloudStorage#defaultModel}.
* If currentStorage.isAutoSaving is true, prompt to save it to to currentStorage's storage service.
* if {@link #currentStorage}'s {@link GoCloudStorage#isAutoSaving} is true).
* @param {boolean} saveBeforeCreate Whether or not to prompt the user to save their current work before creating a new diagram.
* See more at {@link GoCloudStorage#create}. Default value is false.
* @return {Promise} Returns a Promise that resolves a {@link DiagramFile} representing the newly created file (if file was saved).
*/
public create(saveBeforeCreate: boolean = false) {
const storageManager = this;
return new Promise(function(resolve: Function, reject: Function) {
resolve(storageManager.handleAction('Create', saveBeforeCreate));
});
}
/**
* Launches the load interface for {@link #currentStorage}.
* @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the loaded file.
*/
public load() {
const storageManager = this;
return new Promise(function(resolve: Function, reject: Function) {
resolve(storageManager.handleAction('Load'));
});
}
/**
* Launches the remove interface for {@link #currentStorage}.
* @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file.
*/
public remove() {
const storageManager = this;
return new Promise(function(resolve: Function, reject: Function) {
resolve(storageManager.handleAction('Remove'));
});
}
/**
* Either launches the save interface for {@link #currentStorage} or just saves the {@link GoCloudStorage#managedDiagrams}' model data to
* storage at the path supplied in currentStorage's {@link GoCloudStorage#currentDiagramFile}.path value, depending on a parameter.
* @param {boolean} isSaveAs If true, show the save interface for currentStorage. If false, save currentStorage's managedDiagrams' model data to storage.
* Default value is true.
* @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file.
*/
public save(isSaveAs: boolean = true) {
const storageManager = this;
return new Promise(function(resolve: Function, reject: Function) {
if (isSaveAs) resolve(storageManager.handleAction('SaveAs'));
else resolve(storageManager.handleAction('Save'));
});
}
/**
* Display a message on the screen for a given number of seconds. Can be used for a variety of purposes, but a common one is to
* notify users when a file has been loaded / saved / deleted / created by handling the {@link DiagramFile} argument in the
* "then" function of returned Promises (from functions {@link #load}, {@link #create}, {@link #save},
* {@link #remove}) by displaying it as a message.
* @param msg Message to display
* @param seconds Number of seconds to display the message for. If no value is provided, the message will display for two seconds.
*/
public showMessage(msg: string, seconds?: number) {
if (!seconds) seconds = 2;
const messageBox = document.createElement('div');
messageBox.id = 'goCloudStorageManagerMessageBox';
messageBox.innerHTML = '<p>' + msg + '</p>';
document.body.appendChild(messageBox);
setTimeout(function() {
messageBox.style.opacity = '0';
setTimeout(function() { messageBox.parentNode.removeChild(messageBox); }, 1000);
}, 1000 * seconds);
}
/**
* Get the path to the icon for a given {@link GoCloudStorage#className}
* @param className
*/
public getStorageIconPath(className: string) {
const storageManager = this;
if (storageManager.iconsRelativeDirectory == null || storageManager.iconsRelativeDirectory === undefined) return null;
let src: string = storageManager.iconsRelativeDirectory;
switch (className) {
case 'GoGoogleDrive': {
src += 'googleDrive.jpg';
break;
}
case 'GoOneDrive': {
src += 'oneDrive.png';
break;
}
case 'GoLocalStorage': {
src += 'localStorage.png';
break;
}
case 'GoDropBox': {
src += 'dropBox.png';
break;
}
}
return src;
}
/**
* Display options ({@link #storages}) supported by this instance of GoCloudStorageManager.
* Sets {@link #currentStorage} to user's choice.
* @return {Promise} Returns a Promise that resolves with the new {@link #currentStorage} instance
*/
public selectStorageService() {
const storageManager = this;
const storages = this.storages;
return new Promise(function(resolve: Function, reject: Function) {
const menu = storageManager.menu;
const title: string = 'Select Storage Service';
menu.innerHTML = '<strong>' + title + '</strong><hr></hr>';
const selectedStorage: HTMLElement = document.createElement('p');
selectedStorage.id = 'gcsmSelectedStorage'; // Go Cloud Storage Manager selected storage
selectedStorage.innerHTML = storageManager.currentStorage.serviceName;
menu.appendChild(selectedStorage);
// display the name of the currently selected radio button's storage service
menu.onchange = function() {
const radios: NodeListOf<HTMLInputElement> = document.getElementsByName('storageSelection') as NodeListOf<HTMLInputElement>;
let selectedStorageClassName: string = null;
for (let i = 0; i < radios.length; i++) {
if (radios[i].checked) {
selectedStorageClassName = radios[i].id;
}
}
let serviceNameStr: string = '';
storages.iterator.each(function(s) {
if (s.className === selectedStorageClassName) {
serviceNameStr = s.serviceName;
}
});
document.getElementById('gcsmSelectedStorage').innerHTML = serviceNameStr;
};
// document.getElementsByTagName('body')[0].appendChild(storageManager.menu);
storageManager.menu.style.visibility = 'visible';
const optionsDiv: HTMLElement = document.createElement('div');
optionsDiv.id = 'storageOptions';
const it = storages.iterator;
it.each(function(storage) {
// create a radio input box for each service managed by this instace of GoCloudStorageManager
const type: string = storage.className;
const src: string = storageManager.getStorageIconPath(type);
const isChecked: boolean = storage.className === storageManager.currentStorage.className;
let checkedStr: string = '';
if (isChecked) checkedStr = 'checked';
optionsDiv.innerHTML +=
'<label>' +
'<input id=' + type + " type='radio' name='storageSelection' " + checkedStr + ' />' +
"<img class='storageLogo' src=" + src + ' >';
});
menu.appendChild(optionsDiv);
// tslint:disable-next-line:max-line-length
const description: string = 'This will be where you save / load diagram model data to / from. You will need to grant GoCloudStorage permission to access your files on the selected storage service.';
menu.innerHTML += "<p class='description'>" + description + '</p>';
const submitDiv: HTMLElement = document.createElement('div');
const actionButton = document.createElement('button');
actionButton.id = 'actionButton';
actionButton.textContent = 'Select';
actionButton.onclick = function() {
// set currentStorage
const radios: NodeListOf<HTMLInputElement> = document.getElementsByName('storageSelection') as NodeListOf<HTMLInputElement>;
// tslint:disable-next-line:no-shadowed-variable
let selectedStorage: string = null;
for (let i = 0; i < radios.length; i++) {
if (radios[i].checked) {
selectedStorage = radios[i].id;
}
}
storageManager.storages.each(function(storage) {
if (storage.className === selectedStorage) storageManager.currentStorage = storage;
});
if (storageManager.currentStorageNeedsAuth()) {
storageManager.currentStorage.authorize().then(function(resp) {
});
}
resolve(storageManager.currentStorage);
storageManager.hideMenu();
};
submitDiv.appendChild(actionButton);
menu.appendChild(submitDiv);
const cancelDiv: HTMLElement = document.createElement('div');
const cancelButton = document.createElement('button');
cancelButton.id = 'cancelButton';
cancelButton.textContent = 'Cancel';
cancelButton.onclick = function() {
storageManager.hideMenu();
};
cancelDiv.appendChild(cancelButton);
menu.appendChild(cancelDiv);
});
}
/**
* Hide the storage selection {@link #menu}
*/
public hideMenu() {
const storageManager = this;
storageManager.menu.style.visibility = 'hidden';
}
/**
* @private
* Some classes need to be explicitly authorized (get a user-specific auth token) for use with GoCloudStorageManager. Some do not.
* This function simply examines the currently active storage and determines whether or not this explicit authorization is needed.
* @return {boolean}
*/
private currentStorageNeedsAuth() {
const storageManager = this;
const currentStorageClass: string = storageManager.currentStorage.className;
if (currentStorageClass === 'GoGoogleDrive' || currentStorageClass === 'GoDropBox') return true;
return false;
}
/**
* Handle an action with the current {@link #currentStorage}. Possible values for `action` can be:
* - Load
* - Save
* - Save As
* - Create
* - Remove
* @param action
* @param saveBeforeCreate Whether or not to prompt the user to save their current work before creating a new file.
* Default value is false. See more at {@link GoCloudStorage#create}
*/
public handleAction(action: string, saveBeforeCreate: boolean = false) {
const storageManager = this;
const storage: gcs.GoCloudStorage = storageManager.currentStorage;
return new Promise(function(resolve: Function, reject: Function) {
function doAction() {
switch (action) {
case 'Load': {
resolve(storage.loadWithUI());
break;
}
case 'SaveAs': {
resolve(storage.saveWithUI());
break;
}
case 'Save': {
resolve(storage.save());
break;
}
case 'Remove': {
resolve(storage.removeWithUI());
break;
}
case 'Create': {
resolve(storage.create(null, saveBeforeCreate));
break;
}
}
storageManager.hideMenu();
}
if (storageManager.currentStorageNeedsAuth()) {
storage.authorize().then(function() {
doAction();
});
} else doAction();
});
}
}
+672
View File
@@ -0,0 +1,672 @@
/*
* 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);
});
}
}
+492
View File
@@ -0,0 +1,492 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* Go Google Drive
*/
// 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 Google Drive.
* Uses the <a href="https://developers.google.com/drive/v3/reference/">Google Drive V3 API</a> by use of a
* <a href="https://developers.google.com/api-client-library/javascript/">Google Client</a> API object.
* 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 GoGoogleDrive must include a script tag with src set to https://apis.google.com/js/api.js.
* @category Storage
*/
export class GoGoogleDrive extends gcs.GoCloudStorage {
private _pickerApiKey: string;
private _oauthToken: string;
private _scope: string;
/**
* Google Client object
*/
private _gapiClient: any;
/**
* Google Picker object
*/
private _gapiPicker: any;
/**
* @constructor
* @param {go.Diagram|go.Diagram[]} managedDiagrams An array of GoJS {@link Diagram}s whose model(s) will be saved to / loaded from Google Drive.
* Can also be a single Diagram.
* @param {string} clientId The client ID of the Google application linked with this instance of GoGoogleDrive (given in
* <a href="https://console.developers.google.com">Google Developers Console</a> after registering a Google app)
* @param {string} pickerApiKey The <a href="https://developers.google.com/picker/">Google Picker</a> API key. Once
* <a href="https://developers.google.com/picker/docs/">obtained</a>, it can be found in the <a href="https://console.developers.google.com">Google Developers Console</a>
* @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 GoGoogleDrive 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, pickerApiKey: string, defaultModel?: string, iconsRelativeDirectory?: string) {
super(managedDiagrams, defaultModel, clientId, iconsRelativeDirectory);
this._scope = 'https://www.googleapis.com/auth/drive';
this._pickerApiKey = pickerApiKey;
this._oauthToken = null;
this._gapiClient = null;
this._gapiPicker = null;
this.ui.id = 'goGoogleDriveSavePrompt';
this._serviceName = 'Google Drive';
this._className = 'GoGoogleDrive';
}
/**
* Get the Google Picker API key associated with this instance of GoGoogleDrive. This is set with a parameter during construction.
* A Google Picker API key can be obtained by following the process detailed <a href="https://developers.google.com/picker/docs/">here</a>,
* and it can be found in your <a href="https://console.developers.google.com"> Google Developers Console</a>. The pickerApiKey is used only in {@link #createPicker}.
* @function.
* @return {string}
*/
get pickerApiKey(): string { return this._pickerApiKey; }
/**
* Get the scope for the application linked to this instance of GoGoogleDrive (via {@link #clientId}). Scope tells the
* {@link #gapiClient} what permissions it has in making requests. Read more on scope <a href="https://developers.google.com/drive/v3/web/about-auth">here</a>.
* The default value is 'https://www.googleapis.com/auth/drive', set during construction. This can only be modified by changing the source code for
* GoGoogleDrive. As changing scope impacts gapiClient's permissions (and could break the usability of some or all functions of GoGoogleDrive), this is not recommended.
* @function.
* @return {string}
*/
get scope(): string { return this._scope; }
/**
* Get Google API Client. The Google API Client is used in GoGoogleDrive to make many different requests to Google Drive, however, it
* can be used with other Google Libraries to achieve many purposes. To read more about what can be done with a Google API Client object,
* click <a href="https://developers.google.com/api-client-library/javascript/start/start-js">here</a>. gapiClient is set after a succesful
* authorization in {@link #authorize}.
*
* gapiClient is really of type Object, not type any. However, the Google libraries are all written in JavaScript and do not provide
* d.ts files. As such, to avoid TypeScript compilation errors, both gapiClient and {@link #gapiPicker} properties are declared as type any.
* @function.
* @return {any}
*/
get gapiClient(): any { return this._gapiClient; }
/**
* Get <a href="https://developers.google.com/picker/docs/">Google Picker</a> API Object. Used to show the Google filepicker when loading
* / deleting files, in the {@link #createPicker} function. gapiPicker is set after a succesful authorization in {@link #authorize}.
*
* gapiPicker is really of type Object, not type any. However, the Google libraries are all written in JavaScript and do not
* provide d.ts files. As such, to avoid TypeScript compilation errors, both {@link #gapiClient} and gapiPicker properties are declared as type any.
* @function.
* @return {any}
*/
get gapiPicker(): any { return this._gapiPicker; }
/**
* Check if there is a signed in user who has authorized the application connected to this instance of GoGoogleDrive (via {@link #clientId}.
* If not, prompt user to sign into their Google Account and authorize the application. On successful authorization, set {@link #gapiClient} and {@link #gapiPicker}.
* @param {boolean} refreshToken Whether to get a new token (change current Google User)(true) or attempt to fetch a token for the currently signed in Google User (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;
let gapi = null;
if (window['gapi']) gapi = window['gapi'];
else return;
if (refreshToken) {
const href: string = document.location.href;
document.location.href = 'https://www.google.com/accounts/Logout?continue=https://appengine.google.com/_ah/logout?continue=' + href;
}
return new Promise(function(resolve: Function, reject: Function) {
function auth() {
gapi.auth.authorize({
'client_id': storage.clientId,
'scope': storage.scope,
'immediate': false
}, function(authResult) {
if (authResult && !authResult.error) {
storage._oauthToken = authResult.access_token;
}
storage._gapiClient = gapi.client;
if (window['google']) storage._gapiPicker = window['google']['picker'];
resolve(true);
});
}
gapi.load('client:auth', auth);
gapi.load('picker', {});
});
}
/**
* Launch <a href="https://developers.google.com/picker/docs/">Google Picker</a>, a filepicker UI used to graphically select files in
* Google Drive to load or delete. This is accomplished with {@link #gapiPicker}, which is set after succesful authorization, so this
* function may only be called after a successful call to {@link #authorize}.
* @param {Function} cb Callback function that takes the chosen file from the picker as a parameter
*/
public createPicker(cb: Function) {
const storage = this;
if (storage._oauthToken) {
// (appId is just the first number of clientId before '-')
const appId = storage.clientId.substring(0, this.clientId.indexOf('-'));
const view = new storage.gapiPicker.View(storage.gapiPicker.ViewId.DOCS);
view.setMimeTypes('application/json');
view.setQuery('*.diagram');
const picker = new storage.gapiPicker.PickerBuilder()
.enableFeature(storage.gapiPicker.Feature.NAV_HIDDEN)
.enableFeature(storage.gapiPicker.Feature.MULTISELECT_ENABLED)
.setAppId(appId)
.setOrigin(window.location.protocol + '//' + window.location.host)
.setOAuthToken(storage._oauthToken)
.addView(view)
.setDeveloperKey(storage.pickerApiKey)
.setCallback(function(args) {
cb(args);
})
.build();
picker.setVisible(true);
}
}
/**
* Get <a href="https://developers.google.com/drive/v3/reference/about#resource">information</a> about the
* currently logged in Google user. Some fields of particular note include:
* - displayName
* - emailAdrdress
* - kind
* @return {Promise} Returns a Promise that resolves with information about the currently logged in Google user
*/
public getUserInfo() {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
const request = storage.gapiClient.request({
'path': '/drive/v3/about',
'method': 'GET',
'params': { 'fields': 'user' },
callback: function(resp) {
if (resp) resolve(resp.user);
else reject(resp);
}
});
});
}
/**
* Get the Google Drive file reference object at a given path. Fields include:
* - id: The Google Drive-given ID of the file at the provided path
* - name: The name of the file saved to Google Drive at the provided path
* - mimeType: For diagram files, this will always be `text/plain`
* - kind: This will usually be `drive#file`.
*
* **Note:** Name, ID, and path values are requisite for creating valid {@link DiagramFile}s. When creating a DiagramFile for a
* diagram saved to Google Drive, provide the same value for name and path properties.
* @param {string} path A valid GoogleDrive file ID -- not a path. Named 'path' only to preserve system nomenclature
* @return {Promise} Returns a Promise that resolves with a Google Drive file reference object at a given path
*/
public getFile(path: string) {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
const req = storage.gapiClient.request({
path: '/drive/v3/files/' + path,
method: 'GET',
callback: function(resp) {
if (!resp.error) {
resolve(resp);
} else {
reject(resp.error);
}
}
});
});
}
/**
* Check whether a file exists at a given path
* @param {string} path A valid GoogleDrive file ID -- not a path. Named 'path' only to preserve system nomenclature
* @return {Promise} Returns a Promise that resolves with a boolean stating whether a file exists at a given path
*/
public checkFileExists(path: string) {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
const req = storage.gapiClient.request({
path: '/drive/v3/files/' + path,
method: 'GET',
callback: function(resp) {
const bool = (!!resp);
resolve(bool);
}
});
});
}
/**
* Show the custom GoGoogleDrive save prompt; a div with an HTML input element that accepts a file name to save the current {@link #managedDiagrams}
* data to in Google Drive.
* @return {Promise} Returns a Promise that resolves (in {@link #save}, {@link #load}, or {@link #remove}) with a {@link DiagramFile} representing the saved/loaded/deleted file
*/
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 + "googleDrive.jpg'></img><strong>Save Diagram As</strong><hr></hr>";
// user input div
const userInputDiv: HTMLElement = document.createElement('div');
userInputDiv.id = 'userInputDiv';
userInputDiv.innerHTML += '<input id="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() {
storage.saveWithUI();
};
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;
}
/**
* Save the current {@link #managedDiagrams}'s model data to the current Google user's Google Drive using the custom {@link #ui} save prompt.
* @return {Promise} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file
*/
public saveWithUI() {
const storage = this;
const ui = storage.ui;
return new Promise(function(resolve: Function, reject: Function) {
if (ui.style.visibility === 'hidden') {
resolve(storage.showUI());
} else {
const saveName: string = (document.getElementById('userInput') as HTMLInputElement).value;
storage.save(saveName);
resolve(storage.hideUI());
}
});
}
/**
* Save {@link #managedDiagrams}' model data to GoGoogleDrive. 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 GoGoogleDrive 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 name (not a path, not an id) to save this diagram file in Google Drive under. Named 'path' only to preserve system nomenclature
* @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: Function, reject: Function) {
if (path) { // save as
if (path.indexOf('.diagram') === -1) path += '.diagram';
let overwrite: boolean = false;
let overwriteFile: Object = null;
// get saved diagrams
const request = storage.gapiClient.request({
'path': '/drive/v3/files',
'method': 'GET',
'params': { 'q': 'trashed=false and name contains ".diagram" and mimeType = "application/json"' },
callback: function(resp) {
const savedDiagrams: Array<Object> = resp.files;
if (savedDiagrams) {
for (let i = 0; i < savedDiagrams.length; i++) {
if (savedDiagrams[i]['name'] === path) {
overwrite = true;
overwriteFile = savedDiagrams[i];
}
}
}
const boundary = '-------314159265358979323846';
const delimiter = '\r\n--' + boundary + '\r\n';
const closeDelim = '\r\n--' + boundary + '--';
const contentType = 'application/json';
const metadata: Object = {
'name': path,
'mimeType': contentType
};
const data = storage.makeSaveFile();
const multipartRequestBody: string =
delimiter +
'Content-Type: application/json\r\n\r\n' +
JSON.stringify(metadata) +
delimiter +
'Content-Type: ' + contentType + '\r\n\r\n' +
data +
closeDelim;
const req = storage.gapiClient.request({
'path': '/upload/drive/v3/files',
'method': 'POST',
'params': { 'uploadType': 'multipart' },
'headers': {
'Content-Type': 'multipart/related; boundary="' + boundary + '"'
},
'body': multipartRequestBody
});
req.execute(function(response) {
const savedFile: gcs.DiagramFile = { name: response.name, id: response.id, path: response.name };
storage.currentDiagramFile = savedFile;
resolve(savedFile); // used if save was called without UI
// if save has been called in saveDiagramWithUI, need to resolve / reset the Deferred Promise instance variable
storage._deferredPromise.promise.resolve(savedFile);
storage._deferredPromise.promise = storage.makeDeferredPromise();
});
}
});
} else if (storage.currentDiagramFile.path) { // save
const fileId: string = storage.currentDiagramFile.id;
const saveFile: string = storage.makeSaveFile();
storage.gapiClient.request({
path: '/upload/drive/v3/files/' + fileId,
method: 'PATCH',
params: { uploadType: 'media' },
body: saveFile,
callback: function(resp) {
if (!resp.error) {
// successful save
const savedFile: gcs.DiagramFile = { name: resp.name, id: resp.id, path: resp.name };
resolve(savedFile);
} else if (resp.error.code === 401) {
storage.authorize(true);
}
}
});
} else {
resolve(storage.saveWithUI()); // must use UI prompt to get a name if no 'path' is provided
}
});
}
/**
* Load the contents of a saved diagram from Google Drive using the Google Picker (see {@link #gapiPicker} and {@link #createPicker}).
* @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: Function, reject: Function) {
const loadFunction: Function = function(data) {
if (data.action === 'picked') {
const file = data.docs[0];
storage.gapiClient.request({
'path': '/drive/v3/files/' + file.id + '?alt=media',
'method': 'GET',
callback: function(modelData) {
if (file.name.indexOf('.diagram') !== -1) {
const loadedFile = { name: file.name, path: file.name, id: file.id };
resolve(storage.load(file.id));
storage.currentDiagramFile = loadedFile;
}
}
});
}
};
storage.createPicker(loadFunction); // TODO
});
}
/**
* Get the contents of a saved diagram from Google Drive using a given Google Drive file ID. No UI of any sort appears.
* @param {string} path A valid GoogleDrive file ID -- not a path. Named 'path' only to preserve GoCloudStorage system nomenclature
* @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: Function, reject: Function) {
storage.getFile(path).then(function(file: any) {
storage.gapiClient.request({
'path': '/drive/v3/files/' + file.id + '?alt=media',
'method': 'GET',
callback: function(modelData) {
if (modelData) {
if (file.name.indexOf('.diagram') !== -1) {
storage.loadFromFileContents(JSON.stringify(modelData));
const loadedFile: gcs.DiagramFile = { name: file['name'], path: file['name'], id: file['id'] };
storage.currentDiagramFile = loadedFile;
resolve(loadedFile);
}
}
}
});
}).catch(function(e) {
reject(e.message);
});
});
}
/**
* Delete a selected diagram from a user's Google Drive using the Google Picker (see {@link #gapiPicker} and {@link #createPicker}).
* @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: Function, reject: Function) {
const deleteFunction = function(data: Object) {
if (data['action'] === 'picked') {
const file = data['docs'][0];
resolve(storage.remove(file.id));
}
};
storage.createPicker(deleteFunction);
});
}
/**
* Delete a the diagram from a user's Google Drive with the given Google Drive file ID. No UI of any sort appears.
* @param {string} path A valid GoogleDrive file ID -- not a path. Named 'path' only to preserve system nomenclature
* @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: Function, reject: Function) {
storage.getFile(path).then(function(deletedFile: Object) {
storage.gapiClient.request({
'path': 'drive/v3/files/' + path,
'method': 'DELETE',
callback: function() {
if (storage.currentDiagramFile && path === storage.currentDiagramFile.id) storage.currentDiagramFile = { name: null, path: null, id: null };
deletedFile['path'] = deletedFile['name']; // google drive file references don't include path
resolve(deletedFile);
}
});
}).catch(function(e) {
reject(e.message);
});
});
}
}
+392
View File
@@ -0,0 +1,392 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* Go Local Storage
*/
// 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 Local Storage.
* GoLocalStorage is the only {@link GoCloudStorage} subclass than can be used in a local page;
* that is, one not served by a web server.
*
* **Note**: This class will not work with browsers that do not have Local Storage support
* (like some old versions of Internet Explorer).
* @category Storage
*/
export class GoLocalStorage extends gcs.GoCloudStorage {
private _localStorage: Storage;
/**
* 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 {@link Diagram}s whose model(s) will be saved to / loaded from Local Storage.
* Can also be a single Diagram.
* @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 GoLocalStorage exists, in which
* the storage service brand icons can be found. The default value is "../goCloudStorageIcons/".
*/
constructor(managedDiagrams: go.Diagram | Array<go.Diagram>, defaultModel?: string, iconsRelativeDirectory?: string) {
super(managedDiagrams, defaultModel, null, iconsRelativeDirectory);
try {
this._localStorage = window.localStorage;
} catch (e) {
throw new Error('Cannot access localStorage. Make sure your browser supports localStorage.' +
'If so, and this issue persists, try unblocking third-party cookies and site data in your browser settings');
}
this.ui.id = 'goLocalStorageCustomFilepicker';
this._serviceName = 'Local Storage';
this._className = 'GoLocalStorage';
}
/**
* Get the browser window's <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage">Local Storage</a> property.
* @function.
* @return {Storage}
*/
get localStorage(): Storage { return this._localStorage; }
/**
* Check if Local Storage is supported by the current browser.
* @param {boolean} refreshToken This parameter can be ignored. It exists only to maintain GoCloudStorage system structure
* @return {Promise<boolean>} Returns a Promise that resolves with a boolean (true if local storage is supported, false if not)
*/
public authorize(refreshToken: boolean = false): Promise<any> {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
try {
storage.localStorage.setItem('item', 'item');
storage.localStorage.removeItem('item');
resolve(true);
} catch (e) {
// local storage not supported
resolve(false);
}
});
}
/**
* Show the custom Go Local Storage filepicker {@link #ui}.
* @param {string} action Clarify what action is being done after file selection. Must be one of the following:
* - New
* - Open
* - Save
* - Delete
* @param {number} numAdditionalFiles Optional: Number of files to show in UI, in addition to a static numerical 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 Local Storage.
* @return {Promise<any>} 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, numAdditionalFiles?: number) {
const storage = this;
const ui = storage.ui;
const spacestring = 'qwe45qw34'; // used as a placeholder for spaces in IDs
if (!numAdditionalFiles) numAdditionalFiles = 0;
const maxFilesToShow: number = GoLocalStorage._MIN_FILES_IN_UI + numAdditionalFiles;
ui.innerHTML = "<img class='icons' src='" + storage.iconsRelativeDirectory + "localStorage.png'></img>";
const title: string = action + ' Diagram File';
ui.innerHTML += '<strong>' + title + '</strong><hr></hr>';
// document.getElementsByTagName('body')[0].appendChild(ui);
ui.style.visibility = 'visible';
const filesDiv = document.createElement('div');
filesDiv.id = 'fileOptions';
// filter out non-diagram files in local storage (only until max allowed files is reached)
const savedDiagrams: Array<Object> = [];
const numFilesToCheck: number = GoLocalStorage._MIN_FILES_IN_UI + numAdditionalFiles;
let numFilesChecked: number = 0;
let hasCheckedAllFiles: boolean = false;
if (storage.localStorage.length !== 0) {
for (const key in storage.localStorage) {
if (savedDiagrams.length < maxFilesToShow) {
numFilesChecked++;
const fileContent: string = storage.localStorage.getItem(key);
if (fileContent && (fileContent.indexOf('GraphLinksModel') !== -1 || fileContent.indexOf('TreeModel') !== -1)) {
const file: Object = { key: key, model: fileContent };
savedDiagrams.push(file);
}
if (numFilesChecked === storage.localStorage.length) hasCheckedAllFiles = true;
}
}
} else hasCheckedAllFiles = true;
if (savedDiagrams.length !== 0) {
// list diagram files in local storage as selectable files (as many as MIN_FILES_IN_UI + additionalFiles param)
for (let i = 0; i < savedDiagrams.length; i++) {
const kvp: Object = savedDiagrams[i];
const file: string = kvp['key'];
const fileId: string = file.replace(/ /g, spacestring);
if (action !== 'Save') {
filesDiv.innerHTML +=
"<div class='fileOption'>" +
'<input id=' + fileId + " type='radio' name='localStorageFile' />" +
'<label id =' + fileId + '-label' + " for='" + fileId + "'>" + file + '</label>' +
'</div>';
} else {
filesDiv.innerHTML +=
"<div class='fileOption'>" +
'<label id =' + fileId + '-label' + " for='" + fileId + "'>" + file + '</label>' +
'</div>';
}
}
}
// If there may be more diagram files to show, say so and provide user with option to try loading more in the UI
if (!hasCheckedAllFiles) {
const num: number = numAdditionalFiles + 50;
filesDiv.innerHTML += "<p>There may be more diagram files not shown. <a id='localStorageLoadMoreFiles'>Click here</a> to try loading more.</p>";
document.getElementById('localStorageLoadMoreFiles').onclick = function() {
storage.showUI(action, num);
};
}
ui.appendChild(filesDiv);
// italicize currently open file, if a file is currently open
if (storage.currentDiagramFile.id) {
const str: string = storage.currentDiagramFile.id.replace(/ /g, spacestring);
const el: HTMLElement = document.getElementById(str + '-label');
if (el) el.style.fontStyle = 'italic';
}
// user input div (only for save)
if (action === 'Save') {
const userInputDiv = document.createElement('div');
userInputDiv.id = 'userInputDiv';
userInputDiv.innerHTML += '<span>Save Diagram As </span><input id="userInput" placeholder="Enter filename"></input>';
ui.appendChild(userInputDiv);
}
const submitDiv = document.createElement('div');
submitDiv.id = 'submitDiv';
const actionButton = document.createElement('button');
actionButton.textContent = action;
actionButton.id = 'actionButton';
actionButton.onclick = function() {
storage.processUIResult(action);
};
submitDiv.appendChild(actionButton);
ui.appendChild(submitDiv);
const cancelDiv = document.createElement('div');
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']; // will not resolve until action (save, load, delete) completes
}
/**
* @private
* @hidden
* Process the result of pressing the action button on the custom GoLocalStorage filepicker {@link #ui}.
* @param {string} action The action being done. Acceptable values:
* - Save
* - Load
* - Delete
*/
public processUIResult(action: string) {
const storage = this;
// Helper: Return key of the file selected from the custom localstorage filepicker menu
function getSelectedFile() {
const radios = document.getElementsByName('localStorageFile');
let selectedFile: string = null;
for (let i = 0; i < radios.length; i++) {
if ((radios[i] as HTMLInputElement).checked) {
selectedFile = radios[i].id.replace(/qwe45qw34/g, ' ');
}
}
if (selectedFile) return selectedFile;
else return null;
}
const file: string = getSelectedFile();
switch (action) {
case 'Save': {
let name = (document.getElementById('userInput') as HTMLInputElement).value;
if (name) {
name += '.diagram';
storage.save(name);
} else {
// handle bad name
}
break;
}
case 'Load': {
storage.load(file);
break;
}
case 'Delete': {
storage.remove(file);
break;
}
}
storage.hideUI();
}
/**
* Get information about a diagram file saved to Local Storage. This data includes:
* - content: The content of the saved file (a string respresentation of a GoJS Diagram Model)
* - id: The key of the file in local storage
* - name: Same as id value
* - path: Same as id value
*
* **Note:** Id, name, and path are all provided (despite being the same). They are required for creating valid {@link DiagramFile}s.
* @param {string} path A valid key corresponding to a saved diagram file in Local Storage
* @return {Promise<any>} Returns a Promise that resolves with information about a diagram file saved to local storage
*/
public getFile(path: string) {
if (path.indexOf('.diagram') === -1) path += '.diagram';
return new Promise(function(resolve, reject) {
const fileContent: string = (!!window.localStorage.getItem(path)) ? window.localStorage.getItem(path) : null;
const file: Object = { name: path, content: fileContent, path: path, id: path };
resolve(file);
});
}
/**
* Check whether a file exists in Local Storage at a given path.
* @param {string} path A valid key corresponding to a saved diagram file in Local Storage
* @return {Promise<any>} Returns a Promise that resolves with a boolean stating whether a file exists in LocalStorage at a given path
*/
public checkFileExists(path: string) {
if (path.indexOf('.diagram') === -1) path += '.diagram';
return new Promise(function(resolve, reject) {
const fileExists: boolean = !!(window.localStorage.getItem(path));
resolve(fileExists);
});
}
/**
* Save the current {@link #managedDiagrams}'s model data to Local Storage using the custom filepicker {@link #ui}.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file
*/
public saveWithUI() {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
resolve(storage.showUI('Save'));
});
}
/**
* Save {@link #managedDiagrams}' model data to Local 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 key in Local Storage 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 string to save diagram model data to (becomes the key for the file in Local Storage)
* @return {Promise<any>} 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: Function, reject: Function) {
if (path) {
if (path.indexOf('.diagram') === -1) path += '.diagram';
const item: string = storage.makeSaveFile();
storage.localStorage.setItem(path, item);
const savedFile: gcs.DiagramFile = { name: path, id: path, path: path };
storage.currentDiagramFile = savedFile;
resolve(savedFile); // used if saveDiagramAs was called without UI
// if saveDiagramAs has been called in processUIResult, need to resolve / reset the Deferred Promise instance variable
storage._deferredPromise['promise'].resolve(savedFile);
storage._deferredPromise['promise'] = storage.makeDeferredPromise();
} else if (storage.currentDiagramFile.path) {
const saveName: string = storage.currentDiagramFile['path'];
const savedFile: gcs.DiagramFile = { name: saveName, path: saveName, id: saveName };
const item: string = storage.makeSaveFile();
storage.localStorage.setItem(saveName, item);
resolve(saveName);
} else {
resolve(storage.saveWithUI());
}
});
}
/**
* Get the contents of a given file; load to {@link #managedDiagrams} model. Use the custom filepicker {@link #ui}.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the loaded file
*/
public loadWithUI() {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
resolve(storage.showUI('Load'));
}).catch(function(e: any) {
throw Error(e);
});
}
/**
* Get the contents of a given file; load to {@link #managedDiagrams} model.
* @param {string} path A valid localstorage key to load diagram model data from
* @return {Promise<any>} 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: Function, reject: Function) {
if (path) {
const fileContents: string = storage.localStorage.getItem(path);
if (fileContents) {
storage.loadFromFileContents(fileContents);
const loadedFile: gcs.DiagramFile = { name: path, id: path, path: path };
storage.currentDiagramFile = loadedFile;
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 local storage with path ' + path);
} else throw Error('Cannot load file from local storage with path ' + path);
}).catch(function(e: any) {
// console.error(e);
throw Error(e);
});
}
/**
* Delete a diagram from Local Storage using the custom filepicker menu {@link #ui}.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file
*/
public removeWithUI() {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
resolve(storage.showUI('Delete'));
});
}
/**
* Delete a given diagram from Local Storage.
* @param {string} path A valid localstorage key to delete diagram model data from
* @return {Promise<any>} 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: Function, reject: Function) {
if (path) {
const deletedFile: gcs.DiagramFile = { name: path, path: path, id: path };
if (storage.currentDiagramFile && path === storage.currentDiagramFile['name']) storage.currentDiagramFile = { name: null, path: null, id: null };
storage.localStorage.removeItem(path); // remove file from local storage
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();
} else throw Error('Cannot delete file from local storage with path ' + path);
});
}
}
+465
View File
@@ -0,0 +1,465 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* Go Net Core (unfinished)
*/
// import { Promise } from 'es6-promise';
import * as go from 'gojs';
import * as gcs from './GoCloudStorage.js';
/**
* @hidden
* Class for saving / loading GoJS {@link Model}s to / from Local Storage.
* GoLocalStorage is the only {@link GoCloudStorage} subclass than can be used in a local page; that is, one not served by a web server.
*
* **Note**: that this class will not work with browsers that do not have Local Storage support (like some old versions of Internet Explorer).
* @category Storage
*/
export class GoNetCore extends gcs.GoCloudStorage {
/**
* The number of files to display in {@link #ui} before loading more
*/
private static _MIN_FILES_IN_UI = 100;
private _rootEndpoint: string;
/**
* @constructor
* @param {go.Diagram|go.Diagram[]} managedDiagrams An array of GoJS {@link Diagram}s whose model(s) will be saved to / loaded from Local Storage.
* Can also be a single Diagram.
* @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 GoLocalStorage exists, in which
* the storage service brand icons can be found. The default value is "../goCloudStorageIcons/".
*/
constructor(managedDiagrams: go.Diagram | Array<go.Diagram>, rootEndpoint: string, defaultModel?: string, iconsRelativeDirectory?: string) {
super(managedDiagrams, defaultModel);
this._rootEndpoint = rootEndpoint;
this.ui.id = 'goNetCoreCustomFilepicker';
this._serviceName = 'Microsoft ASP .NET Core Web API';
this._className = 'GoNetCore';
}
get rootEndpoint(): string { return this._rootEndpoint; }
/**
* Check if Local Storage is supported by the current browser.
* @param {boolean} refreshToken This parameter can be ignored. It exists only to maintain GoCloudStorage system structure
* @return {Promise<boolean>} Returns a Promise that resolves with a boolean (true if local storage is supported, false if not)
*/
public authorize(refreshToken: boolean = false) {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
resolve(true); // TODO? no auth at all right now
});
}
/*
* TODO TODO TODO
* Show the custom Go Net Core filepicker {@link #ui}.
* @param {string} action Clarify what action is being done after file selection. Must be one of the following:
* - New
* - Open
* - Save
* - Delete
* @param {number} numAdditionalFiles Optional: Number of files to show in UI, in addition to a static numerical 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 Local Storage.
* @return {Promise<any>} 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, numAdditionalFiles?: number) {
const storage = this;
const ui = storage.ui;
const spacestring = 'qwe45qw34'; // used as a placeholder for spaces in IDs
if (!numAdditionalFiles) numAdditionalFiles = 0;
const maxFilesToShow: number = GoNetCore._MIN_FILES_IN_UI + numAdditionalFiles;
ui.innerHTML = ''; // "<img class='icons' src='" + storage.iconsRelativeDirectory + "localStorage.png'></img>"; // TODO
const title: string = action + ' Diagram File';
ui.innerHTML += '<strong>' + title + '</strong><hr></hr>';
document.getElementsByTagName('body')[0].appendChild(ui);
ui.style.visibility = 'visible';
const filesDiv = document.createElement('div');
filesDiv.id = 'fileOptions';
// filter out non-diagram files in local storage (only until max allowed files is reached)
const savedDiagrams: Array<Object> = [];
const numFilesToCheck: number = GoNetCore._MIN_FILES_IN_UI + numAdditionalFiles;
let numFilesChecked: number = 0;
let hasCheckedAllFiles: boolean = false;
storage.getFiles().then(function(files: Array<any>) {
if (files.length !== 0) {
for (const i in files) {
const item = files[i];
if (savedDiagrams.length < maxFilesToShow) {
numFilesChecked++;
const fileContent: string = item.file;
if (fileContent && fileContent.indexOf('GraphLinksModel' || 'TreeModel') !== -1) {
const file: Object = { name: item.name, id: item.id };
savedDiagrams.push(file);
}
if (numFilesChecked === files.length) hasCheckedAllFiles = true;
}
}
} else hasCheckedAllFiles = true;
if (savedDiagrams.length !== 0) {
// list diagram files in local storage as selectable files (as many as MIN_FILES_IN_UI + additionalFiles param)
for (let i = 0; i < savedDiagrams.length; i++) {
const item: any = savedDiagrams[i];
const name: string = item.name;
const id: string = item.id;
if (action !== 'Save') {
filesDiv.innerHTML +=
"<div class='fileOption'>" +
'<input id=' + id + " type='radio' name='localStorageFile' />" +
'<label id =' + id + '-label' + " for='" + name + "'>" + name + '</label>' +
'</div>';
} else {
filesDiv.innerHTML +=
"<div class='fileOption'>" +
'<label id =' + id + '-label' + " for='" + id + "'>" + name + '</label>' +
'</div>';
}
}
}
// If there may be more diagram files to show, say so and provide user with option to try loading more in the UI
if (!hasCheckedAllFiles) {
const num: number = numAdditionalFiles + 50;
filesDiv.innerHTML += "<p>There may be more diagram files not shown. <a id='netCoreLoadMoreFiles'>Click here</a> to try loading more.</p>";
document.getElementById('netCoreLoadMoreFiles').onclick = function() {
storage.showUI(action, num);
};
}
ui.appendChild(filesDiv);
// italicize currently open file, if a file is currently open
if (storage.currentDiagramFile.id) {
const el: HTMLElement = document.getElementById(storage.currentDiagramFile.id + '-label');
if (el) el.style.fontStyle = 'italic';
}
// user input div (only for save)
if (action === 'Save') {
const userInputDiv = document.createElement('div');
userInputDiv.id = 'userInputDiv';
userInputDiv.innerHTML += '<span>Save Diagram As </span><input id="userInput" placeholder="Enter filename"></input>';
ui.appendChild(userInputDiv);
}
const submitDiv = document.createElement('div');
submitDiv.id = 'submitDiv';
const actionButton = document.createElement('button');
actionButton.textContent = action;
actionButton.id = 'actionButton';
actionButton.onclick = function() {
storage.processUIResult(action);
};
submitDiv.appendChild(actionButton);
ui.appendChild(submitDiv);
const cancelDiv = document.createElement('div');
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']; // will not resolve until action (save, load, delete) completes
}
/**
* TODO TODO TODO
* @private
* @hidden
* Process the result of pressing the action button on the custom GoLocalStorage filepicker {@link #ui}.
* @param {string} action The action being done. Acceptable values:
* - Save
* - Load
* - Delete
*/
public processUIResult(action: string) {
const storage = this;
// Helper: Return key of the file selected from the custom localstorage filepicker menu
function getSelectedFile() {
const radios = document.getElementsByName('localStorageFile');
let selectedFile: string = null;
for (let i = 0; i < radios.length; i++) {
if ((radios[i] as HTMLInputElement).checked) {
selectedFile = radios[i].id.replace(/qwe45qw34/g, ' ');
}
}
if (selectedFile) return selectedFile;
else return null;
}
const file: string = getSelectedFile();
switch (action) {
case 'Save': {
let name = (document.getElementById('userInput') as HTMLInputElement).value;
if (name) {
name += '.diagram';
storage.save(name);
} else {
// handle bad name
}
break;
}
case 'Load': {
storage.load(file);
break;
}
case 'Delete': {
storage.remove(file);
break;
}
}
storage.hideUI();
}
/**
* Get information about a diagram file saved to .NET Core database. This data includes:
* - content: The content of the saved file (a string respresentation of a GoJS Diagram Model)
* - id: The key of the file in local storage
* - name: Same as id value
* - path: Same as id value
*
* **Note:** Id, name, and path are all provided (despite being the same). They are requisite for creating valid {@link DiagramFile}s.
* @param {string} path A valid id corresponding to a saved diagram file in the .NET Core database
* @return {Promise<any>} Returns a Promise that resolves with information about a diagram file saved to local storage
*/
public getFile(path: string) {
const storage = this;
const url: string = storage.rootEndpoint + path;
return new Promise(function(resolve, reject) {
if (path) {
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve((JSON.parse(xhr.response)));
}
}
};
xhr.send();
} else reject('Cannot get diagram file from ASP .NET Core Web API with id ' + path);
});
}
/**
* Get all the files in the ASP.NET Core database.
* Returns a Promise that resolves with JSON data about every file, including name, id, and content
*/
public getFiles() {
const storage = this;
return new Promise(function(resolve, reject) {
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.open('GET', storage.rootEndpoint, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response));
} else reject(xhr.response);
}
};
xhr.send();
});
}
/**
* Check whether a file exists in LocalStorage at a given path.
* @param {string} path A valid key corresponding to a saved diagram file in Local Storage
* @return {Promise<any>} Returns a Promise that resolves with a boolean stating whether a file exists in LocalStorage at a given path
*/
public checkFileExists(path: string) {
const storage = this;
const url: string = storage.rootEndpoint + path;
return new Promise(function(resolve, reject) {
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve((true));
} else {
resolve(false);
}
}
};
xhr.send();
});
}
/**
* Save the current {@link #managedDiagrams}'s model data to Local Storage using the custom filepicker {@link #ui}.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file
*/
public saveWithUI() {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
resolve(storage.showUI('Save'));
});
}
/**
* Save {@link #managedDiagrams}' model data to Local 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 key in Local Storage 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 string to save diagram model data to (becomes the key for the file in Local Storage)
* @return {Promise<any>} 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: Function, reject: Function) {
// PUT -- update current entry
if (storage.currentDiagramFile.id && !path) {
const xhr: XMLHttpRequest = new XMLHttpRequest();
storage.getFile(storage.currentDiagramFile.id).then(function(resp) {
const item = {
id: storage.currentDiagramFile.id,
name: storage.currentDiagramFile.name,
file: storage.makeSaveFile()
};
const savedFile: gcs.DiagramFile = { id: item.id, name: item.name, path: item.name };
xhr.open('PUT', storage.rootEndpoint + storage.currentDiagramFile.id, true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status > 200 && xhr.status < 300) {
resolve(savedFile);
} else {
reject(xhr.responseText);
}
}
};
xhr.send(JSON.stringify(item));
});
} else {
const xhr: XMLHttpRequest = new XMLHttpRequest();
const item = {
name: (path !== null) ? path : 'New diagram',
file: storage.makeSaveFile()
};
xhr.open('POST', storage.rootEndpoint, true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status > 200 && xhr.status < 300) {
const id = JSON.parse(xhr.response).id;
const savedFile: gcs.DiagramFile = { id: id, name: item.name, path: item.name };
storage.currentDiagramFile = savedFile;
resolve(savedFile); // used if saveDiagram was called without UI
// if saveDiagram has been called in processUIResult, need to resolve / reset the Deferred Promise instance variable
storage._deferredPromise['promise'].resolve(savedFile);
storage._deferredPromise['promise'] = storage.makeDeferredPromise();
} else {
reject(xhr.responseText);
}
}
};
xhr.send(JSON.stringify(item));
}
});
}
/**
* Get the contents of a given file; load to {@link #managedDiagrams} model. Use the custom filepicker {@link #ui}.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the loaded file
*/
public loadWithUI() {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
resolve(storage.showUI('Load'));
}).catch(function(e: any) {
throw Error(e);
});
}
/**
* Get the contents of a given file; load to {@link #managedDiagrams} model.
* @param {string} path A valid localstorage key to load diagram model data from
* @return {Promise<any>} 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: Function, reject: Function) {
if (path) {
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.open('GET', storage.rootEndpoint + path, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
const respJSON = JSON.parse(xhr.response);
storage.loadFromFileContents(respJSON.file);
const loadedFile: gcs.DiagramFile = { id: respJSON.id, path: respJSON.name, name: respJSON.name };
storage.currentDiagramFile = loadedFile;
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 {
reject(xhr.responseText);
}
}
};
xhr.send();
} else throw Error('Cannot load file from .NET Core Web API with path ' + path);
}).catch(function(e: any) {
throw Error(e);
});
}
/**
* Delete a diagram from Local Storage using the custom filepicker menu {@link #ui}.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file
*/
public removeWithUI() {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
resolve(storage.showUI('Delete'));
});
}
/**
* Delete a given diagram from Local Storage.
* @param {string} path A valid localstorage key to delete diagram model data from
* @return {Promise<any>} 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: Function, reject: Function) {
if (path) {
storage.getFile(path).then(function(resp: any) {
const deletedFile: gcs.DiagramFile = { name: resp.name, path: resp.name, id: path };
if (storage.currentDiagramFile && resp.name === storage.currentDiagramFile.name) storage.currentDiagramFile = { name: null, path: null, id: null };
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.open('DELETE', storage.rootEndpoint + path, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
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();
} else {
reject(xhr.responseText);
}
}
};
xhr.send();
});
} else throw Error('Cannot delete file from local storage with id ' + path);
});
}
}
+485
View File
@@ -0,0 +1,485 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* Go One Drive
*/
// import { Promise } from 'es6-promise';
import * as go from 'gojs';
import * as gcs from './GoCloudStorage.js';
/**
* Class for saving / loading GoJS {@link Diagram#model}s to / from Microsoft One Drive.
* As with all {@link GoCloudStorage} subclasses (with the exception of {@link GoLocalStorage}, any page using GoDropBox must be served on a web server.
*
* **Note**: Makes use of <a href="https://dev.onedrive.com/sdk/js-v72/js-picker-overview.htm">OneDrive FilePicker for JavaScript v7.2</a>. Any page
* using GoOneDrive must include a script tag with src set to https://js.live.net/v7.2/OneDrive.js.
* @category Storage
*/
export class GoOneDrive extends gcs.GoCloudStorage {
private _oneDriveFilepicker: any;
private _oauthToken: string;
/**
* @constructor
* @param {go.Diagram[]} managedDiagrams An array of GoJS {@link Diagram}s whose model(s) will be saved to
* / loaded from OneDrive. Can also be a single Diagram.
* @param {string} clientId The client ID of the application in use (given by Microsoft in Microsoft Dev Center)
* @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 GoOneDrive 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);
this._oauthToken = null;
this.ui.id = 'goOneDriveSavePrompt';
if (window['OneDrive']) {
this._oneDriveFilepicker = window['OneDrive'];
}
this.authorize(false); // // on construction, check if there is an access_token in the window URI (there will be if redirected from a permissions grant page)
this._serviceName = 'Microsoft OneDrive';
this._className = 'GoOneDrive';
}
/**
* Get / set the global oauthToken. Only used to authorize requests in {@link #load}, {@link #save}, and {@link #remove}
* when the {@link #oneDriveFilepicker} property is not used. Not needed when the oneDriveFilePicker is used, as action-specific tokens (issued by the Microsoft-provided
* <a href="https://dev.onedrive.com/sdk/js-v72/js-picker-overview.htm">OneDrive Filepicker</a>) are issued then. oauthToken is null after construction,
* but can be set or refreshed with calls to {@link #authorize}.
*/
get oauthToken(): string { return this._oauthToken; }
set oauthToken(value: string) { this._oauthToken = value; }
/**
* Get <a href="https://dev.onedrive.com/sdk/js-v72/js-picker-overview.htm">OneDrive Filepicker</a> object. Used to display a Microsoft user's OneDrive files.
*/
get oneDriveFilepicker(): any { return this._oneDriveFilepicker; }
/**
* Get OAuth 2.0 token for Microsoft OneDrive API requests with a specific Microsoft account. Sets {@link #oauthToken}.
* @param {boolean} refreshToken Whether to get a new access 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): Promise<boolean> {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
if (!refreshToken && window.location.hash.indexOf('access_token') !== -1) {
const accessToken: string = window.location.hash.substring(window.location.hash.indexOf('=') + 1, window.location.hash.indexOf('&'));
storage.oauthToken = accessToken;
resolve(true);
} else if (refreshToken) {
const authUrl: string = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=' + storage.clientId +
'&scope=files.readwrite.all&response_type=token&redirect_uri=' + window.location.href + '';
window.location.href = authUrl;
resolve(true);
}
});
}
/**
* Get information about the currently logged in Microsoft user. Some fields of particular note include:
* - displayName
* - givenName
* - id
* - jobTitle
* - userPrincipalName (email)
*
* **Note:** If {@link #oauthToken} is not valid or has expired, a page redirect to the Microsoft Account sign in will occur.
* @return {Promise<any>} Returns a Promise that resolves with information about the currently logged in Microsoft user
*/
public getUserInfo(): Promise<any> {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
const xhr: XMLHttpRequest = new XMLHttpRequest();
if (!storage.oauthToken) {
if (window.location.hash.indexOf('access_token') === -1) {
reject('No acessToken in current uri');
storage.authorize(true);
} else {
reject('oauthToken not set');
storage.authorize(false);
}
} else {
xhr.open('GET', 'https://graph.microsoft.com/v1.0/me');
xhr.setRequestHeader('Authorization', 'Bearer ' + storage.oauthToken);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
resolve(JSON.parse(xhr.response));
} else if (xhr.status === 401) { // unauthorized request (expired token)
storage.authorize(true);
reject(xhr.response);
}
};
xhr.send();
}
});
}
/**
* Check whether a file exists at a given path.
* @param {string} path A valid MS OneDrive filepath to save current diagram model to
* Path must be of the form: `/drive/root:/{item-path}`
* @return {Promise<any>} Returns a Promise that resolves with a boolean stating whether a file exists at a given path
*/
public checkFileExists(path: string): Promise<any> {
const storage = this;
if (path.indexOf('.diagram') === -1) path += '.diagram';
return new Promise(function(resolve: Function, reject: Function) {
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.open('GET', 'https://graph.microsoft.com/v1.0' + path, true);
xhr.setRequestHeader('Authorization', 'Bearer ' + storage.oauthToken);
xhr.onreadystatechange = function() {
let bool; let err;
if (xhr.readyState === 4) {
if (xhr.status === 200) {
bool = true;
} else if (xhr.status === 401) {
storage.authorize(true);
} else if (xhr.status === 404) {
bool = false;
} else {
err = xhr.response;
}
resolve(bool);
if (err) reject(err);
}
};
xhr.send();
});
}
/**
* Get the OneDrive file reference object at a given path. Properties of particular note include:
* - name: The name of the file in OneDrive
* - id: The OneDrive-given file ID
* - parentReference
* - path: The path of the parent folder of the file at the provided path
*
* **Note:** Name, ID, and path are requisite for creating valid {@link DiagramFile}s. A path can be constructed by concatenating the
* parentReference.path with name. DiagramFiles optionally also contain parentReference and token values (both used exclusively by GoOneDrive and its methods).
* @param {string} path A valid MS OneDrive filepath to save current diagram model to
* Path must be of the form: `/drive/root:/{item-path}`
* @param {string} token Optional: A token received by OneDrive filepicker (loadDiagramWithUI) to allow for its file to be loaded.
* If no token is given, use global oauthToken
* @return {Promise<any>} Returns a Promise that resolves with a OneDrive file reference object at a given path
*/
public getFile(path: string, token?: string): Promise<any> {
const storage = this;
if (path.indexOf('.diagram') === -1) path += '.diagram';
return new Promise(function(resolve: Function, reject: Function) {
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.open('GET', 'https://graph.microsoft.com/v1.0' + path, true);
const t: string = (token) ? token : storage.oauthToken;
xhr.setRequestHeader('Authorization', 'Bearer ' + t);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) { // 200=OK
const file: Object = JSON.parse(xhr.response);
resolve(file);
} else if (xhr.status === 401) { // unauthorized request
storage.authorize(true);
} else {
reject(xhr.response);
}
}
}; // end filexhr
xhr.send();
});
}
/**
* Show the custom GoOneDrive save prompt {@link #ui}.
* @return {Promise<any>} Returns a Promise that resolves (in {@link #save}, {@link #load}, or {@link #remove}) with a {@link DiagramFile}
* representing the saved/loaded/deleted file
*/
public showUI(): Promise<any> {
const storage = this;
const ui = storage.ui;
ui.innerHTML = ''; // clear div
ui.style.visibility = 'visible';
ui.innerHTML = "<img class='icons' src='" + storage.iconsRelativeDirectory + "oneDrive.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="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() {
storage.saveWithUI();
};
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;
}
/**
* Save the each {@link #managedDiagrams}' model data to a user's One Drive account, using
* <a href="https://dev.onedrive.com/sdk/js-v72/js-picker-overview.htm">One Drive FilePicker</a>.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file.
*/
public saveWithUI(): Promise<any> {
const storage = this;
const ui = storage.ui;
return new Promise(function(resolve, reject) {
if (ui.style.visibility === 'hidden') {
resolve(storage.showUI());
} else {
let saveName = (document.getElementById('userInput') as HTMLInputElement).value;
if (saveName && saveName.indexOf('.diagram') === -1) saveName += '.diagram';
const odOptions: Object = {
clientId: storage.clientId,
action: 'query',
openInNewWindow: true,
success: function(selection) {
const folder: Object = selection.value[0];
const token: string = selection.accessToken;
storage.currentDiagramFile = {
id: null,
name: saveName,
token: token,
parentReference: {
driveId: folder['parentReference']['driveId'],
id: folder['id']
},
path: 'placeholder' // will be defined in saveDiagram
};
storage.hideUI();
storage.save();
}
};
if (saveName && saveName !== '' && saveName !== undefined) storage.oneDriveFilepicker.save(odOptions);
else reject('Cannot save file to OneDrive with save name ' + saveName);
}
});
}
/**
* Save {@link #managedDiagrams}' model data to Microsoft OneDrive. 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 OneDrive corresponding to currentDiagramFile.path with current managedDiagrams' model data.
* @param {string} path A valid MS OneDrive filepath to save current diagram model to. Path syntax is
* `/drive/root:/{path-to-file}/{filename}`; i.e. `/drive/root:/Documents/example.diagram`
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file.
*/
public save(path?: string): Promise<any> {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
if (path) { // save as
const xhr: XMLHttpRequest = new XMLHttpRequest();
if (path.indexOf('.diagram') === -1) path += '.diagram';
const bodyContent: string = storage.makeSaveFile();
xhr.open('PUT', 'https://graph.microsoft.com/v1.0' + path + ':/content', true);
xhr.setRequestHeader('Authorization', 'Bearer ' + storage.oauthToken);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
const file: Object = JSON.parse(xhr.response);
const savedFile: gcs.DiagramFile = {
name: file['name'], id: file['id'],
path: file['parentReference']['path'] + '/' + file['name'], parentReference: file['parentReference']
};
resolve(savedFile);
} else if (xhr.status === 401) { // unauthorized request
storage.authorize(true);
} else {
throw Error(xhr.response);
}
}
};
xhr.send(bodyContent);
} else if (storage.currentDiagramFile.path) { // save
const token: string = storage.currentDiagramFile.token;
const url: string = storage.generateGraphUrl(storage.currentDiagramFile, true, true);
const bodyContent: string = storage.makeSaveFile();
const t: string = (!token) ? storage.oauthToken : storage.currentDiagramFile.token;
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('Authorization', 'Bearer ' + t);
xhr.onload = function() {
if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201)) {
const file: Object = JSON.parse(xhr.response);
const savedFile: gcs.DiagramFile = {
name: file['name'], id: file['id'],
path: file['parentReference']['path'] + '/' + file['name'], token: token, parentReference: file['parentReference']
};
storage.currentDiagramFile = savedFile;
resolve(savedFile); // used if saveDiagram was called without UI
// if save has been called in processUIResult, need to resolve / reset the Deferred Promise instance variable
storage._deferredPromise.promise.resolve(savedFile);
storage._deferredPromise.promise = storage.makeDeferredPromise();
} else {
reject(xhr.response); // failed save
}
}; // end xhr onload
xhr.send(bodyContent);
} else {
resolve(storage.saveWithUI());
}
});
}
/**
* Load diagram model data from a given OneDrive-specific file path into {@link #managedDiagrams} using the MS OneDrive filepicker.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the loaded file
*/
public loadWithUI(): Promise<any> {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
const odOptions: Object = {
clientId: storage.clientId,
action: 'share',
multiSelect: false,
advanced: {
filter: '.diagram' // only show diagram files
},
success: function(files: Object) {
const file: Object = files['value'][0];
const token: string = files['accessToken'];
const filePath = file['parentReference']['path'] + '/' + file['name'];
resolve(storage.load(filePath, token));
}
};
storage.oneDriveFilepicker.open(odOptions);
});
}
/**
* Load the contents of a saved diagram from MS OneDrive to diagram model.
* @param {string} path A valid Microsoft OneDrive filepath to load diagram model data from. Path syntax is
* `/drive/root:/{path-to-file}/{filename}`; i.e. `/drive/root:/Documents/example.diagram`
* @param {string} token A token received by <a href="https://dev.onedrive.com/sdk/js-v72/js-picker-overview.htm">OneDrive Filepicker</a>
* (passed from {@link #loadWithUI}) to allow for its file to be loaded. If no token is given, use global {@link #oauthToken}
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the loaded file
*/
public load(path: string, token?: string): Promise<any> {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
if (path) {
const t: string = (token) ? token : storage.oauthToken;
storage.getFile(path, t).then(function(file: Object) {
const downloadLink: string = file['@microsoft.graph.downloadUrl'];
// Download file from download link
const downloadxhr: XMLHttpRequest = new XMLHttpRequest();
downloadxhr.open('GET', downloadLink, true);
downloadxhr.onreadystatechange = function() {
if (downloadxhr.readyState === 4) {
if (downloadxhr.status === 200) {
storage.loadFromFileContents(downloadxhr.response);
const loadedFile: gcs.DiagramFile = {
name: file['name'], id: file['id'], path: file['parentReference']['path'] + '/' + file['name'], token: token,
parentReference: { id: file['parentReference']['id'], driveId: file['parentReference']['driveId'] }
};
storage.currentDiagramFile = loadedFile;
resolve(loadedFile);
}
}
}; // end downloadxhr
downloadxhr.send();
});
} else reject('Cannot load file from OneDrive with path ' + path);
});
}
/**
* Delete a diagram from a user's OneDrive using the <a href="https://dev.onedrive.com/sdk/js-v72/js-picker-overview.htm">OneDrive Filepicker</a>.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file
*/
public removeWithUI(): Promise<any> {
const storage = this;
return new Promise(function(resolve: Function, reject: Function) {
const odOptions = {
clientId: storage.clientId,
action: 'share',
openInNewWindow: true,
success: function(files: Object) {
if (files) {
const file: Object = files['value'][0];
const token: string = files['accessToken'];
const filePath: string = file['parentReference']['path'] + '/' + file['name'];
resolve(new Promise(function(res: Function, rej: Function) {
res(storage.remove(filePath, token));
}));
}
}
};
storage.oneDriveFilepicker.open(odOptions);
});
}
/**
* Delete a diagram from a user's OneDrive at a given path.
* @param {string} path A valid Microsoft OneDrive filepath to delete. Path syntax is
* `/drive/root:/{path-to-file}/{filename}`; i.e. `/drive/root:/Documents/example.diagram`
* @param {string} token A token received by OneDrive filepicker (passed from {@link #removeWithUI}) to allow for
* its file to be deleted. If no token is given, use global {@link #oauthToken}
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file
*/
public remove(path: string, token?: string): Promise<any> {
const storage = this;
const t: string = (token) ? token : storage.oauthToken;
return new Promise(function(resolve: Function, reject: Function) {
storage.getFile(path, t).then(function(file: Object) {
const deletedFile: gcs.DiagramFile = { name: file['name'], id: file['id'], path: file['parentReference']['path'] + '/' + file['name'] };
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.open('DELETE', 'https://graph.microsoft.com/v1.0' + path, true);
xhr.setRequestHeader('Authorization', 'Bearer' + t);
xhr.onload = function() {
if (xhr.readyState === 4 && xhr.status === 204) {
if (storage.currentDiagramFile && path === storage.currentDiagramFile.path) storage.currentDiagramFile = { id: null, path: null, name: null };
resolve(deletedFile);
} else if (xhr.status === 401) { // unauthorized request
storage.authorize(true);
} else {
reject(xhr.response);
}
};
xhr.send();
}).catch(function(err: string) {
throw Error(err);
});
});
}
/**
* Generate and return a Microsoft Graph URL for a target item
* @param {Object} driveItem The item to generate the URL for
* @param {Boolean} targetParentFolder Indicates whether to target the parent folder + filename instead of the item itself
* @param {Boolean} itemRelativeApiPath Indicates whether to append /content to the item URL
* @return {string} Returns a Microsoft Graph URL for a target item
*/
public generateGraphUrl(driveItem: Object, targetParentFolder: boolean, itemRelativeApiPath: boolean) {
let url: string = 'https://graph.microsoft.com/v1.0/';
if (targetParentFolder) url += 'drives/' + driveItem['parentReference']['driveId'] + '/items/' + driveItem['parentReference']['id'] + '/children/' + driveItem['name'];
else url += 'drives/' + driveItem['parentReference']['driveId'] + '/items/' + driveItem['id'];
if (itemRelativeApiPath) url += '/content';
return url;
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*/
import * as GoCloudStorage from './GoCloudStorage.js';
import * as GoDropBox from './GoDropBox.js';
import * as GoGoogleDrive from './GoGoogleDrive.js';
import * as GoLocalStorage from './GoLocalStorage.js';
import * as GoNetCore from './GoNetCore.js';
import * as GoOneDrive from './GoOneDrive.js';
module.exports = {
GoLocalStorage: require('./GoLocalStorage').GoLocalStorage,
GoDropBox: require('./GoDropBox').GoDropBox,
GoGoogleDrive: require('./GoGoogleDrive').GoGoogleDrive,
GoOneDrive: require('./GoOneDrive').GoOneDrive,
GoNetCore: require('./GoNetCore').GoNetCore,
GoCloudStorageManager: require('./GoCloudStorageManager').GoCloudStorageManager
};