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 { ...@@ -266,6 +266,7 @@ class GfmAutoComplete {
}, },
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${username}', insertTpl: '${atwho-at}${username}',
limit: 10,
searchKey: 'search', searchKey: 'search',
alwaysHighlightFirst: true, alwaysHighlightFirst: true,
skipSpecialCharacterTest: true, skipSpecialCharacterTest: true,
...@@ -311,6 +312,31 @@ class GfmAutoComplete { ...@@ -311,6 +312,31 @@ class GfmAutoComplete {
return data; 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 = { ...@@ -772,6 +798,14 @@ GfmAutoComplete.Members = {
title, title,
)}${availabilityStatus}</small> ${icon}</li>`; )}${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 = { GfmAutoComplete.Labels = {
templateFunction(color, title) { 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 ...@@ -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_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_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(: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(:group) { create(:group, name: 'Ancestor') }
let_it_be(:child_group) { create(:group, parent: group, name: 'My group') } let_it_be(:child_group) { create(:group, parent: group, name: 'My group') }
let_it_be(:project) { create(:project, group: child_group) } let_it_be(:project) { create(:project, group: child_group) }
...@@ -16,6 +17,7 @@ RSpec.describe 'GFM autocomplete', :js do ...@@ -16,6 +17,7 @@ RSpec.describe 'GFM autocomplete', :js do
before_all do before_all do
project.add_maintainer(user) project.add_maintainer(user)
project.add_maintainer(user_xss) project.add_maintainer(user_xss)
project.add_maintainer(user2)
end end
describe 'when tribute_autocomplete feature flag is off' do describe 'when tribute_autocomplete feature flag is off' do
...@@ -86,11 +88,7 @@ RSpec.describe 'GFM autocomplete', :js do ...@@ -86,11 +88,7 @@ RSpec.describe 'GFM autocomplete', :js do
wait_for_requests wait_for_requests
expect(page).to have_selector('.atwho-container') expect(find_highlighted_autocomplete_item).to have_content(user_xss.username)
page.within '.atwho-container #at-view-users' do
expect(find('li').text).to have_content(user_xss.username)
end
end end
it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do 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 ...@@ -190,7 +188,23 @@ RSpec.describe 'GFM autocomplete', :js do
wait_for_requests 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 end
it 'selects the first item for non-assignee dropdowns if a query is entered' do 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 ...@@ -1004,4 +1018,8 @@ RSpec.describe 'GFM autocomplete', :js do
wait_for_requests wait_for_requests
end end
def find_highlighted_autocomplete_item
find('.atwho-view li.cur', visible: true)
end
end end
...@@ -576,55 +576,95 @@ describe('GfmAutoComplete', () => { ...@@ -576,55 +576,95 @@ describe('GfmAutoComplete', () => {
}); });
}); });
describe('Members.templateFunction', () => { describe('GfmAutoComplete.Members', () => {
it('should return html with avatarTag and username', () => { const member = {
expect( name: 'Marge Simpson',
GfmAutoComplete.Members.templateFunction({ username: 'msimpson',
avatarTag: 'IMG', search: 'MargeSimpson msimpson',
username: 'my-group', };
title: '',
icon: '',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small></small> </li>');
});
it('should add icon if icon is set', () => { describe('templateFunction', () => {
expect( it('should return html with avatarTag and username', () => {
GfmAutoComplete.Members.templateFunction({ expect(
avatarTag: 'IMG', GfmAutoComplete.Members.templateFunction({
username: 'my-group', avatarTag: 'IMG',
title: '', username: 'my-group',
icon: '<i class="icon"/>', title: '',
availabilityStatus: '', icon: '',
}), availabilityStatus: '',
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>'); }),
).toBe('<li>IMG my-group <small></small> </li>');
});
it('should add icon if icon is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>');
});
it('should add escaped title if title is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: 'MyGroup+',
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>');
});
it('should add user availability status if availabilityStatus is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
availabilityStatus: '<span class="gl-text-gray-500"> (Busy)</span>',
}),
).toBe(
'<li>IMG my-group <small><span class="gl-text-gray-500"> (Busy)</span></small> <i class="icon"/></li>',
);
});
}); });
it('should add escaped title if title is set', () => { describe('nameOrUsernameStartsWith', () => {
expect( it.each`
GfmAutoComplete.Members.templateFunction({ query | result
avatarTag: 'IMG', ${'mar'} | ${true}
username: 'my-group', ${'msi'} | ${true}
title: 'MyGroup+', ${'margesimpson'} | ${true}
icon: '<i class="icon"/>', ${'msimpson'} | ${true}
availabilityStatus: '', ${'arge'} | ${false}
}), ${'rgesimp'} | ${false}
).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>'); ${'maria'} | ${false}
${'homer'} | ${false}
`('returns $result for $query', ({ query, result }) => {
expect(GfmAutoComplete.Members.nameOrUsernameStartsWith(member, query)).toBe(result);
});
}); });
it('should add user availability status if availabilityStatus is set', () => { describe('nameOrUsernameIncludes', () => {
expect( it.each`
GfmAutoComplete.Members.templateFunction({ query | result
avatarTag: 'IMG', ${'mar'} | ${true}
username: 'my-group', ${'msi'} | ${true}
title: '', ${'margesimpson'} | ${true}
icon: '<i class="icon"/>', ${'msimpson'} | ${true}
availabilityStatus: '<span class="gl-text-gray-500"> (Busy)</span>', ${'arge'} | ${true}
}), ${'rgesimp'} | ${true}
).toBe( ${'maria'} | ${false}
'<li>IMG my-group <small><span class="gl-text-gray-500"> (Busy)</span></small> <i class="icon"/></li>', ${'homer'} | ${false}
); `('returns $result for $query', ({ query, result }) => {
expect(GfmAutoComplete.Members.nameOrUsernameIncludes(member, query)).toBe(result);
});
}); });
}); });
......
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