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 @@
let $commentButtonTemplate;
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;
COMMENT_BUTTON_CLASS = '.add-diff-note';
......@@ -55,14 +55,19 @@ window.FilesCommentButton = (function() {
textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({
discussionID: lineContentElement.attr('data-discussion-id'),
lineType: lineContentElement.attr('data-line-type'),
noteableType: textFileElement.attr('data-noteable-type'),
noteableID: textFileElement.attr('data-noteable-id'),
commitID: textFileElement.attr('data-commit-id'),
noteType: lineContentElement.attr('data-note-type'),
position: lineContentElement.attr('data-position'),
lineType: lineContentElement.attr('data-line-type'),
discussionID: lineContentElement.attr('data-discussion-id'),
lineCode: lineContentElement.attr('data-line-code')
// LegacyDiffNote
lineCode: lineContentElement.attr('data-line-code'),
// DiffNote
position: lineContentElement.attr('data-position')
}));
};
......@@ -76,14 +81,19 @@ window.FilesCommentButton = (function() {
FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
return $commentButtonTemplate.clone().attr({
'data-discussion-id': buttonAttributes.discussionID,
'data-line-type': buttonAttributes.lineType,
'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID,
'data-note-type': buttonAttributes.noteType,
// LegacyDiffNote
'data-line-code': buttonAttributes.lineCode,
'data-position': buttonAttributes.position,
'data-discussion-id': buttonAttributes.discussionID,
'data-line-type': buttonAttributes.lineType
// DiffNote
'data-position': buttonAttributes.position
});
};
......
......@@ -213,11 +213,7 @@ require('./task_list');
_this.last_fetched_at = data.last_fetched_at;
_this.setPollingInterval(data.notes.length);
return $.each(notes, function(i, note) {
if (note.discussion_html != null) {
return _this.renderDiscussionNote(note);
} else {
return _this.renderNote(note);
}
_this.renderNote(note);
});
};
})(this)
......@@ -278,6 +274,10 @@ require('./task_list');
Notes.prototype.renderNote = function(note) {
var $notesList;
if (note.discussion_html != null) {
return this.renderDiscussionNote(note);
}
if (!note.valid) {
if (note.errors.commands_only) {
new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
......@@ -323,9 +323,9 @@ require('./task_list');
return;
}
this.note_ids.push(note.id);
form = $("#new-discussion-note-form-" + note.discussion_id);
if ((note.original_discussion_id != null) && form.length === 0) {
form = $("#new-discussion-note-form-" + note.original_discussion_id);
form = $(".js-discussion-note-form[data-discussion-id='" + note.discussion_id + "']");
if (form.length === 0) {
form = $(".js-discussion-note-form[data-original-discussion-id='" + note.original_discussion_id + "']");
}
row = form.closest("tr");
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
......@@ -334,8 +334,8 @@ require('./task_list');
note_html.renderGFM();
// is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
if (discussionContainer.length === 0) {
discussionContainer = $(".notes[data-original-discussion-id='" + note.original_discussion_id + "']");
}
if (discussionContainer.length === 0) {
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
......@@ -363,7 +363,7 @@ require('./task_list');
// Add note to 'Changes' page discussions
discussionContainer.append(note_html);
// 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();
}
} else {
......@@ -456,6 +456,7 @@ require('./task_list');
form.find("#note_line_code").remove();
form.find("#note_position").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();
return this.parentTimeline = form.parents('.timeline');
};
......@@ -470,10 +471,24 @@ require('./task_list');
*/
Notes.prototype.setupNoteForm = function(form) {
var textarea;
var textarea, key;
new gl.GLForm(form);
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');
}
}
this.renderDiscussionNote(note);
this.renderNote(note);
// cleanup after successfully creating a diff/discussion note
this.removeDiscussionNoteForm($form);
};
......@@ -727,23 +742,35 @@ require('./task_list');
Sets some hidden fields in the form.
Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
and "noteableId" data attributes set.
Note: dataHolder must have the "discussionId" and "lineCode" data attributes set.
*/
Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
// 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.find("#note_type").val(dataHolder.data("noteType"));
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_type").val(dataHolder.data("noteType"));
// LegacyDiffNote
form.find("#note_line_code").val(dataHolder.data("lineCode"));
// DiffNote
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-target-close').remove();
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
......
......@@ -2,6 +2,7 @@
#
# Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController
include NotesHelper
include CreatesCommit
include DiffForPath
include DiffHelper
......@@ -111,22 +112,19 @@ class Projects::CommitController < Projects::ApplicationController
end
def define_note_vars
@grouped_diff_discussions = commit.notes.grouped_diff_discussions
@notes = commit.notes.non_diff_notes.fresh
Banzai::NoteRenderer.render(
@grouped_diff_discussions.values.flat_map(&:notes) + @notes,
@project,
current_user,
)
@noteable = @commit
@note = @project.build_commit_note(commit)
@noteable = @commit
@comments_target = {
@new_diff_note_attrs = {
noteable_type: 'Commit',
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
def assign_change_commit_vars
......
......@@ -28,7 +28,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
end
def discussion
@discussion ||= @merge_request.find_diff_discussion(params[:id]) || render_404
@discussion ||= @merge_request.find_discussion(params[:id]) || render_404
end
def authorize_resolve_discussion!
......
......@@ -84,15 +84,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
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
@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|
format.html
......
......@@ -570,20 +570,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@note = @project.notes.new(noteable: @merge_request)
@discussions = @merge_request.discussions
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)
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end
def define_widget_vars
......@@ -596,22 +583,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def define_diff_comment_vars
@comments_target = {
@new_diff_note_attrs = {
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
}
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
@grouped_diff_discussions = @merge_request.notes.inc_relations_for_view.grouped_diff_discussions
Banzai::NoteRenderer.render(
@grouped_diff_discussions.values.flat_map(&:notes),
@project,
current_user,
@path,
@project_wiki,
@ref
)
@grouped_diff_discussions = @merge_request.grouped_diff_discussions
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flat_map(&:notes))
end
def define_pipelines_vars
......
......@@ -6,13 +6,14 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
before_action :find_current_user_notes, only: [:index]
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes = notes_finder.execute.inc_author
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
......@@ -23,7 +24,11 @@ class Projects::NotesController < Projects::ApplicationController
end
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
if @note.is_a?(Note)
......@@ -111,6 +116,17 @@ class Projects::NotesController < Projects::ApplicationController
)
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)
return unless discussion.diff_discussion?
......@@ -135,17 +151,6 @@ class Projects::NotesController < Projects::ApplicationController
)
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)
attrs = {
id: note.id
......@@ -156,33 +161,22 @@ class Projects::NotesController < Projects::ApplicationController
attrs.merge!(
valid: true,
discussion_id: note.discussion_id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
if note.diff_note?
discussion = note.to_discussion
discussion = note.to_discussion(noteable)
unless discussion.render_as_individual_notes?
attrs.merge!(
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
# element on the merge request page. Among other things, the discussion_id
# contains the sha of head commit of the merge request.
# When new commits are pushed into the merge request after the initial
# 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
# Since the `discussion_id` can change, for example when new commits are pushed into an MR,
# the never-changing `original_discussion_id` is used as a fallback to the find the relevant
# discussion container to add this note to.
original_discussion_id: note.original_discussion_id
)
end
else
attrs.merge!(
......@@ -205,14 +199,30 @@ class Projects::NotesController < Projects::ApplicationController
def note_params
params.require(:note).permit(
:note, :noteable, :noteable_id, :noteable_type, :project_id,
:attachment, :line_code, :commit_id, :type, :position
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def find_current_user_notes
@notes = NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
.execute.inc_author
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
......
class Projects::SnippetsController < Projects::ApplicationController
include NotesHelper
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
......@@ -55,8 +56,10 @@ class Projects::SnippetsController < Projects::ApplicationController
def show
@note = @project.notes.new(noteable: @snippet)
@notes = Banzai::NoteRenderer.render(@snippet.notes.fresh, @project, current_user)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end
def destroy
......
......@@ -17,29 +17,46 @@ class NotesFinder
@project = project
@current_user = current_user
@params = params
init_collection
end
def execute
@notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at]
@notes = init_collection
@notes = since_fetch_at(@params[:last_fetched_at], @notes) if @params[:last_fetched_at]
@notes
end
private
def target
return @target if defined?(@target)
def init_collection
@notes =
if @params[:target_id]
on_target(@params[:target_type], @params[:target_id])
target_type = @params[:target_type]
target_id = @params[:target_id]
return @target = nil unless target_type && target_id
@target =
if target_type == "commit"
if Ability.allowed?(@current_user, :download_code, @project)
@project.commit(target_id)
end
else
notes_of_any_type
noteables_for_type(target_type).find(target_id)
end
end
private
def init_collection
if target
notes_on_target
else
notes_of_any_type
end
end
def notes_of_any_type
types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) }
note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search]
note_relations.map! { |notes| search(@params[:search], notes) } if @params[:search]
UnionFinder.new.find_union(note_relations, Note)
end
......@@ -69,17 +86,11 @@ class NotesFinder
end
end
def on_target(target_type, target_id)
if target_type == "commit"
notes_for_type('commit').for_commit_id(target_id)
def notes_on_target
if target.respond_to?(:related_notes)
target.related_notes
else
target = noteables_for_type(target_type).find(target_id)
if target.respond_to?(:related_notes)
target.related_notes
else
target.notes
end
target.notes
end
end
......@@ -94,10 +105,9 @@ class NotesFinder
# Notes changed since last fetch
# Uses overlapping intervals to avoid worrying about race conditions
def since_fetch_at(fetch_time)
def since_fetch_at(fetch_time, notes_relation = @notes)
# Default to 0 to remain compatible with old clients
last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
@notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
notes_relation.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
end
end
......@@ -24,9 +24,9 @@ module NotesHelper
end
def diff_view_data
return {} unless @comments_target
return {} unless @new_diff_note_attrs
@comments_target.slice(:noteable_id, :noteable_type, :commit_id)
@new_diff_note_attrs.slice(:noteable_id, :noteable_type, :commit_id)
end
def diff_view_line_data(line_code, position, line_type)
......@@ -53,37 +53,26 @@ module NotesHelper
}
if use_legacy_diff_note
discussion_id = LegacyDiffNote.discussion_id(
@comments_target[:noteable_type],
@comments_target[:noteable_id] || @comments_target[:commit_id],
line_code
)
data.merge!(
note_type: LegacyDiffNote.name,
discussion_id: discussion_id
)
new_note = LegacyDiffNote.new(@new_diff_note_attrs.merge(line_code: line_code))
discussion_id = new_note.discussion_id
else
discussion_id = DiffNote.discussion_id(
@comments_target[:noteable_type],
@comments_target[:noteable_id] || @comments_target[:commit_id],
position
)
data.merge!(
position: position.to_json,
note_type: DiffNote.name,
discussion_id: discussion_id
)
new_note = DiffNote.new(@new_diff_note_attrs.merge(position: position))
discussion_id = new_note.discussion_id
data[:position] = position.to_json
end
data
data.merge(
note_type: new_note.type,
discussion_id: discussion_id
)
end
def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user
data = discussion.reply_attributes.merge(line_type: line_type)
data = { discussion_id: discussion.id, original_discussion_id: discussion.original_id, line_type: line_type }
data[:line_code] = discussion.line_code if discussion.respond_to?(:line_code)
button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
data: data, title: 'Add a reply'
......@@ -95,7 +84,15 @@ module NotesHelper
end
def preload_noteable_for_regular_notes(notes)
ActiveRecord::Associations::Preloader.new.preload(notes.select { |note| !note.for_commit? }, :noteable)
ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable)
end
def prepare_notes_for_rendering(notes)
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
Banzai::NoteRenderer.render(notes, @project, current_user)
notes
end
def note_max_access_for_user(note)
......
......@@ -2,6 +2,7 @@ class Commit
extend ActiveModel::Naming
include ActiveModel::Conversion
include Noteable
include Participable
include Mentionable
include Referable
......@@ -203,6 +204,10 @@ class Commit
project.notes.for_commit_id(self.id)
end
def discussion_notes
notes.non_diff_notes
end
def notes_with_associations
notes.includes(:author)
end
......
class CommitDiscussion < Discussion
def self.override_discussion_id(note)
discussion_id(note)
end
def potentially_resolvable?
false
end
end
......@@ -24,12 +24,4 @@ module NoteOnDiff