diff options
author | Lukas Reschke <lukas@statuscode.ch> | 2014-05-30 13:42:24 +0200 |
---|---|---|
committer | Lukas Reschke <lukas@statuscode.ch> | 2014-05-30 13:42:24 +0200 |
commit | 517501ffbf369b24191d8b9a9f2ce44a9891fb97 (patch) | |
tree | f638ba7e2f3554b7b1a31f628496c7d0740fd2ac /apps | |
parent | 929882a32a020b6c05605f416fa55024b9a60d33 (diff) | |
parent | 7fac2b62e954b0f8a693516da1151c97efa2ee99 (diff) | |
download | nextcloud-server-517501ffbf369b24191d8b9a9f2ce44a9891fb97.tar.gz nextcloud-server-517501ffbf369b24191d8b9a9f2ce44a9891fb97.zip |
Merge pull request #8417 from owncloud/share-overview
Sharing overview page
Diffstat (limited to 'apps')
24 files changed, 1405 insertions, 131 deletions
diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 731dd7a23e7..0bcea2eceaf 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -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; diff --git a/apps/files/index.php b/apps/files/index.php index e24c535cb20..95ae7977ecc 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -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(); diff --git a/apps/files/js/app.js b/apps/files/js/app.js index 9155fb38cdb..71802948a5c 100644 --- a/apps/files/js/app.js +++ b/apps/files/js/app.js @@ -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 @@ -58,6 +65,22 @@ }, /** + * 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 */ _setupEvents: function() { @@ -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(); diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 085195e961d..3df62f37518 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -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; + // also set on global object for legacy apps + window.FileActions.currentFile = self.currentFile; + var file = self.getCurrentFile(); + var $tr = $(this).closest('tr'); - event.data.actionFunc(file); + 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; -})(); -// for backward compatibility -window.FileActions = OCA.Files.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); + }; +})(); diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 72e1a688041..68b22207144 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -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); @@ -784,13 +809,6 @@ }, /** - * 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 * @param changeUrl true to also update the URL, false otherwise (default) @@ -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); diff --git a/apps/files/tests/js/appSpec.js b/apps/files/tests/js/appSpec.js index 0e9abad6989..a9bbab03ecb 100644 --- a/apps/files/tests/js/appSpec.js +++ b/apps/files/tests/js/appSpec.js @@ -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() { diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js index 9152dbb58c3..490594a1773 100644 --- a/apps/files/tests/js/fileactionsSpec.js +++ b/apps/files/tests/js/fileactionsSpec.js @@ -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'); + 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=%2Fsubdir&files=testName.txt'); + 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'); + }); }); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index a3dc5b255a1..3e9950dfe19 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -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(); diff --git a/apps/files_sharing/appinfo/app.php b/apps/files_sharing/appinfo/app.php index fa43f33721c..21b2646c5ea 100644 --- a/apps/files_sharing/appinfo/app.php +++ b/apps/files_sharing/appinfo/app.php @@ -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') + ) +); diff --git a/apps/files_sharing/css/sharedfilelist.css b/apps/files_sharing/css/sharedfilelist.css new file mode 100644 index 00000000000..6b0c7d2226e --- /dev/null +++ b/apps/files_sharing/css/sharedfilelist.css @@ -0,0 +1,3 @@ +#filestable.shareList .summary .filesize { + display: none; +} diff --git a/apps/files_sharing/js/app.js b/apps/files_sharing/js/app.js new file mode 100644 index 00000000000..3764328a5d0 --- /dev/null +++ b/apps/files_sharing/js/app.js @@ -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(); + }); +}); + diff --git a/apps/files_sharing/js/public.js b/apps/files_sharing/js/public.js index d825ee9de15..27e8d361ff9 100644 --- a/apps/files_sharing/js/public.js +++ b/apps/files_sharing/js/public.js @@ -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: diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js index 84c5bf57b38..5a42604c866 100644 --- a/apps/files_sharing/js/share.js +++ b/apps/files_sharing/js/share.js @@ -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); } }); } diff --git a/apps/files_sharing/js/sharedfilelist.js b/apps/files_sharing/js/sharedfilelist.js new file mode 100644 index 00000000000..ef1034ecfdc --- /dev/null +++ b/apps/files_sharing/js/sharedfilelist.js @@ -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; +})(); diff --git a/apps/files_sharing/lib/api.php b/apps/files_sharing/lib/api.php index 21fd5d00a4c..dc4e5cf6c49 100644 --- a/apps/files_sharing/lib/api.php +++ b/apps/files_sharing/lib/api.php @@ -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); } } @@ -196,6 +207,27 @@ class Api { } /** + * 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 * @return \OC_OCS_Result diff --git a/apps/files_sharing/list.php b/apps/files_sharing/list.php new file mode 100644 index 00000000000..bad690ea95f --- /dev/null +++ b/apps/files_sharing/list.php @@ -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(); diff --git a/apps/files_sharing/templates/list.php b/apps/files_sharing/templates/list.php new file mode 100644 index 00000000000..a1d95ebc1f1 --- /dev/null +++ b/apps/files_sharing/templates/list.php @@ -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> diff --git a/apps/files_sharing/tests/js/appSpec.js b/apps/files_sharing/tests/js/appSpec.js new file mode 100644 index 00000000000..ad95ee53942 --- /dev/null +++ b/apps/files_sharing/tests/js/appSpec.js @@ -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; + }); + }); +}); diff --git a/apps/files_sharing/tests/js/sharedfilelistSpec.js b/apps/files_sharing/tests/js/sharedfilelistSpec.js new file mode 100644 index 00000000000..7aec8322a44 --- /dev/null +++ b/apps/files_sharing/tests/js/sharedfilelistSpec.js @@ -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'); + }); + }); +}); diff --git a/apps/files_trashbin/appinfo/app.php b/apps/files_trashbin/appinfo/app.php index b8900ee0de3..383115b8e63 100644 --- a/apps/files_trashbin/appinfo/app.php +++ b/apps/files_trashbin/appinfo/app.php @@ -9,7 +9,7 @@ array( "id" => 'trashbin', "appname" => 'files_trashbin', "script" => 'list.php', - "order" => 1, + "order" => 50, "name" => $l->t('Deleted files') ) ); diff --git a/apps/files_trashbin/js/app.js b/apps/files_trashbin/js/app.js index aa499ae1791..c59a132b8c4 100644 --- a/apps/files_trashbin/js/app.js +++ b/apps/files_trashbin/js/app.js @@ -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; } }; diff --git a/apps/files_trashbin/js/filelist.js b/apps/files_trashbin/js/filelist.js index 205f879f335..826c1bd64d5 100644 --- a/apps/files_trashbin/js/filelist.js +++ b/apps/files_trashbin/js/filelist.js @@ -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', diff --git a/apps/files_trashbin/tests/js/filelistSpec.js b/apps/files_trashbin/tests/js/filelistSpec.js index d41c24c3cc9..11eeff68df8 100644 --- a/apps/files_trashbin/tests/js/filelistSpec.js +++ b/apps/files_trashbin/tests/js/filelistSpec.js @@ -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(); diff --git a/apps/files_versions/js/versions.js b/apps/files_versions/js/versions.js index b452bc25b13..942a1a929f7 100644 --- a/apps/files_versions/js/versions.js +++ b/apps/files_versions/js/versions.js @@ -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 { |