FEATURE: Added UI for adding and removing watched and muted categories
This commit is contained in:
parent
1b259c59a5
commit
2da5d2311b
|
@ -0,0 +1,42 @@
|
|||
Discourse.CategoryGroupComponent = Ember.Component.extend({
|
||||
|
||||
didInsertElement: function(){
|
||||
var self = this;
|
||||
|
||||
this.$('input').autocomplete({
|
||||
items: this.get('categories'),
|
||||
single: false,
|
||||
allowAny: false,
|
||||
dataSource: function(term){
|
||||
return Discourse.Category.list().filter(function(category){
|
||||
var regex = new RegExp(term, "i");
|
||||
return category.get("name").match(regex) &&
|
||||
!_.contains(self.get('categories'), category);
|
||||
});
|
||||
},
|
||||
onChangeItems: function(items) {
|
||||
self.set("categories", items);
|
||||
},
|
||||
template: Discourse.CategoryGroupComponent.templateFunction(),
|
||||
transformComplete: function(category){
|
||||
return Discourse.HTML.categoryLink(category);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Discourse.CategoryGroupComponent.reopenClass({
|
||||
templateFunction: function(){
|
||||
this.compiled = this.compiled || Handlebars.compile("<div class='autocomplete'>" +
|
||||
"<ul>" +
|
||||
"{{#each options}}" +
|
||||
"<li>" +
|
||||
"{{categoryLinkRaw this}}" +
|
||||
"</li>" +
|
||||
"{{/each}}" +
|
||||
"</ul>" +
|
||||
"</div>");
|
||||
return this.compiled;
|
||||
}
|
||||
});
|
|
@ -65,6 +65,10 @@ Handlebars.registerHelper('categoryLink', function(property, options) {
|
|||
return categoryLinkHTML(Ember.Handlebars.get(this, property, options), options);
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('categoryLinkRaw', function(property, options) {
|
||||
return categoryLinkHTML(property, options);
|
||||
});
|
||||
|
||||
/**
|
||||
Produces a bound link to a category
|
||||
|
||||
|
|
|
@ -74,7 +74,6 @@ $.fn.autocomplete = function(options) {
|
|||
var isInput = this[0].tagName === "INPUT";
|
||||
var inputSelectedItems = [];
|
||||
|
||||
|
||||
var closeAutocomplete = function() {
|
||||
if (div) {
|
||||
div.hide().remove();
|
||||
|
@ -93,7 +92,7 @@ $.fn.autocomplete = function(options) {
|
|||
// dump what we have in single mode, just in case
|
||||
inputSelectedItems = [];
|
||||
}
|
||||
var d = $("<div class='item'><span>" + (transformed || item) + "<a href='#'><i class='fa fa-times'></i></a></span></div>");
|
||||
var d = $("<div class='item'><span>" + (transformed || item) + "<a class='remove' href='#'><i class='fa fa-times'></i></a></span></div>");
|
||||
var prev = me.parent().find('.item:last');
|
||||
if (prev.length === 0) {
|
||||
me.parent().prepend(d);
|
||||
|
@ -158,6 +157,11 @@ $.fn.autocomplete = function(options) {
|
|||
addInputSelectedItem(x);
|
||||
}
|
||||
});
|
||||
if(options.items) {
|
||||
_.each(options.items, function(item){
|
||||
addInputSelectedItem(item);
|
||||
});
|
||||
}
|
||||
this.val("");
|
||||
completeStart = 0;
|
||||
wrap.click(function() {
|
||||
|
@ -225,8 +229,14 @@ $.fn.autocomplete = function(options) {
|
|||
};
|
||||
|
||||
var updateAutoComplete = function(r) {
|
||||
|
||||
if (completeStart === null) return;
|
||||
|
||||
if (r && r.then && typeof(r.then) === "function") {
|
||||
r.then(updateAutoComplete);
|
||||
return;
|
||||
}
|
||||
|
||||
autocompleteOptions = r;
|
||||
if (!r || r.length === 0) {
|
||||
closeAutocomplete();
|
||||
|
@ -257,7 +267,7 @@ $.fn.autocomplete = function(options) {
|
|||
if (!prevChar || /\s/.test(prevChar)) {
|
||||
completeStart = completeEnd = caretPosition;
|
||||
var term = "";
|
||||
options.dataSource(term).then(updateAutoComplete);
|
||||
updateAutoComplete(options.dataSource(term));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -304,7 +314,7 @@ $.fn.autocomplete = function(options) {
|
|||
completeStart = c;
|
||||
caretPosition = completeEnd = initial;
|
||||
term = me[0].value.substring(c + 1, initial);
|
||||
options.dataSource(term).then(updateAutoComplete);
|
||||
updateAutoComplete(options.dataSource(term));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -395,7 +405,7 @@ $.fn.autocomplete = function(options) {
|
|||
}
|
||||
}
|
||||
|
||||
options.dataSource(term).then(updateAutoComplete);
|
||||
updateAutoComplete(options.dataSource(term));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -204,6 +204,11 @@ Discourse.Category.reopenClass({
|
|||
});
|
||||
},
|
||||
|
||||
// TODO: optimise, slow for no real reason
|
||||
findById: function(id){
|
||||
return Discourse.Category.list().findBy('id', id);
|
||||
},
|
||||
|
||||
findBySlug: function(slug, parentSlug) {
|
||||
|
||||
var categories = Discourse.Category.list(),
|
||||
|
|
|
@ -167,8 +167,7 @@ Discourse.User = Discourse.Model.extend({
|
|||
**/
|
||||
save: function() {
|
||||
var user = this;
|
||||
return Discourse.ajax("/users/" + this.get('username_lower'), {
|
||||
data: this.getProperties('auto_track_topics_after_msecs',
|
||||
var data = this.getProperties('auto_track_topics_after_msecs',
|
||||
'bio_raw',
|
||||
'website',
|
||||
'name',
|
||||
|
@ -181,7 +180,12 @@ Discourse.User = Discourse.Model.extend({
|
|||
'new_topic_duration_minutes',
|
||||
'external_links_in_new_tab',
|
||||
'watch_new_topics',
|
||||
'enable_quoting'),
|
||||
'enable_quoting');
|
||||
data.watched_category_ids = this.get('watchedCategories').map(function(c){ return c.get('id')});
|
||||
data.muted_category_ids = this.get('mutedCategories').map(function(c){ return c.get('id')});
|
||||
|
||||
return Discourse.ajax("/users/" + this.get('username_lower'), {
|
||||
data: data,
|
||||
type: 'PUT'
|
||||
}).then(function(data) {
|
||||
user.set('bio_excerpt',data.user.bio_excerpt);
|
||||
|
@ -350,8 +354,19 @@ Discourse.User = Discourse.Model.extend({
|
|||
}
|
||||
}
|
||||
return Discourse.Utilities.defaultHomepage();
|
||||
}.property("trust_level", "hasBeenSeenInTheLastMonth")
|
||||
}.property("trust_level", "hasBeenSeenInTheLastMonth"),
|
||||
|
||||
updateMutedCategories: function() {
|
||||
this.set("mutedCategories", _.map(this.muted_category_ids, function(id){
|
||||
return Discourse.Category.findById(id);
|
||||
}));
|
||||
}.observes("muted_category_ids"),
|
||||
|
||||
updateWatchedCategories: function() {
|
||||
this.set("watchedCategories", _.map(this.watched_category_ids, function(id){
|
||||
return Discourse.Category.findById(id);
|
||||
}));
|
||||
}.observes("watched_category_ids")
|
||||
});
|
||||
|
||||
Discourse.User.reopenClass(Discourse.Singleton, {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<input class='category-group' type='text'>
|
|
@ -134,6 +134,20 @@
|
|||
|
||||
</div>
|
||||
|
||||
<div class="control-group category">
|
||||
<label class="control-label">{{i18n user.categories_settings}}</label>
|
||||
<div class="controls">
|
||||
<label>{{i18n user.watched_categories}}</label>
|
||||
{{category-group categories=watchedCategories}}
|
||||
<div class="instructions">{{i18n user.watched_categories_instructions}}</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label>{{i18n user.muted_categories}}</label>
|
||||
{{category-group categories=mutedCategories}}
|
||||
<div class="instructions">{{i18n user.muted_categories_instructions}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<button {{action save}} {{bindAttr disabled="saveDisabled"}} class="btn btn-primary">{{saveButtonText}}</button>
|
||||
|
|
|
@ -387,7 +387,7 @@ div.ac-wrap {
|
|||
line-height: 22px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
a {
|
||||
a.remove {
|
||||
margin-left: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 10px;
|
||||
|
|
|
@ -3,6 +3,19 @@
|
|||
@import "common/foundation/mixins";
|
||||
|
||||
.user-preferences {
|
||||
input.category-group {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.autocomplete .badge-category {
|
||||
margin: 2px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.autocomplete .badge-category.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 530px;
|
||||
height: 100px;
|
||||
|
|
|
@ -2,6 +2,10 @@ class CategoryUser < ActiveRecord::Base
|
|||
belongs_to :category
|
||||
belongs_to :user
|
||||
|
||||
def self.lookup(user, level)
|
||||
self.where(user: user, notification_level: notification_levels[level])
|
||||
end
|
||||
|
||||
# same for now
|
||||
def self.notification_levels
|
||||
TopicUser.notification_levels
|
||||
|
@ -15,6 +19,21 @@ class CategoryUser < ActiveRecord::Base
|
|||
)
|
||||
end
|
||||
|
||||
def self.batch_set(user, level, category_ids)
|
||||
records = CategoryUser.where(user: user, notification_level: notification_levels[level])
|
||||
|
||||
old_ids = records.pluck(:category_id)
|
||||
|
||||
remove = (old_ids - category_ids)
|
||||
if remove.present?
|
||||
records.where('category_id in (?)', remove).destroy_all
|
||||
end
|
||||
|
||||
(category_ids - old_ids).each do |id|
|
||||
CategoryUser.create!(user: user, category_id: id, notification_level: notification_levels[level])
|
||||
end
|
||||
end
|
||||
|
||||
def self.auto_mute_new_topic(topic)
|
||||
apply_default_to_topic(
|
||||
topic,
|
||||
|
@ -23,6 +42,15 @@ class CategoryUser < ActiveRecord::Base
|
|||
)
|
||||
end
|
||||
|
||||
def notification_level1=(val)
|
||||
val = Symbol === val ? CategoryUser.notification_levels[val] : val
|
||||
attributes[:notification_level] = val
|
||||
end
|
||||
|
||||
def notification_level1
|
||||
attributes[:notification_level]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.apply_default_to_topic(topic, level, reason)
|
||||
|
|
|
@ -60,7 +60,9 @@ class UserSerializer < BasicUserSerializer
|
|||
:use_uploaded_avatar,
|
||||
:has_uploaded_avatar,
|
||||
:gravatar_template,
|
||||
:uploaded_avatar_template
|
||||
:uploaded_avatar_template,
|
||||
:muted_category_ids,
|
||||
:watched_category_ids
|
||||
|
||||
|
||||
def auto_track_topics_after_msecs
|
||||
|
@ -101,8 +103,16 @@ class UserSerializer < BasicUserSerializer
|
|||
def include_suspend_reason?
|
||||
object.suspended?
|
||||
end
|
||||
|
||||
def include_suspended_till?
|
||||
object.suspended?
|
||||
end
|
||||
|
||||
def muted_category_ids
|
||||
CategoryUser.lookup(object, :muted).pluck(:category_id)
|
||||
end
|
||||
|
||||
def watched_category_ids
|
||||
CategoryUser.lookup(object, :watching).pluck(:category_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,8 +11,16 @@ class UserUpdater
|
|||
user.name = attributes[:name] || user.name
|
||||
user.digest_after_days = attributes[:digest_after_days] || user.digest_after_days
|
||||
|
||||
if ids = attributes[:watched_category_ids]
|
||||
CategoryUser.batch_set(user, :watching, ids)
|
||||
end
|
||||
|
||||
if ids = attributes[:muted_category_ids]
|
||||
CategoryUser.batch_set(user, :muted, ids)
|
||||
end
|
||||
|
||||
if attributes[:auto_track_topics_after_msecs]
|
||||
user.auto_track_topics_after_msecs = attributes[:auto_track_topics_after_msecs].to_i
|
||||
user.auto_track_topics_after_msecs = attributes[:auto_track_topics_after_msecs].to_i
|
||||
end
|
||||
|
||||
if attributes[:new_topic_duration_minutes]
|
||||
|
|
|
@ -228,6 +228,10 @@ en:
|
|||
suspended_notice: "This user is suspended until {{date}}."
|
||||
suspended_reason: "Reason: "
|
||||
watch_new_topics: "Automatically watch all new topics posted on the forum"
|
||||
watched_categories: "Watched"
|
||||
watched_categories_instructions: "You will automatically watch all topics in these categories"
|
||||
muted_categories: "Muted"
|
||||
muted_categories_instructions: "You will automatically mute all topics in these categories"
|
||||
|
||||
messages:
|
||||
all: "All"
|
||||
|
@ -313,6 +317,7 @@ en:
|
|||
email_always: "Receive email notifications and email digests even if I am active on the forum"
|
||||
|
||||
other_settings: "Other"
|
||||
categories_settings: "Categories"
|
||||
|
||||
new_topic_duration:
|
||||
label: "Consider topics new when"
|
||||
|
|
|
@ -4,6 +4,25 @@ require 'spec_helper'
|
|||
require_dependency 'post_creator'
|
||||
|
||||
describe CategoryUser do
|
||||
|
||||
it 'allows batch set' do
|
||||
user = Fabricate(:user)
|
||||
category1 = Fabricate(:category)
|
||||
category2 = Fabricate(:category)
|
||||
|
||||
watching = CategoryUser.where(user_id: user.id, notification_level: CategoryUser.notification_levels[:watching])
|
||||
|
||||
CategoryUser.batch_set(user, :watching, [category1.id, category2.id])
|
||||
watching.pluck(:category_id).sort.should == [category1.id, category2.id]
|
||||
|
||||
CategoryUser.batch_set(user, :watching, [])
|
||||
watching.count.should == 0
|
||||
|
||||
CategoryUser.batch_set(user, :watching, [category2.id])
|
||||
watching.count.should == 1
|
||||
end
|
||||
|
||||
|
||||
context 'integration' do
|
||||
before do
|
||||
ActiveRecord::Base.observers.enable :all
|
||||
|
|
Loading…
Reference in New Issue