Sorting on all series views is now working

New: Sorting is now persisted on a per page and browser basis
New: Series lists now support sorting on all views
This commit is contained in:
Mark McDowall 2013-12-27 00:31:34 -08:00
parent 4d6d477947
commit 6ba17782aa
13 changed files with 414 additions and 123 deletions

View File

@ -46,6 +46,21 @@
.page-toolbar { .page-toolbar {
margin-top : 10px; margin-top : 10px;
margin-bottom : 30px; margin-bottom : 30px;
.toolbar-group {
display: inline-block;
}
.sorting-buttons {
li {
a {
span {
display: inline-block;
width: 110px;
}
}
}
}
} }
.page-container { .page-container {

View File

@ -1,8 +1,8 @@
'use strict'; 'use strict';
define( define(
['Config'], ['underscore', 'Config'],
function (Config) { function (_, Config) {
return function () { return function () {
@ -22,7 +22,8 @@ define(
_setInitialState.call(this); _setInitialState.call(this);
this.on('backgrid:sort', _storeState, this); this.on('backgrid:sort', _storeStateFromBackgrid, this);
this.on('drone:sort', _storeState, this);
if (originalInit) { if (originalInit) {
originalInit.call(this, options); originalInit.call(this, options);
@ -38,9 +39,17 @@ define(
this.state.order = order; this.state.order = order;
}; };
var _storeState = function (column, sortDirection) { var _storeStateFromBackgrid = function (column, sortDirection) {
var order = _convertDirectionToInt(sortDirection); var order = _convertDirectionToInt(sortDirection);
var sortKey = column.has('sortValue') ? column.get('sortValue') : column.get('name'); var sortKey = column.has('sortValue') && _.isString(column.get('sortValue')) ? column.get('sortValue') : column.get('name');
Config.setValue('{0}.sortKey'.format(this.tableName), sortKey);
Config.setValue('{0}.sortDirection'.format(this.tableName), order);
};
var _storeState = function (sortModel, sortDirection) {
var order = _convertDirectionToInt(sortDirection);
var sortKey = sortModel.get('name');
Config.setValue('{0}.sortKey'.format(this.tableName), sortKey); Config.setValue('{0}.sortKey'.format(this.tableName), sortKey);
Config.setValue('{0}.sortDirection'.format(this.tableName), order); Config.setValue('{0}.sortDirection'.format(this.tableName), order);

View File

@ -41,66 +41,65 @@ define(
template: 'Series/Index/SeriesIndexLayoutTemplate', template: 'Series/Index/SeriesIndexLayoutTemplate',
regions: { regions: {
seriesRegion: '#x-series', seriesRegion : '#x-series',
toolbar : '#x-toolbar', toolbar : '#x-toolbar',
footer : '#x-series-footer' footer : '#x-series-footer'
}, },
columns: columns: [
[ {
{ name : 'statusWeight',
name : 'statusWeight', label : '',
label : '', cell : SeriesStatusCell
cell : SeriesStatusCell },
}, {
{ name : 'title',
name : 'title', label : 'Title',
label : 'Title', cell : SeriesTitleCell,
cell : SeriesTitleCell, cellValue: 'this'
cellValue: 'this' },
}, {
{ name : 'seasonCount',
name : 'seasonCount', label: 'Seasons',
label: 'Seasons', cell : 'integer'
cell : 'integer' },
}, {
{ name : 'qualityProfileId',
name : 'qualityProfileId', label: 'Quality',
label: 'Quality', cell : QualityProfileCell
cell : QualityProfileCell },
}, {
{ name : 'network',
name : 'network', label: 'Network',
label: 'Network', cell : 'string'
cell : 'string' },
}, {
{ name : 'nextAiring',
name : 'nextAiring', label : 'Next Airing',
label : 'Next Airing', cell : RelativeDateCell,
cell : RelativeDateCell, sortValue : function (model) {
sortValue : function (model) { var nextAiring = model.get('nextAiring');
var nextAiring = model.get('nextAiring');
if (!nextAiring) { if (!nextAiring) {
return Number.MAX_VALUE; return Number.MAX_VALUE;
}
return Moment(nextAiring).unix();
} }
},
{ return Moment(nextAiring).unix();
name : 'percentOfEpisodes',
label : 'Episodes',
cell : EpisodeProgressCell,
className: 'episode-progress-cell'
},
{
name : 'this',
label : '',
sortable: false,
cell : SeriesActionsCell
} }
], },
{
name : 'percentOfEpisodes',
label : 'Episodes',
cell : EpisodeProgressCell,
className: 'episode-progress-cell'
},
{
name : 'this',
label : '',
sortable: false,
cell : SeriesActionsCell
}
],
leftSideButtons: { leftSideButtons: {
type : 'default', type : 'default',
@ -138,25 +137,46 @@ define(
] ]
}, },
_showTable: function () { sortingOptions: {
this.currentView = new Backgrid.Grid({ type : 'sorting',
collection: SeriesCollection, storeState : false,
columns : this.columns, viewCollection: SeriesCollection,
className : 'table table-hover' items :
}); [
{
title: 'Title',
name : 'title'
},
{
title: 'Seasons',
name : 'seasonCount'
},
{
title: 'Quality',
name : 'qualityProfileId'
},
{
title: 'Network',
name : 'network'
},
{
title : 'Next Airing',
name : 'nextAiring',
sortValue : function (model) {
var nextAiring = model.get('nextAiring');
this._renderView(); if (!nextAiring) {
this._fetchCollection(); return Number.MAX_VALUE;
}, }
_showList: function () { return Moment(nextAiring).unix();
this.currentView = new ListCollectionView(); }
this._fetchCollection(); },
}, {
title: 'Episodes',
_showPosters: function () { name : 'percentOfEpisodes'
this.currentView = new PosterCollectionView(); }
this._fetchCollection(); ]
}, },
initialize: function () { initialize: function () {
@ -164,39 +184,8 @@ define(
this.listenTo(SeriesCollection, 'sync', this._renderView); this.listenTo(SeriesCollection, 'sync', this._renderView);
this.listenTo(SeriesCollection, 'remove', this._renderView); this.listenTo(SeriesCollection, 'remove', this._renderView);
},
_renderView: function () { this.viewButtons = {
if (SeriesCollection.length === 0) {
this.seriesRegion.show(new EmptyView());
this.toolbar.close();
}
else {
this.currentView.collection = SeriesCollection;
this.seriesRegion.show(this.currentView);
this._showToolbar();
this._showFooter();
}
},
onShow: function () {
this._showToolbar();
this._renderView();
},
_fetchCollection: function () {
SeriesCollection.fetch();
},
_showToolbar: function () {
if (this.toolbar.currentView) {
return;
}
var viewButtons = {
type : 'radio', type : 'radio',
storeState : true, storeState : true,
menuKey : 'seriesViewMode', menuKey : 'seriesViewMode',
@ -226,12 +215,71 @@ define(
} }
] ]
}; };
},
_showTable: function () {
this.currentView = new Backgrid.Grid({
collection: SeriesCollection,
columns : this.columns,
className : 'table table-hover'
});
this._fetchCollection();
},
_showList: function () {
this.currentView = new ListCollectionView({ collection: SeriesCollection });
this._fetchCollection();
},
_showPosters: function () {
this.currentView = new PosterCollectionView({ collection: SeriesCollection });
this._fetchCollection();
},
_renderView: function () {
if (SeriesCollection.length === 0) {
this.seriesRegion.show(new EmptyView());
this.toolbar.close();
}
else {
this.seriesRegion.show(this.currentView);
this._showToolbar();
this._showFooter();
}
},
onShow: function () {
this._showToolbar();
this._renderView();
},
_fetchCollection: function () {
SeriesCollection.fetch();
},
_showToolbar: function () {
if (this.toolbar.currentView) {
return;
}
var rightButtons = [
this.viewButtons
];
if (this.showSortingButton) {
rightButtons.splice(0, 0, this.sortingOptions);
}
rightButtons.splice(0, 0, this.sortingOptions);
this.toolbar.show(new ToolbarLayout({ this.toolbar.show(new ToolbarLayout({
right : right : rightButtons,
[
viewButtons
],
left : left :
[ [
this.leftSideButtons this.leftSideButtons

View File

@ -8,7 +8,7 @@ define(
'api!series', 'api!series',
'Mixins/AsPersistedStateCollection' 'Mixins/AsPersistedStateCollection'
], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsPersistedStateCollection) { ], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsPersistedStateCollection) {
var Collection = Backbone.Collection.extend({ var Collection = PageableCollection.extend({
url : window.NzbDrone.ApiRoot + '/series', url : window.NzbDrone.ApiRoot + '/series',
model: SeriesModel, model: SeriesModel,
tableName: 'series', tableName: 'series',

View File

@ -11,6 +11,14 @@ define(
'click': 'onClick' 'click': 'onClick'
}, },
_originalInit: Backgrid.HeaderCell.prototype.initialize,
initialize: function (options) {
this._originalInit.call(this, options);
this.listenTo(this.collection, 'drone:sort', this.render);
},
render: function () { render: function () {
this.$el.empty(); this.$el.empty();
this.$el.append(this.column.get('label')); this.$el.append(this.column.get('label'));
@ -37,6 +45,10 @@ define(
if (key === this.column.get('name')) { if (key === this.column.get('name')) {
this._setSortIcon(order); this._setSortIcon(order);
} }
else {
this._removeSortIcon();
}
} }
return this; return this;

View File

@ -1,13 +1,29 @@
'use strict'; 'use strict';
define( define(
[ [
'underscore',
'backbone' 'backbone'
], function (Backbone) { ], function (_, Backbone) {
return Backbone.Model.extend({ return Backbone.Model.extend({
defaults: { defaults: {
'target' : '/nzbdrone/route', 'target' : '/nzbdrone/route',
'title' : '', 'title' : '',
'active' : false, 'active' : false,
'tooltip': undefined } 'tooltip': undefined
},
sortValue: function () {
var sortValue = this.get('sortValue');
if (_.isString(sortValue)) {
return this[sortValue];
}
else if (_.isFunction(sortValue)) {
return sortValue;
}
return function (model, colName) {
return model.get(colName);
};
}
}); });
}); });

View File

@ -13,7 +13,6 @@ define(
'click': 'onClick' 'click': 'onClick'
}, },
initialize: function () { initialize: function () {
this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key'); this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key');
@ -53,7 +52,6 @@ define(
callback.call(this.model.ownerContext); callback.call(this.model.ownerContext);
} }
} }
}); });
}); });

View File

@ -0,0 +1,87 @@
'use strict';
define(
[
'backbone.pageable',
'marionette',
'Shared/Toolbar/Sorting/SortingButtonView'
], function (PageableCollection, Marionette, ButtonView) {
return Marionette.CompositeView.extend({
itemView : ButtonView,
template : 'Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate',
itemViewContainer: '.dropdown-menu',
initialize: function (options) {
this.viewCollection = options.viewCollection;
this.listenTo(this.viewCollection, 'drone:sort', this.sort);
},
itemViewOptions: function () {
return {
viewCollection: this.viewCollection
};
},
sort: function (sortModel, sortDirection) {
var collection = this.viewCollection;
var order;
if (sortDirection === 'ascending') {
order = -1;
}
else if (sortDirection === 'descending') {
order = 1;
}
else {
order = null;
}
var comparator = this.makeComparator(sortModel.get('name'), order,
order ?
sortModel.sortValue() :
function (model) {
return model.cid;
});
if (PageableCollection &&
collection instanceof PageableCollection) {
collection.setSorting(order && sortModel.get('name'), order,
{sortValue: sortModel.sortValue()});
if (collection.mode === 'client') {
if (collection.fullCollection.comparator === null) {
collection.fullCollection.comparator = comparator;
}
collection.fullCollection.sort();
}
else {
collection.fetch({reset: true});
}
}
else {
collection.comparator = comparator;
collection.sort();
}
return this;
},
makeComparator: function (attr, order, func) {
return function (left, right) {
// extract the values from the models
var l = func(left, attr), r = func(right, attr), t;
// if descending order, swap left and right
if (order === 1) t = l, l = r, r = t;
// compare as usual
if (l === r) return 0;
else if (l < r) return -1;
return 1;
};
}
});
});

View File

@ -0,0 +1,8 @@
<div class="btn-group sorting-buttons">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
Sort <span class="caret"></span>
</a>
<ul class="dropdown-menu">
</ul>
</div>

View File

@ -0,0 +1,84 @@
'use strict';
define(
[
'backbone',
'marionette',
'underscore'
], function (Backbone, Marionette, _) {
return Marionette.ItemView.extend({
template : 'Shared/Toolbar/Sorting/SortingButtonViewTemplate',
tagName : 'li',
ui: {
icon: 'i'
},
events: {
'click': 'onClick'
},
initialize: function (options) {
this.viewCollection = options.viewCollection;
this.listenTo(this.viewCollection, 'drone:sort', this.render);
this.listenTo(this.viewCollection, 'backgrid:sort', this.render);
},
onRender: function () {
if (this.viewCollection.state) {
var key = this.viewCollection.state.sortKey;
var order = this.viewCollection.state.order;
if (key === this.model.get('name')) {
this._setSortIcon(order);
}
else {
this._removeSortIcon();
}
}
},
onClick: function (e) {
e.preventDefault();
var collection = this.viewCollection;
var event = 'drone:sort';
collection.state.sortKey = this.model.get('name');
var direction = collection.state.order;
if (direction === 'ascending' || direction === -1)
{
collection.state.order = 'descending';
collection.trigger(event, this.model, 'descending');
}
else
{
collection.state.order = 'ascending';
collection.trigger(event, this.model, 'ascending');
}
},
_convertDirectionToIcon: function (dir) {
if (dir === 'ascending' || dir === -1) {
return 'icon-sort-up';
}
return 'icon-sort-down';
},
_setSortIcon: function (dir) {
this._removeSortIcon();
this.ui.icon.addClass(this._convertDirectionToIcon(dir));
},
_removeSortIcon: function () {
this.ui.icon.removeClass('icon-sort-up icon-sort-down');
}
});
});

View File

@ -0,0 +1,4 @@
<a href="#">
<span>{{title}}</span>
<i class=""></i>
</a>

View File

@ -6,8 +6,9 @@ define(
'Shared/Toolbar/ButtonModel', 'Shared/Toolbar/ButtonModel',
'Shared/Toolbar/Radio/RadioButtonCollectionView', 'Shared/Toolbar/Radio/RadioButtonCollectionView',
'Shared/Toolbar/Button/ButtonCollectionView', 'Shared/Toolbar/Button/ButtonCollectionView',
'Shared/Toolbar/Sorting/SortingButtonCollectionView',
'underscore' 'underscore'
], function (Marionette, ButtonCollection, ButtonModel, RadioButtonCollectionView, ButtonCollectionView,_) { ], function (Marionette, ButtonCollection, ButtonModel, RadioButtonCollectionView, ButtonCollectionView, SortingButtonCollectionView, _) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'Shared/Toolbar/ToolbarLayoutTemplate', template: 'Shared/Toolbar/ToolbarLayoutTemplate',
@ -78,6 +79,15 @@ define(
}); });
break; break;
} }
case 'sorting':
{
buttonGroupView = new SortingButtonCollectionView({
collection : groupCollection,
menu : buttonGroup,
viewCollection: buttonGroup.viewCollection
});
break;
}
default : default :
{ {
buttonGroupView = new ButtonCollectionView({ buttonGroupView = new ButtonCollectionView({

View File

@ -1,8 +1,8 @@
<div class="pull-left page-toolbar"> <div class="pull-left page-toolbar">
<div class="x-toolbar-left-1"/> <div class="toolbar-group x-toolbar-left-1"/>
<div class="x-toolbar-left-2"/> <div class="toolbar-group x-toolbar-left-2"/>
</div> </div>
<div class="pull-right page-toolbar"> <div class="pull-right page-toolbar">
<div class="x-toolbar-right-1"/> <div class="toolbar-group x-toolbar-right-1"/>
<div class="x-toolbar-right-2"/> <div class="toolbar-group x-toolbar-right-2"/>
</div> </div>