Commit 18c4fafb authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'ph/326168/batchSuggestionsCommitMessage' into 'master'

Allow users to add a commit message with batch suggestions

See merge request gitlab-org/gitlab!66247
parents b9bcd3ad fea4041b
...@@ -499,10 +499,10 @@ const Api = { ...@@ -499,10 +499,10 @@ const Api = {
return axios.put(url, params); return axios.put(url, params);
}, },
applySuggestionBatch(ids) { applySuggestionBatch(ids, message) {
const url = Api.buildUrl(Api.applySuggestionBatchPath); const url = Api.buildUrl(Api.applySuggestionBatchPath);
return axios.put(url, { ids }); return axios.put(url, { ids, commit_message: message });
}, },
commitPipelines(projectId, sha) { commitPipelines(projectId, sha) {
......
...@@ -51,7 +51,7 @@ export default { ...@@ -51,7 +51,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters(['getDiscussion', 'suggestionsCount']), ...mapGetters(['getDiscussion', 'suggestionsCount', 'getSuggestionsFilePaths']),
...mapGetters('diffs', ['suggestionCommitMessage']), ...mapGetters('diffs', ['suggestionCommitMessage']),
discussion() { discussion() {
if (!this.note.isDraft) return {}; if (!this.note.isDraft) return {};
...@@ -74,9 +74,10 @@ export default { ...@@ -74,9 +74,10 @@ export default {
// Please see this issue comment for why these // Please see this issue comment for why these
// are hard-coded to 1: // are hard-coded to 1:
// https://gitlab.com/gitlab-org/gitlab/-/issues/291027#note_468308022 // https://gitlab.com/gitlab-org/gitlab/-/issues/291027#note_468308022
const suggestionsCount = 1; const suggestionsCount = this.batchSuggestionsInfo.length || 1;
const filesCount = 1; const batchFilePaths = this.getSuggestionsFilePaths();
const filePaths = this.file ? [this.file.file_path] : []; const filePaths = batchFilePaths.length ? batchFilePaths : [this.file.file_path];
const filesCount = filePaths.length;
const suggestion = this.suggestionCommitMessage({ const suggestion = this.suggestionCommitMessage({
file_paths: filePaths.join(', '), file_paths: filePaths.join(', '),
suggestions_count: suggestionsCount, suggestions_count: suggestionsCount,
...@@ -131,8 +132,8 @@ export default { ...@@ -131,8 +132,8 @@ export default {
message, message,
}).then(callback); }).then(callback);
}, },
applySuggestionBatch({ flashContainer }) { applySuggestionBatch({ message, flashContainer }) {
return this.submitSuggestionBatch({ flashContainer }); return this.submitSuggestionBatch({ message, flashContainer });
}, },
addSuggestionToBatch(suggestionId) { addSuggestionToBatch(suggestionId) {
const { discussion_id: discussionId, id: noteId } = this.note; const { discussion_id: discussionId, id: noteId } = this.note;
......
...@@ -631,7 +631,7 @@ export const submitSuggestion = ( ...@@ -631,7 +631,7 @@ export const submitSuggestion = (
}); });
}; };
export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContainer }) => { export const submitSuggestionBatch = ({ commit, dispatch, state }, { message, flashContainer }) => {
const suggestionIds = state.batchSuggestionsInfo.map(({ suggestionId }) => suggestionId); const suggestionIds = state.batchSuggestionsInfo.map(({ suggestionId }) => suggestionId);
const resolveAllDiscussions = () => const resolveAllDiscussions = () =>
...@@ -644,7 +644,7 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai ...@@ -644,7 +644,7 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai
commit(types.SET_RESOLVING_DISCUSSION, true); commit(types.SET_RESOLVING_DISCUSSION, true);
dispatch('stopPolling'); dispatch('stopPolling');
return Api.applySuggestionBatch(suggestionIds) return Api.applySuggestionBatch(suggestionIds, message)
.then(() => Promise.all(resolveAllDiscussions())) .then(() => Promise.all(resolveAllDiscussions()))
.then(() => commit(types.CLEAR_SUGGESTION_BATCH)) .then(() => commit(types.CLEAR_SUGGESTION_BATCH))
.catch((err) => { .catch((err) => {
......
...@@ -283,3 +283,14 @@ export const suggestionsCount = (state, getters) => ...@@ -283,3 +283,14 @@ export const suggestionsCount = (state, getters) =>
export const hasDrafts = (state, getters, rootState, rootGetters) => export const hasDrafts = (state, getters, rootState, rootGetters) =>
Boolean(rootGetters['batchComments/hasDrafts']); Boolean(rootGetters['batchComments/hasDrafts']);
export const getSuggestionsFilePaths = (state) => () =>
state.batchSuggestionsInfo.reduce((acc, suggestion) => {
const discussion = state.discussions.find((d) => d.id === suggestion.discussionId);
if (acc.indexOf(discussion?.diff_file?.file_path) === -1) {
acc.push(discussion.diff_file.file_path);
}
return acc;
}, []);
<script> <script>
import { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton } from '@gitlab/ui'; import { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton } from '@gitlab/ui';
import { __, n__ } from '~/locale';
export default { export default {
components: { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton }, components: { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton },
...@@ -13,12 +14,26 @@ export default { ...@@ -13,12 +14,26 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
batchSuggestionsCount: {
type: Number,
required: false,
default: 0,
},
}, },
data() { data() {
return { return {
message: null, message: null,
}; };
}, },
computed: {
dropdownText() {
if (this.batchSuggestionsCount <= 1) {
return __('Apply suggestion');
}
return n__('Apply %d suggestion', 'Apply %d suggestions', this.batchSuggestionsCount);
},
},
methods: { methods: {
onApply() { onApply() {
this.$emit('apply', this.message); this.$emit('apply', this.message);
...@@ -29,10 +44,11 @@ export default { ...@@ -29,10 +44,11 @@ export default {
<template> <template>
<gl-dropdown <gl-dropdown
:text="__('Apply suggestion')" :text="dropdownText"
:disabled="disabled" :disabled="disabled"
boundary="window" boundary="window"
right right
lazy
menu-class="gl-w-full!" menu-class="gl-w-full!"
data-qa-selector="apply_suggestion_dropdown" data-qa-selector="apply_suggestion_dropdown"
@shown="$refs.commitMessage.$el.focus()" @shown="$refs.commitMessage.$el.focus()"
......
...@@ -54,8 +54,8 @@ export default { ...@@ -54,8 +54,8 @@ export default {
applySuggestion(callback, message) { applySuggestion(callback, message) {
this.$emit('apply', { suggestionId: this.suggestion.id, callback, message }); this.$emit('apply', { suggestionId: this.suggestion.id, callback, message });
}, },
applySuggestionBatch() { applySuggestionBatch(message) {
this.$emit('applyBatch'); this.$emit('applyBatch', message);
}, },
addSuggestionToBatch() { addSuggestionToBatch() {
this.$emit('addToBatch', this.suggestion.id); this.$emit('addToBatch', this.suggestion.id);
......
...@@ -58,12 +58,19 @@ export default { ...@@ -58,12 +58,19 @@ export default {
isApplyingSingle: false, isApplyingSingle: false,
}; };
}, },
computed: { computed: {
isApplying() { isApplying() {
return this.isApplyingSingle || this.isApplyingBatch; return this.isApplyingSingle || this.isApplyingBatch;
}, },
tooltipMessage() { tooltipMessage() {
return this.canApply ? __('This also resolves this thread') : this.inapplicableReason; if (!this.canApply) {
return this.inapplicableReason;
}
return this.batchSuggestionsCount > 1
? __('This also resolves all related threads')
: __('This also resolves this thread');
}, },
isDisableButton() { isDisableButton() {
return this.isApplying || !this.canApply; return this.isApplying || !this.canApply;
...@@ -72,13 +79,30 @@ export default { ...@@ -72,13 +79,30 @@ export default {
if (this.isApplyingSingle || this.batchSuggestionsCount < 2) { if (this.isApplyingSingle || this.batchSuggestionsCount < 2) {
return __('Applying suggestion...'); return __('Applying suggestion...');
} }
return __('Applying suggestions...'); return __('Applying suggestions...');
}, },
isLoggedIn() { isLoggedIn() {
return isLoggedIn(); return isLoggedIn();
}, },
showApplySuggestion() {
if (!this.isLoggedIn) return false;
if (this.batchSuggestionsCount >= 1 && !this.isBatched) {
return false;
}
return true;
},
}, },
methods: { methods: {
apply(message) {
if (this.batchSuggestionsCount > 1) {
this.applySuggestionBatch(message);
} else {
this.applySuggestion(message);
}
},
applySuggestion(message) { applySuggestion(message) {
if (!this.canApply) return; if (!this.canApply) return;
this.isApplyingSingle = true; this.isApplyingSingle = true;
...@@ -88,9 +112,9 @@ export default { ...@@ -88,9 +112,9 @@ export default {
applySuggestionCallback() { applySuggestionCallback() {
this.isApplyingSingle = false; this.isApplyingSingle = false;
}, },
applySuggestionBatch() { applySuggestionBatch(message) {
if (!this.canApply) return; if (!this.canApply) return;
this.$emit('applyBatch'); this.$emit('applyBatch', message);
}, },
addSuggestionToBatch() { addSuggestionToBatch() {
this.$emit('addToBatch'); this.$emit('addToBatch');
...@@ -115,45 +139,35 @@ export default { ...@@ -115,45 +139,35 @@ export default {
<gl-loading-icon size="sm" class="d-flex-center mr-2" /> <gl-loading-icon size="sm" class="d-flex-center mr-2" />
<span>{{ applyingSuggestionsMessage }}</span> <span>{{ applyingSuggestionsMessage }}</span>
</div> </div>
<div v-else-if="canApply && isBatched" class="d-flex align-items-center"> <div v-else-if="canApply" class="d-flex align-items-center">
<gl-button <div v-if="isBatched">
class="btn-inverted js-remove-from-batch-btn btn-grouped" <gl-button
:disabled="isApplying" class="btn-inverted js-remove-from-batch-btn btn-grouped"
@click="removeSuggestionFromBatch" :disabled="isApplying"
> @click="removeSuggestionFromBatch"
{{ __('Remove from batch') }} >
</gl-button> {{ __('Remove from batch') }}
<gl-button </gl-button>
v-gl-tooltip.viewport="__('This also resolves all related threads')" </div>
class="btn-inverted js-apply-batch-btn btn-grouped" <div v-else>
data-qa-selector="apply_suggestions_batch_button" <gl-button
:disabled="isApplying" v-if="!isDisableButton && suggestionsCount > 1"
variant="success" class="btn-inverted js-add-to-batch-btn btn-grouped"
@click="applySuggestionBatch" data-qa-selector="add_suggestion_batch_button"
> :disabled="isDisableButton"
{{ __('Apply suggestions') }} @click="addSuggestionToBatch"
<span class="badge badge-pill badge-pill-success"> >
{{ batchSuggestionsCount }} {{ __('Add suggestion to batch') }}
</span> </gl-button>
</gl-button> </div>
</div>
<div v-else class="d-flex align-items-center">
<gl-button
v-if="suggestionsCount > 1 && !isDisableButton"
class="btn-inverted js-add-to-batch-btn btn-grouped"
data-qa-selector="add_suggestion_batch_button"
:disabled="isDisableButton"
@click="addSuggestionToBatch"
>
{{ __('Add suggestion to batch') }}
</gl-button>
<apply-suggestion <apply-suggestion
v-if="isLoggedIn" v-if="showApplySuggestion"
v-gl-tooltip.viewport="tooltipMessage" v-gl-tooltip.viewport="tooltipMessage"
:disabled="isDisableButton" :disabled="isDisableButton"
:default-commit-message="defaultCommitMessage" :default-commit-message="defaultCommitMessage"
:batch-suggestions-count="batchSuggestionsCount"
class="gl-ml-3" class="gl-ml-3"
@apply="applySuggestion" @apply="apply"
/> />
</div> </div>
</div> </div>
......
...@@ -68,6 +68,10 @@ export default { ...@@ -68,6 +68,10 @@ export default {
if (this.suggestionsWatch) { if (this.suggestionsWatch) {
this.suggestionsWatch(); this.suggestionsWatch();
} }
if (this.defaultCommitMessageWatch) {
this.defaultCommitMessageWatch();
}
}, },
methods: { methods: {
renderSuggestions() { renderSuggestions() {
...@@ -123,12 +127,16 @@ export default { ...@@ -123,12 +127,16 @@ export default {
suggestionDiff.suggestionsCount = this.suggestionsCount; suggestionDiff.suggestionsCount = this.suggestionsCount;
}); });
this.defaultCommitMessageWatch = this.$watch('defaultCommitMessage', () => {
suggestionDiff.defaultCommitMessage = this.defaultCommitMessage;
});
suggestionDiff.$on('apply', ({ suggestionId, callback, message }) => { suggestionDiff.$on('apply', ({ suggestionId, callback, message }) => {
this.$emit('apply', { suggestionId, callback, flashContainer: this.$el, message }); this.$emit('apply', { suggestionId, callback, flashContainer: this.$el, message });
}); });
suggestionDiff.$on('applyBatch', () => { suggestionDiff.$on('applyBatch', (message) => {
this.$emit('applyBatch', { flashContainer: this.$el }); this.$emit('applyBatch', { message, flashContainer: this.$el });
}); });
suggestionDiff.$on('addToBatch', (suggestionId) => { suggestionDiff.$on('addToBatch', (suggestionId) => {
......
...@@ -153,6 +153,10 @@ ...@@ -153,6 +153,10 @@
vertical-align: middle; vertical-align: middle;
margin-bottom: 3px; margin-bottom: 3px;
} }
.dropdown-chevron {
margin-bottom: 0;
}
} }
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
......
...@@ -4110,6 +4110,11 @@ msgstr "" ...@@ -4110,6 +4110,11 @@ msgstr ""
msgid "Apply" msgid "Apply"
msgstr "" msgstr ""
msgid "Apply %d suggestion"
msgid_plural "Apply %d suggestions"
msgstr[0] ""
msgstr[1] ""
msgid "Apply a label" msgid "Apply a label"
msgstr "" msgstr ""
...@@ -4119,9 +4124,6 @@ msgstr "" ...@@ -4119,9 +4124,6 @@ msgstr ""
msgid "Apply suggestion" msgid "Apply suggestion"
msgstr "" msgstr ""
msgid "Apply suggestions"
msgstr ""
msgid "Apply template" msgid "Apply template"
msgstr "" msgstr ""
......
...@@ -93,7 +93,6 @@ module QA ...@@ -93,7 +93,6 @@ module QA
end end
view 'app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue' do view 'app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue' do
element :apply_suggestions_batch_button
element :add_suggestion_batch_button element :add_suggestion_batch_button
end end
...@@ -353,10 +352,6 @@ module QA ...@@ -353,10 +352,6 @@ module QA
all_elements(:add_suggestion_batch_button, minimum: 1).first.click all_elements(:add_suggestion_batch_button, minimum: 1).first.click
end end
def apply_suggestions_batch
all_elements(:apply_suggestions_batch_button, minimum: 1).first.click
end
def cherry_pick! def cherry_pick!
click_element(:cherry_pick_button, Page::Component::CommitModal) click_element(:cherry_pick_button, Page::Component::CommitModal)
click_element(:submit_commit_button) click_element(:submit_commit_button)
......
...@@ -50,7 +50,7 @@ module QA ...@@ -50,7 +50,7 @@ module QA
Page::MergeRequest::Show.perform do |merge_request| Page::MergeRequest::Show.perform do |merge_request|
merge_request.click_diffs_tab merge_request.click_diffs_tab
4.times { merge_request.add_suggestion_to_batch } 4.times { merge_request.add_suggestion_to_batch }
merge_request.apply_suggestions_batch merge_request.apply_suggestion_with_message("Custom commit message")
expect(merge_request).to have_css('.badge-success', text: "Applied", count: 4) expect(merge_request).to have_css('.badge-success', text: "Applied", count: 4)
end end
......
...@@ -159,7 +159,12 @@ RSpec.describe 'User comments on a diff', :js do ...@@ -159,7 +159,12 @@ RSpec.describe 'User comments on a diff', :js do
wait_for_requests wait_for_requests
expect(page).to have_content('Remove from batch') expect(page).to have_content('Remove from batch')
expect(page).to have_content("Apply suggestions #{index + 1}")
if index < 1
expect(page).to have_content("Apply suggestion")
else
expect(page).to have_content("Apply #{index + 1} suggestions")
end
end end
end end
...@@ -167,13 +172,12 @@ RSpec.describe 'User comments on a diff', :js do ...@@ -167,13 +172,12 @@ RSpec.describe 'User comments on a diff', :js do
click_button('Remove from batch') click_button('Remove from batch')
wait_for_requests wait_for_requests
expect(page).to have_content('Apply suggestion')
expect(page).to have_content('Add suggestion to batch') expect(page).to have_content('Add suggestion to batch')
end end
page.within("[id='#{files[1][:hash]}']") do page.within("[id='#{files[1][:hash]}']") do
expect(page).to have_content('Remove from batch') expect(page).to have_content('Remove from batch')
expect(page).to have_content('Apply suggestions 1') expect(page).to have_content('Apply suggestion')
end end
end end
......
...@@ -60,7 +60,7 @@ describe('Suggestion Diff component', () => { ...@@ -60,7 +60,7 @@ describe('Suggestion Diff component', () => {
expect(findHelpButton().exists()).toBe(true); expect(findHelpButton().exists()).toBe(true);
}); });
it('renders apply suggestion and add to batch buttons', () => { it('renders add to batch button when more than 1 suggestion', () => {
createComponent({ createComponent({
suggestionsCount: 2, suggestionsCount: 2,
}); });
...@@ -68,8 +68,7 @@ describe('Suggestion Diff component', () => { ...@@ -68,8 +68,7 @@ describe('Suggestion Diff component', () => {
const applyBtn = findApplyButton(); const applyBtn = findApplyButton();
const addToBatchBtn = findAddToBatchButton(); const addToBatchBtn = findAddToBatchButton();
expect(applyBtn.exists()).toBe(true); expect(applyBtn.exists()).toBe(false);
expect(applyBtn.html().includes('Apply suggestion')).toBe(true);
expect(addToBatchBtn.exists()).toBe(true); expect(addToBatchBtn.exists()).toBe(true);
expect(addToBatchBtn.html().includes('Add suggestion to batch')).toBe(true); expect(addToBatchBtn.html().includes('Add suggestion to batch')).toBe(true);
...@@ -85,7 +84,7 @@ describe('Suggestion Diff component', () => { ...@@ -85,7 +84,7 @@ describe('Suggestion Diff component', () => {
describe('when apply suggestion is clicked', () => { describe('when apply suggestion is clicked', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({ batchSuggestionsCount: 0 });
findApplyButton().vm.$emit('apply'); findApplyButton().vm.$emit('apply');
}); });
...@@ -140,11 +139,11 @@ describe('Suggestion Diff component', () => { ...@@ -140,11 +139,11 @@ describe('Suggestion Diff component', () => {
describe('apply suggestions is clicked', () => { describe('apply suggestions is clicked', () => {
it('emits applyBatch', () => { it('emits applyBatch', () => {
createComponent({ isBatched: true }); createComponent({ isBatched: true, batchSuggestionsCount: 2 });
findApplyBatchButton().vm.$emit('click'); findApplyButton().vm.$emit('apply');
expect(wrapper.emitted().applyBatch).toEqual([[]]); expect(wrapper.emitted().applyBatch).toEqual([[undefined]]);
}); });
}); });
...@@ -155,23 +154,24 @@ describe('Suggestion Diff component', () => { ...@@ -155,23 +154,24 @@ describe('Suggestion Diff component', () => {
isBatched: true, isBatched: true,
}); });
const applyBatchBtn = findApplyBatchButton(); const applyBatchBtn = findApplyButton();
const removeFromBatchBtn = findRemoveFromBatchButton(); const removeFromBatchBtn = findRemoveFromBatchButton();
expect(removeFromBatchBtn.exists()).toBe(true); expect(removeFromBatchBtn.exists()).toBe(true);
expect(removeFromBatchBtn.html().includes('Remove from batch')).toBe(true); expect(removeFromBatchBtn.html().includes('Remove from batch')).toBe(true);
expect(applyBatchBtn.exists()).toBe(true); expect(applyBatchBtn.exists()).toBe(true);
expect(applyBatchBtn.html().includes('Apply suggestions')).toBe(true); expect(applyBatchBtn.html().includes('Apply suggestion')).toBe(true);
expect(applyBatchBtn.html().includes(String('9'))).toBe(true); expect(applyBatchBtn.html().includes(String('9'))).toBe(true);
}); });
it('hides add to batch and apply buttons', () => { it('hides add to batch and apply buttons', () => {
createComponent({ createComponent({
isBatched: true, isBatched: true,
batchSuggestionsCount: 9,
}); });
expect(findApplyButton().exists()).toBe(false); expect(findApplyButton().exists()).toBe(true);
expect(findAddToBatchButton().exists()).toBe(false); expect(findAddToBatchButton().exists()).toBe(false);
}); });
...@@ -215,9 +215,8 @@ describe('Suggestion Diff component', () => { ...@@ -215,9 +215,8 @@ describe('Suggestion Diff component', () => {
}); });
it('disables apply suggestion and hides add to batch button', () => { it('disables apply suggestion and hides add to batch button', () => {
expect(findApplyButton().exists()).toBe(true); expect(findApplyButton().exists()).toBe(false);
expect(findAddToBatchButton().exists()).toBe(false); expect(findAddToBatchButton().exists()).toBe(false);
expect(findApplyButton().attributes('disabled')).toBe('true');
}); });
}); });
...@@ -225,20 +224,11 @@ describe('Suggestion Diff component', () => { ...@@ -225,20 +224,11 @@ describe('Suggestion Diff component', () => {
const findTooltip = () => getBinding(findApplyButton().element, 'gl-tooltip'); const findTooltip = () => getBinding(findApplyButton().element, 'gl-tooltip');
it('renders correct tooltip message when button is applicable', () => { it('renders correct tooltip message when button is applicable', () => {
createComponent(); createComponent({ batchSuggestionsCount: 0 });
const tooltip = findTooltip(); const tooltip = findTooltip();
expect(tooltip.modifiers.viewport).toBe(true); expect(tooltip.modifiers.viewport).toBe(true);
expect(tooltip.value).toBe('This also resolves this thread'); expect(tooltip.value).toBe('This also resolves this thread');
}); });
it('renders the inapplicable reason in the tooltip when button is not applicable', () => {
const inapplicableReason = 'lorem';
createComponent({ canApply: false, inapplicableReason });
const tooltip = findTooltip();
expect(tooltip.modifiers.viewport).toBe(true);
expect(tooltip.value).toBe(inapplicableReason);
});
}); });
}); });
...@@ -77,7 +77,7 @@ describe('Suggestion Diff component', () => { ...@@ -77,7 +77,7 @@ describe('Suggestion Diff component', () => {
it.each` it.each`
event | childArgs | args event | childArgs | args
${'apply'} | ${['test-event']} | ${[{ callback: 'test-event', suggestionId }]} ${'apply'} | ${['test-event']} | ${[{ callback: 'test-event', suggestionId }]}
${'applyBatch'} | ${[]} | ${[]} ${'applyBatch'} | ${['test-event']} | ${['test-event']}
${'addToBatch'} | ${[]} | ${[suggestionId]} ${'addToBatch'} | ${[]} | ${[suggestionId]}
${'removeFromBatch'} | ${[]} | ${[suggestionId]} ${'removeFromBatch'} | ${[]} | ${[suggestionId]}
`('emits $event event on sugestion diff header $event', ({ event, childArgs, args }) => { `('emits $event event on sugestion diff header $event', ({ event, childArgs, args }) => {
......
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