Merge pull request #17656 from owncloud/files-rightsidebar

Basic work for right sidebar
This commit is contained in:
Vincent Petry 2015-08-10 13:14:15 +02:00
commit 15e16d335d
24 changed files with 1194 additions and 27 deletions

View file

@ -0,0 +1,55 @@
#app-sidebar .detailFileInfoContainer {
min-height: 50px;
padding: 15px;
}
#app-sidebar .detailFileInfoContainer > div {
clear: both;
}
#app-sidebar .mainFileInfoView {
margin-right: 20px; /* accomodate for close icon */
}
#app-sidebar .thumbnail {
width: 50px;
height: 50px;
float: left;
margin-right: 10px;
background-size: 50px;
}
#app-sidebar .ellipsis {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
#app-sidebar .fileName {
font-size: 16px;
padding-top: 3px;
}
#app-sidebar .file-details {
margin-top: 3px;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
opacity: .5;
}
#app-sidebar .action-favorite {
vertical-align: text-bottom;
padding: 10px;
margin: -10px;
}
#app-sidebar .detailList {
float: left;
}
#app-sidebar .close {
position: absolute;
top: 0;
right: 0;
padding: 15px;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
opacity: .5;
}

View file

@ -103,6 +103,10 @@
min-height: 100%;
}
.app-files #app-content {
overflow-x: hidden;
}
/* icons for sidebar */
.nav-icon-files {
background-image: url('../img/folder.svg');
@ -143,6 +147,7 @@
#filestable tbody tr:active {
background-color: rgb(240,240,240);
}
#filestable tbody tr.highlighted,
#filestable tbody tr.selected {
background-color: rgb(230,230,230);
}

View file

@ -50,6 +50,12 @@ OCP\Util::addscript('files', 'search');
\OCP\Util::addScript('files', 'tagsplugin');
\OCP\Util::addScript('files', 'favoritesplugin');
\OCP\Util::addScript('files', 'detailfileinfoview');
\OCP\Util::addScript('files', 'detailtabview');
\OCP\Util::addScript('files', 'mainfileinfodetailview');
\OCP\Util::addScript('files', 'detailsview');
\OCP\Util::addStyle('files', 'detailsView');
\OC_Util::addVendorScript('core', 'handlebars/handlebars');
OCP\App::setActiveNavigationEntry('files_index');

View file

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

View file

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

View file

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

View file

@ -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);
@ -64,6 +65,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));
@ -262,6 +282,37 @@
this.fileActions.on('setDefault', this._onFileActionsUpdated);
},
/**
* 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
*/
@ -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();
}
}
},
@ -825,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');
@ -1239,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
@ -1521,6 +1591,7 @@
tr.remove();
tr = self.add(fileInfo, {updateSummary: false, silent: true});
self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)}));
self._updateDetailsView(fileInfo);
}
});
} else {
@ -2177,6 +2248,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);
}
};

View file

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

View file

