mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) Some cleanup: remove old unused modules.
Summary: - Remove modules related to old login / profile that we don't plan to bring back. - Remove old unused DocListModel. - Remove ext* tests that have been skipped and don't work. - Remove old ModalDialog, and switch its one remaining usage to the newer way. Test Plan: All tests should pass, and as many as before. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2668
This commit is contained in:
		
							parent
							
								
									2e22966289
								
							
						
					
					
						commit
						f24a82e8d4
					
				| @ -1,30 +0,0 @@ | ||||
| // Grist client libs
 | ||||
| import * as ModalDialog from 'app/client/components/ModalDialog'; | ||||
| import * as dom from 'app/client/lib/dom'; | ||||
| import * as kd from 'app/client/lib/koDom'; | ||||
| import * as kf from 'app/client/lib/koForm'; | ||||
| 
 | ||||
| export function showConfirmDialog(title: string, btnText: string, onConfirm: () => Promise<void>, | ||||
|                                   explanation?: Element|string): void { | ||||
|   const body = dom('div.confirm', | ||||
|     explanation ? kf.row(explanation, kd.style('margin-bottom', '2rem')) : null, | ||||
|     kf.row( | ||||
|       1, kf.buttonGroup( | ||||
|         kf.button(() => dialog.hide(), 'Cancel') | ||||
|       ), | ||||
|       1, kf.buttonGroup( | ||||
|         kf.accentButton(async () => { | ||||
|           await onConfirm(); | ||||
|           dialog.hide(); | ||||
|         }, btnText) | ||||
|       ) | ||||
|     ) | ||||
|   ); | ||||
|   const dialog = ModalDialog.create({ | ||||
|     title, | ||||
|     body, | ||||
|     width: '300px', | ||||
|     show: true | ||||
|   }); | ||||
|   dialog.once('close', () => dialog.dispose()); | ||||
| } | ||||
| @ -1,205 +0,0 @@ | ||||
| // External dependencies
 | ||||
| const _                     = require('underscore'); | ||||
| const ko                    = require('knockout'); | ||||
| const BackboneEvents        = require('backbone').Events; | ||||
| 
 | ||||
| // Grist client libs
 | ||||
| const dispose               = require('../lib/dispose'); | ||||
| const dom                   = require('../lib/dom'); | ||||
| const kd                    = require('../lib/koDom'); | ||||
| const kf                    = require('../lib/koForm'); | ||||
| const ModalDialog           = require('./ModalDialog'); | ||||
| const gutil                 = require('app/common/gutil'); | ||||
| 
 | ||||
| const BASE_URL = 'https://syvvdfor2a.execute-api.us-east-1.amazonaws.com/test'; | ||||
| 
 | ||||
| /** | ||||
|  * EmbedForm - Handles logic and dom for the modal embedding instruction box. | ||||
|  */ | ||||
| function EmbedForm(gristDoc) { | ||||
|   this._docComm = gristDoc.docComm; | ||||
|   this._login = gristDoc.app.login; | ||||
|   this._basketId = gristDoc.docInfo.basketId; | ||||
|   this._tableIds = gristDoc.docModel.allTableIds.peek().sort(); | ||||
| 
 | ||||
|   // Arrays of published and unpublished tables, initialized in this._refreshTables()
 | ||||
|   this._published = ko.observable([]); | ||||
|   this._unpublished = ko.observable([]); | ||||
| 
 | ||||
|   // Notify strings which are displayed to the user when set
 | ||||
|   this._errorNotify = ko.observable(); | ||||
|   this._updateNotify = ko.observable(); | ||||
| 
 | ||||
|   // The state of initialization, either 'connecting', 'failed', or 'done'.
 | ||||
|   this._initState = ko.observable('connecting'); | ||||
| 
 | ||||
|   this._embedDialog = this.autoDispose(ModalDialog.create({ | ||||
|     title: 'Upload for External Embedding', | ||||
|     body: this._buildEmbedDom(), | ||||
|     width: '420px' | ||||
|   })); | ||||
|   this._embedDialog.show(); | ||||
| 
 | ||||
|   this.listenTo(this._embedDialog, 'close', () => this.dispose()); | ||||
| 
 | ||||
|   // Perform the initial fetch to see which tables are published.
 | ||||
|   this._initFetch(); | ||||
| } | ||||
| _.extend(EmbedForm.prototype, BackboneEvents); | ||||
| dispose.makeDisposable(EmbedForm); | ||||
| 
 | ||||
