Commit 23910d26 authored by Mark Florian's avatar Mark Florian

Merge branch...

Merge branch '236003-tribute-mentions-long-usernames-make-the-autocomplete-menu-very-wide' into 'master'

Improve tribute @mentions UX

See merge request gitlab-org/gitlab!39998
parents aea67051 e84a0fd5
<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