@ -0,0 +1,105 @@
/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2015 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
describe('OCA.Files.DetailsView tests', function() {
var detailsView;
beforeEach(function() {
detailsView = new OCA.Files.DetailsView();
});
afterEach(function() {
detailsView.destroy();
detailsView = undefined;
});
it('renders itself empty when nothing registered', function() {
detailsView.render();
expect(detailsView.$el.find('.detailFileInfoContainer').length).toEqual(1);
expect(detailsView.$el.find('.tabsContainer').length).toEqual(1);
});
describe('file info detail view', function() {
it('renders registered view', function() {
var testView = new OCA.Files.DetailFileInfoView();
var testView2 = new OCA.Files.DetailFileInfoView();
detailsView.addDetailView(testView);
detailsView.addDetailView(testView2);
detailsView.render();
expect(detailsView.$el.find('.detailFileInfoContainer .detailFileInfoView').length).toEqual(2);
});
it('updates registered tabs when fileinfo is updated', function() {
var viewRenderStub = sinon.stub(OCA.Files.DetailFileInfoView.prototype, 'render');
var testView = new OCA.Files.DetailFileInfoView();
var testView2 = new OCA.Files.DetailFileInfoView();
detailsView.addDetailView(testView);
detailsView.addDetailView(testView2);
detailsView.render();
var fileInfo = {id: 5, name: 'test.txt'};
viewRenderStub.reset();
detailsView.setFileInfo(fileInfo);
expect(testView.getFileInfo()).toEqual(fileInfo);
expect(testView2.getFileInfo()).toEqual(fileInfo);
expect(viewRenderStub.callCount).toEqual(2);
viewRenderStub.restore();
});
});
describe('tabs', function() {
var testView, testView2;
beforeEach(function() {
testView = new OCA.Files.DetailTabView('test1');
testView2 = new OCA.Files.DetailTabView('test2');
detailsView.addTabView(testView);
detailsView.addTabView(testView2);
detailsView.render();
});
it('renders registered tabs', function() {
expect(detailsView.$el.find('.tab').length).toEqual(2);
});
it('updates registered tabs when fileinfo is updated', function() {
var tabRenderStub = sinon.stub(OCA.Files.DetailTabView.prototype, 'render');
var fileInfo = {id: 5, name: 'test.txt'};
tabRenderStub.reset();
detailsView.setFileInfo(fileInfo);
expect(testView.getFileInfo()).toEqual(fileInfo);
expect(testView2.getFileInfo()).toEqual(fileInfo);
expect(tabRenderStub.callCount).toEqual(2);
tabRenderStub.restore();
});
it('selects the first tab by default', function() {
expect(detailsView.$el.find('.tabHeader').eq(0).hasClass('selected')).toEqual(true);
expect(detailsView.$el.find('.tabHeader').eq(1).hasClass('selected')).toEqual(false);
expect(detailsView.$el.find('.tab').eq(0).hasClass('hidden')).toEqual(false);
expect(detailsView.$el.find('.tab').eq(1).hasClass('hidden')).toEqual(true);
});
it('switches the current tab when clicking on tab header', function() {
detailsView.$el.find('.tabHeader').eq(1).click();
expect(detailsView.$el.find('.tabHeader').eq(0).hasClass('selected')).toEqual(false);
expect(detailsView.$el.find('.tabHeader').eq(1).hasClass('selected')).toEqual(true);
expect(detailsView.$el.find('.tab').eq(0).hasClass('hidden')).toEqual(true);
expect(detailsView.$el.find('.tab').eq(1).hasClass('hidden')).toEqual(false);
});
});
});

View file

@ -113,7 +113,7 @@ describe('OCA.Files.FavoritesPlugin tests', function() {
shareOwner: 'user2'
}]);
fileList.findFileEl('testdir').find('td a.name').click();
fileList.findFileEl('testdir').find('td .nametext').click();
expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir');

View file

