FEATURE: Move metadata user results to list bottom (#18977)

Partial username or name matches were shown together with metadata
matched results. This created a bad user experience because results
that look unrelated were before even partial or exact group matches.
This commit is contained in:
Bianca Nenciu 2023-01-30 15:38:41 +02:00 committed by GitHub
parent 335c3f4621
commit 9a196ced08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 102 additions and 57 deletions

View File

@ -196,21 +196,6 @@ export default Component.extend(ComposerUploadUppy, {
};
},
@bind
_userSearchTerm(term) {
const topicId = this.get("topic.id");
// maybe this is a brand new topic, so grab category from composer
const categoryId =
this.get("topic.category_id") || this.get("composer.categoryId");
return userSearch({
term,
topicId,
categoryId,
includeGroups: true,
});
},
@bind
_afterMentionComplete(value) {
this.composer.set("reply", value);
@ -230,7 +215,13 @@ export default Component.extend(ComposerUploadUppy, {
if (this.siteSettings.enable_mentions) {
$input.autocomplete({
template: findRawTemplate("user-selector-autocomplete"),
dataSource: this._userSearchTerm,
dataSource: (term) =>
userSearch({
term,
topicId: this.topic?.id,
categoryId: this.topic?.category_id || this.composer?.categoryId,
includeGroups: true,
}),
key: "@",
transformComplete: (v) => v.username || v.name,
afterComplete: this._afterMentionComplete,

View File

@ -145,6 +145,10 @@ let debouncedSearch = function (
);
};
function lowerCaseIncludes(string, term) {
return string && term && string.toLowerCase().includes(term.toLowerCase());
}
function organizeResults(r, options) {
if (r === CANCELLED_STATUS) {
return r;
@ -152,39 +156,54 @@ function organizeResults(r, options) {
const exclude = options.exclude || [];
const results = [],
users = [],
// Sometimes the term passed contains spaces, but the search is limited
// to the first word only.
const term = options.term?.trim()?.split(/\s/, 1)?.[0];
const users = [],
emails = [],
groups = [];
let resultsLength = 0;
if (r.users) {
r.users.forEach((user) => {
if (results.length < options.limit && !exclude.includes(user.username)) {
results.push(user);
if (resultsLength < options.limit && !exclude.includes(user.username)) {
user.isUser = true;
user.isMetadataMatch =
!lowerCaseIncludes(user.username, term) &&
!lowerCaseIncludes(user.name, term);
users.push(user);
resultsLength += 1;
}
});
}
if (options.allowEmails && emailValid(options.term)) {
const result = { username: options.term };
results.push(result);
emails.push(result);
emails.push({ username: options.term, isEmail: true });
resultsLength += 1;
}
if (r.groups) {
r.groups.forEach((group) => {
if (
(options.term.toLowerCase() === group.name.toLowerCase() ||
results.length < options.limit) &&
resultsLength < options.limit) &&
!exclude.includes(group.name)
) {
results.push(group);
group.isGroup = true;
groups.push(group);
resultsLength += 1;
}
});
}
const results = [
...users.filter((u) => !u.isMetadataMatch),
...emails,
...groups,
...users.filter((u) => u.isMetadataMatch),
];
results.users = users;
results.emails = emails;
results.groups = groups;

View File

@ -1,47 +1,45 @@
<div class='autocomplete ac-user'>
<ul>
{{#each options.users as |user|}}
{{#each options as |item|}}
{{#if item.isUser}}
<li>
<a href title="{{user.name}}" class="{{user.cssClasses}}">
<a href title="{{item.name}}" class="{{item.cssClasses}}">
{{avatar user imageSize="tiny"}}
<span class='username'>{{format-username user.username}}</span>
{{#if user.name}}
<span class='name'>{{user.name}}</span>
<span class='username'>{{format-username item.username}}</span>
{{#if item.name}}
<span class='name'>{{item.name}}</span>
{{/if}}
{{#if user.status}}
{{emoji user.status.emoji}}
<span class='status-description' title='{{user.status.description}}'>
{{user.status.description}}
{{#if item.status}}
{{emoji item.status.emoji}}
<span class='status-description' title='{{item.status.description}}'>
{{item.status.description}}
</span>
{{#if user.status.ends_at}}
{{format-age user.status.ends_at}}
{{#if item.status.ends_at}}
{{format-age item.status.ends_at}}
{{/if}}
{{/if}}
</a>
</li>
{{/each}}
{{/if}}
{{#if options.emails}}
{{#each options.emails as |email|}}
{{#if item.isEmail}}
<li>
<a href title="{{email.username}}">
<a href title="{{item.username}}">
{{d-icon 'envelope'}}
<span class='username'>{{format-username email.username}}</span>
<span class='username'>{{format-username item.username}}</span>
</a>
</li>
{{/each}}
{{/if}}
{{#if options.groups}}
{{#each options.groups as |group|}}
{{#if item.isGroup}}
<li>
<a href title="{{group.full_name}}">
<a href title="{{item.full_name}}">
{{d-icon "users"}}
<span class='username'>{{group.name}}</span>
<span class='name'>{{group.full_name}}</span>
<span class='username'>{{item.name}}</span>
<span class='name'>{{item.full_name}}</span>
</a>
</li>
{{/each}}
{{/if}}
{{/each}}
</ul>
</div>

View File

@ -6,6 +6,7 @@ import {
fakeTime,
loggedInUser,
query,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import { setCaretPosition } from "discourse/lib/utilities";
@ -43,6 +44,17 @@ acceptance("Composer - editor mentions", function (needs) {
avatar_template:
"https://avatars.discourse.org/v3/letter/t/41988e/{size}.png",
},
{
username: "foo",
avatar_template:
"https://avatars.discourse.org/v3/letter/t/41988e/{size}.png",
},
],
groups: [
{
name: "user_group",
full_name: "Group",
},
],
});
});
@ -157,4 +169,29 @@ acceptance("Composer - editor mentions", function (needs) {
"status expiration time is shown"
);
});
test("metadata matches are moved to the end", async function (assert) {
await visit("/");
await click("#create-topic");
await fillIn(".d-editor-input", "abc @");
await triggerKeyEvent(".d-editor-input", "keyup", "@");
await fillIn(".d-editor-input", "abc @u");
await triggerKeyEvent(".d-editor-input", "keyup", "U");
assert.deepEqual(
[...queryAll(".ac-user .username")].map((e) => e.innerText),
["user", "user2", "user_group", "foo"]
);
await fillIn(".d-editor-input", "abc @");
await triggerKeyEvent(".d-editor-input", "keyup", "@");
await fillIn(".d-editor-input", "abc @f");
await triggerKeyEvent(".d-editor-input", "keyup", "F");
assert.deepEqual(
[...queryAll(".ac-user .username")].map((e) => e.innerText),
["foo", "user_group", "user", "user2"]
);
});
});