Commit 5d12cc53 authored by 🙈  jacopo beschi 🙉's avatar 🙈 jacopo beschi 🙉 Committed by Mayra Cabrera

Display a better message when starting a discussion on a deleted comment

Shows "Your comment could not be submitted because the original comment
has been deleted" when replying to a deleted comment.
parent a236468a
...@@ -193,23 +193,10 @@ export default { ...@@ -193,23 +193,10 @@ export default {
this.stopPolling(); this.stopPolling();
this.saveNote(noteData) this.saveNote(noteData)
.then(res => { .then(() => {
this.enableButton(); this.enableButton();
this.restartPolling(); this.restartPolling();
if (res.errors) {
if (res.errors.commands_only) {
this.discard(); this.discard();
} else {
Flash(
__('Something went wrong while adding your comment. Please try again.'),
'alert',
this.$refs.commentForm,
);
}
} else {
this.discard();
}
if (withIssueAction) { if (withIssueAction) {
this.toggleIssueState(); this.toggleIssueState();
......
...@@ -198,16 +198,16 @@ export default { ...@@ -198,16 +198,16 @@ export default {
data: postData, data: postData,
}; };
this.isReplying = false;
this.saveNote(replyData) this.saveNote(replyData)
.then(() => { .then(res => {
if (res.hasFlash !== true) {
this.isReplying = false;
clearDraft(this.autosaveKey); clearDraft(this.autosaveKey);
}
callback(); callback();
}) })
.catch(err => { .catch(err => {
this.removePlaceholderNotes(); this.removePlaceholderNotes();
this.isReplying = true;
this.$nextTick(() => {
const msg = __( const msg = __(
'Your comment could not be submitted! Please check your network connection and try again.', 'Your comment could not be submitted! Please check your network connection and try again.',
); );
...@@ -215,7 +215,6 @@ export default { ...@@ -215,7 +215,6 @@ export default {
this.$refs.noteForm.note = noteText; this.$refs.noteForm.note = noteText;
callback(err); callback(err);
}); });
});
}, },
jumpToNextDiscussion() { jumpToNextDiscussion() {
const nextId = this.nextUnresolvedDiscussionId( const nextId = this.nextUnresolvedDiscussionId(
......
...@@ -14,7 +14,7 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; ...@@ -14,7 +14,7 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils'; import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility'; import { mergeUrlParams } from '../../lib/utils/url_utility';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
import { __ } from '~/locale'; import { __, sprintf } from '~/locale';
import Api from '~/api'; import Api from '~/api';
let eTagPoll; let eTagPoll;
...@@ -252,29 +252,22 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -252,29 +252,22 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
} }
} }
const processErrors = res => { const processQuickActions = res => {
const { errors } = res; const { errors: { commands_only: message } = { commands_only: null } } = res;
if (!errors || !Object.keys(errors).length) {
return res;
}
/* /*
The following reply means that quick actions have been successfully applied: The following reply means that quick actions have been successfully applied:
{"commands_changes":{},"valid":false,"errors":{"commands_only":["Commands applied"]}} {"commands_changes":{},"valid":false,"errors":{"commands_only":["Commands applied"]}}
*/ */
if (hasQuickActions) { if (hasQuickActions && message) {
eTagPoll.makeRequest(); eTagPoll.makeRequest();
$('.js-gfm-input').trigger('clear-commands-cache.atwho'); $('.js-gfm-input').trigger('clear-commands-cache.atwho');
const { commands_only: message } = errors;
Flash(message || __('Commands applied'), 'notice', noteData.flashContainer); Flash(message || __('Commands applied'), 'notice', noteData.flashContainer);
return res;
} }
throw new Error(__('Failed to save comment!')); return res;
}; };
const processEmojiAward = res => { const processEmojiAward = res => {
...@@ -321,11 +314,33 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -321,11 +314,33 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
return res; return res;
}; };
const processErrors = error => {
if (error.response) {
const {
response: { data = {} },
} = error;
const { errors = {} } = data;
const { base = [] } = errors;
// we handle only errors.base for now
if (base.length > 0) {
const errorMsg = sprintf(__('Your comment could not be submitted because %{error}'), {
error: base[0].toLowerCase(),
});
Flash(errorMsg, 'alert', noteData.flashContainer);
return { ...data, hasFlash: true };
}
}
throw error;
};
return dispatch(methodToDispatch, postData, { root: true }) return dispatch(methodToDispatch, postData, { root: true })
.then(processErrors) .then(processQuickActions)
.then(processEmojiAward) .then(processEmojiAward)
.then(processTimeTracking) .then(processTimeTracking)
.then(removePlaceholder); .then(removePlaceholder)
.catch(processErrors);
}; };
const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
......
...@@ -11,7 +11,7 @@ module Notes ...@@ -11,7 +11,7 @@ module Notes
unless discussion && can?(current_user, :create_note, discussion.noteable) unless discussion && can?(current_user, :create_note, discussion.noteable)
note = Note.new note = Note.new
note.errors.add(:base, 'Discussion to reply to cannot be found') note.errors.add(:base, _('Discussion to reply to cannot be found'))
return note return note
end end
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
module Notes module Notes
class CreateService < ::Notes::BaseService class CreateService < ::Notes::BaseService
# rubocop:disable Metrics/CyclomaticComplexity
def execute def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha) merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
...@@ -9,7 +10,9 @@ module Notes ...@@ -9,7 +10,9 @@ module Notes
# n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/37440 # n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/37440
note_valid = Gitlab::GitalyClient.allow_n_plus_1_calls do note_valid = Gitlab::GitalyClient.allow_n_plus_1_calls do
note.valid? # We may set errors manually in Notes::BuildService for this reason
# we also need to check for already existing errors.
note.errors.empty? && note.valid?
end end
return note unless note_valid return note unless note_valid
...@@ -67,6 +70,7 @@ module Notes ...@@ -67,6 +70,7 @@ module Notes
note note
end end
# rubocop:enable Metrics/CyclomaticComplexity
private private
......
---
title: Display a better message when starting a discussion on a deleted comment
merge_request: 20031
author: Jacopo Beschi @jacopo-beschi
type: changed
...@@ -6078,6 +6078,9 @@ msgstr "" ...@@ -6078,6 +6078,9 @@ msgstr ""
msgid "Discussion" msgid "Discussion"
msgstr "" msgstr ""
msgid "Discussion to reply to cannot be found"
msgstr ""
msgid "Disk Usage" msgid "Disk Usage"
msgstr "" msgstr ""
...@@ -7395,9 +7398,6 @@ msgstr "" ...@@ -7395,9 +7398,6 @@ msgstr ""
msgid "Failed to reset key. Please try again." msgid "Failed to reset key. Please try again."
msgstr "" msgstr ""
msgid "Failed to save comment!"
msgstr ""
msgid "Failed to save merge conflicts resolutions. Please try again!" msgid "Failed to save merge conflicts resolutions. Please try again!"
msgstr "" msgstr ""
...@@ -16399,9 +16399,6 @@ msgstr "" ...@@ -16399,9 +16399,6 @@ msgstr ""
msgid "Something went wrong while adding your award. Please try again." msgid "Something went wrong while adding your award. Please try again."
msgstr "" msgstr ""
msgid "Something went wrong while adding your comment. Please try again."
msgstr ""
msgid "Something went wrong while applying the suggestion. Please try again." msgid "Something went wrong while applying the suggestion. Please try again."
msgstr "" msgstr ""
...@@ -20647,6 +20644,9 @@ msgstr "" ...@@ -20647,6 +20644,9 @@ msgstr ""
msgid "Your changes have been successfully committed." msgid "Your changes have been successfully committed."
msgstr "" msgstr ""
msgid "Your comment could not be submitted because %{error}"
msgstr ""
msgid "Your comment could not be submitted! Please check your network connection and try again." msgid "Your comment could not be submitted! Please check your network connection and try again."
msgstr "" msgstr ""
......
...@@ -95,6 +95,24 @@ describe 'Merge request > User posts notes', :js do ...@@ -95,6 +95,24 @@ describe 'Merge request > User posts notes', :js do
end end
end end
describe 'reply on a deleted conversation' do
before do
visit project_merge_request_path(project, merge_request)
end
it 'shows an error message' do
find('.js-reply-button').click
note.delete
page.within('.discussion-reply-holder') do
fill_in 'note[note]', with: 'A reply'
click_button 'Comment'
wait_for_requests
expect(page).to have_content('Your comment could not be submitted because discussion to reply to cannot be found')
end
end
end
describe 'when previewing a note' do describe 'when previewing a note' do
it 'shows the toolbar buttons when editing a note' do it 'shows the toolbar buttons when editing a note' do
page.within('.js-main-target-form') do page.within('.js-main-target-form') do
......
...@@ -751,29 +751,59 @@ describe('Actions Notes Store', () => { ...@@ -751,29 +751,59 @@ describe('Actions Notes Store', () => {
}); });
describe('saveNote', () => { describe('saveNote', () => {
const payload = { endpoint: TEST_HOST, data: { 'note[note]': 'some text' } }; const flashContainer = {};
const payload = { endpoint: TEST_HOST, data: { 'note[note]': 'some text' }, flashContainer };
describe('if response contains errors', () => { describe('if response contains errors', () => {
const res = { errors: { something: ['went wrong'] } }; const res = { errors: { something: ['went wrong'] } };
const error = { message: 'Unprocessable entity', response: { data: res } };
it('throws an error', done => { it('throws an error', done => {
actions actions
.saveNote( .saveNote(
{ {
commit() {}, commit() {},
dispatch: () => Promise.resolve(res), dispatch: () => Promise.reject(error),
}, },
payload, payload,
) )
.then(() => done.fail('Expected error to be thrown!')) .then(() => done.fail('Expected error to be thrown!'))
.catch(error => { .catch(err => {
expect(error.message).toBe('Failed to save comment!'); expect(err).toBe(error);
expect(flashSpy).not.toHaveBeenCalled();
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('if response contains errors.base', () => {
const res = { errors: { base: ['something went wrong'] } };
const error = { message: 'Unprocessable entity', response: { data: res } };
it('sets flash alert using errors.base message', done => {
actions
.saveNote(
{
commit() {},
dispatch: () => Promise.reject(error),
},
{ ...payload, flashContainer },
)
.then(resp => {
expect(resp.hasFlash).toBe(true);
expect(flashSpy).toHaveBeenCalledWith(
'Your comment could not be submitted because something went wrong',
'alert',
flashContainer,
);
})
.catch(() => done.fail('Expected success response!'))
.then(done)
.catch(done.fail);
});
});
describe('if response contains no errors', () => { describe('if response contains no errors', () => {
const res = { valid: true }; const res = { valid: true };
...@@ -788,6 +818,7 @@ describe('Actions Notes Store', () => { ...@@ -788,6 +818,7 @@ describe('Actions Notes Store', () => {
) )
.then(data => { .then(data => {
expect(data).toBe(res); expect(data).toBe(res);
expect(flashSpy).not.toHaveBeenCalled();
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
......
...@@ -382,6 +382,19 @@ describe Notes::CreateService do ...@@ -382,6 +382,19 @@ describe Notes::CreateService do
end.to change { existing_note.type }.from(nil).to('DiscussionNote') end.to change { existing_note.type }.from(nil).to('DiscussionNote')
.and change { existing_note.updated_at } .and change { existing_note.updated_at }
end end
context 'discussion to reply cannot be found' do
before do
existing_note.delete
end
it 'returns an note with errors' do
note = subject
expect(note.errors).not_to be_empty
expect(note.errors[:base]).to eq(['Discussion to reply to cannot be found'])
end
end
end end
describe "usage counter" do describe "usage counter" do
......
...@@ -26,6 +26,7 @@ end ...@@ -26,6 +26,7 @@ end
RSpec.shared_examples 'a Note mutation when there are active record validation errors' do |model: Note| RSpec.shared_examples 'a Note mutation when there are active record validation errors' do |model: Note|
before do before do
expect_next_instance_of(model) do |note| expect_next_instance_of(model) do |note|
allow(note).to receive_message_chain(:errors, :empty?).and_return(true)
expect(note).to receive(:valid?).at_least(:once).and_return(false) expect(note).to receive(:valid?).at_least(:once).and_return(false)
expect(note).to receive_message_chain( expect(note).to receive_message_chain(
:errors, :errors,
......
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