| /** | ||||
|  * Performs the initial fetch to see which tables are published. | ||||
|  * Times out after 4 seconds, giving the user the option to retry. | ||||
|  */ | ||||
| EmbedForm.prototype._initFetch = function() { | ||||
|   this._initState('connecting'); | ||||
|   return this._refreshTables() | ||||
|   .timeout(4000) | ||||
|   .then(() => { | ||||
|     this._initState('done'); | ||||
|   }) | ||||
|   .catch(err => { | ||||
|     console.error("EmbedForm._initFetch failed", err); | ||||
|     this._initState('failed'); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Calls on basket to see which tables are published, then updates the published | ||||
|  * and unpublished local observables. | ||||
|  */ | ||||
| EmbedForm.prototype._refreshTables = function() { | ||||
|   // Fetch the tables from the basket
 | ||||
|   return this._login.getBasketTables(this._docComm) | ||||
|   .then(basketTableIds => { | ||||
|     let published = []; | ||||
|     let unpublished = []; | ||||
|     gutil.sortedScan(this._tableIds, basketTableIds.sort(), (local, cloud) => { | ||||
|       let item = { | ||||
|         tableId: local || cloud, | ||||
|         local: Boolean(local), | ||||
|         cloud: Boolean(cloud) | ||||
|       }; | ||||
|       if (cloud) { | ||||
|         published.push(item); | ||||
|       } else { | ||||
|         unpublished.push(item); | ||||
|       } | ||||
|     }); | ||||
|     this._published(published); | ||||
|     this._unpublished(unpublished); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Builds the part of the form showing the table names and their status, and | ||||
|  * the buttons to change their status. | ||||
|  */ | ||||
| EmbedForm.prototype._buildTablesDom = function() { | ||||
|   return dom('div.embed-form-tables', | ||||
|     kd.scope(this._published, published => { | ||||
|       return published.length > 0 ? dom('div.embed-form-published', | ||||
|         dom('div.embed-form-desc', `Published to Basket (basketId: ${this._basketId()})`), | ||||
|         published.map(t => { | ||||
|           return kf.row( | ||||
|             16, dom('a.embed-form-table-id', { href: this._getUrl(t.tableId), target: "_blank" }, | ||||
|               t.tableId), | ||||
|             8, t.local ? this._makeButton('Update', t.tableId, 'update') : 'Only in Basket', | ||||
|             1, dom('div'), | ||||
|             2, this._makeButton('x', t.tableId, 'delete') | ||||
|           ); | ||||
|         }) | ||||
|       ) : null; | ||||
|     }), | ||||
|     dom('div.embed-form-unpublished', | ||||
|       kd.scope(this._unpublished, unpublished => { | ||||
|         return unpublished.map(t => { | ||||
|           return kf.row( | ||||
|             16, dom('span.embed-form-table-id', t.tableId), | ||||
|             8, this._makeButton('Publish', t.tableId, 'add'), | ||||
|             3, dom('div') | ||||
|           ); | ||||
|         }); | ||||
|       }) | ||||
|     ) | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Builds the body of the table publishing modal form. | ||||
|  */ | ||||
| EmbedForm.prototype._buildEmbedDom = function() { | ||||
|   // TODO: Include links to the npm page and to download basket-api.js.
 | ||||
|   return dom('div.embed-form', | ||||
|     kd.scope(this._initState, state => { | ||||
|       switch (state) { | ||||
|         case 'connecting': | ||||
|           return dom('div.embed-form-connect', 'Connecting...'); | ||||
|         case 'failed': | ||||
|           return dom('div', | ||||
|             dom('div.embed-form-connect', 'Connection to Basket failed'), | ||||
|             kf.buttonGroup( | ||||
|               kf.button(() => { | ||||
|                 this._initFetch(); | ||||
|               }, 'Retry') | ||||
|             ) | ||||
|           ); | ||||
|         case 'done': | ||||
|           return dom('div', | ||||
|             dom('div.embed-form-desc', 'Manage tables published to the cloud via Grist Basket.'), | ||||
|             dom('div.embed-form-desc', 'Note that by default, published tables are public.'), | ||||
|             this._buildTablesDom(), | ||||
|             dom('div.embed-form-desc', 'Basket is used to provide easy access to cloud-synced data:'), | ||||
|             dom('div.embed-form-link', | ||||
|               dom('a', { href: 'https://github.com/gristlabs/basket-api', target: "_blank" }, | ||||
|                 'Basket API on GitHub') | ||||
|             ) | ||||
|           ); | ||||
|       } | ||||
|     }), | ||||
|     kd.maybe(this._updateNotify, update => { | ||||
|       return dom('div.login-success-notify', | ||||
|         dom('div.login-success-text', update) | ||||
|       ); | ||||
|     }), | ||||
|     kd.maybe(this._errorNotify, err => { | ||||
|       return dom('div.login-error-notify', | ||||
|         dom('div.login-error-text', err) | ||||
|       ); | ||||
|     }) | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // Helper to perform embedAction ('add' | 'update' | 'delete') on tableId.
 | ||||
| EmbedForm.prototype._embedTable = function(tableId, embedAction) { | ||||
|   this._errorNotify(''); | ||||
|   this._updateNotify(''); | ||||
|   return this._docComm.embedTable(tableId, embedAction) | ||||
|   .then(() => { | ||||
|     return this._refreshTables(); | ||||
|   }) | ||||
|   .then(() => { | ||||
|     if (embedAction === 'update') { | ||||
|       this._updateNotify(`Updated table ${tableId}`); | ||||
|     } | ||||
|   }) | ||||
|   .catch(err => { | ||||
|     this._errorNotify(err.message); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| // Helper to make a button with text, that when pressed performs embedAction
 | ||||
| //  ('add' | 'update' | 'delete') on tableId.
 | ||||
| EmbedForm.prototype._makeButton = function(text, tableId, embedAction) { | ||||
|   return kf.buttonGroup( | ||||
|     kf.button(() => this._embedTable(tableId, embedAction), text) | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // Returns the URL to see the hosted data for tableId.
 | ||||
| EmbedForm.prototype._getUrl = function(tableId) { | ||||
|   return `${BASE_URL}/${this._basketId()}/tables/${tableId}`; | ||||
| }; | ||||
| 
 | ||||
| module.exports = EmbedForm; | ||||
| @ -1,129 +0,0 @@ | ||||
| /* global window */ | ||||
| 
 | ||||
| 
 | ||||
| // External dependencies
 | ||||
| const Promise               = require('bluebird'); | ||||
| const ko                    = require('knockout'); | ||||
| 
 | ||||
| // Grist client libs
 | ||||
| const dispose               = require('../lib/dispose'); | ||||
| const ProfileForm           = require('./ProfileForm'); | ||||
| 
 | ||||
| /** | ||||
|  * Login - Handles dom and settings for the login box. | ||||
|  * @param {app} app - The app instance. | ||||
|  */ | ||||
| function Login(app) { | ||||
|   this.app = app; | ||||
|   this.comm = this.app.comm; | ||||
| 
 | ||||
|   // When logged in, an object containing user profile properties.
 | ||||
|   this._profile = ko.observable(); | ||||
|   this.isLoggedIn = ko.observable(false); | ||||
|   this.emailObs = this.autoDispose(ko.computed(() => ((this._profile() && this._profile().email) || ''))); | ||||
|   this.nameObs = this.autoDispose(ko.computed(() => ((this._profile() && this._profile().name) || ''))); | ||||
|   this.buttonText = this.autoDispose(ko.computed(() => | ||||
|     this.isLoggedIn() ? this.emailObs() : 'Log in')); | ||||
| 
 | ||||
|   // Instantialized with createLoginForm() and createProfileForm()
 | ||||
|   this.profileForm = null; | ||||
| } | ||||
| dispose.makeDisposable(Login); | ||||
| 
 | ||||
| /** | ||||
|  * Returns the current profile object. | ||||
|  */ | ||||
| Login.prototype.getProfile = function() { | ||||
|   return this._profile(); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Opens the Cognito login form in a new browser window to allow the user to log in. | ||||
|  * The login tokens are sent back to the server to which this client belongs. | ||||
|  */ | ||||
| Login.prototype.login = function() { | ||||
|   if (window.isRunningUnderElectron) { | ||||
|     // Under electron, we open the login URL (it opens in a user's default browser).
 | ||||
|     // With null for redirectUrl, it will close automatically when login completes.
 | ||||
|     return this.comm.getLoginUrl(null) | ||||
|     .then((loginUrl) => window.open(loginUrl)); | ||||
|   } else { | ||||
|     // In hosted / dev version, we redirect to the login URL, and it will redirect back to the
 | ||||
|     // starting URL when login completes.
 | ||||
|     return this.comm.getLoginUrl(window.location.href) | ||||
|     .then((loginUrl) => { window.location.href = loginUrl; }); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Tells the server to log out, and also opens a new window to the logout URL to get Cognito to | ||||
|  * clear its cookies. The new window will hit our page which will close the window automatically. | ||||
|  */ | ||||
| Login.prototype.logout = function() { | ||||
|   // We both log out the server, and for hosted version, visit a logout URL to clear AWS cookies.
 | ||||
|   if (window.isRunningUnderElectron) { | ||||
|     // Under electron, only clear the server state. Don't open the user's default browser
 | ||||
|     // to clear cookies there because it serves dubious purpose and is annoying to the user.
 | ||||
|     return this.comm.logout(null); | ||||
|   } else { | ||||
|     // In hosted / dev version, we redirect to the logout URL, which will clear cookies and
 | ||||
|     // redirect back to the starting URL when logout completes.
 | ||||
|     return this.comm.logout(window.location.href) | ||||
|     .then((logoutUrl) => { window.location.href = logoutUrl; }); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Retrieves the updated user profile from DynamoDB and creates the profile form. | ||||
|  * Also sends the fetched user profile to the server to keep it up to date. | ||||
|  */ | ||||
| Login.prototype.createProfileForm = function() { | ||||
|   // ProfileForm disposes itself, no need to handle disposal.
 | ||||
|   this.profileForm = ProfileForm.create(this); | ||||
| }; | ||||
| 
 | ||||
| // Called when the user logs out in this or another tab.
 | ||||
| Login.prototype.onLogout = function() { | ||||
|   this._profile(null); | ||||
|   this.isLoggedIn(false); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Update the internally-stored profile given a profile object from the server. | ||||
|  */ | ||||
| Login.prototype.updateProfileFromServer = function(profileObj) { | ||||
|   this._profile(profileObj); | ||||
|   this.isLoggedIn(Boolean(this._profile.peek())); | ||||
| }; | ||||
| 
 | ||||
| Login.prototype.setProfileItem = function(key, val) { | ||||
|   return this.comm.updateProfile({[key]: val}); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Returns an array of tableIds in the basket of the current document. If the current | ||||
|  *  document has no basket, an empty array is returned. | ||||
|  */ | ||||
| Login.prototype.getBasketTables = function(docComm) { | ||||
|   return docComm.getBasketTables(); | ||||
| }; | ||||
| 
 | ||||
| // Execute func if the user is logged in. Otherwise, prompt the user to log in
 | ||||
| //  and then execute the function. Attempts refresh if the token is expired.
 | ||||
| Login.prototype.tryWithLogin = function(func) { | ||||
|   return Promise.try(() => { | ||||
|     if (!this.isLoggedIn()) { | ||||
|       return this.login(); | ||||
|     } | ||||
|   }) | ||||
|   .then(() => func()) | ||||
|   .catch(err => { | ||||
|     if (err.code === 'LoginClosedError') { | ||||
|       console.log("Login#tryWithLogin", err); | ||||
|     } else { | ||||
|       throw err; | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| module.exports = Login; | ||||
| @ -1,125 +0,0 @@ | ||||
| /* global $, document, window */ | ||||
| 
 | ||||
| var _ = require('underscore'); | ||||
| var ko = require('knockout'); | ||||
| var BackboneEvents = require('backbone').Events; | ||||
| var dom = require('../lib/dom'); | ||||
| var kd = require('../lib/koDom'); | ||||
| var Base = require('./Base'); | ||||
| 
 | ||||
| /** | ||||
|  * ModalDialog constructor creates a new ModalDialog element. The dialog accepts a bunch of | ||||
|  * options, and the content can be overridde when calling .show(), so that a single ModalDialog | ||||
|  * component can be used for different purposes. It triggers a 'close' event (using | ||||
|  * Backbone.Events) when hidden. | ||||
|  * | ||||
|  * The DOM of the dialog is always attached to document.body. | ||||
|  * | ||||
|  * @param {Boolean} [options.fade]      Include `fade` css class to fade the modal in/out. | ||||
|  * @param {Boolean} [options.close]     Include a close icon in the corner (default true). | ||||
|  * @param {Boolean} [options.backdrop]  Include a modal-backdrop element (default true). | ||||
|  * @param {Boolean} [options.keyboard]  Close the modal on Escape key (default true). | ||||
|  * @param {Boolean} [options.show]      Shows the modal when initialized (default true). | ||||
|  * @param {CSS String} [options.width]  Optional css width to override default. | ||||
|  * @param {DOM|String} [options.title]  The content to place in the title. | ||||
|  * @param {DOM|String} [options.body]   The content to place in the body. | ||||
|  * @param {DOM|String} [options.footer] The content to place in the footer. | ||||
|  */ | ||||
| function ModalDialog(options) { | ||||
|   Base.call(this, options); | ||||
|   options = options || {}; | ||||
|   this.options = _.defaults(options, { | ||||
|     fade:     false,        // Controls whether the model pops or fades into view
 | ||||
|     close:    true,        // Determines whether the "x" dismiss icon appears in the modal
 | ||||
|     backdrop: true, | ||||
|     keyboard: true, | ||||
|     show:     false, | ||||
|   }); | ||||
| 
 | ||||
|   // If the width option is set, the margins must be set to auto to keep the dialog centered.
 | ||||
|   this.style = options.width ? | ||||
|     `width: ${this.options.width}; margin-left: auto; margin-right: auto;` : ''; | ||||
|   this.title = ko.observable(options.title || null); | ||||
|   this.body = ko.observable(options.body || null); | ||||
|   this.footer = ko.observable(options.footer || null); | ||||
| 
 | ||||
|   this.modal = this.autoDispose(this._buildDom()); | ||||
|   document.body.appendChild(this.modal); | ||||
|   $(this.modal).modal(_.pick(this.options, 'backdrop', 'keyboard', 'show')); | ||||
| 
 | ||||
|   // On applyState event, close the modal.
 | ||||
|   this.onEvent(window, 'applyState', () => this.hide()); | ||||
| 
 | ||||
|   // If disposed, let the underlying JQuery Modal run its hiding logic, and trigger 'close' event.
 | ||||
|   this.autoDisposeCallback(this.hide); | ||||
| } | ||||
| Base.setBaseFor(ModalDialog); | ||||
| _.extend(ModalDialog.prototype, BackboneEvents); | ||||
| 
 | ||||
| /** | ||||
|  * Shows the ModalDialog. It accepts the same `title`, `body`, and `footer` options as the | ||||
|  * constructor, which will replace previous content of those sections. | ||||
|  */ | ||||
| ModalDialog.prototype.show = function(options) { | ||||
|   options = options || {}; | ||||
|   // Allow options to specify new title, body, and footer content.
 | ||||
|   ['title', 'body', 'footer'].forEach(function(prop) { | ||||
|     if (options.hasOwnProperty(prop)) { | ||||
|       this[prop](options[prop]); | ||||
|     } | ||||
|   }, this); | ||||
| 
 | ||||
|   $(this.modal).modal('show'); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Hides the ModalDialog. This triggers the `close` to be triggered using Backbone.Events. | ||||
|  */ | ||||
| ModalDialog.prototype.hide = function() { | ||||
|   $(this.modal).modal('hide'); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Internal helper to build the DOM of the dialog. | ||||
|  */ | ||||
| ModalDialog.prototype._buildDom = function() { | ||||
|   var self = this; | ||||
|   // The .clipboard_focus class tells Clipboard.js to let this component have focus. Otherwise
 | ||||
|   // it's impossible to select text.
 | ||||
|   return dom('div.modal.clipboard_focus', | ||||
|     { "role": "dialog", "tabIndex": -1 }, | ||||
| 
 | ||||
|     // Emit a 'close' Backbone.Event whenever the dialog is hidden.
 | ||||
|     dom.on('hidden.bs.modal', function() { | ||||
|       self.trigger('close'); | ||||
|     }), | ||||
| 
 | ||||
|     dom('div.modal-dialog', { style: this.style }, | ||||
|       dom('div.modal-content', | ||||
|         kd.toggleClass('fade', self.options.fade), | ||||
| 
 | ||||
|         kd.maybe(this.title, function(title) { | ||||
|           return dom('div.modal-header', | ||||
|             kd.maybe(self.options.close, function () { | ||||
|               return dom('button.close', | ||||
|                 {"data-dismiss": "modal", "aria-label": "Close"}, | ||||
|                 dom('span', {"aria-hidden": true}, '×') | ||||
|               ); | ||||
|             }), | ||||
|             dom('h4.modal-title', title) | ||||
|           ); | ||||
|         }), | ||||
| 
 | ||||
|         kd.maybe(this.body, function(body) { | ||||
|           return dom('div.modal-body', body); | ||||
|         }), | ||||
| 
 | ||||
|         kd.maybe(this.footer, function(footer) { | ||||
|           return dom('div.modal-footer', footer); | ||||
|         }) | ||||
|       ) | ||||
|     ) | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| module.exports = ModalDialog; | ||||
| @ -1,160 +0,0 @@ | ||||
| /* global document */ | ||||
| 
 | ||||
| // External dependencies
 | ||||
| const _                     = require('underscore'); | ||||
| const ko                    = require('knockout'); | ||||
| const BackboneEvents        = require('backbone').Events; | ||||
| 
 | ||||
| // Grist client libs
 | ||||
| const dispose               = require('../lib/dispose'); | ||||
| const dom                   = require('../lib/dom'); | ||||
| const kf                    = require('../lib/koForm'); | ||||
| const kd                    = require('../lib/koDom'); | ||||
| const ModalDialog           = require('./ModalDialog'); | ||||
| 
 | ||||
| /** | ||||
|  * ProfileForm - Handles dom and settings for the profile box. | ||||
|  * @param {Login} login - The login instance. | ||||
|  */ | ||||
| function ProfileForm(login) { | ||||
|   this._login = login; | ||||
|   this._comm = this._login.comm; | ||||
|   this._gristLogin = this._login.gristLogin; | ||||
|   this._errorNotify = ko.observable(); | ||||
|   this._successNotify = ko.observable(); | ||||
| 
 | ||||
|   // Form data which may be filled in when modifying profile information.
 | ||||
|   this._newName = ko.observable(''); | ||||
| 
 | ||||
|   // Counter used to provide each edit profile sub-form with an id which indicates
 | ||||
|   // when it is visible.
 | ||||
|   this._formId = 1; | ||||
|   this._editingId = ko.observable(null); | ||||
| 
 | ||||
|   this._profileDialog = this.autoDispose(ModalDialog.create({ | ||||
|     title: 'User profile', | ||||
|     body: this._buildProfileDom(), | ||||
|     width: '420px' | ||||
|   })); | ||||
|   this._profileDialog.show(); | ||||
| 
 | ||||
|   // TODO: Some indication is necessary that verification is occurring between
 | ||||
|   //  submitting the form and waiting for the box to close.
 | ||||
|   this.listenTo(this._comm, 'clientLogout', () => this.dispose()); | ||||
|   this.listenTo(this._profileDialog, 'close', () => this.dispose()); | ||||
| } | ||||
| _.extend(ProfileForm.prototype, BackboneEvents); | ||||
| dispose.makeDisposable(ProfileForm); | ||||
| 
 | ||||
| /** | ||||
|  * Builds the body of the profile modal form. | ||||
|  */ | ||||
| ProfileForm.prototype._buildProfileDom = function() { | ||||
|   return dom('div.profile-form', | ||||
|     // Email
 | ||||
|     // TODO: Allow changing email
 | ||||
|     this._buildProfileRow('Email', { | ||||
|       buildDisplayFunc: () => dom('div', | ||||
|         kd.text(this._login.emailObs), | ||||
|         dom.testId('ProfileForm_viewEmail') | ||||
|       ) | ||||
|     }), | ||||
|     // Name
 | ||||
|     this._buildProfileRow('Name', { | ||||
|       buildDisplayFunc: () => dom('div', | ||||
|         kd.text(this._login.nameObs), | ||||
|         dom.testId('ProfileForm_viewName') | ||||
|       ), | ||||
|       buildEditFunc: () => dom('div', | ||||
|         kf.label('New name'), | ||||
|         kf.text(this._newName, {}, dom.testId('ProfileForm_newName')) | ||||
|       ), | ||||
|       submitFunc: () => this._submitNameChange() | ||||
|     }), | ||||
|     // TODO: Allow editing profile image.
 | ||||
|     kd.maybe(this._successNotify, success => { | ||||
|       return dom('div.login-success-notify', | ||||
|         dom('div.login-success-text', success) | ||||
|       ); | ||||
|     }), | ||||
|     kd.maybe(this._errorNotify, err => { | ||||
|       return dom('div.login-error-notify', | ||||
|         dom('div.login-error-text', err) | ||||
|       ); | ||||
|     }) | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Builds a row of the profile form. | ||||
|  * @param {String} label - Indicates the profile item displayed by the row. | ||||
|  * @param {Function} options.buildDisplayFunc - A function which returns dom representing | ||||
|  *  the value of the profile item to be displayed. If omitted, no value is visible. | ||||
|  * @param {Function} options.buildEditFunc - A function which returns dom to change the | ||||
|  *  value of the profile item. If omitted, the profile item may not be edited. | ||||
|  * @param {Function} options.submitFunc - A function to call to save changes to the | ||||
|  *  profile item. MUST be included if buildEditFunc is included. | ||||
|  */ | ||||
| ProfileForm.prototype._buildProfileRow = function(label, options) { | ||||
|   options = options || {}; | ||||
|   let formId = this._formId++; | ||||
| 
 | ||||
|   return dom('div.profile-row', | ||||
|     kf.row( | ||||
|       2, kf.label(label), | ||||
|       5, options.buildDisplayFunc ? options.buildDisplayFunc() : '', | ||||
|       1, dom('div.btn.edit-profile.glyphicon.glyphicon-pencil', | ||||
|         { style: `visibility: ${options.buildEditFunc ? 'visible' : 'hidden'}` }, | ||||
|         dom.testId(`ProfileForm_edit${label}`), | ||||
|         dom.on('click', () => { | ||||
|           this._editingId(this._editingId() === formId ? null : formId); | ||||
|         }) | ||||
|       ) | ||||
|     ), | ||||
|     kd.maybe(() => this._editingId() === formId, () => { | ||||
|       return dom('div', | ||||
|         dom.on('keydown', e => { | ||||
|           if (e.keyCode === 13) { | ||||
|             // Current element is likely a knockout text field with changes that haven't yet been
 | ||||
|             // saved to the observable. Blur the current element to ensure its value is saved.
 | ||||
|             document.activeElement.blur(); | ||||
|             options.submitFunc(); | ||||
|           } | ||||
|         }), | ||||
|         dom('div.edit-profile-form', | ||||
|           options.buildEditFunc(), | ||||
|           dom('div.login-btns.flexhbox', | ||||
|             kf.buttonGroup( | ||||
|               kf.button(() => this._editingId(null), 'Cancel', | ||||
|                 dom.testId('ProfileForm_cancel')) | ||||
|             ), | ||||
|             kf.buttonGroup( | ||||
|               kf.accentButton(() => options.submitFunc(), 'Submit', | ||||
|                 dom.testId('ProfileForm_submit')) | ||||
|             ) | ||||
|           ) | ||||
|         ) | ||||
|       ); | ||||
|     }) | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // Submits the profile name change form.
 | ||||
| ProfileForm.prototype._submitNameChange = function() { | ||||
|   if (!this._newName()) { | ||||
|     throw new Error('Name may not be blank.'); | ||||
|   } | ||||
|   return this._login.setProfileItem('name', this._newName()) | ||||
|   // TODO: attemptRefreshToken() should be handled in a general way for all methods
 | ||||
|   // which require using tokens after sign in.
 | ||||
|   .then(() => { | ||||
|     this._editingId(null); | ||||
|     this._successNotify('Successfully changed name.'); | ||||
|   }) | ||||
|   .catch(err => { | ||||
|     console.error('Error changing name', err); | ||||
|     this._errorNotify(err.message); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| module.exports = ProfileForm; | ||||
| @ -5,7 +5,6 @@ var dom = require('../lib/dom'); | ||||
| var kd = require('../lib/koDom'); | ||||
| var kf = require('../lib/koForm'); | ||||
| var koArray = require('../lib/koArray'); | ||||
| var {showConfirmDialog} = require('./Confirm'); | ||||
| var SummaryConfig = require('./SummaryConfig'); | ||||
| var commands = require('./commands'); | ||||
| var {CustomSectionElement} = require('../lib/CustomSectionElement'); | ||||
| @ -20,6 +19,7 @@ const {basicButton, primaryButton} = require('app/client/ui2018/buttons'); | ||||
| const {colors} = require('app/client/ui2018/cssVars'); | ||||
| const {cssDragger} = require('app/client/ui2018/draggableList'); | ||||
| const {menu, menuItem, select} = require('app/client/ui2018/menus'); | ||||
| const {confirmModal} = require('app/client/ui2018/modals'); | ||||
| const isEqual = require('lodash/isEqual'); | ||||
| const {cssMenuItem} = require('popweasel'); | ||||
| 
 | ||||
| @ -421,18 +421,18 @@ ViewConfigTab.prototype._makeOnDemand = function(table) { | ||||
|   } | ||||
| 
 | ||||
|   if (table.onDemand()) { | ||||
|     showConfirmDialog('Unmark table On-Demand?', 'Unmark On-Demand', onConfirm, | ||||
|     confirmModal('Unmark table On-Demand?', 'Unmark On-Demand', onConfirm, | ||||
|       dom('div', 'If you unmark table ', dom('b', table), ' as On-Demand, ' + | ||||
|         'its data will be loaded into the calculation engine and will be available ' + | ||||
|         'for use in formulas. For a big table, this may greatly increase load times.', | ||||
|         dom('br'), 'Changing this setting will reload the document for all users.') | ||||
|         dom('br'), dom('br'), 'Changing this setting will reload the document for all users.') | ||||
|     ); | ||||
|   } else { | ||||
|     showConfirmDialog('Make table On-Demand?', 'Make On-Demand', onConfirm, | ||||
|     confirmModal('Make table On-Demand?', 'Make On-Demand', onConfirm, | ||||
|       dom('div', 'If you make table ', dom('b', table), ' On-Demand, ' + | ||||
|         'its data will no longer be loaded into the calculation engine and will not be available ' + | ||||
|         'for use in formulas. It will remain available for viewing and editing.', | ||||
|         dom('br'), 'Changing this setting will reload the document for all users.') | ||||
|         dom('br'), dom('br'), 'Changing this setting will reload the document for all users.') | ||||
|     ); | ||||
|   } | ||||
| }; | ||||
|  | ||||
							
								
								
									
										4
									
								
								app/client/declarations.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								app/client/declarations.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -3,13 +3,10 @@ declare module "app/client/components/Clipboard"; | ||||
| declare module "app/client/components/CodeEditorPanel"; | ||||
| declare module "app/client/components/DetailView"; | ||||
| declare module "app/client/components/DocConfigTab"; | ||||
| declare module "app/client/components/EmbedForm"; | ||||
| declare module "app/client/components/FieldConfigTab"; | ||||
| declare module "app/client/components/GridView"; | ||||
| declare module "app/client/components/Layout"; | ||||
| declare module "app/client/components/LayoutEditor"; | ||||
| declare module "app/client/components/Login"; | ||||
| declare module "app/client/components/ModalDialog"; | ||||
| declare module "app/client/components/REPLTab"; | ||||
| declare module "app/client/components/commandList"; | ||||
| declare module "app/client/lib/Mousetrap"; | ||||
| @ -18,7 +15,6 @@ declare module "app/client/lib/dom"; | ||||
| declare module "app/client/lib/koDom"; | ||||
| declare module "app/client/lib/koForm"; | ||||
| declare module "app/client/lib/koSession"; | ||||
| declare module "app/client/models/DocListModel"; | ||||
| declare module "app/client/widgets/UserType"; | ||||
| declare module "app/client/widgets/UserTypeImpl"; | ||||
| 
 | ||||
|  | ||||
| @ -7,7 +7,6 @@ Object.assign(window.exposedModules, { | ||||
|   ko: require('knockout'), | ||||
|   moment: require('moment-timezone'), | ||||
|   Comm: require('./components/Comm'), | ||||
|   ProfileForm: require('./components/ProfileForm'), | ||||
|   _loadScript: require('./lib/loadScript'), | ||||
|   ConnectState: require('./models/ConnectState'), | ||||
| }); | ||||
|  | ||||
| @ -1,118 +0,0 @@ | ||||
| var koArray = require('../lib/koArray'); | ||||
| var dispose = require('../lib/dispose'); | ||||
| var _ = require('underscore'); | ||||
| var BackboneEvents = require('backbone').Events; | ||||
| var {pageHasDocList} = require('app/common/urlUtils'); | ||||
| 
 | ||||
| /** | ||||
|  * Constructor for DocListModel | ||||
|  * @param {Object} comm: A map of server methods availble on this document. | ||||
|  */ | ||||
| function DocListModel(app) { | ||||
|   this.app = app; | ||||
|   this.comm = this.app.comm; | ||||
|   this.docs = koArray(); | ||||
|   this.docInvites = koArray(); | ||||
| 
 | ||||
|   if (pageHasDocList()) { | ||||
|     this.listenTo(this.comm, 'docListAction', this.docListActionHandler); | ||||
|     this.listenTo(this.comm, 'receiveInvites', () => this.refreshDocList()); | ||||
| 
 | ||||
|     // Initialize the DocListModel
 | ||||
|     this.refreshDocList(); | ||||
|   } else { | ||||
|     console.log("Page has no DocList support"); | ||||
|   } | ||||
| } | ||||
| dispose.makeDisposable(DocListModel); | ||||
| _.extend(DocListModel.prototype, BackboneEvents); | ||||
| 
 | ||||
| /** | ||||
|  * Rebuilds DocListModel with a direct call to the server. | ||||
|  */ | ||||
| DocListModel.prototype.refreshDocList = function() { | ||||
|   return this.comm.getDocList() | ||||
|     .then(docListObj => { | ||||
|       this.docs.assign(docListObj.docs); | ||||
|       this.docInvites.assign(docListObj.docInvites); | ||||
|     }) | ||||
|     .catch((err) => { | ||||
|       console.error('Failed to load DocListModel: %s', err); | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Updates the DocListModel docs and docInvites arrays in response to docListAction events | ||||
|  * @param {Object} message: A docListAction message received from the server. | ||||
|  */ | ||||
| DocListModel.prototype.docListActionHandler = function(message) { | ||||
|   console.log('docListActionHandler message', message); | ||||
|   if (message && message.data) { | ||||
|     _.each(message.data.addDocs, this.addDoc, this); | ||||
|     _.each(message.data.removeDocs, this.removeDoc, this); | ||||
|     _.each(message.data.changeDocs, this.changeDoc, this); | ||||
|     _.each(message.data.addInvites, this.addInvite, this); | ||||
|     _.each(message.data.removeInvites, this.removeInvite, this); | ||||
|     // DocListModel can ignore rename events since renames also broadcast add/remove events.
 | ||||
|   } else { | ||||
|     console.error('Unrecognized message', message); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| DocListModel.prototype._removeAtIndex = function(collection, index) { | ||||
|   collection.splice(index, 1); | ||||
| }; | ||||
| 
 | ||||
| DocListModel.prototype._removeItem = function(collection, name) { | ||||
|   var index = this._findItem(collection, name); | ||||
|   if (index !== -1) { | ||||
|     this._removeAtIndex(collection, index); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // Binary search is disabled in _.indexOf because the docs may not be sorted by name.
 | ||||
| DocListModel.prototype._findItem = function(collection, name) { | ||||
|   var matchIndex = _.indexOf(collection.all().map(item => item.name), name, false); | ||||
|   if (matchIndex === -1) { | ||||
|     console.error('DocListModel does not contain name:', name); | ||||
|   } | ||||
|   return matchIndex; | ||||
| }; | ||||
| 
 | ||||
| DocListModel.prototype.removeDoc = function(name) { | ||||
|   this._removeItem(this.docs, name); | ||||
| }; | ||||
| 
 | ||||
| // TODO: removeInvite is unused
 | ||||
| DocListModel.prototype.removeInvite = function(name) { | ||||
|   this._removeItem(this.docInvites, name); | ||||
| }; | ||||
| 
 | ||||
| DocListModel.prototype._addItem = function(collection, fileObj) { | ||||
|   var insertIndex = _.sortedIndex(collection.all(), fileObj, 'name'); | ||||
|   this._addItemAtIndex(collection, insertIndex, fileObj); | ||||
| }; | ||||
| 
 | ||||
| DocListModel.prototype._addItemAtIndex = function(collection, index, fileObj) { | ||||
|   collection.splice(index, 0, fileObj); | ||||
| }; | ||||
| 
 | ||||
| DocListModel.prototype.addDoc = function(fileObj) { | ||||
|   this._addItem(this.docs, fileObj); | ||||
| }; | ||||
| 
 | ||||
| // TODO: addInvite is unused
 | ||||
| DocListModel.prototype.addInvite = function(fileObj) { | ||||
|   this._addItem(this.docInvites, fileObj); | ||||
| }; | ||||
| 
 | ||||
| // Called when the metadata for a doc changes.
 | ||||
| DocListModel.prototype.changeDoc = function(fileObj) { | ||||
|   let idx = this._findItem(this.docs, fileObj.name); | ||||
|   if (idx !== -1) { | ||||
|     this._removeAtIndex(this.docs, idx); | ||||
|     this._addItem(this.docs, fileObj); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| module.exports = DocListModel; | ||||
| @ -3,13 +3,11 @@ import * as Clipboard from 'app/client/components/Clipboard'; | ||||
| import {Comm} from 'app/client/components/Comm'; | ||||
| import * as commandList from 'app/client/components/commandList'; | ||||
| import * as commands from 'app/client/components/commands'; | ||||
| import * as Login from 'app/client/components/Login'; | ||||
| import {unsavedChanges} from 'app/client/components/UnsavedChanges'; | ||||
| import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals'; | ||||
| import {isDesktop} from 'app/client/lib/browserInfo'; | ||||
| import * as koUtil from 'app/client/lib/koUtil'; | ||||
| import {reportError, TopAppModel, TopAppModelImpl} from 'app/client/models/AppModel'; | ||||
| import * as DocListModel from 'app/client/models/DocListModel'; | ||||
| import {setUpErrorHandling} from 'app/client/models/errors'; | ||||
| import {createAppUI} from 'app/client/ui/AppUI'; | ||||
| import {attachCssRootVars} from 'app/client/ui2018/cssVars'; | ||||
| @ -24,9 +22,6 @@ import * as ko from 'knockout'; | ||||
| 
 | ||||
| const G = getBrowserGlobals('document', 'window'); | ||||
| 
 | ||||
| type DocListModel = any; | ||||
| type Login = any; | ||||
| 
 | ||||
| /** | ||||
|  * Main Grist App UI component. | ||||
|  */ | ||||
| @ -40,9 +35,7 @@ export class App extends DisposableWithEvents { | ||||
|   public comm = this.autoDispose(Comm.create()); | ||||
|   public clientScope: ClientScope; | ||||
|   public features: ko.Computed<ISupportedFeatures>; | ||||
|   public login: Login; | ||||
|   public topAppModel: TopAppModel;    // Exposed because used by test/nbrowser/gristUtils.
 | ||||
|   public docListModel: DocListModel; | ||||
| 
 | ||||
|   private _settings: ko.Observable<{features?: ISupportedFeatures}>; | ||||
| 
 | ||||
| @ -64,16 +57,11 @@ export class App extends DisposableWithEvents { | ||||
|     this._settings = ko.observable({}); | ||||
|     this.features = ko.computed(() => this._settings().features || {}); | ||||
| 
 | ||||
|     // Creates a Login instance which handles building the login form, login/signup, logout,
 | ||||
|     // and refreshing tokens. Uses .features, so instantiated after that.
 | ||||
|     this.login = this.autoDispose(Login.create(this)); | ||||
| 
 | ||||
|     if (isDesktop()) { | ||||
|       this.autoDispose(Clipboard.create(this)); | ||||
|     } | ||||
| 
 | ||||
|     this.topAppModel = this.autoDispose(TopAppModelImpl.create(null, G.window)); | ||||
|     this.docListModel = this.autoDispose(DocListModel.create(this)); | ||||
| 
 | ||||
|     const isHelpPaneVisible = ko.observable(false); | ||||
| 
 | ||||
| @ -125,7 +113,6 @@ export class App extends DisposableWithEvents { | ||||
|     this.listenTo(this.comm, 'clientConnect', (message) => { | ||||
|       console.log(`App clientConnect event: resetClientId ${message.resetClientId} version ${message.serverVersion}`); | ||||
|       this._settings(message.settings); | ||||
|       this.login.updateProfileFromServer(message.profile); | ||||
|       if (message.serverVersion === 'dead' || (this._serverVersion && this._serverVersion !== message.serverVersion)) { | ||||
|         console.log("Upgrading..."); | ||||
|         // Server has upgraded.  Upgrade client.  TODO: be gentle and polite.
 | ||||
| @ -143,12 +130,6 @@ export class App extends DisposableWithEvents { | ||||
|       this.topAppModel.notifier.setConnectState(isConnected); | ||||
|     }); | ||||
| 
 | ||||
|     this.listenTo(this.comm, 'profileFetch', (message) => { | ||||
|       this.login.updateProfileFromServer(message.data); | ||||
|     }); | ||||
| 
 | ||||
|     this.listenTo(this.comm, 'clientLogout', () => this.login.onLogout()); | ||||
| 
 | ||||
|     this.listenTo(this.comm, 'docShutdown', () => { | ||||
|       console.log("Received docShutdown"); | ||||
|       // Reload on next tick, to let other objects process 'docShutdown' before they get disposed.
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user