diff options
-rw-r--r-- | apps/files/index.php | 1 | ||||
-rw-r--r-- | apps/files/js/detailfileinfoview.js | 52 | ||||
-rw-r--r-- | apps/files/js/detailsview.js | 80 | ||||
-rw-r--r-- | apps/files/js/detailtabview.js | 75 | ||||
-rw-r--r-- | apps/files/js/fileactions.js | 89 | ||||
-rw-r--r-- | apps/files/js/fileinfomodel.js | 71 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 71 | ||||
-rw-r--r-- | apps/files/js/mainfileinfodetailview.js | 107 | ||||
-rw-r--r-- | apps/files/js/tagsplugin.js | 9 | ||||
-rw-r--r-- | apps/files/tests/js/detailsviewSpec.js | 2 | ||||
-rw-r--r-- | apps/files/tests/js/fileactionsSpec.js | 32 | ||||
-rw-r--r-- | apps/files/tests/js/filelistSpec.js | 14 | ||||
-rw-r--r-- | apps/files/tests/js/mainfileinfodetailviewSpec.js | 129 | ||||
-rw-r--r-- | apps/files_sharing/js/sharetabview.js | 27 | ||||
-rw-r--r-- | core/js/js.js | 2 |
15 files changed, 495 insertions, 266 deletions
diff --git a/apps/files/index.php b/apps/files/index.php index a41ec059b55..beae585cea4 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -41,6 +41,7 @@ OCP\Util::addscript('files', 'file-upload'); OCP\Util::addscript('files', 'jquery.iframe-transport'); OCP\Util::addscript('files', 'jquery.fileupload'); OCP\Util::addscript('files', 'jquery-visibility'); +OCP\Util::addscript('files', 'fileinfomodel'); OCP\Util::addscript('files', 'filesummary'); OCP\Util::addscript('files', 'breadcrumb'); OCP\Util::addscript('files', 'filelist'); diff --git a/apps/files/js/detailfileinfoview.js b/apps/files/js/detailfileinfoview.js index 9a88b5e2d8a..43595001212 100644 --- a/apps/files/js/detailfileinfoview.js +++ b/apps/files/js/detailfileinfoview.js @@ -16,35 +16,13 @@ * Displays a block of details about the file info. * */ - var DetailFileInfoView = function() { - this.initialize(); - }; - /** - * @memberof OCA.Files - */ - DetailFileInfoView.prototype = { - /** - * jQuery element - */ - $el: null, + var DetailFileInfoView = OC.Backbone.View.extend({ + tagName: 'div', + className: 'detailFileInfoView', _template: null, /** - * Currently displayed file info - * - * @type OCA.Files.FileInfo - */ - _fileInfo: null, - - /** - * Initialize the details view - */ - initialize: function() { - this.$el = $('<div class="detailFileInfoView"></div>'); - }, - - /** * returns the jQuery object for HTML output * * @returns {jQuery} @@ -54,30 +32,12 @@ }, /** - * Destroy / uninitialize this instance. - */ - destroy: function() { - if (this.$el) { - this.$el.remove(); - } - }, - - /** - * Renders this details view - * - * @abstract - */ - render: function() { - // to be implemented in subclass - }, - - /** * Sets the file info to be displayed in the view * * @param {OCA.Files.FileInfo} fileInfo file info to set */ setFileInfo: function(fileInfo) { - this._fileInfo = fileInfo; + this.model = fileInfo; this.render(); }, @@ -87,9 +47,9 @@ * @return {OCA.Files.FileInfo} file info */ getFileInfo: function() { - return this._fileInfo; + return this.model; } - }; + }); OCA.Files.DetailFileInfoView = DetailFileInfoView; })(); diff --git a/apps/files/js/detailsview.js b/apps/files/js/detailsview.js index 7b7bd013f9e..4df359e4523 100644 --- a/apps/files/js/detailsview.js +++ b/apps/files/js/detailsview.js @@ -33,31 +33,15 @@ * The details view show details about a selected file. * */ - var DetailsView = function() { - this.initialize(); - }; - - /** - * @memberof OCA.Files - */ - DetailsView.prototype = { - - /** - * jQuery element - */ - $el: null, + var DetailsView = OC.Backbone.View.extend({ + id: 'app-sidebar', + tabName: 'div', + className: 'detailsView', _template: null, _templateTabHeader: null, /** - * Currently displayed file info - * - * @type OCA.Files.FileInfo - */ - _fileInfo: null, - - /** * List of detail tab views * * @type Array<OCA.Files.DetailTabView> @@ -78,33 +62,25 @@ */ _currentTabId: null, + events: { + 'click a.close': '_onClose', + 'click .tabHeaders .tabHeader': '_onClickTab' + }, + /** * Initialize the details view */ initialize: function() { - this.$el = $('<div id="app-sidebar"></div>'); - this.fileInfo = null; this._tabViews = []; this._detailFileInfoViews = []; - this.$el.on('click', 'a.close', function(event) { - OC.Apps.hideAppSidebar(); - event.preventDefault(); - }); - - this.$el.on('click', '.tabHeaders .tabHeader', _.bind(this._onClickTab, this)); - // uncomment to add some dummy tabs for testing - //this._addTestTabs(); + // this._addTestTabs(); }, - /** - * Destroy / uninitialize this instance. - */ - destroy: function() { - if (this.$el) { - this.$el.remove(); - } + _onClose: function(event) { + OC.Apps.hideAppSidebar(); + event.preventDefault(); }, _onClickTab: function(e) { @@ -148,7 +124,6 @@ */ render: function() { var self = this; - this.$el.empty(); if (!this._template) { this._template = Handlebars.compile(TEMPLATE); @@ -158,12 +133,13 @@ this._templateTabHeader = Handlebars.compile(TEMPLATE_TAB_HEADER); } - var $el = $(this._template({ + this.$el.html(this._template({ closeLabel: t('files', 'Close') })); - var $tabsContainer = $el.find('.tabsContainer'); - var $tabHeadsContainer = $el.find('.tabHeaders'); - var $detailsContainer = $el.find('.detailFileInfoContainer'); + + var $tabsContainer = this.$el.find('.tabsContainer'); + var $tabHeadsContainer = this.$el.find('.tabHeaders'); + var $detailsContainer = this.$el.find('.detailFileInfoContainer'); // render details _.each(this._detailFileInfoViews, function(detailView) { @@ -172,40 +148,36 @@ if (this._tabViews.length > 0) { if (!this._currentTab) { - this._currentTab = this._tabViews[0].getId(); + this._currentTab = this._tabViews[0].id; } // render tabs _.each(this._tabViews, function(tabView, i) { // hidden by default var $el = tabView.get$(); - var isCurrent = (tabView.getId() === self._currentTab); + var isCurrent = (tabView.id === self._currentTab); if (!isCurrent) { $el.addClass('hidden'); } $tabsContainer.append($el); $tabHeadsContainer.append(self._templateTabHeader({ - tabId: tabView.getId(), + tabId: tabView.id, tabIndex: i, label: tabView.getLabel(), selected: isCurrent })); }); } - - // TODO: select current tab - - this.$el.append($el); }, /** * Sets the file info to be displayed in the view * - * @param {OCA.Files.FileInfo} fileInfo file info to set + * @param {OCA.Files.FileInfoModel} fileInfo file info to set */ setFileInfo: function(fileInfo) { - this._fileInfo = fileInfo; + this.model = fileInfo; this.render(); @@ -221,10 +193,10 @@ /** * Returns the file info. * - * @return {OCA.Files.FileInfo} file info + * @return {OCA.Files.FileInfoModel} file info */ getFileInfo: function() { - return this._fileInfo; + return this.model; }, /** @@ -244,7 +216,7 @@ addDetailView: function(detailView) { this._detailFileInfoViews.push(detailView); } - }; + }); OCA.Files.DetailsView = DetailsView; })(); diff --git a/apps/files/js/detailtabview.js b/apps/files/js/detailtabview.js index b9b1dda2ccc..b2e02971fb4 100644 --- a/apps/files/js/detailtabview.js +++ b/apps/files/js/detailtabview.js @@ -17,23 +17,10 @@ * Base class for tab views to display file information. * */ - var DetailTabView = function(id) { - this.initialize(id); - }; + var DetailTabView = OC.Backbone.View.extend({ + tag: 'div', - /** - * @memberof OCA.Files - */ - DetailTabView.prototype = { - /** - * jQuery element - */ - $el: null, - - /** - * Tab id - */ - _id: null, + className: 'tab', /** * Tab label @@ -42,52 +29,20 @@ _template: null, - /** - * Currently displayed file info - * - * @type OCA.Files.FileInfo - */ - _fileInfo: null, - - /** - * Initialize the details view - * - * @param {string} id tab id - */ - initialize: function(id) { - if (!id) { - throw 'Argument "id" is required'; - } - this._id = id; - this.$el = $('<div class="tab"></div>'); - this.$el.attr('data-tabid', id); - }, - - /** - * Destroy / uninitialize this instance. - */ - destroy: function() { - if (this.$el) { - this.$el.remove(); + initialize: function() { + if (!this.id) { + this.id = 'detailTabView' + DetailTabView._TAB_COUNT; + DetailTabView._TAB_COUNT++; } }, /** - * Returns the tab element id - * - * @return {string} tab id - */ - getId: function() { - return this._id; - }, - - /** * Returns the tab label * * @return {String} label */ getLabel: function() { - return 'Tab ' + this._id; + return 'Tab ' + this.id; }, /** @@ -107,29 +62,29 @@ render: function() { // to be implemented in subclass // FIXME: code is only for testing - this.$el.empty(); - this.$el.append('<div>Hello ' + this._id + '</div>'); + this.$el.html('<div>Hello ' + this.id + '</div>'); }, /** * Sets the file info to be displayed in the view * - * @param {OCA.Files.FileInfo} fileInfo file info to set + * @param {OCA.Files.FileInfoModel} fileInfo file info to set */ setFileInfo: function(fileInfo) { - this._fileInfo = fileInfo; + this.model = fileInfo; this.render(); }, /** * Returns the file info. * - * @return {OCA.Files.FileInfo} file info + * @return {OCA.Files.FileInfoModel} file info */ getFileInfo: function() { - return this._fileInfo; + return this.model; } - }; + }); + DetailTabView._TAB_COUNT = 0; OCA.Files.DetailTabView = DetailTabView; })(); diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 2edbed7f3c4..1d0493f2140 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -31,6 +31,10 @@ actions: {}, defaults: {}, icons: {}, + + /** + * @deprecated + */ currentFile: null, /** @@ -331,6 +335,9 @@ $trigger.addClass('open'); menu = new OCA.Files.FileActionsMenu(); + + context.$file.find('td.filename').append(menu.$el); + menu.$el.on('afterHide', function() { context.$file.removeClass('mouseOver'); $trigger.removeClass('open'); @@ -338,7 +345,6 @@ }); context.$file.addClass('mouseOver'); - context.$file.find('td.filename').append(menu.$el); menu.show(context); }, @@ -401,11 +407,22 @@ // also set on global object for legacy apps window.FileActions.currentFile = currentFile; + var callContext = _.extend({}, context); + + if (!context.dir && context.fileList) { + callContext.dir = $file.attr('data-path') || context.fileList.getCurrentDirectory(); + } + + if (!context.fileInfoModel && context.fileList) { + callContext.fileInfoModel = context.fileList.getModelForFile(fileName); + if (!callContext.fileInfoModel) { + console.warn('No file info model found for file "' + fileName + '"'); + } + } + actionSpec.action( fileName, - _.extend(context, { - dir: $file.attr('data-path') || context.fileList.getCurrentDirectory() - }) + callContext ); } ); @@ -414,6 +431,63 @@ }, /** + * Trigger the given action on the given file. + * + * @param {string} actionName action name + * @param {OCA.Files.FileInfoModel} fileInfoModel file info model + * @param {OCA.Files.FileList} [fileList] file list, for compatibility with older action handlers [DEPRECATED] + * + * @return {boolean} true if the action handler was called, false otherwise + * + * @since 8.2 + */ + triggerAction: function(actionName, fileInfoModel, fileList) { + var actionFunc; + var actions = this.get( + fileInfoModel.get('mimetype'), + fileInfoModel.isDirectory() ? 'dir' : 'file', + fileInfoModel.get('permissions') + ); + + if (actionName) { + actionFunc = actions[actionName]; + } else { + actionFunc = this.getDefault( + fileInfoModel.get('mimetype'), + fileInfoModel.isDirectory() ? 'dir' : 'file', + fileInfoModel.get('permissions') + ); + } + + if (!actionFunc) { + actionFunc = actions['Download']; + } + + if (!actionFunc) { + return false; + } + + var context = { + fileActions: this, + fileInfoModel: fileInfoModel, + dir: fileInfoModel.get('path') + }; + + var fileName = fileInfoModel.get('name'); + this.currentFile = fileName; + // also set on global object for legacy apps + window.FileActions.currentFile = fileName; + + if (fileList) { + // compatibility with action handlers that expect these + context.fileList = fileList; + context.$file = fileList.findFileEl(fileName); + } + + actionFunc(fileName, context); + }, + + /** * Display file actions for the given element * @param parent "td" element of the file for which to display actions * @param triggerEvent if true, triggers the fileActionsReady on the file @@ -627,11 +701,12 @@ * Action handler function for file actions * * @callback OCA.Files.FileActions~actionHandler - * @param {String} fileName name of the clicked file + * @param {String} fileName name of the file on which the action must be performed * @param context context * @param {String} context.dir directory of the file - * @param context.$file jQuery element of the file - * @param {OCA.Files.FileList} context.fileList the FileList instance on which the action occurred + * @param {OCA.Files.FileInfoModel} fileInfoModel file info model + * @param {Object} [context.$file] jQuery element of the file [DEPRECATED] + * @param {OCA.Files.FileList} [context.fileList] the FileList instance on which the action occurred [DEPRECATED] * @param {OCA.Files.FileActions} context.fileActions the FileActions instance on which the action occurred */ diff --git a/apps/files/js/fileinfomodel.js b/apps/files/js/fileinfomodel.js new file mode 100644 index 00000000000..05060854fba --- /dev/null +++ b/apps/files/js/fileinfomodel.js @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OC, OCA) { + + /** + * @class OC.Files.FileInfo + * @classdesc File information + * + * @param {Object} attributes file data + * @param {int} attributes.id file id + * @param {string} attributes.name file name + * @param {string} attributes.path path leading to the file, + * without the file name and with a leading slash + * @param {int} attributes.size size + * @param {string} attributes.mimetype mime type + * @param {string} attributes.icon icon URL + * @param {int} attributes.permissions permissions + * @param {Date} attributes.mtime modification time + * @param {string} attributes.etag etag + * @param {string} mountType mount type + * + * @since 8.2 + */ + var FileInfoModel = OC.Backbone.Model.extend({ + + initialize: function(data) { + if (!_.isUndefined(data.id)) { + data.id = parseInt(data.id, 10); + } + + // TODO: normalize path + data.path = data.path || ''; + data.name = data.name; + + data.mimetype = data.mimetype || 'application/octet-stream'; + }, + + /** + * Returns whether this file is a directory + * + * @return {boolean} true if this is a directory, false otherwise + */ + isDirectory: function() { + return this.get('mimetype') === 'httpd/unix-directory'; + }, + + /** + * Returns the full path to this file + * + * @return {string} full path + */ + getFullPath: function() { + return OC.joinPaths(this.get('path'), this.get('name')); + } + }); + + if (!OCA.Files) { + OCA.Files = {}; + } + OCA.Files.FileInfoModel = FileInfoModel; + +})(OC, OCA); + diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index f488325b0a6..4802e07e965 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -213,7 +213,7 @@ if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) { this._detailsView = new OCA.Files.DetailsView(); - this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView()); + this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView({fileList: this, fileActions: this.fileActions})); this._detailsView.$el.insertBefore(this.$el); this._detailsView.$el.addClass('disappear'); } @@ -285,32 +285,74 @@ }, /** + * Returns a unique model for the given file name. + * + * @param {string|object} fileName file name or jquery row + * @return {OCA.Files.FileInfoModel} file info model + */ + getModelForFile: function(fileName) { + var $tr; + // jQuery object ? + if (fileName.is) { + $tr = fileName; + fileName = $tr.attr('data-file'); + } else { + $tr = this.findFileEl(fileName); + } + + if (!$tr || !$tr.length) { + return null; + } + + // if requesting the selected model, return it + if (this._currentFileModel && this._currentFileModel.get('name') === fileName) { + return this._currentFileModel; + } + + // TODO: note, this is a temporary model required for synchronising + // state between different views. + // In the future the FileList should work with Backbone.Collection + // and contain existing models that can be used. + // This method would in the future simply retrieve the matching model from the collection. + var model = new OCA.Files.FileInfoModel(this.elementToFile($tr)); + if (!model.has('path')) { + model.set('path', this.getCurrentDirectory(), {silent: true}); + } + return model; + }, + + /** * Update the details view to display the given file * - * @param {OCA.Files.FileInfo} fileInfo file info to display + * @param {string} fileName file name from the current list */ - _updateDetailsView: function(fileInfo) { + _updateDetailsView: function(fileName) { if (!this._detailsView) { return; } - var self = this; var oldFileInfo = this._detailsView.getFileInfo(); if (oldFileInfo) { // TODO: use more efficient way, maybe track the highlight - this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.id).removeClass('highlighted'); + this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.get('id')).removeClass('highlighted'); + oldFileInfo.off('change', this._onSelectedModelChanged, this); } - if (!fileInfo) { + if (!fileName) { OC.Apps.hideAppSidebar(); this._detailsView.setFileInfo(null); + this._currentFileModel = null; return; } - this.$fileList.children().filterAttr('data-id', '' + fileInfo.id).addClass('highlighted'); - this._detailsView.setFileInfo(_.extend({ - path: this.getCurrentDirectory() - }, fileInfo)); + var $tr = this.findFileEl(fileName); + var model = this.getModelForFile($tr); + + this._currentFileModel = model; + + $tr.addClass('highlighted'); + + this._detailsView.setFileInfo(model); this._detailsView.$el.scrollTop(0); _.defer(OC.Apps.showAppSidebar); }, @@ -369,7 +411,7 @@ this._selectionSummary.remove(data); } if (this._selectionSummary.getTotal() === 1) { - this._updateDetailsView(_.values(this._selectedFiles)[0]); + this._updateDetailsView(_.values(this._selectedFiles)[0].name); } else { // show nothing when multiple files are selected this._updateDetailsView(null); @@ -434,8 +476,7 @@ $(event.target).closest('a').blur(); } } else { - var fileInfo = this.files[$tr.index()]; - this._updateDetailsView(fileInfo); + this._updateDetailsView($tr.attr('data-file')); event.preventDefault(); } } @@ -1191,6 +1232,8 @@ sortdirection: this._sortDirection } }); + // close sidebar + this._updateDetailsView(null); var callBack = this.reloadCallback.bind(this); return this._reloadCall.then(callBack, callBack); }, @@ -1590,7 +1633,7 @@ tr.remove(); tr = self.add(fileInfo, {updateSummary: false, silent: true}); self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)})); - self._updateDetailsView(fileInfo); + self._updateDetailsView(fileInfo.name); } }); } else { diff --git a/apps/files/js/mainfileinfodetailview.js b/apps/files/js/mainfileinfodetailview.js index a00d907d0d6..6910e5f2be5 100644 --- a/apps/files/js/mainfileinfodetailview.js +++ b/apps/files/js/mainfileinfodetailview.js @@ -10,7 +10,7 @@ (function() { var TEMPLATE = - '<div class="thumbnail"></div><div title="{{name}}" class="fileName ellipsis">{{name}}</div>' + + '<a href="#" class="thumbnail action-default"></a><div title="{{name}}" class="fileName ellipsis">{{name}}</div>' + '<div class="file-details ellipsis">' + ' <a href="#" ' + ' alt="{{starAltText}}"' + @@ -27,58 +27,104 @@ * Displays main details about a file * */ - var MainFileInfoDetailView = function() { - this.initialize(); - }; - /** - * @memberof OCA.Files - */ - MainFileInfoDetailView.prototype = _.extend({}, OCA.Files.DetailFileInfoView.prototype, + var MainFileInfoDetailView = OCA.Files.DetailFileInfoView.extend( /** @lends OCA.Files.MainFileInfoDetailView.prototype */ { - _template: null, + + className: 'mainFileInfoView', /** - * Initialize the details view + * Associated file list instance, for file actions + * + * @type {OCA.Files.FileList} */ - initialize: function() { - this.$el = $('<div class="mainFileInfoView"></div>'); - }, + _fileList: null, /** - * Renders this details view + * File actions + * + * @type {OCA.Files.FileActions} */ - render: function() { - this.$el.empty(); + _fileActions: null, + + events: { + 'click a.action-favorite': '_onClickFavorite', + 'click a.action-default': '_onClickDefaultAction' + }, + template: function(data) { if (!this._template) { this._template = Handlebars.compile(TEMPLATE); } + return this._template(data); + }, - if (this._fileInfo) { - var isFavorite = (this._fileInfo.tags || []).indexOf(OC.TAG_FAVORITE) >= 0; - this.$el.append(this._template({ + initialize: function(options) { + options = options || {}; + this._fileList = options.fileList; + this._fileActions = options.fileActions; + if (!this._fileList) { + throw 'Missing requird parameter "fileList"'; + } + if (!this._fileActions) { + throw 'Missing requird parameter "fileActions"'; + } + }, + + _onClickFavorite: function(event) { + event.preventDefault(); + this._fileActions.triggerAction('Favorite', this.model, this._fileList); + }, + + _onClickDefaultAction: function(event) { + event.preventDefault(); + this._fileActions.triggerAction(null, this.model, this._fileList); + }, + + _onModelChanged: function() { + // simply re-render + this.render(); + }, + + setFileInfo: function(fileInfo) { + if (this.model) { + this.model.off('change', this._onModelChanged, this); + } + this.model = fileInfo; + if (this.model) { + this.model.on('change', this._onModelChanged, this); + } + this.render(); + }, + + /** + * Renders this details view + */ + render: function() { + if (this.model) { + var isFavorite = (this.model.get('tags') || []).indexOf(OC.TAG_FAVORITE) >= 0; + this.$el.html(this.template({ nameLabel: t('files', 'Name'), - name: this._fileInfo.name, + name: this.model.get('name'), pathLabel: t('files', 'Path'), - path: this._fileInfo.path, + path: this.model.get('path'), sizeLabel: t('files', 'Size'), - size: OC.Util.humanFileSize(this._fileInfo.size, true), - altSize: n('files', '%n byte', '%n bytes', this._fileInfo.size), + size: OC.Util.humanFileSize(this.model.get('size'), true), + altSize: n('files', '%n byte', '%n bytes', this.model.get('size')), dateLabel: t('files', 'Modified'), - altDate: OC.Util.formatDate(this._fileInfo.mtime), - date: OC.Util.relativeModifiedDate(this._fileInfo.mtime), + altDate: OC.Util.formatDate(this.model.get('mtime')), + date: OC.Util.relativeModifiedDate(this.model.get('mtime')), starAltText: isFavorite ? t('files', 'Favorited') : t('files', 'Favorite'), starIcon: OC.imagePath('core', isFavorite ? 'actions/starred' : 'actions/star') })); // TODO: we really need OC.Previews var $iconDiv = this.$el.find('.thumbnail'); - if (this._fileInfo.mimetype !== 'httpd/unix-directory') { + if (!this.model.isDirectory()) { // TODO: inject utility class? FileList.lazyLoadPreview({ - path: this._fileInfo.path + '/' + this._fileInfo.name, - mime: this._fileInfo.mimetype, - etag: this._fileInfo.etag, + path: this.model.getFullPath(), + mime: this.model.get('mimetype'), + etag: this.model.get('etag'), x: 50, y: 50, callback: function(previewUrl) { @@ -90,7 +136,10 @@ $iconDiv.css('background-image', 'url("' + OC.MimeType.getIconUrl('dir') + '")'); } this.$el.find('[title]').tooltip({placement: 'bottom'}); + } else { + this.$el.empty(); } + this.delegateEvents(); } }); diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index ec69ce4b965..d8552c71e45 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -77,7 +77,7 @@ var self = this; // register "star" action fileActions.registerAction({ - name: 'favorite', + name: 'Favorite', displayName: 'Favorite', mime: 'all', permissions: OC.PERMISSION_READ, @@ -124,6 +124,7 @@ toggleStar($actionEl, (newTags.indexOf(OC.TAG_FAVORITE) >= 0)); $file.attr('data-tags', newTags.join('|')); $file.attr('data-favorite', !isFavorite); + context.fileInfoModel.set('tags', newTags); fileInfo.tags = newTags; }); } @@ -145,6 +146,12 @@ $tr.find('td:first').prepend('<div class="favorite"></div>'); return $tr; }; + var oldElementToFile = fileList.elementToFile; + fileList.elementToFile = function($el) { + var fileInfo = oldElementToFile.apply(this, arguments); + fileInfo.tags = $el.attr('data-tags') || []; + return fileInfo; + }; }, attach: function(fileList) { diff --git a/apps/files/tests/js/detailsviewSpec.js b/apps/files/tests/js/detailsviewSpec.js index db1e24fd68e..4261aa53c94 100644 --- a/apps/files/tests/js/detailsviewSpec.js +++ b/apps/files/tests/js/detailsviewSpec.js @@ -26,7 +26,7 @@ describe('OCA.Files.DetailsView tests', function() { detailsView = new OCA.Files.DetailsView(); }); afterEach(function() { - detailsView.destroy(); + detailsView.remove(); detailsView = undefined; }); it('renders itself empty when nothing registered', function() { diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js index 236cff6cafd..1254843e66a 100644 --- a/apps/files/tests/js/fileactionsSpec.js +++ b/apps/files/tests/js/fileactionsSpec.js @@ -27,6 +27,7 @@ describe('OCA.Files.FileActions tests', function() { var $body = $('#testArea'); $body.append('<input type="hidden" id="dir" value="/subdir"></input>'); $body.append('<input type="hidden" id="permissions" value="31"></input>'); + $body.append('<table id="filestable"><tbody id="fileList"></tbody></table>'); // dummy files table fileActions = new OCA.Files.FileActions(); fileActions.registerAction({ @@ -152,9 +153,10 @@ describe('OCA.Files.FileActions tests', function() { }); }); describe('action handler', function() { - var actionStub, $tr; + var actionStub, $tr, clock; beforeEach(function() { + clock = sinon.useFakeTimers(); var fileData = { id: 18, type: 'file', @@ -175,6 +177,12 @@ describe('OCA.Files.FileActions tests', function() { }); $tr = fileList.add(fileData); }); + afterEach(function() { + OC.hideMenus(); + // jump past animations + clock.tick(1000); + clock.restore(); + }); it('passes context to action handler', function() { $tr.find('.action-test').click(); expect(actionStub.calledOnce).toEqual(true); @@ -184,6 +192,7 @@ describe('OCA.Files.FileActions tests', function() { expect(context.fileList).toBeDefined(); expect(context.fileActions).toBeDefined(); expect(context.dir).toEqual('/subdir'); + expect(context.fileInfoModel.get('name')).toEqual('testName.txt'); // when data-path is defined actionStub.reset(); @@ -192,6 +201,22 @@ describe('OCA.Files.FileActions tests', function() { context = actionStub.getCall(0).args[1]; expect(context.dir).toEqual('/somepath'); }); + it('also triggers action handler when calling triggerAction()', function() { + var model = new OCA.Files.FileInfoModel({ + id: 1, + name: 'Test.txt', + path: '/subdir', + mime: 'text/plain', + permissions: 31 + }); + fileActions.triggerAction('Test', model, fileList); + + expect(actionStub.calledOnce).toEqual(true); + expect(actionStub.getCall(0).args[0]).toEqual('Test.txt'); + expect(actionStub.getCall(0).args[1].fileList).toEqual(fileList); + expect(actionStub.getCall(0).args[1].fileActions).toEqual(fileActions); + expect(actionStub.getCall(0).args[1].fileInfoModel).toEqual(model); + }); describe('actions menu', function() { it('shows actions menu inside row when clicking the menu trigger', function() { expect($tr.find('td.filename .fileActionsMenu').length).toEqual(0); @@ -203,12 +228,13 @@ describe('OCA.Files.FileActions tests', function() { expect($tr.hasClass('mouseOver')).toEqual(true); }); it('cleans up after hiding', function() { - var clock = sinon.useFakeTimers(); + var slideUpStub = sinon.stub($.fn, 'slideUp'); $tr.find('.action-menu').click(); expect($tr.find('.fileActionsMenu').length).toEqual(1); OC.hideMenus(); // sliding animation - clock.tick(500); + expect(slideUpStub.calledOnce).toEqual(true); + slideUpStub.getCall(0).args[1](); expect($tr.hasClass('mouseOver')).toEqual(false); expect($tr.find('.fileActionsMenu').length).toEqual(0); }); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 57e16626403..38073389382 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -1912,6 +1912,20 @@ describe('OCA.Files.FileList tests', function() { expect($tr.hasClass('highlighted')).toEqual(false); expect(fileList._detailsView.getFileInfo()).toEqual(null); }); + it('returns the currently selected model instance when calling getModelForFile', function() { + var $tr = fileList.findFileEl('One.txt'); + $tr.find('td.filename>a.name').click(); + + var model1 = fileList.getModelForFile('One.txt'); + var model2 = fileList.getModelForFile('One.txt'); + model1.set('test', true); + + // it's the same model + expect(model2).toEqual(model1); + + var model3 = fileList.getModelForFile($tr); + expect(model3).toEqual(model1); + }); }); describe('File actions', function() { it('Clicking on a file name will trigger default action', function() { diff --git a/apps/files/tests/js/mainfileinfodetailviewSpec.js b/apps/files/tests/js/mainfileinfodetailviewSpec.js index 10ad38097c6..ca7384f6207 100644 --- a/apps/files/tests/js/mainfileinfodetailviewSpec.js +++ b/apps/files/tests/js/mainfileinfodetailviewSpec.js @@ -20,32 +20,37 @@ */ describe('OCA.Files.MainFileInfoDetailView tests', function() { - var view, tooltipStub, previewStub, fncLazyLoadPreview, fileListMock; + var view, tooltipStub, fileListMock, fileActions, fileList, testFileInfo; beforeEach(function() { tooltipStub = sinon.stub($.fn, 'tooltip'); fileListMock = sinon.mock(OCA.Files.FileList.prototype); - view = new OCA.Files.MainFileInfoDetailView(); + fileActions = new OCA.Files.FileActions(); + fileList = new OCA.Files.FileList($('<table></table>'), { + fileActions: fileActions + }); + view = new OCA.Files.MainFileInfoDetailView({ + fileList: fileList, + fileActions: fileActions + }); + testFileInfo = new OCA.Files.FileInfoModel({ + id: 5, + name: 'One.txt', + mimetype: 'text/plain', + permissions: 31, + path: '/subdir', + size: 123456789, + mtime: Date.UTC(2015, 6, 17, 1, 2, 0, 0) + }); }); afterEach(function() { - view.destroy(); + view.remove(); view = undefined; tooltipStub.restore(); fileListMock.restore(); }); describe('rendering', function() { - var testFileInfo; - beforeEach(function() { - view = new OCA.Files.MainFileInfoDetailView(); - testFileInfo = { - id: 5, - name: 'One.txt', - path: '/subdir', - size: 123456789, - mtime: Date.UTC(2015, 6, 17, 1, 2, 0, 0) - }; - }); it('displays basic info', function() { var clock = sinon.useFakeTimers(Date.UTC(2015, 6, 17, 1, 2, 0, 3)); var dateExpected = OC.Util.formatDate(Date(Date.UTC(2015, 6, 17, 1, 2, 0, 0))); @@ -59,39 +64,34 @@ describe('OCA.Files.MainFileInfoDetailView tests', function() { clock.restore(); }); it('displays favorite icon', function() { - view.setFileInfo(_.extend(testFileInfo, { - tags: [OC.TAG_FAVORITE] - })); + testFileInfo.set('tags', [OC.TAG_FAVORITE]); + view.setFileInfo(testFileInfo); expect(view.$el.find('.favorite img').attr('src')) .toEqual(OC.imagePath('core', 'actions/starred')); - view.setFileInfo(_.extend(testFileInfo, { - tags: [] - })); + testFileInfo.set('tags', []); + view.setFileInfo(testFileInfo); expect(view.$el.find('.favorite img').attr('src')) .toEqual(OC.imagePath('core', 'actions/star')); }); it('displays mime icon', function() { // File - view.setFileInfo(_.extend(testFileInfo, { - mimetype: 'text/calendar' - })); + testFileInfo.set('mimetype', 'text/calendar'); + view.setFileInfo(testFileInfo); expect(view.$el.find('.thumbnail').css('background-image')) .toContain('filetypes/text-calendar.svg'); // Folder - view.setFileInfo(_.extend(testFileInfo, { - mimetype: 'httpd/unix-directory' - })); + testFileInfo.set('mimetype', 'httpd/unix-directory'); + view.setFileInfo(testFileInfo); expect(view.$el.find('.thumbnail').css('background-image')) .toContain('filetypes/folder.svg'); }); it('displays thumbnail', function() { - view.setFileInfo(_.extend(testFileInfo, { - mimetype: 'text/plain' - })); + testFileInfo.set('mimetype', 'test/plain'); + view.setFileInfo(testFileInfo); var expectation = fileListMock.expects('lazyLoadPreview'); expectation.once(); @@ -100,5 +100,76 @@ describe('OCA.Files.MainFileInfoDetailView tests', function() { fileListMock.verify(); }); + it('rerenders when changes are made on the model', function() { + view.setFileInfo(testFileInfo); + + testFileInfo.set('tags', [OC.TAG_FAVORITE]); + + expect(view.$el.find('.favorite img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/starred')); + + testFileInfo.set('tags', []); + + expect(view.$el.find('.favorite img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/star')); + }); + it('unbinds change listener from model', function() { + view.setFileInfo(testFileInfo); + view.setFileInfo(new OCA.Files.FileInfoModel({ + id: 999, + name: 'test.txt', + path: '/' + })); + + // set value on old model + testFileInfo.set('tags', [OC.TAG_FAVORITE]); + + // no change + expect(view.$el.find('.favorite img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/star')); + }); + }); + describe('events', function() { + it('triggers default action when clicking on the thumbnail', function() { + var actionHandler = sinon.stub(); + + fileActions.registerAction({ + name: 'Something', + mime: 'all', + permissions: OC.PERMISSION_READ, + actionHandler: actionHandler + }); + fileActions.setDefault('text/plain', 'Something'); + + view.setFileInfo(testFileInfo); + + view.$el.find('.thumbnail').click(); + + expect(actionHandler.calledOnce).toEqual(true); + expect(actionHandler.getCall(0).args[0]).toEqual('One.txt'); + expect(actionHandler.getCall(0).args[1].fileList).toEqual(fileList); + expect(actionHandler.getCall(0).args[1].fileActions).toEqual(fileActions); + expect(actionHandler.getCall(0).args[1].fileInfoModel).toEqual(testFileInfo); + }); + it('triggers "Favorite" action when clicking on the star', function() { + var actionHandler = sinon.stub(); + + fileActions.registerAction({ + name: 'Favorite', + mime: 'all', + permissions: OC.PERMISSION_READ, + actionHandler: actionHandler + }); + + view.setFileInfo(testFileInfo); + + view.$el.find('.action-favorite').click(); + + expect(actionHandler.calledOnce).toEqual(true); + expect(actionHandler.getCall(0).args[0]).toEqual('One.txt'); + expect(actionHandler.getCall(0).args[1].fileList).toEqual(fileList); + expect(actionHandler.getCall(0).args[1].fileActions).toEqual(fileActions); + expect(actionHandler.getCall(0).args[1].fileInfoModel).toEqual(testFileInfo); + }); }); }); diff --git a/apps/files_sharing/js/sharetabview.js b/apps/files_sharing/js/sharetabview.js index e02de923751..5f4a21a4a57 100644 --- a/apps/files_sharing/js/sharetabview.js +++ b/apps/files_sharing/js/sharetabview.js @@ -13,29 +13,14 @@ '<div>Owner: {{owner}}'; /** - * @class OCA.Sharing.ShareTabView - * @classdesc - * - * Displays sharing information - * - */ - var ShareTabView = function(id) { - this.initialize(id); - }; - /** * @memberof OCA.Sharing */ - ShareTabView.prototype = _.extend({}, OCA.Files.DetailTabView.prototype, + var ShareTabView = OCA.Files.DetailTabView.extend( /** @lends OCA.Sharing.ShareTabView.prototype */ { - _template: null, + id: 'shareTabView', + className: 'tab shareTabView', - /** - * Initialize the details view - */ - initialize: function() { - OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments); - this.$el.addClass('shareTabView'); - }, + _template: null, getLabel: function() { return t('files_sharing', 'Sharing'); @@ -51,9 +36,9 @@ this._template = Handlebars.compile(TEMPLATE); } - if (this._fileInfo) { + if (this.model) { this.$el.append(this._template({ - owner: this._fileInfo.shareOwner || OC.currentUser + owner: this.model.get('shareOwner') || OC.currentUser })); } else { diff --git a/core/js/js.js b/core/js/js.js index 11818f4c992..52cf076472a 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -642,7 +642,7 @@ var OC={ $menuEl.show(); $menuEl.trigger(new $.Event('afterShow')); // no animation - if (_.isFunction()) { + if (_.isFunction(complete)) { complete(); } }, |