Merge pull request #8417 from owncloud/share-overview

Sharing overview page
This commit is contained in:
Lukas Reschke 2014-05-30 13:42:24 +02:00
commit 517501ffbf
27 changed files with 1496 additions and 164 deletions

View file

@ -151,7 +151,13 @@ tr:hover span.extension {
}
table tr.mouseOver td { background-color:#eee; }
table th { height:24px; padding:0 8px; color:#999; }
table th { height:24px; padding:0 8px; }
table th, table th a {
color: #999;
}
table.multiselect th a {
color: #000;
}
table th .columntitle {
display: inline-block;
padding: 15px;

View file

@ -74,7 +74,12 @@ if (OC_App::isEnabled('files_encryption')) {
$nav = new OCP\Template('files', 'appnavigation', '');
function sortNavigationItems($item1, $item2) {
return $item1['order'] - $item2['order'];
}
$navItems = \OCA\Files\App::getNavigationManager()->getAll();
usort($navItems, 'sortNavigationItems');
$nav->assign('navigationItems', $navItems);
$contentItems = array();

View file

@ -24,20 +24,27 @@
initialize: function() {
this.navigation = new OCA.Files.Navigation($('#app-navigation'));
// TODO: ideally these should be in a separate class / app (the embedded "all files" app)
this.fileActions = OCA.Files.FileActions;
var fileActions = new OCA.Files.FileActions();
// default actions
fileActions.registerDefaultActions();
// legacy actions
fileActions.merge(window.FileActions);
// regular actions
fileActions.merge(OCA.Files.fileActions);
this.files = OCA.Files.Files;
// TODO: ideally these should be in a separate class / app (the embedded "all files" app)
this.fileList = new OCA.Files.FileList(
$('#app-content-files'), {
scrollContainer: $('#app-content'),
dragOptions: dragOptions,
folderDropOptions: folderDropOptions
folderDropOptions: folderDropOptions,
fileActions: fileActions,
allowLegacyActions: true
}
);
this.files.initialize();
this.fileActions.registerDefaultActions(this.fileList);
this.fileList.setFileActions(this.fileActions);
// for backward compatibility, the global FileList will
// refer to the one of the "files" view
@ -57,6 +64,22 @@
return this.navigation.getActiveContainer();
},
/**
* Sets the currently active view
* @param viewId view id
*/
setActiveView: function(viewId, options) {
this.navigation.setActiveItem(viewId, options);
},
/**
* Returns the view id of the currently active view
* @return view id
*/
getActiveView: function() {
return this.navigation.getActiveItem();
},
/**
* Setup events based on URL changes
*/
@ -138,7 +161,7 @@
})();
$(document).ready(function() {
// wait for other apps/extensions to register their event handlers
// wait for other apps/extensions to register their event handlers and file actions
// in the "ready" clause
_.defer(function() {
OCA.Files.App.initialize();

View file

@ -11,11 +11,40 @@
/* global trashBinApp */
(function() {
var FileActions = {
/**
* Construct a new FileActions instance
*/
var FileActions = function() {
this.initialize();
}
FileActions.prototype = {
actions: {},
defaults: {},
icons: {},
currentFile: null,
initialize: function() {
this.clear();
},
/**
* Merges the actions from the given fileActions into
* this instance.
*
* @param fileActions instance of OCA.Files.FileActions
*/
merge: function(fileActions) {
var self = this;
// merge first level to avoid unintended overwriting
_.each(fileActions.actions, function(sourceMimeData, mime) {
var targetMimeData = self.actions[mime];
if (!targetMimeData) {
targetMimeData = {};
}
self.actions[mime] = _.extend(targetMimeData, sourceMimeData);
});
this.defaults = _.extend(this.defaults, fileActions.defaults);
this.icons = _.extend(this.icons, fileActions.icons);
},
register: function (mime, name, permissions, icon, action, displayName) {
if (!this.actions[mime]) {
this.actions[mime] = {};
@ -98,8 +127,13 @@
* @param parent "td" element of the file for which to display actions
* @param triggerEvent if true, triggers the fileActionsReady on the file
* list afterwards (false by default)
* @param fileList OCA.Files.FileList instance on which the action is
* done, defaults to OCA.Files.App.fileList
*/
display: function (parent, triggerEvent) {
display: function (parent, triggerEvent, fileList) {
if (!fileList) {
console.warn('FileActions.display() MUST be called with a OCA.Files.FileList instance');
}
this.currentFile = parent;
var self = this;
var actions = this.getActions(this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions());
@ -120,9 +154,18 @@
event.preventDefault();
self.currentFile = event.data.elem;
var file = self.getCurrentFile();
// also set on global object for legacy apps
window.FileActions.currentFile = self.currentFile;
event.data.actionFunc(file);
var file = self.getCurrentFile();
var $tr = $(this).closest('tr');
event.data.actionFunc(file, {
$file: $tr,
fileList: fileList || OCA.Files.App.fileList,
fileActions: self,
dir: $tr.attr('data-path') || fileList.getCurrentDirectory()
});
};
var addAction = function (name, action, displayName) {
@ -189,7 +232,7 @@
}
if (triggerEvent){
$('#fileList').trigger(jQuery.Event("fileActionsReady"));
fileList.$fileList.trigger(jQuery.Event("fileActionsReady", {fileList: fileList}));
}
},
getCurrentFile: function () {
@ -208,29 +251,27 @@
/**
* Register the actions that are used by default for the files app.
*/
registerDefaultActions: function(fileList) {
// TODO: try to find a way to not make it depend on fileList,
// maybe get a handler or listener to trigger events on
registerDefaultActions: function() {
this.register('all', 'Delete', OC.PERMISSION_DELETE, function () {
return OC.imagePath('core', 'actions/delete');
}, function (filename) {
fileList.do_delete(filename);
}, function (filename, context) {
context.fileList.do_delete(filename);
$('.tipsy').remove();
});
// t('files', 'Rename')
this.register('all', 'Rename', OC.PERMISSION_UPDATE, function () {
return OC.imagePath('core', 'actions/rename');
}, function (filename) {
fileList.rename(filename);
}, function (filename, context) {
context.fileList.rename(filename);
});
this.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename) {
var dir = fileList.getCurrentDirectory();
this.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
var dir = context.fileList.getCurrentDirectory();
if (dir !== '/') {
dir = dir + '/';
}
fileList.changeDirectory(dir + filename);
context.fileList.changeDirectory(dir + filename);
});
this.setDefault('dir', 'Open');
@ -243,20 +284,38 @@
this.register(downloadScope, 'Download', OC.PERMISSION_READ, function () {
return OC.imagePath('core', 'actions/download');
}, function (filename) {
var url = fileList.getDownloadUrl(filename, fileList.getCurrentDirectory());
}, function (filename, context) {
var dir = context.dir || context.fileList.getCurrentDirectory();
var url = context.fileList.getDownloadUrl(filename, dir);
if (url) {
OC.redirect(url);
}
});
fileList.$fileList.trigger(jQuery.Event("fileActionsReady"));
}
};
OCA.Files.FileActions = FileActions;
// global file actions to be used by all lists
OCA.Files.fileActions = new OCA.Files.FileActions();
OCA.Files.legacyFileActions = new OCA.Files.FileActions();
// for backward compatibility
//
// legacy apps are expecting a stateful global FileActions object to register
// their actions on. Since legacy apps are very likely to break with other
// FileList views than the main one ("All files"), actions registered
// through window.FileActions will be limited to the main file list.
window.FileActions = OCA.Files.legacyFileActions;
window.FileActions.register = function (mime, name, permissions, icon, action, displayName) {
console.warn('FileActions.register() is deprecated, please use OCA.Files.fileActions.register() instead', arguments);
OCA.Files.FileActions.prototype.register.call(
window.FileActions, mime, name, permissions, icon, action, displayName
);
};
window.FileActions.setDefault = function (mime, name) {
console.warn('FileActions.setDefault() is deprecated, please use OCA.Files.fileActions.setDefault() instead', mime, name);
OCA.Files.FileActions.prototype.setDefault.call(window.FileActions, mime, name);
};
})();
// for backward compatibility
window.FileActions = OCA.Files.FileActions;

View file

@ -125,7 +125,7 @@
this.$container = options.scrollContainer || $(window);
this.$table = $el.find('table:first');
this.$fileList = $el.find('#fileList');
this.fileActions = OCA.Files.FileActions;
this._initFileActions(options.fileActions);
this.files = [];
this._selectedFiles = {};
this._selectionSummary = new OCA.Files.FileSummary();
@ -168,6 +168,14 @@
this.$container.on('scroll', _.bind(this._onScroll, this));
},
_initFileActions: function(fileActions) {
this.fileActions = fileActions;
if (!this.fileActions) {
this.fileActions = new OCA.Files.FileActions();
this.fileActions.registerDefaultActions();
}
},
/**
* Event handler for when the URL changed
*/
@ -248,7 +256,14 @@
var action = this.fileActions.getDefault(mime,type, permissions);
if (action) {
event.preventDefault();
action(filename);
// also set on global object for legacy apps
window.FileActions.currentFile = this.fileActions.currentFile;
action(filename, {
$file: $tr,
fileList: this,
fileActions: this.fileActions,
dir: $tr.attr('data-path') || this.getCurrentDirectory()
});
}
}
}
@ -448,7 +463,7 @@
while (count > 0 && index < this.files.length) {
fileData = this.files[index];
tr = this._renderRow(fileData, {updateSummary: false});
tr = this._renderRow(fileData, {updateSummary: false, silent: true});
this.$fileList.append(tr);
if (isAllSelected || this._selectedFiles[fileData.id]) {
tr.addClass('selected');
@ -493,7 +508,7 @@
this.$el.find('thead').after(this.$fileList);
this.updateEmptyContent();
this.$fileList.trigger(jQuery.Event("fileActionsReady"));
this.$fileList.trigger($.Event('fileActionsReady', {fileList: this}));
this.fileSummary.calculate(filesArray);
@ -515,6 +530,7 @@
type = fileData.type || 'file',
mtime = parseInt(fileData.mtime, 10) || new Date().getTime(),
mime = fileData.mimetype,
path = fileData.path,
linkUrl;
options = options || {};
@ -534,6 +550,13 @@
"data-permissions": fileData.permissions || this.getDirectoryPermissions()
});
if (!_.isUndefined(path)) {
tr.attr('data-path', path);
}
else {
path = this.getCurrentDirectory();
}
if (type === 'dir') {
// use default folder icon
icon = icon || OC.imagePath('core', 'filetypes/folder');
@ -550,10 +573,10 @@
// linkUrl
if (type === 'dir') {
linkUrl = this.linkTo(this.getCurrentDirectory() + '/' + name);
linkUrl = this.linkTo(path + '/' + name);
}
else {
linkUrl = this.getDownloadUrl(name, this.getCurrentDirectory());
linkUrl = this.getDownloadUrl(name, path);
}
td.append('<input id="select-' + this.id + '-' + fileData.id +
'" type="checkbox" /><label for="select-' + this.id + '-' + fileData.id + '"></label>');
@ -621,7 +644,8 @@
*
* @param fileData map of file attributes
* @param options map of attributes:
* - "updateSummary" true to update the summary after adding (default), false otherwise
* - "updateSummary": true to update the summary after adding (default), false otherwise
* - "silent": true to prevent firing events like "fileActionsReady"
* @return new tr element (not appended to the table)
*/
add: function(fileData, options) {
@ -693,6 +717,7 @@
options = options || {};
var type = fileData.type || 'file',
mime = fileData.mimetype,
path = fileData.path || this.getCurrentDirectory(),
permissions = parseInt(fileData.permissions, 10) || 0;
if (fileData.isShareMountPoint) {
@ -723,13 +748,13 @@
}
// display actions
this.fileActions.display(filenameTd, false);
this.fileActions.display(filenameTd, !options.silent, this);
if (fileData.isPreviewAvailable) {
// lazy load / newly inserted td ?
if (!fileData.icon) {
this.lazyLoadPreview({
path: this.getCurrentDirectory() + '/' + fileData.name,
path: path + '/' + fileData.name,
mime: mime,
etag: fileData.etag,
callback: function(url) {
@ -740,7 +765,7 @@
else {
// set the preview URL directly
var urlSpec = {
file: this.getCurrentDirectory() + '/' + fileData.name,
file: path + '/' + fileData.name,
c: fileData.etag
};
var previewUrl = this.generatePreviewUrl(urlSpec);
@ -783,13 +808,6 @@
return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/');
},
/**
* Sets the file actions handler
*/
setFileActions: function(fileActions) {
this.fileActions = fileActions;
},
/**
* Sets the current directory name and updates the breadcrumb.
* @param targetDir directory to display
@ -1213,16 +1231,16 @@
// reinsert row
self.files.splice(tr.index(), 1);
tr.remove();
self.add(fileInfo, {updateSummary: false});
self.$fileList.trigger($.Event('fileActionsReady'));
self.add(fileInfo, {updateSummary: false, silent: true});
self.$fileList.trigger($.Event('fileActionsReady', {fileList: self}));
}
});
} else {
// add back the old file info when cancelled
self.files.splice(tr.index(), 1);
tr.remove();
self.add(oldFileInfo, {updateSummary: false});
self.$fileList.trigger($.Event('fileActionsReady'));
self.add(oldFileInfo, {updateSummary: false, silent: true});
self.$fileList.trigger($.Event('fileActionsReady', {fileList: self}));
}
} catch (error) {
input.attr('title', error);

View file

@ -41,6 +41,10 @@ describe('OCA.Files.App tests', function() {
'</div>'
);
window.FileActions = new OCA.Files.FileActions();
OCA.Files.legacyFileActions = window.FileActions;
OCA.Files.fileActions = new OCA.Files.FileActions();
pushStateStub = sinon.stub(OC.Util.History, 'pushState');
parseUrlQueryStub = sinon.stub(OC.Util.History, 'parseUrlQuery');
parseUrlQueryStub.returns({});
@ -51,8 +55,6 @@ describe('OCA.Files.App tests', function() {
App.navigation = null;
App.fileList = null;
App.files = null;
App.fileActions.clear();
App.fileActions = null;
pushStateStub.restore();
parseUrlQueryStub.restore();
@ -64,6 +66,53 @@ describe('OCA.Files.App tests', function() {
expect(App.fileList.fileActions.actions.all).toBeDefined();
expect(App.fileList.$el.is('#app-content-files')).toEqual(true);
});
it('merges the legacy file actions with the default ones', function() {
var legacyActionStub = sinon.stub();
var actionStub = sinon.stub();
// legacy action
window.FileActions.register(
'all',
'LegacyTest',
OC.PERMISSION_READ,
OC.imagePath('core', 'actions/test'),
legacyActionStub
);
// legacy action to be overwritten
window.FileActions.register(
'all',
'OverwriteThis',
OC.PERMISSION_READ,
OC.imagePath('core', 'actions/test'),
legacyActionStub
);
// regular file actions
OCA.Files.fileActions.register(
'all',
'RegularTest',
OC.PERMISSION_READ,
OC.imagePath('core', 'actions/test'),
actionStub
);
// overwrite
OCA.Files.fileActions.register(
'all',
'OverwriteThis',
OC.PERMISSION_READ,
OC.imagePath('core', 'actions/test'),
actionStub
);
App.initialize();
var actions = App.fileList.fileActions.actions;
expect(actions.all.OverwriteThis.action).toBe(actionStub);
expect(actions.all.LegacyTest.action).toBe(legacyActionStub);
expect(actions.all.RegularTest.action).toBe(actionStub);
// default one still there
expect(actions.dir.Open.action).toBeDefined();
});
});
describe('URL handling', function() {

View file

@ -21,7 +21,7 @@
describe('OCA.Files.FileActions tests', function() {
var $filesTable, fileList;
var FileActions = OCA.Files.FileActions;
var FileActions;
beforeEach(function() {
// init horrible parameters
@ -31,10 +31,11 @@ describe('OCA.Files.FileActions tests', function() {
// dummy files table
$filesTable = $body.append('<table id="filestable"></table>');
fileList = new OCA.Files.FileList($('#testArea'));
FileActions.registerDefaultActions(fileList);
FileActions = new OCA.Files.FileActions();
FileActions.registerDefaultActions();
});
afterEach(function() {
FileActions.clear();
FileActions = null;
fileList = undefined;
$('#dir, #permissions, #filestable').remove();
});
@ -78,8 +79,8 @@ describe('OCA.Files.FileActions tests', function() {
};
var $tr = fileList.add(fileData);
FileActions.display($tr.find('td.filename'), true);
FileActions.display($tr.find('td.filename'), true);
FileActions.display($tr.find('td.filename'), true, fileList);
FileActions.display($tr.find('td.filename'), true, fileList);
// actions defined after cal
expect($tr.find('.action.action-download').length).toEqual(1);
@ -98,12 +99,39 @@ describe('OCA.Files.FileActions tests', function() {
mtime: '123456'
};
var $tr = fileList.add(fileData);
FileActions.display($tr.find('td.filename'), true);
FileActions.display($tr.find('td.filename'), true, fileList);
$tr.find('.action-download').click();
expect(redirectStub.calledOnce).toEqual(true);
expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=testName.txt');
expect(redirectStub.getCall(0).args[0]).toEqual(
OC.webroot +
'/index.php/apps/files/ajax/download.php' +
'?dir=%2Fsubdir&files=testName.txt');
redirectStub.restore();
});
it('takes the file\'s path into account when clicking download', function() {
var redirectStub = sinon.stub(OC, 'redirect');
var fileData = {
id: 18,
type: 'file',
name: 'testName.txt',
path: '/anotherpath/there',
mimetype: 'text/plain',
size: '1234',
etag: 'a01234c',
mtime: '123456'
};
var $tr = fileList.add(fileData);
FileActions.display($tr.find('td.filename'), true, fileList);
$tr.find('.action-download').click();
expect(redirectStub.calledOnce).toEqual(true);
expect(redirectStub.getCall(0).args[0]).toEqual(
OC.webroot + '/index.php/apps/files/ajax/download.php' +
'?dir=%2Fanotherpath%2Fthere&files=testName.txt'
);
redirectStub.restore();
});
it('deletes file when clicking delete', function() {
@ -118,11 +146,47 @@ describe('OCA.Files.FileActions tests', function() {
mtime: '123456'
};
var $tr = fileList.add(fileData);
FileActions.display($tr.find('td.filename'), true);
FileActions.display($tr.find('td.filename'), true, fileList);
$tr.find('.action.delete').click();
expect(deleteStub.calledOnce).toEqual(true);
deleteStub.restore();
});
it('passes context to action handler', function() {
var actionStub = sinon.stub();
var fileData = {
id: 18,
type: 'file',
name: 'testName.txt',
mimetype: 'text/plain',
size: '1234',
etag: 'a01234c',
mtime: '123456'
};
var $tr = fileList.add(fileData);
FileActions.register(
'all',
'Test',
OC.PERMISSION_READ,
OC.imagePath('core', 'actions/test'),
actionStub
);
FileActions.display($tr.find('td.filename'), true, fileList);
$tr.find('.action-test').click();
expect(actionStub.calledOnce).toEqual(true);
expect(actionStub.getCall(0).args[0]).toEqual('testName.txt');
var context = actionStub.getCall(0).args[1];
expect(context.$file.is($tr)).toEqual(true);
expect(context.fileList).toBeDefined();
expect(context.fileActions).toBeDefined();
expect(context.dir).toEqual('/subdir');
// when data-path is defined
actionStub.reset();
$tr.attr('data-path', '/somepath');
$tr.find('.action-test').click();
context = actionStub.getCall(0).args[1];
expect(context.dir).toEqual('/somepath');
});
});

View file

@ -21,7 +21,6 @@
describe('OCA.Files.FileList tests', function() {
var testFiles, alertStub, notificationStub, fileList;
var FileActions = OCA.Files.FileActions;
/**
* Generate test file data
@ -117,15 +116,11 @@ describe('OCA.Files.FileList tests', function() {
}];
fileList = new OCA.Files.FileList($('#app-content-files'));
FileActions.clear();
FileActions.registerDefaultActions(fileList);
fileList.setFileActions(FileActions);
});
afterEach(function() {
testFiles = undefined;
fileList = undefined;
FileActions.clear();
notificationStub.restore();
alertStub.restore();
});
@ -488,7 +483,7 @@ describe('OCA.Files.FileList tests', function() {
var $input, request;
for (var i = 0; i < testFiles.length; i++) {
fileList.add(testFiles[i]);
fileList.add(testFiles[i], {silent: true});
}
// trigger rename prompt
@ -753,6 +748,20 @@ describe('OCA.Files.FileList tests', function() {
fileList.setFiles(testFiles);
expect(handler.calledOnce).toEqual(true);
});
it('triggers "fileActionsReady" event after single add', function() {
var handler = sinon.stub();
fileList.setFiles(testFiles);
fileList.$fileList.on('fileActionsReady', handler);
fileList.add({name: 'test.txt'});
expect(handler.calledOnce).toEqual(true);
});
it('does not trigger "fileActionsReady" event after single add with silent argument', function() {
var handler = sinon.stub();
fileList.setFiles(testFiles);
fileList.$fileList.on('fileActionsReady', handler);
fileList.add({name: 'test.txt'}, {silent: true});
expect(handler.notCalled).toEqual(true);
});
it('triggers "updated" event after update', function() {
var handler = sinon.stub();
fileList.$fileList.on('updated', handler);
@ -1512,6 +1521,32 @@ describe('OCA.Files.FileList tests', function() {
expect(fileList.getSelectedFiles()).toEqual([]);
});
});
describe('File actions', function() {
it('Clicking on a file name will trigger default action', function() {
var actionStub = sinon.stub();
fileList.setFiles(testFiles);
fileList.fileActions.register(
'text/plain',
'Test',
OC.PERMISSION_ALL,
function() {
// Specify icon for hitory button
return OC.imagePath('core','actions/history');
},
actionStub
);
fileList.fileActions.setDefault('text/plain', 'Test');
var $tr = fileList.findFileEl('One.txt');
$tr.find('td.filename>a.name').click();
expect(actionStub.calledOnce).toEqual(true);
expect(actionStub.getCall(0).args[0]).toEqual('One.txt');
var context = actionStub.getCall(0).args[1];
expect(context.$file.is($tr)).toEqual(true);
expect(context.fileList).toBeDefined();
expect(context.fileActions).toBeDefined();
expect(context.dir).toEqual('/subdir');
});
});
describe('Sorting files', function() {
it('Sorts by name by default', function() {
fileList.reload();

View file

@ -1,4 +1,5 @@
<?php
$l = OC_L10N::get('files_sharing');
OC::$CLASSPATH['OC_Share_Backend_File'] = 'files_sharing/lib/share/file.php';
OC::$CLASSPATH['OC_Share_Backend_Folder'] = 'files_sharing/lib/share/folder.php';
@ -21,3 +22,22 @@ OCP\Util::addScript('files_sharing', 'share');
\OC_Hook::connect('OC_Appconfig', 'post_set_value', '\OCA\Files\Share\Maintainer', 'configChangeHook');
OC_FileProxy::register(new OCA\Files\Share\Proxy());
\OCA\Files\App::getNavigationManager()->add(
array(
"id" => 'sharingin',
"appname" => 'files_sharing',
"script" => 'list.php',
"order" => 10,
"name" => $l->t('Shared with you')
)
);
\OCA\Files\App::getNavigationManager()->add(
array(
"id" => 'sharingout',
"appname" => 'files_sharing',
"script" => 'list.php',
"order" => 15,
"name" => $l->t('Shared with others')
)
);

View file

@ -0,0 +1,3 @@
#filestable.shareList .summary .filesize {
display: none;
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
OCA.Sharing = {};
OCA.Sharing.App = {
_inFileList: null,
_outFileList: null,
initSharingIn: function($el) {
if (this._inFileList) {
return this._inFileList;
}
this._inFileList = new OCA.Sharing.FileList(
$el,
{
scrollContainer: $('#app-content'),
sharedWithUser: true,
fileActions: this._createFileActions()
}
);
this._extendFileList(this._inFileList);
this._inFileList.appName = t('files_sharing', 'Shared with you');
this._inFileList.$el.find('#emptycontent').text(t('files_sharing', 'No files have been shared with you yet.'));
return this._inFileList;
},
initSharingOut: function($el) {
if (this._outFileList) {
return this._outFileList;
}
this._outFileList = new OCA.Sharing.FileList(
$el,
{
scrollContainer: $('#app-content'),
sharedWithUser: false,
fileActions: this._createFileActions()
}
);
this._extendFileList(this._outFileList);
this._outFileList.appName = t('files_sharing', 'Shared with others');
this._outFileList.$el.find('#emptycontent').text(t('files_sharing', 'You haven\'t shared any files yet.'));
return this._outFileList;
},
removeSharingIn: function() {
if (this._inFileList) {
this._inFileList.$fileList.empty();
}
},
removeSharingOut: function() {
if (this._outFileList) {
this._outFileList.$fileList.empty();
}
},
_createFileActions: function() {
// inherit file actions from the files app
var fileActions = new OCA.Files.FileActions();
// note: not merging the legacy actions because legacy apps are not
// compatible with the sharing overview and need to be adapted first
fileActions.registerDefaultActions();
fileActions.merge(OCA.Files.fileActions);
// when the user clicks on a folder, redirect to the corresponding
// folder in the files app instead of opening it directly
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
OCA.Files.App.setActiveView('files', {silent: true});
OCA.Files.App.fileList.changeDirectory(context.$file.attr('data-path') + '/' + filename, true, true);
});
fileActions.setDefault('dir', 'Open');
return fileActions;
},
_extendFileList: function(fileList) {
// remove size column from summary
fileList.fileSummary.$el.find('.filesize').remove();
}
};
$(document).ready(function() {
$('#app-content-sharingin').on('show', function(e) {
OCA.Sharing.App.initSharingIn($(e.target));
});
$('#app-content-sharingin').on('hide', function() {
OCA.Sharing.App.removeSharingIn();
});
$('#app-content-sharingout').on('show', function(e) {
OCA.Sharing.App.initSharingOut($(e.target));
});
$('#app-content-sharingout').on('hide', function() {
OCA.Sharing.App.removeSharingOut();
});
});

View file

@ -19,9 +19,18 @@ OCA.Sharing.PublicApp = {
initialize: function($el) {
var self = this;
var fileActions;
if (this._initialized) {
return;
}
fileActions = new OCA.Files.FileActions();
// default actions
fileActions.registerDefaultActions();
// legacy actions
fileActions.merge(window.FileActions);
// regular actions
fileActions.merge(OCA.Files.fileActions);
this._initialized = true;
this.initialDir = $('#dir').val();
@ -32,7 +41,8 @@ OCA.Sharing.PublicApp = {
{
scrollContainer: $(window),
dragOptions: dragOptions,
folderDropOptions: folderDropOptions
folderDropOptions: folderDropOptions,
fileActions: fileActions
}
);
this.files = OCA.Files.Files;
@ -121,10 +131,8 @@ OCA.Sharing.PublicApp = {
};
});
this.fileActions = _.extend({}, OCA.Files.FileActions);
this.fileActions.registerDefaultActions(this.fileList);
delete this.fileActions.actions.all.Share;
this.fileList.setFileActions(this.fileActions);
// do not allow sharing from the public page
delete this.fileList.fileActions.actions.all.Share;
this.fileList.changeDirectory(this.initialDir || '/', false, true);
@ -158,7 +166,10 @@ OCA.Sharing.PublicApp = {
$(document).ready(function() {
var App = OCA.Sharing.PublicApp;
App.initialize($('#preview'));
// defer app init, to give a chance to plugins to register file actions
_.defer(function() {
App.initialize($('#preview'));
});
if (window.Files) {
// HACK: for oc-dialogs previews that depends on Files:

View file

@ -8,12 +8,8 @@
*
*/
/* global FileList, FileActions */
$(document).ready(function() {
var sharesLoaded = false;
if (typeof OC.Share !== 'undefined' && typeof FileActions !== 'undefined') {
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;
@ -31,10 +27,12 @@ $(document).ready(function() {
};
}
$('#fileList').on('fileActionsReady',function(){
// 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);
@ -62,46 +60,50 @@ $(document).ready(function() {
return $result;
});
}
})
});
// FIXME: these calls are also working on hard-coded
// list selectors...
if (!sharesLoaded){
OC.Share.loadIcons('file');
if (!OCA.Sharing.sharesLoaded){
OC.Share.loadIcons('file', fileList);
// assume that we got all shares, so switching directories
// will not invalidate that list
sharesLoaded = true;
OCA.Sharing.sharesLoaded = true;
}
else{
OC.Share.updateIcons('file');
OC.Share.updateIcons('file', fileList);
}
});
FileActions.register('all', 'Share', OC.PERMISSION_SHARE, OC.imagePath('core', 'actions/share'), function(filename) {
var tr = FileList.findFileEl(filename);
OCA.Files.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') {
if ($tr.data('type') === 'dir') {
itemType = 'folder';
}
var possiblePermissions = $(tr).data('reshare-permissions');
var possiblePermissions = $tr.data('reshare-permissions');
if (_.isUndefined(possiblePermissions)) {
possiblePermissions = $(tr).data('permissions');
possiblePermissions = $tr.data('permissions');
}
var appendTo = $(tr).find('td.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')) {
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);
$tr.addClass('mouseOver');
OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename);
});
} else {
OC.Share.hideDropDown();
}
} else {
$(tr).addClass('mouseOver');
OC.Share.showDropDown(itemType, $(tr).data('id'), appendTo, true, possiblePermissions, filename);
$tr.addClass('mouseOver');
OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename);
}
});
}

View file

@ -0,0 +1,235 @@
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* Sharing file list
*
* Contains both "shared with others" and "shared with you" modes.
*/
var FileList = function($el, options) {
this.initialize($el, options);
};
FileList.prototype = _.extend({}, OCA.Files.FileList.prototype, {
appName: 'Shares',
/**
* Whether the list shows the files shared with the user (true) or
* the files that the user shared with others (false).
*/
_sharedWithUser: false,
initialize: function($el, options) {
OCA.Files.FileList.prototype.initialize.apply(this, arguments);
if (this.initialized) {
return;
}
if (options && options.sharedWithUser) {
this._sharedWithUser = true;
}
},
_createRow: function(fileData) {
// TODO: hook earlier and render the whole row here
var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments);
$tr.find('.filesize').remove();
$tr.find('td.date').before($tr.children('td:first'));
$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);
}
return $tr;
},
/**
* Set whether the list should contain outgoing shares
* or incoming shares.
*
* @param state true for incoming shares, false otherwise
*/
setSharedWithUser: function(state) {
this._sharedWithUser = !!state;
},
updateEmptyContent: function() {
var dir = this.getCurrentDirectory();
if (dir === '/') {
// root has special permissions
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
}
else {
OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
}
},
getDirectoryPermissions: function() {
return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
},
updateStorageStatistics: function() {
// no op because it doesn't have
// storage info like free space / used space
},
reload: function() {
var self = this;
this.showMask();
if (this._reloadCall) {
this._reloadCall.abort();
}
this._reloadCall = $.ajax({
url: OC.linkToOCS('apps/files_sharing/api/v1') + 'shares',
/* jshint camelcase: false */
data: {
format: 'json',
shared_with_me: !!this._sharedWithUser
},
type: 'GET',
beforeSend: function(xhr) {
xhr.setRequestHeader('OCS-APIREQUEST', 'true');
},
error: function(result) {
self.reloadCallback(result);
},
success: function(result) {
self.reloadCallback(result);
}
});
},
reloadCallback: function(result) {
delete this._reloadCall;
this.hideMask();
this.$el.find('#headerSharedWith').text(
t('files_sharing', this._sharedWithUser ? 'Shared by' : 'Shared with')
);
if (result.ocs && result.ocs.data) {
this.setFiles(this._makeFilesFromShares(result.ocs.data));
}
else {
// TODO: error handling
}
},
/**
* Converts the OCS API share response data to a file info
* list
* @param OCS API share array
* @return array of file info maps
*/
_makeFilesFromShares: function(data) {
var self = this;
// OCS API uses non-camelcased names
var files = _.chain(data)
// convert share data to file data
.map(function(share) {
/* jshint camelcase: false */
var file = {
id: share.file_source,
mimetype: share.mimetype
};
if (share.item_type === 'folder') {
file.type = 'dir';
file.mimetype = 'httpd/unix-directory';
}
else {
file.type = 'file';
// force preview retrieval as we don't have mime types,
// the preview endpoint will fall back to the mime type
// icon if no preview exists
file.isPreviewAvailable = true;
file.icon = true;
}
file.share = {
id: share.id,
type: share.share_type,
target: share.share_with,
stime: share.stime * 1000,
};
if (self._sharedWithUser) {
file.share.ownerDisplayName = share.displayname_owner;
file.name = OC.basename(share.file_target);
file.path = OC.dirname(share.file_target);
file.permissions = share.permissions;
}
else {
file.share.targetDisplayName = share.share_with_displayname;
file.name = OC.basename(share.path);
file.path = OC.dirname(share.path);
file.permissions = OC.PERMISSION_ALL;
}
return file;
})
// Group all files and have a "shares" array with
// the share info for each file.
//
// This uses a hash memo to cumulate share information
// 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;
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 = {};
// counter is cheaper than calling _.keys().length
data.counterPartsCount = 0;
data.mtime = file.share.stime;
}
else {
// always take the most recent stime
if (file.share.stime > data.mtime) {
data.mtime = file.share.stime;
}
data.shares.push(file.share);
}
if (file.share.type === OC.Share.SHARE_TYPE_LINK) {
data.hasLinkShare = true;
} else if (counterPart && data.counterPartsCount < 10) {
// limit counterparts for output
data.counterParts[counterPart] = true;
data.counterPartsCount++;
}
delete file.share;
return memo;
}, {})
// Retrieve only the values of the returned hash
.values()
// Clean up
.each(function(data) {
// convert the counterParts 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;
})
// Sort by expected sort comparator
.sortBy(this._sortComparator)
// Finish the chain by getting the result
.value();
return files;
}
});
OCA.Sharing.FileList = FileList;
})();

