(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
pull/3/head
Dmitry S 4 years ago
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.')
);
}
};

@ -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…
Cancel
Save