Unverified Commit 08bbb9fc authored by Douwe Maan's avatar Douwe Maan Committed by Luke "Jared" Bennett

Add option to start a new discussion on an MR

parent 8bdfee8b
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
let $commentButtonTemplate; let $commentButtonTemplate;
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
window.FilesCommentButton = (function() { this.FilesCommentButton = (function() {
var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
COMMENT_BUTTON_CLASS = '.add-diff-note'; COMMENT_BUTTON_CLASS = '.add-diff-note';
...@@ -55,14 +55,19 @@ window.FilesCommentButton = (function() { ...@@ -55,14 +55,19 @@ window.FilesCommentButton = (function() {
textFileElement = this.getTextFileElement($currentTarget); textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({ buttonParentElement.append(this.buildButton({
discussionID: lineContentElement.attr('data-discussion-id'),
lineType: lineContentElement.attr('data-line-type'),
noteableType: textFileElement.attr('data-noteable-type'), noteableType: textFileElement.attr('data-noteable-type'),
noteableID: textFileElement.attr('data-noteable-id'), noteableID: textFileElement.attr('data-noteable-id'),
commitID: textFileElement.attr('data-commit-id'), commitID: textFileElement.attr('data-commit-id'),
noteType: lineContentElement.attr('data-note-type'), noteType: lineContentElement.attr('data-note-type'),
position: lineContentElement.attr('data-position'),
lineType: lineContentElement.attr('data-line-type'), // LegacyDiffNote
discussionID: lineContentElement.attr('data-discussion-id'), lineCode: lineContentElement.attr('data-line-code'),
lineCode: lineContentElement.attr('data-line-code')
// DiffNote
position: lineContentElement.attr('data-position')
})); }));
}; };
...@@ -76,14 +81,19 @@ window.FilesCommentButton = (function() { ...@@ -76,14 +81,19 @@ window.FilesCommentButton = (function() {
FilesCommentButton.prototype.buildButton = function(buttonAttributes) { FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
return $commentButtonTemplate.clone().attr({ return $commentButtonTemplate.clone().attr({
'data-discussion-id': buttonAttributes.discussionID,
'data-line-type': buttonAttributes.lineType,
'data-noteable-type': buttonAttributes.noteableType, 'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID, 'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID, 'data-commit-id': buttonAttributes.commitID,
'data-note-type': buttonAttributes.noteType, 'data-note-type': buttonAttributes.noteType,
// LegacyDiffNote
'data-line-code': buttonAttributes.lineCode, 'data-line-code': buttonAttributes.lineCode,
'data-position': buttonAttributes.position,
'data-discussion-id': buttonAttributes.discussionID, // DiffNote
'data-line-type': buttonAttributes.lineType 'data-position': buttonAttributes.position
}); });
}; };
......
...@@ -213,11 +213,7 @@ require('./task_list'); ...@@ -213,11 +213,7 @@ require('./task_list');
_this.last_fetched_at = data.last_fetched_at; _this.last_fetched_at = data.last_fetched_at;
_this.setPollingInterval(data.notes.length); _this.setPollingInterval(data.notes.length);
return $.each(notes, function(i, note) { return $.each(notes, function(i, note) {
if (note.discussion_html != null) { _this.renderNote(note);
return _this.renderDiscussionNote(note);
} else {
return _this.renderNote(note);
}
}); });
}; };
})(this) })(this)
...@@ -278,6 +274,10 @@ require('./task_list'); ...@@ -278,6 +274,10 @@ require('./task_list');
Notes.prototype.renderNote = function(note) { Notes.prototype.renderNote = function(note) {
var $notesList; var $notesList;
if (note.discussion_html != null) {
return this.renderDiscussionNote(note);
}
if (!note.valid) { if (!note.valid) {
if (note.errors.commands_only) { if (note.errors.commands_only) {
new Flash(note.errors.commands_only, 'notice', this.parentTimeline); new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
...@@ -323,9 +323,9 @@ require('./task_list'); ...@@ -323,9 +323,9 @@ require('./task_list');
return; return;
} }
this.note_ids.push(note.id); this.note_ids.push(note.id);
form = $("#new-discussion-note-form-" + note.discussion_id); form = $(".js-discussion-note-form[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && form.length === 0) { if (form.length === 0) {
form = $("#new-discussion-note-form-" + note.original_discussion_id); form = $(".js-discussion-note-form[data-original-discussion-id='" + note.original_discussion_id + "']");
} }
row = form.closest("tr"); row = form.closest("tr");
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
...@@ -334,8 +334,8 @@ require('./task_list'); ...@@ -334,8 +334,8 @@ require('./task_list');
note_html.renderGFM(); note_html.renderGFM();
// is this the first note of discussion? // is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && discussionContainer.length === 0) { if (discussionContainer.length === 0) {
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']"); discussionContainer = $(".notes[data-original-discussion-id='" + note.original_discussion_id + "']");
} }
if (discussionContainer.length === 0) { if (discussionContainer.length === 0) {
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
...@@ -363,7 +363,7 @@ require('./task_list'); ...@@ -363,7 +363,7 @@ require('./task_list');
// Add note to 'Changes' page discussions // Add note to 'Changes' page discussions
discussionContainer.append(note_html); discussionContainer.append(note_html);
// Init discussion on 'Discussion' page if it is merge request page // Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) { if ($('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
$('ul.main-notes-list').append(note.discussion_html).renderGFM(); $('ul.main-notes-list').append(note.discussion_html).renderGFM();
} }
} else { } else {
...@@ -456,6 +456,7 @@ require('./task_list'); ...@@ -456,6 +456,7 @@ require('./task_list');
form.find("#note_line_code").remove(); form.find("#note_line_code").remove();
form.find("#note_position").remove(); form.find("#note_position").remove();
form.find("#note_type").remove(); form.find("#note_type").remove();
form.find("#note_in_reply_to_discussion_id").remove();
form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
return this.parentTimeline = form.parents('.timeline'); return this.parentTimeline = form.parents('.timeline');
}; };
...@@ -470,10 +471,24 @@ require('./task_list'); ...@@ -470,10 +471,24 @@ require('./task_list');
*/ */
Notes.prototype.setupNoteForm = function(form) { Notes.prototype.setupNoteForm = function(form) {
var textarea; var textarea, key;
new gl.GLForm(form); new gl.GLForm(form);
textarea = form.find(".js-note-text"); textarea = form.find(".js-note-text");
return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]); key = [
"Note",
form.find("#note_noteable_type").val(),
form.find("#note_noteable_id").val(),
form.find("#note_commit_id").val(),
form.find("#note_type").val(),
form.find("#in_reply_to_discussion_id").val(),
// LegacyDiffNote
form.find("#note_line_code").val(),
// DiffNote
form.find("#note_position").val()
];
return new Autosave(textarea, key);
}; };
/* /*
...@@ -510,7 +525,7 @@ require('./task_list'); ...@@ -510,7 +525,7 @@ require('./task_list');
} }
} }
this.renderDiscussionNote(note); this.renderNote(note);
// cleanup after successfully creating a diff/discussion note // cleanup after successfully creating a diff/discussion note
this.removeDiscussionNoteForm($form); this.removeDiscussionNoteForm($form);
}; };
...@@ -727,23 +742,35 @@ require('./task_list'); ...@@ -727,23 +742,35 @@ require('./task_list');
Sets some hidden fields in the form. Sets some hidden fields in the form.
Note: dataHolder must have the "discussionId", "lineCode", "noteableType" Note: dataHolder must have the "discussionId" and "lineCode" data attributes set.
and "noteableId" data attributes set.
*/ */
Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) { Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
// setup note target // setup note target
form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId"))); var discussionID = dataHolder.data("discussionId");
form.attr('id', "new-discussion-note-form-" + discussionID);
form.attr("data-discussion-id", discussionID);
form.attr("data-original-discussion-id", dataHolder.data("originalDiscussionId") || discussionID);
form.attr("data-line-code", dataHolder.data("lineCode")); form.attr("data-line-code", dataHolder.data("lineCode"));
form.find("#note_type").val(dataHolder.data("noteType"));
form.find("#line_type").val(dataHolder.data("lineType")); form.find("#line_type").val(dataHolder.data("lineType"));
form.find("#in_reply_to_discussion_id").val(dataHolder.data("originalDiscussionId"));
form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
form.find("#note_commit_id").val(dataHolder.data("commitId")); form.find("#note_commit_id").val(dataHolder.data("commitId"));
form.find("#note_type").val(dataHolder.data("noteType"));
// LegacyDiffNote
form.find("#note_line_code").val(dataHolder.data("lineCode")); form.find("#note_line_code").val(dataHolder.data("lineCode"));
// DiffNote
form.find("#note_position").val(dataHolder.attr("data-position")); form.find("#note_position").val(dataHolder.attr("data-position"));
form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text')); form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
form.find('.js-note-target-close').remove(); form.find('.js-note-target-close').remove();
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form); this.setupNoteForm(form);
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# #
# Not to be confused with CommitsController, plural. # Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController class Projects::CommitController < Projects::ApplicationController
include NotesHelper
include CreatesCommit include CreatesCommit
include DiffForPath include DiffForPath
include DiffHelper include DiffHelper
...@@ -111,22 +112,19 @@ def define_commit_vars ...@@ -111,22 +112,19 @@ def define_commit_vars
end end
def define_note_vars def define_note_vars
@grouped_diff_discussions = commit.notes.grouped_diff_discussions @noteable = @commit
@notes = commit.notes.non_diff_notes.fresh
Banzai::NoteRenderer.render(
@grouped_diff_discussions.values.flat_map(&:notes) + @notes,
@project,
current_user,
)
@note = @project.build_commit_note(commit) @note = @project.build_commit_note(commit)
@noteable = @commit @new_diff_note_attrs = {
@comments_target = {
noteable_type: 'Commit', noteable_type: 'Commit',
commit_id: @commit.id commit_id: @commit.id
} }
@grouped_diff_discussions = commit.grouped_diff_discussions
@discussions = commit.discussions
@notes = (@grouped_diff_discussions.values + @discussions).flat_map(&:notes)
@notes = prepare_notes_for_rendering(@notes)
end end
def assign_change_commit_vars def assign_change_commit_vars
......
...@@ -28,7 +28,7 @@ def merge_request ...@@ -28,7 +28,7 @@ def merge_request
end end
def discussion def discussion
@discussion ||= @merge_request.find_diff_discussion(params[:id]) || render_404 @discussion ||= @merge_request.find_discussion(params[:id]) || render_404
end end
def authorize_resolve_discussion! def authorize_resolve_discussion!
......
...@@ -84,15 +84,11 @@ def edit ...@@ -84,15 +84,11 @@ def edit
end end
def show def show
raw_notes = @issue.notes.inc_relations_for_view.fresh
@notes = Banzai::NoteRenderer.
render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
@note = @project.notes.new(noteable: @issue)
@noteable = @issue @noteable = @issue
@note = @project.notes.new(noteable: @issue)
preload_max_access_for_authors(@notes, @project) @discussions = @issue.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -570,20 +570,7 @@ def define_discussion_vars ...@@ -570,20 +570,7 @@ def define_discussion_vars
@note = @project.notes.new(noteable: @merge_request) @note = @project.notes.new(noteable: @merge_request)
@discussions = @merge_request.discussions @discussions = @merge_request.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
preload_noteable_for_regular_notes(@discussions.flat_map(&:notes))
# This is not executed lazily
@notes = Banzai::NoteRenderer.render(
@discussions.flat_map(&:notes),
@project,
current_user,
@path,
@project_wiki,
@ref
)
preload_max_access_for_authors(@notes, @project)
end end
def define_widget_vars def define_widget_vars
...@@ -596,22 +583,15 @@ def define_commit_vars ...@@ -596,22 +583,15 @@ def define_commit_vars
end end
def define_diff_comment_vars def define_diff_comment_vars
@comments_target = { @new_diff_note_attrs = {
noteable_type: 'MergeRequest', noteable_type: 'MergeRequest',
noteable_id: @merge_request.id noteable_id: @merge_request.id
} }
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs? @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
@grouped_diff_discussions = @merge_request.notes.inc_relations_for_view.grouped_diff_discussions
@grouped_diff_discussions = @merge_request.grouped_diff_discussions
Banzai::NoteRenderer.render( @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flat_map(&:notes))
@grouped_diff_discussions.values.flat_map(&:notes),
@project,
current_user,
@path,
@project_wiki,
@ref
)
end end
def define_pipelines_vars def define_pipelines_vars
......
...@@ -6,13 +6,14 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -6,13 +6,14 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve] before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
before_action :find_current_user_notes, only: [:index]
def index def index
current_fetched_at = Time.now.to_i current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at } notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes = notes_finder.execute.inc_author
@notes.each do |note| @notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user) next if note.cross_reference_not_visible_for?(current_user)
...@@ -23,7 +24,11 @@ def index ...@@ -23,7 +24,11 @@ def index
end end
def create def create
create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha]) create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id],
new_discussion: params[:new_discussion],
)
@note = Notes::CreateService.new(project, current_user, create_params).execute @note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note) if @note.is_a?(Note)
...@@ -111,6 +116,17 @@ def note_html(note) ...@@ -111,6 +116,17 @@ def note_html(note)
) )
end end
def discussion_html(discussion)
return if discussion.render_as_individual_notes?
render_to_string(
"discussions/_discussion",
layout: false,
formats: [:html],
locals: { discussion: discussion }
)
end
def diff_discussion_html(discussion) def diff_discussion_html(discussion)
return unless discussion.diff_discussion? return unless discussion.diff_discussion?
...@@ -135,17 +151,6 @@ def diff_discussion_html(discussion) ...@@ -135,17 +151,6 @@ def diff_discussion_html(discussion)
) )
end end
def discussion_html(discussion)
return unless discussion.diff_discussion?
render_to_string(
"discussions/_discussion",
layout: false,
formats: [:html],
locals: { discussion: discussion }
)
end
def note_json(note) def note_json(note)
attrs = { attrs = {
id: note.id id: note.id
...@@ -156,33 +161,22 @@ def note_json(note) ...@@ -156,33 +161,22 @@ def note_json(note)
attrs.merge!( attrs.merge!(
valid: true, valid: true,
discussion_id: note.discussion_id, discussion_id: note.discussion_id(noteable),
html: note_html(note), html: note_html(note),
note: note.note note: note.note
) )
if note.diff_note? discussion = note.to_discussion(noteable)
discussion = note.to_discussion unless discussion.render_as_individual_notes?
attrs.merge!( attrs.merge!(
diff_discussion_html: diff_discussion_html(discussion), diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion) discussion_html: discussion_html(discussion),
)
# The discussion_id is used to add the comment to the correct discussion # Since the `discussion_id` can change, for example when new commits are pushed into an MR,
# element on the merge request page. Among other things, the discussion_id # the never-changing `original_discussion_id` is used as a fallback to the find the relevant
# contains the sha of head commit of the merge request. # discussion container to add this note to.
# When new commits are pushed into the merge request after the initial original_discussion_id: note.original_discussion_id
# load of the merge request page, the discussion elements will still have )
# the old discussion_ids, with the old head commit sha. The new comment,
# however, will have the new discussion_id with the new commit sha.
# To ensure that these new comments will still end up in the correct
# discussion element, we also send the original discussion_id, with the
# old commit sha, along, and fall back on this value when no discussion
# element with the new discussion_id could be found.
if note.new_diff_note? && note.position != note.original_position
attrs[:original_discussion_id] = note.original_discussion_id
end
end end
else else
attrs.merge!( attrs.merge!(
...@@ -205,14 +199,30 @@ def authorize_resolve_note! ...@@ -205,14 +199,30 @@ def authorize_resolve_note!
def note_params def note_params
params.require(:note).permit(