diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 566b8c09ca1410415ad90369e48c125a2e18b516..77e6de5b0e21ffa6e671919ab1b76e55450ead94 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -140,6 +140,30 @@ export const highlighter = (li, query) => {
return li.replace(regexp, (str, $1, $2, $3) => `> ${$1}${$2}${$3} <`);
};
+/**
+ * Sets up subommands for quickaction for the given
+ * input with the provided command and the subcommand descriptions.
+ *
+ * @param {Object} $input input element
+ * @param {string} cmd command that triggers subcommand selection
+ * @param {Record} data object containing names of commands as keys with description and header as values
+ *
+ */
+export const setupSubcommands = ($input, cmd, data) => {
+ $input.filter('[data-supports-quick-actions="true"]').atwho({
+ // Always keep the trailing space otherwise the command won't display correctly
+ at: `/${cmd} `,
+ alias: cmd,
+ data: Object.keys(data),
+ maxLen: 100,
+ displayTpl({ name }) {
+ const { header, description } = data[name];
+
+ return `${lodashEscape(header)}${lodashEscape(description)}`;
+ },
+ });
+};
+
export const defaultAutocompleteConfig = {
emojis: true,
members: true,
@@ -318,18 +342,7 @@ class GfmAutoComplete {
},
};
- $input.filter('[data-supports-quick-actions="true"]').atwho({
- // Always keep the trailing space otherwise the command won't display correctly
- at: '/submit_review ',
- alias: 'submit_review',
- data: Object.keys(REVIEW_STATES),
- maxLen: 100,
- displayTpl({ name }) {
- const reviewState = REVIEW_STATES[name];
-
- return `${reviewState.header}${reviewState.description}`;
- },
- });
+ setupSubcommands($input, 'submit_review', REVIEW_STATES);
}
setupEmoji($input) {
@@ -977,9 +990,7 @@ class GfmAutoComplete {
} else if (dataSource) {
AjaxCache.retrieve(dataSource, true)
.then((data) => {
- if (data.some((c) => c.name === 'submit_review')) {
- this.setSubmitReviewStates($input);
- }
+ this.loadSubcommands($input, data);
this.loadData($input, at, data);
})
.catch(() => {
@@ -990,6 +1001,12 @@ class GfmAutoComplete {
}
}
+ loadSubcommands($input, data) {
+ if (data.some((c) => c.name === 'submit_review')) {
+ this.setSubmitReviewStates($input);
+ }
+ }
+
// eslint-disable-next-line max-params
loadData($input, at, data, { search } = {}) {
this.isLoadingData[at] = false;
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index dc00603c3b29d1a428f02a608617f95c8d44624e..cdf9d562c5b9abe7c0f536b1598cc65e89b20520 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -382,6 +382,7 @@ export default {
:autosave-key="autosaveKey"
:disabled="isSubmitting"
:autocomplete-data-sources="autocompleteDataSources"
+ :noteable-type="noteableType"
supports-quick-actions
@keydown.up="editCurrentUserLastNote()"
@keydown.shift.meta.enter="handleSave()"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index d8b33784624e66c68019f710b768188a74b620d5..1469f43200c7f9bce557492e6815377e8d8970a9 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -153,6 +153,9 @@ export default {
}
return '#';
},
+ noteableType() {
+ return this.getNoteableData.noteableType;
+ },
diffParams() {
if (this.diffFile) {
return {
@@ -305,7 +308,7 @@ export default {
trackSavedUsingEditor(
this.$refs.markdownEditor.isContentEditorActive,
- `${this.getNoteableData.noteableType}_note`,
+ `${this.noteableType}_note`,
);
this.$emit(
@@ -373,6 +376,7 @@ export default {
:add-spacing-classes="false"
:help-page-path="helpPagePath"
:note="discussionNote"
+ :noteable-type="noteableType"
:form-field-props="formFieldProps"
:autosave-key="autosaveKey"
:autocomplete-data-sources="autocompleteDataSources"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index 0fb156f6fe528328390dc62525ade14bcb500205..fee75832effdf851e2856e6efa1f94f8d8dc20c7 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -119,6 +119,11 @@ export default {
required: false,
default: () => ({}),
},
+ noteableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
restrictedToolBarItems: {
type: Array,
required: false,
@@ -388,6 +393,8 @@ export default {
:value="markdown"
class="note-textarea js-gfm-input markdown-area"
dir="auto"
+ :data-can-suggest="codeSuggestionsConfig.canSuggest"
+ :data-noteable-type="noteableType"
:data-supports-quick-actions="supportsQuickActions"
:data-testid="formFieldProps['data-testid'] || 'markdown-editor-form-field'"
:disabled="disabled"
diff --git a/ee/app/assets/javascripts/gfm_auto_complete.js b/ee/app/assets/javascripts/gfm_auto_complete.js
index 2019d81cc343c818482d8f454f75917f60d415d7..07c54c80e2c2c72ba7c895e1653de06b339bae66 100644
--- a/ee/app/assets/javascripts/gfm_auto_complete.js
+++ b/ee/app/assets/javascripts/gfm_auto_complete.js
@@ -1,7 +1,9 @@
import $ from 'jquery';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import '~/lib/utils/jquery_at_who';
-import GfmAutoComplete, { showAndHideHelper, escape } from '~/gfm_auto_complete';
+import GfmAutoComplete, { showAndHideHelper, escape, setupSubcommands } from '~/gfm_auto_complete';
+import { s__ } from '~/locale';
+import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants';
/**
* This is added to keep the export parity with the CE counterpart.
@@ -23,6 +25,54 @@ const EPICS_ALIAS = 'epics';
const ITERATIONS_ALIAS = 'iterations';
const VULNERABILITIES_ALIAS = 'vulnerabilities';
+export const Q_ISSUE_SUB_COMMANDS = {
+ dev: {
+ header: s__('AmazonQ|dev'),
+ description: s__('AmazonQ|Create a merge request to incorporate Amazon Q suggestions (Beta)'),
+ },
+ transform: {
+ header: s__('AmazonQ|transform'),
+ description: s__('AmazonQ|Upgrade Java Maven application to Java 17 (Beta)'),
+ },
+};
+
+export const Q_MERGE_REQUEST_SUB_COMMANDS = {
+ dev: {
+ header: s__('AmazonQ|dev'),
+ description: s__('AmazonQ|Apply changes to this merge request based on the comments (Beta)'),
+ },
+ fix: {
+ header: s__('AmazonQ|fix'),
+ description: s__('AmazonQ|Create fixes for review findings (Beta)'),
+ },
+ review: {
+ header: s__('AmazonQ|review'),
+ description: s__('AmazonQ|Review merge request for code quality and security issues (Beta)'),
+ },
+};
+
+export const Q_MERGE_REQUEST_CAN_SUGGEST_SUB_COMMANDS = {
+ test: {
+ header: s__('AmazonQ|test'),
+ description: s__(
+ 'AmazonQ|Create unit tests for selected lines of code in Java or Python files (Beta)',
+ ),
+ },
+};
+
+const getQSubCommands = ($input) => {
+ if ($input.data('noteableType') === MERGE_REQUEST_NOTEABLE_TYPE) {
+ const canSuggest = $input.data('canSuggest');
+
+ return {
+ ...Q_MERGE_REQUEST_SUB_COMMANDS,
+ ...(canSuggest ? Q_MERGE_REQUEST_CAN_SUGGEST_SUB_COMMANDS : {}),
+ };
+ }
+
+ return Q_ISSUE_SUB_COMMANDS;
+};
+
GfmAutoComplete.Iterations = {
templateFunction({ id, title }) {
return `*iteration:${id} ${escape(title)}`;
@@ -46,6 +96,14 @@ class GfmAutoCompleteEE extends GfmAutoComplete {
super.setupAtWho($input);
}
+ loadSubcommands($input, data) {
+ if (data.some((c) => c.name === 'q')) {
+ setupSubcommands($input, 'q', getQSubCommands($input));
+ }
+
+ super.loadSubcommands($input, data);
+ }
+
// eslint-disable-next-line class-methods-use-this
setupAutoCompleteEpics = ($input, defaultCallbacks) => {
$input.atwho({
diff --git a/ee/spec/frontend/gfm_auto_complete_spec.js b/ee/spec/frontend/gfm_auto_complete_spec.js
index dd107513ec3d2ae0b25590ace8bef37a7b4a4df4..f32e7b35d3f66f693c247c3459105e50af03d022 100644
--- a/ee/spec/frontend/gfm_auto_complete_spec.js
+++ b/ee/spec/frontend/gfm_auto_complete_spec.js
@@ -1,10 +1,16 @@
import $ from 'jquery';
-import GfmAutoCompleteEE from 'ee/gfm_auto_complete';
+import GfmAutoCompleteEE, {
+ Q_ISSUE_SUB_COMMANDS,
+ Q_MERGE_REQUEST_SUB_COMMANDS,
+ Q_MERGE_REQUEST_CAN_SUGGEST_SUB_COMMANDS,
+} from 'ee/gfm_auto_complete';
import { TEST_HOST } from 'helpers/test_constants';
import GfmAutoComplete from '~/gfm_auto_complete';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
import { iterationsMock } from 'ee_jest/gfm_auto_complete/mock_data';
import { ISSUABLE_EPIC } from '~/work_items/constants';
+import AjaxCache from '~/lib/utils/ajax_cache';
const mockSpriteIcons = '/icons.svg';
@@ -15,18 +21,48 @@ describe('GfmAutoCompleteEE', () => {
};
let instance;
+ let $textarea;
+
+ const triggerDropdown = (text) => {
+ $textarea
+ .trigger('focus')
+ .val($textarea.val() + text)
+ .caret('pos', -1);
+ $textarea.trigger('keyup');
+
+ jest.runOnlyPendingTimers();
+ };
+
+ const getDropdownItems = (id) => {
+ const dropdown = document.getElementById(id);
+
+ return Array.from(dropdown?.getElementsByTagName('li') || []);
+ };
+
+ const getDropdownSubcommands = (id) =>
+ getDropdownItems(id).map((x) => ({
+ name: x.querySelector('.name').textContent,
+ description: x.querySelector('.description').textContent,
+ }));
beforeEach(() => {
window.gon = { sprite_icons: mockSpriteIcons };
});
+ afterEach(() => {
+ resetHTMLFixture();
+
+ $textarea = null;
+
+ instance?.destroy();
+ instance = null;
+ });
+
it('should have enableMap', () => {
instance = new GfmAutoCompleteEE(dataSources);
instance.setup($(''));
expect(instance.enableMap).not.toBeNull();
-
- instance.destroy();
});
describe('Issues.templateFunction', () => {
@@ -59,8 +95,6 @@ describe('GfmAutoCompleteEE', () => {
});
describe('Iterations', () => {
- let $textarea;
-
beforeEach(() => {
setHTMLFixture('');
$textarea = $('textarea');
@@ -68,24 +102,6 @@ describe('GfmAutoCompleteEE', () => {
instance.setup($textarea, { iterations: true });
});
- afterEach(() => {
- instance.destroy();
- resetHTMLFixture();
- });
-
- const triggerDropdown = (text) => {
- $textarea.trigger('focus').val(text).caret('pos', -1);
- $textarea.trigger('keyup');
-
- jest.runOnlyPendingTimers();
- };
-
- const getDropdownItems = () => {
- const dropdown = document.getElementById('at-view-iterations');
- const items = dropdown.getElementsByTagName('li');
- return [].map.call(items, (item) => item.textContent.trim());
- };
-
it("should list iterations when '/iteration *iteration:' is typed", () => {
instance.cachedData['*iteration:'] = [...iterationsMock];
@@ -94,7 +110,9 @@ describe('GfmAutoCompleteEE', () => {
triggerDropdown('/iteration *iteration:');
- expect(getDropdownItems()).toEqual(expectedDropdownItems);
+ expect(getDropdownItems('at-view-iterations').map((x) => x.textContent.trim())).toEqual(
+ expectedDropdownItems,
+ );
});
describe('templateFunction', () => {
@@ -118,4 +136,73 @@ describe('GfmAutoCompleteEE', () => {
});
});
});
+
+ describe('AmazonQ quick action', () => {
+ const EXPECTATION_ISSUE_SUB_COMMANDS = [
+ {
+ name: 'dev',
+ description: Q_ISSUE_SUB_COMMANDS.dev.description,
+ },
+ {
+ name: 'transform',
+ description: Q_ISSUE_SUB_COMMANDS.transform.description,
+ },
+ ];
+ const EXPECTATION_MR_SUB_COMMANDS = [
+ {
+ name: 'dev',
+ description: Q_MERGE_REQUEST_SUB_COMMANDS.dev.description,
+ },
+ {
+ name: 'fix',
+ description: Q_MERGE_REQUEST_SUB_COMMANDS.fix.description,
+ },
+ {
+ name: 'review',
+ description: Q_MERGE_REQUEST_SUB_COMMANDS.review.description,
+ },
+ ];
+ const EXPECTATION_MR_DIFF_SUB_COMMANDS = [
+ ...EXPECTATION_MR_SUB_COMMANDS,
+ {
+ name: 'test',
+ description: Q_MERGE_REQUEST_CAN_SUGGEST_SUB_COMMANDS.test.description,
+ },
+ ];
+
+ describe.each`
+ availableCommand | textareaAttributes | expectation
+ ${'foo'} | ${''} | ${[]}
+ ${'q'} | ${''} | ${EXPECTATION_ISSUE_SUB_COMMANDS}
+ ${'q'} | ${'data-noteable-type="MergeRequest"'} | ${EXPECTATION_MR_SUB_COMMANDS}
+ ${'q'} | ${'data-noteable-type="MergeRequest" data-can-suggest="true"'} | ${EXPECTATION_MR_DIFF_SUB_COMMANDS}
+ `(
+ 'with availableCommands=$availableCommand, textareaAttributes=$textareaAttributes',
+ ({ availableCommand, textareaAttributes, expectation }) => {
+ beforeEach(() => {
+ jest
+ .spyOn(AjaxCache, 'retrieve')
+ .mockReturnValue(Promise.resolve([{ name: availableCommand }]));
+ setHTMLFixture(
+ ``,
+ );
+ instance = new GfmAutoCompleteEE({
+ commands: `${TEST_HOST}/autocomplete_sources/commands`,
+ });
+ $textarea = $('textarea');
+ instance.setup($textarea, {});
+ });
+
+ it('renders expected sub commands', async () => {
+ triggerDropdown('/');
+
+ await waitForPromises();
+
+ triggerDropdown('q ');
+
+ expect(getDropdownSubcommands('at-view-q')).toEqual(expectation);
+ });
+ },
+ );
+ });
});
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4f36ec83756b4214566071e0945ca860f7c3092f..8046d991e48214a10fb1d38b545e34c682cd1db8 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5941,6 +5941,9 @@ msgstr ""
msgid "AmazonQ|An unexpected error occurred while submitting the form. Please see the browser console log for more details."
msgstr ""
+msgid "AmazonQ|Apply changes to this merge request based on the comments (Beta)"
+msgstr ""
+
msgid "AmazonQ|Audience"
msgstr ""
@@ -5962,9 +5965,18 @@ msgstr ""
msgid "AmazonQ|Copy to clipboard"
msgstr ""
+msgid "AmazonQ|Create a merge request to incorporate Amazon Q suggestions (Beta)"
+msgstr ""
+
msgid "AmazonQ|Create an identity provider for this GitLab instance within AWS using the following values. %{helpStart}Learn more%{helpEnd}."
msgstr ""
+msgid "AmazonQ|Create fixes for review findings (Beta)"
+msgstr ""
+
+msgid "AmazonQ|Create unit tests for selected lines of code in Java or Python files (Beta)"
+msgstr ""
+
msgid "AmazonQ|Enter the IAM role's ARN."
msgstr ""
@@ -6007,6 +6019,9 @@ msgstr ""
msgid "AmazonQ|Provider type"
msgstr ""
+msgid "AmazonQ|Review merge request for code quality and security issues (Beta)"
+msgstr ""
+
msgid "AmazonQ|Save changes"
msgstr ""
@@ -6022,6 +6037,9 @@ msgstr ""
msgid "AmazonQ|Status"
msgstr ""
+msgid "AmazonQ|Upgrade Java Maven application to Java 17 (Beta)"
+msgstr ""
+
msgid "AmazonQ|Use Amazon Q to automate workflows, create a merge request from an issue, upgrade Java, and improve your code with AI-powered reviews."
msgstr ""
@@ -6037,6 +6055,21 @@ msgstr ""
msgid "AmazonQ|Within your AWS account, create an IAM role for Amazon Q and the relevant identity provider. %{helpStart}Learn how to create an IAM role%{helpEnd}."
msgstr ""
+msgid "AmazonQ|dev"
+msgstr ""
+
+msgid "AmazonQ|fix"
+msgstr ""
+
+msgid "AmazonQ|review"
+msgstr ""
+
+msgid "AmazonQ|test"
+msgstr ""
+
+msgid "AmazonQ|transform"
+msgstr ""
+
msgid "AmbiguousRef|There is a branch and a tag with the same name of %{ref}."
msgstr ""
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 71037750bce838050a7821cc6a7c8ec92e287154..0c6c35c6032dc37c2d932f3f18a313318d355cde 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -348,10 +348,13 @@ describe('issue_comment_form component', () => {
expect(findMarkdownEditor().find('textarea').attributes('disabled')).toBeDefined();
});
- it('should support quick actions', () => {
+ it('should support quick actions and other props', () => {
mountComponent({ mountFunction: mountExtended });
- expect(findMarkdownEditor().props('supportsQuickActions')).toBe(true);
+ expect(findMarkdownEditor().props()).toMatchObject({
+ supportsQuickActions: true,
+ noteableType: noteableDataMock.noteableType,
+ });
});
it('should link to markdown docs', () => {
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index ff17dbe3aba6ad3955f4e0fdc40cf541fee8d1c3..6fc9ae78ca7c7cdc51bdca9c0b91ceb727710170 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -118,6 +118,10 @@ describe('issue_note_form component', () => {
createComponentWrapper();
});
+ it('should render text area with noteable type', () => {
+ expect(textarea.attributes('data-noteable-type')).toBe(noteableDataMock.noteableType);
+ });
+
it('should render text area with placeholder', () => {
expect(textarea.attributes('placeholder')).toBe('Write a comment or drag your files hereā¦');
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index b33712f819f7915d83e0aeef1b89d04d8d397633..4b7c07591ecaf654d8ca95433edfab4454209518 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -363,13 +363,27 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
name: formFieldName,
placeholder: formFieldPlaceholder,
'aria-label': formFieldAriaLabel,
+ 'data-noteable-type': '',
'data-supports-quick-actions': 'true',
}),
);
+ expect(findTextarea().attributes('data-can-suggest')).toBeUndefined();
expect(findTextarea().element.value).toBe(value);
});
+ it('renders data on textarea for noteable type', () => {
+ buildWrapper({ propsData: { noteableType: 'MergeRequest' } });
+
+ expect(findTextarea().attributes('data-noteable-type')).toBe('MergeRequest');
+ });
+
+ it('renders data on textarea for can suggest', () => {
+ buildWrapper({ propsData: { codeSuggestionsConfig: { canSuggest: true } } });
+
+ expect(findTextarea().attributes('data-can-suggest')).toBe('true');
+ });
+
it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, async () => {
buildWrapper();