mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +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