diff --git a/app/client/components/Confirm.ts b/app/client/components/Confirm.ts deleted file mode 100644 index 78955165..00000000 --- a/app/client/components/Confirm.ts +++ /dev/null @@ -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, - 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()); -} diff --git a/app/client/components/EmbedForm.js b/app/client/components/EmbedForm.js deleted file mode 100644 index d0ec5faa..00000000 --- a/app/client/components/EmbedForm.js +++ /dev/null @@ -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; diff --git a/app/client/components/Login.js b/app/client/components/Login.js deleted file mode 100644 index 4134f5f5..00000000 --- a/app/client/components/Login.js +++ /dev/null @@ -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; diff --git a/app/client/components/ModalDialog.js b/app/client/components/ModalDialog.js deleted file mode 100644 index 4872b5ee..00000000 --- a/app/client/components/ModalDialog.js +++ /dev/null @@ -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; diff --git a/app/client/components/ProfileForm.js b/app/client/components/ProfileForm.js deleted file mode 100644 index 723f94d0..00000000 --- a/app/client/components/ProfileForm.js +++ /dev/null @@ -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; diff --git a/app/client/components/ViewConfigTab.js b/app/client/components/ViewConfigTab.js index c7077009..974985fd 100644 --- a/app/client/components/ViewConfigTab.js +++ b/app/client/components/ViewConfigTab.js @@ -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.') ); } }; diff --git a/app/client/declarations.d.ts b/app/client/declarations.d.ts index c6a01954..208c5fb7 100644 --- a/app/client/declarations.d.ts +++ b/app/client/declarations.d.ts @@ -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"; diff --git a/app/client/exposeModulesForTests.js b/app/client/exposeModulesForTests.js index 21bb26d5..7fba8607 100644 --- a/app/client/exposeModulesForTests.js +++ b/app/client/exposeModulesForTests.js @@ -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'), }); diff --git a/app/client/models/DocListModel.js b/app/client/models/DocListModel.js deleted file mode 100644 index 3ea506c8..00000000 --- a/app/client/models/DocListModel.js +++ /dev/null @@ -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; diff --git a/app/client/ui/App.ts b/app/client/ui/App.ts index 7e14951f..4f050214 100644 --- a/app/client/ui/App.ts +++ b/app/client/ui/App.ts @@ -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; - 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.