Commit e84a0fd5 authored by Coung Ngo's avatar Coung Ngo Committed by Mark Florian

Improve @mentions UX for tribute

The @mentions autocomplete menu becomes very wide when groups with
very long display names or paths are in the list. This change
reduces the width by only showing the leaf group names.
parent 304bdcec
<script> <script>
import { escape } from 'lodash'; import { escape, last } from 'lodash';
import Tribute from 'tributejs'; import Tribute from 'tributejs';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils'; import { spriteIcon } from '~/lib/utils/common_utils';
...@@ -12,6 +12,8 @@ const AutoComplete = { ...@@ -12,6 +12,8 @@ const AutoComplete = {
MergeRequests: 'mergeRequests', MergeRequests: 'mergeRequests',
}; };
const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings
function doesCurrentLineStartWith(searchString, fullText, selectionStart) { function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
const currentLineNumber = fullText.slice(0, selectionStart).split('\n').length; const currentLineNumber = fullText.slice(0, selectionStart).split('\n').length;
const currentLine = fullText.split('\n')[currentLineNumber - 1]; const currentLine = fullText.split('\n')[currentLineNumber - 1];
...@@ -79,30 +81,40 @@ const autoCompleteMap = { ...@@ -79,30 +81,40 @@ const autoCompleteMap = {
return this.members; return this.members;
}, },
menuItemTemplate({ original }) { menuItemTemplate({ original }) {
const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : ''; const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
const noAvatarClasses = `${commonClasses} gl-rounded-small
const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass} gl-display-flex gl-align-items-center gl-justify-content-center`;
gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
const avatar = original.avatar_url
const avatarTag = original.avatar_url ? `<img class="${commonClasses} gl-avatar-circle" src="${original.avatar_url}" alt="" />`
? `<img : `<div class="${noAvatarClasses}" aria-hidden="true">
src="${original.avatar_url}" ${original.username.charAt(0).toUpperCase()}</div>`;
alt="${original.username}'s avatar"
class="${avatarClasses}"/>` let displayName = original.name;
: `<div class="${avatarClasses}">${original.username.charAt(0).toUpperCase()}</div>`; let parentGroupOrUsername = `@${original.username}`;
const name = escape(original.name); if (original.type === groupType) {
const splitName = original.name.split(' / ');
displayName = splitName.pop();
parentGroupOrUsername = splitName.pop();
}
const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : ''; const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
const icon = original.mentionsDisabled const disabledMentionsIcon = original.mentionsDisabled
? spriteIcon('notifications-off', 's16 gl-vertical-align-middle gl-ml-3') ? spriteIcon('notifications-off', 's16 gl-ml-3')
: ''; : '';
return `${avatarTag} return `
${original.username} <div class="gl-display-flex gl-align-items-center">
<small class="gl-text-small gl-font-weight-normal gl-reset-color">${name}${count}</small> ${avatar}
${icon}`; <div class="gl-font-sm gl-line-height-normal gl-ml-3">
<div>${escape(displayName)}${count}</div>
<div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
</div>
${disabledMentionsIcon}
</div>
`;
}, },
}, },
[AutoComplete.MergeRequests]: { [AutoComplete.MergeRequests]: {
...@@ -139,7 +151,8 @@ export default { ...@@ -139,7 +151,8 @@ export default {
{ {
trigger: '@', trigger: '@',
fillAttr: 'username', fillAttr: 'username',
lookup: value => value.name + value.username, lookup: value =>
value.type === groupType ? last(value.name.split(' / ')) : value.name + value.username,
menuItemTemplate: autoCompleteMap[AutoComplete.Members].menuItemTemplate, menuItemTemplate: autoCompleteMap[AutoComplete.Members].menuItemTemplate,
values: this.getValues(AutoComplete.Members), values: this.getValues(AutoComplete.Members),
}, },
......
...@@ -6,7 +6,9 @@ RSpec.describe 'GFM autocomplete', :js do ...@@ -6,7 +6,9 @@ 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(:project) { create(:project) } 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) }
let_it_be(:label) { create(:label, project: project, title: 'special+') } let_it_be(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
...@@ -535,7 +537,7 @@ RSpec.describe 'GFM autocomplete', :js do ...@@ -535,7 +537,7 @@ RSpec.describe 'GFM autocomplete', :js do
expect(page).to have_selector('.tribute-container', visible: true) expect(page).to have_selector('.tribute-container', visible: true)
expect(find('.tribute-container ul', visible: true).text).to have_content(user_xss.username) expect(find('.tribute-container ul', visible: true)).to have_text(user_xss.username)
end end
it 'selects the first item for assignee dropdowns' do it 'selects the first item for assignee dropdowns' do
...@@ -563,6 +565,24 @@ RSpec.describe 'GFM autocomplete', :js do ...@@ -563,6 +565,24 @@ RSpec.describe 'GFM autocomplete', :js do
expect(find('.tribute-container ul', visible: true)).to have_content(user.name) expect(find('.tribute-container ul', visible: true)).to have_content(user.name)
end end
context 'when autocompleting for groups' do
it 'shows the group when searching for the name of the group' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@mygroup')
end
expect(find('.tribute-container ul', visible: true)).to have_text('My group')
end
it 'does not show the group when searching for the name of the parent of the group' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@ancestor')
end
expect(find('.tribute-container ul', visible: true)).not_to have_text('My group')
end
end
context 'if a selected value has special characters' do context 'if a selected value has special characters' do
it 'wraps the result in double quotes' do it 'wraps the result in double quotes' do
note = find('#note-body') note = find('#note-body')
......
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