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 {
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