Controls for sorting topic columns
This commit is contained in:
parent
7e7d951152
commit
df568df9dc
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -11,6 +11,10 @@ Discourse.UserTopicsListController = Discourse.ObjectController.extend({
|
|||
actions: {
|
||||
loadMore: function() {
|
||||
this.get('model').loadMore();
|
||||
},
|
||||
|
||||
changeSort: function() {
|
||||
console.log('sort changed!');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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') });
|
||||
}
|
||||
});
|
|
@ -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}}
|
|
@ -0,0 +1,2 @@
|
|||
{{yield}}
|
||||
<i {{bindAttr class="iconSortClass"}}></i>
|
|
@ -1 +1 @@
|
|||
{{discourse-basic-topic-list topics=model.topics hideCategories="true"}}
|
||||
{{discourse-basic-topic-list topicList=model}}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
||||
});
|
Loading…
Reference in New Issue