Merge pull request #18318 from owncloud/files-sidebar-fixtabs

Improve right sidebar tabs behavior
This commit is contained in:
Morris Jobke 2015-08-16 12:53:50 +02:00
commit 5b7143885d
7 changed files with 202 additions and 86 deletions

View File

@ -9,23 +9,26 @@
*/
(function() {
var TEMPLATE =
'<div>' +
' <div class="detailFileInfoContainer">' +
' </div>' +
' <div>' +
' {{#if tabHeaders}}' +
' <ul class="tabHeaders">' +
' {{#each tabHeaders}}' +
' <li class="tabHeader" data-tabid="{{tabId}}" data-tabindex="{{tabIndex}}">' +
' <a href="#">{{label}}</a>' +
' </li>' +
' {{/each}}' +
' </ul>' +
' {{/if}}' +
' <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
@ -39,7 +42,6 @@
className: 'detailsView',
_template: null,
_templateTabHeader: null,
/**
* List of detail tab views
@ -62,6 +64,11 @@
*/
_currentTabId: null,
/**
* Dirty flag, whether the view needs to be rerendered
*/
_dirty: false,
events: {
'click a.close': '_onClose',
'click .tabHeaders .tabHeader': '_onClickTab'
@ -74,8 +81,10 @@
this._tabViews = [];
this._detailFileInfoViews = [];
this._dirty = true;
// uncomment to add some dummy tabs for testing
// this._addTestTabs();
//this._addTestTabs();
},
_onClose: function(event) {
@ -85,28 +94,21 @@
_onClickTab: function(e) {
var $target = $(e.target);
e.preventDefault();
if (!$target.hasClass('tabHeader')) {
$target = $target.closest('.tabHeader');
}
var tabIndex = $target.attr('data-tabindex');
var targetTab;
if (_.isUndefined(tabIndex)) {
var tabId = $target.attr('data-tabid');
if (_.isUndefined(tabId)) {
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();
this.selectTab(tabId);
},
_addTestTabs: function() {
for (var j = 0; j < 2; j++) {
var testView = new OCA.Files.DetailTabView('testtab' + j);
var testView = new OCA.Files.DetailTabView({id: 'testtab' + j});
testView.index = j;
testView.getLabel = function() { return 'Test tab ' + this.index; };
testView.render = function() {
@ -119,26 +121,34 @@
}
},
template: function(vars) {
if (!this._template) {
this._template = Handlebars.compile(TEMPLATE);
}
return this._template(vars);
},
/**
* Renders this details view
*/
render: function() {
var self = this;
if (!this._template) {
this._template = Handlebars.compile(TEMPLATE);
}
if (!this._templateTabHeader) {
this._templateTabHeader = Handlebars.compile(TEMPLATE_TAB_HEADER);
}
this.$el.html(this._template({
var templateVars = {
closeLabel: t('files', 'Close')
}));
};
if (this._tabViews.length > 1) {
// only render headers if there is more than one available
templateVars.tabHeaders = _.map(this._tabViews, function(tabView, i) {
return {
tabId: tabView.id,
tabIndex: i,
label: tabView.getLabel()
};
});
}
this.$el.html(this.template(templateVars));
var $tabsContainer = this.$el.find('.tabsContainer');
var $tabHeadsContainer = this.$el.find('.tabHeaders');
var $detailsContainer = this.$el.find('.detailFileInfoContainer');
// render details
@ -146,29 +156,58 @@
$detailsContainer.append(detailView.get$());
});
if (this._tabViews.length > 0) {
if (!this._currentTab) {
this._currentTab = this._tabViews[0].id;
if (!this._currentTabId && this._tabViews.length > 0) {
this._currentTabId = this._tabViews[0].id;
}
// render tabs
_.each(this._tabViews, function(tabView, i) {
// hidden by default
var $el = tabView.get$();
var isCurrent = (tabView.id === self._currentTab);
if (!isCurrent) {
$el.addClass('hidden');
}
$tabsContainer.append($el);
this.selectTab(this._currentTabId);
$tabHeadsContainer.append(self._templateTabHeader({
tabId: tabView.id,
tabIndex: i,
label: tabView.getLabel(),
selected: isCurrent
}));
this._dirty = false;
},
/**
* Selects the given tab by id
*
* @param {string} tabId tab id
*/
selectTab: function(tabId) {
if (!tabId) {
return;
}
var tabView = _.find(this._tabViews, function(tab) {
return tab.id === tabId;
});
if (!tabView) {
console.warn('Details view tab with id "' + tabId + '" not found');
return;
}
this._currentTabId = tabId;
var $tabsContainer = this.$el.find('.tabsContainer');
var $tabEl = $tabsContainer.find('#' + tabId);
// hide other tabs
$tabsContainer.find('.tab').addClass('hidden');
// tab already rendered ?
if (!$tabEl.length) {
// render tab
$tabsContainer.append(tabView.$el);
$tabEl = tabView.$el;
}
// this should trigger tab rendering
tabView.setFileInfo(this.model);
$tabEl.removeClass('hidden');
// update tab headers
var $tabHeaders = this.$el.find('.tabHeaders li');
$tabHeaders.removeClass('selected');
$tabHeaders.filterAttr('data-tabid', tabView.id).addClass('selected');
},
/**
@ -179,12 +218,19 @@
setFileInfo: function(fileInfo) {
this.model = fileInfo;
if (this._dirty) {
this.render();
}
// notify all panels
_.each(this._tabViews, function(tabView) {
tabView.setFileInfo(fileInfo);
if (this._currentTabId) {
// only update current tab, others will be updated on-demand
var tabId = this._currentTabId;
var tabView = _.find(this._tabViews, function(tab) {
return tab.id === tabId;
});
tabView.setFileInfo(fileInfo);
}
_.each(this._detailFileInfoViews, function(detailView) {
detailView.setFileInfo(fileInfo);
});
@ -206,6 +252,7 @@
*/
addTabView: function(tabView) {
this._tabViews.push(tabView);
this._dirty = true;
},
/**
@ -215,6 +262,7 @@
*/
addDetailView: function(detailView) {
this._detailFileInfoViews.push(detailView);
this._dirty = true;
}
});

View File

@ -71,8 +71,10 @@
* @param {OCA.Files.FileInfoModel} fileInfo file info to set
*/
setFileInfo: function(fileInfo) {
if (this.model !== fileInfo) {
this.model = fileInfo;
this.render();
}
},
/**

View File

@ -108,6 +108,8 @@
}
toggleStar($actionEl, !isFavorite);
context.fileInfoModel.set('tags', tags);
self.applyFileTags(
dir + '/' + fileName,
tags,
@ -124,7 +126,6 @@
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;
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -67,32 +67,75 @@ describe('OCA.Files.DetailsView tests', function() {
var testView, testView2;
beforeEach(function() {
testView = new OCA.Files.DetailTabView('test1');
testView2 = new OCA.Files.DetailTabView('test2');
testView = new OCA.Files.DetailTabView({id: 'test1'});
testView2 = new OCA.Files.DetailTabView({id: 'test2'});
detailsView.addTabView(testView);
detailsView.addTabView(testView2);
detailsView.render();
});
it('renders registered tabs', function() {
expect(detailsView.$el.find('.tab').length).toEqual(2);
it('initially renders only the selected tab', function() {
expect(detailsView.$el.find('.tab').length).toEqual(1);
expect(detailsView.$el.find('.tab').attr('id')).toEqual('test1');
});
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);
it('updates tab model and rerenders on-demand as soon as it gets selected', function() {
var tab1RenderStub = sinon.stub(testView, 'render');
var tab2RenderStub = sinon.stub(testView2, 'render');
var fileInfo1 = new OCA.Files.FileInfoModel({id: 5, name: 'test.txt'});
var fileInfo2 = new OCA.Files.FileInfoModel({id: 8, name: 'test2.txt'});
expect(testView.getFileInfo()).toEqual(fileInfo);
expect(testView2.getFileInfo()).toEqual(fileInfo);
detailsView.setFileInfo(fileInfo1);
expect(tabRenderStub.callCount).toEqual(2);
tabRenderStub.restore();
// first tab renders, not the second one
expect(tab1RenderStub.calledOnce).toEqual(true);
expect(tab2RenderStub.notCalled).toEqual(true);
// info got set only to the first visible tab
expect(testView.getFileInfo()).toEqual(fileInfo1);
expect(testView2.getFileInfo()).toBeUndefined();
// select second tab for first render
detailsView.$el.find('.tabHeader').eq(1).click();
// second tab got rendered
expect(tab2RenderStub.calledOnce).toEqual(true);
expect(testView2.getFileInfo()).toEqual(fileInfo1);
// select the first tab again
detailsView.$el.find('.tabHeader').eq(0).click();
// no re-render
expect(tab1RenderStub.calledOnce).toEqual(true);
expect(tab2RenderStub.calledOnce).toEqual(true);
tab1RenderStub.reset();
tab2RenderStub.reset();
// switch to another file
detailsView.setFileInfo(fileInfo2);
// only the visible tab was updated and rerendered
expect(tab1RenderStub.calledOnce).toEqual(true);
expect(testView.getFileInfo()).toEqual(fileInfo2);
// second/invisible tab still has old info, not rerendered
expect(tab2RenderStub.notCalled).toEqual(true);
expect(testView2.getFileInfo()).toEqual(fileInfo1);
// reselect the second one
detailsView.$el.find('.tabHeader').eq(1).click();
// second tab becomes visible, updated and rendered
expect(testView2.getFileInfo()).toEqual(fileInfo2);
expect(tab2RenderStub.calledOnce).toEqual(true);
tab1RenderStub.restore();
tab2RenderStub.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);
expect(detailsView.$el.find('.tab').eq(1).length).toEqual(0);
});
it('switches the current tab when clicking on tab header', function() {
detailsView.$el.find('.tabHeader').eq(1).click();
@ -101,5 +144,14 @@ describe('OCA.Files.DetailsView tests', function() {
expect(detailsView.$el.find('.tab').eq(0).hasClass('hidden')).toEqual(true);
expect(detailsView.$el.find('.tab').eq(1).hasClass('hidden')).toEqual(false);
});
it('does not render tab headers when only one tab exists', function() {
detailsView.remove();
detailsView = new OCA.Files.DetailsView();
testView = new OCA.Files.DetailTabView({id: 'test1'});
detailsView.addTabView(testView);
detailsView.render();
expect(detailsView.$el.find('.tabHeader').length).toEqual(0);
});
});
});

View File

@ -60,6 +60,14 @@
return tr;
};
var oldElementToFile = fileList.elementToFile;
fileList.elementToFile = function($el) {
var fileInfo = oldElementToFile.apply(this, arguments);
fileInfo.sharePermissions = $el.attr('data-share-permissions') || undefined;
fileInfo.shareOwner = $el.attr('data-share-owner') || undefined;
return fileInfo;
};
// use delegate to catch the case with multiple file lists
fileList.$el.on('fileActionsReady', function(ev){
var fileList = ev.fileList;

View File

@ -10,7 +10,7 @@
(function() {
var TEMPLATE =
'<div>Owner: {{owner}}';
'<div><ul>{{#if owner}}<li>Owner: {{owner}}</li>{{/if}}</ul></div>';
/**
* @memberof OCA.Sharing
@ -37,8 +37,13 @@
}
if (this.model) {
console.log(this.model);
var owner = this.model.get('shareOwner');
if (owner === OC.currentUser) {
owner = null;
}
this.$el.append(this._template({
owner: this.model.get('shareOwner') || OC.currentUser
owner: owner
}));
} else {