From 6ba17782aa2134de479f288b252e4c6ae119372a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 27 Dec 2013 00:31:34 -0800 Subject: [PATCH] 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 --- src/UI/Content/theme.less | 15 ++ src/UI/Mixins/AsPersistedStateCollection.js | 19 +- src/UI/Series/Index/SeriesIndexLayout.js | 248 +++++++++++------- src/UI/Series/SeriesCollection.js | 2 +- src/UI/Shared/Grid/HeaderCell.js | 12 + src/UI/Shared/Toolbar/ButtonModel.js | 20 +- .../Shared/Toolbar/Radio/RadioButtonView.js | 2 - .../Sorting/SortingButtonCollectionView.js | 87 ++++++ .../SortingButtonCollectionViewTemplate.html | 8 + .../Toolbar/Sorting/SortingButtonView.js | 84 ++++++ .../Sorting/SortingButtonViewTemplate.html | 4 + src/UI/Shared/Toolbar/ToolbarLayout.js | 12 +- .../Shared/Toolbar/ToolbarLayoutTemplate.html | 8 +- 13 files changed, 406 insertions(+), 115 deletions(-) create mode 100644 src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js create mode 100644 src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html create mode 100644 src/UI/Shared/Toolbar/Sorting/SortingButtonView.js create mode 100644 src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.html diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index 8bf71358e..bf90d7856 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -46,6 +46,21 @@ .page-toolbar { margin-top : 10px; margin-bottom : 30px; + + .toolbar-group { + display: inline-block; + } + + .sorting-buttons { + li { + a { + span { + display: inline-block; + width: 110px; + } + } + } + } } .page-container { diff --git a/src/UI/Mixins/AsPersistedStateCollection.js b/src/UI/Mixins/AsPersistedStateCollection.js index 721c4c68a..2a957cc84 100644 --- a/src/UI/Mixins/AsPersistedStateCollection.js +++ b/src/UI/Mixins/AsPersistedStateCollection.js @@ -1,8 +1,8 @@ 'use strict'; define( - ['Config'], - function (Config) { + ['underscore', 'Config'], + function (_, Config) { return function () { @@ -22,7 +22,8 @@ define( _setInitialState.call(this); - this.on('backgrid:sort', _storeState, this); + this.on('backgrid:sort', _storeStateFromBackgrid, this); + this.on('drone:sort', _storeState, this); if (originalInit) { originalInit.call(this, options); @@ -38,9 +39,17 @@ define( this.state.order = order; }; - var _storeState = function (column, sortDirection) { + var _storeStateFromBackgrid = function (column, 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}.sortDirection'.format(this.tableName), order); diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index 9a6390cb7..adbd0a84c 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -41,66 +41,65 @@ define( template: 'Series/Index/SeriesIndexLayoutTemplate', regions: { - seriesRegion: '#x-series', - toolbar : '#x-toolbar', - footer : '#x-series-footer' + seriesRegion : '#x-series', + toolbar : '#x-toolbar', + footer : '#x-series-footer' }, - columns: - [ - { - name : 'statusWeight', - label : '', - cell : SeriesStatusCell - }, - { - name : 'title', - label : 'Title', - cell : SeriesTitleCell, - cellValue: 'this' - }, - { - name : 'seasonCount', - label: 'Seasons', - cell : 'integer' - }, - { - name : 'qualityProfileId', - label: 'Quality', - cell : QualityProfileCell - }, - { - name : 'network', - label: 'Network', - cell : 'string' - }, - { - name : 'nextAiring', - label : 'Next Airing', - cell : RelativeDateCell, - sortValue : function (model) { - var nextAiring = model.get('nextAiring'); - - if (!nextAiring) { - return Number.MAX_VALUE; - } + columns: [ + { + name : 'statusWeight', + label : '', + cell : SeriesStatusCell + }, + { + name : 'title', + label : 'Title', + cell : SeriesTitleCell, + cellValue: 'this' + }, + { + name : 'seasonCount', + label: 'Seasons', + cell : 'integer' + }, + { + name : 'qualityProfileId', + label: 'Quality', + cell : QualityProfileCell + }, + { + name : 'network', + label: 'Network', + cell : 'string' + }, + { + name : 'nextAiring', + label : 'Next Airing', + cell : RelativeDateCell, + sortValue : function (model) { + var nextAiring = model.get('nextAiring'); - return Moment(nextAiring).unix(); + if (!nextAiring) { + return Number.MAX_VALUE; } - }, - { - name : 'percentOfEpisodes', - label : 'Episodes', - cell : EpisodeProgressCell, - className: 'episode-progress-cell' - }, - { - name : 'this', - label : '', - sortable: false, - cell : SeriesActionsCell + + return Moment(nextAiring).unix(); } - ], + }, + { + name : 'percentOfEpisodes', + label : 'Episodes', + cell : EpisodeProgressCell, + className: 'episode-progress-cell' + }, + { + name : 'this', + label : '', + sortable: false, + cell : SeriesActionsCell + } + ], leftSideButtons: { type : 'default', @@ -138,6 +137,86 @@ define( ] }, + sortingOptions: { + type : 'sorting', + storeState : false, + viewCollection: SeriesCollection, + 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'); + + if (!nextAiring) { + return Number.MAX_VALUE; + } + + return Moment(nextAiring).unix(); + } + }, + { + title: 'Episodes', + name : 'percentOfEpisodes' + } + ] + }, + + initialize: function () { + this.seriesCollection = SeriesCollection; + + this.listenTo(SeriesCollection, 'sync', this._renderView); + this.listenTo(SeriesCollection, 'remove', this._renderView); + + this.viewButtons = { + type : 'radio', + storeState : true, + menuKey : 'seriesViewMode', + defaultAction: 'listView', + items : + [ + { + key : 'posterView', + title : '', + tooltip : 'Posters', + icon : 'icon-th-large', + callback: this._showPosters + }, + { + key : 'listView', + title : '', + tooltip : 'Overview List', + icon : 'icon-th-list', + callback: this._showList + }, + { + key : 'tableView', + title : '', + tooltip : 'Table', + icon : 'icon-table', + callback: this._showTable + } + ] + }; + }, + _showTable: function () { this.currentView = new Backgrid.Grid({ collection: SeriesCollection, @@ -145,25 +224,19 @@ define( className : 'table table-hover' }); - this._renderView(); this._fetchCollection(); }, _showList: function () { - this.currentView = new ListCollectionView(); - this._fetchCollection(); - }, + this.currentView = new ListCollectionView({ collection: SeriesCollection }); - _showPosters: function () { - this.currentView = new PosterCollectionView(); this._fetchCollection(); }, - initialize: function () { - this.seriesCollection = SeriesCollection; + _showPosters: function () { + this.currentView = new PosterCollectionView({ collection: SeriesCollection }); - this.listenTo(SeriesCollection, 'sync', this._renderView); - this.listenTo(SeriesCollection, 'remove', this._renderView); + this._fetchCollection(); }, _renderView: function () { @@ -173,7 +246,6 @@ define( this.toolbar.close(); } else { - this.currentView.collection = SeriesCollection; this.seriesRegion.show(this.currentView); this._showToolbar(); @@ -196,42 +268,18 @@ define( return; } - var viewButtons = { - type : 'radio', - storeState : true, - menuKey : 'seriesViewMode', - defaultAction: 'listView', - items : - [ - { - key : 'posterView', - title : '', - tooltip : 'Posters', - icon : 'icon-th-large', - callback: this._showPosters - }, - { - key : 'listView', - title : '', - tooltip : 'Overview List', - icon : 'icon-th-list', - callback: this._showList - }, - { - key : 'tableView', - title : '', - tooltip : 'Table', - icon : 'icon-table', - callback: this._showTable - } - ] - }; + var rightButtons = [ + this.viewButtons + ]; + + if (this.showSortingButton) { + rightButtons.splice(0, 0, this.sortingOptions); + } + + rightButtons.splice(0, 0, this.sortingOptions); this.toolbar.show(new ToolbarLayout({ - right : - [ - viewButtons - ], + right : rightButtons, left : [ this.leftSideButtons diff --git a/src/UI/Series/SeriesCollection.js b/src/UI/Series/SeriesCollection.js index 74c16f27d..2c7b049f8 100644 --- a/src/UI/Series/SeriesCollection.js +++ b/src/UI/Series/SeriesCollection.js @@ -8,7 +8,7 @@ define( 'api!series', 'Mixins/AsPersistedStateCollection' ], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsPersistedStateCollection) { - var Collection = Backbone.Collection.extend({ + var Collection = PageableCollection.extend({ url : window.NzbDrone.ApiRoot + '/series', model: SeriesModel, tableName: 'series', diff --git a/src/UI/Shared/Grid/HeaderCell.js b/src/UI/Shared/Grid/HeaderCell.js index 2f1f3c429..45b402589 100644 --- a/src/UI/Shared/Grid/HeaderCell.js +++ b/src/UI/Shared/Grid/HeaderCell.js @@ -11,6 +11,14 @@ define( '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 () { this.$el.empty(); this.$el.append(this.column.get('label')); @@ -37,6 +45,10 @@ define( if (key === this.column.get('name')) { this._setSortIcon(order); } + + else { + this._removeSortIcon(); + } } return this; diff --git a/src/UI/Shared/Toolbar/ButtonModel.js b/src/UI/Shared/Toolbar/ButtonModel.js index f6c7c8a64..b7bd6d4dc 100644 --- a/src/UI/Shared/Toolbar/ButtonModel.js +++ b/src/UI/Shared/Toolbar/ButtonModel.js @@ -1,13 +1,29 @@ 'use strict'; define( [ + 'underscore', 'backbone' - ], function (Backbone) { + ], function (_, Backbone) { return Backbone.Model.extend({ defaults: { 'target' : '/nzbdrone/route', 'title' : '', '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); + }; + } }); }); diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js index 3f5be2a6f..1fb788250 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js @@ -13,7 +13,6 @@ define( 'click': 'onClick' }, - initialize: function () { this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key'); @@ -53,7 +52,6 @@ define( callback.call(this.model.ownerContext); } } - }); }); diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js new file mode 100644 index 000000000..a79abc2f0 --- /dev/null +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js @@ -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; + }; + } + }); + }); + + diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html new file mode 100644 index 000000000..62f6da91e --- /dev/null +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html @@ -0,0 +1,8 @@ +
+ + Sort + + +
diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js b/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js new file mode 100644 index 000000000..7421e628f --- /dev/null +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js @@ -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'); + } + }); + }); + + + + diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.html b/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.html new file mode 100644 index 000000000..a969d5dc2 --- /dev/null +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.html @@ -0,0 +1,4 @@ + + {{title}} + + \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ToolbarLayout.js b/src/UI/Shared/Toolbar/ToolbarLayout.js index 7f4b64bf9..8a907dad6 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayout.js +++ b/src/UI/Shared/Toolbar/ToolbarLayout.js @@ -6,8 +6,9 @@ define( 'Shared/Toolbar/ButtonModel', 'Shared/Toolbar/Radio/RadioButtonCollectionView', 'Shared/Toolbar/Button/ButtonCollectionView', + 'Shared/Toolbar/Sorting/SortingButtonCollectionView', 'underscore' - ], function (Marionette, ButtonCollection, ButtonModel, RadioButtonCollectionView, ButtonCollectionView,_) { + ], function (Marionette, ButtonCollection, ButtonModel, RadioButtonCollectionView, ButtonCollectionView, SortingButtonCollectionView, _) { return Marionette.Layout.extend({ template: 'Shared/Toolbar/ToolbarLayoutTemplate', @@ -78,6 +79,15 @@ define( }); break; } + case 'sorting': + { + buttonGroupView = new SortingButtonCollectionView({ + collection : groupCollection, + menu : buttonGroup, + viewCollection: buttonGroup.viewCollection + }); + break; + } default : { buttonGroupView = new ButtonCollectionView({ diff --git a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html index c2f497b3f..b4cd4dcda 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html +++ b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html @@ -1,8 +1,8 @@ 
-
-
+
+
-
-
+
+