From 01f1788a354cfaa0593e89b70196ffc914478502 Mon Sep 17 00:00:00 2001
From: Coung Ngo <cngo@gitlab.com>
Date: Wed, 20 May 2020 15:45:29 +0100
Subject: [PATCH] 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
---
 app/assets/javascripts/gl_form.js             |   6 +-
 .../vue_shared/components/gl_mentions.vue     |  30 +-
 .../vue_shared/components/markdown/field.vue  |  31 +-
 app/controllers/projects/issues_controller.rb |   1 +
 .../issues/gfm_autocomplete_ee_spec.rb        |  64 +-
 package.json                                  |   2 +-
 spec/features/issues/gfm_autocomplete_spec.rb | 733 +++++++++++-------
 .../participants_autocomplete_spec.rb         |  23 +
 yarn.lock                                     |   8 +-
 9 files changed, 584 insertions(+), 314 deletions(-)

diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 0b7735a7db9e3..28f1e3afd3de3 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -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
diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
index a7fba5e760bb6..0ef4f1eda2747 100644
--- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
@@ -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);
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 0e05f4a462251..89844f07e7e8f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -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="#"
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 693329848de59..72fea21b52b1c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -46,6 +46,7 @@ def set_issuables_index_only_actions
 
   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
diff --git a/ee/spec/features/issues/gfm_autocomplete_ee_spec.rb b/ee/spec/features/issues/gfm_autocomplete_ee_spec.rb
index 525d290f616bb..ed0cfaa4e6e74 100644
--- a/ee/spec/features/issues/gfm_autocomplete_ee_spec.rb
+++ b/ee/spec/features/issues/gfm_autocomplete_ee_spec.rb
@@ -15,29 +15,63 @@
   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
diff --git a/package.json b/package.json
index 7b2f268145f07..d8cc0a832a09d 100644
--- a/package.json
+++ b/package.json
@@ -134,7 +134,7 @@
     "tiptap": "^1.8.0",
     "tiptap-commands": "^1.4.0",
     "tiptap-extensions": "^1.8.0",
-    "tributejs": "4.1.3",
+    "tributejs": "5.1.3",
     "unfetch": "^4.1.0",
     "url-loader": "^3.0.0",
     "uuid": "8.1.0",
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 4a7e1ba99e9ec..eca9e75fed654 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -14,444 +14,629 @@
   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
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index 5c29ac870c04e..d6f23b21d655d 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -36,12 +36,35 @@
     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
diff --git a/yarn.lock b/yarn.lock
index 50b0249d7fb1e..25620a753b9c1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"
-- 
GitLab