@ -1870,6 +1870,50 @@ describe('OCA.Files.FileList tests', function() {
});
})
});
describe('Details sidebar', function() {
beforeEach(function() {
fileList.setFiles(testFiles);
});
it('Clicking on a file row will trigger file action if no details view configured', function() {
fileList._detailsView = null;
var updateDetailsViewStub = sinon.stub(fileList, '_updateDetailsView');
var actionStub = sinon.stub();
fileList.setFiles(testFiles);
fileList.fileActions.register(
'text/plain',
'Test',
OC.PERMISSION_ALL,
function() {
// Specify icon for hitory button
return OC.imagePath('core','actions/history');
},
actionStub
);
fileList.fileActions.setDefault('text/plain', 'Test');
var $tr = fileList.findFileEl('One.txt');
$tr.find('td.filename>a.name').click();
expect(actionStub.calledOnce).toEqual(true);
expect(updateDetailsViewStub.notCalled).toEqual(true);
updateDetailsViewStub.restore();
});
it('Clicking on a file row will trigger details sidebar', function() {
fileList.fileActions.setDefault('text/plain', 'Test');
var $tr = fileList.findFileEl('One.txt');
$tr.find('td.filename>a.name').click();
expect($tr.hasClass('highlighted')).toEqual(true);
expect(fileList._detailsView.getFileInfo().id).toEqual(1);
});
it('Clicking outside to deselect a file row will trigger details sidebar', function() {
var $tr = fileList.findFileEl('One.txt');
$tr.find('td.filename>a.name').click();
fileList.$el.find('tfoot').click();
expect($tr.hasClass('highlighted')).toEqual(false);
expect(fileList._detailsView.getFileInfo()).toEqual(null);
});
});
describe('File actions', function() {
it('Clicking on a file name will trigger default action', function() {
var actionStub = sinon.stub();
@ -1886,7 +1930,7 @@ describe('OCA.Files.FileList tests', function() {
);
fileList.fileActions.setDefault('text/plain', 'Test');
var $tr = fileList.findFileEl('One.txt');
$tr.find('td.filename>a.name').click();
$tr.find('td.filename .nametext').click();
expect(actionStub.calledOnce).toEqual(true);
expect(actionStub.getCall(0).args[0]).toEqual('One.txt');
var context = actionStub.getCall(0).args[1];

View file

@ -0,0 +1,104 @@
/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2015 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
describe('OCA.Files.MainFileInfoDetailView tests', function() {
var view, tooltipStub, previewStub, fncLazyLoadPreview, fileListMock;
beforeEach(function() {
tooltipStub = sinon.stub($.fn, 'tooltip');
fileListMock = sinon.mock(OCA.Files.FileList.prototype);
view = new OCA.Files.MainFileInfoDetailView();
});
afterEach(function() {
view.destroy();
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)));
view.setFileInfo(testFileInfo);
expect(view.$el.find('.fileName').text()).toEqual('One.txt');
expect(view.$el.find('.fileName').attr('title')).toEqual('One.txt');
expect(view.$el.find('.size').text()).toEqual('117.7 MB');
expect(view.$el.find('.size').attr('title')).toEqual('123456789 bytes');
expect(view.$el.find('.date').text()).toEqual('a few seconds ago');
expect(view.$el.find('.date').attr('title')).toEqual(dateExpected);
clock.restore();
});
it('displays favorite icon', function() {
view.setFileInfo(_.extend(testFileInfo, {
tags: [OC.TAG_FAVORITE]
}));
expect(view.$el.find('.favorite img').attr('src'))
.toEqual(OC.imagePath('core', 'actions/starred'));
view.setFileInfo(_.extend(testFileInfo, {
tags: []
}));
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'
}));
expect(view.$el.find('.thumbnail').css('background-image'))
.toContain('filetypes/text-calendar.svg');
// Folder
view.setFileInfo(_.extend(testFileInfo, {
mimetype: 'httpd/unix-directory'
}));
expect(view.$el.find('.thumbnail').css('background-image'))
.toContain('filetypes/folder.svg');
});
it('displays thumbnail', function() {
view.setFileInfo(_.extend(testFileInfo, {
mimetype: 'text/plain'
}));
var expectation = fileListMock.expects('lazyLoadPreview');
expectation.once();
view.setFileInfo(testFileInfo);
fileListMock.verify();
});
});
});

View file

@ -56,6 +56,7 @@ $application->setupPropagation();
\OCP\Util::addScript('files_sharing', 'share');
\OCP\Util::addScript('files_sharing', 'external');
\OCP\Util::addStyle('files_sharing', 'sharetabview');
// FIXME: registering a job here will cause additional useless SQL queries
// when the route is not cron.php, needs a better way

View file

@ -0,0 +1,3 @@
.app-files .shareTabView {
min-height: 100px;
}

View file

@ -57,7 +57,8 @@ OCA.Sharing.PublicApp = {
scrollContainer: $(window),
dragOptions: dragOptions,
folderDropOptions: folderDropOptions,
fileActions: fileActions
fileActions: fileActions,
detailsViewEnabled: false
}
);
this.files = OCA.Files.Files;

View file

@ -140,6 +140,10 @@
}
});
}, t('files_sharing', 'Share'));
OC.addScript('files_sharing', 'sharetabview').done(function() {
fileList.registerTabView(new OCA.Sharing.ShareTabView('shareTabView'));
});
},
/**

View file

@ -0,0 +1,67 @@
/*
* 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>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,
/** @lends OCA.Sharing.ShareTabView.prototype */ {
_template: null,
/**
* Initialize the details view
*/
initialize: function() {
OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments);
this.$el.addClass('shareTabView');
},
getLabel: function() {
return t('files_sharing', 'Sharing');
},
/**
* Renders this details view
*/
render: function() {
this.$el.empty();
if (!this._template) {
this._template = Handlebars.compile(TEMPLATE);
}
if (this._fileInfo) {
this.$el.append(this._template({
owner: this._fileInfo.shareOwner || OC.currentUser
}));
} else {
// TODO: render placeholder text?
}
}
});
OCA.Sharing.ShareTabView = ShareTabView;
})();

View file

