Commit c0919c82 authored by Coung Ngo's avatar Coung Ngo

Add quick actions autocomplete to tribute autocomplete

Add this as part of the migration from jQuery-based at.js to
tribute.

https://gitlab.com/gitlab-org/gitlab/-/issues/293709
https://gitlab.com/groups/gitlab-org/-/epics/4002
parent 2a9cfa06
<script> <script>
import updateMixin from '../../mixins/update'; import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue'; import markdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
markdownField, markdownField,
}, },
mixins: [updateMixin], mixins: [glFeatureFlagsMixin(), updateMixin],
props: { props: {
formState: { formState: {
type: Object, type: Object,
...@@ -55,7 +56,7 @@ export default { ...@@ -55,7 +56,7 @@ export default {
class="note-textarea js-gfm-input js-autosize markdown-area class="note-textarea js-gfm-input js-autosize markdown-area
qa-description-textarea" qa-description-textarea"
dir="auto" dir="auto"
data-supports-quick-actions="true" :data-supports-quick-actions="!glFeatures.tributeAutocomplete"
:aria-label="__('Description')" :aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')" :placeholder="__('Write a comment or drag your files here…')"
@keydown.meta.enter="updateIssuable" @keydown.meta.enter="updateIssuable"
......
...@@ -20,6 +20,7 @@ import eventHub from '../event_hub'; ...@@ -20,6 +20,7 @@ import eventHub from '../event_hub';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
import markdownField from '~/vue_shared/components/markdown/field.vue'; import markdownField from '~/vue_shared/components/markdown/field.vue';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import noteSignedOutWidget from './note_signed_out_widget.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue'; import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
...@@ -36,7 +37,7 @@ export default { ...@@ -36,7 +37,7 @@ export default {
TimelineEntryItem, TimelineEntryItem,
GlIcon, GlIcon,
}, },
mixins: [issuableStateMixin], mixins: [glFeatureFlagsMixin(), issuableStateMixin],
props: { props: {
noteableType: { noteableType: {
type: String, type: String,
...@@ -339,7 +340,7 @@ export default { ...@@ -339,7 +340,7 @@ export default {
class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area" class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area"
data-qa-selector="comment_field" data-qa-selector="comment_field"
data-testid="comment-field" data-testid="comment-field"
data-supports-quick-actions="true" :data-supports-quick-actions="!glFeatures.tributeAutocomplete"
:aria-label="__('Description')" :aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')" :placeholder="__('Write a comment or drag your files here…')"
@keydown.up="editCurrentUserLastNote()" @keydown.up="editCurrentUserLastNote()"
......
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
import { mapGetters, mapActions, mapState } from 'vuex'; import { mapGetters, mapActions, mapState } from 'vuex';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue'; import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import markdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
...@@ -16,7 +17,7 @@ export default { ...@@ -16,7 +17,7 @@ export default {
NoteableWarning, NoteableWarning,
markdownField, markdownField,
}, },
mixins: [issuableStateMixin, resolvable], mixins: [glFeatureFlagsMixin(), issuableStateMixin, resolvable],
props: { props: {
noteBody: { noteBody: {
type: String, type: String,
...@@ -342,7 +343,7 @@ export default { ...@@ -342,7 +343,7 @@ export default {
ref="textarea" ref="textarea"
slot="textarea" slot="textarea"
v-model="updatedNoteBody" v-model="updatedNoteBody"
:data-supports-quick-actions="!isEditing" :data-supports-quick-actions="!isEditing && !glFeatures.tributeAutocomplete"
name="note[note]" name="note[note]"
class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form" class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form"
data-qa-selector="reply_field" data-qa-selector="reply_field"
......
...@@ -13,6 +13,7 @@ export const GfmAutocompleteType = { ...@@ -13,6 +13,7 @@ export const GfmAutocompleteType = {
Members: 'members', Members: 'members',
MergeRequests: 'mergeRequests', MergeRequests: 'mergeRequests',
Milestones: 'milestones', Milestones: 'milestones',
QuickActions: 'commands',
Snippets: 'snippets', Snippets: 'snippets',
}; };
...@@ -142,6 +143,34 @@ export const tributeConfig = { ...@@ -142,6 +143,34 @@ export const tributeConfig = {
}, },
}, },
[GfmAutocompleteType.QuickActions]: {
config: {
trigger: '/',
fillAttr: 'name',
lookup: value => `${value.name}${value.aliases.join()}`,
menuItemTemplate: ({ original }) => {
const aliases = original.aliases.length
? `<small>(or /${original.aliases.join(', /')})</small>`
: '';
const params = original.params.length ? `<small>${original.params.join(' ')}</small>` : '';
let description = '';
if (original.warning) {
const confidentialIcon =
original.icon === 'confidential' ? spriteIcon('eye-slash', 's16 gl-mr-2') : '';
description = `<small>${confidentialIcon}<em>${original.warning}</em></small>`;
} else if (original.description) {
description = `<small><em>${original.description}</em></small>`;
}
return `<div>/${original.name} ${aliases} ${params}</div>
<div>${description}</div>`;
},
},
},
[GfmAutocompleteType.Snippets]: { [GfmAutocompleteType.Snippets]: {
config: { config: {
trigger: '$', trigger: '$',
......
...@@ -62,16 +62,15 @@ RSpec.describe 'GFM autocomplete EE', :js do ...@@ -62,16 +62,15 @@ RSpec.describe 'GFM autocomplete EE', :js do
it 'only lists users who are currently assigned to the issue when using /unassign' do it 'only lists users who are currently assigned to the issue when using /unassign' do
note = find('#note-body') note = find('#note-body')
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
note.native.send_keys('/una') note.native.send_keys('/unassign ')
# The `/unassign` ajax response might replace the one by `@` below causing a failed test
# so we need to wait for the `/assign` ajax request to finish first
wait_for_requests
note.native.send_keys('@')
wait_for_requests
end end
find('.atwho-view li', text: '/unassign') users = find('.tribute-container ul', visible: true)
note.native.send_keys(:tab)
note.native.send_keys(:right)
wait_for_requests
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
......
...@@ -723,20 +723,15 @@ RSpec.describe 'GFM autocomplete', :js do ...@@ -723,20 +723,15 @@ RSpec.describe 'GFM autocomplete', :js do
expect(page).not_to have_selector('.tribute-container') expect(page).not_to have_selector('.tribute-container')
end end
it 'triggers autocomplete after selecting a quick action' do it 'autocompletes for quick actions' do
note = find('#note-body') note = find('#note-body')
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
note.native.send_keys('/as') note.native.send_keys('/as')
wait_for_requests
note.native.send_keys(:tab)
end end
find('.atwho-view li', text: '/assign') expect(note.value).to have_text('/assign')
note.native.send_keys(:tab)
note.native.send_keys(:right)
wait_for_requests
user_item = find('.tribute-container ul', text: user.username, visible: true)
expect(user_item).to have_content(user.username)
end end
end end
...@@ -755,15 +750,14 @@ RSpec.describe 'GFM autocomplete', :js do ...@@ -755,15 +750,14 @@ RSpec.describe 'GFM autocomplete', :js do
note = find('#note-body') note = find('#note-body')
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
note.native.send_keys('/as') note.native.send_keys('/assign ')
# The `/assign` ajax response might replace the one by `@` below causing a failed test
# so we need to wait for the `/assign` ajax request to finish first
wait_for_requests
note.native.send_keys('@')
wait_for_requests
end 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', visible: true)).not_to have_content(user.username) expect(find('.tribute-container ul', visible: true)).not_to have_content(user.username)
expect(find('.tribute-container ul', visible: true)).to have_content(unassigned_user.username) expect(find('.tribute-container ul', visible: true)).to have_content(unassigned_user.username)
end end
...@@ -775,12 +769,14 @@ RSpec.describe 'GFM autocomplete', :js do ...@@ -775,12 +769,14 @@ RSpec.describe 'GFM autocomplete', :js do
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
note.native.send_keys('/assign @user2') note.native.send_keys('/assign @user2')
note.native.send_keys(:enter) note.native.send_keys(:enter)
note.native.send_keys('/assign @') note.native.send_keys('/assign ')
note.native.send_keys(:right) # The `/assign` ajax response might replace the one by `@` below causing a failed test
# so we need to wait for the `/assign` ajax request to finish first
wait_for_requests
note.native.send_keys('@')
wait_for_requests
end end
wait_for_requests
expect(find('.tribute-container ul', visible: true)).not_to have_content(user.username) expect(find('.tribute-container ul', visible: true)).not_to have_content(user.username)
expect(find('.tribute-container ul', visible: true)).to have_content(unassigned_user.username) expect(find('.tribute-container ul', visible: true)).to have_content(unassigned_user.username)
end end
......
...@@ -10,6 +10,7 @@ RSpec.describe "User comments on issue", :js do ...@@ -10,6 +10,7 @@ RSpec.describe "User comments on issue", :js do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
stub_feature_flags(tribute_autocomplete: false)
project.add_guest(user) project.add_guest(user)
sign_in(user) sign_in(user)
......
...@@ -52,4 +52,9 @@ exports[`gfm_autocomplete/utils merge requests config shows the reference and ti ...@@ -52,4 +52,9 @@ exports[`gfm_autocomplete/utils merge requests config shows the reference and ti
exports[`gfm_autocomplete/utils milestones config shows the title in the menu item 1`] = `"13.2 &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`; exports[`gfm_autocomplete/utils milestones config shows the title in the menu item 1`] = `"13.2 &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
exports[`gfm_autocomplete/utils quick actions config shows the name, aliases, params and description in the menu item 1`] = `
"<div>/unlabel <small>(or /remove_label)</small> <small>~label1 ~\\"label 2\\"</small></div>
<div><small><em>Remove all or specific label(s)</em></small></div>"
`;
exports[`gfm_autocomplete/utils snippets config shows the id and title in the menu item 1`] = `"<small>123456</small> Snippet title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`; exports[`gfm_autocomplete/utils snippets config shows the id and title in the menu item 1`] = `"<small>123456</small> Snippet title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
...@@ -339,6 +339,36 @@ describe('gfm_autocomplete/utils', () => { ...@@ -339,6 +339,36 @@ describe('gfm_autocomplete/utils', () => {
}); });
}); });
describe('quick actions config', () => {
const quickActionsConfig = tributeConfig[GfmAutocompleteType.QuickActions].config;
const quickAction = {
name: 'unlabel',
aliases: ['remove_label'],
description: 'Remove all or specific label(s)',
warning: '',
icon: '',
params: ['~label1 ~"label 2"'],
};
it('uses / as the trigger', () => {
expect(quickActionsConfig.trigger).toBe('/');
});
it('inserts the name on autocomplete selection', () => {
expect(quickActionsConfig.fillAttr).toBe('name');
});
it('searches using both the name and aliases', () => {
expect(quickActionsConfig.lookup(quickAction)).toBe(
`${quickAction.name}${quickAction.aliases.join(', /')}`,
);
});
it('shows the name, aliases, params and description in the menu item', () => {
expect(quickActionsConfig.menuItemTemplate({ original: quickAction })).toMatchSnapshot();
});
});
describe('snippets config', () => { describe('snippets config', () => {
const snippetsConfig = tributeConfig[GfmAutocompleteType.Snippets].config; const snippetsConfig = tributeConfig[GfmAutocompleteType.Snippets].config;
const snippet = { const snippet = {
......
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