diff --git a/NEWS.md b/NEWS.md index 2d7267dac07d..dd8d72200da7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -25,6 +25,7 @@ * Remove tmp and unused files [#2328](https://github.com/CartoDB/cartodb/pull/2328) * Open privacy dialog directly from items [#2442](https://github.com/CartoDB/cartodb/pull/2442) * Fixes error handling when adding an erroneous WMS URL. +* Add loading+error state for privacy dialog [#2484](https://github.com/CartoDB/cartodb/pull/2484) Bugfixes: * When being in any configuration page remove the arrow from the breadcrumb [#2312](https://github.com/CartoDB/cartodb/pull/2312) diff --git a/config/frontend.yml b/config/frontend.yml index 913a40a7c60f..3fb50950cb49 100644 --- a/config/frontend.yml +++ b/config/frontend.yml @@ -1,2 +1,2 @@ #This file defines the version of the frontend code that is going to be loaded. -3.7.5 +3.7.6 diff --git a/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/header_template.jst.ejs b/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/header_template.jst.ejs index 5c67e56fc642..13ded8ffe1c9 100644 --- a/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/header_template.jst.ejs +++ b/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/header_template.jst.ejs @@ -6,6 +6,6 @@ <%= itemName %> privacy

- Although we believe in the power of open data, you can also protected your tables. + Although we believe in the power of open data, you can also protect your <%= itemType %>.

diff --git a/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/share_view.js b/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/share_view.js index 48a0adbb36fb..c7ed4605d812 100644 --- a/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/share_view.js +++ b/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/share_view.js @@ -15,11 +15,12 @@ module.exports = cdb.core.View.extend({ 'click .js-back' : '_onClickBack' }, - initialize: function(args) { - this._org = args.organization; - this._permission = args.permission; - this._canChangeWriteAccess = args.canChangeWriteAccess; - this._tableMetadata = args.tableMetadata; + initialize: function() { + if (!this.options.viewModel) { + throw new Error('viewModel is compulsory'); + } + this._viewModel = this.options.viewModel; + this.add_related_model(this._viewModel); this._template = cdb.templates.getTemplate('new_dashboard/dialogs/change_privacy/share_view/template'); }, @@ -27,7 +28,7 @@ module.exports = cdb.core.View.extend({ render: function() { this.clearSubViews(); - var usersCount = this._org.users.length; + var usersCount = this._organization().users.length; this.$el.html( this._template({ @@ -42,12 +43,16 @@ module.exports = cdb.core.View.extend({ return this; }, + _organization: function() { + return this._viewModel.get('user').organization; + }, + _renderOrganizationPermissionView: function() { this._appendPermissionView( new PermissionView({ - model: this._org, - permission: this._permission, - canChangeWriteAccess: this._canChangeWriteAccess, + model: this._organization(), + permission: this._viewModel.get('permission'), + canChangeWriteAccess: this._viewModel.canChangeWriteAccess(), title: 'Default settings for your Organization', desc: 'New users will have this permission' }) @@ -55,14 +60,14 @@ module.exports = cdb.core.View.extend({ }, _renderUserPermissionViews: function() { - var usersUsingVis = this._usersUsingVis(); + var usersUsingVis = this._viewModel.usersUsingVis(); - this._org.users.each(function(user) { + this._organization().users.each(function(user) { this._appendPermissionView( new PermissionView({ model: user, - permission: this._permission, - canChangeWriteAccess: this._canChangeWriteAccess, + permission: this._viewModel.get('permission'), + canChangeWriteAccess: this._viewModel.canChangeWriteAccess(), title: user.get('username'), desc: user.get('name'), avatarUrl: user.get('avatar_url'), @@ -72,18 +77,6 @@ module.exports = cdb.core.View.extend({ }, this); }, - _usersUsingVis: function() { - return _.chain(_.union( - this._tableMetadata.get('dependent_visualizations'), - this._tableMetadata.get('non_dependent_visualizations') - )) - .compact() - .map(function(visData) { - return visData.permission.owner; - }) - .value(); - }, - _appendPermissionView: function(view) { this.$('.js-permissions').append(view.render().el); this.addView(view); @@ -91,11 +84,11 @@ module.exports = cdb.core.View.extend({ _onClickBack: function(ev) { this.killEvent(ev); - this.trigger('click:back'); + this._viewModel.changeState('Start'); }, _onClickSave: function(ev) { this.killEvent(ev); - this.trigger('click:save'); + this._viewModel.save(); } }); diff --git a/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/start_view.js b/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/start_view.js index afd59efbce34..8a0c95c0971b 100644 --- a/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/start_view.js +++ b/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/start_view.js @@ -1,5 +1,6 @@ var cdb = require('cartodb.js'); var _ = require('underscore'); +var $ = require('jquery'); var DISABLED_SAVE_CLASS_NAME = 'is-disabled'; @@ -13,37 +14,38 @@ module.exports = cdb.core.View.extend({ 'click .js-save' : '_onClickSave', 'click .js-option' : '_selectOption', 'click .js-share' : '_onClickShare', - 'keyup .js-password-input' : '_updatePassword', - 'blur .js-password-input' : 'render' // make sure the view is consistently rendered after writing password + 'keyup .js-password-input' : '_updatePassword' }, - initialize: function(args) { - this.vis = args.vis; // of model cdb.admin.Visualization - this.user = args.user; - this.upgradeUrl = args.upgradeUrl; - this.privacyOptions = args.privacyOptions; + initialize: function() { + if (!this.options.viewModel) { + throw new Error('viewModel is compulsory'); + } + this._viewModel = this.options.viewModel; + this._viewModel.get('privacyOptions').bind('change', this.render, this); + this.add_related_model(this._viewModel); this.template = cdb.templates.getTemplate('new_dashboard/dialogs/change_privacy/start_view_template'); - - this.privacyOptions.bind('change', this.render, this); - this.add_related_model(this.privacyOptions); }, render: function() { - var pwdOption = this.privacyOptions.passwordOption(); - var selectedOption = this.privacyOptions.selectedOption(); - var org = this.user.organization; + this.clearSubViews(); + + var privacyOptions = this._viewModel.get('privacyOptions'); + var pwdOption = privacyOptions.passwordOption(); + var selectedOption = privacyOptions.selectedOption(); + var upgradeUrl = this._viewModel.get('upgradeUrl'); this.$el.html( this.template({ - itemName: this.vis.get('name'), - options: this.privacyOptions, + itemName: this._viewModel.get('vis').get('name'), + options: privacyOptions, showPasswordInput: pwdOption === selectedOption, pwdOption: pwdOption, saveBtnClassNames: selectedOption.canSave() ? '' : DISABLED_SAVE_CLASS_NAME, - showUpgradeBanner: !!this.upgradeUrl && this.privacyOptions.any(function(option) { return !!option.get('disabled'); }), - upgradeUrl: this.upgradeUrl, - showShareBanner: !!org + showUpgradeBanner: upgradeUrl && privacyOptions.any(function(o) { return !!o.get('disabled'); }), + upgradeUrl: upgradeUrl, + showShareBanner: this._viewModel.shouldShowShareBanner() }) ); @@ -51,7 +53,9 @@ module.exports = cdb.core.View.extend({ }, _selectOption: function(ev) { - var option = this.privacyOptions.at( $(ev.target).closest('.js-option').attr('data-index') ); + var i = $(ev.target).closest('.js-option').data('index'); + var option = this._viewModel.get('privacyOptions').at(i); + if (!option.get('disabled')) { option.set('selected', true); } @@ -60,19 +64,19 @@ module.exports = cdb.core.View.extend({ _updatePassword: function(ev) { // Reflect state directly in DOM instead of re-rendering to avoid loosing the focus on input var pwd = ev.target.value; - this.privacyOptions.passwordOption().set({ password: pwd }, { silent: true }); + this._viewModel.get('privacyOptions').passwordOption().set({ password: pwd }, { silent: true }); this.$('.js-save')[ _.isEmpty(pwd) ? 'addClass' : 'removeClass' ](DISABLED_SAVE_CLASS_NAME); }, _onClickShare: function(ev) { - if (this.privacyOptions.selectedOption().canSave()) { + if (this._viewModel.canSave()) { this.killEvent(ev); - this.trigger('click:share'); + this._viewModel.changeState('Share'); } }, _onClickSave: function(ev) { this.killEvent(ev); - this.trigger('click:save'); + this._viewModel.save(); } }); diff --git a/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/view_model.js b/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/view_model.js new file mode 100644 index 000000000000..c6de317ee35a --- /dev/null +++ b/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy/view_model.js @@ -0,0 +1,121 @@ +var cdb = require('cartodb.js'); +var _ = require('underscore'); +var PrivacyOptions = require('./options_collection'); + +/** + * View model for a change privacy dialog + */ +module.exports = cdb.core.Model.extend({ + + defaults: { + vis: undefined, + user: undefined, + upgradeUrl: '', + state: 'Start', + hasPermissionsChanged: false, + privacyOptions: undefined + }, + + initialize: function(attrs) { + if (!attrs.vis) { + throw new Error('vis is required'); + } + if (!attrs.user) { + throw new Error('user is required'); + } + this.set('privacyOptions', PrivacyOptions.byVisAndUser(attrs.vis, attrs.user)); + + if (attrs.user.organization) { + this._setupSharePrerequisities(); + } + }, + + changeState: function(newState) { + // Force a change event + this.set('state', undefined, { silent: true }); + this.set('state', newState); + }, + + canShare: function() { + return !!this.get('permission'); + }, + + usersUsingVis: function() { + var metadata = this.get('vis').tableMetadata(); + return _.chain(_.union( + metadata.get('dependent_visualizations'), + metadata.get('non_dependent_visualizations') + )) + .compact() + .map(function(d) { + return d.permission.owner; + }) + .value(); + }, + + shouldShowShareBanner: function() { + return this.get('user').organization; + }, + + shouldRenderDialogWithExpandedLayout: function() { + return this.get('state') === 'Share'; + }, + + canChangeWriteAccess: function() { + return !this.get('vis').isVisualization(); + }, + + canSave: function() { + return this.get('privacyOptions').selectedOption().canSave(); + }, + + save: function() { + var selectedOption = this.get('privacyOptions').selectedOption(); + if (selectedOption.canSave()) { + this.changeState('Saving'); + var self = this; + selectedOption.saveToVis(this.get('vis')) + .done(function() { + self.get('hasPermissionsChanged') ? self._savePermissionChanges() : self._saveDone(); + }) + .fail(this._saveFail.bind(this)); + } + }, + + _setupSharePrerequisities: function() { + var vis = this.get('vis'); + this.set('permission', vis.permission.clone()); + this.get('permission').acl.bind('all', function() { + this.set('hasPermissionsChanged', true); + }, this); + + if (!vis.isVisualization()) { + var self = this; + vis.tableMetadata().fetch({ + silent: true, + success: function() { + if (self.get('state') === 'Share') { + self.changeState('Share'); + } + } + }); + } + }, + + _savePermissionChanges: function() { + var originalPermission = this.get('vis').permission; + originalPermission.overwriteAcl(this.get('permission')); + originalPermission.save() + .done(this._saveDone.bind(this)) + .fail(this._saveFail.bind(this)); + }, + + _saveDone: function() { + this.changeState('SaveDone'); + }, + + _saveFail: function() { + this.changeState('SaveFail'); + } + +}); diff --git a/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy_view.js b/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy_view.js index f411ab2c727e..4b63dc354b43 100644 --- a/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy_view.js +++ b/lib/assets/javascripts/cartodb/new_dashboard/dialogs/change_privacy_view.js @@ -3,10 +3,6 @@ var $ = require('jquery'); var BaseDialog = require('../../new_common/views/base_dialog/view'); var StartView = require('./change_privacy/start_view'); var ShareView = require('./change_privacy/share_view'); -var PrivacyOptions = require('./change_privacy/options_collection'); - -var START_VIEW = 'start_view'; -var SHARE_VIEW = 'share_view'; /** * Change privacy datasets/maps dialog. @@ -15,145 +11,79 @@ module.exports = BaseDialog.extend({ initialize: function() { this.elder('initialize'); - this._vis = this.options.vis; - this._user = this.options.user; - this._upgradeUrl = this.options.upgradeUrl; - this._privacyOptions = PrivacyOptions.byVisAndUser(this._vis, this._user); + if (!this.options.viewModel) { + throw new Error('viewModel is compulsory'); + } + this._viewModel = this.options.viewModel; + this.add_related_model(this._viewModel); + + this._startView = new StartView({ viewModel: this._viewModel }); + this.addView(this._startView); - if (this._user.organization) { - this._permission = this._vis.permission.clone(); + if (this._viewModel.canShare()) { + this._shareView = new ShareView({ viewModel: this._viewModel }); + this.addView(this._shareView); } - this._contentPane = this._newContentPane(); - this._contentPane.active(START_VIEW); - this.addView(this._contentPane); + this._viewModel.bind('change:state', function() { + if (this._viewModel.get('state') === 'SaveDone') { + this.close(); + } else { + this.render(); + var methodName = this._viewModel.shouldRenderDialogWithExpandedLayout() ? 'addClass' : 'removeClass'; + this.$('.content')[ methodName ]('Dialog-content--expanded'); + } + }, this); }, /** * @implements cdb.ui.common.Dialog.prototype.render_content */ render_content: function() { - return [ - this._renderHeaderTemplate(), - this._renderActiveContentPane().el - ]; + return this['_render' + this._viewModel.get('state')](); }, + cancel: function() { this.clean(); }, - _renderHeaderTemplate: function() { - return $( - cdb.templates.getTemplate('new_dashboard/dialogs/change_privacy/header_template')({ - itemName: this._vis.get('name') - }) - )[0]; + _renderStart: function() { + return this._getRenderedElements('_startView'); }, - _renderActiveContentPane: function() { - var view = this._contentPane.getActivePane(); - view.delegateEvents(); // For some reason events gets undelegated upon changing pane, so force enable on activate - return view.render(); + _renderShare: function() { + return this._getRenderedElements('_shareView'); }, - _newContentPane: function() { - var pane = new cdb.ui.common.TabPane(); - pane.addTab(START_VIEW, this._newStartView()); + _getRenderedElements: function(varViewName) { + var view = this[varViewName]; + view.render(); + view.delegateEvents(); // reset events since they seem to get undelegated on changes - if (this._hasOrganization()) { - var shareView = this._newShareView(); - pane.addTab(SHARE_VIEW, shareView); - - pane.getPane(SHARE_VIEW).bind('click:back', this._changeToStartView, this); - pane.getPane(START_VIEW).bind('click:share', this._changeToShareView, this); - } - - pane.each(function(name, view) { - view.bind('click:save', this._ok, this); - }.bind(this)); - - return pane; + return [ + this._headerContentEl(), + view.el + ]; }, - _newStartView: function() { - return new StartView({ - vis: this._vis, - user: this._user, - upgradeUrl: this._upgradeUrl, - privacyOptions: this._privacyOptions - }); + _headerContentEl: function() { + return $( + cdb.templates.getTemplate('new_dashboard/dialogs/change_privacy/header_template')({ + itemName: this._viewModel.get('vis').get('name'), + itemType: this._viewModel.get('vis').isVisualization() ? 'map' : 'dataset' + }) + )[0]; }, - _newShareView: function() { - var tableMetadata = this._vis.tableMetadata(); - var visIsTable = !this._vis.isVisualization(); - - var shareView = new ShareView({ - organization: this._user.organization, - permission: this._permission, - canChangeWriteAccess: visIsTable, - tableMetadata: tableMetadata + _renderSaving: function() { + return cdb.templates.getTemplate('new_dashboard/templates/loading')({ + title: 'Saving privacy...' }); - - if (visIsTable) { - tableMetadata.fetch({ - silent: true, - success: function() { - shareView.render(); - } - }); - } - - return shareView; - }, - - _changeToStartView: function() { - this._contentPane.active(START_VIEW); - this.render(); - this.$('.content').removeClass('Dialog-content--expanded'); - }, - - _changeToShareView: function() { - this._contentPane.active(SHARE_VIEW); - this.render(); - this.$('.content').addClass('Dialog-content--expanded'); - }, - - _ok: function() { - var selectedOption = this._privacyOptions.selectedOption(); - if (selectedOption.canSave()) { - this._contentPane.each(function(name, view) { - view.undelegateEvents(); - }); - - var self = this; - selectedOption.saveToVis(this._vis) - .done(function() { - if (self._hasOrganization()) { - self._savePermissionChanges() - } else { - self.close() - } - }) - .fail(this._delegateAllEvents.bind(this)); - } - }, - - _savePermissionChanges: function() { - var originalPermission = this._vis.permission; - originalPermission.overwriteAcl(this._permission); - originalPermission.save() - .done(this.close.bind(this)) - .fail(this._delegateAllEvents.bind(this)); - }, - - _hasOrganization: function() { - return !!this._permission; }, - _delegateAllEvents: function() { - this._contentPane.each(function(name, view) { - view.delegateEvents(); + _renderSaveFail: function() { + return cdb.templates.getTemplate('new_dashboard/templates/fail')({ + msg: '' }); } }); diff --git a/lib/assets/javascripts/cartodb/new_dashboard/entry.js b/lib/assets/javascripts/cartodb/new_dashboard/entry.js index 2127c2d569b0..5e042e899d91 100644 --- a/lib/assets/javascripts/cartodb/new_dashboard/entry.js +++ b/lib/assets/javascripts/cartodb/new_dashboard/entry.js @@ -5,6 +5,7 @@ var MainView = require('./main_view'); var sendUsageToMixpanel = require('./send_usage_to_mixpanel'); var urls = require('../new_common/urls_fn'); var ChangePrivacyDialog = require('./dialogs/change_privacy_view'); +var ChangePrivacyViewModel = require('./dialogs/change_privacy/view_model'); if (window.trackJs) { window.trackJs.configure({ @@ -52,14 +53,19 @@ $(function() { cdb.god.bind('openPrivacyDialog', function(vis) { if (vis.isOwnedByUser(currentUser)) { - var privacyDlg = new ChangePrivacyDialog({ + var viewModel = new ChangePrivacyViewModel({ vis: vis, user: currentUser, - upgradeUrl: currentUserUrl.toUpgradeAccount(), + upgradeUrl: currentUserUrl.toUpgradeAccount() + }); + window.vm = viewModel; + + var dialog = new ChangePrivacyDialog({ + viewModel: viewModel, enter_to_confirm: true, clean_on_hide: true }); - privacyDlg.appendToBody(); + dialog.appendToBody(); } }); }); diff --git a/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/share_view.spec.js b/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/share_view.spec.js index 241e6607dc98..d5900d772a70 100644 --- a/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/share_view.spec.js +++ b/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/share_view.spec.js @@ -1,4 +1,5 @@ var ShareView = require('../../../../../../javascripts/cartodb/new_dashboard/dialogs/change_privacy/share_view'); +var ViewModel = require('../../../../../../javascripts/cartodb/new_dashboard/dialogs/change_privacy/view_model'); var cdbAdmin = require('cdb.admin'); /** @@ -7,21 +8,40 @@ var cdbAdmin = require('cdb.admin'); */ describe('new_dashboard/dialogs/change_privacy/share_view', function() { beforeEach(function() { - this.permission = new cdbAdmin.Permission(); - this.org = new cdbAdmin.Organization({ - users: [{ - id: 'abc-123', - username: 'pepe' - }] + this.user = new cdbAdmin.User({ + username: 'pepe', + actions: {}, + organization: { + users: [{ + id: 'abc-123', + username: 'paco' + }] + } }); - this.tableMetadata = new cdbAdmin.CartoDBTableMetadata(); - this.view = new ShareView({ - organization: this.org, - permission: this.permission, - tableMetadata: this.tableMetadata + this.vis = new cdbAdmin.Visualization({ + type: 'derived', + privacy: 'PUBLIC' }); - this.view.render(); + + this.createView = function() { + this.viewModel = new ViewModel({ + vis: this.vis, + user: this.user, + upgradeUrl: this.upgradeUrl + }); + spyOn(this.viewModel, 'changeState'); + spyOn(this.viewModel, 'save'); + + this.view = new ShareView({ + viewModel: this.viewModel + }); + spyOn(this.view, 'killEvent'); + + this.view.render(); + }; + + this.createView(); }); it('should have no leaks', function() { @@ -30,9 +50,6 @@ describe('new_dashboard/dialogs/change_privacy/share_view', function() { describe('on click .js-back', function() { beforeEach(function() { - spyOn(this.view, 'killEvent'); - spyOn(this.view, 'trigger'); - this.view.$('.js-back').click(); }); @@ -40,21 +57,17 @@ describe('new_dashboard/dialogs/change_privacy/share_view', function() { expect(this.view.killEvent).toHaveBeenCalled(); }); - it('should fire a click:back event', function() { - expect(this.view.trigger).toHaveBeenCalledWith('click:back'); + it('should change state to start', function() { + expect(this.viewModel.changeState).toHaveBeenCalledWith('Start'); }); }); describe('given there is at least one dependant visualization', function() { beforeEach(function() { - this.visData = { - permission: { - owner: { - id: 'abc-123' - } - } + this.dependantUser = { + id: 'abc-123' }; - this.tableMetadata.set('dependent_visualizations', [ this.visData ]); + spyOn(this.viewModel, 'usersUsingVis').and.returnValue([ this.dependantUser ]); }); it('should render OK', function() { @@ -62,8 +75,21 @@ describe('new_dashboard/dialogs/change_privacy/share_view', function() { }); it('should rendered users', function() { - this.view.render(); - expect(this.innerHTML()).toContain('pepe'); + expect(this.innerHTML()).toContain('paco'); + }); + }); + + describe('on click .js-save', function() { + beforeEach(function() { + this.view.$('.js-save').click(); + }); + + it('should kill event', function() { + expect(this.view.killEvent).toHaveBeenCalled(); + }); + + it('should stop listening on events while processing', function() { + expect(this.viewModel.save).toHaveBeenCalled(); }); }); }); diff --git a/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/start_view.spec.js b/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/start_view.spec.js index e099e940c232..7d5ef8a5c61e 100644 --- a/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/start_view.spec.js +++ b/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/start_view.spec.js @@ -1,6 +1,7 @@ var StartView = require('../../../../../../javascripts/cartodb/new_dashboard/dialogs/change_privacy/start_view'); -var PrivacyOptions = require('../../../../../../javascripts/cartodb/new_dashboard/dialogs/change_privacy/options_collection'); +var ViewModel = require('../../../../../../javascripts/cartodb/new_dashboard/dialogs/change_privacy/view_model'); var cdbAdmin = require('cdb.admin'); +var $ = require('jquery'); /** * Most high-fidelity details are covered in underlying collection/model, so no need to re-test that here. @@ -21,14 +22,26 @@ describe('new_dashboard/dialogs/change_privacy/start_view', function() { privacy: 'PUBLIC' }); - this.privacyOptions = PrivacyOptions.byVisAndUser(this.vis, this.user); + this.upgradeUrl = ''; - this.view = new StartView({ - vis: this.vis, - user: this.user, - privacyOptions: this.privacyOptions - }); - this.view.render(); + this.createView = function() { + this.viewModel = new ViewModel({ + vis: this.vis, + user: this.user, + upgradeUrl: this.upgradeUrl + }); + spyOn(this.viewModel, 'changeState'); + spyOn(this.viewModel, 'save'); + + this.view = new StartView({ + viewModel: this.viewModel + }); + spyOn(this.view, 'killEvent'); + + this.view.render(); + }; + + this.createView(); }); it('should have no leaks', function() { @@ -58,18 +71,13 @@ describe('new_dashboard/dialogs/change_privacy/start_view', function() { describe('given user do not have all privacy options available', function() { beforeEach(function() { this.user.get('actions').private_maps = false; - this.privacyOptions = PrivacyOptions.byVisAndUser(this.vis, this.user); + this.createView(); }); - describe('and has a upgradeUrl', function() { + describe('when has a upgradeUrl', function() { beforeEach(function() { - this.view = new StartView({ - vis: this.vis, - user: this.user, - upgradeUrl: '/account/upgrade', - privacyOptions: this.privacyOptions - }); - this.view.render(); + this.upgradeUrl = '/account/upgrade'; + this.createView(); }); it('should render the upgrade banner', function() { @@ -78,16 +86,6 @@ describe('new_dashboard/dialogs/change_privacy/start_view', function() { }); describe('and has no upgradeUrl', function() { - beforeEach(function() { - this.view = new StartView({ - vis: this.vis, - user: this.user, - upgradeUrl: '', - privacyOptions: this.privacyOptions - }); - this.view.render(); - }); - it('should not render the upgrade banner', function() { expect(this.innerHTML()).not.toContain('upgrade'); }); @@ -104,15 +102,8 @@ describe('new_dashboard/dialogs/change_privacy/start_view', function() { users: [] } }); - this.privacyOptions = PrivacyOptions.byVisAndUser(this.vis, this.user); - this.view = new StartView({ - vis: this.vis, - user: this.user, - upgradeUrl: '', - privacyOptions: this.privacyOptions - }); - this.view.render(); + this.createView(); }); it('should render the share permissions banner', function() { @@ -121,9 +112,6 @@ describe('new_dashboard/dialogs/change_privacy/start_view', function() { describe('on click .js-share', function() { beforeEach(function() { - spyOn(this.view, 'killEvent'); - spyOn(this.view, 'trigger'); - this.view.$('.js-share').click(); }); @@ -131,8 +119,8 @@ describe('new_dashboard/dialogs/change_privacy/start_view', function() { expect(this.view.killEvent).toHaveBeenCalled(); }); - it('should fire a click:share event', function() { - expect(this.view.trigger).toHaveBeenCalledWith('click:share'); + it('should change state to share view', function() { + expect(this.viewModel.changeState).toHaveBeenCalledWith('Share'); }); }); }); @@ -141,18 +129,19 @@ describe('new_dashboard/dialogs/change_privacy/start_view', function() { beforeEach(function() { this.select = function(index) { $(this.view.$('.js-option')[index]).click(); - } + }; }); it('should have selected item', function() { - expect(this.privacyOptions.at(1).get('selected')).toBeFalsy(); + var privacyOptions = this.viewModel.get('privacyOptions'); + expect(privacyOptions.at(1).get('selected')).toBeFalsy(); this.select(1); - expect(this.privacyOptions.at(1).get('selected')).toBeTruthy(); + expect(privacyOptions.at(1).get('selected')).toBeTruthy(); this.select(0); - expect(this.privacyOptions.at(0).get('selected')).toBeTruthy(); - expect(this.privacyOptions.at(1).get('selected')).toBeFalsy(); + expect(privacyOptions.at(0).get('selected')).toBeTruthy(); + expect(privacyOptions.at(1).get('selected')).toBeFalsy(); }); it("should set the .is-selected class on the selected item's DOM", function() { @@ -169,7 +158,7 @@ describe('new_dashboard/dialogs/change_privacy/start_view', function() { describe('on select password option', function() { beforeEach(function() { - this.passwordOption = this.privacyOptions.where({ privacy: 'PASSWORD' })[0]; + this.passwordOption = this.viewModel.get('privacyOptions').where({ privacy: 'PASSWORD' })[0]; this.passwordOption.set('selected', true); }); @@ -200,11 +189,6 @@ describe('new_dashboard/dialogs/change_privacy/start_view', function() { describe('on click .js-save', function() { beforeEach(function() { - spyOn(this.view, 'killEvent'); - this.view.bind('click:save', function() { - this.clickedSave = true; - }, this); - this.view.$('.js-save').click(); }); @@ -213,7 +197,7 @@ describe('new_dashboard/dialogs/change_privacy/start_view', function() { }); it('should stop listening on events while processing', function() { - expect(this.clickedSave).toBeTruthy(); + expect(this.viewModel.save).toHaveBeenCalled(); }); }); }); diff --git a/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/view_model.spec.js b/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/view_model.spec.js new file mode 100644 index 000000000000..1c083398c31e --- /dev/null +++ b/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy/view_model.spec.js @@ -0,0 +1,246 @@ +var ChangePrivacyViewModel = require('../../../../../../javascripts/cartodb/new_dashboard/dialogs/change_privacy/view_model'); +var cdb = require('cartodb.js'); +var $ = require('jquery'); + +describe('new_dashboard/dialogs/change_privacy_view_model', function() { + beforeEach(function() { + this.vis = new cdb.admin.Visualization({ + type: 'derived', + privacy: 'PUBLIC' + }); + + // Org user + this.user = new cdb.admin.User({ + username: 'pepe', + actions: { + }, + organization: { + users: [ + { + username: 'paco' + }, + { + username: 'pito' + } + ] + } + }); + + this.createNormalUser = function() { + this.user = new cdb.admin.User({ + username: 'pepe', + actions: { + } + }); + }; + + this.createViewModel = function() { + this.viewModel = new ChangePrivacyViewModel({ + vis: this.vis, + user: this.user + }); + }; + this.createViewModel(); + }); + + it('should have a privacy options', function() { + expect(this.viewModel.get('privacyOptions')).toEqual(jasmine.any(Object)); + }); + + describe('.changeState', function() { + it('should have Start as start state', function() { + expect(this.viewModel.get('state')).toEqual('Start'); + }); + + describe('when called multiple times', function() { + beforeEach(function() { + this.viewModel.bind('change:state', function() { + this.changedState = true; + }, this); + + this.viewModel.changeState('Start'); + }); + + it('should trigger a change:state event', function() { + expect(this.changedState).toBeTruthy(); + }); + }); + }); + + describe('.canShare', function() { + describe('when the user is a normal user', function() { + beforeEach(function() { + this.createNormalUser(); + this.createViewModel(); + }); + + it('should return false', function() { + expect(this.viewModel.canShare()).toBeFalsy(); + }); + }); + + describe('when the user is part of organization', function() { + it('should return true', function() { + expect(this.viewModel.canShare()).toBeTruthy(); + }); + }); + }); + + describe('.usersUsingVis', function() { + beforeEach(function() { + this.createNormalUser(); + this.createViewModel(); + }); + + describe('when no other created a vis on top of it', function() { + it('should return an empty list', function() { + expect(this.viewModel.usersUsingVis()).toEqual([]); + }); + }); + + describe('when there are at least one user that used datasets', function() { + beforeEach(function() { + this.owner = {}; + var stub = jasmine.createSpy('cdb.admin.Visualization'); + stub.permission = stub; + stub.permission.owner = this.owner; + + this.metadata = jasmine.createSpyObj('table metadata', ['get']); + spyOn(this.vis, 'tableMetadata').and.returnValue(this.metadata); + this.metadata.get.and.returnValue([ stub ]); + }); + + it('should return them as a list', function() { + expect(this.viewModel.usersUsingVis()).toEqual([ this.owner ]); + }); + }); + }); + + describe('.shouldShowShareBanner', function() { + describe('when user is a normal user', function() { + beforeEach(function() { + this.createNormalUser(); + this.createViewModel(); + }); + + it('should return false', function() { + expect(this.viewModel.shouldShowShareBanner()).toBeFalsy(); + }); + }); + + describe('when user is part of organization', function() { + it('should return true', function() { + expect(this.viewModel.shouldShowShareBanner()).toBeTruthy(); + }); + }); + }); + + describe('.shouldRenderDialogWithExpandedLayout', function() { + it('should return true if state is share', function() { + expect(this.viewModel.shouldRenderDialogWithExpandedLayout()).toBeFalsy(); + + this.viewModel.set('state', 'Share'); + expect(this.viewModel.shouldRenderDialogWithExpandedLayout()).toBeTruthy(); + }); + }); + + describe('.canChangeWriteAccess', function() { + it('should return true if vis is a table', function() { + expect(this.viewModel.canChangeWriteAccess()).toBeFalsy(); + + spyOn(this.vis, 'isVisualization').and.returnValue(false); + expect(this.viewModel.canChangeWriteAccess()).toBeTruthy(); + }); + }); + + describe('.canSave', function() { + it('should return true if selected option allows it', function() { + expect(this.viewModel.canSave()).toBeTruthy(); + + spyOn(this.viewModel.get('privacyOptions').selectedOption(), 'canSave').and.returnValue(false); + expect(this.viewModel.canSave()).toBeFalsy(); + }); + }); + + describe('.save', function() { + describe('when can save', function() { + beforeEach(function() { + this.deferred = $.Deferred(); + this.selected = this.viewModel.get('privacyOptions').selectedOption(); + spyOn(this.selected, 'saveToVis').and.returnValue(this.deferred.promise()); + this.viewModel.save(); + }); + + it('should change state to Saving', function() { + expect(this.viewModel.get('state')).toEqual('Saving'); + }); + + it('should call .saveToVis on selected option', function() { + expect(this.selected.saveToVis).toHaveBeenCalled(); + expect(this.selected.saveToVis).toHaveBeenCalledWith(this.viewModel.get('vis')); + }); + + describe('when save fails', function() { + beforeEach(function() { + this.deferred.reject(); + }); + + it('should change to save fail state', function() { + expect(this.viewModel.get('state')).toEqual('SaveFail'); + }); + }); + + describe('when save resolves', function() { + describe('when permissions also have changed', function() { + beforeEach(function() { + this.viewModel.get('permission').acl.reset([{}]); + + this.orgPermission = this.viewModel.get('vis').permission; + spyOn(this.orgPermission, 'overwriteAcl'); + + this.secDeferred = $.Deferred(); + spyOn(this.orgPermission, 'save').and.returnValue(this.secDeferred); + + this.deferred.resolve(); + }); + + it('should save permission', function() { + expect(this.orgPermission.overwriteAcl).toHaveBeenCalled(); + expect(this.orgPermission.overwriteAcl).toHaveBeenCalledWith(this.viewModel.get('permission')); + expect(this.orgPermission.save).toHaveBeenCalled(); + }); + + describe('when 2nd save resolves', function() { + beforeEach(function() { + this.secDeferred.resolve(); + }); + + it('should change state to SaveDone', function() { + expect(this.viewModel.get('state')).toEqual('SaveDone'); + }); + }); + + describe('when 2nd save fails', function() { + beforeEach(function() { + this.secDeferred.reject(); + }); + + it('should change state to SaveDone', function() { + expect(this.viewModel.get('state')).toEqual('SaveFail'); + }); + }); + }); + + describe('when permissions have not been changed', function() { + beforeEach(function() { + this.deferred.resolve(); + }); + + it('should change to save done state', function() { + expect(this.viewModel.get('state')).toEqual('SaveDone'); + }); + }); + }); + }); + }); +}); diff --git a/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy_view.spec.js b/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy_view.spec.js index cb9fcfdcdbdf..3a6ff6b8ac83 100644 --- a/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy_view.spec.js +++ b/lib/assets/test/spec/cartodb/new_dashboard/dialogs/change_privacy_view.spec.js @@ -1,5 +1,7 @@ var ChangePrivacyDialog = require('../../../../../javascripts/cartodb/new_dashboard/dialogs/change_privacy_view'); var cdbAdmin = require('cdb.admin'); +var ViewModel = require('../../../../../javascripts/cartodb/new_dashboard/dialogs/change_privacy/view_model'); +var $ = require('jquery'); /** * Most high-fidelity details are covered in underlying tabpane views, see separate tests. @@ -11,88 +13,74 @@ describe('new_dashboard/dialogs/change_privacy_view', function() { type: 'derived', privacy: 'PUBLIC' }); - + this.user = new cdbAdmin.User({ username: 'pepe', - actions: { + actions: {}, + organization: { + users: [ + { + username: 'paco' + }, + { + username: 'pito' + } + ] } }); - this.upgradeUrl ='/account/upgrade'; + this.viewModel = new ViewModel({ + vis: this.vis, + user: this.user + }); + + spyOn(this.viewModel, 'shouldRenderDialogWithExpandedLayout'); this.setupView = function() { - if (this.view) this.view.clean(); // in case this is called more than once in a test case this.view = new ChangePrivacyDialog({ - vis: this.vis, - user: this.user, - upgradeUrl: this.upgradeUrl + viewModel: this.viewModel }); - this.view.render(); + this.view.render(); $(document.body).append(this.view.$el); - } + }; }); - - describe('given a normal user', function() { + describe('when can not share vis', function() { beforeEach(function() { - this.user = new cdbAdmin.User({ - username: 'pepe', - actions: {} - }); - + spyOn(this.viewModel, 'canShare').and.returnValue(false); this.setupView(); }); + it('should not have any share view', function() { + expect(this.view._shareView).toBeUndefined(); + }); + it('should have no leaks', function() { expect(this.view).toHaveNoLeaks(); - }); + }); }); - describe('given a organization user', function() { + describe('when can share vis', function() { beforeEach(function() { - this.user = new cdbAdmin.User({ - username: 'pepe', - actions: {}, - organization: { - users: [ - { - username: 'paco' - }, - { - username: 'pito' - } - ] - } - }); - + spyOn(this.viewModel, 'canShare').and.returnValue(true); this.setupView(); }); - - it('should rendered the start view by default', function() { - expect(this.view._contentPane.getPane('start_view').$el.is(':visible')).toBeTruthy(); - expect(this.view._contentPane.getPane('share_view').$el.is(':hidden')).toBeTruthy(); + + it('should have a share view', function() { + expect(this.view._shareView).not.toBeUndefined(); + }); + + it('should rendered the start view to start with', function() { + expect(this.innerHTML()).toContain('privacy'); }); - - describe('given a click:share event is triggered on start view', function() { + + describe('when change state to something else', function() { beforeEach(function() { - this.setupView(); - this.view._contentPane.getPane('start_view').trigger('click:share'); - }); - - it('should render the share tab', function() { - expect(this.view._contentPane.getPane('start_view').$el.is(':hidden')).toBeTruthy(); - expect(this.view._contentPane.getPane('share_view').$el.is(':visible')).toBeTruthy(); + this.viewModel.changeState('Share'); }); - - describe('and then a click:back event is fired on share tab', function() { - beforeEach(function() { - this.view._contentPane.getPane('share_view').trigger('click:back'); - }); - - it('should render the share tab', function() { - expect(this.view._contentPane.getPane('start_view').$el.is(':visible')).toBeTruthy(); - expect(this.view._contentPane.getPane('share_view').$el.is(':hidden')).toBeTruthy(); - }); + + it('should have rendered share view', function() { + expect(this.innerHTML()).toContain('Share with your'); }); }); @@ -101,44 +89,43 @@ describe('new_dashboard/dialogs/change_privacy_view', function() { }); }); - describe('on pane click:save event', function() { + describe('when change state changes to SaveDone', function() { beforeEach(function() { this.setupView(); spyOn(this.view, 'close'); + this.viewModel.changeState('SaveDone'); + }); - this.selected = this.view._privacyOptions.find(function(option) { return option.get('selected'); }); - spyOn(this.selected, 'saveToVis'); - this.deferred = $.Deferred(); - this.selected.saveToVis.and.returnValue(this.deferred.promise()); - - this.view._contentPane.getActivePane().trigger('click:save'); + it('should close the view', function() { + expect(this.view.close).toHaveBeenCalled(); }); + }); - it('should save selected privacy to visualization', function() { - expect(this.selected.saveToVis).toHaveBeenCalled(); - expect(this.selected.saveToVis).toHaveBeenCalledWith(this.vis); + describe('when change state changes to Saving', function() { + beforeEach(function() { + this.setupView(); + this.viewModel.changeState('Saving'); }); - describe('given save finishes successfully', function() { - beforeEach(function() { - this.deferred.resolve(); - }); + it('should render saving view', function() { + expect(this.innerHTML()).toContain('Saving privacy...'); + }); + }); - it('should close the dialog', function() { - expect(this.view.close).toHaveBeenCalled(); - }); + describe('when change state changes to SaveFail ', function() { + beforeEach(function() { + this.setupView(); + this.viewModel.changeState('SaveFail'); }); - describe('given save fails', function() { - beforeEach(function() { - this.deferred.reject('fail'); - }); + it('should render error view', function() { + expect(this.innerHTML()).toContain('error'); }); }); - afterEach(function() { - this.view.clean(); + if (this.view) { + this.view.clean(); + } }); }); - diff --git a/package.json b/package.json index effdf7e84825..ba2d1d0832c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cartodb-ui", - "version": "3.7.5", + "version": "3.7.6", "description": "CartoDB UI frontend", "repository": { "type": "git",