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 @@