diff --git a/app/assets/javascripts/discourse/controllers/preferences/users.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/users.js.es6
index fc675cf052d..ba3e866901b 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/users.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/users.js.es6
@@ -1,4 +1,6 @@
+import { makeArray } from "discourse-common/lib/helpers";
import { alias, gte, or } from "@ember/object/computed";
+import { action, computed } from "@ember/object";
import Controller from "@ember/controller";
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
@@ -8,19 +10,36 @@ export default Controller.extend(PreferencesTabController, {
userIsMemberOrAbove: gte("model.trust_level", 2),
ignoredEnabled: or("userIsMemberOrAbove", "model.staff"),
+ mutedUsernames: computed("model.muted_usernames", {
+ get() {
+ let usernames = this.model.muted_usernames;
+
+ if (typeof usernames === "string") {
+ usernames = usernames.split(",").filter(Boolean);
+ }
+
+ return makeArray(usernames).uniq();
+ }
+ }),
+
init() {
this._super(...arguments);
this.saveAttrNames = ["muted_usernames", "ignored_usernames"];
},
- actions: {
- save() {
- this.set("saved", false);
- return this.model
- .save(this.saveAttrNames)
- .then(() => this.set("saved", true))
- .catch(popupAjaxError);
- }
+ @action
+ onChangeMutedUsernames(usernames) {
+ this.model.set("muted_usernames", usernames.uniq().join(","));
+ },
+
+ @action
+ save() {
+ this.set("saved", false);
+
+ return this.model
+ .save(this.saveAttrNames)
+ .then(() => this.set("saved", true))
+ .catch(popupAjaxError);
}
});
diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6
index 2a5890aa3f4..fcdc4214278 100644
--- a/app/assets/javascripts/discourse/lib/user-search.js.es6
+++ b/app/assets/javascripts/discourse/lib/user-search.js.es6
@@ -27,7 +27,7 @@ function performSearch(
return;
}
- const eagerComplete = term === "" && !!(topicId || categoryId);
+ const eagerComplete = eagerCompleteSearch(term, topicId || categoryId);
if (term === "" && !eagerComplete) {
// The server returns no results in this case, so no point checking
@@ -138,7 +138,7 @@ function organizeResults(r, options) {
// we also ignore if we notice a double space or a string that is only a space
const ignoreRegex = /([\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*,\/:;<=>?\[\]^`{|}~])|\s\s|^\s$|^[^+]*\+[^@]*$/;
-function skipSearch(term, allowEmails) {
+export function skipSearch(term, allowEmails) {
if (term.indexOf("@") > -1 && !allowEmails) {
return true;
}
@@ -146,6 +146,10 @@ function skipSearch(term, allowEmails) {
return !!term.match(ignoreRegex);
}
+export function eagerCompleteSearch(term, scopedId) {
+ return term === "" && !!scopedId;
+}
+
export default function userSearch(options) {
if (options.term && options.term.length > 0 && options.term[0] === "@") {
options.term = options.term.substring(1);
diff --git a/app/assets/javascripts/discourse/templates/preferences/users.hbs b/app/assets/javascripts/discourse/templates/preferences/users.hbs
index c43db04fddb..50804371497 100644
--- a/app/assets/javascripts/discourse/templates/preferences/users.hbs
+++ b/app/assets/javascripts/discourse/templates/preferences/users.hbs
@@ -14,7 +14,13 @@
{{d-icon "d-muted" class="icon"}}
{{i18n 'user.muted_users'}}
- {{user-selector excludeCurrentUser=true usernames=model.muted_usernames class="user-selector"}}
+ {{user-chooser
+ value=mutedUsernames
+ onChange=(action "onChangeMutedUsernames")
+ options=(hash
+ excludeCurrentUser=true
+ )
+ }}
{{i18n 'user.muted_users_instructions'}}
diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6
index e7471b0e508..ba1197f02da 100644
--- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6
+++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6
@@ -18,6 +18,7 @@ export default ComboBox.extend(TagsMixin, {
maxTagSearchResults: setting("max_tag_search_results"),
maxTagsPerTopic: setting("max_tags_per_topic"),
highlightedTag: null,
+ singleSelect: false,
collections: computed(
"mainCollection.[]",
diff --git a/app/assets/javascripts/select-kit/components/multi-select.js.es6 b/app/assets/javascripts/select-kit/components/multi-select.js.es6
index a15a9e79ea3..dd24a6b9175 100644
--- a/app/assets/javascripts/select-kit/components/multi-select.js.es6
+++ b/app/assets/javascripts/select-kit/components/multi-select.js.es6
@@ -1,7 +1,7 @@
import deprecated from "discourse-common/lib/deprecated";
import SelectKitComponent from "select-kit/components/select-kit";
import { computed } from "@ember/object";
-const { makeArray } = Ember;
+import { makeArray } from "discourse-common/lib/helpers";
export default SelectKitComponent.extend({
pluginApiIdentifiers: ["multi-select"],
diff --git a/app/assets/javascripts/select-kit/components/select-kit.js.es6 b/app/assets/javascripts/select-kit/components/select-kit.js.es6
index c1169723157..4f474009ec6 100644
--- a/app/assets/javascripts/select-kit/components/select-kit.js.es6
+++ b/app/assets/javascripts/select-kit/components/select-kit.js.es6
@@ -58,6 +58,8 @@ export default Component.extend(
options: null,
valueProperty: "id",
nameProperty: "name",
+ singleSelect: false,
+ multiSelect: false,
init() {
this._super(...arguments);
@@ -401,6 +403,28 @@ export default Component.extend(
items = [];
}
+ value = makeArray(value);
+ items = makeArray(items);
+
+ if (this.multiSelect) {
+ items = items.filter(
+ i =>
+ i !== this.newItem &&
+ i !== this.noneItem &&
+ this.getValue(i) !== null
+ );
+
+ if (this.selectKit.options.maximum === 1) {
+ value = value.slice(0, 1);
+ items = items.slice(0, 1);
+ }
+ }
+
+ if (this.singleSelect) {
+ value = value.firstObject || null;
+ items = items.firstObject || null;
+ }
+
this._boundaryActionHandler("onChange", value, items);
resolve(items);
}).finally(() => {
diff --git a/app/assets/javascripts/select-kit/components/user-chooser.js.es6 b/app/assets/javascripts/select-kit/components/user-chooser.js.es6
new file mode 100644
index 00000000000..b84e97ca9a4
--- /dev/null
+++ b/app/assets/javascripts/select-kit/components/user-chooser.js.es6
@@ -0,0 +1,85 @@
+import MultiSelectComponent from "select-kit/components/multi-select";
+import { computed } from "@ember/object";
+import {
+ default as userSearch,
+ skipSearch,
+ eagerCompleteSearch
+} from "discourse/lib/user-search";
+import { makeArray } from "discourse-common/lib/helpers";
+
+export default MultiSelectComponent.extend({
+ pluginApiIdentifiers: ["user-chooser"],
+ classNames: ["user-chooser"],
+ valueProperty: "username",
+
+ modifyComponentForRow() {
+ return "user-chooser/user-row";
+ },
+
+ selectKitOptions: {
+ topicId: undefined,
+ categoryId: undefined,
+ includeGroups: false,
+ allowedUsers: false,
+ includeMentionableGroups: false,
+ includeMessageableGroups: false,
+ allowEmails: false,
+ groupMembersOf: undefined
+ },
+
+ content: computed("value.[]", function() {
+ return Ember.makeArray(this.value).map(x => this.defaultItem(x, x));
+ }),
+
+ excludedUsers: computed(
+ "value",
+ "currentUser",
+ "selectKit.options.{excludeCurrentUser,excludedUsernames}",
+ {
+ get() {
+ const options = this.selectKit.options;
+ let usernames = makeArray(this.value);
+
+ if (this.currentUser && options.excludeCurrentUser) {
+ usernames.concat([this.currentUser.username]);
+ }
+
+ return usernames.concat(options.excludedUsernames || []);
+ }
+ }
+ ),
+
+ search(filter = "") {
+ filter = filter || "";
+ const options = this.selectKit.options;
+
+ // prevents doing ajax request for nothing
+ const skippedSearch = skipSearch(filter, options.allowEmails);
+ const eagerComplete = eagerCompleteSearch(
+ filter,
+ options.topicId || options.categoryId
+ );
+ if (skippedSearch || (filter === "" && !eagerComplete)) {
+ return;
+ }
+
+ return userSearch({
+ term: filter,
+ topicId: options.topicId,
+ categoryId: options.categoryId,
+ exclude: this.excludedUsers,
+ includeGroups: options.includeGroups,
+ allowedUsers: options.allowedUsers,
+ includeMentionableGroups: options.includeMentionableGroups,
+ includeMessageableGroups: options.includeMessageableGroups,
+ groupMembersOf: options.groupMembersOf,
+ allowEmails: options.allowEmails
+ }).then(result => {
+ if (typeof result === "string") {
+ // do nothing promise probably got cancelled
+ } else {
+ return result;
+ }
+ });
+ }
+});
diff --git a/app/assets/javascripts/select-kit/components/user-chooser/user-row.js.es6 b/app/assets/javascripts/select-kit/components/user-chooser/user-row.js.es6
new file mode 100644
index 00000000000..b85ac70d61b
--- /dev/null
+++ b/app/assets/javascripts/select-kit/components/user-chooser/user-row.js.es6
@@ -0,0 +1,6 @@
+import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
+
+export default SelectKitRowComponent.extend({
+ layoutName: "select-kit/templates/components/user-chooser/user-row",
+ classNames: ["user-row"]
+});
diff --git a/app/assets/javascripts/select-kit/templates/components/user-chooser/user-row.hbs b/app/assets/javascripts/select-kit/templates/components/user-chooser/user-row.hbs
new file mode 100644
index 00000000000..2fc47dda495
--- /dev/null
+++ b/app/assets/javascripts/select-kit/templates/components/user-chooser/user-row.hbs
@@ -0,0 +1,3 @@
+{{avatar item imageSize="tiny"}}
+{{format-username item.username}}
+{{item.name}}
diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss
index d7ea24129f3..5aebe7c1e77 100644
--- a/app/assets/stylesheets/common.scss
+++ b/app/assets/stylesheets/common.scss
@@ -5,30 +5,7 @@
@import "common/foundation/mixins";
@import "common/foundation/variables";
@import "common/foundation/spacing";
-@import "common/select-kit/categories-admin-dropdown";
-@import "common/select-kit/category-chooser";
-@import "common/select-kit/category-drop";
-@import "common/select-kit/category-row";
-@import "common/select-kit/category-selector";
-@import "common/select-kit/combo-box";
-@import "common/select-kit/composer-actions";
-@import "common/select-kit/dropdown-select-box";
-@import "common/select-kit/future-date-input-selector";
-@import "common/select-kit/list-setting";
-@import "common/select-kit/mini-tag-chooser";
-@import "common/select-kit/multi-select";
-@import "common/select-kit/notifications-button";
-@import "common/select-kit/period-chooser";
-@import "common/select-kit/pinned-button";
-@import "common/select-kit/select-kit";
-@import "common/select-kit/single-select";
-@import "common/select-kit/tag-chooser";
-@import "common/select-kit/tag-drop";
-@import "common/select-kit/icon-picker";
-@import "common/select-kit/toolbar-popup-menu-options";
-@import "common/select-kit/topic-notifications-button";
-@import "common/select-kit/user-notifications-dropdown";
-@import "common/select-kit/color-palettes";
+@import "common/select-kit/*";
@import "common/components/*";
@import "common/input_tip";
@import "common/topic-entrance";
diff --git a/app/assets/stylesheets/common/select-kit/user-row.scss b/app/assets/stylesheets/common/select-kit/user-row.scss
new file mode 100644
index 00000000000..fbc4f848b4c
--- /dev/null
+++ b/app/assets/stylesheets/common/select-kit/user-row.scss
@@ -0,0 +1,22 @@
+.user-chooser {
+ .select-kit-row.user-row {
+ .avatar {
+ margin-left: 0;
+ margin-right: 0.5em;
+ }
+
+ .username {
+ color: $primary;
+ white-space: nowrap;
+ }
+
+ .name {
+ color: $primary-high;
+ font-size: $font-down-1;
+ margin-left: 0.5em;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index a7c73db6c51..6cfaa87a813 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -239,7 +239,8 @@
.category-selector,
.tag-chooser,
textarea,
- input.user-selector {
+ input.user-selector,
+ .user-chooser {
width: 100%;
}
diff --git a/test/javascripts/components/select-kit/user-chooser-test.js.es6 b/test/javascripts/components/select-kit/user-chooser-test.js.es6
new file mode 100644
index 00000000000..068d8c6a1f0
--- /dev/null
+++ b/test/javascripts/components/select-kit/user-chooser-test.js.es6
@@ -0,0 +1,58 @@
+import componentTest from "helpers/component-test";
+import { testSelectKitModule } from "./select-kit-test-helper";
+
+testSelectKitModule("user-chooser");
+
+function template() {
+ return `{{user-chooser value=value}}`;
+}
+
+componentTest("displays usernames", {
+ template: template(),
+
+ beforeEach() {
+ this.set("value", ["bob", "martin"]);
+ },
+
+ async test(assert) {
+ assert.equal(this.subject.header().name(), "bob,martin");
+ }
+});
+
+componentTest("can remove a username", {
+ template: template(),
+
+ beforeEach() {
+ this.set("value", ["bob", "martin"]);
+ },
+
+ async test(assert) {
+ await this.subject.deselectItem("bob");
+ assert.equal(this.subject.header().name(), "martin");
+ }
+});
+
+componentTest("can add a username", {
+ template: template(),
+
+ beforeEach() {
+ this.set("value", ["bob", "martin"]);
+
+ const response = object => {
+ return [200, { "Content-Type": "application/json" }, object];
+ };
+
+ // prettier-ignore
+ server.get("/u/search/users", () => { //eslint-disable-line
+ return response({users:[{username: "maja", name: "Maja"}]});
+ });
+ },
+
+ async test(assert) {
+ await this.subject.expand();
+ await this.subject.fillInFilter("maja");
+ await this.subject.keyboard("enter");
+
+ assert.equal(this.subject.header().name(), "bob,martin,maja");
+ }
+});
diff --git a/test/javascripts/helpers/select-kit-helper.js.es6 b/test/javascripts/helpers/select-kit-helper.js.es6
index e78369de155..a6ada50b9b4 100644
--- a/test/javascripts/helpers/select-kit-helper.js.es6
+++ b/test/javascripts/helpers/select-kit-helper.js.es6
@@ -263,6 +263,14 @@ export default function selectKit(selector) {
return rowHelper(find(selector).find(".select-kit-row.is-highlighted"));
},
+ async deselectItem(value) {
+ await click(
+ find(selector)
+ .find(".select-kit-header")
+ .find(`[data-value=${value}]`)
+ );
+ },
+
exists() {
return exists(selector);
}