UNPKG

gojs

Version:

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

493 lines (461 loc) 25.2 kB
/* * 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); }); }); } }