Commit 01f1788a authored by Coung Ngo's avatar Coung Ngo

Add new mentions component to Issues

As part of the refactor from at.js to tribute, the new
tribute component was added to the Issues description, note
and comment textareas
parent ecfc5247
...@@ -9,13 +9,15 @@ export default class GLForm { ...@@ -9,13 +9,15 @@ export default class GLForm {
this.form = form; this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input'); this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = { ...defaultAutocompleteConfig, ...enableGFM }; this.enableGFM = { ...defaultAutocompleteConfig, ...enableGFM };
// Disable autocomplete for keywords which do not have dataSources available // Disable autocomplete for keywords which do not have dataSources available
const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {}; const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
Object.keys(this.enableGFM).forEach(item => { Object.keys(this.enableGFM).forEach(item => {
if (item !== 'emojis') { if (item !== 'emojis' && !dataSources[item]) {
this.enableGFM[item] = Boolean(dataSources[item]); this.enableGFM[item] = false;
} }
}); });
// Before we start, we should clean up any previous data for this form // Before we start, we should clean up any previous data for this form
this.destroy(); this.destroy();
// Set up the form // Set up the form
......
...@@ -3,18 +3,19 @@ import { escape } from 'lodash'; ...@@ -3,18 +3,19 @@ import { escape } 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';
import SidebarMediator from '~/sidebar/sidebar_mediator';
/** /**
* Creates the HTML template for each row of the mentions dropdown. * Creates the HTML template for each row of the mentions dropdown.
* *
* @param original An object from the array returned from the `autocomplete_sources/members` API * @param original - An object from the array returned from the `autocomplete_sources/members` API
* @returns {string} An HTML template * @returns {string} - An HTML template
*/ */
function menuItemTemplate({ original }) { function menuItemTemplate({ original }) {
const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : ''; const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass} const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
gl-display-inline-flex gl-align-items-center gl-justify-content-center`; gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
const avatarTag = original.avatar_url const avatarTag = original.avatar_url
? `<img ? `<img
...@@ -48,6 +49,7 @@ export default { ...@@ -48,6 +49,7 @@ export default {
}, },
data() { data() {
return { return {
assignees: undefined,
members: undefined, members: undefined,
}; };
}, },
...@@ -76,19 +78,37 @@ export default { ...@@ -76,19 +78,37 @@ export default {
*/ */
getMembers(inputText, processValues) { getMembers(inputText, processValues) {
if (this.members) { if (this.members) {
processValues(this.members); processValues(this.getFilteredMembers());
} else if (this.dataSources.members) { } else if (this.dataSources.members) {
axios axios
.get(this.dataSources.members) .get(this.dataSources.members)
.then(response => { .then(response => {
this.members = response.data; this.members = response.data;
processValues(response.data); processValues(this.getFilteredMembers());
}) })
.catch(() => {}); .catch(() => {});
} else { } else {
processValues([]); processValues([]);
} }
}, },
getFilteredMembers() {
const fullText = this.$slots.default[0].elm.value;
if (!this.assignees) {
this.assignees =
SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
}
if (fullText.startsWith('/assign @')) {
return this.members.filter(member => !this.assignees.includes(member.username));
}
if (fullText.startsWith('/unassign @')) {
return this.members.filter(member => this.assignees.includes(member.username));
}
return this.members;
},
}, },
render(createElement) { render(createElement) {
return createElement('div', this.$slots.default); return createElement('div', this.$slots.default);
......
...@@ -4,21 +4,25 @@ import '~/behaviors/markdown/render_gfm'; ...@@ -4,21 +4,25 @@ import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash'; import { unescape } from 'lodash';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility'; import { stripHtml } from '~/lib/utils/text_utility';
import Flash from '../../../flash'; import Flash from '~/flash';
import GLForm from '../../../gl_form'; import GLForm from '~/gl_form';
import markdownHeader from './header.vue'; import MarkdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue'; import MarkdownToolbar from './toolbar.vue';
import icon from '../icon.vue'; import Icon from '../icon.vue';
import GlMentions from '~/vue_shared/components/gl_mentions.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
export default { export default {
components: { components: {
markdownHeader, GlMentions,
markdownToolbar, MarkdownHeader,
icon, MarkdownToolbar,
Icon,
Suggestions, Suggestions,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
isSubmitting: { isSubmitting: {
type: Boolean, type: Boolean,
...@@ -159,12 +163,10 @@ export default { ...@@ -159,12 +163,10 @@ export default {
}, },
}, },
mounted() { mounted() {
/* // GLForm class handles all the toolbar buttons
GLForm class handles all the toolbar buttons
*/
return new GLForm($(this.$refs['gl-form']), { return new GLForm($(this.$refs['gl-form']), {
emojis: this.enableAutocomplete, emojis: this.enableAutocomplete,
members: this.enableAutocomplete, members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
issues: this.enableAutocomplete, issues: this.enableAutocomplete,
mergeRequests: this.enableAutocomplete, mergeRequests: this.enableAutocomplete,
epics: this.enableAutocomplete, epics: this.enableAutocomplete,
...@@ -243,7 +245,10 @@ export default { ...@@ -243,7 +245,10 @@ export default {
/> />
<div v-show="!previewMarkdown" class="md-write-holder"> <div v-show="!previewMarkdown" class="md-write-holder">
<div class="zen-backdrop"> <div class="zen-backdrop">
<slot name="textarea"></slot> <gl-mentions v-if="glFeatures.tributeAutocomplete">
<slot name="textarea"></slot>
</gl-mentions>
<slot v-else name="textarea"></slot>
<a <a
class="zen-control zen-control-leave js-zen-leave gl-text-gray-700" class="zen-control zen-control-leave js-zen-leave gl-text-gray-700"
href="#" href="#"
......
...@@ -46,6 +46,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -46,6 +46,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group) push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
push_frontend_feature_flag(:tribute_autocomplete, @project)
end end
before_action only: :show do before_action only: :show do
......
...@@ -15,29 +15,63 @@ RSpec.describe 'GFM autocomplete EE', :js do ...@@ -15,29 +15,63 @@ RSpec.describe 'GFM autocomplete EE', :js do
context 'assignees' do context 'assignees' do
let(:issue_assignee) { create(:issue, project: project) } let(:issue_assignee) { create(:issue, project: project) }
before do describe 'when tribute_autocomplete feature flag is off' do
issue_assignee.update(assignees: [user]) before do
stub_feature_flags(tribute_autocomplete: false)
sign_in(user) issue_assignee.update(assignees: [user])
visit project_issue_path(project, issue_assignee)
wait_for_requests sign_in(user)
visit project_issue_path(project, issue_assignee)
wait_for_requests
end
it 'only lists users who are currently assigned to the issue when using /unassign' do
note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('/una')
end
find('.atwho-view li', text: '/unassign')
note.native.send_keys(:tab)
wait_for_requests
users = find('#at-view-users .atwho-view-ul')
expect(users).to have_content(user.username)
expect(users).not_to have_content(another_user.username)
end
end end
it 'only lists users who are currently assigned to the issue when using /unassign' do describe 'when tribute_autocomplete feature flag is on' do
note = find('#note-body') before do
page.within '.timeline-content-form' do stub_feature_flags(tribute_autocomplete: true)
note.native.send_keys('/una')
issue_assignee.update(assignees: [user])
sign_in(user)
visit project_issue_path(project, issue_assignee)
wait_for_requests
end end
find('.atwho-view li', text: '/unassign') it 'only lists users who are currently assigned to the issue when using /unassign' do
note.native.send_keys(:tab) note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('/una')
end
find('.atwho-view li', text: '/unassign')
note.native.send_keys(:tab)
note.native.send_keys(:right)
wait_for_requests wait_for_requests
users = find('#at-view-users .atwho-view-ul') users = find('.tribute-container ul')
expect(users).to have_content(user.username) expect(users).to have_content(user.username)
expect(users).not_to have_content(another_user.username) expect(users).not_to have_content(another_user.username)
end
end end
end end
end end
...@@ -14,444 +14,629 @@ RSpec.describe 'GFM autocomplete', :js do ...@@ -14,444 +14,629 @@ RSpec.describe 'GFM autocomplete', :js do
let(:label) { create(:label, project: project, title: 'special+') } let(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
before do describe 'when tribute_autocomplete feature flag is off' do
project.add_maintainer(user) before do
project.add_maintainer(user_xss) stub_feature_flags(tribute_autocomplete: false)
sign_in(user) project.add_maintainer(user)
visit project_issue_path(project, issue) project.add_maintainer(user_xss)
wait_for_requests sign_in(user)
end visit project_issue_path(project, issue)
it 'updates issue description with GFM reference' do wait_for_requests
find('.js-issuable-edit').click end
wait_for_requests it 'updates issue description with GFM reference' do
find('.js-issuable-edit').click
simulate_input('#issue-description', "@#{user.name[0...3]}") wait_for_requests
wait_for_requests simulate_input('#issue-description', "@#{user.name[0...3]}")
find('.atwho-view .cur').click wait_for_requests
click_button 'Save changes' find('.atwho-view .cur').click
wait_for_requests click_button 'Save changes'
expect(find('.description')).to have_content(user.to_reference) wait_for_requests
end
it 'opens autocomplete menu when field starts with text' do expect(find('.description')).to have_content(user.to_reference)
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@')
end end
expect(page).to have_selector('.atwho-container') it 'opens autocomplete menu when field starts with text' do
end page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@')
it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do end
create(:issue, project: project, title: issue_xss_title)
page.within '.timeline-content-form' do expect(page).to have_selector('.atwho-container')
find('#note-body').native.send_keys('#')
end end
wait_for_requests it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
create(:issue, project: project, title: issue_xss_title)
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('#')
end
expect(page).to have_selector('.atwho-container') wait_for_requests
page.within '.atwho-container #at-view-issues' do expect(page).to have_selector('.atwho-container')
expect(page.all('li').first.text).to include(issue_xss_title)
end
end
it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do page.within '.atwho-container #at-view-issues' do
page.within '.timeline-content-form' do expect(page.all('li').first.text).to include(issue_xss_title)
find('#note-body').native.send_keys('@ev') end
end end
wait_for_requests it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@ev')
end
expect(page).to have_selector('.atwho-container') wait_for_requests
page.within '.atwho-container #at-view-users' do expect(page).to have_selector('.atwho-container')
expect(find('li').text).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
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
create(:milestone, project: project, title: milestone_xss_title) create(:milestone, project: project, title: milestone_xss_title)
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
find('#note-body').native.send_keys('%') find('#note-body').native.send_keys('%')
end end
wait_for_requests wait_for_requests
expect(page).to have_selector('.atwho-container') expect(page).to have_selector('.atwho-container')
page.within '.atwho-container #at-view-milestones' do page.within '.atwho-container #at-view-milestones' do
expect(find('li').text).to have_content('alert milestone') expect(find('li').text).to have_content('alert milestone')
end
end end
end
it 'doesnt open autocomplete menu character is prefixed with text' do it 'doesnt open autocomplete menu character is prefixed with text' do
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
find('#note-body').native.send_keys('testing') find('#note-body').native.send_keys('testing')
find('#note-body').native.send_keys('@') find('#note-body').native.send_keys('@')
end
expect(page).not_to have_selector('.atwho-view')
end end
expect(page).not_to have_selector('.atwho-view') it 'doesnt select the first item for non-assignee dropdowns' do
end page.within '.timeline-content-form' do
find('#note-body').native.send_keys(':')
end
it 'doesnt select the first item for non-assignee dropdowns' do expect(page).to have_selector('.atwho-container')
page.within '.timeline-content-form' do
find('#note-body').native.send_keys(':') wait_for_requests
expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type')
end end
expect(page).to have_selector('.atwho-container') it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do
note = find('#note-body')
wait_for_requests # Number.
page.within '.timeline-content-form' do
note.native.send_keys('7:')
end
expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type') expect(page).not_to have_selector('.atwho-view')
end
it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do # ASCII letter.
note = find('#note-body') page.within '.timeline-content-form' do
note.set('')
note.native.send_keys('w:')
end
# Number. expect(page).not_to have_selector('.atwho-view')
page.within '.timeline-content-form' do
note.native.send_keys('7:')
end
expect(page).not_to have_selector('.atwho-view') # Non-ASCII letter.
page.within '.timeline-content-form' do
note.set('')
note.native.send_keys('Ё:')
end
# ASCII letter. expect(page).not_to have_selector('.atwho-view')
page.within '.timeline-content-form' do
note.set('')
note.native.send_keys('w:')
end end
expect(page).not_to have_selector('.atwho-view') it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@')
end
# Non-ASCII letter. expect(page).to have_selector('.atwho-container')
page.within '.timeline-content-form' do
note.set('')
note.native.send_keys('Ё:')
end
expect(page).not_to have_selector('.atwho-view') wait_for_requests
end
it 'selects the first item for assignee dropdowns' do expect(find('#at-view-users')).to have_selector('.cur:first-of-type')
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@')
end end
expect(page).to have_selector('.atwho-container') it 'includes items for assignee dropdowns with non-ASCII characters in name' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('')
simulate_input('#note-body', "@#{user.name[0...8]}")
end
wait_for_requests expect(page).to have_selector('.atwho-container')
expect(find('#at-view-users')).to have_selector('.cur:first-of-type') wait_for_requests
end
it 'includes items for assignee dropdowns with non-ASCII characters in name' do expect(find('#at-view-users')).to have_content(user.name)
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('')
simulate_input('#note-body', "@#{user.name[0...8]}")
end end
expect(page).to have_selector('.atwho-container') it 'selects the first item for non-assignee dropdowns if a query is entered' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys(':1')
end
wait_for_requests expect(page).to have_selector('.atwho-container')
expect(find('#at-view-users')).to have_content(user.name) wait_for_requests
end
it 'selects the first item for non-assignee dropdowns if a query is entered' do expect(find('#at-view-58')).to have_selector('.cur:first-of-type')
page.within '.timeline-content-form' do
find('#note-body').native.send_keys(':1')
end end
expect(page).to have_selector('.atwho-container') context 'if a selected value has special characters' do
it 'wraps the result in double quotes' do
note = find('#note-body')
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('')
simulate_input('#note-body', "~#{label.title[0]}")
end
wait_for_requests label_item = find('.atwho-view li', text: label.title)
expect(find('#at-view-58')).to have_selector('.cur:first-of-type') expect_to_wrap(true, label_item, note, label.title)
end end
context 'if a selected value has special characters' do it "shows dropdown after a new line" do
it 'wraps the result in double quotes' do note = find('#note-body')
note = find('#note-body') page.within '.timeline-content-form' do
page.within '.timeline-content-form' do note.native.send_keys('test')
find('#note-body').native.send_keys('') note.native.send_keys(:enter)
simulate_input('#note-body', "~#{label.title[0]}") note.native.send_keys(:enter)
note.native.send_keys('@')
end
expect(page).to have_selector('.atwho-container')
end end
label_item = find('.atwho-view li', text: label.title) it "does not show dropdown when preceded with a special character" do
note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys("@")
end
expect_to_wrap(true, label_item, note, label.title) expect(page).to have_selector('.atwho-container')
end
it "shows dropdown after a new line" do page.within '.timeline-content-form' do
note = find('#note-body') note.native.send_keys("@")
page.within '.timeline-content-form' do end
note.native.send_keys('test')
note.native.send_keys(:enter) expect(page).to have_selector('.atwho-container', visible: false)
note.native.send_keys(:enter)
note.native.send_keys('@')
end end
expect(page).to have_selector('.atwho-container') it "does not throw an error if no labels exist" do
end note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('~')
end
it "does not show dropdown when preceded with a special character" do expect(page).to have_selector('.atwho-container', visible: false)
note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys("@")
end end
expect(page).to have_selector('.atwho-container') it 'doesn\'t wrap for assignee values' do
note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys("@#{user.username[0]}")
end
page.within '.timeline-content-form' do user_item = find('.atwho-view li', text: user.username)
note.native.send_keys("@")
expect_to_wrap(false, user_item, note, user.username)
end end
expect(page).to have_selector('.atwho-container', visible: false) it 'doesn\'t wrap for emoji values' do
end note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys(":cartwheel_")
end
it "does not throw an error if no labels exist" do emoji_item = find('.atwho-view li', text: 'cartwheel_tone1')
note = find('#note-body')
page.within '.timeline-content-form' do expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
note.native.send_keys('~')
end end
expect(page).to have_selector('.atwho-container', visible: false) it 'doesn\'t open autocomplete after non-word character' do
end page.within '.timeline-content-form' do
find('#note-body').native.send_keys("@#{user.username[0..2]}!")
end
it 'doesn\'t wrap for assignee values' do expect(page).not_to have_selector('.atwho-view')
note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys("@#{user.username[0]}")
end end
user_item = find('.atwho-view li', text: user.username) it 'doesn\'t open autocomplete if there is no space before' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys("hello:#{user.username[0..2]}")
end
expect(page).not_to have_selector('.atwho-view')
end
expect_to_wrap(false, user_item, note, user.username) it 'triggers autocomplete after selecting a quick action' do
note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('/as')
end
find('.atwho-view li', text: '/assign')
note.native.send_keys(:tab)
user_item = find('.atwho-view li', text: user.username)
expect(user_item).to have_content(user.username)
end
end end
it 'doesn\'t wrap for emoji values' do context 'assignees' do
note = find('#note-body') let(:issue_assignee) { create(:issue, project: project) }
page.within '.timeline-content-form' do let(:unassigned_user) { create(:user) }
note.native.send_keys(":cartwheel_")
before do
issue_assignee.update(assignees: [user])
project.add_maintainer(unassigned_user)
end end
emoji_item = find('.atwho-view li', text: 'cartwheel_tone1') it 'lists users who are currently not assigned to the issue when using /assign' do
visit project_issue_path(project, issue_assignee)
expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1') note = find('#note-body')
end page.within '.timeline-content-form' do
note.native.send_keys('/as')
end
it 'doesn\'t open autocomplete after non-word character' do find('.atwho-view li', text: '/assign')
page.within '.timeline-content-form' do note.native.send_keys(:tab)
find('#note-body').native.send_keys("@#{user.username[0..2]}!")
wait_for_requests
expect(find('#at-view-users .atwho-view-ul')).not_to have_content(user.username)
expect(find('#at-view-users .atwho-view-ul')).to have_content(unassigned_user.username)
end end
expect(page).not_to have_selector('.atwho-view') it 'shows dropdown on new issue form' do
visit new_project_issue_path(project)
textarea = find('#issue_description')
textarea.native.send_keys('/ass')
find('.atwho-view li', text: '/assign')
textarea.native.send_keys(:tab)
expect(find('#at-view-users .atwho-view-ul')).to have_content(unassigned_user.username)
expect(find('#at-view-users .atwho-view-ul')).to have_content(user.username)
end
end end
it 'doesn\'t open autocomplete if there is no space before' do context 'labels' do
page.within '.timeline-content-form' do it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
find('#note-body').native.send_keys("hello:#{user.username[0..2]}") create(:label, project: project, title: label_xss_title)
note = find('#note-body')
# It should show all the labels on "~".
type(note, '~')
wait_for_requests
page.within '.atwho-container #at-view-labels' do
expect(find('.atwho-view-ul').text).to have_content('alert label')
end
end end
expect(page).not_to have_selector('.atwho-view') it 'allows colons when autocompleting scoped labels' do
end create(:label, project: project, title: 'scoped:label')
it 'triggers autocomplete after selecting a quick action' do note = find('#note-body')
note = find('#note-body') type(note, '~scoped:')
page.within '.timeline-content-form' do
note.native.send_keys('/as') wait_for_requests
page.within '.atwho-container #at-view-labels' do
expect(find('.atwho-view-ul').text).to have_content('scoped:label')
end
end end
find('.atwho-view li', text: '/assign') it 'allows colons when autocompleting scoped labels with double colons' do
note.native.send_keys(:tab) create(:label, project: project, title: 'scoped::label')
user_item = find('.atwho-view li', text: user.username) note = find('#note-body')
expect(user_item).to have_content(user.username) type(note, '~scoped::')
end
end
context 'assignees' do wait_for_requests
let(:issue_assignee) { create(:issue, project: project) }
let(:unassigned_user) { create(:user) }
before do page.within '.atwho-container #at-view-labels' do
issue_assignee.update(assignees: [user]) expect(find('.atwho-view-ul').text).to have_content('scoped::label')
end
end
it 'allows spaces when autocompleting multi-word labels' do
create(:label, project: project, title: 'Accepting merge requests')
note = find('#note-body')
type(note, '~Accepting merge')
wait_for_requests
project.add_maintainer(unassigned_user) page.within '.atwho-container #at-view-labels' do
expect(find('.atwho-view-ul').text).to have_content('Accepting merge requests')
end
end
it 'only autocompletes the latest label' do
create(:label, project: project, title: 'Accepting merge requests')
create(:label, project: project, title: 'Accepting job applicants')
note = find('#note-body')
type(note, '~Accepting merge requests foo bar ~Accepting job')
wait_for_requests
page.within '.atwho-container #at-view-labels' do
expect(find('.atwho-view-ul').text).to have_content('Accepting job applicants')
end
end
it 'does not autocomplete labels if no tilde is typed' do
create(:label, project: project, title: 'Accepting merge requests')
note = find('#note-body')
type(note, 'Accepting merge')
wait_for_requests
expect(page).not_to have_css('.atwho-container #at-view-labels')
end
end end
it 'lists users who are currently not assigned to the issue when using /assign' do shared_examples 'autocomplete suggestions' do
visit project_issue_path(project, issue_assignee) it 'suggests objects correctly' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys(object.class.reference_prefix)
end
note = find('#note-body') page.within '.atwho-container' do
page.within '.timeline-content-form' do expect(page).to have_content(object.title)
note.native.send_keys('/as')
find('ul li').click
end
expect(find('.new-note #note-body').value).to include(expected_body)
end end
end
find('.atwho-view li', text: '/assign') context 'issues' do
note.native.send_keys(:tab) let(:object) { issue }
let(:expected_body) { object.to_reference }
wait_for_requests it_behaves_like 'autocomplete suggestions'
end
expect(find('#at-view-users .atwho-view-ul')).not_to have_content(user.username) context 'merge requests' do
expect(find('#at-view-users .atwho-view-ul')).to have_content(unassigned_user.username) let(:object) { create(:merge_request, source_project: project) }
let(:expected_body) { object.to_reference }
it_behaves_like 'autocomplete suggestions'
end end
it 'shows dropdown on new issue form' do context 'project snippets' do
visit new_project_issue_path(project) let!(:object) { create(:project_snippet, project: project, title: 'code snippet') }
let(:expected_body) { object.to_reference }
it_behaves_like 'autocomplete suggestions'
end
textarea = find('#issue_description') context 'label' do
textarea.native.send_keys('/ass') let!(:object) { label }
find('.atwho-view li', text: '/assign') let(:expected_body) { object.title }
textarea.native.send_keys(:tab)
expect(find('#at-view-users .atwho-view-ul')).to have_content(unassigned_user.username) it_behaves_like 'autocomplete suggestions'
expect(find('#at-view-users .atwho-view-ul')).to have_content(user.username) end
context 'milestone' do
let!(:object) { create(:milestone, project: project) }
let(:expected_body) { object.to_reference }
it_behaves_like 'autocomplete suggestions'
end end
end end
context 'labels' do describe 'when tribute_autocomplete feature flag is on' do
it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do before do
create(:label, project: project, title: label_xss_title) stub_feature_flags(tribute_autocomplete: true)
note = find('#note-body') project.add_maintainer(user)
project.add_maintainer(user_xss)
# It should show all the labels on "~". sign_in(user)
type(note, '~') visit project_issue_path(project, issue)
wait_for_requests wait_for_requests
page.within '.atwho-container #at-view-labels' do
expect(find('.atwho-view-ul').text).to have_content('alert label')
end
end end
it 'allows colons when autocompleting scoped labels' do it 'updates issue description with GFM reference' do
create(:label, project: project, title: 'scoped:label') find('.js-issuable-edit').click
note = find('#note-body')
type(note, '~scoped:')
wait_for_requests wait_for_requests
page.within '.atwho-container #at-view-labels' do simulate_input('#issue-description', "@#{user.name[0...3]}")
expect(find('.atwho-view-ul').text).to have_content('scoped:label')
end
end
it 'allows colons when autocompleting scoped labels with double colons' do wait_for_requests
create(:label, project: project, title: 'scoped::label')
note = find('#note-body') find('.tribute-container .highlight').click
type(note, '~scoped::')
click_button 'Save changes'
wait_for_requests wait_for_requests
page.within '.atwho-container #at-view-labels' do expect(find('.description')).to have_content(user.to_reference)
expect(find('.atwho-view-ul').text).to have_content('scoped::label')
end
end end
it 'allows spaces when autocompleting multi-word labels' do it 'opens autocomplete menu when field starts with text' do
create(:label, project: project, title: 'Accepting merge requests') page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@')
end
note = find('#note-body') expect(page).to have_selector('.tribute-container')
type(note, '~Accepting merge') end
it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@ev')
end
wait_for_requests wait_for_requests
page.within '.atwho-container #at-view-labels' do expect(page).to have_selector('.tribute-container')
expect(find('.atwho-view-ul').text).to have_content('Accepting merge requests')
page.within '.tribute-container ul' do
expect(find('li').text).to have_content(user_xss.username)
end end
end end
it 'only autocompletes the latest label' do it 'doesnt open autocomplete menu character is prefixed with text' do
create(:label, project: project, title: 'Accepting merge requests') page.within '.timeline-content-form' do
create(:label, project: project, title: 'Accepting job applicants') find('#note-body').native.send_keys('testing')
find('#note-body').native.send_keys('@')
end
note = find('#note-body') expect(page).not_to have_selector('.tribute-container')
type(note, '~Accepting merge requests foo bar ~Accepting job') end
it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@')
end
expect(page).to have_selector('.tribute-container')
wait_for_requests wait_for_requests
page.within '.atwho-container #at-view-labels' do expect(find('.tribute-container ul')).to have_selector('.highlight:first-of-type')
expect(find('.atwho-view-ul').text).to have_content('Accepting job applicants')
end
end end
it 'does not autocomplete labels if no tilde is typed' do it 'includes items for assignee dropdowns with non-ASCII characters in name' do
create(:label, project: project, title: 'Accepting merge requests') page.within '.timeline-content-form' do
find('#note-body').native.send_keys('')
simulate_input('#note-body', "@#{user.name[0...8]}")
end
note = find('#note-body') expect(page).to have_selector('.tribute-container')
type(note, 'Accepting merge')
wait_for_requests wait_for_requests
expect(page).not_to have_css('.atwho-container #at-view-labels') expect(find('.tribute-container')).to have_content(user.name)
end end
end
shared_examples 'autocomplete suggestions' do context 'if a selected value has special characters' do
it 'suggests objects correctly' do it "shows dropdown after a new line" do
page.within '.timeline-content-form' do note = find('#note-body')
find('#note-body').native.send_keys(object.class.reference_prefix) page.within '.timeline-content-form' do
note.native.send_keys('test')
note.native.send_keys(:enter)
note.native.send_keys(:enter)
note.native.send_keys('@')
end
expect(page).to have_selector('.tribute-container')
end end
page.within '.atwho-container' do it "does not show dropdown when preceded with a special character" do
expect(page).to have_content(object.title) note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys("@")
end
expect(page).to have_selector('.tribute-container')
find('ul li').click page.within '.timeline-content-form' do
note.native.send_keys("@")
end
expect(page).to have_selector('.tribute-container', visible: false)
end end
expect(find('.new-note #note-body').value).to include(expected_body) it 'doesn\'t wrap for assignee values' do
end note = find('#note-body')
end page.within '.timeline-content-form' do
note.native.send_keys("@#{user.username[0]}")
end
context 'issues' do user_item = find('.tribute-container li', text: user.username)
let(:object) { issue }
let(:expected_body) { object.to_reference }
it_behaves_like 'autocomplete suggestions' expect_to_wrap(false, user_item, note, user.username)
end end
context 'merge requests' do it 'doesn\'t open autocomplete after non-word character' do
let(:object) { create(:merge_request, source_project: project) } page.within '.timeline-content-form' do
let(:expected_body) { object.to_reference } find('#note-body').native.send_keys("@#{user.username[0..2]}!")
end
it_behaves_like 'autocomplete suggestions' expect(page).not_to have_selector('.tribute-container')
end end
context 'project snippets' do it 'triggers autocomplete after selecting a quick action' do
let!(:object) { create(:project_snippet, project: project, title: 'code snippet') } note = find('#note-body')
let(:expected_body) { object.to_reference } page.within '.timeline-content-form' do
note.native.send_keys('/as')
end
it_behaves_like 'autocomplete suggestions' find('.atwho-view li', text: '/assign')
end note.native.send_keys(:tab)
note.native.send_keys(:right)
context 'label' do wait_for_requests
let!(:object) { label }
let(:expected_body) { object.title }
it_behaves_like 'autocomplete suggestions' user_item = find('.tribute-container li', text: user.username)
end expect(user_item).to have_content(user.username)
end
end
context 'milestone' do context 'assignees' do
let!(:object) { create(:milestone, project: project) } let(:issue_assignee) { create(:issue, project: project) }
let(:expected_body) { object.to_reference } let(:unassigned_user) { create(:user) }
before do
issue_assignee.update(assignees: [user])
project.add_maintainer(unassigned_user)
end
it_behaves_like 'autocomplete suggestions' it 'lists users who are currently not assigned to the issue when using /assign' do
visit project_issue_path(project, issue_assignee)
note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('/as')
end
find('.atwho-view li', text: '/assign')
note.native.send_keys(:tab)
note.native.send_keys(:right)
wait_for_requests
expect(find('.tribute-container ul')).not_to have_content(user.username)
expect(find('.tribute-container ul')).to have_content(unassigned_user.username)
end
end
end end
private private
......
...@@ -36,12 +36,35 @@ RSpec.describe 'Member autocomplete', :js do ...@@ -36,12 +36,35 @@ RSpec.describe 'Member autocomplete', :js do
let(:noteable) { create(:issue, author: author, project: project) } let(:noteable) { create(:issue, author: author, project: project) }
before do before do
stub_feature_flags(tribute_autocomplete: false)
visit project_issue_path(project, noteable) visit project_issue_path(project, noteable)
end end
include_examples "open suggestions when typing @", 'issue' include_examples "open suggestions when typing @", 'issue'
end end
describe 'when tribute_autocomplete feature flag is on' do
context 'adding a new note on a Issue' do
let(:noteable) { create(:issue, author: author, project: project) }
before do
stub_feature_flags(tribute_autocomplete: true)
visit project_issue_path(project, noteable)
page.within('.new-note') do
find('#note-body').send_keys('@')
end
end
it 'suggests noteable author and note author' do
page.within('.tribute-container', visible: true) do
expect(page).to have_content(author.username)
expect(page).to have_content(note.author.username)
end
end
end
end
context 'adding a new note on a Merge Request' do context 'adding a new note on a Merge Request' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
let(:noteable) do let(:noteable) do
......
...@@ -11507,10 +11507,10 @@ tr46@^2.0.2: ...@@ -11507,10 +11507,10 @@ tr46@^2.0.2:
dependencies: dependencies:
punycode "^2.1.1" punycode "^2.1.1"
tributejs@4.1.3: tributejs@5.1.3:
version "4.1.3" version "5.1.3"
resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-4.1.3.tgz#2e1be7d9a1e403ed4c394f91d859812267e4691c" resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.3.tgz#980600fc72865be5868893078b4bfde721129eae"
integrity sha512-+VUqyi8p7tCdaqCINCWHf95E2hJFMIML180BhplTpXNooz3E2r96AONXI9qO2Ru6Ugp7MsMPJjB+rnBq+hAmzA== integrity sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==
trim-newlines@^1.0.0: trim-newlines@^1.0.0:
version "1.0.0" version "1.0.0"
......
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