From df568df9dc31f84d40749f8c1445ae0e26225897 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 11 Nov 2013 19:35:57 -0500 Subject: [PATCH] Controls for sorting topic columns --- .../components/basic_topic_list_component.js | 28 +++- .../discourse/components/heading_component.js | 27 ++++ .../user_topics_list_controller.js | 4 + .../discourse/helpers/i18n_helpers.js | 5 +- .../discourse/models/sort_order.js | 30 ++++ .../discourse/models/topic_list.js | 72 +++++++--- .../routes/user_topic_list_routes.js | 2 +- .../discourse-basic-topic-list.js.handlebars | 128 ++++++++++-------- .../discourse-heading.js.handlebars | 2 + .../list/user_topics_list.js.handlebars | 2 +- .../stylesheets/desktop/topic-list.scss | 30 +++- app/controllers/list_controller.rb | 4 +- lib/topic_query.rb | 17 ++- test/javascripts/models/sort_order_test.js | 23 ++++ 14 files changed, 287 insertions(+), 87 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/heading_component.js create mode 100644 app/assets/javascripts/discourse/models/sort_order.js create mode 100644 app/assets/javascripts/discourse/templates/components/discourse-heading.js.handlebars create mode 100644 test/javascripts/models/sort_order_test.js diff --git a/app/assets/javascripts/discourse/components/basic_topic_list_component.js b/app/assets/javascripts/discourse/components/basic_topic_list_component.js index 51c92bae95..ca725e889d 100644 --- a/app/assets/javascripts/discourse/components/basic_topic_list_component.js +++ b/app/assets/javascripts/discourse/components/basic_topic_list_component.js @@ -6,4 +6,30 @@ @namespace Discourse @module Discourse **/ -Discourse.BasicTopicListComponent = Ember.Component.extend({}); +Discourse.DiscourseBasicTopicListComponent = Ember.Component.extend({ + + loaded: function() { + var topicList = this.get('topicList'); + if (topicList) { + return topicList.get('loaded'); + } else { + return true; + } + }.property('topicList.loaded'), + + init: function() { + this._super(); + + var topicList = this.get('topicList'); + if (topicList) { + this.setProperties({ + topics: topicList.get('topics'), + sortOrder: topicList.get('sortOrder') + }); + } else { + // Without a topic list, we assume it's loaded always. + this.set('loaded', true); + } + } + +}); diff --git a/app/assets/javascripts/discourse/components/heading_component.js b/app/assets/javascripts/discourse/components/heading_component.js new file mode 100644 index 0000000000..80fe7a0d0d --- /dev/null +++ b/app/assets/javascripts/discourse/components/heading_component.js @@ -0,0 +1,27 @@ +Discourse.DiscourseHeadingComponent = Ember.Component.extend({ + tagName: 'th', + + classNameBindings: ['number:num', 'sortBy', 'iconSortClass:sorting', 'sortable'], + attributeBindings: ['colspan'], + + sortable: function() { + return this.get('sortOrder') && this.get('sortBy'); + }.property('sortOrder', 'sortBy'), + + iconSortClass: function() { + var sortable = this.get('sortable'); + + if (sortable && this.get('sortBy') === this.get('sortOrder.order')) { + return this.get('sortOrder.descending') ? 'icon-chevron-down' : 'icon-chevron-up'; + } + }.property('sortable', 'sortOrder.order', 'sortOrder.descending'), + + click: function() { + var sortOrder = this.get('sortOrder'), + sortBy = this.get('sortBy'); + + if (sortBy && sortOrder) { + sortOrder.toggle(sortBy); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/user_topics_list_controller.js b/app/assets/javascripts/discourse/controllers/user_topics_list_controller.js index 2e13d91fd2..b5a304e6d2 100644 --- a/app/assets/javascripts/discourse/controllers/user_topics_list_controller.js +++ b/app/assets/javascripts/discourse/controllers/user_topics_list_controller.js @@ -11,6 +11,10 @@ Discourse.UserTopicsListController = Discourse.ObjectController.extend({ actions: { loadMore: function() { this.get('model').loadMore(); + }, + + changeSort: function() { + console.log('sort changed!'); } } diff --git a/app/assets/javascripts/discourse/helpers/i18n_helpers.js b/app/assets/javascripts/discourse/helpers/i18n_helpers.js index 40622566e9..eea353f031 100644 --- a/app/assets/javascripts/discourse/helpers/i18n_helpers.js +++ b/app/assets/javascripts/discourse/helpers/i18n_helpers.js @@ -25,12 +25,13 @@ I18n.toHumanSize = function(number, options) { **/ Ember.Handlebars.registerHelper('i18n', function(property, options) { // Resolve any properties - var params, + var params = options.hash, self = this; - params = options.hash; + _.each(params, function(value, key) { params[key] = Em.Handlebars.get(self, value, options); }); + return I18n.t(property, params); }); diff --git a/app/assets/javascripts/discourse/models/sort_order.js b/app/assets/javascripts/discourse/models/sort_order.js new file mode 100644 index 0000000000..5cf9512b87 --- /dev/null +++ b/app/assets/javascripts/discourse/models/sort_order.js @@ -0,0 +1,30 @@ +/** + Represents the sort order of something, for example a topics list. + + @class SortOrder + @extends Ember.Object + @namespace Discourse + @module Discourse +**/ +Discourse.SortOrder = Ember.Object.extend({ + order: 'default', + descending: true, + + /** + Changes the sort to another column + + @method toggle + @params {String} order the new sort order + **/ + toggle: function(order) { + if (this.get('order') === order) { + this.toggleProperty('descending'); + } else { + this.setProperties({ + order: order, + descending: true + }); + } + } + +}); diff --git a/app/assets/javascripts/discourse/models/topic_list.js b/app/assets/javascripts/discourse/models/topic_list.js index ea0fb75e10..308260dde4 100644 --- a/app/assets/javascripts/discourse/models/topic_list.js +++ b/app/assets/javascripts/discourse/models/topic_list.js @@ -7,6 +7,26 @@ @module Discourse **/ +function finderFor(filter, params) { + return function() { + var url = Discourse.getURL("/") + filter + ".json"; + + if (params) { + var keys = Object.keys(params); + + if (keys.length > 0) { + var encoded = []; + keys.forEach(function(p) { + encoded.push(p + "=" + params[p]); + }); + + url += "?" + encoded.join('&'); + } + } + return Discourse.ajax(url); + } +} + Discourse.TopicList = Discourse.Model.extend({ forEachNew: function(topics, callback) { @@ -22,8 +42,38 @@ Discourse.TopicList = Discourse.Model.extend({ }); }, - loadMore: function() { + sortOrder: function() { + return Discourse.SortOrder.create(); + }.property(), + /** + If the sort order changes, replace the topics in the list with the new + order. + + @observes sortOrder + **/ + _sortOrderChanged: function() { + var self = this, + sortOrder = this.get('sortOrder'), + params = this.get('params'); + + params.sort_order = sortOrder.get('order'); + params.sort_descending = sortOrder.get('descending'); + + this.set('loaded', false); + var finder = finderFor(this.get('filter'), params); + finder().then(function (result) { + var newTopics = Discourse.TopicList.topicsFrom(result), + topics = self.get('topics'); + + topics.clear(); + topics.pushObjects(newTopics); + self.set('loaded', true); + }); + + }.observes('sortOrder.order', 'sortOrder.descending'), + + loadMore: function() { if (this.get('loadingMore')) { return Ember.RSVP.reject(); } var moreUrl = this.get('more_topics_url'); @@ -146,26 +196,16 @@ Discourse.TopicList.reopenClass({ return Ember.RSVP.resolve(list); } session.setProperties({topicList: null, topicListScrollPos: null}); - return Discourse.TopicList.find(filter, menuItem.get('excludeCategory')); - } -}); + return Discourse.TopicList.find(filter, {exclude_category: menuItem.get('excludeCategory')}); + }, + find: function(filter, params) { -Discourse.TopicList.reopenClass({ - - find: function(filter, excludeCategory) { - - // How we find our topic list - var finder = function() { - var url = Discourse.getURL("/") + filter + ".json"; - if (excludeCategory) { url += "?exclude_category=" + excludeCategory; } - return Discourse.ajax(url); - }; - - return PreloadStore.getAndRemove("topic_list", finder).then(function(result) { + return PreloadStore.getAndRemove("topic_list", finderFor(filter, params)).then(function(result) { var topicList = Discourse.TopicList.create({ inserted: Em.A(), filter: filter, + params: params || {}, topics: Discourse.TopicList.topicsFrom(result), can_create_topic: result.topic_list.can_create_topic, more_topics_url: result.topic_list.more_topics_url, diff --git a/app/assets/javascripts/discourse/routes/user_topic_list_routes.js b/app/assets/javascripts/discourse/routes/user_topic_list_routes.js index cbf9d60d73..99830a2a5e 100644 --- a/app/assets/javascripts/discourse/routes/user_topic_list_routes.js +++ b/app/assets/javascripts/discourse/routes/user_topic_list_routes.js @@ -45,6 +45,6 @@ Discourse.UserActivityFavoritesRoute = Discourse.UserTopicListRoute.extend({ userActionType: Discourse.UserAction.TYPES.favorites, model: function() { - return Discourse.TopicList.find('favorited?user_id=' + this.modelFor('user').get('id')); + return Discourse.TopicList.find('favorited', {user_id: this.modelFor('user').get('id') }); } }); \ No newline at end of file diff --git a/app/assets/javascripts/discourse/templates/components/discourse-basic-topic-list.js.handlebars b/app/assets/javascripts/discourse/templates/components/discourse-basic-topic-list.js.handlebars index 2b3152da26..a8b67a62a3 100644 --- a/app/assets/javascripts/discourse/templates/components/discourse-basic-topic-list.js.handlebars +++ b/app/assets/javascripts/discourse/templates/components/discourse-basic-topic-list.js.handlebars @@ -1,66 +1,76 @@ -{{#if topics}} - - - - {{#unless hideCategories}} - - {{/unless}} - - - - - +{{#if loaded}} + {{#if topics}} +
- {{i18n topic.title}} - {{i18n category_title}}{{i18n posts}}{{i18n views}}{{i18n activity}}
+ + {{#discourse-heading sortBy="default" sortOrder=sortOrder}} + {{i18n topic.title}} + {{/discourse-heading}} + {{#discourse-heading}} + {{i18n category_title}} + {{/discourse-heading}} + {{#discourse-heading sortBy="posts" number=true sortOrder=sortOrder}} + {{i18n posts}} + {{/discourse-heading}} + {{#discourse-heading sortBy="likes" number=true sortOrder=sortOrder}} + {{i18n likes}} + {{/discourse-heading}} + {{#discourse-heading sortBy="views" number=true sortOrder=sortOrder}} + {{i18n views}} + {{/discourse-heading}} + {{#discourse-heading sortBy="activity" number=true colspan="2" sortOrder=sortOrder}} + {{i18n activity}} + {{/discourse-heading}} + - {{#groupedEach topic in topics}} - - - {{#unless view.hideCategories}} - + + - {{/unless}} - + - + + + {{#if topic.bumped}} + + + {{else}} + + {{/if}} - + + {{/groupedEach}} - - {{#if topic.bumped}} - - - {{else}} - - - {{/if}} - - {{/groupedEach}} - -
+ {{#groupedEach topic in topics}} +
{{categoryLink topic.category}} {{number topic.posts_count numberKey="posts_long"}}{{number topic.posts_count numberKey="posts_long"}}{{number topic.views numberKey="views_long"}} + {{unboundAge topic.created_at}} + + {{unboundAge topic.bumped_at}} + + {{unboundAge topic.created_at}} +
{{number topic.views numberKey="views_long"}} - {{unboundAge topic.created_at}} - - {{unboundAge topic.bumped_at}} - - {{unboundAge topic.created_at}} -
+ + {{else}} +
+ {{i18n choose_topic.none_found}} +
+ {{/if}} {{else}} -
- {{i18n choose_topic.none_found}} -
-{{/if}} +
{{i18n loading}}
+{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/templates/components/discourse-heading.js.handlebars b/app/assets/javascripts/discourse/templates/components/discourse-heading.js.handlebars new file mode 100644 index 0000000000..773a9c5ded --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/discourse-heading.js.handlebars @@ -0,0 +1,2 @@ +{{yield}} + diff --git a/app/assets/javascripts/discourse/templates/list/user_topics_list.js.handlebars b/app/assets/javascripts/discourse/templates/list/user_topics_list.js.handlebars index 5cfef1025e..7eb83d9574 100644 --- a/app/assets/javascripts/discourse/templates/list/user_topics_list.js.handlebars +++ b/app/assets/javascripts/discourse/templates/list/user_topics_list.js.handlebars @@ -1 +1 @@ -{{discourse-basic-topic-list topics=model.topics hideCategories="true"}} +{{discourse-basic-topic-list topicList=model}} diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index c8d7b5897c..820c67c02e 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -21,9 +21,9 @@ font-weight: normal; } - a.badge-category {padding: 3px 12px; font-size: 16px; + a.badge-category {padding: 3px 12px; font-size: 16px; - &.category-dropdown-button { + &.category-dropdown-button { padding: 3px 9px 2px 9px; i {height: 20px;} @@ -104,6 +104,7 @@ font-size: 13px; background: #eee; + } td { //border-top: 1px solid $topic-list-td-border-color; @@ -197,6 +198,16 @@ color: inherit; } } + .sorting { + color: #009; + } + .sortable { + cursor: pointer; + &:hover { + background-color: #e6e6e6; + } + @include unselectable; + } .likes { width: 50px; } @@ -208,6 +219,21 @@ } } +.paginated-topics-list { + #topic-list { + .posts { + width: 95px; + } + .likes { + width: 95px; + } + .views { + width: 95px; + } + + } +} + #topic-list tbody tr.has-excerpt .star { vertical-align: top; margin-top: 3px; diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index bbae95c6ca..446a9b6512 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -140,7 +140,9 @@ class ListController < ApplicationController page: params[:page], topic_ids: param_to_integer_list(:topic_ids), exclude_category: (params[:exclude_category] || menu_item.try(:filter)), - category: params[:category] + category: params[:category], + sort_order: params[:sort_order], + sort_descending: params[:sort_order] } end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 003dd7565a..8c464390e8 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -7,7 +7,16 @@ require_dependency 'suggested_topics_builder' class TopicQuery # Could be rewritten to %i if Ruby 1.9 is no longer supported - VALID_OPTIONS = %w(except_topic_id exclude_category limit page per_page topic_ids visible category).map(&:to_sym) + VALID_OPTIONS = %w(except_topic_id + exclude_category + limit + page + per_page + topic_ids + visible + category + sort_order + sort_descending).map(&:to_sym) class << self # use the constants in conjuction with COALESCE to determine the order with regard to pinned @@ -30,8 +39,8 @@ class TopicQuery END DESC" end - def order_hotness - if @user + def order_hotness(user) + if user # When logged in take into accounts what pins you've closed "CASE WHEN (COALESCE(topics.pinned_at, '#{lowest_date}') > COALESCE(tu.cleared_pinned_at, '#{lowest_date}')) @@ -113,7 +122,7 @@ class TopicQuery def list_hot create_list(:hot, unordered: true) do |topics| - topics.joins(:hot_topic).order(TopicQuery.order_hotness) + topics.joins(:hot_topic).order(TopicQuery.order_hotness(@user)) end end diff --git a/test/javascripts/models/sort_order_test.js b/test/javascripts/models/sort_order_test.js new file mode 100644 index 0000000000..2cbd86c050 --- /dev/null +++ b/test/javascripts/models/sort_order_test.js @@ -0,0 +1,23 @@ +module("Discourse.SortOrder"); + +test('defaults', function() { + var sortOrder = Discourse.SortOrder.create(); + equal(sortOrder.get('order'), 'default', 'it is `default` by default'); + equal(sortOrder.get('descending'), true, 'it is descending by default'); +}); + +test('toggle', function() { + var sortOrder = Discourse.SortOrder.create(); + + sortOrder.toggle('default'); + equal(sortOrder.get('descending'), false, 'if we toggle the same name it swaps the asc/desc'); + + sortOrder.toggle('name'); + equal(sortOrder.get('order'), 'name', 'it changes the order'); + equal(sortOrder.get('descending'), true, 'when toggling names it switches back to descending'); + + sortOrder.toggle('name'); + sortOrder.toggle('name'); + equal(sortOrder.get('descending'), true, 'toggling twice goes back to descending'); + +}); \ No newline at end of file