diff options
Diffstat (limited to 'core/js')
-rw-r--r-- | core/js/contactsmenu.js | 523 | ||||
-rw-r--r-- | core/js/core.json | 1 | ||||
-rw-r--r-- | core/js/js.js | 78 | ||||
-rw-r--r-- | core/js/login/authpicker.js | 13 | ||||
-rw-r--r-- | core/js/login/redirect.js | 3 | ||||
-rw-r--r-- | core/js/sharedialoglinkshareview.js | 8 | ||||
-rw-r--r-- | core/js/sharedialogview.js | 81 | ||||
-rw-r--r-- | core/js/shareitemmodel.js | 62 | ||||
-rw-r--r-- | core/js/tests/specs/contactsmenuSpec.js | 265 | ||||
-rw-r--r-- | core/js/tests/specs/jquery.avatarSpec.js | 2 | ||||
-rw-r--r-- | core/js/tests/specs/sharedialoglinkshareview.js | 143 | ||||
-rw-r--r-- | core/js/tests/specs/sharedialogviewSpec.js | 4 | ||||
-rw-r--r-- | core/js/tests/specs/shareitemmodelSpec.js | 153 |
13 files changed, 1252 insertions, 84 deletions
diff --git a/core/js/contactsmenu.js b/core/js/contactsmenu.js new file mode 100644 index 00000000000..15c48887d20 --- /dev/null +++ b/core/js/contactsmenu.js @@ -0,0 +1,523 @@ +/* global OC.Backbone, Handlebars, Promise, _ */ + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +(function(OC, $, _, Handlebars) { + 'use strict'; + + var MENU_TEMPLATE = '' + + '<input id="contactsmenu-search" type="search" placeholder="Search contacts …" value="{{searchTerm}}">' + + '<div class="content">' + + '</div>'; + var CONTACTS_LIST_TEMPLATE = '' + + '{{#unless contacts.length}}' + + '<div class="emptycontent">' + + ' <div class="icon-search"></div>' + + ' <h2>' + t('core', 'No contacts found') + '</h2>' + + '</div>' + + '{{/unless}}' + + '<div id="contactsmenu-contacts"></div>' + + '{{#if contactsAppEnabled}}<div class="footer"><a href="{{contactsAppURL}}">' + t('core', 'Show all contacts …') + '</a></div>{{/if}}'; + var LOADING_TEMPLATE = '' + + '<div class="emptycontent">' + + ' <div class="icon-loading"></div>' + + ' <h2>{{loadingText}}</h2>' + + '</div>'; + var ERROR_TEMPLATE = '' + + '<div class="emptycontent">' + + ' <div class="icon-search"></div>' + + ' <h2>' + t('core', 'There was an error loading your contacts') + '</h2>' + + '</div>'; + var CONTACT_TEMPLATE = '' + + '{{#if contact.avatar}}' + + '<img src="{{contact.avatar}}" class="avatar">' + + '{{else}}' + + '<div class="avatar"></div>' + + '{{/if}}' + + '<div class="body">' + + ' <div class="full-name">{{contact.fullName}}</div>' + + ' <div class="last-message">{{contact.lastMessage}}</div>' + + '</div>' + + '{{#if contact.topAction}}' + + '<a class="top-action" href="{{contact.topAction.hyperlink}}" title="{{contact.topAction.title}}">' + + ' <img src="{{contact.topAction.icon}}">' + + '</a>' + + '{{/if}}' + + '{{#if contact.hasTwoActions}}' + + '<a class="second-action" href="{{contact.secondAction.hyperlink}}">' + + ' <img src="{{contact.secondAction.icon}}">' + + '</a>' + + '{{/if}}' + + '{{#if contact.hasManyActions}}' + + ' <span class="other-actions icon-more"></span>' + + ' <div class="menu popovermenu">' + + ' <ul>' + + ' {{#each contact.actions}}' + + ' <li>' + + ' <a href="{{hyperlink}}">' + + ' <img src="{{icon}}">' + + ' <span>{{title}}</span>' + + ' </a>' + + ' </li>' + + ' {{/each}}' + + ' </ul>' + + ' </div>' + + '{{/if}}'; + + /** + * @class Contact + */ + var Contact = OC.Backbone.Model.extend({ + defaults: { + fullName: '', + lastMessage: '', + actions: [], + hasOneAction: false, + hasTwoActions: false, + hasManyActions: false + }, + + /** + * @returns {undefined} + */ + initialize: function() { + // Add needed property for easier template rendering + if (this.get('actions').length === 0) { + this.set('hasOneAction', true); + } else if (this.get('actions').length === 1) { + this.set('hasTwoActions', true); + this.set('secondAction', this.get('actions')[0]); + } else { + this.set('hasManyActions', true); + } + } + }); + + /** + * @class ContactCollection + */ + var ContactCollection = OC.Backbone.Collection.extend({ + model: Contact + }); + + /** + * @class ContactsListView + */ + var ContactsListView = OC.Backbone.View.extend({ + + /** @type {ContactsCollection} */ + _collection: undefined, + + /** @type {array} */ + _subViews: [], + + /** + * @param {object} options + * @returns {undefined} + */ + initialize: function(options) { + this._collection = options.collection; + }, + + /** + * @returns {self} + */ + render: function() { + var self = this; + self.$el.html(''); + self._subViews = []; + + self._collection.forEach(function(contact) { + var item = new ContactsListItemView({ + model: contact + }); + item.render(); + self.$el.append(item.$el); + item.on('toggle:actionmenu', self._onChildActionMenuToggle, self); + self._subViews.push(item); + }); + + return self; + }, + + /** + * Event callback to propagate opening (another) entry's action menu + * + * @param {type} $src + * @returns {undefined} + */ + _onChildActionMenuToggle: function($src) { + this._subViews.forEach(function(view) { + view.trigger('parent:toggle:actionmenu', $src); + }); + } + }); + + /** + * @class CotnactsListItemView + */ + var ContactsListItemView = OC.Backbone.View.extend({ + + /** @type {string} */ + className: 'contact', + + /** @type {undefined|function} */ + _template: undefined, + + /** @type {Contact} */ + _model: undefined, + + /** @type {boolean} */ + _actionMenuShown: false, + + events: { + 'click .icon-more': '_onToggleActionsMenu' + }, + + /** + * @param {object} data + * @returns {undefined} + */ + template: function(data) { + if (!this._template) { + this._template = Handlebars.compile(CONTACT_TEMPLATE); + } + return this._template(data); + }, + + /** + * @param {object} options + * @returns {undefined} + */ + initialize: function(options) { + this._model = options.model; + this.on('parent:toggle:actionmenu', this._onOtherActionMenuOpened, this); + }, + + /** + * @returns {self} + */ + render: function() { + this.$el.html(this.template({ + contact: this._model.toJSON() + })); + this.delegateEvents(); + + // Show placeholder iff no avatar is available (avatar is rendered as img, not div) + this.$('div.avatar').imageplaceholder(this._model.get('fullName')); + + // Show tooltip for top action + this.$('.top-action').tooltip({placement: 'left'}); + + return this; + }, + + /** + * Toggle the visibility of the action popover menu + * + * @private + * @returns {undefined} + */ + _onToggleActionsMenu: function() { + this._actionMenuShown = !this._actionMenuShown; + if (this._actionMenuShown) { + this.$('.menu').show(); + } else { + this.$('.menu').hide(); + } + this.trigger('toggle:actionmenu', this.$el); + }, + + /** + * @private + * @argument {jQuery} $src + * @returns {undefined} + */ + _onOtherActionMenuOpened: function($src) { + if (this.$el.is($src)) { + // Ignore + return; + } + this._actionMenuShown = false; + this.$('.menu').hide(); + } + }); + + /** + * @class ContactsMenuView + */ + var ContactsMenuView = OC.Backbone.View.extend({ + + /** @type {undefined|function} */ + _loadingTemplate: undefined, + + /** @type {undefined|function} */ + _errorTemplate: undefined, + + /** @type {undefined|function} */ + _contentTemplate: undefined, + + /** @type {undefined|function} */ + _contactsTemplate: undefined, + + /** @type {undefined|ContactCollection} */ + _contacts: undefined, + + events: { + 'input #contactsmenu-search': '_onSearch' + }, + + /** + * @returns {undefined} + */ + _onSearch: _.debounce(function() { + this.trigger('search', this.$('#contactsmenu-search').val()); + }, 700), + + /** + * @param {object} data + * @returns {string} + */ + loadingTemplate: function(data) { + if (!this._loadingTemplate) { + this._loadingTemplate = Handlebars.compile(LOADING_TEMPLATE); + } + return this._loadingTemplate(data); + }, + + /** + * @param {object} data + * @returns {string} + */ + errorTemplate: function(data) { + if (!this._errorTemplate) { + this._errorTemplate = Handlebars.compile(ERROR_TEMPLATE); + } + return this._errorTemplate(data); + }, + + /** + * @param {object} data + * @returns {string} + */ + contentTemplate: function(data) { + if (!this._contentTemplate) { + this._contentTemplate = Handlebars.compile(MENU_TEMPLATE); + } + return this._contentTemplate(data); + }, + + /** + * @param {object} data + * @returns {string} + */ + contactsTemplate: function(data) { + if (!this._contactsTemplate) { + this._contactsTemplate = Handlebars.compile(CONTACTS_LIST_TEMPLATE); + } + return this._contactsTemplate(data); + }, + + /** + * @param {object} options + * @returns {undefined} + */ + initialize: function(options) { + this.options = options; + }, + + /** + * @param {string} text + * @returns {undefined} + */ + showLoading: function(text) { + this.render(); + this._contacts = undefined; + this.$('.content').html(this.loadingTemplate({ + loadingText: text + })); + }, + + /** + * @returns {undefined} + */ + showError: function() { + this.render(); + this._contacts = undefined; + this.$('.content').html(this.errorTemplate()); + }, + + /** + * @param {object} viewData + * @param {string} searchTerm + * @returns {undefined} + */ + showContacts: function(viewData, searchTerm) { + this._contacts = viewData.contacts; + this.render({ + contacts: viewData.contacts + }); + + var list = new ContactsListView({ + collection: viewData.contacts + }); + list.render(); + this.$('.content').html(this.contactsTemplate({ + contacts: viewData.contacts, + searchTerm: searchTerm, + contactsAppEnabled: viewData.contactsAppEnabled, + contactsAppURL: OC.generateUrl('/apps/contacts') + })); + this.$('#contactsmenu-contacts').html(list.$el); + }, + + /** + * @param {object} data + * @returns {self} + */ + render: function(data) { + var searchVal = this.$('#contactsmenu-search').val(); + this.$el.html(this.contentTemplate(data)); + + // Focus search + this.$('#contactsmenu-search').val(searchVal); + this.$('#contactsmenu-search').focus(); + return this; + } + + }); + + /** + * @param {Object} options + * @param {jQuery} options.el + * @param {jQuery} options.trigger + * @class ContactsMenu + */ + var ContactsMenu = function(options) { + this.initialize(options); + }; + + ContactsMenu.prototype = { + /** @type {jQuery} */ + $el: undefined, + + /** @type {jQuery} */ + _$trigger: undefined, + + /** @type {ContactsMenuView} */ + _view: undefined, + + /** @type {Promise} */ + _contactsPromise: undefined, + + /** + * @param {Object} options + * @param {jQuery} options.el - the element to render the menu in + * @param {jQuery} options.trigger - the element to click on to open the menu + * @returns {undefined} + */ + initialize: function(options) { + this.$el = options.el; + this._$trigger = options.trigger; + + this._view = new ContactsMenuView({ + el: this.$el + }); + this._view.on('search', function(searchTerm) { + this._loadContacts(searchTerm); + }, this); + + OC.registerMenu(this._$trigger, this.$el, function() { + this._toggleVisibility(true); + }.bind(this)); + this.$el.on('beforeHide', function() { + this._toggleVisibility(false); + }.bind(this)); + }, + + /** + * @private + * @param {boolean} show + * @returns {Promise} + */ + _toggleVisibility: function(show) { + if (show) { + return this._loadContacts(); + } else { + this.$el.html(''); + return Promise.resolve(); + } + }, + + /** + * @private + * @param {string|undefined} searchTerm + * @returns {Promise} + */ + _getContacts: function(searchTerm) { + var url = OC.generateUrl('/contactsmenu/contacts'); + return Promise.resolve($.ajax(url, { + method: 'POST', + data: { + filter: searchTerm + } + })); + }, + + /** + * @param {string|undefined} searchTerm + * @returns {undefined} + */ + _loadContacts: function(searchTerm) { + var self = this; + + if (!self._contactsPromise) { + self._contactsPromise = self._getContacts(searchTerm); + } + + if (_.isUndefined(searchTerm) || searchTerm === '') { + self._view.showLoading(t('core', 'Loading your contacts …')); + } else { + self._view.showLoading(t('core', 'Looking for {term} …', { + term: searchTerm + })); + } + return self._contactsPromise.then(function(data) { + // Convert contact entries to Backbone collection + data.contacts = new ContactCollection(data.contacts); + + self._view.showContacts(data, searchTerm); + }, function(e) { + self._view.showError(); + console.error('There was an error loading your contacts', e); + }).then(function() { + // Delete promise, so that contacts are fetched again when the + // menu is opened the next time. + delete self._contactsPromise; + }).catch(console.error.bind(this)); + } + }; + + OC.ContactsMenu = ContactsMenu; + +})(OC, $, _, Handlebars); diff --git a/core/js/core.json b/core/js/core.json index 6494d4105f8..aadd66a0558 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -40,6 +40,7 @@ "sharedialogresharerinfoview.js", "sharedialogshareelistview.js", "octemplate.js", + "contactsmenu.js", "eventsource.js", "config.js", "public/appconfig.js", diff --git a/core/js/js.js b/core/js/js.js index 883431b2b02..d601f79033e 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -654,8 +654,13 @@ var OCP = {}, /** * For menu toggling * @todo Write documentation + * + * @param {jQuery} $toggle + * @param {jQuery} $menuEl + * @param {function|undefined} toggle callback invoked everytime the menu is opened + * @returns {undefined} */ - registerMenu: function($toggle, $menuEl) { + registerMenu: function($toggle, $menuEl, toggle) { var self = this; $menuEl.addClass('menu'); $toggle.on('click.menu', function(event) { @@ -671,7 +676,7 @@ var OCP = {}, // close it self.hideMenus(); } - $menuEl.slideToggle(OC.menuSpeed); + $menuEl.slideToggle(OC.menuSpeed, toggle); OC._currentMenu = $menuEl; OC._currentMenuToggle = $toggle; }); @@ -1398,6 +1403,7 @@ function initCore() { // toggle the navigation var $toggle = $('#header .header-appname-container'); var $navigation = $('#navigation'); + var $appmenu = $('#appmenu'); // init the menu OC.registerMenu($toggle, $navigation); @@ -1427,6 +1433,20 @@ function initCore() { OC.hideMenus(function(){return false}); } }); + + $appmenu.delegate('a', 'click', function(event) { + var $app = $(event.target); + if(!$app.is('a')) { + $app = $app.closest('a'); + } + if(event.which === 1 && !event.ctrlKey && !event.metaKey) { + $app.addClass('app-loading'); + } else { + // Close navigation when opening app in + // a new tab + OC.hideMenus(function(){return false}); + } + }); } function setupUserMenu() { @@ -1458,8 +1478,16 @@ function initCore() { }); } + function setupContactsMenu() { + new OC.ContactsMenu({ + el: $('#contactsmenu .menu'), + trigger: $('#contactsmenu .menutoggle') + }); + } + setupMainMenu(); setupUserMenu(); + setupContactsMenu(); // move triangle of apps dropdown to align with app name triangle // 2 is the additional offset between the triangles @@ -1482,6 +1510,52 @@ function initCore() { }); } + var resizeMenu = function() { + var maxApps = 8; + var appList = $('#appmenu li'); + var availableWidth = $('#header-left').width() - $('#nextcloud').width() - 44; + var appCount = Math.floor((availableWidth)/44); + // show a maximum of 8 apps + if(appCount >= maxApps) { + appCount = maxApps; + } + // show at least 2 apps in the popover + if(appList.length-1-appCount >= 1) { + appCount--; + } + + $('#more-apps a').removeClass('active'); + var lastShownApp; + for (var k = 0; k < appList.length-1; k++) { + var name = $(appList[k]).data('id'); + if(k < appCount) { + $(appList[k]).removeClass('hidden'); + $('#apps li[data-id=' + name + ']').addClass('in-header'); + lastShownApp = appList[k]; + } else { + $(appList[k]).addClass('hidden'); + $('#apps li[data-id=' + name + ']').removeClass('in-header'); + // move active app to last position if it is active + if(appCount > 0 && $(appList[k]).children('a').hasClass('active')) { + $(lastShownApp).addClass('hidden'); + $('#apps li[data-id=' + $(lastShownApp).data('id') + ']').removeClass('in-header'); + $(appList[k]).removeClass('hidden'); + $('#apps li[data-id=' + name + ']').addClass('in-header'); + } + } + } + + // show/hide more apps icon + if($('#apps li:not(.in-header)').length === 0) { + $('#more-apps').hide(); + $('#navigation').hide(); + } else { + $('#more-apps').show(); + } + }; + $(window).resize(resizeMenu); + resizeMenu(); + // just add snapper for logged in users if($('#app-navigation').length && !$('html').hasClass('lte9')) { diff --git a/core/js/login/authpicker.js b/core/js/login/authpicker.js new file mode 100644 index 00000000000..6d8a6bb4160 --- /dev/null +++ b/core/js/login/authpicker.js @@ -0,0 +1,13 @@ +jQuery(document).ready(function() { + $('#app-token-login').click(function (e) { + e.preventDefault(); + $(this).addClass('hidden'); + $('#redirect-link').addClass('hidden'); + $('#app-token-login-field').removeClass('hidden'); + }); + + $('#submit-app-token-login').click(function(e) { + e.preventDefault(); + window.location.href = 'nc://' + encodeURIComponent($('#user').val()) + ':' + encodeURIComponent($('#password').val()) + '@' + encodeURIComponent($('#serverHost').val()); + }); +}); diff --git a/core/js/login/redirect.js b/core/js/login/redirect.js new file mode 100644 index 00000000000..ea214feab2d --- /dev/null +++ b/core/js/login/redirect.js @@ -0,0 +1,3 @@ +jQuery(document).ready(function() { + $('#submit-redirect-form').trigger('click'); +}); diff --git a/core/js/sharedialoglinkshareview.js b/core/js/sharedialoglinkshareview.js index 6017714b305..9368982d916 100644 --- a/core/js/sharedialoglinkshareview.js +++ b/core/js/sharedialoglinkshareview.js @@ -60,14 +60,14 @@ '<input type="checkbox" name="showPassword" id="showPassword-{{cid}}" class="checkbox showPasswordCheckbox" {{#if isPasswordSet}}checked="checked"{{/if}} value="1" />' + '<label for="showPassword-{{cid}}">{{enablePasswordLabel}}</label>' + ' {{/if}}' + - '<div id="linkPass" class="linkPass {{#unless isPasswordSet}}hidden{{/unless}}">' + + '<div id="linkPass" class="oneline linkPass {{#unless isPasswordSet}}hidden{{/unless}}">' + ' <label for="linkPassText-{{cid}}" class="hidden-visually">{{passwordLabel}}</label>' + ' {{#if showPasswordCheckBox}}' + ' <input id="linkPassText-{{cid}}" class="linkPassText" type="password" placeholder="{{passwordPlaceholder}}" />' + ' {{else}}' + ' <input id="linkPassText-{{cid}}" class="linkPassText" type="password" placeholder="{{passwordPlaceholderInitial}}" />' + ' {{/if}}' + - ' <span class="icon-loading-small hidden"></span>' + + ' <span class="icon icon-loading-small hidden"></span>' + '</div>' + '{{else}}' + // FIXME: this doesn't belong in this view @@ -307,10 +307,12 @@ this.model.saveLinkShare({ password: password }, { + complete: function(model) { + $loading.removeClass('inlineblock').addClass('hidden'); + }, error: function(model, msg) { // destroy old tooltips $input.tooltip('destroy'); - $loading.removeClass('inlineblock').addClass('hidden'); $input.addClass('error'); $input.attr('title', msg); $input.tooltip({placement: 'bottom', trigger: 'manual'}); diff --git a/core/js/sharedialogview.js b/core/js/sharedialogview.js index 16a2be0c4a9..3b09d13a7e2 100644 --- a/core/js/sharedialogview.js +++ b/core/js/sharedialogview.js @@ -22,7 +22,7 @@ '<div class="oneline">' + ' <input id="shareWith-{{cid}}" class="shareWithField" type="text" placeholder="{{sharePlaceholder}}" />' + ' <span class="shareWithLoading icon-loading-small hidden"></span>'+ - '{{{remoteShareInfo}}}' + + '{{{shareInfo}}}' + '</div>' + '{{/if}}' + '<div class="shareeListView subView"></div>' + @@ -30,9 +30,9 @@ '<div class="expirationView subView"></div>' + '<div class="loading hidden" style="height: 50px"></div>'; - var TEMPLATE_REMOTE_SHARE_INFO = - '<a target="_blank" class="icon icon-info shareWithRemoteInfo hasTooltip" href="{{docLink}}" ' + - 'title="{{tooltip}}"></a>'; + var TEMPLATE_SHARE_INFO = + '<span class="icon icon-info shareWithRemoteInfo hasTooltip" ' + + 'title="{{tooltip}}"></span>'; /** * @class OCA.Share.ShareDialogView @@ -135,7 +135,7 @@ var $shareWithField = $('.shareWithField'), view = this, $loading = this.$el.find('.shareWithLoading'), - $remoteShareInfo = this.$el.find('.shareWithRemoteInfo'); + $shareInfo = this.$el.find('.shareWithRemoteInfo'); var count = oc_config['sharing.minSearchStringLength']; if (search.term.trim().length < count) { @@ -160,7 +160,7 @@ $loading.removeClass('hidden'); $loading.addClass('inlineblock'); - $remoteShareInfo.addClass('hidden'); + $shareInfo.addClass('hidden'); $shareWithField.removeClass('error') .tooltip('hide'); @@ -177,7 +177,7 @@ function (result) { $loading.addClass('hidden'); $loading.removeClass('inlineblock'); - $remoteShareInfo.removeClass('hidden'); + $shareInfo.removeClass('hidden'); if (result.ocs.meta.statuscode === 100) { var users = result.ocs.data.exact.users.concat(result.ocs.data.users); var groups = result.ocs.data.exact.groups.concat(result.ocs.data.groups); @@ -314,7 +314,7 @@ ).fail(function() { $loading.addClass('hidden'); $loading.removeClass('inlineblock'); - $remoteShareInfo.removeClass('hidden'); + $shareInfo.removeClass('hidden'); OC.Notification.show(t('core', 'An error occurred. Please try again')); window.setTimeout(OC.Notification.hide, 5000); }); @@ -359,22 +359,22 @@ var $loading = this.$el.find('.shareWithLoading'); $loading.removeClass('hidden') .addClass('inlineblock'); - var $remoteShareInfo = this.$el.find('.shareWithRemoteInfo'); - $remoteShareInfo.addClass('hidden'); + var $shareInfo = this.$el.find('.shareWithRemoteInfo'); + $shareInfo.addClass('hidden'); this.model.addShare(s.item.value, {success: function() { $(e.target).val('') .attr('disabled', false); $loading.addClass('hidden') .removeClass('inlineblock'); - $remoteShareInfo.removeClass('hidden'); + $shareInfo.removeClass('hidden'); }, error: function(obj, msg) { OC.Notification.showTemporary(msg); $(e.target).attr('disabled', false) .autocomplete('search', $(e.target).val()); $loading.addClass('hidden') .removeClass('inlineblock'); - $remoteShareInfo.removeClass('hidden'); + $shareInfo.removeClass('hidden'); }}); }, @@ -412,7 +412,7 @@ cid: this.cid, shareLabel: t('core', 'Share'), sharePlaceholder: this._renderSharePlaceholderPart(), - remoteShareInfo: this._renderRemoteShareInfoPart(), + shareInfo: this._renderShareInfoPart(), isSharingAllowed: this.model.sharePermissionPossible() })); @@ -457,47 +457,42 @@ this.linkShareView.showLink = this._showLink; }, - _renderRemoteShareInfoPart: function() { - var remoteShareInfo = ''; - if(this.configModel.get('isRemoteShareAllowed')) { - var infoTemplate = this._getRemoteShareInfoTemplate(); - remoteShareInfo = infoTemplate({ - docLink: this.configModel.getFederatedShareDocLink(), - tooltip: t('core', 'Share with people on other servers using their Federated Cloud ID username@example.com/nextcloud') + _renderShareInfoPart: function() { + var shareInfo = ''; + var infoTemplate = this._getShareInfoTemplate(); + + if(this.configModel.get('isMailShareAllowed') && this.configModel.get('isRemoteShareAllowed')) { + shareInfo = infoTemplate({ + tooltip: t('core', 'Share with other people by entering a user or group, a federated cloud ID or an email address.') + }); + } else if(this.configModel.get('isRemoteShareAllowed')) { + shareInfo = infoTemplate({ + tooltip: t('core', 'Share with other people by entering a user or group or a federated cloud ID.') + }); + } else if(this.configModel.get('isMailShareAllowed')) { + shareInfo = infoTemplate({ + tooltip: t('core', 'Share with other people by entering a user or group or an email address.') }); } - return remoteShareInfo; + return shareInfo; }, _renderSharePlaceholderPart: function () { - var allowGroupSharing = this.configModel.get('allowGroupSharing'); var allowRemoteSharing = this.configModel.get('isRemoteShareAllowed'); var allowMailSharing = this.configModel.get('isMailShareAllowed'); - if (!allowGroupSharing && !allowRemoteSharing && allowMailSharing) { - return t('core', 'Share with users or by mail...'); - } - if (!allowGroupSharing && allowRemoteSharing && !allowMailSharing) { - return t('core', 'Share with users or remote users...'); - } - if (!allowGroupSharing && allowRemoteSharing && allowMailSharing) { - return t('core', 'Share with users, remote users or by mail...'); - } - if (allowGroupSharing && !allowRemoteSharing && !allowMailSharing) { - return t('core', 'Share with users or groups...'); - } - if (allowGroupSharing && !allowRemoteSharing && allowMailSharing) { - return t('core', 'Share with users, groups or by mail...'); + if (!allowRemoteSharing && allowMailSharing) { + return t('core', 'Name or email address...'); } - if (allowGroupSharing && allowRemoteSharing && !allowMailSharing) { - return t('core', 'Share with users, groups or remote users...'); + if (allowRemoteSharing && !allowMailSharing) { + return t('core', 'Name or federated cloud ID...'); } - if (allowGroupSharing && allowRemoteSharing && allowMailSharing) { - return t('core', 'Share with users, groups, remote users or by mail...'); + if (allowRemoteSharing && allowMailSharing) { + return t('core', 'Name, federated cloud ID or email address...'); } - return t('core', 'Share with users...'); + return t('core', 'Name...'); }, /** @@ -520,8 +515,8 @@ * @returns {Function} * @private */ - _getRemoteShareInfoTemplate: function() { - return this._getTemplate('remoteShareInfo', TEMPLATE_REMOTE_SHARE_INFO); + _getShareInfoTemplate: function() { + return this._getTemplate('shareInfo', TEMPLATE_SHARE_INFO); } }); diff --git a/core/js/shareitemmodel.js b/core/js/shareitemmodel.js index bff006f7ef3..41f9eb5e0aa 100644 --- a/core/js/shareitemmodel.js +++ b/core/js/shareitemmodel.js @@ -104,7 +104,14 @@ /** * Saves the current link share information. * - * This will trigger an ajax call and refetch the model afterwards. + * This will trigger an ajax call and, if successful, refetch the model + * afterwards. Callbacks "success", "error" and "complete" can be given + * in the options object; "success" is called after a successful save + * once the model is refetch, "error" is called after a failed save, and + * "complete" is called both after a successful save and after a failed + * save. Note that "complete" is called before "success" and "error" are + * called (unlike in jQuery, in which it is called after them); this + * ensures that "complete" is called even if refetching the model fails. * * TODO: this should be a separate model */ @@ -149,7 +156,6 @@ addShare: function(attributes, options) { var shareType = attributes.shareType; - options = options || {}; attributes = _.extend({}, attributes); // Default permissions are Edit (CRUD) and Share @@ -173,53 +179,43 @@ attributes.path = this.fileInfoModel.getFullPath(); } - var self = this; - return $.ajax({ + return this._addOrUpdateShare({ type: 'POST', url: this._getUrl('shares'), data: attributes, dataType: 'json' - }).done(function() { - self.fetch().done(function() { - if (_.isFunction(options.success)) { - options.success(self); - } - }); - }).fail(function(xhr) { - var msg = t('core', 'Error'); - var result = xhr.responseJSON; - if (result && result.ocs && result.ocs.meta) { - msg = result.ocs.meta.message; - } - - if (_.isFunction(options.error)) { - options.error(self, msg); - } else { - OC.dialogs.alert(msg, t('core', 'Error while sharing')); - } - }); + }, options); }, updateShare: function(shareId, attrs, options) { - var self = this; - options = options || {}; - return $.ajax({ + return this._addOrUpdateShare({ type: 'PUT', url: this._getUrl('shares/' + encodeURIComponent(shareId)), data: attrs, dataType: 'json' + }, options); + }, + + _addOrUpdateShare: function(ajaxSettings, options) { + var self = this; + options = options || {}; + + return $.ajax( + ajaxSettings + ).always(function() { + if (_.isFunction(options.complete)) { + options.complete(self); + } }).done(function() { - self.fetch({ - success: function() { - if (_.isFunction(options.success)) { - options.success(self); - } + self.fetch().done(function() { + if (_.isFunction(options.success)) { + options.success(self); } }); }).fail(function(xhr) { var msg = t('core', 'Error'); var result = xhr.responseJSON; - if (result.ocs && result.ocs.meta) { + if (result && result.ocs && result.ocs.meta) { msg = result.ocs.meta.message; } @@ -802,7 +798,7 @@ isLinkShare: true, id: share.id, token: share.token, - password: share.password, + password: share.share_with, link: link, permissions: share.permissions, // currently expiration is only effective for link shares. diff --git a/core/js/tests/specs/contactsmenuSpec.js b/core/js/tests/specs/contactsmenuSpec.js new file mode 100644 index 00000000000..8e57dc35f01 --- /dev/null +++ b/core/js/tests/specs/contactsmenuSpec.js @@ -0,0 +1,265 @@ +/* global expect, sinon, _, spyOn, Promise */ + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +describe('Contacts menu', function() { + var $triggerEl, + $menuEl, + menu; + + /** + * @private + * @returns {Promise} + */ + function openMenu() { + return menu._toggleVisibility(true); + } + + beforeEach(function(done) { + $triggerEl = $('<div class="menutoggle">'); + $menuEl = $('<div class="menu">'); + + menu = new OC.ContactsMenu({ + el: $menuEl, + trigger: $triggerEl + }); + done(); + }); + + it('shows a loading message while data is being fetched', function() { + fakeServer.respondWith('GET', OC.generateUrl('/contactsmenu/contacts'), [ + 200, + {}, + '' + ]); + + openMenu(); + + expect($menuEl.html()).toContain('Loading your contacts …'); + }); + + it('shows an error message when loading the contacts data fails', function(done) { + spyOn(console, 'error'); + fakeServer.respondWith('GET', OC.generateUrl('/contactsmenu/contacts'), [ + 500, + {}, + '' + ]); + + var opening = openMenu(); + + expect($menuEl.html()).toContain('Loading your contacts …'); + fakeServer.respond(); + + opening.then(function() { + expect($menuEl.html()).toContain('There was an error loading your contacts'); + expect(console.error).toHaveBeenCalledTimes(1); + done(); + }, function(e) { + done.fail(e); + }); + }); + + it('loads data successfully', function(done) { + spyOn(menu, '_getContacts').and.returnValue(Promise.resolve({ + contacts: [ + { + id: null, + fullName: 'Acosta Lancaster', + topAction: { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:deboraoliver%40centrexin.com' + }, + actions: [ + { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:mathisholland%40virxo.com' + }, + { + title: 'Details', + icon: 'icon-info', + hyperlink: 'https:\/\/localhost\/index.php\/apps\/contacts' + } + ], + lastMessage: '' + }, + { + id: null, + fullName: 'Adeline Snider', + topAction: { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:ceciliasoto%40essensia.com' + }, + actions: [ + { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:pearliesellers%40inventure.com' + }, + { + title: 'Details', + icon: 'icon-info', + hyperlink: 'https://localhost\/index.php\/apps\/contacts' + } + ], + lastMessage: 'cu' + } + ], + contactsAppEnabled: true + })); + + openMenu().then(function() { + expect(menu._getContacts).toHaveBeenCalled(); + expect($menuEl.html()).toContain('Acosta Lancaster'); + expect($menuEl.html()).toContain('Adeline Snider'); + expect($menuEl.html()).toContain('Show all contacts …'); + done(); + }, function(e) { + done.fail(e); + }); + + }); + + it('doesn\'t show a link to the contacts app if it\'s disabled', function(done) { + spyOn(menu, '_getContacts').and.returnValue(Promise.resolve({ + contacts: [ + { + id: null, + fullName: 'Acosta Lancaster', + topAction: { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:deboraoliver%40centrexin.com' + }, + actions: [ + { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:mathisholland%40virxo.com' + }, + { + title: 'Details', + icon: 'icon-info', + hyperlink: 'https:\/\/localhost\/index.php\/apps\/contacts' + } + ], + lastMessage: '' + } + ], + contactsAppEnabled: false + })); + + openMenu().then(function() { + expect(menu._getContacts).toHaveBeenCalled(); + expect($menuEl.html()).not.toContain('Show all contacts …'); + done(); + }, function(e) { + done.fail(e); + }); + }); + + it('shows only one entry\'s action menu at a time', function(done) { + spyOn(menu, '_getContacts').and.returnValue(Promise.resolve({ + contacts: [ + { + id: null, + fullName: 'Acosta Lancaster', + topAction: { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:deboraoliver%40centrexin.com' + }, + actions: [ + { + title: 'Info', + icon: 'icon-info', + hyperlink: 'https:\/\/localhost\/index.php\/apps\/contacts' + }, + { + title: 'Details', + icon: 'icon-info', + hyperlink: 'https:\/\/localhost\/index.php\/apps\/contacts' + } + ], + lastMessage: '' + }, + { + id: null, + fullName: 'Adeline Snider', + topAction: { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:ceciliasoto%40essensia.com' + }, + actions: [ + { + title: 'Info', + icon: 'icon-info', + hyperlink: 'https://localhost\/index.php\/apps\/contacts' + }, + { + title: 'Details', + icon: 'icon-info', + hyperlink: 'https://localhost\/index.php\/apps\/contacts' + } + ], + lastMessage: 'cu' + } + ], + contactsAppEnabled: true + })); + + openMenu().then(function() { + expect(menu._getContacts).toHaveBeenCalled(); + expect($menuEl.html()).toContain('Adeline Snider'); + expect($menuEl.html()).toContain('Show all contacts …'); + + // Both menus are closed at the beginning + expect($menuEl.find('.contact').eq(0).find('.menu').is(':visible')).toBe(false); + expect($menuEl.find('.contact').eq(1).find('.menu').is(':visible')).toBe(false); + + // Open the first one + $menuEl.find('.contact').eq(0).find('.other-actions').click(); + expect($menuEl.find('.contact').eq(0).find('.menu').css('display')).toBe('block'); + expect($menuEl.find('.contact').eq(1).find('.menu').css('display')).toBe('none'); + + // Open the second one + $menuEl.find('.contact').eq(1).find('.other-actions').click(); + expect($menuEl.find('.contact').eq(0).find('.menu').css('display')).toBe('none'); + expect($menuEl.find('.contact').eq(1).find('.menu').css('display')).toBe('block'); + + // Close the second one + $menuEl.find('.contact').eq(1).find('.other-actions').click(); + expect($menuEl.find('.contact').eq(0).find('.menu').css('display')).toBe('none'); + expect($menuEl.find('.contact').eq(1).find('.menu').css('display')).toBe('none'); + + done(); + }, function(e) { + done.fail(e); + }); + }); + +}); diff --git a/core/js/tests/specs/jquery.avatarSpec.js b/core/js/tests/specs/jquery.avatarSpec.js index 9bb10c41be7..dab78500d0b 100644 --- a/core/js/tests/specs/jquery.avatarSpec.js +++ b/core/js/tests/specs/jquery.avatarSpec.js @@ -186,7 +186,7 @@ describe('jquery.avatar tests', function() { }); it('with ie8 fix', function() { - sinon.stub(Math, 'random', function() { + sinon.stub(Math, 'random').callsFake(function() { return 0.5; }); diff --git a/core/js/tests/specs/sharedialoglinkshareview.js b/core/js/tests/specs/sharedialoglinkshareview.js new file mode 100644 index 00000000000..811919b5603 --- /dev/null +++ b/core/js/tests/specs/sharedialoglinkshareview.js @@ -0,0 +1,143 @@ +/** + * + * @copyright Copyright (c) 2015, Tom Needham (tom@owncloud.com) + * @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com) + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +describe('OC.Share.ShareDialogLinkShareView', function () { + + var configModel; + var shareModel; + var view; + + beforeEach(function () { + + var fileInfoModel = new OCA.Files.FileInfoModel({ + id: 123, + name: 'shared_file_name.txt', + path: '/subdir', + size: 100, + mimetype: 'text/plain', + permissions: OC.PERMISSION_ALL, + sharePermissions: OC.PERMISSION_ALL + }); + + var attributes = { + itemType: fileInfoModel.isDirectory() ? 'folder' : 'file', + itemSource: fileInfoModel.get('id'), + possiblePermissions: OC.PERMISSION_ALL, + permissions: OC.PERMISSION_ALL + }; + + configModel = new OC.Share.ShareConfigModel({ + enforcePasswordForPublicLink: false, + isResharingAllowed: true, + enforcePasswordForPublicLink: false, + isDefaultExpireDateEnabled: false, + isDefaultExpireDateEnforced: false, + defaultExpireDate: 7 + }); + + sinon.stub(configModel, 'isShareWithLinkAllowed'); + + shareModel = new OC.Share.ShareItemModel(attributes, { + configModel: configModel, + fileInfoModel: fileInfoModel + }); + + view = new OC.Share.ShareDialogLinkShareView({ + configModel: configModel, + model: shareModel + }); + + }); + + afterEach(function () { + view.remove(); + configModel.isShareWithLinkAllowed.restore(); + }); + + describe('onPasswordEntered', function () { + + var $passwordText; + var $workingIcon; + + beforeEach(function () { + + // Needed to render the view + configModel.isShareWithLinkAllowed.returns(true); + + // Setting the share also triggers the rendering + shareModel.set({ + linkShare: { + isLinkShare: true, + password: 'password' + } + }); + + var $passwordDiv = view.$el.find('#linkPass'); + $passwordText = view.$el.find('.linkPassText'); + $workingIcon = view.$el.find('.linkPass .icon-loading-small'); + + sinon.stub(shareModel, 'saveLinkShare'); + + expect($passwordDiv.hasClass('hidden')).toBeFalsy(); + expect($passwordText.hasClass('hidden')).toBeFalsy(); + expect($workingIcon.hasClass('hidden')).toBeTruthy(); + + $passwordText.val('myPassword'); + }); + + afterEach(function () { + shareModel.saveLinkShare.restore(); + }); + + it('shows the working icon when called', function () { + view.onPasswordEntered(); + + expect($workingIcon.hasClass('hidden')).toBeFalsy(); + expect(shareModel.saveLinkShare.withArgs({ password: 'myPassword' }).calledOnce).toBeTruthy(); + }); + + it('hides the working icon when saving the password succeeds', function () { + view.onPasswordEntered(); + + expect($workingIcon.hasClass('hidden')).toBeFalsy(); + expect(shareModel.saveLinkShare.withArgs({ password: 'myPassword' }).calledOnce).toBeTruthy(); + + shareModel.saveLinkShare.yieldTo("complete", [shareModel]); + + expect($workingIcon.hasClass('hidden')).toBeTruthy(); + }); + + it('hides the working icon when saving the password fails', function () { + view.onPasswordEntered(); + + expect($workingIcon.hasClass('hidden')).toBeFalsy(); + expect(shareModel.saveLinkShare.withArgs({ password: 'myPassword' }).calledOnce).toBeTruthy(); + + shareModel.saveLinkShare.yieldTo("complete", [shareModel]); + shareModel.saveLinkShare.yieldTo("error", [shareModel, "The error message"]); + + expect($workingIcon.hasClass('hidden')).toBeTruthy(); + }); + + }); + +}); diff --git a/core/js/tests/specs/sharedialogviewSpec.js b/core/js/tests/specs/sharedialogviewSpec.js index 307adea85ff..95349bc4875 100644 --- a/core/js/tests/specs/sharedialogviewSpec.js +++ b/core/js/tests/specs/sharedialogviewSpec.js @@ -19,7 +19,7 @@ * */ -/* global oc_appconfig */ +/* global oc_appconfig, sinon */ describe('OC.Share.ShareDialogView', function() { var $container; var oldAppConfig; @@ -90,7 +90,7 @@ describe('OC.Share.ShareDialogView', function() { linkShare: {isLinkShare: false} }); - autocompleteStub = sinon.stub($.fn, 'autocomplete', function() { + autocompleteStub = sinon.stub($.fn, 'autocomplete').callsFake(function() { // dummy container with the expected attributes if (!$(this).length) { // simulate the real autocomplete that returns diff --git a/core/js/tests/specs/shareitemmodelSpec.js b/core/js/tests/specs/shareitemmodelSpec.js index 3d3baf75d15..771a9263709 100644 --- a/core/js/tests/specs/shareitemmodelSpec.js +++ b/core/js/tests/specs/shareitemmodelSpec.js @@ -670,6 +670,83 @@ describe('OC.Share.ShareItemModel', function() { shareWith: 'group1' }); }); + it('calls complete handler before refreshing the model', function() { + var completeStub = sinon.stub(); + model.addShare({ + shareType: OC.Share.SHARE_TYPE_GROUP, + shareWith: 'group1' + }, { + complete: completeStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ }) + ); + + expect(completeStub.calledOnce).toEqual(true); + expect(completeStub.lastCall.args[0]).toEqual(model); + + fetchReshareDeferred.resolve(makeOcsResponse([])); + fetchSharesDeferred.resolve(makeOcsResponse([])); + + expect(completeStub.calledOnce).toEqual(true); + }); + it('calls success handler after refreshing the model', function() { + var successStub = sinon.stub(); + model.addShare({ + shareType: OC.Share.SHARE_TYPE_GROUP, + shareWith: 'group1' + }, { + success: successStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ }) + ); + + expect(successStub.called).toEqual(false); + + fetchReshareDeferred.resolve(makeOcsResponse([])); + fetchSharesDeferred.resolve(makeOcsResponse([])); + + expect(successStub.calledOnce).toEqual(true); + expect(successStub.lastCall.args[0]).toEqual(model); + }); + it('calls complete handler before error handler', function() { + var completeStub = sinon.stub(); + var errorStub = sinon.stub(); + model.addShare({ + shareType: OC.Share.SHARE_TYPE_GROUP, + shareWith: 'group1' + }, { + complete: completeStub, + error: errorStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 400, + { 'Content-Type': 'application/json' }, + JSON.stringify({ + ocs: { + meta: { + message: 'Some error message' + } + } + }) + ); + + expect(completeStub.calledOnce).toEqual(true); + expect(completeStub.lastCall.args[0]).toEqual(model); + expect(errorStub.calledOnce).toEqual(true); + expect(completeStub.calledBefore(errorStub)).toEqual(true); + }); it('calls error handler with error message', function() { var errorStub = sinon.stub(); model.addShare({ @@ -693,6 +770,7 @@ describe('OC.Share.ShareItemModel', function() { ); expect(errorStub.calledOnce).toEqual(true); + expect(errorStub.lastCall.args[0]).toEqual(model); expect(errorStub.lastCall.args[1]).toEqual('Some error message'); }); }); @@ -712,6 +790,80 @@ describe('OC.Share.ShareItemModel', function() { permissions: '' + (OC.PERMISSION_READ | OC.PERMISSION_SHARE) }); }); + it('calls complete handler before refreshing the model', function() { + var completeStub = sinon.stub(); + model.updateShare(123, { + permissions: OC.PERMISSION_READ | OC.PERMISSION_SHARE + }, { + complete: completeStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ }) + ); + + expect(completeStub.calledOnce).toEqual(true); + expect(completeStub.lastCall.args[0]).toEqual(model); + + fetchReshareDeferred.resolve(makeOcsResponse([])); + fetchSharesDeferred.resolve(makeOcsResponse([])); + + expect(completeStub.calledOnce).toEqual(true); + }); + it('calls success handler after refreshing the model', function() { + var successStub = sinon.stub(); + model.updateShare(123, { + permissions: OC.PERMISSION_READ | OC.PERMISSION_SHARE + }, { + success: successStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ }) + ); + + expect(successStub.called).toEqual(false); + + fetchReshareDeferred.resolve(makeOcsResponse([])); + fetchSharesDeferred.resolve(makeOcsResponse([])); + + expect(successStub.calledOnce).toEqual(true); + expect(successStub.lastCall.args[0]).toEqual(model); + }); + it('calls complete handler before error handler', function() { + var completeStub = sinon.stub(); + var errorStub = sinon.stub(); + model.updateShare(123, { + permissions: OC.PERMISSION_READ | OC.PERMISSION_SHARE + }, { + complete: completeStub, + error: errorStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 400, + { 'Content-Type': 'application/json' }, + JSON.stringify({ + ocs: { + meta: { + message: 'Some error message' + } + } + }) + ); + + expect(completeStub.calledOnce).toEqual(true); + expect(completeStub.lastCall.args[0]).toEqual(model); + expect(errorStub.calledOnce).toEqual(true); + expect(completeStub.calledBefore(errorStub)).toEqual(true); + }); it('calls error handler with error message', function() { var errorStub = sinon.stub(); model.updateShare(123, { @@ -734,6 +886,7 @@ describe('OC.Share.ShareItemModel', function() { ); expect(errorStub.calledOnce).toEqual(true); + expect(errorStub.lastCall.args[0]).toEqual(model); expect(errorStub.lastCall.args[1]).toEqual('Some error message'); }); }); |