Controls for sorting topic columns

This commit is contained in:
Robin Ward 2013-11-11 19:35:57 -05:00
parent 7e7d951152
commit df568df9dc
14 changed files with 287 additions and 87 deletions

View File

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

View File

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

View File

@ -11,6 +11,10 @@ Discourse.UserTopicsListController = Discourse.ObjectController.extend({
actions: {
loadMore: function() {
this.get('model').loadMore();
},
changeSort: function() {
console.log('sort changed!');
}
}

View File

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

View File

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

View File

@ -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,

View File

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

View File

@ -1,66 +1,76 @@
{{#if topics}}
<table id="topic-list">
<tr>
<th>
{{i18n topic.title}}
</th>
{{#unless hideCategories}}
<th>{{i18n category_title}}</th>
{{/unless}}
<th class='num posts'>{{i18n posts}}</th>
<th class='num likes'>{{i18n likes}}</th>
<th class='num views'>{{i18n views}}</th>
<th class='num activity' colspan='2'>{{i18n activity}}</th>
</tr>
{{#if loaded}}
{{#if topics}}
<table id="topic-list">
<tr>
{{#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}}
</tr>
{{#groupedEach topic in topics}}
<tr {{bindAttr class="archived"}}>
<td class='main-link'>
{{topicStatus topic=topic}}
<a class='title' href="{{unbound topic.lastUnreadUrl}}">{{{unbound topic.fancy_title}}}</a>
{{#if unread}}
<a href="{{unbound topic.lastUnreadUrl}}" class='badge unread badge-notification' title='{{i18n topic.unread_posts count="unread"}}'>{{unbound topic.unread}}</a>
{{/if}}
{{#if topic.new_posts}}
<a href="{{unbound topic.lastUnreadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new_posts count="new_posts"}}'>{{unbound topic.new_posts}}</a>
{{/if}}
{{#if topic.unseen}}
<a href="{{unbound topic.lastUnreadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='icon icon-asterisk'></i></a>
{{/if}}
</td>
{{#unless view.hideCategories}}
<td class='category'>
{{#groupedEach topic in topics}}
<tr {{bindAttr class="archived"}}>
<td class='main-link'>
{{topicStatus topic=topic}}
<a class='title' href="{{unbound topic.lastUnreadUrl}}">{{{unbound topic.fancy_title}}}</a>
{{#if unread}}
<a href="{{unbound topic.lastUnreadUrl}}" class='badge unread badge-notification' title='{{i18n topic.unread_posts count="unread"}}'>{{unbound topic.unread}}</a>
{{/if}}
{{#if topic.new_posts}}
<a href="{{unbound topic.lastUnreadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new_posts count="new_posts"}}'>{{unbound topic.new_posts}}</a>
{{/if}}
{{#if topic.unseen}}
<a href="{{unbound topic.lastUnreadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='icon icon-asterisk'></i></a>
{{/if}}
</td>
<td class="category">
{{categoryLink topic.category}}
</td>
{{/unless}}
<td class='num posts'><a href="{{unbound topic.lastUnreadUrl}}" class='badge-posts'>{{number topic.posts_count numberKey="posts_long"}}</a></td>
<td class='num posts'><a href="{{unbound topic.lastUnreadUrl}}" class='badge-posts'>{{number topic.posts_count numberKey="posts_long"}}</a></td>
<td class='num likes'>
{{#if topic.like_count}}
<a href='{{unbound topic.url}}{{#if topic.has_best_of}}?filter=best_of{{/if}}'>{{unbound topic.like_count}} <i class='icon-heart'></i></a>
<td class='num likes'>
{{#if topic.like_count}}
<a href='{{unbound topic.url}}{{#if topic.has_best_of}}?filter=best_of{{/if}}'>{{unbound topic.like_count}} <i class='icon-heart'></i></a>
{{/if}}
</td>
<td {{bindAttr class=":num :views topic.viewsHeat"}}>{{number topic.views numberKey="views_long"}}</td>
{{#if topic.bumped}}
<td class='num activity'>
<a href="{{unbound topic.url}}" {{{bindAttr class=":age topic.ageCold"}}} title='{{i18n first_post}}: {{{unboundDate topic.created_at}}}' >{{unboundAge topic.created_at}}</a>
</td>
<td class='num activity last'>
<a href="{{unbound topic.lastPostUrl}}" class='age' title='{{i18n last_post}}: {{{unboundDate topic.bumped_at}}}'>{{unboundAge topic.bumped_at}}</a>
</td>
{{else}}
<td class='num activity'>
<a href="{{unbound topic.url}}" class='age' title='{{i18n first_post}}: {{{unboundDate topic.created_at}}}'>{{unboundAge topic.created_at}}</a>
</td>
<td class="activity"></td>
{{/if}}
</td>
</tr>
{{/groupedEach}}
<td {{bindAttr class=":num :views topic.viewsHeat"}}>{{number topic.views numberKey="views_long"}}</td>
{{#if topic.bumped}}
<td class='num activity'>
<a href="{{unbound topic.url}}" {{{bindAttr class=":age topic.ageCold"}}} title='{{i18n first_post}}: {{{unboundDate topic.created_at}}}' >{{unboundAge topic.created_at}}</a>
</td>
<td class='num activity last'>
<a href="{{unbound topic.lastPostUrl}}" class='age' title='{{i18n last_post}}: {{{unboundDate topic.bumped_at}}}'>{{unboundAge topic.bumped_at}}</a>
</td>
{{else}}
<td class='num activity'>
<a href="{{unbound topic.url}}" class='age' title='{{i18n first_post}}: {{{unboundDate topic.created_at}}}'>{{unboundAge topic.created_at}}</a>
</td>
<td class="activity"></td>
{{/if}}
</tr>
{{/groupedEach}}
</table>
</table>
{{else}}
<div class='alert alert-info'>
{{i18n choose_topic.none_found}}
</div>
{{/if}}
{{else}}
<div class='alert alert-info'>
{{i18n choose_topic.none_found}}
</div>
{{/if}}
<div class='spinner'>{{i18n loading}}</div>
{{/if}}

View File

@ -0,0 +1,2 @@
{{yield}}
<i {{bindAttr class="iconSortClass"}}></i>

View File

@ -1 +1 @@
{{discourse-basic-topic-list topics=model.topics hideCategories="true"}}
{{discourse-basic-topic-list topicList=model}}

View File

@ -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;

View File

@ -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

View File

@ -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

View File

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