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
...@@ -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