Commit d48bce3a authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '207134-add-new-mentions-component-to-issues' into 'master'

Add new mentions component to Issues textareas

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