diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index b75fa65b842e6b364f50f965f07bcb74bf9c3086..253caedf75ab23bfa287ba8433cc5282ea66264b 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -38,6 +38,7 @@ const Api = {
userPostStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits',
applySuggestionPath: '/api/:version/suggestions/:id/apply',
+ applySuggestionBatchPath: '/api/:version/suggestions/batch_apply',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
@@ -322,6 +323,12 @@ const Api = {
return axios.put(url);
},
+ applySuggestionBatch(ids) {
+ const url = Api.buildUrl(Api.applySuggestionBatchPath);
+
+ return axios.put(url, { ids });
+ },
+
commitPipelines(projectId, sha) {
const encodedProjectId = projectId
.split('/')
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 0ba104d064c8f27f2e0df961d518ff57521d49e2..42b78929f8a9c6a3111348381fde4f14e8ce42ef 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,5 +1,5 @@
@@ -105,10 +124,14 @@ export default {
{
+ const suggestionIds = state.batchSuggestionsInfo.map(({ suggestionId }) => suggestionId);
+
+ const applyAllSuggestions = () =>
+ state.batchSuggestionsInfo.map(suggestionInfo =>
+ commit(types.APPLY_SUGGESTION, suggestionInfo),
+ );
+
+ const resolveAllDiscussions = () =>
+ state.batchSuggestionsInfo.map(suggestionInfo => {
+ const { discussionId } = suggestionInfo;
+ return dispatch('resolveDiscussion', { discussionId }).catch(() => {});
+ });
+
+ commit(types.SET_APPLYING_BATCH_STATE, true);
+
+ return Api.applySuggestionBatch(suggestionIds)
+ .then(() => Promise.all(applyAllSuggestions()))
+ .then(() => Promise.all(resolveAllDiscussions()))
+ .then(() => commit(types.CLEAR_SUGGESTION_BATCH))
+ .catch(err => {
+ const defaultMessage = __(
+ 'Something went wrong while applying the batch of suggestions. Please try again.',
+ );
+
+ const errorMessage = err.response.data?.message;
+
+ const flashMessage = errorMessage || defaultMessage;
+
+ Flash(__(flashMessage), 'alert', flashContainer);
+ })
+ .finally(() => commit(types.SET_APPLYING_BATCH_STATE, false));
+};
+
+export const addSuggestionInfoToBatch = ({ commit }, { suggestionId, noteId, discussionId }) =>
+ commit(types.ADD_SUGGESTION_TO_BATCH, { suggestionId, noteId, discussionId });
+
+export const removeSuggestionInfoFromBatch = ({ commit }, suggestionId) =>
+ commit(types.REMOVE_SUGGESTION_FROM_BATCH, suggestionId);
+
export const convertToDiscussion = ({ commit }, noteId) =>
commit(types.CONVERT_TO_DISCUSSION, noteId);
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 25f0f546103bec9695a1294b72206568f981f60b..329bf5e147e2fcf3feaa129be5e67d964805c0b3 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -11,6 +11,7 @@ export default () => ({
targetNoteHash: null,
lastFetchedAt: null,
currentDiscussionId: null,
+ batchSuggestionsInfo: [],
// View layer
isToggleStateButtonLoading: false,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 2f7b2788d8a2826174e7b3a15b6a265035884430..5c88e152280ddda6d3bff4badda798eb05028302 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -17,6 +17,10 @@ export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
export const APPLY_SUGGESTION = 'APPLY_SUGGESTION';
+export const SET_APPLYING_BATCH_STATE = 'SET_APPLYING_BATCH_STATE';
+export const ADD_SUGGESTION_TO_BATCH = 'ADD_SUGGESTION_TO_BATCH';
+export const REMOVE_SUGGESTION_FROM_BATCH = 'REMOVE_SUGGESTION_FROM_BATCH';
+export const CLEAR_SUGGESTION_BATCH = 'CLEAR_SUGGESTION_BATCH';
export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION';
export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index f06874991f018e0816db1958ab3edf0eb78ea8dc..38f5551695da42b01bd63464f4cc179ce827fa3e 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -225,6 +225,39 @@ export default {
}));
},
+ [types.SET_APPLYING_BATCH_STATE](state, isApplyingBatch) {
+ state.batchSuggestionsInfo.forEach(suggestionInfo => {
+ const { discussionId, noteId, suggestionId } = suggestionInfo;
+
+ const noteObj = utils.findNoteObjectById(state.discussions, discussionId);
+ const comment = utils.findNoteObjectById(noteObj.notes, noteId);
+
+ comment.suggestions = comment.suggestions.map(suggestion => ({
+ ...suggestion,
+ is_applying_batch: suggestion.id === suggestionId && isApplyingBatch,
+ }));
+ });
+ },
+
+ [types.ADD_SUGGESTION_TO_BATCH](state, { noteId, discussionId, suggestionId }) {
+ state.batchSuggestionsInfo.push({
+ suggestionId,
+ noteId,
+ discussionId,
+ });
+ },
+
+ [types.REMOVE_SUGGESTION_FROM_BATCH](state, id) {
+ const index = state.batchSuggestionsInfo.findIndex(({ suggestionId }) => suggestionId === id);
+ if (index !== -1) {
+ state.batchSuggestionsInfo.splice(index, 1);
+ }
+ },
+
+ [types.CLEAR_SUGGESTION_BATCH](state) {
+ state.batchSuggestionsInfo.splice(0, state.batchSuggestionsInfo.length);
+ },
+
[types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData;
const selectedDiscussion = state.discussions.find(disc => disc.id === note.id);
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
index a7cd292e01ddb05c41d4a6793e6bda5d789231da..6dac448d5de1a846099cd9d9b72d28885ca494bf 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
@@ -13,6 +13,11 @@ export default {
type: Object,
required: true,
},
+ batchSuggestionsInfo: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
disabled: {
type: Boolean,
required: false,
@@ -24,6 +29,14 @@ export default {
},
},
computed: {
+ batchSuggestionsCount() {
+ return this.batchSuggestionsInfo.length;
+ },
+ isBatched() {
+ return Boolean(
+ this.batchSuggestionsInfo.find(({ suggestionId }) => suggestionId === this.suggestion.id),
+ );
+ },
lines() {
return selectDiffLines(this.suggestion.diff_lines);
},
@@ -32,6 +45,15 @@ export default {
applySuggestion(callback) {
this.$emit('apply', { suggestionId: this.suggestion.id, callback });
},
+ applySuggestionBatch() {
+ this.$emit('applyBatch');
+ },
+ addSuggestionToBatch() {
+ this.$emit('addToBatch', this.suggestion.id);
+ },
+ removeSuggestionFromBatch() {
+ this.$emit('removeFromBatch', this.suggestion.id);
+ },
},
};
@@ -42,8 +64,14 @@ export default {
class="qa-suggestion-diff-header js-suggestion-diff-header"
:can-apply="suggestion.appliable && suggestion.current_user.can_apply && !disabled"
:is-applied="suggestion.applied"
+ :is-batched="isBatched"
+ :is-applying-batch="suggestion.is_applying_batch"
+ :batch-suggestions-count="batchSuggestionsCount"
:help-page-path="helpPagePath"
@apply="applySuggestion"
+ @applyBatch="applySuggestionBatch"
+ @addToBatch="addSuggestionToBatch"
+ @removeFromBatch="removeSuggestionFromBatch"
/>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index af438ce5619add6c6c53ea6461c26b74a3f4a3a6..fc5b2b99c18b2460efff62353305d7c75f20c8d8 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -1,11 +1,17 @@