View file

@ -31,6 +31,9 @@ class Api {
* @return \OC_OCS_Result share information
*/
public static function getAllShares($params) {
if (isset($_GET['shared_with_me']) && $_GET['shared_with_me'] !== 'false') {
return self::getFilesSharedWithMe();
}
// if a file is specified, get the share for this file
if (isset($_GET['path'])) {
$params['itemSource'] = self::getFileId($_GET['path']);
@ -49,12 +52,20 @@ class Api {
return self::collectShares($params);
}
$share = \OCP\Share::getItemShared('file', null);
$shares = \OCP\Share::getItemShared('file', null);
if ($share === false) {
if ($shares === false) {
return new \OC_OCS_Result(null, 404, 'could not get shares');
} else {
return new \OC_OCS_Result($share);
foreach ($shares as &$share) {
// file_target might not be set if the target user hasn't mounted
// the filesystem yet
if ($share['item_type'] === 'file' && isset($share['file_target'])) {
$share['mimetype'] = \OC_Helper::getFileNameMimeType($share['file_target']);
}
$newShares[] = $share;
}
return new \OC_OCS_Result($shares);
}
}
@ -195,6 +206,27 @@ class Api {
return new \OC_OCS_Result($result);
}
/**
* get files shared with the user
* @return \OC_OCS_Result
*/
private static function getFilesSharedWithMe() {
try {
$shares = \OCP\Share::getItemsSharedWith('file');
foreach ($shares as &$share) {
if ($share['item_type'] === 'file') {
$share['mimetype'] = \OC_Helper::getFileNameMimeType($share['file_target']);
}
}
$result = new \OC_OCS_Result($shares);
} catch (\Exception $e) {
$result = new \OC_OCS_Result(null, 403, $e->getMessage());
}
return $result;
}
/**
* create a new share
* @param array $params

View file

@ -0,0 +1,11 @@
<?php
// Check if we are a user
OCP\User::checkLoggedIn();
$tmpl = new OCP\Template('files_sharing', 'list', '');
OCP\Util::addScript('files_sharing', 'app');
OCP\Util::addScript('files_sharing', 'sharedfilelist');
$tmpl->printPage();

View file

@ -0,0 +1,28 @@
<?php /** @var $l OC_L10N */ ?>
<div id="controls">
<div id="file_action_panel"></div>
</div>
<div id='notification'></div>
<div id="emptycontent" class="hidden"></div>
<input type="hidden" name="dir" value="" id="dir">
<table id="filestable">
<thead>
<tr>
<th id='headerName' class="hidden column-name">
<div id="headerName-container">
<a class="name sort columntitle" data-sort="name"><span><?php p($l->t( 'Name' )); ?></span><span class="sort-indicator"></span></a>
</div>
</th>
<th id="headerDate" class="hidden column-mtime">
<a id="modified" class="columntitle" data-sort="mtime"><span><?php p($l->t( 'Share time' )); ?></span><span class="sort-indicator"></span></a>
</th>
</tr>
</thead>
<tbody id="fileList">
</tbody>
<tfoot>
</tfoot>
</table>

View file

@ -0,0 +1,143 @@
/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
describe('OCA.Sharing.App tests', function() {
var App = OCA.Sharing.App;
var fileListIn;
var fileListOut;
beforeEach(function() {
$('#testArea').append(
'<div id="app-navigation">' +
'<ul><li data-id="files"><a>Files</a></li>' +
'<li data-id="sharingin"><a></a></li>' +
'<li data-id="sharingout"><a></a></li>' +
'</ul></div>' +
'<div id="app-content">' +
'<div id="app-content-files" class="hidden">' +
'</div>' +
'<div id="app-content-sharingin" class="hidden">' +
'</div>' +
'<div id="app-content-sharingout" class="hidden">' +
'</div>' +
'</div>' +
'</div>'
);
fileListIn = App.initSharingIn($('#app-content-sharingin'));
fileListOut = App.initSharingOut($('#app-content-sharingout'));
});
afterEach(function() {
App._inFileList = null;
App._outFileList = null;
fileListIn = null;
fileListOut = null;
});
describe('initialization', function() {
it('inits sharing-in list on show', function() {
expect(fileListIn._sharedWithUser).toEqual(true);
});
it('inits sharing-out list on show', function() {
expect(fileListOut._sharedWithUser).toBeFalsy();
});
});
describe('file actions', function() {
it('provides default file actions', function() {
_.each([fileListIn, fileListOut], function(fileList) {
var fileActions = fileList.fileActions;
expect(fileActions.actions.all).toBeDefined();
expect(fileActions.actions.all.Delete).toBeDefined();
expect(fileActions.actions.all.Rename).toBeDefined();
expect(fileActions.actions.file.Download).toBeDefined();
expect(fileActions.defaults.dir).toEqual('Open');
});
});
it('provides custom file actions', function() {
var actionStub = sinon.stub();
// regular file action
OCA.Files.fileActions.register(
'all',
'RegularTest',
OC.PERMISSION_READ,
OC.imagePath('core', 'actions/shared'),
actionStub
);
App._inFileList = null;
fileListIn = App.initSharingIn($('#app-content-sharingin'));
expect(fileListIn.fileActions.actions.all.RegularTest).toBeDefined();
});
it('does not provide legacy file actions', function() {
var actionStub = sinon.stub();
// legacy file action
window.FileActions.register(
'all',
'LegacyTest',
OC.PERMISSION_READ,
OC.imagePath('core', 'actions/shared'),
actionStub
);
App._inFileList = null;
fileListIn = App.initSharingIn($('#app-content-sharingin'));
expect(fileListIn.fileActions.actions.all.LegacyTest).not.toBeDefined();
});
it('redirects to files app when opening a directory', function() {
var oldList = OCA.Files.App.fileList;
// dummy new list to make sure it exists
OCA.Files.App.fileList = new OCA.Files.FileList($('<table><thead></thead><tbody></tbody></table>'));
var setActiveViewStub = sinon.stub(OCA.Files.App, 'setActiveView');
// create dummy table so we can click the dom
var $table = '<table><thead></thead><tbody id="fileList"></tbody></table>';
$('#app-content-sharingin').append($table);
App._inFileList = null;
fileListIn = App.initSharingIn($('#app-content-sharingin'));
fileListIn.setFiles([{
name: 'testdir',
type: 'dir',
path: '/somewhere/inside/subdir',
counterParts: ['user2'],
shares: [{
ownerDisplayName: 'user2'
}]
}]);
fileListIn.findFileEl('testdir').find('td a.name').click();
expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir');
expect(setActiveViewStub.calledOnce).toEqual(true);
expect(setActiveViewStub.calledWith('files')).toEqual(true);
setActiveViewStub.restore();
// restore old list
OCA.Files.App.fileList = oldList;
});
});
});

View file

@ -0,0 +1,412 @@
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
describe('OCA.Sharing.FileList tests', function() {
var testFiles, alertStub, notificationStub, fileList;
beforeEach(function() {
alertStub = sinon.stub(OC.dialogs, 'alert');
notificationStub = sinon.stub(OC.Notification, 'show');
// init parameters and test table elements
$('#testArea').append(
'<div id="app-content-container">' +
// init horrible parameters
'<input type="hidden" id="dir" value="/"></input>' +
'<input type="hidden" id="permissions" value="31"></input>' +
// dummy controls
'<div id="controls">' +
' <div class="actions creatable"></div>' +
' <div class="notCreatable"></div>' +
'</div>' +
// dummy table
// TODO: at some point this will be rendered by the fileList class itself!
'<table id="filestable">' +
'<thead><tr>' +
'<th id="headerName" class="hidden column-name">' +
'<input type="checkbox" id="select_all_files" class="select-all">' +
'<a class="name columntitle" data-sort="name"><span>Name</span><span class="sort-indicator"></span></a>' +
'<span class="selectedActions hidden">' +
'</th>' +
'<th class="hidden column-mtime">' +
'<a class="columntitle" data-sort="mtime"><span class="sort-indicator"></span></a>' +
'</th>' +
'</tr></thead>' +
'<tbody id="fileList"></tbody>' +
'<tfoot></tfoot>' +
'</table>' +
'<div id="emptycontent">Empty content message</div>' +
'</div>'
);
});
afterEach(function() {
testFiles = undefined;
fileList = undefined;
notificationStub.restore();
alertStub.restore();
});
describe('loading file list for incoming shares', function() {
var ocsResponse;
beforeEach(function() {
fileList = new OCA.Sharing.FileList(
$('#app-content-container'), {
sharedWithUser: true
}
);
fileList.reload();
/* jshint camelcase: false */
ocsResponse = {
ocs: {
meta: {
status: 'ok',
statuscode: 100,
message: null
},
data: [{
id: 7,
item_type: 'file',
item_source: 49,
item_target: '/49',
file_source: 49,
file_target: '/local path/local name.txt',
path: 'files/something shared.txt',
permissions: 31,
stime: 11111,
share_type: OC.Share.SHARE_TYPE_USER,
share_with: 'user1',
share_with_displayname: 'User One',
mimetype: 'text/plain',
uid_owner: 'user2',
displayname_owner: 'User Two'
}]
}
};
});
it('render file shares', function() {
var request;
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(
OC.linkToOCS('apps/files_sharing/api/v1') +
'shares?format=json&shared_with_me=true'
);
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(ocsResponse)
);
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(1);
expect($tr.attr('data-id')).toEqual('49');
expect($tr.attr('data-type')).toEqual('file');
expect($tr.attr('data-file')).toEqual('local name.txt');
expect($tr.attr('data-path')).toEqual('/local path');
expect($tr.attr('data-size')).not.toBeDefined();
expect($tr.attr('data-permissions')).toEqual('31'); // read and delete
expect($tr.attr('data-mime')).toEqual('text/plain');
expect($tr.attr('data-mtime')).toEqual('11111000');
expect($tr.attr('data-share-owner')).toEqual('User Two');
expect($tr.attr('data-share-id')).toEqual('7');
expect($tr.find('a.name').attr('href')).toEqual(
OC.webroot +
'/index.php/apps/files/ajax/download.php' +
'?dir=%2Flocal%20path&files=local%20name.txt'
);
expect($tr.find('.nametext').text().trim()).toEqual('local name.txt');
});
it('render folder shares', function() {
/* jshint camelcase: false */
var request;
ocsResponse.ocs.data[0] = _.extend(ocsResponse.ocs.data[0], {
item_type: 'folder',
file_target: '/local path/local name',
path: 'files/something shared',
});
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(
OC.linkToOCS('apps/files_sharing/api/v1') +
'shares?format=json&shared_with_me=true'
);
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(ocsResponse)
);
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(1);
expect($tr.attr('data-id')).toEqual('49');
expect($tr.attr('data-type')).toEqual('dir');
expect($tr.attr('data-file')).toEqual('local name');
expect($tr.attr('data-path')).toEqual('/local path');
expect($tr.attr('data-size')).not.toBeDefined();
expect($tr.attr('data-permissions')).toEqual('31'); // read and delete
expect($tr.attr('data-mime')).toEqual('httpd/unix-directory');
expect($tr.attr('data-mtime')).toEqual('11111000');
expect($tr.attr('data-share-owner')).toEqual('User Two');
expect($tr.attr('data-share-id')).toEqual('7');
expect($tr.find('a.name').attr('href')).toEqual(
OC.webroot +
'/index.php/apps/files' +
'?dir=/local%20path/local%20name'
);
expect($tr.find('.nametext').text().trim()).toEqual('local name');
});
});
describe('loading file list for outgoing shares', function() {
var ocsResponse;
beforeEach(function() {
fileList = new OCA.Sharing.FileList(
$('#app-content-container'), {
sharedWithUser: false
}
);
fileList.reload();
/* jshint camelcase: false */
ocsResponse = {
ocs: {
meta: {
status: 'ok',
statuscode: 100,
message: null
},
data: [{
id: 7,
item_type: 'file',
item_source: 49,
file_source: 49,
path: '/local path/local name.txt',
permissions: 27,
stime: 11111,
share_type: OC.Share.SHARE_TYPE_USER,
share_with: 'user2',
share_with_displayname: 'User Two',
mimetype: 'text/plain',
uid_owner: 'user1',
displayname_owner: 'User One'
}]
}
};
});
it('render file shares', function() {
var request;
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(
OC.linkToOCS('apps/files_sharing/api/v1') +
'shares?format=json&shared_with_me=false'
);
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(ocsResponse)
);
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(1);
expect($tr.attr('data-id')).toEqual('49');
expect($tr.attr('data-type')).toEqual('file');
expect($tr.attr('data-file')).toEqual('local name.txt');
expect($tr.attr('data-path')).toEqual('/local path');
expect($tr.attr('data-size')).not.toBeDefined();
expect($tr.attr('data-permissions')).toEqual('31'); // read and delete
expect($tr.attr('data-mime')).toEqual('text/plain');
expect($tr.attr('data-mtime')).toEqual('11111000');
expect($tr.attr('data-share-owner')).not.toBeDefined();
expect($tr.attr('data-share-id')).toEqual('7');
expect($tr.find('a.name').attr('href')).toEqual(
OC.webroot +
'/index.php/apps/files/ajax/download.php' +
'?dir=%2Flocal%20path&files=local%20name.txt'
);
expect($tr.find('.nametext').text().trim()).toEqual('local name.txt');
});
it('render folder shares', function() {
var request;
/* jshint camelcase: false */
ocsResponse.ocs.data[0] = _.extend(ocsResponse.ocs.data[0], {
item_type: 'folder',
path: '/local path/local name',
});
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(
OC.linkToOCS('apps/files_sharing/api/v1') +
'shares?format=json&shared_with_me=false'
);
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(ocsResponse)
);
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(1);
expect($tr.attr('data-id')).toEqual('49');
expect($tr.attr('data-type')).toEqual('dir');
expect($tr.attr('data-file')).toEqual('local name');
expect($tr.attr('data-path')).toEqual('/local path');
expect($tr.attr('data-size')).not.toBeDefined();
expect($tr.attr('data-permissions')).toEqual('31'); // read and delete
expect($tr.attr('data-mime')).toEqual('httpd/unix-directory');
expect($tr.attr('data-mtime')).toEqual('11111000');
expect($tr.attr('data-share-owner')).not.toBeDefined();
expect($tr.attr('data-share-id')).toEqual('7');
expect($tr.find('a.name').attr('href')).toEqual(
OC.webroot +
'/index.php/apps/files' +
'?dir=/local%20path/local%20name'
);
expect($tr.find('.nametext').text().trim()).toEqual('local name');
});
it('render link shares', function() {
/* jshint camelcase: false */
var request;
ocsResponse.ocs.data[0] = {
id: 7,
item_type: 'file',
item_source: 49,
file_source: 49,
path: '/local path/local name.txt',
permissions: 1,
stime: 11111,
share_type: OC.Share.SHARE_TYPE_LINK,
share_with: null,
token: 'abc',
mimetype: 'text/plain',
uid_owner: 'user1',
displayname_owner: 'User One'
};
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(
OC.linkToOCS('apps/files_sharing/api/v1') +
'shares?format=json&shared_with_me=false'
);
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(ocsResponse)
);
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(1);
expect($tr.attr('data-id')).toEqual('49');
expect($tr.attr('data-type')).toEqual('file');
expect($tr.attr('data-file')).toEqual('local name.txt');
expect($tr.attr('data-path')).toEqual('/local path');
expect($tr.attr('data-size')).not.toBeDefined();
expect($tr.attr('data-permissions')).toEqual('31'); // read and delete
expect($tr.attr('data-mime')).toEqual('text/plain');
expect($tr.attr('data-mtime')).toEqual('11111000');
expect($tr.attr('data-share-owner')).not.toBeDefined();
expect($tr.attr('data-share-id')).toEqual('7');
expect($tr.find('a.name').attr('href')).toEqual(
OC.webroot +
'/index.php/apps/files/ajax/download.php' +
'?dir=%2Flocal%20path&files=local%20name.txt');
expect($tr.find('.nametext').text().trim()).toEqual('local name.txt');
});
it('groups link shares with regular shares', function() {
/* jshint camelcase: false */
var request;
// link share
ocsResponse.ocs.data.push({
id: 8,
item_type: 'file',
item_source: 49,
file_source: 49,
path: '/local path/local name.txt',
permissions: 1,
stime: 11111,
share_type: OC.Share.SHARE_TYPE_LINK,
share_with: null,
token: 'abc',
mimetype: 'text/plain',
uid_owner: 'user1',
displayname_owner: 'User One'
});
// another share of the same file
ocsResponse.ocs.data.push({
id: 9,
item_type: 'file',
item_source: 49,
file_source: 49,
path: '/local path/local name.txt',
permissions: 27,
stime: 22222,
share_type: OC.Share.SHARE_TYPE_USER,
share_with: 'user3',
share_with_displayname: 'User Three',
mimetype: 'text/plain',
uid_owner: 'user1',
displayname_owner: 'User One'
});
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(
OC.linkToOCS('apps/files_sharing/api/v1') +
'shares?format=json&shared_with_me=false'
);
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(ocsResponse)
);
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(1);
expect($tr.attr('data-id')).toEqual('49');
expect($tr.attr('data-type')).toEqual('file');
expect($tr.attr('data-file')).toEqual('local name.txt');
expect($tr.attr('data-path')).toEqual('/local path');
expect($tr.attr('data-size')).not.toBeDefined();
expect($tr.attr('data-permissions')).toEqual('31'); // read and delete
expect($tr.attr('data-mime')).toEqual('text/plain');
// always use the most recent stime
expect($tr.attr('data-mtime')).toEqual('22222000');
expect($tr.attr('data-share-owner')).not.toBeDefined();
expect($tr.attr('data-share-id')).toEqual('7,8,9');
expect($tr.find('a.name').attr('href')).toEqual(
OC.webroot +
'/index.php/apps/files/ajax/download.php' +
'?dir=%2Flocal%20path&files=local%20name.txt'
);
expect($tr.find('.nametext').text().trim()).toEqual('local name.txt');
});
});
});

