Commit 0bf9d165 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '118597-improve-autofill-suggestions-for-usernames' into 'master'

Improve at.js members autocomplete matching

See merge request gitlab-org/gitlab!54072
parents deddde90 978ce291
......@@ -266,6 +266,7 @@ class GfmAutoComplete {
},
// eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${username}',
limit: 10,
searchKey: 'search',
alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
......@@ -311,6 +312,31 @@ class GfmAutoComplete {
return data;
},
sorter(query, items) {
if (!query) {
return items;
}
const lowercaseQuery = query.toLowerCase();
const members = items.slice();
const { nameOrUsernameStartsWith, nameOrUsernameIncludes } = GfmAutoComplete.Members;
return members.sort((a, b) => {
if (nameOrUsernameStartsWith(a, lowercaseQuery)) {
return -1;
}
if (nameOrUsernameStartsWith(b, lowercaseQuery)) {
return 1;
}
if (nameOrUsernameIncludes(a, lowercaseQuery)) {
return -1;
}
if (nameOrUsernameIncludes(b, lowercaseQuery)) {
return 1;
}
return 0;
});
},
},
});
}
......@@ -772,6 +798,14 @@ GfmAutoComplete.Members = {
title,
)}${availabilityStatus}</small> ${icon}</li>`;
},
nameOrUsernameStartsWith(member, query) {
// `member.search` is a name:username string like `MargeSimpson msimpson`
return member.search.split(' ').some((name) => name.toLowerCase().startsWith(query));
},
nameOrUsernameIncludes(member, query) {
// `member.search` is a name:username string like `MargeSimpson msimpson`
return member.search.toLowerCase().includes(query);
},
};
GfmAutoComplete.Labels = {
templateFunction(color, title) {
......
---
title: Improve at.js members autocomplete matching
merge_request: 54072
author:
type: changed
......@@ -6,6 +6,7 @@ RSpec.describe 'GFM autocomplete', :js do
let_it_be(:user_xss_title) { 'eve <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;' }
let_it_be(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') }
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
let_it_be(:group) { create(:group, name: 'Ancestor') }
let_it_be(:child_group) { create(:group, parent: group, name: 'My group') }
let_it_be(:project) { create(:project, group: child_group) }
......@@ -16,6 +17,7 @@ RSpec.describe 'GFM autocomplete', :js do
before_all do
project.add_maintainer(user)
project.add_maintainer(user_xss)
project.add_maintainer(user2)
end
describe 'when tribute_autocomplete feature flag is off' do
......@@ -86,11 +88,7 @@ RSpec.describe 'GFM autocomplete', :js do
wait_for_requests
expect(page).to have_selector('.atwho-container')
page.within '.atwho-container #at-view-users' do
expect(find('li').text).to have_content(user_xss.username)
end
expect(find_highlighted_autocomplete_item).to have_content(user_xss.username)
end
it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
......@@ -190,7 +188,23 @@ RSpec.describe 'GFM autocomplete', :js do
wait_for_requests
expect(find('.atwho-view li', visible: true)).to have_content(user.name)
expect(find_highlighted_autocomplete_item).to have_content(user.name)
end
it 'shows names that start with the query as the top result' do
type(find('#note-body'), '@mar')
wait_for_requests
expect(find_highlighted_autocomplete_item).to have_content(user2.name)
end
it 'shows usernames that start with the query as the top result' do
type(find('#note-body'), '@msi')
wait_for_requests
expect(find_highlighted_autocomplete_item).to have_content(user2.name)
end
it 'selects the first item for non-assignee dropdowns if a query is entered' do
......@@ -1004,4 +1018,8 @@ RSpec.describe 'GFM autocomplete', :js do
wait_for_requests
end
def find_highlighted_autocomplete_item
find('.atwho-view li.cur', visible: true)
end
end
......@@ -576,7 +576,14 @@ describe('GfmAutoComplete', () => {
});
});
describe('Members.templateFunction', () => {
describe('GfmAutoComplete.Members', () => {
const member = {
name: 'Marge Simpson',
username: 'msimpson',
search: 'MargeSimpson msimpson',
};
describe('templateFunction', () => {
it('should return html with avatarTag and username', () => {
expect(
GfmAutoComplete.Members.templateFunction({
......@@ -628,6 +635,39 @@ describe('GfmAutoComplete', () => {
});
});
describe('nameOrUsernameStartsWith', () => {
it.each`
query | result
${'mar'} | ${true}
${'msi'} | ${true}
${'margesimpson'} | ${true}
${'msimpson'} | ${true}
${'arge'} | ${false}
${'rgesimp'} | ${false}
${'maria'} | ${false}
${'homer'} | ${false}
`('returns $result for $query', ({ query, result }) => {
expect(GfmAutoComplete.Members.nameOrUsernameStartsWith(member, query)).toBe(result);
});
});
describe('nameOrUsernameIncludes', () => {
it.each`
query | result
${'mar'} | ${true}
${'msi'} | ${true}
${'margesimpson'} | ${true}
${'msimpson'} | ${true}
${'arge'} | ${true}
${'rgesimp'} | ${true}
${'maria'} | ${false}
${'homer'} | ${false}
`('returns $result for $query', ({ query, result }) => {
expect(GfmAutoComplete.Members.nameOrUsernameIncludes(member, query)).toBe(result);
});
});
});
describe('labels', () => {
const dataSources = {
labels: `${TEST_HOST}/autocomplete_sources/labels`,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment