Commit a13abd67 authored by Patrick Derichs's avatar Patrick Derichs Committed by Kamil Trzciński

Add edit_note and spec for editing quick actions

Call QuickActionsService on Note update

Add support for notes which just contain
commands after editing

Return http status gone (410) if note was deleted

Temporary frontend addition so it is not
failing when a note is deleted

Move specs to shared examples

Fix rubocop style issue

Deleting note on frontend when status is 410

Use guard clause for note which got deleted

Simplified condition for nil note

This method should no longer be called
with nil note

Refactoring of execute method to reduce
complexity

Move errors update to delete_note method

Note is now deleted visually when it only
contains commands after update

Add expectation

Fix style issues

Changing action to fix tests

Add tests for removeNote and update
deleteNote expectations
parent e5e6a5fb
...@@ -19,6 +19,7 @@ const httpStatusCodes = { ...@@ -19,6 +19,7 @@ const httpStatusCodes = {
UNAUTHORIZED: 401, UNAUTHORIZED: 401,
FORBIDDEN: 403, FORBIDDEN: 403,
NOT_FOUND: 404, NOT_FOUND: 404,
GONE: 410,
UNPROCESSABLE_ENTITY: 422, UNPROCESSABLE_ENTITY: 422,
}; };
......
...@@ -14,6 +14,7 @@ import NoteBody from './note_body.vue'; ...@@ -14,6 +14,7 @@ import NoteBody from './note_body.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
import httpStatusCodes from '~/lib/utils/http_status';
export default { export default {
name: 'NoteableNote', name: 'NoteableNote',
...@@ -122,7 +123,13 @@ export default { ...@@ -122,7 +123,13 @@ export default {
}, },
methods: { methods: {
...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']), ...mapActions([
'deleteNote',
'removeNote',
'updateNote',
'toggleResolveNote',
'scrollToNoteIfNeeded',
]),
editHandler() { editHandler() {
this.isEditing = true; this.isEditing = true;
this.$emit('handleEdit'); this.$emit('handleEdit');
...@@ -185,15 +192,21 @@ export default { ...@@ -185,15 +192,21 @@ export default {
this.updateSuccess(); this.updateSuccess();
callback(); callback();
}) })
.catch(() => { .catch(response => {
this.isRequesting = false; if (response.status === httpStatusCodes.GONE) {
this.isEditing = true; this.removeNote(this.note);
this.$nextTick(() => { this.updateSuccess();
const msg = __('Something went wrong while editing your comment. Please try again.');
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback(); callback();
}); } else {
this.isRequesting = false;
this.isEditing = true;
this.$nextTick(() => {
const msg = __('Something went wrong while editing your comment. Please try again.');
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
});
}
}); });
}, },
formCancelHandler(shouldConfirm, isDirty) { formCancelHandler(shouldConfirm, isDirty) {
......
...@@ -61,18 +61,22 @@ export const updateDiscussion = ({ commit, state }, discussion) => { ...@@ -61,18 +61,22 @@ export const updateDiscussion = ({ commit, state }, discussion) => {
return utils.findNoteObjectById(state.discussions, discussion.id); return utils.findNoteObjectById(state.discussions, discussion.id);
}; };
export const deleteNote = ({ commit, dispatch, state }, note) => export const removeNote = ({ commit, dispatch, state }, note) => {
axios.delete(note.path).then(() => { const discussion = state.discussions.find(({ id }) => id === note.discussion_id);
const discussion = state.discussions.find(({ id }) => id === note.discussion_id);
commit(types.DELETE_NOTE, note); commit(types.DELETE_NOTE, note);
dispatch('updateMergeRequestWidget'); dispatch('updateMergeRequestWidget');
dispatch('updateResolvableDiscussionsCounts'); dispatch('updateResolvableDiscussionsCounts');
if (isInMRPage()) { if (isInMRPage()) {
dispatch('diffs/removeDiscussionsFromDiff', discussion); dispatch('diffs/removeDiscussionsFromDiff', discussion);
} }
};
export const deleteNote = ({ dispatch }, note) =>
axios.delete(note.path).then(() => {
dispatch('removeNote', note);
}); });
export const updateNote = ({ commit, dispatch }, { endpoint, note }) => export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
......
...@@ -73,6 +73,11 @@ module NotesActions ...@@ -73,6 +73,11 @@ module NotesActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def update def update
@note = Notes::UpdateService.new(project, current_user, update_note_params).execute(note) @note = Notes::UpdateService.new(project, current_user, update_note_params).execute(note)
unless @note
head :gone
return
end
prepare_notes_for_rendering([@note]) prepare_notes_for_rendering([@note])
respond_to do |format| respond_to do |format|
......
...@@ -6,7 +6,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -6,7 +6,7 @@ class Projects::NotesController < Projects::ApplicationController
include NotesHelper include NotesHelper
include ToggleAwardEmoji include ToggleAwardEmoji
before_action :whitelist_query_limiting, only: [:create] before_action :whitelist_query_limiting, only: [:create, :update]
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve] before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
......
...@@ -8,24 +8,70 @@ module Notes ...@@ -8,24 +8,70 @@ module Notes
old_mentioned_users = note.mentioned_users.to_a old_mentioned_users = note.mentioned_users.to_a
note.update(params.merge(updated_by: current_user)) note.update(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user)
if note.previous_changes.include?('note') only_commands = false
TodoService.new.update_note(note, current_user, old_mentioned_users)
quick_actions_service = QuickActionsService.new(project, current_user)
if quick_actions_service.supported?(note)
content, update_params, message = quick_actions_service.execute(note, {})
only_commands = content.empty?
note.note = content
end
unless only_commands
note.create_new_cross_references!(current_user)
update_todos(note, old_mentioned_users)
update_suggestions(note)
end end
if note.supports_suggestion? if quick_actions_service.commands_executed_count.to_i > 0
Suggestion.transaction do if update_params.present?
note.suggestions.delete_all quick_actions_service.apply_updates(update_params, note)
Suggestions::CreateService.new(note).execute note.commands_changes = update_params
end end
# We need to refresh the previous suggestions call cache if only_commands
# in order to get the new records. delete_note(note, message)
note.reset note = nil
else
note.save
end
end end
note note
end end
private
def delete_note(note, message)
# We must add the error after we call #save because errors are reset
# when #save is called
note.errors.add(:commands_only, message.presence || _('Commands did not apply'))
Notes::DestroyService.new(project, current_user).execute(note)
end
def update_suggestions(note)
return unless note.supports_suggestion?
Suggestion.transaction do
note.suggestions.delete_all
Suggestions::CreateService.new(note).execute
end
# We need to refresh the previous suggestions call cache
# in order to get the new records.
note.reset
end
def update_todos(note, old_mentioned_users)
return unless note.previous_changes.include?('note')
TodoService.new.update_note(note, current_user, old_mentioned_users)
end
end end
end end
---
title: Apply quickactions when modifying comments
merge_request: 31136
author:
type: added
...@@ -2977,6 +2977,9 @@ msgstr "" ...@@ -2977,6 +2977,9 @@ msgstr ""
msgid "Commands applied" msgid "Commands applied"
msgstr "" msgstr ""
msgid "Commands did not apply"
msgstr ""
msgid "Comment" msgid "Comment"
msgstr "" msgstr ""
......
...@@ -336,7 +336,7 @@ describe('Actions Notes Store', () => { ...@@ -336,7 +336,7 @@ describe('Actions Notes Store', () => {
}); });
}); });
describe('deleteNote', () => { describe('removeNote', () => {
const endpoint = `${TEST_HOST}/note`; const endpoint = `${TEST_HOST}/note`;
let axiosMock; let axiosMock;
...@@ -357,7 +357,7 @@ describe('Actions Notes Store', () => { ...@@ -357,7 +357,7 @@ describe('Actions Notes Store', () => {
const note = { path: endpoint, id: 1 }; const note = { path: endpoint, id: 1 };
testAction( testAction(
actions.deleteNote, actions.removeNote,
note, note,
store.state, store.state,
[ [
...@@ -384,7 +384,7 @@ describe('Actions Notes Store', () => { ...@@ -384,7 +384,7 @@ describe('Actions Notes Store', () => {
$('body').attr('data-page', 'projects:merge_requests:show'); $('body').attr('data-page', 'projects:merge_requests:show');
testAction( testAction(
actions.deleteNote, actions.removeNote,
note, note,
store.state, store.state,
[ [
...@@ -409,6 +409,45 @@ describe('Actions Notes Store', () => { ...@@ -409,6 +409,45 @@ describe('Actions Notes Store', () => {
}); });
}); });
describe('deleteNote', () => {
const endpoint = `${TEST_HOST}/note`;
let axiosMock;
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
axiosMock.onDelete(endpoint).replyOnce(200, {});
$('body').attr('data-page', '');
});
afterEach(() => {
axiosMock.restore();
$('body').attr('data-page', '');
});
it('dispatches removeNote', done => {
const note = { path: endpoint, id: 1 };
testAction(
actions.deleteNote,
note,
{},
[],
[
{
type: 'removeNote',
payload: {
id: 1,
path: 'http://test.host/note',
},
},
],
done,
);
});
});
describe('createNewNote', () => { describe('createNewNote', () => {
describe('success', () => { describe('success', () => {
const res = { const res = {
......
...@@ -89,5 +89,54 @@ shared_examples 'move quick action' do ...@@ -89,5 +89,54 @@ shared_examples 'move quick action' do
it_behaves_like 'applies the commands to issues in both projects, target and source' it_behaves_like 'applies the commands to issues in both projects, target and source'
end end
end end
context 'when editing comments' do
let(:target_project) { create(:project, :public) }
before do
target_project.add_maintainer(user)
sign_in(user)
visit project_issue_path(project, issue)
wait_for_all_requests
end
it 'moves the issue after quickcommand note was updated' do
# misspelled quick action
add_note("test note.\n/mvoe #{target_project.full_path}")
expect(issue.reload).not_to be_closed
edit_note("/mvoe #{target_project.full_path}", "test note.\n/move #{target_project.full_path}")
wait_for_all_requests
expect(page).to have_content 'test note.'
expect(issue.reload).to be_closed
visit project_issue_path(target_project, issue)
wait_for_all_requests
expect(page).to have_content 'Issues 1'
end
it 'deletes the note if it was updated to just contain a command' do
# missspelled quick action
add_note("test note.\n/mvoe #{target_project.full_path}")
expect(page).not_to have_content 'Commands applied'
expect(issue.reload).not_to be_closed
edit_note("/mvoe #{target_project.full_path}", "/move #{target_project.full_path}")
wait_for_all_requests
expect(page).not_to have_content "/move #{target_project.full_path}"
expect(issue.reload).to be_closed
visit project_issue_path(target_project, issue)
wait_for_all_requests
expect(page).to have_content 'Issues 1'
end
end
end end
end end
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