core/js/sharedialogview.js
/*
* Copyright (c) 2015
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
if(!OC.Share) {
OC.Share = {};
}
var TEMPLATE_BASE =
'<div class="resharerInfoView subView"></div>' +
'{{#if isSharingAllowed}}' +
'{{#if isLinkSharingAllowed}}' +
'<ul class="subTabHeaders">' +
' <li class="subTabHeader selected subtab-localshare">{{localSharesLabel}}</li>' +
' <li class="subTabHeader subtab-publicshare">{{publicSharesLabel}}</li>' +
'</ul>' +
'{{/if}}' +
'<div class="tabsContainer">' +
// TODO: this really should be a separate view class
' <div class="localShareView tab" style="padding-left:0;padding-right:0;">' +
' <label for="shareWith-{{cid}}" class="hidden-visually">{{shareLabel}}</label>' +
' <div class="oneline">' +
' <input id="shareWith-{{cid}}" class="shareWithField" type="text" placeholder="{{sharePlaceholder}}" />' +
' <span class="shareWithLoading icon-loading-small hidden"></span>'+
' {{{remoteShareInfo}}}' +
' </div>' +
' <div class="shareeListView subView"></div>' +
' <div class="shareeTreeUserGroupListView subView"></div>' +
' </div>' +
' <div class="linkShareView tab hidden" style="padding-left:0;padding-right:0;">' +
' <div class="linkListView"></div>' +
' <div class="shareeTreeLinkListView subView"></div>' +
' </div>' +
'</div>' +
'{{else}}' +
'<div class="noSharingPlaceholder">{{noSharingPlaceholder}}</div>' +
'{{/if}}' +
'<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_AUTOCOMPLETE_ITEM =
'<li class="{{shareTypeClass}}">' +
'<a>' +
'<div class="share-autocomplete-item">' +
'{{#if showAvatar}}' +
'<div class="avatardiv"></div>' +
'{{/if}}' +
'<div class="autocomplete-item-text">' +
'<span class="autocomplete-item-displayname">{{displayName}}</span>' +
'{{#if additionalInfo}}' +
'<br/><span class="autocomplete-item-additional-info">({{additionalInfo}})</span>' +
'{{/if}}' +
'<br/><span class="autocomplete-item-typeInfo">{{typeInfo}}</span>' +
'</div>' +
'{{#if showIcon}}' +
'<span class="icon {{iconClass}}"></span>' +
'{{/if}}' +
'</div>' +
'</a>' +
'</li>';
/**
* @class OCA.Share.ShareDialogView
* @member {OC.Share.ShareItemModel} model
* @member {jQuery} $el
* @memberof OCA.Sharing
* @classdesc
*
* Represents the GUI of the share dialogue
*
*/
var ShareDialogView = OC.Backbone.View.extend({
/** @type {Object} **/
_templates: {},
/** @type {string} **/
tagName: 'div',
/** @type {OC.Share.ShareConfigModel} **/
configModel: undefined,
/** @type {object} **/
resharerInfoView: undefined,
/** @type {object} **/
linkShareView: undefined,
/** @type {object} **/
shareeListView: undefined,
/** @type {string} **/
batchActionSeparator: ',',
events: {
'input .shareWithField': 'onShareWithFieldChanged',
'click .subTabHeader': '_onClickTabHeader'
},
initialize: function(options) {
var view = this;
this.model.on('fetchError', function() {
OC.Notification.showTemporary(t('core', 'Share details could not be loaded for this item.'));
});
if(!_.isUndefined(options.configModel)) {
this.configModel = options.configModel;
} else {
throw 'missing OC.Share.ShareConfigModel';
}
this.configModel.on('change:isRemoteShareAllowed', function() {
view.render();
});
this.model.on('change:permissions', function() {
view.render();
});
this.model.on('request', this._onRequest, this);
this.model.on('sync', this._onEndRequest, this);
var subViewOptions = {
model: this.model,
configModel: this.configModel
};
var subViews = {
resharerInfoView: 'ShareDialogResharerInfoView',
shareeListView: 'ShareDialogShareeListView'
};
for(var name in subViews) {
var className = subViews[name];
this[name] = _.isUndefined(options[name])
? new OC.Share[className](subViewOptions)
: options[name];
}
this.linkShareView = null;
_.bindAll(this,
'autocompleteHandler',
'_onSelectRecipient',
'onShareWithFieldChanged'
);
OC.Plugins.attach('OC.Share.ShareDialogView', this);
},
_onClickTabHeader: function(ev) {
var $target = $(ev.target);
this.$('.subTabHeaders .subTabHeader.selected').removeClass('selected');
$target.addClass('selected');
this.$('.localShareView').toggleClass('hidden', !$target.hasClass('subtab-localshare'));
var $linkShareView = this.$('.linkShareView');
var $linkListView = this.$('.linkListView');
if ($linkShareView.length) {
$linkShareView.toggleClass('hidden', !$target.hasClass('subtab-publicshare'));
if (!this.linkShareView) {
this.linkShareView = new OC.Share.ShareDialogLinkListView({
collection: this.model.getLinkSharesCollection(),
// pass in the legacy stuff...
itemModel: this.model
});
this.linkShareView.render();
$linkListView.html(this.linkShareView.$el);
}
}
},
onShareWithFieldChanged: function() {
var $el = this.$el.find('.shareWithField');
if ($el.val().length < 2) {
$el.removeClass('error').tooltip('hide');
}
},
autocompleteHandler: function (search, response) {
var view = this;
var $loading = this.$el.find('.shareWithLoading');
var trimmedSearch = search.term.trim();
$loading.removeClass('hidden');
$loading.addClass('inlineblock');
$.get(
OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees',
{
format: 'json',
search: trimmedSearch,
perPage: 200,
itemType: view.model.get('itemType')
},
function (result) {
$loading.addClass('hidden');
$loading.removeClass('inlineblock');
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 usersLength;
var groupsLength;
var remotesLength;
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;
}
}
}
}
var isBatch = trimmedSearch.indexOf(view.batchActionSeparator) !== -1;
var suggestions = users.concat(groups);
if (suggestions.length < 1 && !isBatch) {
suggestions = suggestions.concat(remotes);
}
if (isBatch) {
return view._getUsersForBatchAction(trimmedSearch).then(function (res) {
if (res.found.length) {
//Filter out the current user
for (i = 0; i < res.found.length; i++) {
if (res.found[i].shareWith === OC.currentUser) {
res.found.splice(i, 1);
continue;
}
}
}
if (res.found.length) {
// Build the label for the list of users to be batch-added
var labelArray = [];
for (i = 0; i < res.found.length; i++) {
labelArray.push(res.found[i].shareWith)
}
suggestions.push({
batch: res.found,
failedBatch: res.notFound,
label: labelArray.join(', '),
value: {}
});
}
$loading.addClass('hidden');
$loading.removeClass('inlineblock');
if (suggestions.length) {
$('.shareWithField').removeClass('error')
.tooltip('hide')
.autocomplete("option", "autoFocus", true);
return response(suggestions, result);
}
view._displayError(t('core', 'No users or groups found'));
return response(undefined, result);
})
}
if (suggestions.length > 0) {
suggestions.sort(function (a, b) {
return OC.Util.naturalSortCompare(a.label, b.label);
});
$('.shareWithField').removeClass('error')
.tooltip('hide')
.autocomplete("option", "autoFocus", true);
response(suggestions, result);
} 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()});
}
var suggestStarts = OC.getCapabilities().files_sharing.search_min_length;
if (suggestStarts > $('.shareWithField').val().length) {
title = title + '. ' + n(
'core',
'Please enter at least {chars} character for suggestions',
'Please enter at least {chars} characters for suggestions',
suggestStarts,
{chars: suggestStarts}
);
}
view._displayError(title);
response(undefined, result);
}
} else {
response(undefined, result);
}
}
).fail(function() {
$loading.addClass('hidden');
$loading.removeClass('inlineblock');
OC.Notification.show(t('core', 'An error occurred. Please try again'));
window.setTimeout(OC.Notification.hide, 5000);
});
},
autocompleteRenderItem: function(ul, item) {
var text = item.label;
var showIcon = false;
var iconClass = "";
var typeInfo = t('core', 'User');
if (item.batch) {
typeInfo = this._getBatchActionLabel();
}
if (item.value.shareType === OC.Share.SHARE_TYPE_GROUP) {
typeInfo = t('core', 'Group');
}
if (item.value.shareType === OC.Share.SHARE_TYPE_REMOTE_GROUP) {
typeInfo = t('core', 'Federated Group');
showIcon = true
iconClass = "icon-contacts-dark"
}
if (item.value.shareType === OC.Share.SHARE_TYPE_GUEST) {
typeInfo = t('core', 'Guest');
}
if (item.value.shareType === OC.Share.SHARE_TYPE_REMOTE) {
if (item.value.server) {
typeInfo = t('core', 'At {server}', {
server: item.value.server
});
} else {
typeInfo = t('core', 'Federated');
}
}
if (item.value.userType === OC.User.USER_TYPE_GUEST) {
typeInfo = t('core', 'Guest');
}
var template = this._getAutocompleteItemTemplate();
var $el = $(template({
showAvatar: this.configModel.areAvatarsEnabled(),
displayName: text,
showIcon: showIcon,
iconClass: iconClass,
typeInfo: typeInfo,
additionalInfo: item.value.shareWithAdditionalInfo,
shareTypeClass: (item.value.shareType === OC.Share.SHARE_TYPE_GROUP) ? 'group' : 'user'
}));
if (this.configModel.areAvatarsEnabled()) {
var $avatar = $el.find('.avatardiv');
if (item.value.shareType === OC.Share.SHARE_TYPE_USER) {
$avatar.avatar(item.value.shareWith, 32, undefined, undefined, undefined, item.label);
} else {
$avatar.imageplaceholder(text, undefined, 32);
}
}
return $el.appendTo(ul);
},
_onSelectRecipient: function(e, s) {
e.preventDefault();
$(e.target).attr('disabled', true)
.val(s.item.label);
var $loading = this.$el.find('.shareWithLoading');
$loading.removeClass('hidden')
.addClass('inlineblock');
if (s.item.failedBatch && s.item.failedBatch.length) {
this._showFailedBatchSharees(s.item.failedBatch);
}
var shares = s.item.batch || [s.item.value];
for (var i = 0; i < shares.length; i++) {
this.model.addShare(shares[i], {
success: function () {
$(e.target).val('').attr('disabled', false);
$loading.addClass('hidden').removeClass('inlineblock');
}, error: function (obj, msg) {
OC.Notification.showTemporary(msg);
$(e.target).attr('disabled', false).autocomplete('search', $(e.target).val());
$loading.addClass('hidden').removeClass('inlineblock');
}
});
}
},
_toggleLoading: function(state) {
this._loading = state;
this.$el.find('.localShareView, .noSharingPlaceholder').toggleClass('hidden', state);
this.$el.find('.loading').toggleClass('hidden', !state);
},
_onRequest: function() {
// only show the loading spinner for the first request (for now)
if (!this._loadingOnce) {
this._toggleLoading(true);
}
},
_onEndRequest: function() {
var self = this;
this._toggleLoading(false);
if (!this._loadingOnce) {
this._loadingOnce = true;
// the first time, focus on the share field after the spinner disappeared
if (!bowser.msie) {
_.defer(function () {
self.$('.shareWithField').focus();
});
}
}
},
render: function() {
var baseTemplate = this._getTemplate('base', TEMPLATE_BASE);
this.$el.html(baseTemplate({
cid: this.cid,
shareLabel: t('core', 'Share'),
sharePlaceholder: this._renderSharePlaceholderPart(),
remoteShareInfo: this._renderRemoteShareInfoPart(),
isSharingAllowed: this.model.sharePermissionPossible(),
isLinkSharingAllowed: this.configModel.isShareWithLinkAllowed(),
localSharesLabel: t('core', 'User and Groups'),
publicSharesLabel: t('core', 'Public Links'),
noSharingPlaceholder: t('core', 'Sharing is not allowed')
}));
var $shareField = this.$el.find('.shareWithField');
if ($shareField.length) {
$shareField.autocomplete({
minLength: 1,
delay: 750,
focus: function(event) {
event.preventDefault();
},
source: this.autocompleteHandler,
select: this._onSelectRecipient,
appendTo: '#shareTabView'
}).data('ui-autocomplete')._renderItem = _.bind(this.autocompleteRenderItem, this);
}
this.resharerInfoView.$el = this.$el.find('.resharerInfoView');
this.resharerInfoView.render();
var resharingAllowed = this.model.sharePermissionPossible();
if (!resharingAllowed || !this.configModel.isShareWithLinkAllowed()) {
this.$('.tabHeaders, .linkShareView').remove();
}
this.shareeListView.$el = this.$el.find('.shareeListView');
this.shareeListView.render();
this.$el.find('.hasTooltip').tooltip();
return this;
},
_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 ownClouds using the syntax username@example.com/owncloud')
});
}
return remoteShareInfo;
},
_renderSharePlaceholderPart: function () {
var sharePlaceholder = t('core', 'Share with users…');
if (this.configModel.get('allowGroupSharing')) {
if (this.configModel.get('isRemoteShareAllowed')) {
sharePlaceholder = t('core', 'Share with users, groups or federated users…');
} else {
sharePlaceholder = t('core', 'Share with users or groups…')
}
} else if (this.configModel.get('isRemoteShareAllowed')) {
sharePlaceholder = t('core', 'Share with users or federated users…');
}
return sharePlaceholder;
},
/**
*
* @param {string} key - an identifier for the template
* @param {string} template - the HTML to be compiled by Handlebars
* @returns {Function} from Handlebars
* @private
*/
_getTemplate: function (key, template) {
if (!this._templates[key]) {
this._templates[key] = Handlebars.compile(template);
}
return this._templates[key];
},
/**
* returns the info template for remote sharing
*
* @returns {Function}
* @private
*/
_getRemoteShareInfoTemplate: function() {
return this._getTemplate('remoteShareInfo', TEMPLATE_REMOTE_SHARE_INFO);
},
/**
* Returns the autocomplete item template
*
* @returns {Function}
* @private
*/
_getAutocompleteItemTemplate: function() {
return this._getTemplate('autocompleteItem', TEMPLATE_AUTOCOMPLETE_ITEM);
},
/**
* Shows all users with whom the batch share action failed.
*
* @param {Array.<String>} users
* @private
*/
_showFailedBatchSharees: function(users) {
var failedUsersStr = users.join(', ');
OC.Notification.show(
t('core', 'Could not be shared with the following users: {users}', {users: failedUsersStr}),
{type: 'error'}
);
},
/**
* Returns the label for the batch action
*
* @returns {string}
* @private
*/
_getBatchActionLabel: function() {
return t('core', 'Add multiple users');
},
/**
* Displays an error, e.g. when the autocomplete doesn't have results.
*
* @param {string} title - title of the error
* @private
*/
_displayError: function(title) {
$('.shareWithField').addClass('error')
.attr('data-original-title', title)
.tooltip('hide')
.tooltip({
placement: 'bottom',
trigger: 'manual'
})
.tooltip('fixTitle')
.tooltip('show');
},
/**
* Returns a promise which includes all fetched users for batch actions once resolved.
*
* @param {string} search - trimmed search term
* @returns {Promise}
* @private
*/
_getUsersForBatchAction: function(search) {
var view = this;
var foundUsers = [];
var notFound = [];
var promises = [];
var users = Array.from(new Set(search.replace(new RegExp(this.batchActionSeparator + '\\s+', 'g'), this.batchActionSeparator).split(this.batchActionSeparator)));
for (var i = 0; i < users.length; i++) {
if (!users[i] || users[i] === OC.currentUser) {
continue;
}
var user = users[i].trim();
promises.push(
$.ajax({
url: OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees',
contentType: 'application/json',
dataType: 'json',
data: {
format: 'json',
search: user,
perPage: 200,
itemType: view.model.get('itemType')
},
context: { user: user }
})
.done(function (result) {
if (result.ocs.data.exact.users.length) {
var addShare = true;
var shares = view.model.get('shares');
if (!shares.length) {
foundUsers.push(result.ocs.data.exact.users[0].value);
return;
}
// filter out all sharees that are already shared with
for (j= 0; j < shares.length; j++) {
if (shares[j].share_type === OC.Share.SHARE_TYPE_USER
&& result.ocs.data.exact.users[0].value.shareWith === shares[j].share_with) {
addShare = false;
break;
}
}
if (addShare) {
foundUsers.push(result.ocs.data.exact.users[0].value);
}
} else {
notFound.push(this.user);
}
})
);
}
return Promise.all(promises).then(function() {
return {found: foundUsers, notFound: notFound};
});
}
});
OC.Share.ShareDialogView = ShareDialogView;
})();