diff --git a/apps/files_sharing/js/app.js b/apps/files_sharing/js/app.js
index 3764328a5d0..800873cd638 100644
--- a/apps/files_sharing/js/app.js
+++ b/apps/files_sharing/js/app.js
@@ -8,7 +8,9 @@
*
*/
-OCA.Sharing = {};
+if (!OCA.Sharing) {
+ OCA.Sharing = {};
+}
OCA.Sharing.App = {
_inFileList: null,
diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js
index 5a42604c866..d63a590fb8e 100644
--- a/apps/files_sharing/js/share.js
+++ b/apps/files_sharing/js/share.js
@@ -8,103 +8,166 @@
*
*/
-$(document).ready(function() {
- if (!_.isUndefined(OC.Share) && !_.isUndefined(OCA.Files)) {
- // TODO: make a separate class for this or a hook or jQuery event ?
- if (OCA.Files.FileList) {
- var oldCreateRow = OCA.Files.FileList.prototype._createRow;
- OCA.Files.FileList.prototype._createRow = function(fileData) {
- var tr = oldCreateRow.apply(this, arguments);
- if (fileData.shareOwner) {
- tr.attr('data-share-owner', fileData.shareOwner);
- // user should always be able to rename a mount point
- if (fileData.isShareMountPoint) {
- tr.attr('data-permissions', fileData.permissions | OC.PERMISSION_UPDATE);
- tr.attr('data-reshare-permissions', fileData.permissions);
+(function() {
+ if (!OCA.Sharing) {
+ OCA.Sharing = {};
+ }
+ OCA.Sharing.Util = {
+ initialize: function(fileActions) {
+ if (OCA.Files.FileList) {
+ var oldCreateRow = OCA.Files.FileList.prototype._createRow;
+ OCA.Files.FileList.prototype._createRow = function(fileData) {
+ var tr = oldCreateRow.apply(this, arguments);
+ if (fileData.shareOwner) {
+ tr.attr('data-share-owner', fileData.shareOwner);
+ // user should always be able to rename a mount point
+ if (fileData.isShareMountPoint) {
+ tr.attr('data-permissions', fileData.permissions | OC.PERMISSION_UPDATE);
+ tr.attr('data-reshare-permissions', fileData.permissions);
+ }
}
- }
- return tr;
- };
- }
+ if (fileData.recipientsDisplayName) {
+ tr.attr('data-share-recipients', fileData.recipientsDisplayName);
+ }
+ return tr;
+ };
- // use delegate to catch the case with multiple file lists
- $('#content').delegate('#fileList', 'fileActionsReady',function(ev){
- // if no share action exists because the admin disabled sharing for this user
- // we create a share notification action to inform the user about files
- // shared with him otherwise we just update the existing share action.
- var fileList = ev.fileList;
- var $fileList = $(this);
- $fileList.find('[data-share-owner]').each(function() {
- var $tr = $(this);
- var $action;
- var owner;
- var message;
- var permissions = $tr.data('permissions');
- if(permissions & OC.PERMISSION_SHARE) {
- $action = $tr.find('[data-Action="Share"]');
- $action.addClass('permanent');
- owner = $tr.closest('tr').attr('data-share-owner');
- message = ' ' + t('files_sharing', 'Shared by {owner}', {owner: owner});
- $action.find('span').text(message);
- } else {
- var shareNotification = '' +
- '
';
- $tr.find('.fileactions').append(function() {
- var owner = $(this).closest('tr').attr('data-share-owner');
- var shareBy = t('files_sharing', 'Shared by {owner}', {owner: owner});
- var $result = $(shareNotification + ' ' + shareBy + '');
- $result.on('click', function() {
- return false;
- });
- return $result;
- });
+ var oldRenderRow = OCA.Files.FileList.prototype._renderRow;
+ OCA.Files.FileList.prototype._renderRow = function(fileData) {
+ var $tr = oldRenderRow.apply(this, arguments);
+ // if the statuses are loaded already, use them for the icon
+ // (needed when scrolling to the next page)
+ var shareStatus = OC.Share.statuses[fileData.id];
+ if (fileData.shareOwner || fileData.recipientsDisplayName || shareStatus) {
+ var permissions = $tr.data('permissions');
+ var hasLink = !!(shareStatus && shareStatus.link);
+ if (permissions & OC.PERMISSION_SHARE) {
+ OC.Share.markFileAsShared($tr, true, hasLink);
+ } else {
+ // if no share action exists because the admin disabled sharing for this user
+ // we create a share notification action to inform the user about files
+ // shared with him otherwise we just update the existing share action.
+ // TODO: make this work like/with OC.Share.markFileAsShared()
+ var shareNotification = '' +
+ '
';
+ $tr.find('.fileactions').append(function() {
+ var shareBy = t('files_sharing', 'Shared by {owner}', {owner: fileData.shareOwner});
+ var $result = $(shareNotification + ' ' + shareBy + '');
+ $result.on('click', function() {
+ return false;
+ });
+ return $result;
+ });
+ }
+ }
+ return $tr;
+ };
+ }
+
+ // use delegate to catch the case with multiple file lists
+ $('#content').delegate('#fileList', 'fileActionsReady',function(ev){
+ var fileList = ev.fileList;
+ if (!OCA.Sharing.sharesLoaded){
+ OC.Share.loadIcons('file', fileList);
+ // assume that we got all shares, so switching directories
+ // will not invalidate that list
+ OCA.Sharing.sharesLoaded = true;
+ }
+ else{
+ // this will update the icons for all the currently visible elements
+ // additionally added elements when scrolling down will be
+ // updated in the _renderRow override
+ OC.Share.updateIcons('file', fileList);
}
});
- if (!OCA.Sharing.sharesLoaded){
- OC.Share.loadIcons('file', fileList);
- // assume that we got all shares, so switching directories
- // will not invalidate that list
- OCA.Sharing.sharesLoaded = true;
- }
- else{
- OC.Share.updateIcons('file', fileList);
- }
- });
-
- OCA.Files.fileActions.register(
- 'all',
- 'Share',
- OC.PERMISSION_SHARE,
- OC.imagePath('core', 'actions/share'),
- function(filename, context) {
+ fileActions.register(
+ 'all',
+ 'Share',
+ OC.PERMISSION_SHARE,
+ OC.imagePath('core', 'actions/share'),
+ function(filename, context) {
- var $tr = context.$file;
- var itemType = 'file';
- if ($tr.data('type') === 'dir') {
- itemType = 'folder';
- }
- var possiblePermissions = $tr.data('reshare-permissions');
- if (_.isUndefined(possiblePermissions)) {
- possiblePermissions = $tr.data('permissions');
- }
+ var $tr = context.$file;
+ var itemType = 'file';
+ if ($tr.data('type') === 'dir') {
+ itemType = 'folder';
+ }
+ var possiblePermissions = $tr.data('reshare-permissions');
+ if (_.isUndefined(possiblePermissions)) {
+ possiblePermissions = $tr.data('permissions');
+ }
- var appendTo = $tr.find('td.filename');
- // Check if drop down is already visible for a different file
- if (OC.Share.droppedDown) {
- if ($tr.data('id') !== $('#dropdown').attr('data-item-source')) {
- OC.Share.hideDropDown(function () {
- $tr.addClass('mouseOver');
- OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename);
- });
+ var appendTo = $tr.find('td.filename');
+ // Check if drop down is already visible for a different file
+ if (OC.Share.droppedDown) {
+ if ($tr.data('id') !== $('#dropdown').attr('data-item-source')) {
+ OC.Share.hideDropDown(function () {
+ $tr.addClass('mouseOver');
+ OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename);
+ });
+ } else {
+ OC.Share.hideDropDown();
+ }
} else {
- OC.Share.hideDropDown();
+ $tr.addClass('mouseOver');
+ OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename);
}
- } else {
- $tr.addClass('mouseOver');
- OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename);
+ $('#dropdown').on('sharesChanged', function(ev) {
+ // files app current cannot show recipients on load, so we don't update the
+ // icon when changed for consistency
+ if (context.fileList.$el.closest('#app-content-files').length) {
+ return;
+ }
+ var recipients = _.pluck(ev.shares[OC.Share.SHARE_TYPE_USER], 'share_with_displayname');
+ var groupRecipients = _.pluck(ev.shares[OC.Share.SHARE_TYPE_GROUP], 'share_with_displayname');
+ recipients = recipients.concat(groupRecipients);
+ // note: we only update the data attribute because updateIcon()
+ // is called automatically after this event
+ if (recipients.length) {
+ $tr.attr('data-share-recipients', OCA.Sharing.Util.formatRecipients(recipients));
+ }
+ else {
+ $tr.removeAttr('data-share-recipients');
+ }
+ });
+ });
+ },
+
+ /**
+ * Formats a recipients array to be displayed.
+ * The first four recipients will be shown and the
+ * other ones will be shown as "+x" where "x" is the number of
+ * remaining recipients.
+ *
+ * @param recipients recipients array
+ * @param count optional total recipients count (in case the array was shortened)
+ * @return formatted recipients display text
+ */
+ formatRecipients: function(recipients, count) {
+ var maxRecipients = 4;
+ var text;
+ if (!_.isNumber(count)) {
+ count = recipients.length;
+ }
+ // TODO: use natural sort
+ recipients = _.first(recipients, maxRecipients).sort();
+ text = recipients.join(', ');
+ if (count > maxRecipients) {
+ text += ', +' + (count - maxRecipients);
}
- });
+ return text;
+ }
+ };
+})();
+
+$(document).ready(function() {
+ // FIXME: HACK: do not init when running unit tests, need a better way
+ if (!window.TESTING) {
+ if (!_.isUndefined(OC.Share) && !_.isUndefined(OCA.Files)) {
+ OCA.Sharing.Util.initialize(OCA.Files.fileActions);
+ }
}
});
+
diff --git a/apps/files_sharing/js/sharedfilelist.js b/apps/files_sharing/js/sharedfilelist.js
index ef1034ecfdc..97fabf87131 100644
--- a/apps/files_sharing/js/sharedfilelist.js
+++ b/apps/files_sharing/js/sharedfilelist.js
@@ -38,6 +38,13 @@
}
},
+ _renderRow: function() {
+ // HACK: needed to call the overridden _renderRow
+ // this is because at the time this class is created
+ // the overriding hasn't been done yet...
+ return OCA.Files.FileList.prototype._renderRow.apply(this, arguments);
+ },
+
_createRow: function(fileData) {
// TODO: hook earlier and render the whole row here
var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments);
@@ -46,7 +53,7 @@
$tr.find('td.filename input:checkbox').remove();
$tr.attr('data-share-id', _.pluck(fileData.shares, 'id').join(','));
if (this._sharedWithUser) {
- $tr.attr('data-share-owner', fileData.shares[0].ownerDisplayName);
+ $tr.attr('data-share-owner', fileData.shareOwner);
}
return $tr;
},
@@ -159,7 +166,7 @@
stime: share.stime * 1000,
};
if (self._sharedWithUser) {
- file.share.ownerDisplayName = share.displayname_owner;
+ file.shareOwner = share.displayname_owner;
file.name = OC.basename(share.file_target);
file.path = OC.dirname(share.file_target);
file.permissions = share.permissions;
@@ -179,15 +186,15 @@
// inside the same file object (by file id).
.reduce(function(memo, file) {
var data = memo[file.id];
- var counterPart = file.share.ownerDisplayName || file.share.targetDisplayName;
+ var recipient = file.share.targetDisplayName;
if (!data) {
data = memo[file.id] = file;
data.shares = [file.share];
// using a hash to make them unique,
// this is only a list to be displayed
- data.counterParts = {};
+ data.recipients = {};
// counter is cheaper than calling _.keys().length
- data.counterPartsCount = 0;
+ data.recipientsCount = 0;
data.mtime = file.share.stime;
}
else {
@@ -198,12 +205,14 @@
data.shares.push(file.share);
}
- if (file.share.type === OC.Share.SHARE_TYPE_LINK) {
- data.hasLinkShare = true;
- } else if (counterPart && data.counterPartsCount < 10) {
+ if (recipient) {
// limit counterparts for output
- data.counterParts[counterPart] = true;
- data.counterPartsCount++;
+ if (data.recipientsCount < 4) {
+ // only store the first ones, they will be the only ones
+ // displayed
+ data.recipients[recipient] = true;
+ }
+ data.recipientsCount++;
}
delete file.share;
@@ -213,14 +222,14 @@
.values()
// Clean up
.each(function(data) {
- // convert the counterParts map to a flat
+ // convert the recipients map to a flat
// array of sorted names
- data.counterParts = _.chain(data.counterParts).keys().sort().value();
- if (data.hasLinkShare) {
- data.counterParts.unshift(t('files_sharing', 'link'));
- delete data.hasLinkShare;
- }
- delete data.counterPartsCount;
+ data.recipients = _.keys(data.recipients);
+ data.recipientsDisplayName = OCA.Sharing.Util.formatRecipients(
+ data.recipients,
+ data.recipientsCount
+ );
+ delete data.recipientsCount;
})
// Sort by expected sort comparator
.sortBy(this._sortComparator)
diff --git a/apps/files_sharing/tests/js/appSpec.js b/apps/files_sharing/tests/js/appSpec.js
index 3a1b495a16b..9c46b7caf1b 100644
--- a/apps/files_sharing/tests/js/appSpec.js
+++ b/apps/files_sharing/tests/js/appSpec.js
@@ -122,9 +122,7 @@ describe('OCA.Sharing.App tests', function() {
type: 'dir',
path: '/somewhere/inside/subdir',
counterParts: ['user2'],
- shares: [{
- ownerDisplayName: 'user2'
- }]
+ shareOwner: 'user2'
}]);
fileListIn.findFileEl('testdir').find('td a.name').click();
diff --git a/apps/files_sharing/tests/js/shareSpec.js b/apps/files_sharing/tests/js/shareSpec.js
new file mode 100644
index 00000000000..455addaccc0
--- /dev/null
+++ b/apps/files_sharing/tests/js/shareSpec.js
@@ -0,0 +1,450 @@
+/*
+ * Copyright (c) 2014 Vincent Petry
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+describe('OCA.Sharing.Util tests', function() {
+ var oldFileListPrototype;
+ var fileList;
+ var testFiles;
+
+ function getImageUrl($el) {
+ // might be slightly different cross-browser
+ var url = $el.css('background-image');
+ var r = url.match(/url\(['"]?([^'")]*)['"]?\)/);
+ if (!r) {
+ return url;
+ }
+ return r[1];
+ }
+
+ beforeEach(function() {
+ // back up prototype, as it will be extended by
+ // the sharing code
+ oldFileListPrototype = _.extend({}, OCA.Files.FileList.prototype);
+
+ var $content = $('');
+ $('#testArea').append($content);
+ // dummy file list
+ var $div = $(
+ '');
+ $('#content').append($div);
+
+ var fileActions = new OCA.Files.FileActions();
+ OCA.Sharing.Util.initialize(fileActions);
+ fileList = new OCA.Files.FileList(
+ $div, {
+ fileActions : fileActions
+ }
+ );
+
+ testFiles = [{
+ id: 1,
+ type: 'file',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc',
+ shareOwner: 'User One',
+ isShareMountPoint: false
+ }];
+
+ OCA.Sharing.sharesLoaded = true;
+ OC.Share.statuses = {
+ 1: {link: false, path: '/subdir'}
+ };
+ });
+ afterEach(function() {
+ OCA.Files.FileList.prototype = oldFileListPrototype;
+ delete OCA.Sharing.sharesLoaded;
+ delete OC.Share.droppedDown;
+ OC.Share.statuses = {};
+ OC.Share.currentShares = {};
+ });
+
+ describe('Sharing data in table row', function() {
+ // TODO: test data-permissions, data-share-owner, etc
+ });
+ describe('Share action icon', function() {
+ it('do not shows share text when not shared', function() {
+ var $action, $tr;
+ OC.Share.statuses = {};
+ fileList.setFiles([{
+ id: 1,
+ type: 'dir',
+ name: 'One',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc'
+ }]);
+ $tr = fileList.$el.find('tbody tr:first');
+ $action = $tr.find('.action-share');
+ expect($action.hasClass('permanent')).toEqual(false);
+ expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
+ expect(OC.basename(getImageUrl($tr.find('.filename')))).toEqual('folder.svg');
+ expect($action.find('img').length).toEqual(1);
+ });
+ it('shows simple share text with share icon', function() {
+ var $action, $tr;
+ fileList.setFiles([{
+ id: 1,
+ type: 'dir',
+ name: 'One',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc'
+ }]);
+ $tr = fileList.$el.find('tbody tr:first');
+ $action = $tr.find('.action-share');
+ expect($action.hasClass('permanent')).toEqual(true);
+ expect($action.find('>span').text()).toEqual('Shared');
+ expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
+ expect(OC.basename(getImageUrl($tr.find('.filename')))).toEqual('folder-shared.svg');
+ expect($action.find('img').length).toEqual(1);
+ });
+ it('shows simple share text with public icon when shared with link', function() {
+ var $action, $tr;
+ OC.Share.statuses = {1: {link: true, path: '/subdir'}};
+ fileList.setFiles([{
+ id: 1,
+ type: 'dir',
+ name: 'One',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc'
+ }]);
+ $tr = fileList.$el.find('tbody tr:first');
+ $action = $tr.find('.action-share');
+ expect($action.hasClass('permanent')).toEqual(true);
+ expect($action.find('>span').text()).toEqual('Shared');
+ expect(OC.basename($action.find('img').attr('src'))).toEqual('public.svg');
+ expect(OC.basename(getImageUrl($tr.find('.filename')))).toEqual('folder-public.svg');
+ expect($action.find('img').length).toEqual(1);
+ });
+ it('shows owner name when owner is available', function() {
+ var $action, $tr;
+ fileList.setFiles([{
+ id: 1,
+ type: 'dir',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ shareOwner: 'User One',
+ etag: 'abc'
+ }]);
+ $tr = fileList.$el.find('tbody tr:first');
+ $action = $tr.find('.action-share');
+ expect($action.hasClass('permanent')).toEqual(true);
+ expect($action.find('>span').text()).toEqual('Shared by User One');
+ expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
+ expect(OC.basename(getImageUrl($tr.find('.filename')))).toEqual('folder-shared.svg');
+ });
+ it('shows recipients when recipients are available', function() {
+ var $action, $tr;
+ fileList.setFiles([{
+ id: 1,
+ type: 'dir',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ recipientsDisplayName: 'User One, User Two',
+ etag: 'abc'
+ }]);
+ $tr = fileList.$el.find('tbody tr:first');
+ $action = $tr.find('.action-share');
+ expect($action.hasClass('permanent')).toEqual(true);
+ expect($action.find('>span').text()).toEqual('Shared with User One, User Two');
+ expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
+ expect(OC.basename(getImageUrl($tr.find('.filename')))).toEqual('folder-shared.svg');
+ expect($action.find('img').length).toEqual(1);
+ });
+ it('shows static share text when file shared with user that has no share permission', function() {
+ var $action, $tr;
+ fileList.setFiles([{
+ id: 1,
+ type: 'dir',
+ name: 'One',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_CREATE,
+ etag: 'abc',
+ shareOwner: 'User One'
+ }]);
+ $tr = fileList.$el.find('tbody tr:first');
+ expect($tr.find('.action-share').length).toEqual(0);
+ $action = $tr.find('.action-share-notification');
+ expect($action.hasClass('permanent')).toEqual(true);
+ expect($action.find('>span').text().trim()).toEqual('Shared by User One');
+ expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
+ expect(OC.basename(getImageUrl($tr.find('.filename')))).toEqual('folder-shared.svg');
+ expect($action.find('img').length).toEqual(1);
+ });
+ });
+ describe('Share action', function() {
+ var showDropDownStub;
+
+ function makeDummyShareItem(displayName) {
+ return {
+ share_with_displayname: displayName
+ };
+ }
+
+ beforeEach(function() {
+ showDropDownStub = sinon.stub(OC.Share, 'showDropDown', function() {
+ $('#testArea').append($(''));
+ });
+ });
+ afterEach(function() {
+ showDropDownStub.restore();
+ });
+ it('adds share icon after sharing a non-shared file', function() {
+ var $action, $tr;
+ OC.Share.statuses = {};
+ fileList.setFiles([{
+ id: 1,
+ type: 'file',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc'
+ }]);
+ $action = fileList.$el.find('tbody tr:first .action-share');
+ $tr = fileList.$el.find('tr:first');
+
+ expect($action.hasClass('permanent')).toEqual(false);
+
+ $tr.find('.action-share').click();
+
+ expect(showDropDownStub.calledOnce).toEqual(true);
+
+ // simulate what the dropdown does
+ var shares = {};
+ OC.Share.itemShares[OC.Share.SHARE_TYPE_USER] = ['user1', 'user2'];
+ OC.Share.itemShares[OC.Share.SHARE_TYPE_GROUP] = ['group1', 'group2'];
+ shares[OC.Share.SHARE_TYPE_USER] = _.map(['User One', 'User Two'], makeDummyShareItem);
+ shares[OC.Share.SHARE_TYPE_GROUP] = _.map(['Group One', 'Group Two'], makeDummyShareItem);
+ $('#dropdown').trigger(new $.Event('sharesChanged', {shares: shares}));
+
+ expect($tr.attr('data-share-recipients')).toEqual('Group One, Group Two, User One, User Two');
+
+ OC.Share.updateIcon('file', 1);
+ expect($action.hasClass('permanent')).toEqual(true);
+ expect($action.find('>span').text()).toEqual('Shared with Group One, Group Two, User One, User Two');
+ expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
+ });
+ it('updates share icon after updating shares of a file', function() {
+ var $action, $tr;
+ OC.Share.statuses = {1: {link: false, path: '/subdir'}};
+ fileList.setFiles([{
+ id: 1,
+ type: 'file',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc'
+ }]);
+ $action = fileList.$el.find('tbody tr:first .action-share');
+ $tr = fileList.$el.find('tr:first');
+
+ expect($action.hasClass('permanent')).toEqual(true);
+
+ $tr.find('.action-share').click();
+
+ expect(showDropDownStub.calledOnce).toEqual(true);
+
+ // simulate what the dropdown does
+ var shares = {};
+ OC.Share.itemShares[OC.Share.SHARE_TYPE_USER] = ['user1', 'user2', 'user3'];
+ shares[OC.Share.SHARE_TYPE_USER] = _.map(['User One', 'User Two', 'User Three'], makeDummyShareItem);
+ $('#dropdown').trigger(new $.Event('sharesChanged', {shares: shares}));
+
+ expect($tr.attr('data-share-recipients')).toEqual('User One, User Three, User Two');
+
+ OC.Share.updateIcon('file', 1);
+
+ expect($action.hasClass('permanent')).toEqual(true);
+ expect($action.find('>span').text()).toEqual('Shared with User One, User Three, User Two');
+ expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
+ });
+ it('removes share icon after removing all shares from a file', function() {
+ var $action, $tr;
+ OC.Share.statuses = {1: {link: false, path: '/subdir'}};
+ fileList.setFiles([{
+ id: 1,
+ type: 'file',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc',
+ recipients: 'User One, User Two'
+ }]);
+ $action = fileList.$el.find('tbody tr:first .action-share');
+ $tr = fileList.$el.find('tr:first');
+
+ expect($action.hasClass('permanent')).toEqual(true);
+
+ $tr.find('.action-share').click();
+
+ expect(showDropDownStub.calledOnce).toEqual(true);
+
+ // simulate what the dropdown does
+ OC.Share.itemShares = {};
+ $('#dropdown').trigger(new $.Event('sharesChanged', {shares: {}}));
+
+ expect($tr.attr('data-share-recipients')).not.toBeDefined();
+
+ OC.Share.updateIcon('file', 1);
+ expect($action.hasClass('permanent')).toEqual(false);
+ });
+ it('keep share text after updating reshare', function() {
+ var $action, $tr;
+ OC.Share.statuses = {1: {link: false, path: '/subdir'}};
+ fileList.setFiles([{
+ id: 1,
+ type: 'file',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc',
+ shareOwner: 'User One'
+ }]);
+ $action = fileList.$el.find('tbody tr:first .action-share');
+ $tr = fileList.$el.find('tr:first');
+
+ expect($action.hasClass('permanent')).toEqual(true);
+
+ $tr.find('.action-share').click();
+
+ expect(showDropDownStub.calledOnce).toEqual(true);
+
+ // simulate what the dropdown does
+ var shares = {};
+ OC.Share.itemShares[OC.Share.SHARE_TYPE_USER] = ['user2'];
+ shares[OC.Share.SHARE_TYPE_USER] = _.map(['User Two'], makeDummyShareItem);
+ $('#dropdown').trigger(new $.Event('sharesChanged', {shares: shares}));
+
+ expect($tr.attr('data-share-recipients')).toEqual('User Two');
+
+ OC.Share.updateIcon('file', 1);
+
+ expect($action.hasClass('permanent')).toEqual(true);
+ expect($action.find('>span').text()).toEqual('Shared by User One');
+ expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
+ });
+ it('keep share text after unsharing reshare', function() {
+ var $action, $tr;
+ OC.Share.statuses = {1: {link: false, path: '/subdir'}};
+ fileList.setFiles([{
+ id: 1,
+ type: 'file',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc',
+ shareOwner: 'User One',
+ recipients: 'User Two'
+ }]);
+ $action = fileList.$el.find('tbody tr:first .action-share');
+ $tr = fileList.$el.find('tr:first');
+
+ expect($action.hasClass('permanent')).toEqual(true);
+
+ $tr.find('.action-share').click();
+
+ expect(showDropDownStub.calledOnce).toEqual(true);
+
+ // simulate what the dropdown does
+ OC.Share.itemShares = {};
+ $('#dropdown').trigger(new $.Event('sharesChanged', {shares: {}}));
+
+ expect($tr.attr('data-share-recipients')).not.toBeDefined();
+
+ OC.Share.updateIcon('file', 1);
+
+ expect($action.hasClass('permanent')).toEqual(true);
+ expect($action.find('>span').text()).toEqual('Shared by User One');
+ expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
+ });
+ });
+ describe('formatRecipients', function() {
+ it('returns a single recipient when one passed', function() {
+ expect(OCA.Sharing.Util.formatRecipients(['User one']))
+ .toEqual('User one');
+ });
+ it('returns two recipients when two passed', function() {
+ expect(OCA.Sharing.Util.formatRecipients(['User one', 'User two']))
+ .toEqual('User one, User two');
+ });
+ it('returns four recipients with plus when five passed', function() {
+ var recipients = [
+ 'User one',
+ 'User two',
+ 'User three',
+ 'User four',
+ 'User five'
+ ];
+ expect(OCA.Sharing.Util.formatRecipients(recipients))
+ .toEqual('User four, User one, User three, User two, +1');
+ });
+ it('returns four recipients with plus when ten passed', function() {
+ var recipients = [
+ 'User one',
+ 'User two',
+ 'User three',
+ 'User four',
+ 'User five',
+ 'User six',
+ 'User seven',
+ 'User eight',
+ 'User nine',
+ 'User ten'
+ ];
+ expect(OCA.Sharing.Util.formatRecipients(recipients))
+ .toEqual('User four, User one, User three, User two, +6');
+ });
+ it('returns four recipients with plus when four passed with counter', function() {
+ var recipients = [
+ 'User one',
+ 'User two',
+ 'User three',
+ 'User four'
+ ];
+ expect(OCA.Sharing.Util.formatRecipients(recipients, 10))
+ .toEqual('User four, User one, User three, User two, +6');
+ });
+ });
+
+});
diff --git a/apps/files_sharing/tests/js/sharedfilelistSpec.js b/apps/files_sharing/tests/js/sharedfilelistSpec.js
index 7aec8322a44..f53e79e277f 100644
--- a/apps/files_sharing/tests/js/sharedfilelistSpec.js
+++ b/apps/files_sharing/tests/js/sharedfilelistSpec.js
@@ -9,7 +9,8 @@
*/
describe('OCA.Sharing.FileList tests', function() {
- var testFiles, alertStub, notificationStub, fileList;
+ var testFiles, alertStub, notificationStub, fileList, fileActions;
+ var oldFileListPrototype;
beforeEach(function() {
alertStub = sinon.stub(OC.dialogs, 'alert');
@@ -45,10 +46,17 @@ describe('OCA.Sharing.FileList tests', function() {
'Empty content message
' +
''
);
+ // back up prototype, as it will be extended by
+ // the sharing code
+ oldFileListPrototype = _.extend({}, OCA.Files.FileList.prototype);
+ fileActions = new OCA.Files.FileActions();
+ OCA.Sharing.Util.initialize(fileActions);
});
afterEach(function() {
+ OCA.Files.FileList.prototype = oldFileListPrototype;
testFiles = undefined;
fileList = undefined;
+ fileActions = undefined;
notificationStub.restore();
alertStub.restore();
diff --git a/core/img/filetypes/folder-shared.png b/core/img/filetypes/folder-shared.png
index e3c0ee9815a..b4f02ac7399 100644
Binary files a/core/img/filetypes/folder-shared.png and b/core/img/filetypes/folder-shared.png differ
diff --git a/core/img/filetypes/folder-shared.svg b/core/img/filetypes/folder-shared.svg
index a389e535439..aba8cadbe28 100644
--- a/core/img/filetypes/folder-shared.svg
+++ b/core/img/filetypes/folder-shared.svg
@@ -1,60 +1,64 @@
diff --git a/core/js/share.js b/core/js/share.js
index bcc04225b9f..7e3b0d8c65d 100644
--- a/core/js/share.js
+++ b/core/js/share.js
@@ -3,8 +3,25 @@ OC.Share={
SHARE_TYPE_GROUP:1,
SHARE_TYPE_LINK:3,
SHARE_TYPE_EMAIL:4,
+ /**
+ * @deprecated use OC.Share.currentShares instead
+ */
itemShares:[],
+ /**
+ * Full list of all share statuses
+ */
statuses:{},
+ /**
+ * Shares for the currently selected file.
+ * (for which the dropdown is open)
+ *
+ * Key is item type and value is an array or
+ * shares of the given item type.
+ */
+ currentShares: {},
+ /**
+ * Whether the share dropdown is opened.
+ */
droppedDown:false,
/**
* Loads ALL share statuses from server, stores them in
@@ -54,8 +71,9 @@ OC.Share={
$fileList = fileList.$fileList;
currentDir = fileList.getCurrentDirectory();
}
+ // TODO: iterating over the files might be more efficient
for (item in OC.Share.statuses){
- var image = OC.imagePath('core', 'actions/shared');
+ var image = OC.imagePath('core', 'actions/share');
var data = OC.Share.statuses[item];
var hasLink = data.link;
// Links override shared in terms of icon display
@@ -63,20 +81,14 @@ OC.Share={
image = OC.imagePath('core', 'actions/public');
}
if (itemType !== 'file' && itemType !== 'folder') {
- $fileList.find('a.share[data-item="'+item+'"]').css('background', 'url('+image+') no-repeat center');
+ $('a.share[data-item="'+item+'"]').css('background', 'url('+image+') no-repeat center');
} else {
+ // TODO: ultimately this part should be moved to files_sharing app
var file = $fileList.find('tr[data-id="'+item+'"]');
var shareFolder = OC.imagePath('core', 'filetypes/folder-shared');
var img;
if (file.length > 0) {
- var type = file.data('type');
- if (type === 'dir') {
- file.children('.filename').css('background-image', 'url('+shareFolder+')');
- }
- var action = $(file).find('.fileactions .action[data-action="Share"]');
- img = action.find('img').attr('src', image);
- action.addClass('permanent');
- action.html(' '+t('core', 'Shared')+'').prepend(img);
+ this.markFileAsShared(file, true, hasLink);
} else {
var dir = currentDir;
if (dir.length > 1) {
@@ -89,6 +101,7 @@ OC.Share={
var files = $fileList.find('.filename');
var i;
for (i = 0; i < actions.length; i++) {
+ // TODO: use this.markFileAsShared()
img = $(actions[i]).find('img');
if (img.attr('src') !== OC.imagePath('core', 'actions/public')) {
img.attr('src', image);
@@ -125,35 +138,19 @@ OC.Share={
}
} else if (OC.Share.itemShares[index].length > 0) {
shares = true;
- image = OC.imagePath('core', 'actions/shared');
+ image = OC.imagePath('core', 'actions/share');
}
}
});
if (itemType != 'file' && itemType != 'folder') {
$('a.share[data-item="'+itemSource+'"]').css('background', 'url('+image+') no-repeat center');
} else {
- var file = $('tr').filterAttr('data-id', String(itemSource));
- if (file.length > 0) {
- var type = file.data('type');
- var shareFolder = OC.imagePath('core', 'filetypes/folder');
- if (type === 'dir' && shares) {
- shareFolder = OC.imagePath('core', 'filetypes/folder-shared');
- file.children('.filename').css('background-image', 'url('+shareFolder+')');
- } else if (type === 'dir') {
- file.children('.filename').css('background-image', 'url('+shareFolder+')');
- }
- var action = $(file).find('.fileactions .action').filterAttr('data-action', 'Share');
- // in case of multiple lists/rows, there might be more than one visible
- action.each(function() {
- var action = $(this);
- var img = action.find('img').attr('src', image);
- if (shares) {
- action.addClass('permanent');
- action.html(' '+ escapeHTML(t('core', 'Shared'))+'').prepend(img);
- } else {
- action.removeClass('permanent');
- action.html(' '+ escapeHTML(t('core', 'Share'))+'').prepend(img);
- }
+ var $tr = $('tr').filterAttr('data-id', String(itemSource));
+ if ($tr.length > 0) {
+ // it might happen that multiple lists exist in the DOM
+ // with the same id
+ $tr.each(function() {
+ OC.Share.markFileAsShared($(this), shares, link);
});
}
}
@@ -164,6 +161,60 @@ OC.Share={
delete OC.Share.statuses[itemSource];
}
},
+ /**
+ * Marks/unmarks a given file as shared by changing its action icon
+ * and folder icon.
+ *
+ * @param $tr file element to mark as shared
+ * @param hasShares whether shares are available
+ * @param hasLink whether link share is available
+ */
+ markFileAsShared: function($tr, hasShares, hasLink) {
+ var action = $tr.find('.fileactions .action[data-action="Share"]');
+ var type = $tr.data('type');
+ var img = action.find('img');
+ var message;
+ var recipients;
+ var owner = $tr.attr('data-share-owner');
+ var shareFolderIcon;
+ var image = OC.imagePath('core', 'actions/share');
+ // update folder icon
+ if (type === 'dir' && (hasShares || hasLink)) {
+ if (hasLink) {
+ shareFolderIcon = OC.imagePath('core', 'filetypes/folder-public');
+ }
+ else {
+ shareFolderIcon = OC.imagePath('core', 'filetypes/folder-shared');
+ }
+ $tr.children('.filename').css('background-image', 'url(' + shareFolderIcon + ')');
+ } else if (type === 'dir') {
+ shareFolderIcon = OC.imagePath('core', 'filetypes/folder');
+ $tr.children('.filename').css('background-image', 'url(' + shareFolderIcon + ')');
+ }
+ // update share action text / icon
+ if (hasShares || owner) {
+ recipients = $tr.attr('data-share-recipients');
+
+ action.addClass('permanent');
+ message = t('core', 'Shared');
+ // even if reshared, only show "Shared by"
+ if (owner) {
+ message = t('files_sharing', 'Shared by {owner}', {owner: owner});
+ }
+ else if (recipients) {
+ message = t('core', 'Shared with {recipients}', {recipients: recipients});
+ }
+ action.html(' '+ message + '').prepend(img);
+ }
+ else {
+ action.removeClass('permanent');
+ action.html(' '+ escapeHTML(t('core', 'Share'))+'').prepend(img);
+ }
+ if (hasLink) {
+ image = OC.imagePath('core', 'actions/public');
+ }
+ img.attr('src', image);
+ },
loadItem:function(itemType, itemSource) {
var data = '';
var checkReshare = true;
@@ -319,6 +370,7 @@ OC.Share={
dropDownEl = dropDownEl.appendTo(appendTo);
// Reset item shares
OC.Share.itemShares = [];
+ OC.Share.currentShares = {};
if (data.shares) {
$.each(data.shares, function(index, share) {
if (share.share_type == OC.Share.SHARE_TYPE_LINK) {
@@ -392,6 +444,7 @@ OC.Share={
OC.Share.share(itemType, itemSource, shareType, shareWith, permissions, itemSourceName, expirationDate, function() {
OC.Share.addShareWith(shareType, shareWith, selected.item.label, permissions, possiblePermissions);
$('#shareWith').val('');
+ $('#dropdown').trigger(new $.Event('sharesChanged', {shares: OC.Share.currentShares}));
OC.Share.updateIcon(itemType, itemSource);
});
return false;
@@ -448,6 +501,7 @@ OC.Share={
$('#shareWith').focus();
},
hideDropDown:function(callback) {
+ OC.Share.currentShares = null;
$('#dropdown').hide('blind', function() {
OC.Share.droppedDown = false;
$('#dropdown').remove();
@@ -460,6 +514,12 @@ OC.Share={
});
},
addShareWith:function(shareType, shareWith, shareWithDisplayName, permissions, possiblePermissions, mailSend, collection) {
+ var shareItem = {
+ share_type: shareType,
+ share_with: shareWith,
+ share_with_displayname: shareWithDisplayName,
+ permissions: permissions
+ };
if (shareType === 1) {
shareWithDisplayName = shareWithDisplayName + " (" + t('core', 'group') + ')';
}
@@ -538,6 +598,10 @@ OC.Share={
html.find('.cruds').before(showCrudsButton);
}
$('#expiration').show();
+ if (!OC.Share.currentShares[shareType]) {
+ OC.Share.currentShares[shareType] = [];
+ }
+ OC.Share.currentShares[shareType].push(shareItem);
}
},
showLink:function(token, password, itemSource) {
@@ -673,6 +737,9 @@ $(document).ready(function() {
$li.remove();
var index = OC.Share.itemShares[shareType].indexOf(shareWith);
OC.Share.itemShares[shareType].splice(index, 1);
+ // updated list of shares
+ OC.Share.currentShares[shareType].splice(index, 1);
+ $('#dropdown').trigger(new $.Event('sharesChanged', {shares: OC.Share.currentShares}));
OC.Share.updateIcon(itemType, itemSource);
if (typeof OC.Share.statuses[itemSource] === 'undefined') {
$('#expiration').hide('blind');
@@ -731,6 +798,7 @@ $(document).ready(function() {
if (oc_appconfig.core.enforcePasswordForPublicLink === false) {
OC.Share.share(itemType, itemSource, OC.Share.SHARE_TYPE_LINK, '', OC.PERMISSION_READ, itemSourceName, expirationDate, function(data) {
OC.Share.showLink(data.token, null, itemSource);
+ $('#dropdown').trigger(new $.Event('sharesChanged', {shares: OC.Share.currentShares}));
OC.Share.updateIcon(itemType, itemSource);
});
} else {
@@ -743,6 +811,7 @@ $(document).ready(function() {
if ($('#linkText').val() !== '') {
OC.Share.unshare(itemType, itemSource, OC.Share.SHARE_TYPE_LINK, '', function() {
OC.Share.itemShares[OC.Share.SHARE_TYPE_LINK] = false;
+ $('#dropdown').trigger(new $.Event('sharesChanged', {shares: OC.Share.currentShares}));
OC.Share.updateIcon(itemType, itemSource);
if (typeof OC.Share.statuses[itemSource] === 'undefined') {
$('#expiration').hide('blind');
diff --git a/core/js/tests/specs/shareSpec.js b/core/js/tests/specs/shareSpec.js
index a487b71fdbb..458bc41b6a1 100644
--- a/core/js/tests/specs/shareSpec.js
+++ b/core/js/tests/specs/shareSpec.js
@@ -26,13 +26,17 @@ describe('OC.Share tests', function() {
var oldAppConfig;
var loadItemStub;
var autocompleteStub;
+
beforeEach(function() {
$('#testArea').append($(''));
+ // horrible parameters
+ $('#testArea').append('');
$container = $('#shareContainer');
/* jshint camelcase:false */
- oldAppConfig = oc_appconfig.core;
- loadItemStub = sinon.stub(OC.Share, 'loadItem');
+ oldAppConfig = _.extend({}, oc_appconfig.core);
+ oc_appconfig.core.enforcePasswordForPublicLink = false;
+ loadItemStub = sinon.stub(OC.Share, 'loadItem');
loadItemStub.returns({
reshare: [],
shares: []
@@ -89,9 +93,133 @@ describe('OC.Share tests', function() {
oc_appconfig.core.defaultExpireDate = '';
// TODO: expect that default date was NOT set
});
- // TODO: test password field visibility (whenever enforced or not)
- // TODO: check link share field visibility based on whether it is allowed
- // TODO: check public upload visibility based on config
+ describe('Share with link', function() {
+ // TODO: test ajax calls
+ // TODO: test password field visibility (whenever enforced or not)
+ // TODO: check public upload visibility based on config
+ it('shows share with link checkbox when allowed', function() {
+ $('#allowShareWithLink').val('yes');
+ OC.Share.showDropDown(
+ 'file',
+ 123,
+ $container,
+ 'http://localhost/dummylink',
+ 31,
+ 'shared_file_name.txt'
+ );
+ expect($('#dropdown #linkCheckbox').length).toEqual(1);
+ });
+ it('does not show share with link checkbox when not allowed', function() {
+ $('#allowShareWithLink').val('no');
+ OC.Share.showDropDown(
+ 'file',
+ 123,
+ $container,
+ 'http://localhost/dummylink',
+ 31,
+ 'shared_file_name.txt'
+ );
+ expect($('#dropdown #linkCheckbox').length).toEqual(0);
+ });
+ });
+ describe('"sharesChanged" event', function() {
+ var autocompleteOptions;
+ var handler;
+ beforeEach(function() {
+ handler = sinon.stub();
+ loadItemStub.returns({
+ reshare: [],
+ shares: [{
+ id: 100,
+ item_source: 123,
+ permissions: 31,
+ share_type: OC.Share.SHARE_TYPE_USER,
+ share_with: 'user1',
+ share_with_displayname: 'User One'
+ }]
+ });
+ OC.Share.showDropDown(
+ 'file',
+ 123,
+ $container,
+ 'http://localhost/dummylink',
+ 31,
+ 'shared_file_name.txt'
+ );
+ $('#dropdown').on('sharesChanged', handler);
+ autocompleteOptions = autocompleteStub.getCall(0).args[0];
+ });
+ afterEach(function() {
+ autocompleteOptions = null;
+ handler = null;
+ });
+ it('triggers "sharesChanged" event when adding shares', function() {
+ // simulate autocomplete selection
+ autocompleteOptions.select(new $.Event('select'), {
+ item: {
+ label: 'User Two',
+ value: {
+ shareType: OC.Share.SHARE_TYPE_USER,
+ shareWith: 'user2'
+ }
+ }
+ });
+ fakeServer.requests[0].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({status: 'success'})
+ );
+ expect(handler.calledOnce).toEqual(true);
+ var shares = handler.getCall(0).args[0].shares;
+ expect(shares).toBeDefined();
+ expect(shares[OC.Share.SHARE_TYPE_USER][0].share_with_displayname).toEqual('User One');
+ expect(shares[OC.Share.SHARE_TYPE_USER][1].share_with_displayname).toEqual('User Two');
+ expect(shares[OC.Share.SHARE_TYPE_GROUP]).not.toBeDefined();
+ });
+ it('triggers "sharesChanged" event when deleting shares', function() {
+ $('#dropdown .unshare:eq(0)').click();
+ fakeServer.requests[0].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({status: 'success'})
+ );
+ expect(handler.calledOnce).toEqual(true);
+ var shares = handler.getCall(0).args[0].shares;
+ expect(shares).toBeDefined();
+ expect(shares[OC.Share.SHARE_TYPE_USER]).toEqual([]);
+ expect(shares[OC.Share.SHARE_TYPE_GROUP]).not.toBeDefined();
+ });
+ it('triggers "sharesChanged" event when toggling link share', function() {
+ // simulate autocomplete selection
+ $('#dropdown #linkCheckbox').click();
+ fakeServer.requests[0].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({status: 'success', data: { token: 'abc' }})
+ );
+ expect(handler.calledOnce).toEqual(true);
+ var shares = handler.getCall(0).args[0].shares;
+ expect(shares).toBeDefined();
+ expect(shares[OC.Share.SHARE_TYPE_USER][0].share_with_displayname).toEqual('User One');
+ expect(shares[OC.Share.SHARE_TYPE_GROUP]).not.toBeDefined();
+
+ handler.reset();
+
+ // uncheck checkbox
+ $('#dropdown #linkCheckbox').click();
+ fakeServer.requests[1].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({status: 'success'})
+ );
+
+ expect(handler.calledOnce).toEqual(true);
+ shares = handler.getCall(0).args[0].shares;
+ expect(shares).toBeDefined();
+ expect(shares[OC.Share.SHARE_TYPE_USER][0].share_with_displayname).toEqual('User One');
+ expect(shares[OC.Share.SHARE_TYPE_GROUP]).not.toBeDefined();
+ });
+ });
});
});
diff --git a/tests/karma.config.js b/tests/karma.config.js
index 846e8f7be91..1f903f58210 100644
--- a/tests/karma.config.js
+++ b/tests/karma.config.js
@@ -52,7 +52,8 @@ module.exports = function(config) {
// only test these files, others are not ready and mess
// up with the global namespace/classes/state
'apps/files_sharing/js/app.js',
- 'apps/files_sharing/js/sharedfilelist.js'
+ 'apps/files_sharing/js/sharedfilelist.js',
+ 'apps/files_sharing/js/share.js'
],
testFiles: ['apps/files_sharing/tests/js/*.js']
}];