@ -132,7 +132,7 @@ describe('OCA.Sharing.App tests', function() {
shareOwner: 'user2'
}]);
fileListIn.findFileEl('testdir').find('td a.name').click();
fileListIn.findFileEl('testdir').find('td .nametext').click();
expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir');

View file

@ -417,7 +417,39 @@
min-height: 100%;
}
/* APP-SIDEBAR ----------------------------------------------------------------*/
/*
Sidebar: a sidebar to be used within #app-content
have it as first element within app-content in order to shrink other
sibling containers properly. Compare Files app for example.
*/
#app-sidebar {
position: fixed;
top: 45px;
right: 0;
left: auto;
bottom: 0;
width: 27%;
display: block;
background: #eee;
-webkit-transition: margin-right 300ms;
-moz-transition: margin-right 300ms;
-o-transition: margin-right 300ms;
transition: margin-right 300ms;
overflow-x: hidden;
overflow-y: auto;
visibility: visible;
z-index: 500;
}
#app-content.with-app-sidebar {
margin-right: 27%;
}
#app-sidebar.disappear {
visibility: hidden;
}
/* APP-SETTINGS ---------------------------------------------------------------*/
@ -556,3 +588,50 @@ em {
padding:16px;
}
/* generic tab styles */
.tabHeaders {
margin: 15px;
background-color: #1D2D44;
}
.tabHeaders .tabHeader {
float: left;
border: 1px solid #ddd;
padding: 5px;
cursor: pointer;
background-color: #f8f8f8;
font-weight: bold;
}
.tabHeaders .tabHeader, .tabHeaders .tabHeader a {
color: #888;
}
.tabHeaders .tabHeader:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.tabHeaders .tabHeader:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.tabHeaders .tabHeader.selected,
.tabHeaders .tabHeader:hover {
background-color: #e8e8e8;
}
.tabHeaders .tabHeader.selected,
.tabHeaders .tabHeader.selected a,
.tabHeaders .tabHeader:hover,
.tabHeaders .tabHeader:hover a {
color: #000;
}
.tabsContainer {
clear: left;
}
.tabsContainer .tab {
padding: 15px;
}

View file

@ -103,6 +103,10 @@
z-index: 1000;
}
#app-sidebar{
width: 100%;
}
/* allow horizontal scrollbar in settings
otherwise user management is not usable on mobile */
#body-settings #app-content {

View file

@ -20,6 +20,26 @@
}
};
/**
* Shows the #app-sidebar and add .with-app-sidebar to subsequent siblings
*/
exports.Apps.showAppSidebar = function() {
var $appSidebar = $('#app-sidebar');
$appSidebar.removeClass('disappear')
$('#app-content').addClass('with-app-sidebar');
};
/**
* Shows the #app-sidebar and removes .with-app-sidebar from subsequent
* siblings
*/
exports.Apps.hideAppSidebar = function() {
var $appSidebar = $('#app-sidebar');
$appSidebar.addClass('disappear');
$('#app-content').removeClass('with-app-sidebar');
};
/**
* Provides a way to slide down a target area through a button and slide it
* up if the user clicks somewhere else. Used for the news app settings and

View file

@ -20,6 +20,7 @@
"oc-dialogs.js",
"js.js",
"l10n.js",
"apps.js",
"share.js",
"octemplate.js",
"eventsource.js",

View file

@ -1366,13 +1366,13 @@ function initCore() {
// if there is a scrollbar …
if($('#app-content').get(0).scrollHeight > $('#app-content').height()) {
if($(window).width() > 768) {
controlsWidth = $('#content').width() - $('#app-navigation').width() - getScrollBarWidth();
controlsWidth = $('#content').width() - $('#app-navigation').width() - $('#app-sidebar').width() - getScrollBarWidth();
} else {
controlsWidth = $('#content').width() - getScrollBarWidth();
}
} else { // if there is none
if($(window).width() > 768) {
controlsWidth = $('#content').width() - $('#app-navigation').width();
controlsWidth = $('#content').width() - $('#app-navigation').width() - $('#app-sidebar').width();
} else {
controlsWidth = $('#content').width();
}

View file

@ -121,6 +121,8 @@ window.isPhantom = /phantom/i.test(navigator.userAgent);
OC.TestUtil = TestUtil;
}
moment.locale('en');
// reset plugins
OC.Plugins._plugins = [];