diff options
author | Morris Jobke <hey@morrisjobke.de> | 2018-03-21 15:08:39 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-21 15:08:39 +0100 |
commit | 6f455d5c4153130b0a368938a408aa2369fa66c0 (patch) | |
tree | 91f107ae691804e0bc9a1ce83e0f39346b968b7f /core | |
parent | 24292be917ec99e273b3e39b2a4f99292935e331 (diff) | |
parent | 203bf51543f7d1866efeaa9decd2817cb41b3c3e (diff) | |
download | nextcloud-server-6f455d5c4153130b0a368938a408aa2369fa66c0.tar.gz nextcloud-server-6f455d5c4153130b0a368938a408aa2369fa66c0.zip |
Merge pull request #7196 from nextcloud/sharing-confirm-button
Add confirmation button to share input
Diffstat (limited to 'core')
-rw-r--r-- | core/js/sharedialogview.js | 560 | ||||
-rw-r--r-- | core/js/tests/specs/sharedialogviewSpec.js | 1570 |
2 files changed, 1900 insertions, 230 deletions
diff --git a/core/js/sharedialogview.js b/core/js/sharedialogview.js index d2c63490141..dede768fad5 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>'+ - '{{{shareInfo}}}' + + ' <span class="shareWithConfirm icon icon-confirm"></span>' + '</div>' + '{{/if}}' + '<div class="shareeListView subView"></div>' + @@ -30,10 +30,6 @@ '<div class="expirationView subView"></div>' + '<div class="loading hidden" style="height: 50px"></div>'; - var TEMPLATE_SHARE_INFO = - '<span class="icon icon-info shareWithRemoteInfo hasTooltip" ' + - 'title="{{tooltip}}"></span>'; - /** * @class OCA.Share.ShareDialogView * @member {OC.Share.ShareItemModel} model @@ -69,9 +65,16 @@ /** @type {object} **/ shareeListView: undefined, + /** @type {object} **/ + _lastSuggestions: undefined, + + /** @type {int} **/ + _pendingOperationsCount: 0, + events: { 'focus .shareWithField': 'onShareWithFieldFocus', - 'input .shareWithField': 'onShareWithFieldChanged' + 'input .shareWithField': 'onShareWithFieldChanged', + 'click .shareWithConfirm': '_confirmShare' }, initialize: function(options) { @@ -138,11 +141,182 @@ this.$el.find('.shareWithField').autocomplete("search"); }, + _getSuggestions: function(searchTerm, perPage, model) { + if (this._lastSuggestions && + this._lastSuggestions.searchTerm === searchTerm && + this._lastSuggestions.perPage === perPage && + this._lastSuggestions.model === model) { + return this._lastSuggestions.promise; + } + + var deferred = $.Deferred(); + + $.get( + OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees', + { + format: 'json', + search: searchTerm, + perPage: perPage, + itemType: model.get('itemType') + }, + function (result) { + if (result.ocs.meta.statuscode === 100) { + var filter = function(users, groups, remotes, emails, circles) { + if (typeof(emails) === 'undefined') { + emails = []; + } + if (typeof(circles) === 'undefined') { + circles = []; + } + + var usersLength; + var groupsLength; + var remotesLength; + var emailsLength; + var circlesLength; + + var i, j; + + //Filter out the current user + usersLength = users.length; + for (i = 0; i < usersLength; i++) { + if (users[i].value.shareWith === OC.currentUser) { + users.splice(i, 1); + break; + } + } + + // Filter out the owner of the share + if (model.hasReshare()) { + usersLength = users.length; + for (i = 0 ; i < usersLength; i++) { + if (users[i].value.shareWith === model.getReshareOwner()) { + users.splice(i, 1); + break; + } + } + } + + var shares = model.get('shares'); + var sharesLength = shares.length; + + // Now filter out all sharees that are already shared with + for (i = 0; i < sharesLength; i++) { + var share = shares[i]; + + if (share.share_type === OC.Share.SHARE_TYPE_USER) { + usersLength = users.length; + for (j = 0; j < usersLength; j++) { + if (users[j].value.shareWith === share.share_with) { + users.splice(j, 1); + break; + } + } + } else if (share.share_type === OC.Share.SHARE_TYPE_GROUP) { + groupsLength = groups.length; + for (j = 0; j < groupsLength; j++) { + if (groups[j].value.shareWith === share.share_with) { + groups.splice(j, 1); + break; + } + } + } else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE) { + remotesLength = remotes.length; + for (j = 0; j < remotesLength; j++) { + if (remotes[j].value.shareWith === share.share_with) { + remotes.splice(j, 1); + break; + } + } + } else if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) { + emailsLength = emails.length; + for (j = 0; j < emailsLength; j++) { + if (emails[j].value.shareWith === share.share_with) { + emails.splice(j, 1); + break; + } + } + } else if (share.share_type === OC.Share.SHARE_TYPE_CIRCLE) { + circlesLength = circles.length; + for (j = 0; j < circlesLength; j++) { + if (circles[j].value.shareWith === share.share_with) { + circles.splice(j, 1); + break; + } + } + } + } + }; + + filter( + result.ocs.data.exact.users, + result.ocs.data.exact.groups, + result.ocs.data.exact.remotes, + result.ocs.data.exact.emails, + result.ocs.data.exact.circles + ); + + var exactUsers = result.ocs.data.exact.users; + var exactGroups = result.ocs.data.exact.groups; + var exactRemotes = result.ocs.data.exact.remotes; + var exactEmails = []; + if (typeof(result.ocs.data.emails) !== 'undefined') { + exactEmails = result.ocs.data.exact.emails; + } + var exactCircles = []; + if (typeof(result.ocs.data.circles) !== 'undefined') { + exactCircles = result.ocs.data.exact.circles; + } + + var exactMatches = exactUsers.concat(exactGroups).concat(exactRemotes).concat(exactEmails).concat(exactCircles); + + filter( + result.ocs.data.users, + result.ocs.data.groups, + result.ocs.data.remotes, + result.ocs.data.emails, + result.ocs.data.circles + ); + + var users = result.ocs.data.users; + var groups = result.ocs.data.groups; + var remotes = result.ocs.data.remotes; + var lookup = result.ocs.data.lookup; + var emails = []; + if (typeof(result.ocs.data.emails) !== 'undefined') { + emails = result.ocs.data.emails; + } + var circles = []; + if (typeof(result.ocs.data.circles) !== 'undefined') { + circles = result.ocs.data.circles; + } + + var suggestions = exactMatches.concat(users).concat(groups).concat(remotes).concat(emails).concat(circles).concat(lookup); + + deferred.resolve(suggestions, exactMatches); + } else { + deferred.reject(result.ocs.meta.message); + } + } + ).fail(function() { + deferred.reject(); + }); + + this._lastSuggestions = { + searchTerm: searchTerm, + perPage: perPage, + model: model, + promise: deferred.promise() + }; + + return this._lastSuggestions.promise; + }, + autocompleteHandler: function (search, response) { var $shareWithField = $('.shareWithField'), view = this, $loading = this.$el.find('.shareWithLoading'), - $shareInfo = this.$el.find('.shareWithRemoteInfo'); + $confirm = this.$el.find('.shareWithConfirm'); var count = oc_config['sharing.minSearchStringLength']; if (search.term.trim().length < count) { @@ -167,160 +341,70 @@ $loading.removeClass('hidden'); $loading.addClass('inlineblock'); - $shareInfo.addClass('hidden'); + $confirm.addClass('hidden'); + this._pendingOperationsCount++; $shareWithField.removeClass('error') .tooltip('hide'); var perPage = 200; - $.get( - OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees', - { - format: 'json', - search: search.term.trim(), - perPage: perPage, - itemType: view.model.get('itemType') - }, - function (result) { + this._getSuggestions( + search.term.trim(), + perPage, + view.model + ).done(function(suggestions) { + view._pendingOperationsCount--; + if (view._pendingOperationsCount === 0) { $loading.addClass('hidden'); $loading.removeClass('inlineblock'); - $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); - var remotes = result.ocs.data.exact.remotes.concat(result.ocs.data.remotes); - var lookup = result.ocs.data.lookup; - var emails = [], - circles = []; - if (typeof(result.ocs.data.emails) !== 'undefined') { - emails = result.ocs.data.exact.emails.concat(result.ocs.data.emails); - } - if (typeof(result.ocs.data.circles) !== 'undefined') { - circles = result.ocs.data.exact.circles.concat(result.ocs.data.circles); - } - - var usersLength; - var groupsLength; - var remotesLength; - var emailsLength; - var circlesLength; - - var i, j; - - //Filter out the current user - usersLength = users.length; - for (i = 0; i < usersLength; i++) { - if (users[i].value.shareWith === OC.currentUser) { - users.splice(i, 1); - break; - } - } - - // Filter out the owner of the share - if (view.model.hasReshare()) { - usersLength = users.length; - for (i = 0 ; i < usersLength; i++) { - if (users[i].value.shareWith === view.model.getReshareOwner()) { - users.splice(i, 1); - break; - } - } - } - - var shares = view.model.get('shares'); - var sharesLength = shares.length; - - // Now filter out all sharees that are already shared with - for (i = 0; i < sharesLength; i++) { - var share = shares[i]; - - if (share.share_type === OC.Share.SHARE_TYPE_USER) { - usersLength = users.length; - for (j = 0; j < usersLength; j++) { - if (users[j].value.shareWith === share.share_with) { - users.splice(j, 1); - break; - } - } - } else if (share.share_type === OC.Share.SHARE_TYPE_GROUP) { - groupsLength = groups.length; - for (j = 0; j < groupsLength; j++) { - if (groups[j].value.shareWith === share.share_with) { - groups.splice(j, 1); - break; - } - } - } else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE) { - remotesLength = remotes.length; - for (j = 0; j < remotesLength; j++) { - if (remotes[j].value.shareWith === share.share_with) { - remotes.splice(j, 1); - break; - } - } - } else if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) { - emailsLength = emails.length; - for (j = 0; j < emailsLength; j++) { - if (emails[j].value.shareWith === share.share_with) { - emails.splice(j, 1); - break; - } - } - } else if (share.share_type === OC.Share.SHARE_TYPE_CIRCLE) { - circlesLength = circles.length; - for (j = 0; j < circlesLength; j++) { - if (circles[j].value.shareWith === share.share_with) { - circles.splice(j, 1); - break; - } - } - } - } - - var suggestions = users.concat(groups).concat(remotes).concat(emails).concat(circles).concat(lookup); + $confirm.removeClass('hidden'); + } - if (suggestions.length > 0) { - $shareWithField - .autocomplete("option", "autoFocus", true); + if (suggestions.length > 0) { + $shareWithField + .autocomplete("option", "autoFocus", true); - response(suggestions); + response(suggestions); - // show a notice that the list is truncated - // this is the case if one of the search results is at least as long as the max result config option - if(oc_config['sharing.maxAutocompleteResults'] > 0 && - Math.min(perPage, oc_config['sharing.maxAutocompleteResults']) - <= Math.max(users.length, groups.length, remotes.length, emails.length, lookup.length)) { + // show a notice that the list is truncated + // this is the case if one of the search results is at least as long as the max result config option + if(oc_config['sharing.maxAutocompleteResults'] > 0 && + Math.min(perPage, oc_config['sharing.maxAutocompleteResults']) + <= Math.max(users.length, groups.length, remotes.length, emails.length, lookup.length)) { - var message = t('core', 'This list is maybe truncated - please refine your search term to see more results.'); - $('.ui-autocomplete').append('<li class="autocomplete-note">' + message + '</li>'); - } + var message = t('core', 'This list is maybe truncated - please refine your search term to see more results.'); + $('.ui-autocomplete').append('<li class="autocomplete-note">' + message + '</li>'); + } - } else { - var title = t('core', 'No users or groups found for {search}', {search: $shareWithField.val()}); - if (!view.configModel.get('allowGroupSharing')) { - title = t('core', 'No users found for {search}', {search: $('.shareWithField').val()}); - } - $shareWithField.addClass('error') - .attr('data-original-title', title) - .tooltip('hide') - .tooltip({ - placement: 'bottom', - trigger: 'manual' - }) - .tooltip('fixTitle') - .tooltip('show'); - response(); - } - } else { - response(); + } else { + var title = t('core', 'No users or groups found for {search}', {search: $shareWithField.val()}); + if (!view.configModel.get('allowGroupSharing')) { + title = t('core', 'No users found for {search}', {search: $('.shareWithField').val()}); } + $shareWithField.addClass('error') + .attr('data-original-title', title) + .tooltip('hide') + .tooltip({ + placement: 'bottom', + trigger: 'manual' + }) + .tooltip('fixTitle') + .tooltip('show'); + response(); + } + }).fail(function(message) { + view._pendingOperationsCount--; + if (view._pendingOperationsCount === 0) { + $loading.addClass('hidden'); + $loading.removeClass('inlineblock'); + $confirm.removeClass('hidden'); + } + + if (message) { + OC.Notification.showTemporary(t('core', 'An error occurred ("{message}"). Please try again', { message: message })); + } else { + OC.Notification.showTemporary(t('core', 'An error occurred. Please try again')); } - ).fail(function() { - $loading.addClass('hidden'); - $loading.removeClass('inlineblock'); - $shareInfo.removeClass('hidden'); - OC.Notification.show(t('core', 'An error occurred. Please try again')); - window.setTimeout(OC.Notification.hide, 5000); }); }, @@ -357,31 +441,149 @@ }, _onSelectRecipient: function(e, s) { + var self = this; + e.preventDefault(); + // Ensure that the keydown handler for the input field is not + // called; otherwise it would try to add the recipient again, which + // would fail. + e.stopImmediatePropagation(); $(e.target).attr('disabled', true) .val(s.item.label); + var $loading = this.$el.find('.shareWithLoading'); - $loading.removeClass('hidden') - .addClass('inlineblock'); - var $shareInfo = this.$el.find('.shareWithRemoteInfo'); - $shareInfo.addClass('hidden'); + var $confirm = this.$el.find('.shareWithConfirm'); + + $loading.removeClass('hidden'); + $loading.addClass('inlineblock'); + $confirm.addClass('hidden'); + this._pendingOperationsCount++; this.model.addShare(s.item.value, {success: function() { + // Adding a share changes the suggestions. + self._lastSuggestions = undefined; + $(e.target).val('') .attr('disabled', false); - $loading.addClass('hidden') - .removeClass('inlineblock'); - $shareInfo.removeClass('hidden'); + + self._pendingOperationsCount--; + if (self._pendingOperationsCount === 0) { + $loading.addClass('hidden'); + $loading.removeClass('inlineblock'); + $confirm.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'); - $shareInfo.removeClass('hidden'); + + self._pendingOperationsCount--; + if (self._pendingOperationsCount === 0) { + $loading.addClass('hidden'); + $loading.removeClass('inlineblock'); + $confirm.removeClass('hidden'); + } }}); }, + _confirmShare: function() { + var self = this; + var $shareWithField = $('.shareWithField'); + var $loading = this.$el.find('.shareWithLoading'); + var $confirm = this.$el.find('.shareWithConfirm'); + + $loading.removeClass('hidden'); + $loading.addClass('inlineblock'); + $confirm.addClass('hidden'); + this._pendingOperationsCount++; + + $shareWithField.prop('disabled', true); + + // Disabling the autocompletion does not clear its search timeout; + // removing the focus from the input field does, but only if the + // autocompletion is not disabled when the field loses the focus. + // Thus, the field has to be disabled before disabling the + // autocompletion to prevent an old pending search result from + // appearing once the field is enabled again. + $shareWithField.autocomplete('close'); + $shareWithField.autocomplete('disable'); + + var restoreUI = function() { + self._pendingOperationsCount--; + if (self._pendingOperationsCount === 0) { + $loading.addClass('hidden'); + $loading.removeClass('inlineblock'); + $confirm.removeClass('hidden'); + } + + $shareWithField.prop('disabled', false); + $shareWithField.focus(); + }; + + var perPage = 200; + var onlyExactMatches = true; + this._getSuggestions( + $shareWithField.val(), + perPage, + this.model, + onlyExactMatches + ).done(function(suggestions, exactMatches) { + if (suggestions.length === 0) { + restoreUI(); + + $shareWithField.autocomplete('enable'); + + // There is no need to show an error message here; it will + // be automatically shown when the autocomplete is activated + // again (due to the focus on the field) and it finds no + // matches. + + return; + } + + if (exactMatches.length !== 1) { + restoreUI(); + + $shareWithField.autocomplete('enable'); + + return; + } + + var actionSuccess = function() { + // Adding a share changes the suggestions. + self._lastSuggestions = undefined; + + $shareWithField.val(''); + + restoreUI(); + + $shareWithField.autocomplete('enable'); + }; + + var actionError = function(obj, msg) { + restoreUI(); + + $shareWithField.autocomplete('enable'); + + OC.Notification.showTemporary(msg); + }; + + self.model.addShare(exactMatches[0].value, { + success: actionSuccess, + error: actionError + }); + }).fail(function(message) { + restoreUI(); + + $shareWithField.autocomplete('enable'); + + // There is no need to show an error message here; it will be + // automatically shown when the autocomplete is activated again + // (due to the focus on the field) and getting the suggestions + // fail. + }); + }, + _toggleLoading: function(state) { this._loading = state; this.$el.find('.subView').toggleClass('hidden', state); @@ -410,18 +612,28 @@ }, render: function() { + var self = this; var baseTemplate = this._getTemplate('base', TEMPLATE_BASE); this.$el.html(baseTemplate({ cid: this.cid, shareLabel: t('core', 'Share'), sharePlaceholder: this._renderSharePlaceholderPart(), - shareInfo: this._renderShareInfoPart(), isSharingAllowed: this.model.sharePermissionPossible() })); var $shareField = this.$el.find('.shareWithField'); if ($shareField.length) { + var shareFieldKeydownHandler = function(event) { + if (event.keyCode !== 13) { + return true; + } + + self._confirmShare(); + + return false; + }; + $shareField.autocomplete({ minLength: 1, delay: 750, @@ -431,6 +643,8 @@ source: this.autocompleteHandler, select: this._onSelectRecipient }).data('ui-autocomplete')._renderItem = this.autocompleteRenderItem; + + $shareField.on('keydown', null, shareFieldKeydownHandler); } this.resharerInfoView.$el = this.$el.find('.resharerInfoView'); @@ -461,27 +675,6 @@ this.linkShareView.showLink = this._showLink; }, - _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 shareInfo; - }, - _renderSharePlaceholderPart: function () { var allowRemoteSharing = this.configModel.get('isRemoteShareAllowed'); var allowMailSharing = this.configModel.get('isMailShareAllowed'); @@ -513,15 +706,6 @@ return this._templates[key]; }, - /** - * returns the info template for remote sharing - * - * @returns {Function} - * @private - */ - _getShareInfoTemplate: function() { - return this._getTemplate('shareInfo', TEMPLATE_SHARE_INFO); - } }); OC.Share.ShareDialogView = ShareDialogView; diff --git a/core/js/tests/specs/sharedialogviewSpec.js b/core/js/tests/specs/sharedialogviewSpec.js index c6d5793623c..d3639159849 100644 --- a/core/js/tests/specs/sharedialogviewSpec.js +++ b/core/js/tests/specs/sharedialogviewSpec.js @@ -30,7 +30,6 @@ describe('OC.Share.ShareDialogView', function() { var saveLinkShareStub; var fetchStub; - var notificationStub; var configModel; var shareModel; @@ -472,54 +471,728 @@ describe('OC.Share.ShareDialogView', function() { }); }); }); - describe('remote sharing', function() { - it('shows remote share info when allowed', function() { - configModel.set({ - isRemoteShareAllowed: true + describe('get suggestions', function() { + it('no matches', function() { + var doneStub = sinon.stub(); + var failStub = sinon.stub(); + + dialog._getSuggestions('bob', 42, shareModel).done(doneStub).fail(failStub); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } }); - dialog.render(); - expect(dialog.$el.find('.shareWithRemoteInfo').length).toEqual(1); + + expect(doneStub.called).toEqual(false); + expect(failStub.called).toEqual(false); + + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(doneStub.calledOnce).toEqual(true); + expect(doneStub.calledWithExactly([], [])).toEqual(true); + expect(failStub.called).toEqual(false); }); - it('does not show remote share info when not allowed', function() { - configModel.set({ - isRemoteShareAllowed: false + it('single partial match', function() { + var doneStub = sinon.stub(); + var failStub = sinon.stub(); + + dialog._getSuggestions('bob', 42, shareModel).done(doneStub).fail(failStub); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [] + }, + 'users': [ + { + 'label': 'bobby', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'imbob' + } + } + ], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } }); - dialog.render(); - expect(dialog.$el.find('.shareWithRemoteInfo').length).toEqual(0); + + expect(doneStub.called).toEqual(false); + expect(failStub.called).toEqual(false); + + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(doneStub.calledOnce).toEqual(true); + expect(doneStub.calledWithExactly([{ + 'label': 'bobby', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'imbob'} + }], [ + ])).toEqual(true); + expect(failStub.called).toEqual(false); }); - }); - describe('autocompletion of users', function() { - it('triggers autocomplete display and focus with data when ajax search succeeds', function () { - dialog.render(); - var response = sinon.stub(); - dialog.autocompleteHandler({term: 'bob'}, response); + it('single exact match', function() { + var doneStub = sinon.stub(); + var failStub = sinon.stub(); + + dialog._getSuggestions('bob', 42, shareModel).done(doneStub).fail(failStub); + var jsonData = JSON.stringify({ - 'ocs' : { - 'meta' : { - 'status' : 'success', - 'statuscode' : 100, - 'message' : null + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user1' + } + } + ], + 'groups': [], + 'remotes': [] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } + }); + + expect(doneStub.called).toEqual(false); + expect(failStub.called).toEqual(false); + + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(doneStub.calledOnce).toEqual(true); + expect(doneStub.calledWithExactly([{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }], [{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }])).toEqual(true); + expect(failStub.called).toEqual(false); + }); + it('mixed matches', function() { + var doneStub = sinon.stub(); + var failStub = sinon.stub(); + + dialog._getSuggestions('bob', 42, shareModel).done(doneStub).fail(failStub); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user1' + } + } + ], + 'groups': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_GROUP, + 'shareWith': 'group1' + } + } + ], + 'remotes': [] + }, + 'users': [ + { + 'label': 'bobby', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'imbob' + } + }, + { + 'label': 'bob the second', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user2' + } + } + ], + 'groups': [ + { + 'label': 'bobfans', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_GROUP, + 'shareWith': 'fans' + } + } + ], + 'remotes': [], + 'lookup': [] + } + } + }); + + expect(doneStub.called).toEqual(false); + expect(failStub.called).toEqual(false); + + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(doneStub.calledOnce).toEqual(true); + expect(doneStub.calledWithExactly([{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }, { + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_GROUP, 'shareWith': 'group1'} + }, { + 'label': 'bobby', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'imbob'} + }, { + 'label': 'bob the second', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user2'} + }, { + 'label': 'bobfans', + 'value': {'shareType': OC.Share.SHARE_TYPE_GROUP, 'shareWith': 'fans'} + }], [{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }, { + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_GROUP, 'shareWith': 'group1'} + }])).toEqual(true); + expect(failStub.called).toEqual(false); + }); + + it('does not send a request to the server again for the same parameters', function() { + var doneStub = sinon.stub(); + var failStub = sinon.stub(); + + dialog._getSuggestions('bob', 42, shareModel).done(doneStub).fail(failStub); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null }, - 'data' : { - 'exact' : { - 'users' : [], - 'groups' : [], + 'data': { + 'exact': { + 'users': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user1' + } + } + ], + 'groups': [], 'remotes': [] }, - 'users' : [{'label': 'bob', 'value': {'shareType': 0, 'shareWith': 'test'}}], - 'groups' : [], + 'users': [], + 'groups': [], 'remotes': [], 'lookup': [] } } }); + + expect(doneStub.called).toEqual(false); + expect(failStub.called).toEqual(false); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(doneStub.calledOnce).toEqual(true); + expect(doneStub.calledWithExactly([{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }], [{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }])).toEqual(true); + expect(failStub.called).toEqual(false); + + var done2Stub = sinon.stub(); + var fail2Stub = sinon.stub(); + + dialog._getSuggestions('bob', 42, shareModel).done(done2Stub).fail(fail2Stub); + + expect(doneStub.calledOnce).toEqual(true); + expect(failStub.called).toEqual(false); + + expect(done2Stub.calledOnce).toEqual(true); + expect(done2Stub.calledWithExactly([{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }], [{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }])).toEqual(true); + expect(fail2Stub.called).toEqual(false); + }); + + it('sends a request to the server again for the same parameters if the calls are not consecutive', function() { + var doneStub = sinon.stub(); + var failStub = sinon.stub(); + + dialog._getSuggestions('bob', 42, shareModel).done(doneStub).fail(failStub); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user1' + } + } + ], + 'groups': [], + 'remotes': [] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } + }); + + expect(doneStub.called).toEqual(false); + expect(failStub.called).toEqual(false); + + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(doneStub.calledOnce).toEqual(true); + expect(doneStub.calledWithExactly([{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }], [{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }])).toEqual(true); + expect(failStub.called).toEqual(false); + + var done2Stub = sinon.stub(); + var fail2Stub = sinon.stub(); + + dialog._getSuggestions('bob', 108, shareModel).done(done2Stub).fail(fail2Stub); + + expect(done2Stub.called).toEqual(false); + expect(fail2Stub.called).toEqual(false); + + fakeServer.requests[1].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(done2Stub.calledOnce).toEqual(true); + expect(fail2Stub.called).toEqual(false); + + var done3Stub = sinon.stub(); + var fail3Stub = sinon.stub(); + + dialog._getSuggestions('bob', 42, shareModel).done(done3Stub).fail(fail3Stub); + + expect(done3Stub.called).toEqual(false); + expect(fail3Stub.called).toEqual(false); + + fakeServer.requests[2].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(doneStub.calledOnce).toEqual(true); + expect(failStub.called).toEqual(false); + expect(done2Stub.calledOnce).toEqual(true); + expect(fail2Stub.called).toEqual(false); + + expect(done3Stub.calledOnce).toEqual(true); + expect(done3Stub.calledWithExactly([{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }], [{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }])).toEqual(true); + expect(fail3Stub.called).toEqual(false); + }); + }); + describe('autocompletion of users', function() { + var showTemporaryNotificationStub; + + beforeEach(function() { + showTemporaryNotificationStub = sinon.stub(OC.Notification, 'show'); + }); + + afterEach(function() { + showTemporaryNotificationStub.restore(); + }); + + describe('triggers autocomplete display and focus with data when ajax search succeeds', function () { + it('users', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'bob'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user1' + } + } + ], + 'groups': [], + 'remotes': [] + }, + 'users': [ + { + 'label': 'bobby', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'imbob' + } + } + ], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( 200, {'Content-Type': 'application/json'}, jsonData - ); - expect(response.calledWithExactly(JSON.parse(jsonData).ocs.data.users)).toEqual(true); - expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + ); + expect(response.calledWithExactly([{ + 'label': 'bob', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'user1'} + }, { + 'label': 'bobby', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'imbob'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); + + it('groups', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'group'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [ + { + 'label': 'group', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_GROUP, + 'shareWith': 'group' + } + } + ], + 'remotes': [] + }, + 'users': [], + 'groups': [ + { + 'label': 'group2', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_GROUP, + 'shareWith': 'group2' + } + } + ], + 'remotes': [], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'group', + 'value': {'shareType': OC.Share.SHARE_TYPE_GROUP, 'shareWith': 'group'} + }, { + 'label': 'group2', + 'value': {'shareType': OC.Share.SHARE_TYPE_GROUP, 'shareWith': 'group2'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); + + it('remotes', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'foo@bar.com/baz'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [ + { + 'label': 'foo@bar.com/baz', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_REMOTE, + 'shareWith': 'foo@bar.com/baz' + } + } + ] + }, + 'users': [], + 'groups': [], + 'remotes': [ + { + 'label': 'foo@bar.com/baz2', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_REMOTE, + 'shareWith': 'foo@bar.com/baz2' + } + } + ], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'foo@bar.com/baz', + 'value': {'shareType': OC.Share.SHARE_TYPE_REMOTE, 'shareWith': 'foo@bar.com/baz'} + }, { + 'label': 'foo@bar.com/baz2', + 'value': {'shareType': OC.Share.SHARE_TYPE_REMOTE, 'shareWith': 'foo@bar.com/baz2'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); + + it('emails', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'foo@bar.com'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [], + 'emails': [ + { + 'label': 'foo@bar.com', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_EMAIL, + 'shareWith': 'foo@bar.com' + } + } + ] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [], + 'emails': [ + { + 'label': 'foo@bar.com2', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_EMAIL, + 'shareWith': 'foo@bar.com2' + } + } + ] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'foo@bar.com', + 'value': {'shareType': OC.Share.SHARE_TYPE_EMAIL, 'shareWith': 'foo@bar.com'} + }, { + 'label': 'foo@bar.com2', + 'value': {'shareType': OC.Share.SHARE_TYPE_EMAIL, 'shareWith': 'foo@bar.com2'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); + + it('circles', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'CircleName'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [], + 'circles': [ + { + 'label': 'CircleName (type, owner)', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_CIRCLE, + 'shareWith': 'shortId' + } + }, + { + 'label': 'CircleName (type2, owner)', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_CIRCLE, + 'shareWith': 'shortId2' + } + } + ] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [], + 'circles': [ + { + 'label': 'CircleName2 (type, owner)', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_CIRCLE, + 'shareWith': 'shortId3' + } + } + ] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'CircleName (type, owner)', + 'value': {'shareType': OC.Share.SHARE_TYPE_CIRCLE, 'shareWith': 'shortId'} + }, { + 'label': 'CircleName (type2, owner)', + 'value': {'shareType': OC.Share.SHARE_TYPE_CIRCLE, 'shareWith': 'shortId2'} + }, { + 'label': 'CircleName2 (type, owner)', + 'value': {'shareType': OC.Share.SHARE_TYPE_CIRCLE, 'shareWith': 'shortId3'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); }); describe('filter out', function() { @@ -658,7 +1331,20 @@ describe('OC.Share.ShareDialogView', function() { share_type: OC.Share.SHARE_TYPE_REMOTE, share_with: 'foo@bar.com/baz', share_with_displayname: 'foo@bar.com/baz' - + },{ + id: 103, + item_source: 123, + permissions: 31, + share_type: OC.Share.SHARE_TYPE_EMAIL, + share_with: 'foo@bar.com', + share_with_displayname: 'foo@bar.com' + },{ + id: 104, + item_source: 123, + permissions: 31, + share_type: OC.Share.SHARE_TYPE_CIRCLE, + share_with: 'shortId', + share_with_displayname: 'CircleName (type, owner)' }] }); }); @@ -666,7 +1352,7 @@ describe('OC.Share.ShareDialogView', function() { it('users', function () { dialog.render(); var response = sinon.stub(); - dialog.autocompleteHandler({term: 'bob'}, response); + dialog.autocompleteHandler({term: 'bo'}, response); var jsonData = JSON.stringify({ 'ocs': { 'meta': { @@ -714,10 +1400,62 @@ describe('OC.Share.ShareDialogView', function() { expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); }); + it('users (exact)', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'bob'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user1' + } + } + ], + 'groups': [], + 'remotes': [] + }, + 'users': [ + { + 'label': 'bobby', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'imbob' + } + } + ], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'bobby', + 'value': {'shareType': OC.Share.SHARE_TYPE_USER, 'shareWith': 'imbob'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); + it('groups', function () { dialog.render(); var response = sinon.stub(); - dialog.autocompleteHandler({term: 'group'}, response); + dialog.autocompleteHandler({term: 'grou'}, response); var jsonData = JSON.stringify({ 'ocs': { 'meta': { @@ -765,10 +1503,62 @@ describe('OC.Share.ShareDialogView', function() { expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); }); + it('groups (exact)', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'group'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [ + { + 'label': 'group', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_GROUP, + 'shareWith': 'group' + } + } + ], + 'remotes': [] + }, + 'users': [], + 'groups': [ + { + 'label': 'group2', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_GROUP, + 'shareWith': 'group2' + } + } + ], + 'remotes': [], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'group2', + 'value': {'shareType': OC.Share.SHARE_TYPE_GROUP, 'shareWith': 'group2'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); + it('remotes', function () { dialog.render(); var response = sinon.stub(); - dialog.autocompleteHandler({term: 'bob'}, response); + dialog.autocompleteHandler({term: 'foo'}, response); var jsonData = JSON.stringify({ 'ocs': { 'meta': { @@ -815,10 +1605,286 @@ describe('OC.Share.ShareDialogView', function() { }])).toEqual(true); expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); }); + + it('remotes (exact)', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'foo@bar.com/baz'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [ + { + 'label': 'foo@bar.com/baz', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_REMOTE, + 'shareWith': 'foo@bar.com/baz' + } + } + ] + }, + 'users': [], + 'groups': [], + 'remotes': [ + { + 'label': 'foo@bar.com/baz2', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_REMOTE, + 'shareWith': 'foo@bar.com/baz2' + } + } + ], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'foo@bar.com/baz2', + 'value': {'shareType': OC.Share.SHARE_TYPE_REMOTE, 'shareWith': 'foo@bar.com/baz2'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); + + it('emails', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'foo'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [], + 'emails': [] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [], + 'emails': [ + { + 'label': 'foo@bar.com', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_EMAIL, + 'shareWith': 'foo@bar.com' + } + }, + { + 'label': 'foo2@bar.com', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_EMAIL, + 'shareWith': 'foo2@bar.com' + } + } + ] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'foo2@bar.com', + 'value': {'shareType': OC.Share.SHARE_TYPE_EMAIL, 'shareWith': 'foo2@bar.com'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); + + it('emails (exact)', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'foo@bar.com'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [], + 'emails': [ + { + 'label': 'foo@bar.com', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_EMAIL, + 'shareWith': 'foo@bar.com' + } + } + ] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [], + 'emails': [ + { + 'label': 'foo@bar.com2', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_EMAIL, + 'shareWith': 'foo@bar.com2' + } + } + ] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'foo@bar.com2', + 'value': {'shareType': OC.Share.SHARE_TYPE_EMAIL, 'shareWith': 'foo@bar.com2'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); + + it('circles', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'CircleNam'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [], + 'circles': [] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [], + 'circles': [ + { + 'label': 'CircleName (type, owner)', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_CIRCLE, + 'shareWith': 'shortId' + } + }, + { + 'label': 'CircleName (type2, owner)', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_CIRCLE, + 'shareWith': 'shortId2' + } + } + ] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'CircleName (type2, owner)', + 'value': {'shareType': OC.Share.SHARE_TYPE_CIRCLE, 'shareWith': 'shortId2'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); + + it('circles (exact)', function () { + dialog.render(); + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'CircleName'}, response); + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [], + 'circles': [ + { + 'label': 'CircleName (type, owner)', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_CIRCLE, + 'shareWith': 'shortId' + } + }, + { + 'label': 'CircleName (type2, owner)', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_CIRCLE, + 'shareWith': 'shortId2' + } + } + ] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [], + 'circles': [ + { + 'label': 'CircleName2 (type, owner)', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_CIRCLE, + 'shareWith': 'shortId3' + } + } + ] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + expect(response.calledWithExactly([{ + 'label': 'CircleName (type2, owner)', + 'value': {'shareType': OC.Share.SHARE_TYPE_CIRCLE, 'shareWith': 'shortId2'} + }, { + 'label': 'CircleName2 (type, owner)', + 'value': {'shareType': OC.Share.SHARE_TYPE_CIRCLE, 'shareWith': 'shortId3'} + }])).toEqual(true); + expect(autocompleteStub.calledWith("option", "autoFocus", true)).toEqual(true); + }); }); }); - it('gracefully handles successful ajax call with failure content', function () { + it('throws a notification for a successful ajax call with failure content', function () { dialog.render(); var response = sinon.stub(); dialog.autocompleteHandler({term: 'bob'}, response); @@ -826,7 +1892,8 @@ describe('OC.Share.ShareDialogView', function() { 'ocs' : { 'meta' : { 'status': 'failure', - 'statuscode': 400 + 'statuscode': 400, + 'message': 'error message' } } }); @@ -835,16 +1902,16 @@ describe('OC.Share.ShareDialogView', function() { {'Content-Type': 'application/json'}, jsonData ); - expect(response.calledWithExactly()).toEqual(true); + expect(response.called).toEqual(false); + expect(showTemporaryNotificationStub.calledOnce).toEqual(true); + expect(showTemporaryNotificationStub.firstCall.args[0]).toContain('error message'); }); it('throws a notification when the ajax search lookup fails', function () { - notificationStub = sinon.stub(OC.Notification, 'show'); dialog.render(); dialog.autocompleteHandler({term: 'bob'}, sinon.stub()); fakeServer.requests[0].respond(500); - expect(notificationStub.calledOnce).toEqual(true); - notificationStub.restore(); + expect(showTemporaryNotificationStub.calledOnce).toEqual(true); }); describe('renders the autocomplete elements', function() { @@ -942,6 +2009,425 @@ describe('OC.Share.ShareDialogView', function() { addShareStub.restore(); }); + + it('hides the loading icon when all the pending operations finish', function() { + dialog.render(); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(true); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(false); + + var response = sinon.stub(); + dialog.autocompleteHandler({term: 'bob'}, response); + dialog.autocompleteHandler({term: 'bobby'}, response); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } + }); + + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(false); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(true); + + fakeServer.requests[1].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(true); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(false); + }); + }); + describe('confirm share', function() { + var addShareStub; + var tooltipStub; + var showTemporaryNotificationStub; + + beforeEach(function() { + addShareStub = sinon.stub(shareModel, 'addShare'); + + tooltipStub = sinon.stub($.fn, 'tooltip').callsFake(function() { + return $('<div></div>'); + }); + + showTemporaryNotificationStub = sinon.stub(OC.Notification, 'showTemporary'); + + dialog.render(); + }); + + afterEach(function() { + addShareStub.restore(); + tooltipStub.restore(); + showTemporaryNotificationStub.restore(); + }); + + it('sets the appropriate UI state while waiting to get the suggestions', function() { + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(true); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(false); + expect(autocompleteStub.callCount).toEqual(1); + expect(typeof autocompleteStub.firstCall.args[0]).toEqual('object'); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(false); + + dialog.$el.find('.shareWithField').val('bob'); + + dialog._confirmShare(); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(false); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(true); + expect(autocompleteStub.lastCall.args[0]).toEqual('disable'); + expect(autocompleteStub.calledWith('close')).toEqual(true); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(true); + expect(dialog.$el.find('.shareWithField').val()).toEqual('bob'); + }); + + it('calls addShare with the only suggestion', function() { + dialog.$el.find('.shareWithField').val('bob'); + + dialog._confirmShare(); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user1' + } + } + ], + 'groups': [], + 'remotes': [] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + // Ensure that the UI is not restored before adding the share + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(false); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(true); + expect(autocompleteStub.lastCall.args[0]).toEqual('disable'); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(true); + expect(dialog.$el.find('.shareWithField').val()).toEqual('bob'); + + expect(addShareStub.calledOnce).toEqual(true); + expect(addShareStub.firstCall.args[0]).toEqual({ + shareType: OC.Share.SHARE_TYPE_USER, + shareWith: 'user1' + }); + + // "yield" and "callArg" from SinonJS can not be used, as the + // callback is a property not in the first argument. + addShareStub.firstCall.args[1]['success'].apply(shareModel); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(true); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(false); + expect(autocompleteStub.lastCall.args[0]).toEqual('enable'); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(false); + expect(dialog.$el.find('.shareWithField').val()).toEqual(''); + }); + + it('handles a failure to share', function() { + expect(showTemporaryNotificationStub.called).toEqual(false); + + dialog.$el.find('.shareWithField').val('bob'); + + dialog._confirmShare(); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user1' + } + } + ], + 'groups': [], + 'remotes': [] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + // Ensure that the UI is not restored before adding the share + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(false); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(true); + expect(autocompleteStub.lastCall.args[0]).toEqual('disable'); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(true); + expect(dialog.$el.find('.shareWithField').val()).toEqual('bob'); + + expect(addShareStub.calledOnce).toEqual(true); + expect(addShareStub.firstCall.args[0]).toEqual({ + shareType: OC.Share.SHARE_TYPE_USER, + shareWith: 'user1' + }); + + // "yield" and "callArg" from SinonJS can not be used, as the + // callback is a property not in the first argument. + addShareStub.firstCall.args[1]['error'].apply(shareModel); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(true); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(false); + expect(autocompleteStub.lastCall.args[0]).toEqual('enable'); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(false); + expect(dialog.$el.find('.shareWithField').val()).toEqual('bob'); + + expect(showTemporaryNotificationStub.calledOnce).toEqual(true); + }); + + it('restores UI if there are no matches at all', function() { + dialog.$el.find('.shareWithField').val('bob'); + + dialog._confirmShare(); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(addShareStub.called).toEqual(false); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(true); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(false); + expect(autocompleteStub.lastCall.args[0]).toEqual('enable'); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(false); + expect(dialog.$el.find('.shareWithField').val()).toEqual('bob'); + + // No explicit tooltip is shown; it is automatically shown when the + // autocomplete is activated again and it finds no matches. + expect(tooltipStub.lastCall.args[0]).not.toEqual('show'); + }); + + it('shows tooltip if there are matches but no exact matches', function() { + dialog.$el.find('.shareWithField').val('bo'); + + dialog._confirmShare(); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [], + 'groups': [], + 'remotes': [] + }, + 'users': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user1' + } + } + ], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(addShareStub.called).toEqual(false); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(true); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(false); + expect(autocompleteStub.lastCall.args[0]).toEqual('enable'); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(false); + expect(dialog.$el.find('.shareWithField').val()).toEqual('bo'); + }); + + it('shows tooltip if there is more than one exact match', function() { + dialog.$el.find('.shareWithField').val('bob'); + + dialog._confirmShare(); + + var jsonData = JSON.stringify({ + 'ocs': { + 'meta': { + 'status': 'success', + 'statuscode': 100, + 'message': null + }, + 'data': { + 'exact': { + 'users': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_USER, + 'shareWith': 'user1' + } + } + ], + 'groups': [ + { + 'label': 'bob', + 'value': { + 'shareType': OC.Share.SHARE_TYPE_GROUP, + 'shareWith': 'group1' + } + } + ], + 'remotes': [] + }, + 'users': [], + 'groups': [], + 'remotes': [], + 'lookup': [] + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(addShareStub.called).toEqual(false); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(true); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(false); + expect(autocompleteStub.lastCall.args[0]).toEqual('enable'); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(false); + expect(dialog.$el.find('.shareWithField').val()).toEqual('bob'); + }); + + it('throws a notification for a successful ajax call with failure content', function () { + dialog.$el.find('.shareWithField').val('bob'); + + dialog._confirmShare(); + + var jsonData = JSON.stringify({ + 'ocs' : { + 'meta' : { + 'status': 'failure', + 'statuscode': 400, + 'message': 'error message' + } + } + }); + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'application/json'}, + jsonData + ); + + expect(addShareStub.called).toEqual(false); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(true); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(false); + expect(autocompleteStub.lastCall.args[0]).toEqual('enable'); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(false); + expect(dialog.$el.find('.shareWithField').val()).toEqual('bob'); + + expect(showTemporaryNotificationStub.called).toEqual(false); + }); + + it('throws a notification when the ajax search lookup fails', function () { + dialog.$el.find('.shareWithField').val('bob'); + + dialog._confirmShare(); + + fakeServer.requests[0].respond(500); + + expect(addShareStub.called).toEqual(false); + + expect(dialog.$el.find('.shareWithLoading').hasClass('hidden')).toEqual(true); + expect(dialog.$el.find('.shareWithConfirm').hasClass('hidden')).toEqual(false); + expect(autocompleteStub.lastCall.args[0]).toEqual('enable'); + expect(dialog.$el.find('.shareWithField').prop('disabled')).toEqual(false); + expect(dialog.$el.find('.shareWithField').val()).toEqual('bob'); + + expect(showTemporaryNotificationStub.called).toEqual(false); + }); }); describe('reshare permissions', function() { it('does not show sharing options when sharing not allowed', function() { |