Commit 421108c6 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'dm-create-note-return-discussion' into 'master'

Increase performance when creating discussion on diff

Closes #49002

See merge request gitlab-org/gitlab-ce!21743
parents e9b13d18 21f02674
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import noteForm from '../../notes/components/note_form.vue'; import noteForm from '../../notes/components/note_form.vue';
import { getNoteFormData } from '../store/utils';
import autosave from '../../notes/mixins/autosave'; import autosave from '../../notes/mixins/autosave';
import { DIFF_NOTE_TYPE } from '../constants'; import { DIFF_NOTE_TYPE } from '../constants';
import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils';
export default { export default {
components: { components: {
...@@ -39,6 +36,16 @@ export default { ...@@ -39,6 +36,16 @@ export default {
}), }),
...mapGetters('diffs', ['getDiffFileByHash']), ...mapGetters('diffs', ['getDiffFileByHash']),
...mapGetters(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']), ...mapGetters(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']),
formData() {
return {
noteableData: this.noteableData,
noteableType: this.noteableType,
noteTargetLine: this.noteTargetLine,
diffViewType: this.diffViewType,
diffFile: this.getDiffFileByHash(this.diffFileHash),
linePosition: this.linePosition,
};
},
}, },
mounted() { mounted() {
if (this.isLoggedIn) { if (this.isLoggedIn) {
...@@ -53,8 +60,7 @@ export default { ...@@ -53,8 +60,7 @@ export default {
} }
}, },
methods: { methods: {
...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff']), ...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff', 'saveDiffDiscussion']),
...mapActions(['saveNote', 'refetchDiscussionById']),
handleCancelCommentForm(shouldConfirm, isDirty) { handleCancelCommentForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) { if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?'); const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
...@@ -73,35 +79,9 @@ export default { ...@@ -73,35 +79,9 @@ export default {
}); });
}, },
handleSaveNote(note) { handleSaveNote(note) {
const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash); return this.saveDiffDiscussion({ note, formData: this.formData }).then(() =>
const postData = getNoteFormData({ this.handleCancelCommentForm(),
note, );
noteableData: this.noteableData,
noteableType: this.noteableType,
noteTargetLine: this.noteTargetLine,
diffViewType: this.diffViewType,
diffFile: selectedDiffFile,
linePosition: this.linePosition,
});
this.saveNote(postData)
.then(result => {
const endpoint = this.getNotesDataByProp('discussionsPath');
this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
.then(selectedDiscussion => {
const lineCodeDiscussions = reduceDiscussionsToLineCodes([selectedDiscussion]);
this.assignDiscussionsToDiff(lineCodeDiscussions);
this.handleCancelCommentForm();
})
.catch(() => {
createFlash(s__('MergeRequests|Updating discussions failed'));
});
})
.catch(() => {
createFlash(s__('MergeRequests|Saving the comment failed'));
});
}, },
}, },
}; };
......
import Vue from 'vue'; import Vue from 'vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils'; import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility'; import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
import { getDiffPositionByLineCode } from './utils'; import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils';
import { getDiffPositionByLineCode, getNoteFormData } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
PARALLEL_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE,
...@@ -178,5 +181,19 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => { ...@@ -178,5 +181,19 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
}); });
}; };
export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
const postData = getNoteFormData({
note,
...formData,
});
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion =>
dispatch('assignDiscussionsToDiff', reduceDiscussionsToLineCodes([discussion])),
)
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -55,6 +55,7 @@ export function getNoteFormData(params) { ...@@ -55,6 +55,7 @@ export function getNoteFormData(params) {
note_project_id: '', note_project_id: '',
target_type: noteableData.targetType, target_type: noteableData.targetType,
target_id: noteableData.id, target_id: noteableData.id,
return_discussion: true,
note: { note: {
note, note,
position, position,
......
...@@ -44,23 +44,11 @@ export const fetchDiscussions = ({ commit }, path) => ...@@ -44,23 +44,11 @@ export const fetchDiscussions = ({ commit }, path) =>
commit(types.SET_INITIAL_DISCUSSIONS, discussions); commit(types.SET_INITIAL_DISCUSSIONS, discussions);
}); });
export const refetchDiscussionById = ({ commit, state }, { path, discussionId }) => export const updateDiscussion = ({ commit, state }, discussion) => {
new Promise(resolve => { commit(types.UPDATE_DISCUSSION, discussion);
service
.fetchDiscussions(path) return utils.findNoteObjectById(state.discussions, discussion.id);
.then(res => res.json()) };
.then(discussions => {
const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId);
if (selectedDiscussion) {
commit(types.UPDATE_DISCUSSION, selectedDiscussion);
// We need to refetch as it is now the transformed one in state
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
resolve(discussion);
}
})
.catch(() => {});
});
export const deleteNote = ({ commit, dispatch }, note) => export const deleteNote = ({ commit, dispatch }, note) =>
service.deleteNote(note.path).then(() => { service.deleteNote(note.path).then(() => {
......
...@@ -4,7 +4,8 @@ import * as constants from '../constants'; ...@@ -4,7 +4,8 @@ import * as constants from '../constants';
import { isInMRPage } from '../../lib/utils/common_utils'; import { isInMRPage } from '../../lib/utils/common_utils';
export default { export default {
[types.ADD_NEW_NOTE](state, note) { [types.ADD_NEW_NOTE](state, data) {
const note = data.discussion ? data.discussion.notes[0] : data;
const { discussion_id, type } = note; const { discussion_id, type } = note;
const [exists] = state.discussions.filter(n => n.id === note.discussion_id); const [exists] = state.discussions.filter(n => n.id === note.discussion_id);
const isDiscussion = type === constants.DISCUSSION_NOTE || type === constants.DIFF_NOTE; const isDiscussion = type === constants.DISCUSSION_NOTE || type === constants.DIFF_NOTE;
......
...@@ -43,12 +43,26 @@ module NotesActions ...@@ -43,12 +43,26 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute @note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note)
prepare_notes_for_rendering([@note], noteable)
end
respond_to do |format| respond_to do |format|
format.json { render json: note_json(@note) } format.json do
json = {
commands_changes: @note.commands_changes
}
if @note.persisted? && return_discussion?
json[:valid] = true
discussion = @note.discussion
prepare_notes_for_rendering(discussion.notes)
json[:discussion] = discussion_serializer.represent(discussion, context: self)
else
prepare_notes_for_rendering([@note])
json.merge!(note_json(@note))
end
render json: json
end
format.html { redirect_back_or_default } format.html { redirect_back_or_default }
end end
end end
...@@ -57,10 +71,7 @@ module NotesActions ...@@ -57,10 +71,7 @@ module NotesActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def update def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note) @note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
prepare_notes_for_rendering([@note])
if @note.is_a?(Note)
prepare_notes_for_rendering([@note])
end
respond_to do |format| respond_to do |format|
format.json { render json: note_json(@note) } format.json { render json: note_json(@note) }
...@@ -91,14 +102,17 @@ module NotesActions ...@@ -91,14 +102,17 @@ module NotesActions
end end
def note_json(note) def note_json(note)
attrs = { attrs = {}
commands_changes: note.commands_changes
}
if note.persisted? if note.persisted?
attrs[:valid] = true attrs[:valid] = true
if use_note_serializer? if return_discussion?
discussion = note.discussion
prepare_notes_for_rendering(discussion.notes)
attrs[:discussion] = discussion_serializer.represent(discussion, context: self)
elsif use_note_serializer?
attrs.merge!(note_serializer.represent(note)) attrs.merge!(note_serializer.represent(note))
else else
attrs.merge!( attrs.merge!(
...@@ -218,6 +232,10 @@ module NotesActions ...@@ -218,6 +232,10 @@ module NotesActions
ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user) ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
end end
def discussion_serializer
DiscussionSerializer.new(project: project, noteable: noteable, current_user: current_user, note_entity: ProjectNoteEntity)
end
def note_project def note_project
strong_memoize(:note_project) do strong_memoize(:note_project) do
next nil unless project next nil unless project
...@@ -237,6 +255,10 @@ module NotesActions ...@@ -237,6 +255,10 @@ module NotesActions
end end
end end
def return_discussion?
Gitlab::Utils.to_boolean(params[:return_discussion])
end
def use_note_serializer? def use_note_serializer?
return false if params['html'] return false if params['html']
......
---
title: Increase performance when creating discussions on diff
merge_request:
author:
type: performance
...@@ -3690,9 +3690,6 @@ msgstr "" ...@@ -3690,9 +3690,6 @@ msgstr ""
msgid "MergeRequests|Toggle comments for this file" msgid "MergeRequests|Toggle comments for this file"
msgstr "" msgstr ""
msgid "MergeRequests|Updating discussions failed"
msgstr ""
msgid "MergeRequests|View file @ %{commitId}" msgid "MergeRequests|View file @ %{commitId}"
msgstr "" msgstr ""
......
...@@ -207,6 +207,14 @@ describe Projects::NotesController do ...@@ -207,6 +207,14 @@ describe Projects::NotesController do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it 'returns discussion JSON when the return_discussion param is set' do
post :create, request_params.merge(format: :json, return_discussion: 'true')
expect(response).to have_gitlab_http_status(200)
expect(json_response).to have_key 'discussion'
expect(json_response['discussion']['notes'][0]['note']).to eq(request_params[:note][:note])
end
context 'when merge_request_diff_head_sha present' do context 'when merge_request_diff_head_sha present' do
before do before do
service_params = { service_params = {
......
...@@ -69,22 +69,21 @@ describe('DiffLineNoteForm', () => { ...@@ -69,22 +69,21 @@ describe('DiffLineNoteForm', () => {
describe('saveNoteForm', () => { describe('saveNoteForm', () => {
it('should call saveNote action with proper params', done => { it('should call saveNote action with proper params', done => {
let isPromiseCalled = false; const saveDiffDiscussionSpy = spyOn(component, 'saveDiffDiscussion').and.returnValue(
const formDataSpy = spyOnDependency(DiffLineNoteForm, 'getNoteFormData').and.returnValue({ Promise.resolve(),
postData: 1,
});
const saveNoteSpy = spyOn(component, 'saveNote').and.returnValue(
new Promise(() => {
isPromiseCalled = true;
done();
}),
); );
spyOnProperty(component, 'formData').and.returnValue('formData');
component.handleSaveNote('note body');
component
expect(formDataSpy).toHaveBeenCalled(); .handleSaveNote('note body')
expect(saveNoteSpy).toHaveBeenCalled(); .then(() => {
expect(isPromiseCalled).toEqual(true); expect(saveDiffDiscussionSpy).toHaveBeenCalledWith({
note: 'note body',
formData: 'formData',
});
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
......
...@@ -21,6 +21,7 @@ import actions, { ...@@ -21,6 +21,7 @@ import actions, {
loadCollapsedDiff, loadCollapsedDiff,
expandAllFiles, expandAllFiles,
toggleFileDiscussions, toggleFileDiscussions,
saveDiffDiscussion,
} from '~/diffs/store/actions'; } from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types'; import * as types from '~/diffs/store/mutation_types';
import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils'; import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils';
...@@ -245,7 +246,7 @@ describe('DiffsStoreActions', () => { ...@@ -245,7 +246,7 @@ describe('DiffsStoreActions', () => {
}); });
describe('startRenderDiffsQueue', () => { describe('startRenderDiffsQueue', () => {
it('should set all files to RENDER_FILE', done => { it('should set all files to RENDER_FILE', () => {
const state = { const state = {
diffFiles: [ diffFiles: [
{ {
...@@ -268,16 +269,10 @@ describe('DiffsStoreActions', () => { ...@@ -268,16 +269,10 @@ describe('DiffsStoreActions', () => {
}); });
}; };
startRenderDiffsQueue({ state, commit: pseudoCommit }) startRenderDiffsQueue({ state, commit: pseudoCommit });
.then(() => {
expect(state.diffFiles[0].renderIt).toBeTruthy();
expect(state.diffFiles[1].renderIt).toBeTruthy();
done(); expect(state.diffFiles[0].renderIt).toBe(true);
}) expect(state.diffFiles[1].renderIt).toBe(true);
.catch(() => {
done.fail();
});
}); });
}); });
...@@ -582,4 +577,35 @@ describe('DiffsStoreActions', () => { ...@@ -582,4 +577,35 @@ describe('DiffsStoreActions', () => {
expect(handleLocationHashSpy).toHaveBeenCalledTimes(1); expect(handleLocationHashSpy).toHaveBeenCalledTimes(1);
}); });
}); });
describe('saveDiffDiscussion', () => {
beforeEach(() => {
spyOnDependency(actions, 'getNoteFormData').and.returnValue('testData');
spyOnDependency(actions, 'reduceDiscussionsToLineCodes').and.returnValue('discussions');
});
it('dispatches actions', done => {
const dispatch = jasmine.createSpy('dispatch').and.callFake(name => {
switch (name) {
case 'saveNote':
return Promise.resolve({
discussion: 'test',
});
case 'updateDiscussion':
return Promise.resolve('discussion');
default:
return Promise.resolve({});
}
});
saveDiffDiscussion({ dispatch }, { note: {}, formData: {} })
.then(() => {
expect(dispatch.calls.argsFor(0)).toEqual(['saveNote', 'testData', { root: true }]);
expect(dispatch.calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]);
expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', 'discussions']);
})
.then(done)
.catch(done.fail);
});
});
}); });
...@@ -136,6 +136,7 @@ describe('DiffsStoreUtils', () => { ...@@ -136,6 +136,7 @@ describe('DiffsStoreUtils', () => {
note_project_id: '', note_project_id: '',
target_type: options.noteableType, target_type: options.noteableType,
target_id: options.noteableData.id, target_id: options.noteableData.id,
return_discussion: true,
note: { note: {
noteable_type: options.noteableType, noteable_type: options.noteableType,
noteable_id: options.noteableData.id, noteable_id: options.noteableData.id,
...@@ -194,6 +195,7 @@ describe('DiffsStoreUtils', () => { ...@@ -194,6 +195,7 @@ describe('DiffsStoreUtils', () => {
note_project_id: '', note_project_id: '',
target_type: options.noteableType, target_type: options.noteableType,
target_id: options.noteableData.id, target_id: options.noteableData.id,
return_discussion: true,
note: { note: {
noteable_type: options.noteableType, noteable_type: options.noteableType,
noteable_id: options.noteableData.id, noteable_id: options.noteableData.id,
......
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