mirror of
https://github.com/discourse/discourse.git
synced 2025-09-06 10:50:21 +08:00
FEATURE: Added UI for adding and removing watched and muted categories
This commit is contained in:
parent
1b259c59a5
commit
2da5d2311b
14 changed files with 186 additions and 12 deletions
|
@ -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);
|
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
|
Produces a bound link to a category
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,6 @@ $.fn.autocomplete = function(options) {
|
||||||
var isInput = this[0].tagName === "INPUT";
|
var isInput = this[0].tagName === "INPUT";
|
||||||
var inputSelectedItems = [];
|
var inputSelectedItems = [];
|
||||||
|
|
||||||
|
|
||||||
var closeAutocomplete = function() {
|
var closeAutocomplete = function() {
|
||||||
if (div) {
|
if (div) {
|
||||||
div.hide().remove();
|
div.hide().remove();
|
||||||
|
@ -93,7 +92,7 @@ $.fn.autocomplete = function(options) {
|
||||||
// dump what we have in single mode, just in case
|
// dump what we have in single mode, just in case
|
||||||
inputSelectedItems = [];
|
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');
|
var prev = me.parent().find('.item:last');
|
||||||
if (prev.length === 0) {
|
if (prev.length === 0) {
|
||||||
me.parent().prepend(d);
|
me.parent().prepend(d);
|
||||||
|
@ -158,6 +157,11 @@ $.fn.autocomplete = function(options) {
|
||||||
addInputSelectedItem(x);
|
addInputSelectedItem(x);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if(options.items) {
|
||||||
|
_.each(options.items, function(item){
|
||||||
|
addInputSelectedItem(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
this.val("");
|
this.val("");
|
||||||
completeStart = 0;
|
completeStart = 0;
|
||||||
wrap.click(function() {
|
wrap.click(function() {
|
||||||
|
@ -225,8 +229,14 @@ $.fn.autocomplete = function(options) {
|
||||||
};
|
};
|
||||||
|
|
||||||
var updateAutoComplete = function(r) {
|
var updateAutoComplete = function(r) {
|
||||||
|
|
||||||
if (completeStart === null) return;
|
if (completeStart === null) return;
|
||||||
|
|
||||||
|
if (r && r.then && typeof(r.then) === "function") {
|
||||||
|
r.then(updateAutoComplete);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
autocompleteOptions = r;
|
autocompleteOptions = r;
|
||||||
if (!r || r.length === 0) {
|
if (!r || r.length === 0) {
|
||||||
closeAutocomplete();
|
closeAutocomplete();
|
||||||
|
@ -257,7 +267,7 @@ $.fn.autocomplete = function(options) {
|
||||||
if (!prevChar || /\s/.test(prevChar)) {
|
if (!prevChar || /\s/.test(prevChar)) {
|
||||||
completeStart = completeEnd = caretPosition;
|
completeStart = completeEnd = caretPosition;
|
||||||
var term = "";
|
var term = "";
|
||||||
options.dataSource(term).then(updateAutoComplete);
|
updateAutoComplete(options.dataSource(term));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -304,7 +314,7 @@ $.fn.autocomplete = function(options) {
|
||||||
completeStart = c;
|
completeStart = c;
|
||||||
caretPosition = completeEnd = initial;
|
caretPosition = completeEnd = initial;
|
||||||
term = me[0].value.substring(c + 1, initial);
|
term = me[0].value.substring(c + 1, initial);
|
||||||
options.dataSource(term).then(updateAutoComplete);
|
updateAutoComplete(options.dataSource(term));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -395,7 +405,7 @@ $.fn.autocomplete = function(options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
options.dataSource(term).then(updateAutoComplete);
|
updateAutoComplete(options.dataSource(term));
|
||||||
return true;
|
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) {
|
findBySlug: function(slug, parentSlug) {
|
||||||
|
|
||||||
var categories = Discourse.Category.list(),
|
var categories = Discourse.Category.list(),
|
||||||
|
|
|
@ -167,8 +167,7 @@ Discourse.User = Discourse.Model.extend({
|
||||||
**/
|
**/
|
||||||
save: function() {
|
save: function() {
|
||||||
var user = this;
|
var user = this;
|
||||||
return Discourse.ajax("/users/" + this.get('username_lower'), {
|
var data = this.getProperties('auto_track_topics_after_msecs',
|
||||||
data: this.getProperties('auto_track_topics_after_msecs',
|
|
||||||
'bio_raw',
|
'bio_raw',
|
||||||
'website',
|
'website',
|
||||||
'name',
|
'name',
|
||||||
|
@ -181,7 +180,12 @@ Discourse.User = Discourse.Model.extend({
|
||||||
'new_topic_duration_minutes',
|
'new_topic_duration_minutes',
|
||||||
'external_links_in_new_tab',
|
'external_links_in_new_tab',
|
||||||
'watch_new_topics',
|
'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'
|
type: 'PUT'
|
||||||
}).then(function(data) {
|
}).then(function(data) {
|
||||||
user.set('bio_excerpt',data.user.bio_excerpt);
|
user.set('bio_excerpt',data.user.bio_excerpt);
|
||||||
|
@ -350,8 +354,19 @@ Discourse.User = Discourse.Model.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Discourse.Utilities.defaultHomepage();
|
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, {
|
Discourse.User.reopenClass(Discourse.Singleton, {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<input class='category-group' type='text'>
|
|
@ -134,6 +134,20 @@
|
||||||
|
|
||||||
</div>
|
</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="control-group">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button {{action save}} {{bindAttr disabled="saveDisabled"}} class="btn btn-primary">{{saveButtonText}}</button>
|
<button {{action save}} {{bindAttr disabled="saveDisabled"}} class="btn btn-primary">{{saveButtonText}}</button>
|
||||||
|
|
|
@ -387,7 +387,7 @@ div.ac-wrap {
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
a {
|
a.remove {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 10px;
|
line-height: 10px;
|
||||||
|
|
|
@ -3,6 +3,19 @@
|
||||||
@import "common/foundation/mixins";
|
@import "common/foundation/mixins";
|
||||||
|
|
||||||
.user-preferences {
|
.user-preferences {
|
||||||
|
input.category-group {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete .badge-category {
|
||||||
|
margin: 2px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete .badge-category.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 530px;
|
width: 530px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
|
|
|
@ -2,6 +2,10 @@ class CategoryUser < ActiveRecord::Base
|
||||||
belongs_to :category
|
belongs_to :category
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
def self.lookup(user, level)
|
||||||
|
self.where(user: user, notification_level: notification_levels[level])
|
||||||
|
end
|
||||||
|
|
||||||
# same for now
|
# same for now
|
||||||
def self.notification_levels
|
def self.notification_levels
|
||||||
TopicUser.notification_levels
|
TopicUser.notification_levels
|
||||||
|
@ -15,6 +19,21 @@ class CategoryUser < ActiveRecord::Base
|
||||||
)
|
)
|
||||||
end
|
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)
|
def self.auto_mute_new_topic(topic)
|
||||||
apply_default_to_topic(
|
apply_default_to_topic(
|
||||||
topic,
|
topic,
|
||||||
|
@ -23,6 +42,15 @@ class CategoryUser < ActiveRecord::Base
|
||||||
)
|
)
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def self.apply_default_to_topic(topic, level, reason)
|
def self.apply_default_to_topic(topic, level, reason)
|
||||||
|
|
|
@ -60,7 +60,9 @@ class UserSerializer < BasicUserSerializer
|
||||||
:use_uploaded_avatar,
|
:use_uploaded_avatar,
|
||||||
:has_uploaded_avatar,
|
:has_uploaded_avatar,
|
||||||
:gravatar_template,
|
:gravatar_template,
|
||||||
:uploaded_avatar_template
|
:uploaded_avatar_template,
|
||||||
|
:muted_category_ids,
|
||||||
|
:watched_category_ids
|
||||||
|
|
||||||
|
|
||||||
def auto_track_topics_after_msecs
|
def auto_track_topics_after_msecs
|
||||||
|
@ -101,8 +103,16 @@ class UserSerializer < BasicUserSerializer
|
||||||
def include_suspend_reason?
|
def include_suspend_reason?
|
||||||
object.suspended?
|
object.suspended?
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_suspended_till?
|
def include_suspended_till?
|
||||||
object.suspended?
|
object.suspended?
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -11,8 +11,16 @@ class UserUpdater
|
||||||
user.name = attributes[:name] || user.name
|
user.name = attributes[:name] || user.name
|
||||||
user.digest_after_days = attributes[:digest_after_days] || user.digest_after_days
|
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]
|
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
|
end
|
||||||
|
|
||||||
if attributes[:new_topic_duration_minutes]
|
if attributes[:new_topic_duration_minutes]
|
||||||
|
|
|
@ -228,6 +228,10 @@ en:
|
||||||
suspended_notice: "This user is suspended until {{date}}."
|
suspended_notice: "This user is suspended until {{date}}."
|
||||||
suspended_reason: "Reason: "
|
suspended_reason: "Reason: "
|
||||||
watch_new_topics: "Automatically watch all new topics posted on the forum"
|
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:
|
messages:
|
||||||
all: "All"
|
all: "All"
|
||||||
|
@ -313,6 +317,7 @@ en:
|
||||||
email_always: "Receive email notifications and email digests even if I am active on the forum"
|
email_always: "Receive email notifications and email digests even if I am active on the forum"
|
||||||
|
|
||||||
other_settings: "Other"
|
other_settings: "Other"
|
||||||
|
categories_settings: "Categories"
|
||||||
|
|
||||||
new_topic_duration:
|
new_topic_duration:
|
||||||
label: "Consider topics new when"
|
label: "Consider topics new when"
|
||||||
|
|
|
@ -4,6 +4,25 @@ require 'spec_helper'
|
||||||
require_dependency 'post_creator'
|
require_dependency 'post_creator'
|
||||||
|
|
||||||
describe CategoryUser do
|
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
|
context 'integration' do
|
||||||
before do
|
before do
|
||||||
ActiveRecord::Base.observers.enable :all
|
ActiveRecord::Base.observers.enable :all
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue