aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/js
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/js')
-rw-r--r--apps/files/js/detailfileinfoview.js96
-rw-r--r--apps/files/js/detailsview.js251
-rw-r--r--apps/files/js/detailtabview.js136
-rw-r--r--apps/files/js/fileactions.js330
-rw-r--r--apps/files/js/fileactionsmenu.js132
-rw-r--r--apps/files/js/filelist.js200
-rw-r--r--apps/files/js/files.js27
-rw-r--r--apps/files/js/mainfileinfodetailview.js98
-rw-r--r--apps/files/js/tagsplugin.js1
9 files changed, 1118 insertions, 153 deletions
diff --git a/apps/files/js/detailfileinfoview.js b/apps/files/js/detailfileinfoview.js
new file mode 100644
index 00000000000..9a88b5e2d8a
--- /dev/null
+++ b/apps/files/js/detailfileinfoview.js
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ /**
+ * @class OCA.Files.DetailFileInfoView
+ * @classdesc
+ *
+ * Displays a block of details about the file info.
+ *
+ */
+ var DetailFileInfoView = function() {
+ this.initialize();
+ };
+ /**
+ * @memberof OCA.Files
+ */
+ DetailFileInfoView.prototype = {
+ /**
+ * jQuery element
+ */
+ $el: null,
+
+ _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}
+ */
+ get$: function() {
+ return this.$el;
+ },
+
+ /**
+ * 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.render();
+ },
+
+ /**
+ * Returns the file info.
+ *
+ * @return {OCA.Files.FileInfo} file info
+ */
+ getFileInfo: function() {
+ return this._fileInfo;
+ }
+ };
+
+ OCA.Files.DetailFileInfoView = DetailFileInfoView;
+})();
+
diff --git a/apps/files/js/detailsview.js b/apps/files/js/detailsview.js
new file mode 100644
index 00000000000..7b7bd013f9e
--- /dev/null
+++ b/apps/files/js/detailsview.js
@@ -0,0 +1,251 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+ var TEMPLATE =
+ '<div>' +
+ ' <div class="detailFileInfoContainer">' +
+ ' </div>' +
+ ' <div>' +
+ ' <ul class="tabHeaders">' +
+ ' </ul>' +
+ ' <div class="tabsContainer">' +
+ ' </div>' +
+ ' </div>' +
+ ' <a class="close icon-close" href="#" alt="{{closeLabel}}"></a>' +
+ '</div>';
+
+ var TEMPLATE_TAB_HEADER =
+ '<li class="tabHeader {{#if selected}}selected{{/if}}" data-tabid="{{tabId}}" data-tabindex="{{tabIndex}}"><a href="#">{{label}}</a></li>';
+
+ /**
+ * @class OCA.Files.DetailsView
+ * @classdesc
+ *
+ * The details view show details about a selected file.
+ *
+ */
+ var DetailsView = function() {
+ this.initialize();
+ };
+
+ /**
+ * @memberof OCA.Files
+ */
+ DetailsView.prototype = {
+
+ /**
+ * jQuery element
+ */
+ $el: null,
+
+ _template: null,
+ _templateTabHeader: null,
+
+ /**
+ * Currently displayed file info
+ *
+ * @type OCA.Files.FileInfo
+ */
+ _fileInfo: null,
+
+ /**
+ * List of detail tab views
+ *
+ * @type Array<OCA.Files.DetailTabView>
+ */
+ _tabViews: [],
+
+ /**
+ * List of detail file info views
+ *
+ * @type Array<OCA.Files.DetailFileInfoView>
+ */
+ _detailFileInfoViews: [],
+
+ /**
+ * Id of the currently selected tab
+ *
+ * @type string
+ */
+ _currentTabId: null,
+
+ /**
+ * 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();
+ },
+
+ /**
+ * Destroy / uninitialize this instance.
+ */
+ destroy: function() {
+ if (this.$el) {
+ this.$el.remove();
+ }
+ },
+
+ _onClickTab: function(e) {
+ var $target = $(e.target);
+ if (!$target.hasClass('tabHeader')) {
+ $target = $target.closest('.tabHeader');
+ }
+ var tabIndex = $target.attr('data-tabindex');
+ var targetTab;
+ if (_.isUndefined(tabIndex)) {
+ return;
+ }
+
+ this.$el.find('.tabsContainer .tab').addClass('hidden');
+ targetTab = this._tabViews[tabIndex];
+ targetTab.$el.removeClass('hidden');
+
+ this.$el.find('.tabHeaders li').removeClass('selected');
+ $target.addClass('selected');
+
+ e.preventDefault();
+ },
+
+ _addTestTabs: function() {
+ for (var j = 0; j < 2; j++) {
+ var testView = new OCA.Files.DetailTabView('testtab' + j);
+ testView.index = j;
+ testView.getLabel = function() { return 'Test tab ' + this.index; };
+ testView.render = function() {
+ this.$el.empty();
+ for (var i = 0; i < 100; i++) {
+ this.$el.append('<div>Test tab ' + this.index + ' row ' + i + '</div>');
+ }
+ };
+ this._tabViews.push(testView);
+ }
+ },
+
+ /**
+ * Renders this details view
+ */
+ render: function() {
+ var self = this;
+ this.$el.empty();
+
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+
+ if (!this._templateTabHeader) {
+ this._templateTabHeader = Handlebars.compile(TEMPLATE_TAB_HEADER);
+ }
+
+ var $el = $(this._template({
+ closeLabel: t('files', 'Close')
+ }));
+ var $tabsContainer = $el.find('.tabsContainer');
+ var $tabHeadsContainer = $el.find('.tabHeaders');
+ var $detailsContainer = $el.find('.detailFileInfoContainer');
+
+ // render details
+ _.each(this._detailFileInfoViews, function(detailView) {
+ $detailsContainer.append(detailView.get$());
+ });
+
+ if (this._tabViews.length > 0) {
+ if (!this._currentTab) {
+ this._currentTab = this._tabViews[0].getId();
+ }
+
+ // render tabs
+ _.each(this._tabViews, function(tabView, i) {
+ // hidden by default
+ var $el = tabView.get$();
+ var isCurrent = (tabView.getId() === self._currentTab);
+ if (!isCurrent) {
+ $el.addClass('hidden');
+ }
+ $tabsContainer.append($el);
+
+ $tabHeadsContainer.append(self._templateTabHeader({
+ tabId: tabView.getId(),
+ 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
+ */
+ setFileInfo: function(fileInfo) {
+ this._fileInfo = fileInfo;
+
+ this.render();
+
+ // notify all panels
+ _.each(this._tabViews, function(tabView) {
+ tabView.setFileInfo(fileInfo);
+ });
+ _.each(this._detailFileInfoViews, function(detailView) {
+ detailView.setFileInfo(fileInfo);
+ });
+ },
+
+ /**
+ * Returns the file info.
+ *
+ * @return {OCA.Files.FileInfo} file info
+ */
+ getFileInfo: function() {
+ return this._fileInfo;
+ },
+
+ /**
+ * Adds a tab in the tab view
+ *
+ * @param {OCA.Files.DetailTabView} tab view
+ */
+ addTabView: function(tabView) {
+ this._tabViews.push(tabView);
+ },
+
+ /**
+ * Adds a detail view for file info.
+ *
+ * @param {OCA.Files.DetailFileInfoView} detail view
+ */
+ 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
new file mode 100644
index 00000000000..b9b1dda2ccc
--- /dev/null
+++ b/apps/files/js/detailtabview.js
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+ /**
+ * @class OCA.Files.DetailTabView
+ * @classdesc
+ *
+ * Base class for tab views to display file information.
+ *
+ */
+ var DetailTabView = function(id) {
+ this.initialize(id);
+ };
+
+ /**
+ * @memberof OCA.Files
+ */
+ DetailTabView.prototype = {
+ /**
+ * jQuery element
+ */
+ $el: null,
+
+ /**
+ * Tab id
+ */
+ _id: null,
+
+ /**
+ * Tab label
+ */
+ _label: null,
+
+ _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();
+ }
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * returns the jQuery object for HTML output
+ *
+ * @returns {jQuery}
+ */
+ get$: function() {
+ return this.$el;
+ },
+
+ /**
+ * Renders this details view
+ *
+ * @abstract
+ */
+ render: function() {
+ // to be implemented in subclass
+ // FIXME: code is only for testing
+ this.$el.empty();
+ this.$el.append('<div>Hello ' + this._id + '</div>');
+ },
+
+ /**
+ * 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.render();
+ },
+
+ /**
+ * Returns the file info.
+ *
+ * @return {OCA.Files.FileInfo} file info
+ */
+ getFileInfo: function() {
+ return this._fileInfo;
+ }
+ };
+
+ OCA.Files.DetailTabView = DetailTabView;
+})();
+
diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js
index 1956fda0077..43f74c5816d 100644
--- a/apps/files/js/fileactions.js
+++ b/apps/files/js/fileactions.js
@@ -10,6 +10,12 @@
(function() {
+ var TEMPLATE_FILE_ACTION_TRIGGER =
+ '<a class="action action-{{nameLowerCase}}" href="#" data-action="{{name}}">' +
+ '{{#if icon}}<img class="svg" alt="{{altText}}" src="{{icon}}" />{{/if}}' +
+ '{{#if displayName}}<span> {{displayName}}</span>{{/if}}' +
+ '</a>';
+
/**
* Construct a new FileActions instance
* @constructs FileActions
@@ -18,6 +24,8 @@
var FileActions = function() {
this.initialize();
};
+ FileActions.TYPE_DROPDOWN = 0;
+ FileActions.TYPE_INLINE = 1;
FileActions.prototype = {
/** @lends FileActions.prototype */
actions: {},
@@ -38,6 +46,8 @@
*/
_updateListeners: {},
+ _fileActionTriggerTemplate: null,
+
/**
* @private
*/
@@ -46,6 +56,8 @@
// abusing jquery for events until we get a real event lib
this.$el = $('<div class="dummy-fileactions hidden"></div>');
$('body').append(this.$el);
+
+ this._showMenuClosure = _.bind(this._showMenu, this);
},
/**
@@ -111,6 +123,7 @@
displayName: displayName || name
});
},
+
/**
* Register action
*
@@ -125,15 +138,14 @@
displayName: action.displayName,
mime: mime,
icon: action.icon,
- permissions: action.permissions
+ permissions: action.permissions,
+ type: action.type || FileActions.TYPE_DROPDOWN
};
if (_.isUndefined(action.displayName)) {
actionSpec.displayName = t('files', name);
}
if (_.isFunction(action.render)) {
actionSpec.render = action.render;
- } else {
- actionSpec.render = _.bind(this._defaultRenderAction, this);
}
if (!this.actions[mime]) {
this.actions[mime] = {};
@@ -162,6 +174,16 @@
this.defaults[mime] = name;
this._notifyUpdateListeners('setDefault', {defaultAction: {mime: mime, name: name}});
},
+
+ /**
+ * Returns a map of file actions handlers matching the given conditions
+ *
+ * @param {string} mime mime type
+ * @param {string} type "dir" or "file"
+ * @param {int} permissions permissions
+ *
+ * @return {Object.<string,OCA.Files.FileActions~actionHandler>} map of action name to action spec
+ */
get: function (mime, type, permissions) {
var actions = this.getActions(mime, type, permissions);
var filteredActions = {};
@@ -170,6 +192,16 @@
});
return filteredActions;
},
+
+ /**
+ * Returns an array of file actions matching the given conditions
+ *
+ * @param {string} mime mime type
+ * @param {string} type "dir" or "file"
+ * @param {int} permissions permissions
+ *
+ * @return {Array.<OCA.Files.FileAction>} array of action specs
+ */
getActions: function (mime, type, permissions) {
var actions = {};
if (this.actions.all) {
@@ -197,7 +229,37 @@
});
return filteredActions;
},
+
+ /**
+ * Returns the default file action handler for the given conditions
+ *
+ * @param {string} mime mime type
+ * @param {string} type "dir" or "file"
+ * @param {int} permissions permissions
+ *
+ * @return {OCA.Files.FileActions~actionHandler} action handler
+ *
+ * @deprecated use getDefaultFileAction instead
+ */
getDefault: function (mime, type, permissions) {
+ var defaultActionSpec = this.getDefaultFileAction(mime, type, permissions);
+ if (defaultActionSpec) {
+ return defaultActionSpec.action;
+ }
+ return undefined;
+ },
+
+ /**
+ * Returns the default file action handler for the given conditions
+ *
+ * @param {string} mime mime type
+ * @param {string} type "dir" or "file"
+ * @param {int} permissions permissions
+ *
+ * @return {OCA.Files.FileActions~actionHandler} action handler
+ * @since 8.2
+ */
+ getDefaultFileAction: function(mime, type, permissions) {
var mimePart;
if (mime) {
mimePart = mime.substr(0, mime.indexOf('/'));
@@ -212,9 +274,10 @@
} else {
name = this.defaults.all;
}
- var actions = this.get(mime, type, permissions);
+ var actions = this.getActions(mime, type, permissions);
return actions[name];
},
+
/**
* Default function to render actions
*
@@ -224,87 +287,82 @@
* @param {OCA.Files.FileActionContext} context action context
*/
_defaultRenderAction: function(actionSpec, isDefault, context) {
- var name = actionSpec.name;
- if (name === 'Download' || !isDefault) {
- var $actionLink = this._makeActionLink(actionSpec, context);
+ if (!isDefault) {
+ var params = {
+ name: actionSpec.name,
+ nameLowerCase: actionSpec.name.toLowerCase(),
+ displayName: actionSpec.displayName,
+ icon: actionSpec.icon,
+ altText: actionSpec.altText,
+ };
+ if (_.isFunction(actionSpec.icon)) {
+ params.icon = actionSpec.icon(context.$file.attr('data-file'));
+ }
+
+ var $actionLink = this._makeActionLink(params, context);
context.$file.find('a.name>span.fileactions').append($actionLink);
+ $actionLink.addClass('permanent');
return $actionLink;
}
},
+
/**
* Renders the action link element
*
- * @param {OCA.Files.FileAction} actionSpec action object
- * @param {OCA.Files.FileActionContext} context action context
+ * @param {Object} params action params
*/
- _makeActionLink: function(actionSpec, context) {
- var img = actionSpec.icon;
- if (img && img.call) {
- img = img(context.$file.attr('data-file'));
- }
- var html = '<a href="#">';
- if (img) {
- html += '<img class="svg" alt="" src="' + img + '" />';
+ _makeActionLink: function(params) {
+ if (!this._fileActionTriggerTemplate) {
+ this._fileActionTriggerTemplate = Handlebars.compile(TEMPLATE_FILE_ACTION_TRIGGER);
}
- if (actionSpec.displayName) {
- html += '<span> ' + actionSpec.displayName + '</span>';
- }
- html += '</a>';
- return $(html);
+ return $(this._fileActionTriggerTemplate(params));
},
+
/**
- * Custom renderer for the "Rename" action.
- * Displays the rename action as an icon behind the file name.
+ * Displays the file actions dropdown menu
*
- * @param {OCA.Files.FileAction} actionSpec file action to render
- * @param {boolean} isDefault true if the action is a default action,
- * false otherwise
- * @param {OCAFiles.FileActionContext} context rendering context
+ * @param {string} fileName file name
+ * @param {OCA.Files.FileActionContext} context rendering context
*/
- _renderRenameAction: function(actionSpec, isDefault, context) {
- var $actionEl = this._makeActionLink(actionSpec, context);
- var $container = context.$file.find('a.name span.nametext');
- $actionEl.find('img').attr('alt', t('files', 'Rename'));
- $container.find('.action-rename').remove();
- $container.append($actionEl);
- return $actionEl;
+ _showMenu: function(fileName, context) {
+ var menu;
+ var $trigger = context.$file.closest('tr').find('.fileactions .action-menu');
+ $trigger.addClass('open');
+
+ menu = new OCA.Files.FileActionsMenu();
+ menu.$el.on('afterHide', function() {
+ context.$file.removeClass('mouseOver');
+ $trigger.removeClass('open');
+ menu.remove();
+ });
+
+ context.$file.addClass('mouseOver');
+ context.$file.find('td.filename').append(menu.$el);
+ menu.show(context);
},
+
/**
- * Custom renderer for the "Delete" action.
- * Displays the "Delete" action as a trash icon at the end of
- * the table row.
- *
- * @param {OCA.Files.FileAction} actionSpec file action to render
- * @param {boolean} isDefault true if the action is a default action,
- * false otherwise
- * @param {OCAFiles.FileActionContext} context rendering context
+ * Renders the menu trigger on the given file list row
+ *
+ * @param {Object} $tr file list row element
+ * @param {OCA.Files.FileActionContext} context rendering context
*/
- _renderDeleteAction: function(actionSpec, isDefault, context) {
- var mountType = context.$file.attr('data-mounttype');
- var deleteTitle = t('files', 'Delete');
- if (mountType === 'external-root') {
- deleteTitle = t('files', 'Disconnect storage');
- } else if (mountType === 'shared-root') {
- deleteTitle = t('files', 'Unshare');
- }
- var cssClasses = 'action delete icon-delete';
- if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) {
- // add css class no-permission to delete icon
- cssClasses += ' no-permission';
- deleteTitle = t('files', 'No permission to delete');
- }
- var $actionLink = $('<a href="#" original-title="' +
- escapeHTML(deleteTitle) +
- '" class="' +cssClasses + '">' +
- '<span class="hidden-visually">' + escapeHTML(deleteTitle) + '</span>' +
- '</a>'
- );
- var $container = context.$file.find('td:last');
- $container.find('.delete').remove();
- $container.append($actionLink);
- return $actionLink;
+ _renderMenuTrigger: function($tr, context) {
+ // remove previous
+ $tr.find('.action-menu').remove();
+
+ var $el = this._renderInlineAction({
+ name: 'menu',
+ displayName: '',
+ icon: OC.imagePath('core', 'actions/more'),
+ altText: t('files', 'Actions'),
+ action: this._showMenuClosure
+ }, false, context);
+
+ $el.addClass('permanent');
},
+
/**
* Renders the action element by calling actionSpec.render() and
* registers the click event to process the action.
@@ -312,25 +370,32 @@
* @param {OCA.Files.FileAction} actionSpec file action to render
* @param {boolean} isDefault true if the action is a default action,
* false otherwise
- * @param {OCAFiles.FileActionContext} context rendering context
+ * @param {OCA.Files.FileActionContext} context rendering context
*/
- _renderAction: function(actionSpec, isDefault, context) {
- var $actionEl = actionSpec.render(actionSpec, isDefault, context);
+ _renderInlineAction: function(actionSpec, isDefault, context) {
+ var renderFunc = actionSpec.render || _.bind(this._defaultRenderAction, this);
+ var $actionEl = renderFunc(actionSpec, isDefault, context);
if (!$actionEl || !$actionEl.length) {
return;
}
- $actionEl.addClass('action action-' + actionSpec.name.toLowerCase());
- $actionEl.attr('data-action', actionSpec.name);
$actionEl.on(
'click', {
a: null
},
function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ if ($actionEl.hasClass('open')) {
+ return;
+ }
+
var $file = $(event.target).closest('tr');
+ if ($file.hasClass('busy')) {
+ return;
+ }
var currentFile = $file.find('td.filename');
var fileName = $file.attr('data-file');
- event.stopPropagation();
- event.preventDefault();
context.fileActions.currentFile = currentFile;
// also set on global object for legacy apps
@@ -346,6 +411,7 @@
);
return $actionEl;
},
+
/**
* Display file actions for the given element
* @param parent "td" element of the file for which to display actions
@@ -376,36 +442,29 @@
nameLinks = parent.children('a.name');
nameLinks.find('.fileactions, .nametext .action').remove();
nameLinks.append('<span class="fileactions" />');
- var defaultAction = this.getDefault(
+ var defaultAction = this.getDefaultFileAction(
this.getCurrentMimeType(),
this.getCurrentType(),
this.getCurrentPermissions()
);
+ var context = {
+ $file: $tr,
+ fileActions: this,
+ fileList: fileList
+ };
+
$.each(actions, function (name, actionSpec) {
- if (name !== 'Share') {
- self._renderAction(
+ if (actionSpec.type === FileActions.TYPE_INLINE) {
+ self._renderInlineAction(
actionSpec,
- actionSpec.action === defaultAction, {
- $file: $tr,
- fileActions: this,
- fileList : fileList
- }
+ defaultAction && actionSpec.name === defaultAction.name,
+ context
);
}
});
- // added here to make sure it's always the last action
- var shareActionSpec = actions.Share;
- if (shareActionSpec){
- this._renderAction(
- shareActionSpec,
- shareActionSpec.action === defaultAction, {
- $file: $tr,
- fileActions: this,
- fileList: fileList
- }
- );
- }
+
+ this._renderMenuTrigger($tr, context);
if (triggerEvent){
fileList.$fileList.trigger(jQuery.Event("fileActionsReady", {fileList: fileList, $files: $tr}));
@@ -429,35 +488,42 @@
*/
registerDefaultActions: function() {
this.registerAction({
- name: 'Delete',
- displayName: '',
+ name: 'Download',
+ displayName: t('files', 'Download'),
mime: 'all',
- // permission is READ because we show a hint instead if there is no permission
permissions: OC.PERMISSION_READ,
- icon: function() {
- return OC.imagePath('core', 'actions/delete');
+ icon: function () {
+ return OC.imagePath('core', 'actions/download');
},
- render: _.bind(this._renderDeleteAction, this),
- actionHandler: function(fileName, context) {
- // if there is no permission to delete do nothing
- if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) {
+ actionHandler: function (filename, context) {
+ var dir = context.dir || context.fileList.getCurrentDirectory();
+ var url = context.fileList.getDownloadUrl(filename, dir);
+
+ var downloadFileaction = $(context.$file).find('.fileactions .action-download');
+
+ // don't allow a second click on the download action
+ if(downloadFileaction.hasClass('disabled')) {
return;
}
- context.fileList.do_delete(fileName, context.dir);
- $('.tipsy').remove();
+
+ if (url) {
+ var disableLoadingState = function() {
+ context.fileList.showFileBusyState(filename, false);
+ };
+
+ context.fileList.showFileBusyState(downloadFileaction, true);
+ OCA.Files.Files.handleDownload(url, disableLoadingState);
+ }
}
});
- // t('files', 'Rename')
this.registerAction({
name: 'Rename',
- displayName: '',
mime: 'all',
permissions: OC.PERMISSION_UPDATE,
icon: function() {
return OC.imagePath('core', 'actions/rename');
},
- render: _.bind(this._renderRenameAction, this),
actionHandler: function (filename, context) {
context.fileList.rename(filename);
}
@@ -471,23 +537,51 @@
context.fileList.changeDirectory(dir + filename);
});
- this.setDefault('dir', 'Open');
-
- this.register('all', 'Download', OC.PERMISSION_READ, function () {
- return OC.imagePath('core', 'actions/download');
- }, function (filename, context) {
- var dir = context.dir || context.fileList.getCurrentDirectory();
- var url = context.fileList.getDownloadUrl(filename, dir);
- if (url) {
- OC.redirect(url);
+ this.registerAction({
+ name: 'Delete',
+ mime: 'all',
+ // permission is READ because we show a hint instead if there is no permission
+ permissions: OC.PERMISSION_READ,
+ icon: function() {
+ return OC.imagePath('core', 'actions/delete');
+ },
+ actionHandler: function(fileName, context) {
+ // if there is no permission to delete do nothing
+ if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) {
+ return;
+ }
+ context.fileList.do_delete(fileName, context.dir);
+ $('.tipsy').remove();
}
- }, t('files', 'Download'));
+ });
+
+ this.setDefault('dir', 'Open');
}
};
OCA.Files.FileActions = FileActions;
/**
+ * Replaces the download icon with a loading spinner and vice versa
+ * - also adds the class disabled to the passed in element
+ *
+ * @param downloadButtonElement download fileaction
+ * @param {boolean} showIt whether to show the spinner(true) or to hide it(false)
+ */
+ OCA.Files.FileActions.updateFileActionSpinner = function(downloadButtonElement, showIt) {
+ var icon = downloadButtonElement.find('img'),
+ sourceImage = icon.attr('src');
+
+ if(showIt) {
+ downloadButtonElement.addClass('disabled');
+ icon.attr('src', sourceImage.replace('actions/download.svg', 'loading-small.gif'));
+ } else {
+ downloadButtonElement.removeClass('disabled');
+ icon.attr('src', sourceImage.replace('loading-small.gif', 'actions/download.svg'));
+ }
+ };
+
+ /**
* File action attributes.
*
* @todo make this a real class in the future
diff --git a/apps/files/js/fileactionsmenu.js b/apps/files/js/fileactionsmenu.js
new file mode 100644
index 00000000000..623ebde5442
--- /dev/null
+++ b/apps/files/js/fileactionsmenu.js
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2014
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+ var TEMPLATE_MENU =
+ '<ul>' +
+ '{{#each items}}' +
+ '<li>' +
+ '<a href="#" class="action action-{{nameLowerCase}} permanent" data-action="{{name}}">{{#if icon}}<img src="{{icon}}"/>{{else}}<span class="no-icon"></span>{{/if}}<span>{{displayName}}</span></a>' +
+ '</li>' +
+ '{{/each}}' +
+ '</ul>';
+
+ /**
+ * Construct a new FileActionsMenu instance
+ * @constructs FileActionsMenu
+ * @memberof OCA.Files
+ */
+ var FileActionsMenu = OC.Backbone.View.extend({
+ tagName: 'div',
+ className: 'fileActionsMenu bubble hidden open menu',
+
+ /**
+ * Current context
+ *
+ * @type OCA.Files.FileActionContext
+ */
+ _context: null,
+
+ events: {
+ 'click a.action': '_onClickAction'
+ },
+
+ template: function(data) {
+ if (!OCA.Files.FileActionsMenu._TEMPLATE) {
+ OCA.Files.FileActionsMenu._TEMPLATE = Handlebars.compile(TEMPLATE_MENU);
+ }
+ return OCA.Files.FileActionsMenu._TEMPLATE(data);
+ },
+
+ /**
+ * Event handler whenever an action has been clicked within the menu
+ *
+ * @param {Object} event event object
+ */
+ _onClickAction: function(event) {
+ var $target = $(event.target);
+ if (!$target.is('a')) {
+ $target = $target.closest('a');
+ }
+ var fileActions = this._context.fileActions;
+ var actionName = $target.attr('data-action');
+ var actions = fileActions.getActions(
+ fileActions.getCurrentMimeType(),
+ fileActions.getCurrentType(),
+ fileActions.getCurrentPermissions()
+ );
+ var actionSpec = actions[actionName];
+ var fileName = this._context.$file.attr('data-file');
+
+ event.stopPropagation();
+ event.preventDefault();
+
+ OC.hideMenus();
+
+ actionSpec.action(
+ fileName,
+ this._context
+ );
+ },
+
+ /**
+ * Renders the menu with the currently set items
+ */
+ render: function() {
+ var fileActions = this._context.fileActions;
+ var actions = fileActions.getActions(
+ fileActions.getCurrentMimeType(),
+ fileActions.getCurrentType(),
+ fileActions.getCurrentPermissions()
+ );
+
+ var defaultAction = fileActions.getDefaultFileAction(
+ fileActions.getCurrentMimeType(),
+ fileActions.getCurrentType(),
+ fileActions.getCurrentPermissions()
+ );
+
+ var items = _.filter(actions, function(actionSpec) {
+ return (
+ actionSpec.type === OCA.Files.FileActions.TYPE_DROPDOWN &&
+ (!defaultAction || actionSpec.name !== defaultAction.name)
+ );
+ });
+ items = _.map(items, function(item) {
+ item.nameLowerCase = item.name.toLowerCase();
+ return item;
+ });
+
+ this.$el.html(this.template({
+ items: items
+ }));
+ },
+
+ /**
+ * Displays the menu under the given element
+ *
+ * @param {OCA.Files.FileActionContext} context context
+ * @param {Object} $trigger trigger element
+ */
+ show: function(context) {
+ this._context = context;
+
+ this.render();
+ this.$el.removeClass('hidden');
+
+ OC.showMenu(null, this.$el);
+ }
+ });
+
+ OCA.Files.FileActionsMenu = FileActionsMenu;
+
+})();
+
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index 8236ef3b4ac..e297edcf11b 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -23,6 +23,7 @@
* @param [options.scrollContainer] scrollable container, defaults to $(window)
* @param [options.dragOptions] drag options, disabled by default
* @param [options.folderDropOptions] folder drop options, disabled by default
+ * @param [options.detailsViewEnabled=true] whether to enable details view
*/
var FileList = function($el, options) {
this.initialize($el, options);
@@ -65,6 +66,11 @@
fileSummary: null,
/**
+ * @type OCA.Files.DetailsView
+ */
+ _detailsView: null,
+
+ /**
* Whether the file list was initialized already.
* @type boolean
*/
@@ -205,6 +211,13 @@
}
this.breadcrumb = new OCA.Files.BreadCrumb(breadcrumbOptions);
+ if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) {
+ this._detailsView = new OCA.Files.DetailsView();
+ this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView());
+ this._detailsView.$el.insertBefore(this.$el);
+ this._detailsView.$el.addClass('disappear');
+ }
+
this.$el.find('#controls').prepend(this.breadcrumb.$el);
this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this));
@@ -216,6 +229,13 @@
this.updateSearch();
+ this.$el.on('click', function(event) {
+ var $target = $(event.target);
+ // click outside file row ?
+ if (!$target.closest('tbody').length && !$target.closest('#app-sidebar').length) {
+ self._updateDetailsView(null);
+ }
+ });
this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this));
this.$fileList.on('change', 'td.filename>.selectCheckBox', _.bind(this._onClickFileCheckbox, this));
this.$el.on('urlChanged', _.bind(this._onUrlChanged, this));
@@ -263,6 +283,37 @@
},
/**
+ * Update the details view to display the given file
+ *
+ * @param {OCA.Files.FileInfo} fileInfo file info to display
+ */
+ _updateDetailsView: function(fileInfo) {
+ 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');
+ }
+
+ if (!fileInfo) {
+ OC.Apps.hideAppSidebar();
+ this._detailsView.setFileInfo(null);
+ return;
+ }
+
+ this.$fileList.children().filterAttr('data-id', '' + fileInfo.id).addClass('highlighted');
+ this._detailsView.setFileInfo(_.extend({
+ path: this.getCurrentDirectory()
+ }, fileInfo));
+ this._detailsView.$el.scrollTop(0);
+ _.defer(OC.Apps.showAppSidebar);
+ },
+
+ /**
* Event handler for when the window size changed
*/
_onResize: function() {
@@ -315,6 +366,12 @@
delete this._selectedFiles[$tr.data('id')];
this._selectionSummary.remove(data);
}
+ if (this._selectionSummary.getTotal() === 1) {
+ this._updateDetailsView(_.values(this._selectedFiles)[0]);
+ } else {
+ // show nothing when multiple files are selected
+ this._updateDetailsView(null);
+ }
this.$el.find('.select-all').prop('checked', this._selectionSummary.getTotal() === this.files.length);
},
@@ -350,27 +407,34 @@
this._selectFileEl($tr, !$checkbox.prop('checked'));
this.updateSelectionSummary();
} else {
- var filename = $tr.attr('data-file');
- var renaming = $tr.data('renaming');
- if (!renaming) {
- this.fileActions.currentFile = $tr.find('td');
- var mime = this.fileActions.getCurrentMimeType();
- var type = this.fileActions.getCurrentType();
- var permissions = this.fileActions.getCurrentPermissions();
- var action = this.fileActions.getDefault(mime,type, permissions);
- if (action) {
- event.preventDefault();
- // 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()
- });
+ // clicked directly on the name
+ if (!this._detailsView || $(event.target).is('.nametext') || $(event.target).closest('.nametext').length) {
+ var filename = $tr.attr('data-file');
+ var renaming = $tr.data('renaming');
+ if (!renaming) {
+ this.fileActions.currentFile = $tr.find('td');
+ var mime = this.fileActions.getCurrentMimeType();
+ var type = this.fileActions.getCurrentType();
+ var permissions = this.fileActions.getCurrentPermissions();
+ var action = this.fileActions.getDefault(mime,type, permissions);
+ if (action) {
+ event.preventDefault();
+ // 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()
+ });
+ }
+ // deselect row
+ $(event.target).closest('a').blur();
}
- // deselect row
- $(event.target).closest('a').blur();
+ } else {
+ var fileInfo = this.files[$tr.index()];
+ this._updateDetailsView(fileInfo);
+ event.preventDefault();
}
}
},
@@ -417,7 +481,21 @@
else {
files = _.pluck(this.getSelectedFiles(), 'name');
}
- OC.redirect(this.getDownloadUrl(files, dir));
+
+ var downloadFileaction = $('#selectedActionsList').find('.download');
+
+ // don't allow a second click on the download action
+ if(downloadFileaction.hasClass('disabled')) {
+ event.preventDefault();
+ return;
+ }
+
+ var disableLoadingState = function(){
+ OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, false);
+ };
+
+ OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, true);
+ OCA.Files.Files.handleDownload(this.getDownloadUrl(files, dir), disableLoadingState);
return false;
},
@@ -811,7 +889,7 @@
var formatted;
var text;
if (mtime > 0) {
- formatted = formatDate(mtime);
+ formatted = OC.Util.formatDate(mtime);
text = OC.Util.relativeModifiedDate(mtime);
} else {
formatted = t('files', 'Unable to determine date');
@@ -1225,6 +1303,12 @@
ready(iconURL); // set mimeicon URL
urlSpec.file = OCA.Files.Files.fixPath(path);
+ if (options.x) {
+ urlSpec.x = options.x;
+ }
+ if (options.y) {
+ urlSpec.y = options.y;
+ }
if (etag){
// use etag as cache buster
@@ -1360,9 +1444,7 @@
}
_.each(fileNames, function(fileName) {
var $tr = self.findFileEl(fileName);
- var $thumbEl = $tr.find('.thumbnail');
- var oldBackgroundImage = $thumbEl.css('background-image');
- $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')');
+ self.showFileBusyState($tr, true);
// TODO: improve performance by sending all file names in a single call
$.post(
OC.filePath('files', 'ajax', 'move.php'),
@@ -1404,7 +1486,7 @@
} else {
OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error'));
}
- $thumbEl.css('background-image', oldBackgroundImage);
+ self.showFileBusyState($tr, false);
}
);
});
@@ -1465,14 +1547,13 @@
try {
var newName = input.val();
- var $thumbEl = tr.find('.thumbnail');
input.tipsy('hide');
form.remove();
if (newName !== oldname) {
checkInput();
// mark as loading (temp element)
- $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')');
+ self.showFileBusyState(tr, true);
tr.attr('data-file', newName);
var basename = newName;
if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') {
@@ -1480,7 +1561,6 @@
}
td.find('a.name span.nametext').text(basename);
td.children('a.name').show();
- tr.find('.fileactions, .action').addClass('hidden');
$.ajax({
url: OC.filePath('files','ajax','rename.php'),
@@ -1507,6 +1587,7 @@
tr.remove();
tr = self.add(fileInfo, {updateSummary: false, silent: true});
self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)}));
+ self._updateDetailsView(fileInfo);
}
});
} else {
@@ -1551,6 +1632,44 @@
inList:function(file) {
return this.findFileEl(file).length;
},
+
+ /**
+ * Shows busy state on a given file row or multiple
+ *
+ * @param {string|Array.<string>} files file name or array of file names
+ * @param {bool} [busy=true] busy state, true for busy, false to remove busy state
+ *
+ * @since 8.2
+ */
+ showFileBusyState: function(files, state) {
+ var self = this;
+ if (!_.isArray(files)) {
+ files = [files];
+ }
+
+ if (_.isUndefined(state)) {
+ state = true;
+ }
+
+ _.each(files, function($tr) {
+ // jquery element already ?
+ if (!$tr.is) {
+ $tr = self.findFileEl($tr);
+ }
+
+ var $thumbEl = $tr.find('.thumbnail');
+ $tr.toggleClass('busy', state);
+
+ if (state) {
+ $thumbEl.attr('data-oldimage', $thumbEl.css('background-image'));
+ $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')');
+ } else {
+ $thumbEl.css('background-image', $thumbEl.attr('data-oldimage'));
+ $thumbEl.removeAttr('data-oldimage');
+ }
+ });
+ },
+
/**
* Delete the given files from the given dir
* @param files file names list (without path)
@@ -1564,9 +1683,8 @@
files=[files];
}
if (files) {
+ this.showFileBusyState(files, true);
for (var i=0; i<files.length; i++) {
- var deleteAction = this.findFileEl(files[i]).children("td.date").children(".action.delete");
- deleteAction.removeClass('icon-delete').addClass('icon-loading-small');
}
}
// Finish any existing actions
@@ -1584,7 +1702,7 @@
// no files passed, delete all in current dir
params.allfiles = true;
// show spinner for all files
- this.$fileList.find('tr>td.date .action.delete').removeClass('icon-delete').addClass('icon-loading-small');
+ this.$fileList.find('tr').addClass('busy');
}
$.post(OC.filePath('files', 'ajax', 'delete.php'),
@@ -1627,8 +1745,7 @@
}
else {
$.each(files,function(index,file) {
- var deleteAction = self.findFileEl(file).find('.action.delete');
- deleteAction.removeClass('icon-loading-small').addClass('icon-delete');
+ self.$fileList.find('tr').removeClass('busy');
});
}
}
@@ -1663,6 +1780,7 @@
}
this.$table.addClass('hidden');
+ this.$el.find('#emptycontent').addClass('hidden');
$mask = $('<div class="mask transparent"></div>');
@@ -2162,6 +2280,20 @@
}
});
+ },
+
+ /**
+ * Register a tab view to be added to all views
+ */
+ registerTabView: function(tabView) {
+ this._detailsView.addTabView(tabView);
+ },
+
+ /**
+ * Register a detail view to be added to all views
+ */
+ registerDetailView: function(detailView) {
+ this._detailsView.addDetailView(detailView);
}
};
diff --git a/apps/files/js/files.js b/apps/files/js/files.js
index 034045ee40b..19cc3b26e44 100644
--- a/apps/files/js/files.js
+++ b/apps/files/js/files.js
@@ -271,8 +271,33 @@
FileList.scrollTo(getURLParameter('scrollto'));
}
*/
+ },
+
+ /**
+ * Handles the download and calls the callback function once the download has started
+ * - browser sends download request and adds parameter with a token
+ * - server notices this token and adds a set cookie to the download response
+ * - browser now adds this cookie for the domain
+ * - JS periodically checks for this cookie and then knows when the download has started to call the callback
+ *
+ * @param {string} url download URL
+ * @param {function} callback function to call once the download has started
+ */
+ handleDownload: function(url, callback) {
+ var randomToken = Math.random().toString(36).substring(2),
+ checkForDownloadCookie = function() {
+ if (!OC.Util.isCookieSetToValue('ocDownloadStarted', randomToken)){
+ return false;
+ } else {
+ callback();
+ return true;
+ }
+ };
+
+ OC.redirect(url + '&downloadStartSecret=' + randomToken);
+ OC.Util.waitFor(checkForDownloadCookie, 500);
}
- }
+ };
Files._updateStorageStatisticsDebounced = _.debounce(Files._updateStorageStatistics, 250);
OCA.Files.Files = Files;
diff --git a/apps/files/js/mainfileinfodetailview.js b/apps/files/js/mainfileinfodetailview.js
new file mode 100644
index 00000000000..a00d907d0d6
--- /dev/null
+++ b/apps/files/js/mainfileinfodetailview.js
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ var TEMPLATE =
+ '<div class="thumbnail"></div><div title="{{name}}" class="fileName ellipsis">{{name}}</div>' +
+ '<div class="file-details ellipsis">' +
+ ' <a href="#" ' +
+ ' alt="{{starAltText}}"' +
+ ' class="action action-favorite favorite">' +
+ ' <img class="svg" src="{{starIcon}}" />' +
+ ' </a>' +
+ ' <span class="size" title="{{altSize}}">{{size}}</span>, <span class="date" title="{{altDate}}">{{date}}</span>' +
+ '</div>';
+
+ /**
+ * @class OCA.Files.MainFileInfoDetailView
+ * @classdesc
+ *
+ * Displays main details about a file
+ *
+ */
+ var MainFileInfoDetailView = function() {
+ this.initialize();
+ };
+ /**
+ * @memberof OCA.Files
+ */
+ MainFileInfoDetailView.prototype = _.extend({}, OCA.Files.DetailFileInfoView.prototype,
+ /** @lends OCA.Files.MainFileInfoDetailView.prototype */ {
+ _template: null,
+
+ /**
+ * Initialize the details view
+ */
+ initialize: function() {
+ this.$el = $('<div class="mainFileInfoView"></div>');
+ },
+
+ /**
+ * Renders this details view
+ */
+ render: function() {
+ this.$el.empty();
+
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+
+ if (this._fileInfo) {
+ var isFavorite = (this._fileInfo.tags || []).indexOf(OC.TAG_FAVORITE) >= 0;
+ this.$el.append(this._template({
+ nameLabel: t('files', 'Name'),
+ name: this._fileInfo.name,
+ pathLabel: t('files', 'Path'),
+ path: this._fileInfo.path,
+ sizeLabel: t('files', 'Size'),
+ size: OC.Util.humanFileSize(this._fileInfo.size, true),
+ altSize: n('files', '%n byte', '%n bytes', this._fileInfo.size),
+ dateLabel: t('files', 'Modified'),
+ altDate: OC.Util.formatDate(this._fileInfo.mtime),
+ date: OC.Util.relativeModifiedDate(this._fileInfo.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') {
+ // TODO: inject utility class?
+ FileList.lazyLoadPreview({
+ path: this._fileInfo.path + '/' + this._fileInfo.name,
+ mime: this._fileInfo.mimetype,
+ etag: this._fileInfo.etag,
+ x: 50,
+ y: 50,
+ callback: function(previewUrl) {
+ $iconDiv.css('background-image', 'url("' + previewUrl + '")');
+ }
+ });
+ } else {
+ // TODO: special icons / shared / external
+ $iconDiv.css('background-image', 'url("' + OC.MimeType.getIconUrl('dir') + '")');
+ }
+ this.$el.find('[title]').tooltip({placement: 'bottom'});
+ }
+ }
+ });
+
+ OCA.Files.MainFileInfoDetailView = MainFileInfoDetailView;
+})();
diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js
index 293e25176f3..ec69ce4b965 100644
--- a/apps/files/js/tagsplugin.js
+++ b/apps/files/js/tagsplugin.js
@@ -81,6 +81,7 @@
displayName: 'Favorite',
mime: 'all',
permissions: OC.PERMISSION_READ,
+ type: OCA.Files.FileActions.TYPE_INLINE,
render: function(actionSpec, isDefault, context) {
var $file = context.$file;
var isFavorite = $file.data('favorite') === true;