View file

@ -9,7 +9,7 @@ array(
"id" => 'trashbin',
"appname" => 'files_trashbin',
"script" => 'list.php',
"order" => 1,
"order" => 50,
"name" => $l->t('Deleted files')
)
);

View file

@ -19,27 +19,26 @@ OCA.Trashbin.App = {
this._initialized = true;
this.fileList = new OCA.Trashbin.FileList(
$('#app-content-trashbin'), {
scrollContainer: $('#app-content')
scrollContainer: $('#app-content'),
fileActions: this._createFileActions()
}
);
this.registerFileActions(this.fileList);
},
registerFileActions: function(fileList) {
var self = this;
var fileActions = _.extend({}, OCA.Files.FileActions);
fileActions.clear();
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename) {
var dir = fileList.getCurrentDirectory();
_createFileActions: function() {
var fileActions = new OCA.Files.FileActions();
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
var dir = context.fileList.getCurrentDirectory();
if (dir !== '/') {
dir = dir + '/';
}
fileList.changeDirectory(dir + filename);
context.fileList.changeDirectory(dir + filename);
});
fileActions.setDefault('dir', 'Open');
fileActions.register('all', 'Restore', OC.PERMISSION_READ, OC.imagePath('core', 'actions/history'), function(filename) {
fileActions.register('all', 'Restore', OC.PERMISSION_READ, OC.imagePath('core', 'actions/history'), function(filename, context) {
var fileList = context.fileList;
var tr = fileList.findFileEl(filename);
var deleteAction = tr.children("td.date").children(".action.delete");
deleteAction.removeClass('delete-icon').addClass('progress-icon');
@ -54,7 +53,8 @@ OCA.Trashbin.App = {
fileActions.register('all', 'Delete', OC.PERMISSION_READ, function() {
return OC.imagePath('core', 'actions/delete');
}, function(filename) {
}, function(filename, context) {
var fileList = context.fileList;
$('.tipsy').remove();
var tr = fileList.findFileEl(filename);
var deleteAction = tr.children("td.date").children(".action.delete");
@ -67,7 +67,7 @@ OCA.Trashbin.App = {
_.bind(fileList._removeCallback, fileList)
);
});
fileList.setFileActions(fileActions);
return fileActions;
}
};

View file

@ -26,8 +26,8 @@
return name;
}
var FileList = function($el) {
this.initialize($el);
var FileList = function($el, options) {
this.initialize($el, options);
};
FileList.prototype = _.extend({}, OCA.Files.FileList.prototype, {
id: 'trashbin',

View file

@ -21,7 +21,6 @@
describe('OCA.Trashbin.FileList tests', function() {
var testFiles, alertStub, notificationStub, fileList;
var FileActions = OCA.Files.FileActions;
beforeEach(function() {
alertStub = sinon.stub(OC.dialogs, 'alert');
@ -87,14 +86,18 @@ describe('OCA.Trashbin.FileList tests', function() {
etag: '456'
}];
fileList = new OCA.Trashbin.FileList($('#app-content-trashbin'));
OCA.Trashbin.App.registerFileActions(fileList);
// register file actions like the trashbin App does
var fileActions = OCA.Trashbin.App._createFileActions(fileList);
fileList = new OCA.Trashbin.FileList(
$('#app-content-trashbin'), {
fileActions: fileActions
}
);
});
afterEach(function() {
testFiles = undefined;
fileList = undefined;
FileActions.clear();
$('#dir').remove();
notificationStub.restore();
alertStub.restore();

View file

@ -1,3 +1,14 @@
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
/* global scanFiles, escapeHTML, formatDate */
$(document).ready(function(){
if ($('#isPublic').val()){
@ -7,21 +18,20 @@ $(document).ready(function(){
return;
}
if (typeof FileActions !== 'undefined') {
if (OCA.Files) {
// Add versions button to 'files/index.php'
FileActions.register(
'file'
, 'Versions'
, OC.PERMISSION_UPDATE
, function() {
OCA.Files.fileActions.register(
'file',
'Versions',
OC.PERMISSION_UPDATE,
function() {
// Specify icon for hitory button
return OC.imagePath('core','actions/history');
}
,function(filename){
}, function(filename, context){
// Action to perform when clicked
if (scanFiles.scanning){return;}//workaround to prevent additional http request block scanning feedback
var file = $('#dir').val().replace(/(?!<=\/)$|\/$/, '/' + filename);
var file = context.dir.replace(/(?!<=\/)$|\/$/, '/' + filename);
var createDropDown = true;
// Check if drop down is already visible for a different file
if (($('#dropdown').length > 0) ) {
@ -33,10 +43,9 @@ $(document).ready(function(){
}
if(createDropDown === true) {
createVersionsDropdown(filename, file);
createVersionsDropdown(filename, file, context.fileList);
}
}
, t('files_versions', 'Versions')
}, t('files_versions', 'Versions')
);
}
@ -75,7 +84,7 @@ function goToVersionPage(url){
window.location.assign(url);
}
function createVersionsDropdown(filename, files) {
function createVersionsDropdown(filename, files, fileList) {
var start = 0;
var fileEl;
@ -88,7 +97,7 @@ function createVersionsDropdown(filename, files) {
html += '<input type="button" value="'+ t('files_versions', 'More versions...') + '" name="show-more-versions" id="show-more-versions" style="display: none;" />';
if (filename) {
fileEl = FileList.findFileEl(filename);
fileEl = fileList.findFileEl(filename);
fileEl.addClass('mouseOver');
$(html).appendTo(fileEl.find('td.filename'));
} else {

View file

@ -211,7 +211,16 @@ var OC={
linkToRemote:function(service) {
return window.location.protocol + '//' + window.location.host + OC.linkToRemoteBase(service);
},
/**
* Gets the base path for the given OCS API service.
* @param {string} service name
* @return {string} OCS API base path
*/
linkToOCS: function(service) {
return window.location.protocol + '//' + window.location.host + OC.webroot + '/ocs/v1.php/' + service + '/';
},
/**
* Generates the absolute url for the given relative url, which can contain parameters.
* @param {string} url
@ -1241,7 +1250,7 @@ OC.Util = {
* @return {string} fixed image path with png extension if SVG is not supported
*/
replaceSVGIcon: function(file) {
if (!OC.Util.hasSVGSupport()) {
if (file && !OC.Util.hasSVGSupport()) {
var i = file.lastIndexOf('.svg');
if (i >= 0) {
file = file.substr(0, i) + '.png' + file.substr(i+4);

View file

@ -10,8 +10,11 @@ OC.Share={
* Loads ALL share statuses from server, stores them in OC.Share.statuses then
* calls OC.Share.updateIcons() to update the files "Share" icon to "Shared"
* according to their share status and share type.
*
* @param itemType item type
* @param fileList file list instance, defaults to OCA.Files.App.fileList
*/
loadIcons:function(itemType) {
loadIcons:function(itemType, fileList) {
// Load all share icons
$.get(OC.filePath('core', 'ajax', 'share.php'), { fetch: 'getItemsSharedStatuses', itemType: itemType }, function(result) {
if (result && result.status === 'success') {
@ -19,7 +22,7 @@ OC.Share={
$.each(result.data, function(item, data) {
OC.Share.statuses[item] = data;
});
OC.Share.updateIcons(itemType);
OC.Share.updateIcons(itemType, fileList);
}
});
},
@ -27,40 +30,55 @@ OC.Share={
* Updates the files' "Share" icons according to the known
* sharing states stored in OC.Share.statuses.
* (not reloaded from server)
*
* @param itemType item type
* @param fileList file list instance
* defaults to OCA.Files.App.fileList
*/
updateIcons:function(itemType){
updateIcons:function(itemType, fileList){
var item;
var $fileList;
var currentDir;
if (!fileList && OCA.Files) {
fileList = OCA.Files.App.fileList;
}
// fileList is usually only defined in the files app
if (fileList) {
$fileList = fileList.$fileList;
currentDir = fileList.getCurrentDirectory();
}
for (item in OC.Share.statuses){
var image;
var data = OC.Share.statuses[item];
var hasLink = data['link'];
var hasLink = data.link;
// Links override shared in terms of icon display
if (hasLink) {
var image = OC.imagePath('core', 'actions/public');
image = OC.imagePath('core', 'actions/public');
} else {
var image = OC.imagePath('core', 'actions/shared');
image = OC.imagePath('core', 'actions/shared');
}
if (itemType != 'file' && itemType != 'folder') {
$('a.share[data-item="'+item+'"]').css('background', 'url('+image+') no-repeat center');
if (itemType !== 'file' && itemType !== 'folder') {
$fileList.find('a.share[data-item="'+item+'"]').css('background', 'url('+image+') no-repeat center');
} else {
var file = $('tr[data-id="'+item+'"]');
var file = $fileList.find('tr[data-id="'+item+'"]');
if (file.length > 0) {
var action = $(file).find('.fileactions .action[data-action="Share"]');
var img = action.find('img').attr('src', image);
action.addClass('permanent');
action.html(' <span>'+t('core', 'Shared')+'</span>').prepend(img);
} else {
var dir = $('#dir').val();
var dir = currentDir;
if (dir.length > 1) {
var last = '';
var path = dir;
// Search for possible parent folders that are shared
while (path != last) {
if (path == data['path'] && !data['link']) {
var actions = $('.fileactions .action[data-action="Share"]');
if (path === data.path && !data.link) {
var actions = $fileList.find('.fileactions .action[data-action="Share"]');
$.each(actions, function(index, action) {
var img = $(action).find('img');
if (img.attr('src') != OC.imagePath('core', 'actions/public')) {
if (img.attr('src') !== OC.imagePath('core', 'actions/public')) {
img.attr('src', image);
$(action).addClass('permanent');
$(action).html(' <span>'+t('core', 'Shared')+'</span>').prepend(img);
@ -100,14 +118,18 @@ OC.Share={
var file = $('tr').filterAttr('data-id', String(itemSource));
if (file.length > 0) {
var action = $(file).find('.fileactions .action').filterAttr('data-action', 'Share');
var img = action.find('img').attr('src', image);
if (shares) {
action.addClass('permanent');
action.html(' <span>'+ escapeHTML(t('core', 'Shared'))+'</span>').prepend(img);
} else {
action.removeClass('permanent');
action.html(' <span>'+ escapeHTML(t('core', 'Share'))+'</span>').prepend(img);
}
// 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(' <span>'+ escapeHTML(t('core', 'Shared'))+'</span>').prepend(img);
} else {
action.removeClass('permanent');
action.html(' <span>'+ escapeHTML(t('core', 'Share'))+'</span>').prepend(img);
}
});
}
}
if (shares) {

View file

@ -43,7 +43,19 @@ module.exports = function(config) {
return apps;
*/
// other apps tests don't run yet... needs further research / clean up
return ['files', 'files_trashbin'];
return [
'files',
'files_trashbin',
{
name: 'files_sharing',
srcFiles: [
// 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'
],
testFiles: ['apps/files_sharing/tests/js/*.js']
}];
}
// respect NOCOVERAGE env variable
@ -110,15 +122,30 @@ module.exports = function(config) {
files.push(corePath + 'tests/specs/*.js');
}
for ( var i = 0; i < appsToTest.length; i++ ) {
// add app JS
var srcFile = 'apps/' + appsToTest[i] + '/js/*.js';
files.push(srcFile);
if (enableCoverage) {
preprocessors[srcFile] = 'coverage';
function addApp(app) {
// if only a string was specified, expand to structure
if (typeof(app) === 'string') {
app = {
srcFiles: 'apps/' + app + '/js/*.js',
testFiles: 'apps/' + app + '/tests/js/*.js'
};
}
// add test specs
files.push('apps/' + appsToTest[i] + '/tests/js/*.js');
// add source files/patterns
files = files.concat(app.srcFiles || []);
// add test files/patterns
files = files.concat(app.testFiles || []);
if (enableCoverage) {
// add coverage entry for each file/pattern
for (var i = 0; i < app.srcFiles.length; i++) {
preprocessors[app.srcFiles[i]] = 'coverage';
}
}
}
// add source files for apps to test
for ( var i = 0; i < appsToTest.length; i++ ) {
addApp(appsToTest[i]);
}
// serve images to avoid warnings