Merge pull request #18178 from owncloud/files-sidebar-actions

Sidebar file actions
This commit is contained in:
Jan-Christoph Borchardt 2015-08-13 17:17:10 +02:00
commit 4e53b5922d
15 changed files with 497 additions and 268 deletions

View File

@ -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');

View File

@ -16,34 +16,12 @@
* 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
*
@ -53,31 +31,13 @@
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.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;
})();

View File

@ -33,30 +33,14 @@
* 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
*
@ -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();
},
/**
* 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;
})();

View File

@ -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,43 +29,11 @@
_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';
initialize: function() {
if (!this.id) {
this.id = 'detailTabView' + DetailTabView._TAB_COUNT;
DetailTabView._TAB_COUNT++;
}
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;
},
/**
@ -87,7 +42,7 @@
* @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;
})();

View File

@ -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
);
}
);
@ -413,6 +430,63 @@
return $actionEl;
},
/**
* 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
@ -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
*/

View File

@ -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);

View File

@ -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');
}
@ -284,33 +284,75 @@
this.fileActions.on('setDefault', this._onFileActionsUpdated);
},
/**
* 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 {

View File

@ -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,
/**
* File actions
*
* @type {OCA.Files.FileActions}
*/
_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);
},
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() {
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({
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();
}
});

View File

@ -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) {

View File

@ -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() {

View File

@ -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);
});

View File

@ -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() {

View File

@ -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);
});
});
});

View File

@ -12,30 +12,15 @@
var TEMPLATE =
'<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 {

View File

@ -642,7 +642,7 @@ var OC={
$menuEl.show();
$menuEl.trigger(new $.Event('afterShow'));
// no animation
if (_.isFunction()) {
if (_.isFunction(complete)) {
complete();
}
},