DEV: {{user-selector}} replacement (#11726)
This PR is the first step towards replacing our `{{user-selector}}` and eventually deprecating and removing it from our codebase. Some of `{{user-selector}}` problems are:
1. It's called `{{user-selector}}`, but in reality in can also select groups and emails.
2. It's an Ember component, yet it doesn't have a handlebars template and uses jQuery to render itself and modify the DOM. An example of this problem is when you want to clear the selected users programmatically, see [this](6c155dba77/app/assets/javascripts/discourse/app/components/user-selector.js (L179-L185)
).
3. We now have select kit which does very similar things but a lot better.
This PR introduces `{{email-group-user-chooser}}` which is meant to replace `{{user-selector}}`. It extends select kit and has the same features that `{{user-selector}}` has. `{{user-selector}}` is still used in a few places in core, but they'll all be replaced with the new component in a separate commit.
Once `{{user-selector}}` is not used anywhere in core, it'll be deprecated and then removed after the 2.7 release.
This commit is contained in:
parent
7e4dad3c56
commit
98201ecc24
|
@ -3,6 +3,7 @@ import I18n from "I18n";
|
|||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { isBlank } from "@ember/utils";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { get } from "@ember/object";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
export default Controller.extend({
|
||||
|
@ -30,6 +31,10 @@ export default Controller.extend({
|
|||
},
|
||||
|
||||
actions: {
|
||||
updateUsername(selected) {
|
||||
this.set("model.username", get(selected, "firstObject"));
|
||||
},
|
||||
|
||||
changeUserMode(value) {
|
||||
if (value === "all") {
|
||||
this.model.set("username", null);
|
||||
|
|
|
@ -2,6 +2,7 @@ import { empty, notEmpty, or } from "@ember/object/computed";
|
|||
import Controller from "@ember/controller";
|
||||
import EmailPreview from "admin/models/email-preview";
|
||||
import bootbox from "bootbox";
|
||||
import { get } from "@ember/object";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
export default Controller.extend({
|
||||
|
@ -14,6 +15,10 @@ export default Controller.extend({
|
|||
htmlEmpty: empty("model.html_content"),
|
||||
|
||||
actions: {
|
||||
updateUsername(selected) {
|
||||
this.set("username", get(selected, "firstObject"));
|
||||
},
|
||||
|
||||
refresh() {
|
||||
const model = this.model;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import { action } from "@ember/object";
|
||||
import { action, get } from "@ember/object";
|
||||
import { alias } from "@ember/object/computed";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
|
@ -27,4 +27,9 @@ export default Controller.extend(ModalFunctionality, {
|
|||
close() {
|
||||
this.send("closeModal");
|
||||
},
|
||||
|
||||
@action
|
||||
updateUsername(selected) {
|
||||
this.set("targetUsername", get(selected, "firstObject"));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -25,9 +25,13 @@
|
|||
|
||||
{{#if showUserSelector}}
|
||||
{{#admin-form-row label="admin.api.user"}}
|
||||
{{user-selector single="true"
|
||||
usernames=model.username
|
||||
placeholderKey="admin.api.user_placeholder"
|
||||
{{email-group-user-chooser
|
||||
value=model.username
|
||||
onChange=(action "updateUsername")
|
||||
options=(hash
|
||||
maximum=1
|
||||
filterPlaceholder="admin.api.user_placeholder"
|
||||
)
|
||||
}}
|
||||
{{/admin-form-row}}
|
||||
{{/if}}
|
||||
|
|
|
@ -5,7 +5,13 @@
|
|||
<label for="last-seen">{{i18n "admin.email.last_seen_user"}}</label>
|
||||
{{date-picker-past value=lastSeen id="last-seen"}}
|
||||
<label>{{i18n "admin.email.user"}}:</label>
|
||||
{{user-selector single="true" usernames=username canReceiveUpdates=true}}
|
||||
{{email-group-user-chooser
|
||||
value=username
|
||||
onChange=(action "updateUsername")
|
||||
options=(hash
|
||||
maximum=1
|
||||
)
|
||||
}}
|
||||
{{d-button
|
||||
class="btn-primary digest-refresh-button"
|
||||
action=(action "refresh")
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
<div>
|
||||
{{#d-modal-body rawTitle=(i18n "admin.user.merge.prompt.title" username=username)}}
|
||||
<p>{{html-safe (i18n "admin.user.merge.prompt.description" username=username)}}</p>
|
||||
{{user-selector single=true
|
||||
placeholderKey="admin.user.merge.prompt.target_username_placeholder"
|
||||
usernames=targetUsername
|
||||
autocomplete="discourse"}}
|
||||
{{email-group-user-chooser
|
||||
value=targetUsername
|
||||
autocomplete="discourse"
|
||||
onChange=(action "updateUsername")
|
||||
options=(hash
|
||||
maximum=1
|
||||
filterPlaceholder="admin.user.merge.prompt.target_username_placeholder"
|
||||
)
|
||||
}}
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import Component from "@ember/component";
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
|
||||
import { schedule } from "@ember/runloop";
|
||||
|
||||
export default Component.extend({
|
||||
showSelector: true,
|
||||
shouldHide: false,
|
||||
defaultUsernameCount: 0,
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set("_groups", []);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
@ -17,78 +16,34 @@ export default Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@observes("usernames")
|
||||
_checkWidth() {
|
||||
let width = 0;
|
||||
const $acWrap = $(this.element).find(".ac-wrap");
|
||||
const limit = $acWrap.width();
|
||||
this.set("defaultUsernameCount", 0);
|
||||
@discourseComputed("recipients")
|
||||
splitRecipients(recipients) {
|
||||
return recipients ? recipients.split(",").filter(Boolean) : [];
|
||||
},
|
||||
|
||||
$acWrap
|
||||
.find(".item")
|
||||
.toArray()
|
||||
.forEach((item) => {
|
||||
width += $(item).outerWidth(true);
|
||||
const result = width < limit;
|
||||
|
||||
if (result) {
|
||||
this.incrementProperty("defaultUsernameCount");
|
||||
_updateGroups(selected, newGroups) {
|
||||
const groups = [];
|
||||
this._groups.forEach((existing) => {
|
||||
if (selected.includes(existing)) {
|
||||
groups.addObject(existing);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
if (width >= limit) {
|
||||
this.set("shouldHide", true);
|
||||
} else {
|
||||
this.set("shouldHide", false);
|
||||
newGroups.forEach((newGroup) => {
|
||||
if (!groups.includes(newGroup)) {
|
||||
groups.addObject(newGroup);
|
||||
}
|
||||
},
|
||||
|
||||
@observes("shouldHide")
|
||||
_setFocus() {
|
||||
const selector =
|
||||
"#reply-control #reply-title, #reply-control .d-editor-input";
|
||||
|
||||
if (this.shouldHide) {
|
||||
$(selector).on("focus.composer-user-selector", () => {
|
||||
this.set("showSelector", false);
|
||||
this.appEvents.trigger("composer:resize");
|
||||
});
|
||||
} else {
|
||||
$(selector).off("focus.composer-user-selector");
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("usernames")
|
||||
splitUsernames(usernames) {
|
||||
return usernames.split(",");
|
||||
},
|
||||
|
||||
@discourseComputed("splitUsernames", "defaultUsernameCount")
|
||||
limitedUsernames(splitUsernames, count) {
|
||||
return splitUsernames.slice(0, count).join(", ");
|
||||
},
|
||||
|
||||
@discourseComputed("splitUsernames", "defaultUsernameCount")
|
||||
hiddenUsersCount(splitUsernames, count) {
|
||||
return `${splitUsernames.length - count} ${I18n.t("more")}`;
|
||||
this.setProperties({
|
||||
_groups: groups,
|
||||
hasGroups: groups.length > 0,
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleSelector() {
|
||||
this.set("showSelector", true);
|
||||
|
||||
schedule("afterRender", () => {
|
||||
$(this.element).find("input").focus();
|
||||
});
|
||||
},
|
||||
|
||||
triggerResize() {
|
||||
this.appEvents.trigger("composer:resize");
|
||||
const $this = $(this.element).find(".ac-wrap");
|
||||
if ($this.height() >= 150) {
|
||||
$this.scrollTop($this.height());
|
||||
}
|
||||
updateRecipients(selected, content) {
|
||||
const newGroups = content.filterBy("isGroup").mapBy("id");
|
||||
this._updateGroups(selected, newGroups);
|
||||
this.set("recipients", selected.join(","));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -20,9 +20,9 @@ export default Component.extend({
|
|||
isStaff: readOnly("currentUser.staff"),
|
||||
isAdmin: readOnly("currentUser.admin"),
|
||||
|
||||
// If this isn't defined, it will proxy to the user topic on the preferences
|
||||
// page which is wrong.
|
||||
emailOrUsername: null,
|
||||
// invitee is either a user, group or email
|
||||
invitee: null,
|
||||
isInviteeGroup: false,
|
||||
hasCustomMessage: false,
|
||||
customMessage: null,
|
||||
inviteIcon: "envelope",
|
||||
|
@ -41,7 +41,7 @@ export default Component.extend({
|
|||
|
||||
@discourseComputed(
|
||||
"isAdmin",
|
||||
"emailOrUsername",
|
||||
"invitee",
|
||||
"invitingToTopic",
|
||||
"isPrivateTopic",
|
||||
"groupIds",
|
||||
|
@ -50,7 +50,7 @@ export default Component.extend({
|
|||
)
|
||||
disabled(
|
||||
isAdmin,
|
||||
emailOrUsername,
|
||||
invitee,
|
||||
invitingToTopic,
|
||||
isPrivateTopic,
|
||||
groupIds,
|
||||
|
@ -60,24 +60,22 @@ export default Component.extend({
|
|||
if (saving) {
|
||||
return true;
|
||||
}
|
||||
if (isEmpty(emailOrUsername)) {
|
||||
if (isEmpty(invitee)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const emailTrimmed = emailOrUsername.trim();
|
||||
|
||||
// when inviting to forum, email must be valid
|
||||
if (!invitingToTopic && !emailValid(emailTrimmed)) {
|
||||
if (!invitingToTopic && !emailValid(invitee)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// normal users (not admin) can't invite users to private topic via email
|
||||
if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) {
|
||||
if (!isAdmin && isPrivateTopic && emailValid(invitee)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// when inviting to private topic via email, group name must be specified
|
||||
if (isPrivateTopic && isEmpty(groupIds) && emailValid(emailTrimmed)) {
|
||||
if (isPrivateTopic && isEmpty(groupIds) && emailValid(invitee)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -90,7 +88,7 @@ export default Component.extend({
|
|||
|
||||
@discourseComputed(
|
||||
"isAdmin",
|
||||
"emailOrUsername",
|
||||
"invitee",
|
||||
"inviteModel.saving",
|
||||
"isPrivateTopic",
|
||||
"groupIds",
|
||||
|
@ -98,7 +96,7 @@ export default Component.extend({
|
|||
)
|
||||
disabledCopyLink(
|
||||
isAdmin,
|
||||
emailOrUsername,
|
||||
invitee,
|
||||
saving,
|
||||
isPrivateTopic,
|
||||
groupIds,
|
||||
|
@ -110,24 +108,22 @@ export default Component.extend({
|
|||
if (saving) {
|
||||
return true;
|
||||
}
|
||||
if (isEmpty(emailOrUsername)) {
|
||||
if (isEmpty(invitee)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const email = emailOrUsername.trim();
|
||||
|
||||
// email must be valid
|
||||
if (!emailValid(email)) {
|
||||
if (!emailValid(invitee)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// normal users (not admin) can't invite users to private topic via email
|
||||
if (!isAdmin && isPrivateTopic && emailValid(email)) {
|
||||
if (!isAdmin && isPrivateTopic && emailValid(invitee)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// when inviting to private topic via email, group name must be specified
|
||||
if (isPrivateTopic && isEmpty(groupIds) && emailValid(email)) {
|
||||
if (isPrivateTopic && isEmpty(groupIds) && emailValid(invitee)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -179,7 +175,7 @@ export default Component.extend({
|
|||
// Show Groups? (add invited user to private group)
|
||||
@discourseComputed(
|
||||
"isGroupOwnerOrAdmin",
|
||||
"emailOrUsername",
|
||||
"invitee",
|
||||
"isPrivateTopic",
|
||||
"isPM",
|
||||
"invitingToTopic",
|
||||
|
@ -187,7 +183,7 @@ export default Component.extend({
|
|||
)
|
||||
showGroups(
|
||||
isGroupOwnerOrAdmin,
|
||||
emailOrUsername,
|
||||
invitee,
|
||||
isPrivateTopic,
|
||||
isPM,
|
||||
invitingToTopic,
|
||||
|
@ -197,20 +193,20 @@ export default Component.extend({
|
|||
isGroupOwnerOrAdmin &&
|
||||
canInviteViaEmail &&
|
||||
!isPM &&
|
||||
(emailValid(emailOrUsername) || isPrivateTopic || !invitingToTopic)
|
||||
(emailValid(invitee) || isPrivateTopic || !invitingToTopic)
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("emailOrUsername")
|
||||
showCustomMessage(emailOrUsername) {
|
||||
return this.inviteModel === this.currentUser || emailValid(emailOrUsername);
|
||||
@discourseComputed("invitee")
|
||||
showCustomMessage(invitee) {
|
||||
return this.inviteModel === this.currentUser || emailValid(invitee);
|
||||
},
|
||||
|
||||
// Instructional text for the modal.
|
||||
@discourseComputed(
|
||||
"isPM",
|
||||
"invitingToTopic",
|
||||
"emailOrUsername",
|
||||
"invitee",
|
||||
"isPrivateTopic",
|
||||
"isAdmin",
|
||||
"canInviteViaEmail"
|
||||
|
@ -218,7 +214,7 @@ export default Component.extend({
|
|||
inviteInstructions(
|
||||
isPM,
|
||||
invitingToTopic,
|
||||
emailOrUsername,
|
||||
invitee,
|
||||
isPrivateTopic,
|
||||
isAdmin,
|
||||
canInviteViaEmail
|
||||
|
@ -236,9 +232,9 @@ export default Component.extend({
|
|||
return I18n.t("topic.invite_reply.to_username");
|
||||
} else {
|
||||
// when inviting to a topic, display instructions based on provided entity
|
||||
if (isEmpty(emailOrUsername)) {
|
||||
if (isEmpty(invitee)) {
|
||||
return I18n.t("topic.invite_reply.to_topic_blank");
|
||||
} else if (emailValid(emailOrUsername)) {
|
||||
} else if (emailValid(invitee)) {
|
||||
this.set("inviteIcon", "envelope");
|
||||
return I18n.t("topic.invite_reply.to_topic_email");
|
||||
} else {
|
||||
|
@ -257,18 +253,18 @@ export default Component.extend({
|
|||
return isPrivateTopic ? "required" : "optional";
|
||||
},
|
||||
|
||||
@discourseComputed("isPM", "emailOrUsername", "invitingExistingUserToTopic")
|
||||
successMessage(isPM, emailOrUsername, invitingExistingUserToTopic) {
|
||||
if (this.hasGroups) {
|
||||
@discourseComputed("isPM", "invitee", "invitingExistingUserToTopic")
|
||||
successMessage(isPM, invitee, invitingExistingUserToTopic) {
|
||||
if (this.isInviteeGroup) {
|
||||
return I18n.t("topic.invite_private.success_group");
|
||||
} else if (isPM) {
|
||||
return I18n.t("topic.invite_private.success");
|
||||
} else if (invitingExistingUserToTopic) {
|
||||
return I18n.t("topic.invite_reply.success_existing_email", {
|
||||
emailOrUsername,
|
||||
invitee,
|
||||
});
|
||||
} else if (emailValid(emailOrUsername)) {
|
||||
return I18n.t("topic.invite_reply.success_email", { emailOrUsername });
|
||||
} else if (emailValid(invitee)) {
|
||||
return I18n.t("topic.invite_reply.success_email", { invitee });
|
||||
} else {
|
||||
return I18n.t("topic.invite_reply.success_username");
|
||||
}
|
||||
|
@ -295,7 +291,8 @@ export default Component.extend({
|
|||
// Reset the modal to allow a new user to be invited.
|
||||
reset() {
|
||||
this.setProperties({
|
||||
emailOrUsername: null,
|
||||
invitee: null,
|
||||
isInviteeGroup: false,
|
||||
hasCustomMessage: false,
|
||||
customMessage: null,
|
||||
invitingExistingUserToTopic: false,
|
||||
|
@ -346,9 +343,9 @@ export default Component.extend({
|
|||
model.setProperties({ saving: false, error: true });
|
||||
};
|
||||
|
||||
if (this.hasGroups) {
|
||||
if (this.isInviteeGroup) {
|
||||
return this.inviteModel
|
||||
.createGroupInvite(this.emailOrUsername.trim())
|
||||
.createGroupInvite(this.invitee.trim())
|
||||
.then((data) => {
|
||||
model.setProperties({ saving: false, finished: true });
|
||||
this.get("inviteModel.details.allowed_groups").pushObject(
|
||||
|
@ -359,7 +356,7 @@ export default Component.extend({
|
|||
.catch(onerror);
|
||||
} else {
|
||||
return this.inviteModel
|
||||
.createInvite(this.emailOrUsername.trim(), groupIds, this.customMessage)
|
||||
.createInvite(this.invitee.trim(), groupIds, this.customMessage)
|
||||
.then((result) => {
|
||||
model.setProperties({ saving: false, finished: true });
|
||||
if (!this.invitingToTopic && userInvitedController) {
|
||||
|
@ -379,7 +376,7 @@ export default Component.extend({
|
|||
this.appEvents.trigger("post-stream:refresh", { force: true });
|
||||
} else if (
|
||||
this.invitingToTopic &&
|
||||
emailValid(this.emailOrUsername.trim()) &&
|
||||
emailValid(this.invitee.trim()) &&
|
||||
result &&
|
||||
result.user
|
||||
) {
|
||||
|
@ -407,7 +404,7 @@ export default Component.extend({
|
|||
}
|
||||
|
||||
return model
|
||||
.generateInviteLink(this.emailOrUsername.trim(), groupIds, topicId)
|
||||
.generateInviteLink(this.invitee.trim(), groupIds, topicId)
|
||||
.then((result) => {
|
||||
model.setProperties({
|
||||
saving: false,
|
||||
|
@ -465,7 +462,23 @@ export default Component.extend({
|
|||
@action
|
||||
searchContact() {
|
||||
getNativeContact(this.capabilities, ["email"], false).then((result) => {
|
||||
this.set("emailOrUsername", result[0].email[0]);
|
||||
this.set("invitee", result[0].email[0]);
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
updateInvitee(selected, content) {
|
||||
const invitee = content.findBy("id", selected[0]);
|
||||
if (invitee) {
|
||||
this.setProperties({
|
||||
invitee: invitee.id.trim(),
|
||||
isInviteeGroup: invitee.isGroup || false,
|
||||
});
|
||||
} else {
|
||||
this.setProperties({
|
||||
invitee: null,
|
||||
isInviteeGroup: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,22 +1,14 @@
|
|||
{{#if showSelector}}
|
||||
{{user-selector
|
||||
topicId=topicId
|
||||
onChangeCallback=(action "triggerResize")
|
||||
{{email-group-user-chooser
|
||||
id="private-message-users"
|
||||
includeMessageableGroups="true"
|
||||
placeholderKey="composer.users_placeholder"
|
||||
tabindex="1"
|
||||
usernames=usernames
|
||||
hasGroups=hasGroups
|
||||
allowEmails="true"
|
||||
autocomplete="discourse"
|
||||
canReceiveUpdates=true
|
||||
}}
|
||||
{{else}}
|
||||
<a href {{action "toggleSelector"}}>
|
||||
<div class="ac-wrap composer-user-selector-limited">
|
||||
<span>{{limitedUsernames}}</span>
|
||||
<span class="btn btn-primary">{{hiddenUsersCount}}</span>
|
||||
</div>
|
||||
</a>
|
||||
{{/if}}
|
||||
value=splitRecipients
|
||||
onChange=(action "updateRecipients")
|
||||
options=(hash
|
||||
topicId=topicId
|
||||
filterPlaceholder="composer.users_placeholder"
|
||||
includeMessageableGroups=true
|
||||
allowEmails=true
|
||||
autoWrap=true
|
||||
)
|
||||
}}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<div class="body">
|
||||
{{#if inviteModel.finished}}
|
||||
{{#if inviteModel.inviteLink}}
|
||||
{{generated-invite-link link=inviteModel.inviteLink email=emailOrUsername}}
|
||||
{{generated-invite-link link=inviteModel.inviteLink email=invitee}}
|
||||
{{else}}
|
||||
<div class="success-message">
|
||||
{{html-safe successMessage}}
|
||||
|
@ -18,24 +18,23 @@
|
|||
<label class="instructions">{{inviteInstructions}}</label>
|
||||
<div class="invite-user-input-wrapper">
|
||||
{{#if allowExistingMembers}}
|
||||
{{user-selector
|
||||
fullWidthWrap=true
|
||||
single=true
|
||||
allowAny=true
|
||||
excludeCurrentUser=true
|
||||
includeMessageableGroups=isPM
|
||||
hasGroups=hasGroups
|
||||
usernames=emailOrUsername
|
||||
placeholderKey=placeholderKey
|
||||
allowEmails=canInviteViaEmail
|
||||
{{email-group-user-chooser
|
||||
class="invite-user-input"
|
||||
autocomplete="discourse"
|
||||
value=emailOrUsername
|
||||
value=invitee
|
||||
onChange=(action "updateInvitee")
|
||||
options=(hash
|
||||
maximum=1
|
||||
allowEmails=canInviteViaEmail
|
||||
excludeCurrentUser=true
|
||||
includeMessageableGroups=isPM
|
||||
filterPlaceholder=placeholderKey
|
||||
)
|
||||
}}
|
||||
{{else}}
|
||||
{{text-field
|
||||
class="email-or-username-input"
|
||||
value=emailOrUsername
|
||||
value=invitee
|
||||
placeholderKey="topic.invite_reply.email_placeholder"}}
|
||||
{{/if}}
|
||||
{{#if capabilities.hasContactPicker}}
|
||||
|
|
|
@ -52,11 +52,13 @@
|
|||
{{#if model.canEditTitle}}
|
||||
{{#if model.creatingPrivateMessage}}
|
||||
<div class="user-selector">
|
||||
{{composer-user-selector topicId=topicModel.id
|
||||
usernames=model.targetRecipients
|
||||
{{composer-user-selector
|
||||
topicId=topicModel.id
|
||||
recipients=model.targetRecipients
|
||||
hasGroups=model.hasTargetGroups
|
||||
focusTarget=focusTarget
|
||||
class="users-input"}}
|
||||
class="users-input"
|
||||
}}
|
||||
{{#if showWarning}}
|
||||
<label class="add-warning">
|
||||
{{input type="checkbox" checked=model.isWarning tabindex="3"}}
|
||||
|
|
|
@ -62,7 +62,9 @@ acceptance("Composer Actions", function (needs) {
|
|||
await composerActions.selectRowByValue("reply_as_private_message");
|
||||
|
||||
assert.equal(
|
||||
queryAll(".users-input .item:nth-of-type(1)").text(),
|
||||
queryAll("#private-message-users .selected-name:nth-of-type(1)")
|
||||
.text()
|
||||
.trim(),
|
||||
"codinghorror"
|
||||
);
|
||||
assert.ok(
|
||||
|
@ -164,7 +166,7 @@ acceptance("Composer Actions", function (needs) {
|
|||
await composerActions.selectRowByValue("reply_as_new_group_message");
|
||||
|
||||
const items = [];
|
||||
queryAll(".users-input .item").each((_, item) =>
|
||||
queryAll("#private-message-users .selected-name").each((_, item) =>
|
||||
items.push(item.textContent.trim())
|
||||
);
|
||||
|
||||
|
@ -348,7 +350,9 @@ acceptance("Composer Actions", function (needs) {
|
|||
await composerActions.selectRowByValue("reply_as_private_message");
|
||||
|
||||
assert.equal(
|
||||
queryAll(".users-input .item:nth-of-type(1)").text(),
|
||||
queryAll("#private-message-users .selected-name:nth-of-type(1)")
|
||||
.text()
|
||||
.trim(),
|
||||
"uwe_keim"
|
||||
);
|
||||
assert.ok(
|
||||
|
|
|
@ -713,7 +713,9 @@ acceptance("Composer", function (needs) {
|
|||
await click(".modal .btn-default");
|
||||
|
||||
assert.equal(
|
||||
queryAll(".users-input .item:nth-of-type(1)").text(),
|
||||
queryAll("#private-message-users .selected-name:nth-of-type(1)")
|
||||
.text()
|
||||
.trim(),
|
||||
"codinghorror"
|
||||
);
|
||||
} finally {
|
||||
|
|
|
@ -215,7 +215,7 @@ acceptance("Group - Authenticated", function (needs) {
|
|||
|
||||
assert.ok(count("#reply-control") === 1, "it opens the composer");
|
||||
assert.equal(
|
||||
queryAll(".ac-wrap .item").text(),
|
||||
queryAll("#private-message-users .selected-name").text().trim(),
|
||||
"discourse",
|
||||
"it prefills the group name"
|
||||
);
|
||||
|
|
|
@ -36,7 +36,9 @@ acceptance("New Message - Authenticated", function (needs) {
|
|||
"it pre-fills message body"
|
||||
);
|
||||
assert.equal(
|
||||
queryAll(".users-input .item:nth-of-type(1)").text().trim(),
|
||||
queryAll("#private-message-users .selected-name:nth-of-type(1)")
|
||||
.text()
|
||||
.trim(),
|
||||
"charlie",
|
||||
"it selects correct username"
|
||||
);
|
||||
|
|
|
@ -67,22 +67,25 @@ acceptance("Topic", function (needs) {
|
|||
"it fills composer with the ring string"
|
||||
);
|
||||
|
||||
const targets = queryAll(".item span", ".composer-fields");
|
||||
const targets = queryAll(
|
||||
"#private-message-users .selected-name",
|
||||
".composer-fields"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
$(targets[0]).text(),
|
||||
$(targets[0]).text().trim(),
|
||||
"someguy",
|
||||
"it fills up the composer with the right user to start the PM to"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
$(targets[1]).text(),
|
||||
$(targets[1]).text().trim(),
|
||||
"test",
|
||||
"it fills up the composer with the right user to start the PM to"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
$(targets[2]).text(),
|
||||
$(targets[2]).text().trim(),
|
||||
"Group",
|
||||
"it fills up the composer with the right group to start the PM to"
|
||||
);
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import MultiSelectHeaderComponent from "select-kit/components/multi-select/multi-select-header";
|
||||
import { computed } from "@ember/object";
|
||||
import { gt } from "@ember/object/computed";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import layout from "select-kit/templates/components/email-group-user-chooser-header";
|
||||
|
||||
export default MultiSelectHeaderComponent.extend({
|
||||
layout,
|
||||
classNames: ["email-group-user-chooser-header"],
|
||||
hasHiddenItems: gt("hiddenItemsCount", 0),
|
||||
|
||||
shownItems: computed("hiddenItemsCount", function () {
|
||||
if (
|
||||
this.selectKit.noneItem === this.selectedContent ||
|
||||
this.hiddenItemsCount === 0
|
||||
) {
|
||||
return this.selectedContent;
|
||||
}
|
||||
return this.selectedContent.slice(
|
||||
0,
|
||||
this.selectedContent.length - this.hiddenItemsCount
|
||||
);
|
||||
}),
|
||||
|
||||
hiddenItemsCount: computed(
|
||||
"selectedContent.[]",
|
||||
"selectKit.options.autoWrap",
|
||||
"selectKit.isExpanded",
|
||||
function () {
|
||||
if (
|
||||
!this.selectKit.options.autoWrap ||
|
||||
this.selectKit.isExpanded ||
|
||||
this.selectedContent === this.selectKit.noneItem ||
|
||||
this.selectedContent.length <= 1 ||
|
||||
isTesting()
|
||||
) {
|
||||
return 0;
|
||||
} else {
|
||||
const selectKitHeaderWidth = this.element.offsetWidth;
|
||||
const choices = this.element.querySelectorAll(".selected-name.choice");
|
||||
const input = this.element.querySelector(".filter-input");
|
||||
const alreadyHidden = this.element.querySelector(".x-more-item");
|
||||
if (alreadyHidden) {
|
||||
const hiddenCount = parseInt(
|
||||
alreadyHidden.getAttribute("data-hidden-count"),
|
||||
10
|
||||
);
|
||||
return (
|
||||
hiddenCount +
|
||||
(this.selectedContent.length - (choices.length + hiddenCount))
|
||||
);
|
||||
}
|
||||
if (choices.length === 0 && this.selectedContent.length > 0) {
|
||||
return 0;
|
||||
}
|
||||
let total = choices[0].offsetWidth + input.offsetWidth;
|
||||
let shownItemsCount = 1;
|
||||
let shouldHide = false;
|
||||
for (let i = 1; i < choices.length - 1; i++) {
|
||||
const currentWidth = choices[i].offsetWidth;
|
||||
const nextWidth = choices[i + 1].offsetWidth;
|
||||
const ratio =
|
||||
(total + currentWidth + nextWidth) / selectKitHeaderWidth;
|
||||
if (ratio >= 0.95) {
|
||||
shouldHide = true;
|
||||
break;
|
||||
} else {
|
||||
shownItemsCount++;
|
||||
total += currentWidth;
|
||||
}
|
||||
}
|
||||
return shouldHide ? choices.length - shownItemsCount : 0;
|
||||
}
|
||||
}
|
||||
),
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
|
||||
import layout from "select-kit/templates/components/email-group-user-chooser-row";
|
||||
|
||||
export default SelectKitRowComponent.extend({
|
||||
layout,
|
||||
classNames: ["email-group-user-chooser-row"],
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import UserChooserComponent from "select-kit/components/user-chooser";
|
||||
|
||||
export default UserChooserComponent.extend({
|
||||
pluginApiIdentifiers: ["email-group-user-chooser"],
|
||||
classNames: ["email-group-user-chooser"],
|
||||
valueProperty: "id",
|
||||
nameProperty: "name",
|
||||
|
||||
modifyComponentForRow() {
|
||||
return "email-group-user-chooser-row";
|
||||
},
|
||||
|
||||
selectKitOptions: {
|
||||
headerComponent: "email-group-user-chooser-header",
|
||||
autoWrap: false,
|
||||
},
|
||||
|
||||
search() {
|
||||
const superPromise = this._super(...arguments);
|
||||
if (!superPromise) {
|
||||
return;
|
||||
}
|
||||
return superPromise.then((results) => {
|
||||
if (!results || results.length === 0) {
|
||||
return;
|
||||
}
|
||||
return results.map((item) => {
|
||||
const reconstructed = {};
|
||||
if (item.username) {
|
||||
reconstructed.id = item.username;
|
||||
if (item.username.includes("@")) {
|
||||
reconstructed.isEmail = true;
|
||||
} else {
|
||||
reconstructed.isUser = true;
|
||||
reconstructed.name = item.name;
|
||||
}
|
||||
} else if (item.name) {
|
||||
reconstructed.id = item.name;
|
||||
reconstructed.name = item.full_name;
|
||||
reconstructed.isGroup = true;
|
||||
}
|
||||
return Object.assign({}, item, reconstructed);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
|
@ -87,6 +87,7 @@ export default Component.extend(
|
|||
isHidden: false,
|
||||
isExpanded: false,
|
||||
isFilterExpanded: false,
|
||||
enterDisabled: false,
|
||||
hasSelection: false,
|
||||
hasNoContent: true,
|
||||
highlighted: null,
|
||||
|
@ -570,12 +571,17 @@ export default Component.extend(
|
|||
|
||||
_searchWrapper(filter) {
|
||||
this.clearErrors();
|
||||
this.setProperties({ mainCollection: [], "selectKit.isLoading": true });
|
||||
this.setProperties({
|
||||
mainCollection: [],
|
||||
"selectKit.isLoading": true,
|
||||
"selectKit.enterDisabled": true,
|
||||
});
|
||||
this._safeAfterRender(() => this.popper && this.popper.update());
|
||||
|
||||
let content = [];
|
||||
|
||||
return Promise.resolve(this.search(filter)).then((result) => {
|
||||
return Promise.resolve(this.search(filter))
|
||||
.then((result) => {
|
||||
content = content.concat(makeArray(result));
|
||||
content = this.selectKit.modifyContent(content).filter(Boolean);
|
||||
|
||||
|
@ -627,6 +633,12 @@ export default Component.extend(
|
|||
this.popper && this.popper.update();
|
||||
this._focusFilter();
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.isDestroyed || this.isDestroying) {
|
||||
return;
|
||||
}
|
||||
this.set("selectKit.enterDisabled", false);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -854,11 +866,12 @@ export default Component.extend(
|
|||
}
|
||||
|
||||
const popperElement = data.state.elements.popper;
|
||||
if (
|
||||
const topPlacement =
|
||||
popperElement &&
|
||||
popperElement.getAttribute("data-popper-placement") ===
|
||||
"top-start"
|
||||
) {
|
||||
popperElement
|
||||
.getAttribute("data-popper-placement")
|
||||
.startsWith("top-");
|
||||
if (topPlacement) {
|
||||
this.element.classList.remove("is-under");
|
||||
this.element.classList.add("is-above");
|
||||
} else {
|
||||
|
@ -868,6 +881,20 @@ export default Component.extend(
|
|||
|
||||
wrapper.style.width = `${this.element.offsetWidth}px`;
|
||||
wrapper.style.height = `${height}px`;
|
||||
if (placementStrategy === "fixed") {
|
||||
const rects = this.element.getClientRects()[0];
|
||||
const bodyRects = body && body.getClientRects()[0];
|
||||
wrapper.style.position = "fixed";
|
||||
wrapper.style.left = `${rects.left}px`;
|
||||
if (topPlacement && bodyRects) {
|
||||
wrapper.style.top = `${rects.top - bodyRects.height}px`;
|
||||
} else {
|
||||
wrapper.style.top = `${rects.top}px`;
|
||||
}
|
||||
if (isDocumentRTL()) {
|
||||
wrapper.style.right = "unset";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -56,6 +56,16 @@ export default Component.extend(UtilsMixin, {
|
|||
return true;
|
||||
},
|
||||
|
||||
onKeyup(event) {
|
||||
if (event.keyCode === 13 && this.selectKit.enterDisabled) {
|
||||
this.element.querySelector("input").focus();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
onKeydown(event) {
|
||||
if (!this.selectKit.onKeydown(event)) {
|
||||
return false;
|
||||
|
@ -93,8 +103,15 @@ export default Component.extend(UtilsMixin, {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (event.keyCode === 13 && !this.selectKit.highlighted) {
|
||||
if (
|
||||
event.keyCode === 13 &&
|
||||
(!this.selectKit.highlighted || this.selectKit.enterDisabled)
|
||||
) {
|
||||
this.element.querySelector("input").focus();
|
||||
if (this.selectKit.enterDisabled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -109,6 +126,7 @@ export default Component.extend(UtilsMixin, {
|
|||
this.selectKit.close(event);
|
||||
return;
|
||||
}
|
||||
this.selectKit.set("highlighted", null);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ export default MultiSelectComponent.extend({
|
|||
includeMessageableGroups: false,
|
||||
allowEmails: false,
|
||||
groupMembersOf: undefined,
|
||||
excludeCurrentUser: false,
|
||||
},
|
||||
|
||||
content: computed("value.[]", function () {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<div class="choices">
|
||||
{{#each shownItems as |item|}}
|
||||
{{component selectKit.options.selectedNameComponent
|
||||
tabindex=tabindex
|
||||
item=item
|
||||
selectKit=selectKit
|
||||
}}
|
||||
{{/each}}
|
||||
{{#if hasHiddenItems}}
|
||||
<div class="x-more-item" data-hidden-count={{hiddenItemsCount}}>
|
||||
{{i18n "x_more" count=hiddenItemsCount}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless hasReachedMaximumSelection}}
|
||||
<div class="choice input-wrapper">
|
||||
{{component selectKit.options.filterComponent
|
||||
selectKit=selectKit
|
||||
}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
{{#if item.isUser}}
|
||||
{{avatar item imageSize="tiny"}}
|
||||
<span class="identifier">{{format-username item.id}}</span>
|
||||
<span class="name">{{item.name}}</span>
|
||||
{{else if item.isGroup}}
|
||||
{{d-icon "users"}}
|
||||
<span class="identifier">{{item.id}}</span>
|
||||
<span class="name">{{item.full_name}}</span>
|
||||
{{else}}
|
||||
{{d-icon "envelope"}}
|
||||
<span class="identifier">{{item.id}}</span>
|
||||
{{/if}}
|
|
@ -11,6 +11,7 @@
|
|||
input=(action "onInput")
|
||||
paste=(action "onPaste")
|
||||
keyDown=(action "onKeydown")
|
||||
keyUp=(action "onKeyup")
|
||||
}}
|
||||
|
||||
{{#if selectKit.options.filterIcon}}
|
||||
|
|
|
@ -100,7 +100,8 @@ table.api-keys {
|
|||
|
||||
.value-list,
|
||||
.select-kit,
|
||||
input[type="text"] {
|
||||
input[type="text"],
|
||||
input[type="text"].filter-input {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
@import "combo-box";
|
||||
@import "composer-actions";
|
||||
@import "dropdown-select-box";
|
||||
@import "email-group-user-chooser";
|
||||
@import "future-date-input-selector";
|
||||
@import "icon-picker";
|
||||
@import "list-setting";
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
.select-kit.email-group-user-chooser {
|
||||
.select-kit-row.email-group-user-chooser-row {
|
||||
.identifier {
|
||||
color: var(--primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.name {
|
||||
color: var(--primary-high);
|
||||
font-size: $font-down-1;
|
||||
margin-left: 0.5em;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.avatar,
|
||||
.d-icon {
|
||||
margin-left: 0;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
.select-kit-header {
|
||||
.x-more-item {
|
||||
background: var(--primary-low);
|
||||
padding: 0.25em;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
margin: 2px 0 0px 3px;
|
||||
float: left;
|
||||
height: 30px;
|
||||
color: inherit;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -245,6 +245,9 @@ en:
|
|||
now: "just now"
|
||||
read_more: "read more"
|
||||
more: "More"
|
||||
x_more:
|
||||
one: "%{count} More"
|
||||
other: "%{count} More"
|
||||
less: "Less"
|
||||
never: "never"
|
||||
every_30_minutes: "every 30 minutes"
|
||||
|
|
Loading…
Reference in New Issue