Commit 6ddacaf9 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'riyad-discussions'

parents d8e697ac ac983319
$ ->
$("body").on "click", ".js-details-target", ->
container = $(@).closest(".js-details-container")
container.toggleClass("open")
$ ->
$("body").on "click", ".js-toggler-target", ->
container = $(@).closest(".js-toggler-container")
container.toggleClass("on")
Array.prototype.first = function() {
return this[0];
}
Array.prototype.last = function() {
return this[this.length-1];
}
\ No newline at end of file
...@@ -9,72 +9,315 @@ var NoteList = { ...@@ -9,72 +9,315 @@ var NoteList = {
loading_more_disabled: false, loading_more_disabled: false,
reversed: false, reversed: false,
init: init: function(tid, tt, path) {
function(tid, tt, path) { NoteList.notes_path = path + ".js";
this.notes_path = path + ".js"; NoteList.target_id = tid;
this.target_id = tid; NoteList.target_type = tt;
this.target_type = tt; NoteList.reversed = $("#notes-list").is(".reversed");
this.reversed = $("#notes-list").is(".reversed"); NoteList.target_params = "target_type=" + NoteList.target_type + "&target_id=" + NoteList.target_id;
this.target_params = "target_type=" + this.target_type + "&target_id=" + this.target_id;
NoteList.setupMainTargetNoteForm();
// get initial set of notes
this.getContent(); if(NoteList.reversed) {
var form = $(".js-main-target-form");
$("#notes-list, #new-notes-list").on("ajax:success", ".delete-note", function() { form.find(".buttons, .note_options").hide();
$(this).closest('li').fadeOut(function() { var textarea = form.find(".js-note-text");
$(this).remove(); textarea.css("height", "40px");
NoteList.updateVotes(); textarea.on("focus", function(){
}); textarea.css("height", "80px");
form.find(".buttons, .note_options").show();
}); });
}
$(".note-form-holder").on("ajax:before", function(){ // get initial set of notes
$(".submit_note").disable(); NoteList.getContent();
})
$(".note-form-holder").on("ajax:complete", function(){ // add a new diff note
$(".submit_note").enable(); $(document).on("click",
$('#preview-note').hide(); ".js-add-diff-note-button",
$('#note_note').show(); NoteList.addDiffNote);
})
disableButtonIfEmptyField(".note-text", ".submit_note"); // reply to diff/discussion notes
$(document).on("click",
".js-discussion-reply-button",
NoteList.replyToDiscussionNote);
$("#note_attachment").change(function(e){ // setup note preview
var val = $('.input-file').val(); $(document).on("click",
var filename = val.replace(/^.*[\\\/]/, ''); ".js-note-preview-button",
$(".file_name").text(filename); NoteList.previewNote);
});
// update the file name when an attachment is selected
$(document).on("change",
".js-note-attachment-input",
NoteList.updateFormAttachment);
// hide diff note form
$(document).on("click",
".js-close-discussion-note-form",
NoteList.removeDiscussionNoteForm);
// remove a note (in general)
$(document).on("click",
".js-note-delete",
NoteList.removeNote);
// reset main target form after submit
$(document).on("ajax:complete",
".js-main-target-form",
NoteList.resetMainTargetForm);
$(document).on("click",
".js-choose-note-attachment-button",
NoteList.chooseNoteAttachment);
$(document).on("click",
".js-show-outdated-discussion",
function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() });
},
/**
* When clicking on buttons
*/
/**
* Called when clicking on the "add a comment" button on the side of a diff line.
*
* Inserts a temporary row for the form below the line.
* Sets up the form and shows it.
*/
addDiffNote: function(e) {
e.preventDefault();
// find the form
var form = $(".js-new-note-form");
var row = $(this).closest("tr");
var nextRow = row.next();
// does it already have notes?
if (nextRow.is(".notes_holder")) {
$.proxy(NoteList.replyToDiscussionNote,
nextRow.find(".js-discussion-reply-button")
).call();
} else {
// add a notes row and insert the form
row.after('<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"></td></tr>');
form.clone().appendTo(row.next().find(".notes_content"));
// show the form
NoteList.setupDiscussionNoteForm($(this), row.next().find("form"));
}
},
/**
* Called when clicking the "Choose File" button.
*
* Opesn the file selection dialog.
*/
chooseNoteAttachment: function() {
var form = $(this).closest("form");
if(this.reversed) { form.find(".js-note-attachment-input").click();
var textarea = $(".note-text"); },
$('.note_advanced_opts').hide();
textarea.css("height", "40px"); /**
textarea.on("focus", function(){ * Shows the note preview.
$(this).css("height", "80px"); *
$('.note_advanced_opts').show(); * Lets the server render GFM into Html and displays it.
*
* Note: uses the Toggler behavior to toggle preview/edit views/buttons
*/
previewNote: function(e) {
e.preventDefault();
var form = $(this).closest("form");
var preview = form.find('.js-note-preview');
var noteText = form.find('.js-note-text').val();
if(noteText.trim().length === 0) {
preview.text('Nothing to preview.');
} else {
preview.text('Loading...');
$.post($(this).data('url'), {note: noteText})
.success(function(previewData) {
preview.html(previewData);
}); });
} }
},
/**
* Called in response to "cancel" on a diff note form.
*
* Shows the reply button again.
* Removes the form and if necessary it's temporary row.
*/
removeDiscussionNoteForm: function() {
var form = $(this).closest("form");
var row = form.closest("tr");
// show the reply button (will only work for replys)
form.prev(".js-discussion-reply-button").show();
if (row.is(".js-temp-notes-holder")) {
// remove temporary row for diff lines
row.remove();
} else {
// only remove the form
form.remove();
}
},
// Setup note preview /**
$(document).on('click', '#preview-link', function(e) { * Called in response to deleting a note of any kind.
$('#preview-note').text('Loading...'); *
* Removes the actual note from view.
* Removes the whole discussion if the last note is being removed.
*/
removeNote: function() {
var note = $(this).closest(".note");
var notes = note.closest(".notes");
$(this).text($(this).text() === "Edit" ? "Preview" : "Edit"); // check if this is the last note for this line
if (notes.find(".note").length === 1) {
// for discussions
notes.closest(".discussion").remove();
var note_text = $('#note_note').val(); // for diff lines
notes.closest("tr").remove();
}
if(note_text.trim().length === 0) { note.remove();
$('#preview-note').text('Nothing to preview.'); NoteList.updateVotes();
} else { },
$.post($(this).attr('href'), {note: note_text}).success(function(data) {
$('#preview-note').html(data);
});
}
$('#preview-note, #note_note').toggle(); /**
e.preventDefault(); * Called when clicking on the "reply" button for a diff line.
}); *
}, * Shows the note form below the notes.
*/
replyToDiscussionNote: function() {
// find the form
var form = $(".js-new-note-form");
// hide reply button
$(this).hide();
// insert the form after the button
form.clone().insertAfter($(this));
// show the form
NoteList.setupDiscussionNoteForm($(this), $(this).next("form"));
},
/**
* Helper for inserting and setting up note forms.
*/
/**
* Called in response to creating a note failing validation.
*
* Adds the rendered errors to the respective form.
* If "discussionId" is null or undefined, the main target form is assumed.
*/
errorsOnForm: function(errorsHtml, discussionId) {
// find the form
if (discussionId) {
var form = $("form[rel='"+discussionId+"']");
} else {
var form = $(".js-main-target-form");
}
form.find(".js-errors").remove();
form.prepend(errorsHtml);
form.find(".js-note-text").focus();
},
/**
* Shows the diff/discussion form and does some setup on it.
*
* Sets some hidden fields in the form.
*
* Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
* and "noteableId" data attributes set.
*/
setupDiscussionNoteForm: function(dataHolder, form) {
// setup note target
form.attr("rel", dataHolder.data("discussionId"));
form.find("#note_commit_id").val(dataHolder.data("commitId"));
form.find("#note_line_code").val(dataHolder.data("lineCode"));
form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
NoteList.setupNoteForm(form);
form.find(".js-note-text").focus();
},
/**
* Shows the main form and does some setup on it.
*
* Sets some hidden fields in the form.
*/
setupMainTargetNoteForm: function() {
// find the form
var form = $(".js-new-note-form");
// insert the form after the button
form.clone().replaceAll($(".js-main-target-form"));
form = form.prev("form");
// show the form
NoteList.setupNoteForm(form);
// fix classes
form.removeClass("js-new-note-form");
form.addClass("js-main-target-form");
// remove unnecessary fields and buttons
form.find("#note_line_code").remove();
form.find(".js-close-discussion-note-form").remove();
},
/**
* General note form setup.
*
* * deactivates the submit button when text is empty
* * hides the preview button when text is empty
* * setup GFM auto complete
* * show the form
*/
setupNoteForm: function(form) {
disableButtonIfEmptyField(form.find(".js-note-text"), form.find(".js-comment-button"));
form.removeClass("js-new-note-form");
// setup preview buttons
form.find(".js-note-edit-button, .js-note-preview-button")
.tooltip({ placement: 'left' });
previewButton = form.find(".js-note-preview-button");
form.find(".js-note-text").on("input", function() {
if ($(this).val().trim() !== "") {
previewButton.removeClass("turn-off").addClass("turn-on");
} else {
previewButton.removeClass("turn-on").addClass("turn-off");
}
});
// remove notify commit author checkbox for non-commit notes
if (form.find("#note_noteable_type").val() !== "Commit") {
form.find(".js-notify-commit-author").remove();
}
GitLab.GfmAutoComplete.setup();
form.show();
},
/** /**
...@@ -86,40 +329,39 @@ var NoteList = { ...@@ -86,40 +329,39 @@ var NoteList = {
/** /**
* Gets an inital set of notes. * Gets an inital set of notes.
*/ */
getContent: getContent: function() {
function() { $.ajax({
$.ajax({ url: NoteList.notes_path,
type: "GET", data: NoteList.target_params,
url: this.notes_path, complete: function(){ $('.js-notes-busy').removeClass("loading")},
data: this.target_params, beforeSend: function() { $('.js-notes-busy').addClass("loading") },
complete: function(){ $('.notes-status').removeClass("loading")}, dataType: "script"
beforeSend: function() { $('.notes-status').addClass("loading") }, });
dataType: "script"}); },
},
/** /**
* Called in response to getContent(). * Called in response to getContent().
* Replaces the content of #notes-list with the given html. * Replaces the content of #notes-list with the given html.
*/ */
setContent: setContent: function(newNoteIds, html) {
function(first_id, last_id, html) { NoteList.top_id = newNoteIds.first();
this.top_id = first_id; NoteList.bottom_id = newNoteIds.last();
this.bottom_id = last_id; $("#notes-list").html(html);
$("#notes-list").html(html);
// for the wall
if (NoteList.reversed) {
// init infinite scrolling // init infinite scrolling
this.initLoadMore(); NoteList.initLoadMore();
// init getting new notes // init getting new notes
if (this.reversed) { NoteList.initRefreshNew();
this.initRefreshNew(); }
} },
},
/** /**
* Handle loading more notes when scrolling to the bottom of the page. * Handle loading more notes when scrolling to the bottom of the page.
* The id of the last note in the list is in this.bottom_id. * The id of the last note in the list is in NoteList.bottom_id.
* *
* Set up refreshing only new notes after all notes have been loaded. * Set up refreshing only new notes after all notes have been loaded.
*/ */
...@@ -128,65 +370,57 @@ var NoteList = { ...@@ -128,65 +370,57 @@ var NoteList = {
/** /**
* Initializes loading more notes when scrolling to the bottom of the page. * Initializes loading more notes when scrolling to the bottom of the page.
*/ */
initLoadMore: initLoadMore: function() {
function() { $(document).endlessScroll({
$(document).endlessScroll({ bottomPixels: 400,
bottomPixels: 400, fireDelay: 1000,
fireDelay: 1000, fireOnce:true,
fireOnce:true, ceaseFire: function() {
ceaseFire: function() { return NoteList.loading_more_disabled;
return NoteList.loading_more_disabled; },
}, callback: function(i) {
callback: function(i) { NoteList.getMore();
NoteList.getMore(); }
} });
}); },
},
/** /**
* Gets an additional set of notes. * Gets an additional set of notes.
*/ */
getMore: getMore: function() {
function() { // only load more notes if there are no "new" notes
// only load more notes if there are no "new" notes $('.loading').show();
$('.loading').show(); $.ajax({
$.ajax({ url: NoteList.notes_path,
type: "GET", data: NoteList.target_params + "&loading_more=1&" + (NoteList.reversed ? "before_id" : "after_id") + "=" + NoteList.bottom_id,
url: this.notes_path, complete: function(){ $('.js-notes-busy').removeClass("loading")},
data: this.target_params + "&loading_more=1&" + (this.reversed ? "before_id" : "after_id") + "=" + this.bottom_id, beforeSend: function() { $('.js-notes-busy').addClass("loading") },
complete: function(){ $('.notes-status').removeClass("loading")}, dataType: "script"
beforeSend: function() { $('.notes-status').addClass("loading") }, });
dataType: "script"}); },
},
/** /**
* Called in response to getMore(). * Called in response to getMore().
* Append notes to #notes-list. * Append notes to #notes-list.
*/ */
appendMoreNotes: appendMoreNotes: function(newNoteIds, html) {
function(id, html) { var lastNewNoteId = newNoteIds.last();
if(id != this.bottom_id) { if(lastNewNoteId != NoteList.bottom_id) {
this.bottom_id = id; NoteList.bottom_id = lastNewNoteId;
$("#notes-list").append(html); $("#notes-list").append(html);
} }
}, },
/** /**
* Called in response to getMore(). * Called in response to getMore().
* Disables loading more notes when scrolling to the bottom of the page. * Disables loading more notes when scrolling to the bottom of the page.
* Initalizes refreshing new notes.
*/ */
finishedLoadingMore: finishedLoadingMore: function() {
function() { NoteList.loading_more_disabled = true;
this.loading_more_disabled = true;
// from now on only get new notes // make sure we are up to date
if (!this.reversed) { NoteList.updateVotes();
this.initRefreshNew(); },
}
// make sure we are up to date
this.updateVotes();
},
/** /**
...@@ -194,52 +428,118 @@ var NoteList = { ...@@ -194,52 +428,118 @@ var NoteList = {
* *
* New notes are all notes that are created after the site has been loaded. * New notes are all notes that are created after the site has been loaded.
* The "old" notes are in #notes-list the "new" ones will be in #new-notes-list. * The "old" notes are in #notes-list the "new" ones will be in #new-notes-list.
* The id of the last "old" note is in this.bottom_id. * The id of the last "old" note is in NoteList.bottom_id.
*/ */
/** /**
* Initializes getting new notes every n seconds. * Initializes getting new notes every n seconds.
*
* Note: only used on wall.
*/ */
initRefreshNew: initRefreshNew: function() {
function() { setInterval("NoteList.getNew()", 10000);
setInterval("NoteList.getNew()", 10000); },
},
/** /**
* Gets the new set of notes. * Gets the new set of notes.
*
* Note: only used on wall.
*/ */
getNew: getNew: function() {
function() { $.ajax({
$.ajax({ url: NoteList.notes_path,
type: "GET", data: NoteList.target_params + "&loading_new=1&after_id=" + (NoteList.reversed ? NoteList.top_id : NoteList.bottom_id),
url: this.notes_path, dataType: "script"
data: this.target_params + "&loading_new=1&after_id=" + (this.reversed ? this.top_id : this.bottom_id), });
dataType: "script"}); },
},
/** /**
* Called in response to getNew(). * Called in response to getNew().
* Replaces the content of #new-notes-list with the given html. * Replaces the content of #new-notes-list with the given html.
*
* Note: only used on wall.
*/ */
replaceNewNotes: replaceNewNotes: function(newNoteIds, html) {
function(html) { $("#new-notes-list").html(html);
$("#new-notes-list").html(html); NoteList.updateVotes();
this.updateVotes(); },
},
/** /**
* Adds a single note to #new-notes-list. * Adds a single common note to #notes-list.
*/ */
appendNewNote: appendNewNote: function(id, html) {
function(id, html) { $("#notes-list").append(html);
if (this.reversed) { NoteList.updateVotes();
$("#new-notes-list").prepend(html); },
} else {
$("#new-notes-list").append(html); /**
} * Adds a single discussion note to #notes-list.
this.updateVotes(); *
}, * Also removes the corresponding form.
*/
appendNewDiscussionNote: function(discussionId, diffRowHtml, noteHtml) {
var form = $("form[rel='"+discussionId+"']");
var row = form.closest("tr");
// is this the first note of discussion?
if (row.is(".js-temp-notes-holder")) {
// insert the note and the reply button after the temp row
row.after(diffRowHtml);
// remove the note (will be added again below)
row.next().find(".note").remove();
}
// append new note to all matching discussions
$(".notes[rel='"+discussionId+"']").append(noteHtml);
// cleanup after successfully creating a diff/discussion note
$.proxy(NoteList.removeDiscussionNoteForm, form).call();
},
/**
* Adds a single wall note to #new-notes-list.
*
* Note: only used on wall.
*/
appendNewWallNote: function(id, html) {
$("#new-notes-list").prepend(html);
},
/**
* Called in response the main target form has been successfully submitted.
*
* Removes any errors.
* Resets text and preview.
* Resets buttons.
*/
resetMainTargetForm: function(){
var form = $(this);
// remove validation errors
form.find(".js-errors").remove();
// reset text and preview
var previewContainer = form.find(".js-toggler-container.note_text_and_preview");
if (previewContainer.is(".on")) {
previewContainer.removeClass("on");
}
form.find(".js-note-text").val("").trigger("input");
},
/**
* Called after an attachment file has been selected.
*
* Updates the file name for the selected attachment.
*/
updateFormAttachment: function() {
var form = $(this).closest("form");
// get only the basename
var filename = $(this).val().replace(/^.*[\\\/]/, '');
form.find(".js-attachment-filename").text(filename);
},
/** /**
* Recalculates the votes and updates them (if they are displayed at all). * Recalculates the votes and updates them (if they are displayed at all).
...@@ -249,67 +549,24 @@ var NoteList = { ...@@ -249,67 +549,24 @@ var NoteList = {
* Might produce inaccurate results when not all notes have been loaded and a * Might produce inaccurate results when not all notes have been loaded and a
* recalculation is triggered (e.g. when deleting a note). * recalculation is triggered (e.g. when deleting a note).
*/ */
updateVotes: updateVotes: function() {
function() { var votes = $("#votes .votes");
var votes = $("#votes .votes"); var notes = $("#notes-list .note .vote");
var notes = $("#notes-list, #new-notes-list").find(".note .vote");
// only update if there is a vote display
// only update if there is a vote display if (votes.size()) {
if (votes.size()) { var upvotes = notes.filter(".upvote").size();
var upvotes = notes.filter(".upvote").size(); var downvotes = notes.filter(".downvote").size();
var downvotes = notes.filter(".downvote").size(); var votesCount = upvotes + downvotes;
var votesCount = upvotes + downvotes; var upvotesPercent = votesCount ? (100.0 / votesCount * upvotes) : 0;
var upvotesPercent = votesCount ? (100.0 / votesCount * upvotes) : 0; var downvotesPercent = votesCount ? (100.0 - upvotesPercent) : 0;
var downvotesPercent = votesCount ? (100.0 - upvotesPercent) : 0;
// change vote bar lengths
// change vote bar lengths votes.find(".bar-success").css("width", upvotesPercent+"%");
votes.find(".bar-success").css("width", upvotesPercent+"%"); votes.find(".bar-danger").css("width", downvotesPercent+"%");
votes.find(".bar-danger").css("width", downvotesPercent+"%"); // replace vote numbers
// replace vote numbers votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes));
votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes)); votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes));
votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes));
}
} }
}
}; };
var PerLineNotes = {
init:
function() {
/**
* Called when clicking on the "add note" or "reply" button for a diff line.
*
* Shows the note form below the line.
* Sets some hidden fields in the form.
*/
$(".diff_file_content").on("click", ".line_note_link, .line_note_reply_link", function(e) {
var form = $(".per_line_form");
$(this).closest("tr").after(form);
form.find("#note_line_code").val($(this).data("lineCode"));
form.show();
e.preventDefault();
});
disableButtonIfEmptyField(".line-note-text", ".submit_inline_note");
/**
* Called in response to successfully deleting a note on a diff line.
*
* Removes the actual note from view.
* Removes the reply button if the last note for that line has been removed.
*/
$(".diff_file_content").on("ajax:success", ".delete-note", function() {
var trNote = $(this).closest("tr");
trNote.fadeOut(function() {
$(this).remove();
});
// check if this is the last note for this line
// elements must really be removed for this to work reliably
var trLine = trNote.prev();
var trRpl = trNote.next();
if (trLine.is(".line_holder") && trRpl.is(".reply")) {
trRpl.fadeOut(function() { $(this).remove(); });
}
});
}
}
...@@ -46,3 +46,8 @@ ...@@ -46,3 +46,8 @@
@import "themes/ui_gray.scss"; @import "themes/ui_gray.scss";
@import "themes/ui_color.scss"; @import "themes/ui_color.scss";
/**
* Styles for JS behaviors.
*/
@import "behaviors.scss";
// Details
//--------
.js-details-container .content { display: none; }
.js-details-container .content.hide { display: block; }
.js-details-container.open .content { display: block; }
.js-details-container.open .content.hide { display: none; }
// Toggler
//--------
.js-toggler-container .turn-on { display: inherit; }
.js-toggler-container .turn-off { display: none; }
.js-toggler-container.on .turn-on { display: none; }
.js-toggler-container.on .turn-off { display: inherit; }
...@@ -546,3 +546,9 @@ h1.http_status_code { ...@@ -546,3 +546,9 @@ h1.http_status_code {
} }
} }
} }
img.emoji {
height: 20px;
vertical-align: middle;
width: 20px;
}
...@@ -119,12 +119,14 @@ ...@@ -119,12 +119,14 @@
} }
} }
} }
.old_line, .new_line { .new_line,
margin: 0px; .old_line,
padding: 0px; .notes_line {
border: none; margin:0px;
background: #EEE; padding:0px;
color: #666; border:none;
background:#EEE;
color:#666;
padding: 0px 5px; padding: 0px 5px;
border-right: 1px solid #ccc; border-right: 1px solid #ccc;
text-align: right; text-align: right;
...@@ -134,6 +136,7 @@ ...@@ -134,6 +136,7 @@
moz-user-select: none; moz-user-select: none;
-khtml-user-select: none; -khtml-user-select: none;
user-select: none; user-select: none;
a { a {
float: left; float: left;
width: 35px; width: 35px;
......
/** /**
* Notes * Notes
*
*/ */
#notes-list, ul.notes {
#new-notes-list {
display: block; display: block;
list-style: none; list-style: none;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
}
.issue_notes, .discussion-header,
.wiki_notes { .note-header {
.note_content { @extend .cgray;
float: left; padding-top: 5px;
width: 400px; padding-bottom: 15px;
}
}
/* Note textare */ .avatar {
#note_note { float: left;
height: 80px; margin-right: 10px;
width: 98%; }
font-size: 14px;
}
#new_note { .discussion-last-update,
.attach_holder { .note-last-update {
display: none; font-style: italic;
}
.note-author {
color: $style_color;
font-weight: bold;
&:hover {
color: $primary_color;
}
}
} }
}
.preview_note { .discussion {
margin: 2px; padding: 8px 0;
border: 1px solid #ddd; overflow: hidden;
padding: 10px; display: block;
min-height: 60px; position:relative;
background: #f5f5f5;
}
.note { .discussion-body {
padding: 8px 0; margin-left: 50px;
overflow: hidden;
display: block;
position: relative;
img {float: left; margin-right: 10px;}
img.emoji {float: none;margin: 0;}
.note-author cite{font-style: italic;}
p { color: $style_color; }
.note-author { color: $style_color;}
.note-title { margin-left: 45px; padding-top: 5px;} .diff_file,
.avatar { .discussion-hidden,
margin-top: 3px; .notes {
} @extend .borders;
background-color: #F9F9F9;
}
.diff_file .notes {
/* reset */
background: inherit;
border: none;
@include box-shadow(none);
.delete-note { }
display: none; .discussion-hidden .note {
position: absolute; @extend .cgray;
right: 0; padding: 8px;
top: 0; text-align: center;
}
.notes .note {
border-color: #ddd;
padding: 8px;
}
.reply-btn {
margin-top: 8px;
}
}
} }
&:hover { .note {
.delete-note { display: block; } padding: 8px 0;
} overflow: hidden;
} display: block;
#notes-list:not(.reversed) .note, position:relative;
#new-notes-list:not(.reversed) .note { p { color: $style_color; }
border-bottom: 1px solid #eee;
}
#notes-list.reversed .note,
#new-notes-list.reversed .note {
border-top: 1px solid #eee;
}
/* mark vote notes */
.voting_notes .note {
padding: 8px 0;
}
.notes-status { .avatar {
margin: 18px; margin-top: 3px;
} }
.attachment {
font-size: 14px;
margin-top: -20px;
.icon-attachment {
@extend .icon-paper-clip;
font-size: 24px;
position: relative;
text-align: right;
top: 6px;
}
}
.note-body {
margin-left: 45px;
}
.note-header {
padding-bottom: 5px;
}
}
p.notify_controls input{ // paint top or bottom borders depending on notes direction
margin: 5px; &:not(.reversed) .note,
&:not(.reversed) .discussion {
border-bottom: 1px solid #eee;
}
&.reversed .note,
&.reversed .discussion {
border-top: 1px solid #eee;
}
} }
p.notify_controls span{ .diff_file .notes_holder {
font-weight: 700; font-family: $sansFontFamily;
} font-size: 13px;
line-height: 18px;
tr.line_notes_row { td {
border-bottom: 1px solid #DDD; border: 1px solid #ddd;
border-left: 7px solid #2A79A3; border-left: none;
&.reply { &.notes_line {
background: #eee; text-align: center;
border-left: 7px solid #2A79A3; padding: 10px 0;
border-top: 1px solid #ddd;
td {
padding: 7px 10px;
} }
a.line_note_reply_link { &.notes_content {
border: 1px solid #eaeaea; background-color: $white;
@include border-radius(4px); border-width: 1px 0;
padding: 3px 10px; padding-top: 0;
margin-left: 5px;
color: white;
background: #2A79A3;
border-color: #2A79A3;
} }
} }
ul {
margin: 0; .reply-btn {
li { margin-top: 8px;
padding: 0;
border: none;
}
} }
} }
.line_notes_row, .per_line_form { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
.per_line_form {
background: #f5f5f5; /**
border-top: 1px solid #eee; * Actions for Discussions/Notes
form { margin: 0; } */
td {
border-bottom: 1px solid #ddd; .discussion,
.note {
&.note:hover {
.note-actions { display: block; }
}
.discussion-header:hover {
.discussion-actions { display: block; }
} }
.note_actions {
margin: 0;
padding-top: 10px;
.buttons { .discussion-actions,
float: left; .note-actions {
width: 300px; display: none;
} float: right;
.options {
.labels { [class^="icon-"],
float: left; [class*="icon-"] {
padding-left: 10px; font-size: 16px;
label { line-height: 16px;
padding: 6px 0; vertical-align: middle;
margin: 0; }
width: 120px;
} a {
@extend .cgray;
&:hover {
color: $primary_color;
&.danger { @extend .cred; }
} }
} }
} }
} }
.diff_file .note .note-actions {
right: 0;
top: 0;
}
/**
* Line note button on the side of diffs
*/
td .line_note_link { .diff_file tr.line_holder {
position: absolute; .add-diff-note {
margin-left:-70px; background: url("diff_note_add.png") no-repeat left 0;
margin-top:-10px; height: 22px;
z-index: 10; margin-left: -65px;
background: url("comment_add.png") no-repeat left 0; position: absolute;
width: 32px; width: 22px;
height: 32px; z-index: 10;
opacity: 0.0; // "hide" it by default
filter: alpha(opacity=0); opacity: 0.0;
filter: alpha(opacity=0);
&:hover { &:hover {
opacity: 1.0; opacity: 1.0;
filter: alpha(opacity=100); filter: alpha(opacity=100);
}
}
// "show" the icon also if we just hover somwhere over the line
&:hover > td {
background: $hover !important;
.add-diff-note {
opacity: 1.0;
filter: alpha(opacity=100);
}
} }
} }
.diff_file_content tr.line_holder:hover > td { background: $hover !important; }
.diff_file_content tr.line_holder:hover > td .line_note_link {
opacity: 1.0; /**
filter: alpha(opacity=100); * Note Form
*/
.comment-btn,
.reply-btn {
@extend .save-btn;
} }
.diff_file,
.discussion {
.new_note {
margin: 8px 5px 8px 0;
.new_note { .note_options {
.input-file { // because of the smaller width and the extra "cancel" button
font: 500px monospace; margin-top: 8px;
opacity: 0; }
filter: alpha(opacity=0);
position: absolute;
z-index: 1;
top: 0;
right: 0;
padding: 0;
margin: 0;
} }
}
.new_note {
display: none;
.note_advanced_opts { .buttons {
float: left;
margin-top: 8px;
}
.clearfix {
margin-bottom: 0;
}
.note_options {
h6 { h6 {
line-height: 32px; @extend .left;
padding-right: 15px; line-height: 20px;
padding-right: 16px;
padding-bottom: 16px;
}
label {
padding: 0;
} }
}
.attachments { .attachment {
position: relative; @extend .right;
width: 350px; position: relative;
height: 50px; width: 350px;
overflow: hidden; height: 50px;
margin:0 0 5px !important; margin:0 0 5px !important;
.input_file { // hide the actual file field
.file_upload { input {
position: absolute; display: none;
right: 14px;
top: 7px;
} }
.file_name { .choose-btn {
line-height: 30px;
width: 240px;
height: 28px;
overflow: hidden;
}
.input-file {
width: 260px;
height: 41px;
float: right; float: right;
} }
} }
.notify_options {
@extend .right;
}
} }
.note_text_and_preview {
// makes the "absolute" position for links relative to this
position: relative;
// preview/edit buttons
> a {
font-size: 24px;
padding: 4px;
position: absolute;
right: 10px;
}
.note_preview {
background: #f5f5f5;
border: 1px solid #ddd;
@include border-radius(4px);
min-height: 80px;
padding: 4px 6px;
}
.note_text {
border: 1px solid #DDD;
box-shadow: none;
font-size: 14px;
height: 80px;
width: 98.6%;
}
}
}
/* loading indicator */
.notes-busy {
margin: 18px;
} }
.note-text { .note-image-attach {
border: 1px solid #DDD; @extend .span4;
box-shadow: none; @extend .thumbnail;
margin-left: 45px;
} }
...@@ -3,8 +3,8 @@ module Notes ...@@ -3,8 +3,8 @@ module Notes
def execute def execute
note = project.notes.new(params[:note]) note = project.notes.new(params[:note])
note.author = current_user note.author = current_user
note.notify = true if params[:notify] == '1' note.notify = params[:notify].present?
note.notify_author = true if params[:notify_author] == '1' note.notify_author = params[:notify_author].present?
note.save note.save
note note
end end
......
...@@ -9,11 +9,11 @@ module Notes ...@@ -9,11 +9,11 @@ module Notes
@notes = case target_type @notes = case target_type
when "commit" when "commit"
project.notes.for_commit_id(target_id).not_inline.fresh.limit(20) project.notes.for_commit_id(target_id).not_inline.fresh
when "issue" when "issue"
project.issues.find(target_id).notes.inc_author.fresh.limit(20) project.issues.find(target_id).notes.inc_author.fresh
when "merge_request" when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author.fresh.limit(20) project.merge_requests.find(target_id).mr_and_commit_notes.inc_author.fresh
when "snippet" when "snippet"
project.snippets.find(target_id).notes.fresh project.snippets.find(target_id).notes.fresh
when "wall" when "wall"
......
...@@ -13,11 +13,17 @@ class CommitController < ProjectResourceController ...@@ -13,11 +13,17 @@ class CommitController < ProjectResourceController
@commit = result[:commit] @commit = result[:commit]
git_not_found! unless @commit git_not_found! unless @commit
@suppress_diff = result[:suppress_diff] @suppress_diff = result[:suppress_diff]
@note = result[:note]
@line_notes = result[:line_notes] @note = result[:note]
@notes_count = result[:notes_count] @line_notes = result[:line_notes]
@comments_allowed = true @notes_count = result[:notes_count]
@target_type = :commit
@target_id = @commit.id
@comments_allowed = @reply_allowed = true
@comments_target = { noteable_type: 'Commit',
commit_id: @commit.id }
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -35,6 +35,8 @@ class IssuesController < ProjectResourceController ...@@ -35,6 +35,8 @@ class IssuesController < ProjectResourceController
def show def show
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
@target_type = :issue
@target_id = @issue.id
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -18,6 +18,9 @@ class MergeRequestsController < ProjectResourceController ...@@ -18,6 +18,9 @@ class MergeRequestsController < ProjectResourceController
end end
def show def show
@target_type = :merge_request
@target_id = @merge_request.id
respond_to do |format| respond_to do |format|
format.html format.html
format.js format.js
...@@ -31,7 +34,9 @@ class MergeRequestsController < ProjectResourceController ...@@ -31,7 +34,9 @@ class MergeRequestsController < ProjectResourceController
@diffs = @merge_request.diffs @diffs = @merge_request.diffs
@commit = @merge_request.last_commit @commit = @merge_request.last_commit
@comments_allowed = true @comments_allowed = @reply_allowed = true
@comments_target = { noteable_type: 'MergeRequest',
noteable_id: @merge_request.id }
@line_notes = @merge_request.notes.where("line_code is not null") @line_notes = @merge_request.notes.where("line_code is not null")
end end
......
...@@ -6,10 +6,12 @@ class NotesController < ProjectResourceController ...@@ -6,10 +6,12 @@ class NotesController < ProjectResourceController
respond_to :js respond_to :js
def index def index
notes @notes = Notes::LoadContext.new(project, current_user, params).execute
@target_type = params[:target_type].camelize
@target_id = params[:target_id]
if params[:target_type] == "merge_request" if params[:target_type] == "merge_request"
@mixed_targets = true @discussions = discussions_from_notes
@main_target_type = params[:target_type].camelize
end end
respond_with(@notes) respond_with(@notes)
...@@ -17,6 +19,8 @@ class NotesController < ProjectResourceController ...@@ -17,6 +19,8 @@ class NotesController < ProjectResourceController
def create def create
@note = Notes::CreateContext.new(project, current_user, params).execute @note = Notes::CreateContext.new(project, current_user, params).execute
@target_type = params[:target_type].camelize
@target_id = params[:target_id]
respond_to do |format| respond_to do |format|
format.html {redirect_to :back} format.html {redirect_to :back}
...@@ -40,7 +44,34 @@ class NotesController < ProjectResourceController ...@@ -40,7 +44,34 @@ class NotesController < ProjectResourceController
protected protected
def notes def discussion_notes_for(note)
@notes = Notes::LoadContext.new(project, current_user, params).execute @notes.select do |other_note|
note.discussion_id == other_note.discussion_id
end
end
def discussions_from_notes
discussion_ids = []
discussions = []
@notes.each do |note|
next if discussion_ids.include?(note.discussion_id)
# don't group notes for the main target
if note_for_main_target?(note)
discussions << [note]
else
discussions << discussion_notes_for(note)
discussion_ids << note.discussion_id
end
end
discussions
end
# Helps to distinguish e.g. commit notes in mr notes list
def note_for_main_target?(note)
note.for_wall? ||
(@target_type.camelize == note.noteable_type && !note.for_diff_line?)
end end
end end
...@@ -80,7 +80,10 @@ class ProjectsController < ProjectResourceController ...@@ -80,7 +80,10 @@ class ProjectsController < ProjectResourceController
def wall def wall
return render_404 unless @project.wall_enabled return render_404 unless @project.wall_enabled
@note = Note.new
@target_type = :wall
@target_id = nil
@note = @project.notes.new
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -50,6 +50,8 @@ class SnippetsController < ProjectResourceController ...@@ -50,6 +50,8 @@ class SnippetsController < ProjectResourceController
def show def show
@note = @project.notes.new(noteable: @snippet) @note = @project.notes.new(noteable: @snippet)
@target_type = :snippet
@target_id = @snippet.id
end end
def destroy def destroy
......
...@@ -9,11 +9,13 @@ module CommitsHelper ...@@ -9,11 +9,13 @@ module CommitsHelper
end end
end end
def build_line_anchor(index, line_new, line_old) def build_line_anchor(diff, line_new, line_old)
"#{index}_#{line_old}_#{line_new}" "#{hexdigest(diff.new_path)}_#{line_old}_#{line_new}"
end end
def each_diff_line(diff_arr, index) def each_diff_line(diff, index)
diff_arr = diff.diff.lines.to_a
line_old = 1 line_old = 1
line_new = 1 line_new = 1
type = nil type = nil
...@@ -39,7 +41,7 @@ module CommitsHelper ...@@ -39,7 +41,7 @@ module CommitsHelper
next next
else else
type = identification_type(line) type = identification_type(line)
line_code = build_line_anchor(index, line_new, line_old) line_code = build_line_anchor(diff, line_new, line_old)
yield(full_line, type, line_code, line_new, line_old) yield(full_line, type, line_code, line_new, line_old)
end end
......
module NotesHelper module NotesHelper
def loading_more_notes? # Helps to distinguish e.g. commit notes in mr notes list
params[:loading_more].present? def note_for_main_target?(note)
note.for_wall? ||
(@target_type.camelize == note.noteable_type && !note.for_diff_line?)
end end
def loading_new_notes? def note_target_fields
params[:loading_new].present? hidden_field_tag(:target_type, @target_type) +
hidden_field_tag(:target_id, @target_id)
end end
# Helps to distinguish e.g. commit notes in mr notes list def link_to_commit_diff_line_note(note)
def note_for_main_target?(note) if note.for_commit_diff_line?
!@mixed_targets || @main_target_type == note.noteable_type link_to "#{note.diff_file_name}:L#{note.diff_new_line}", project_commit_path(@project, note.noteable, anchor: note.line_code)
end
end end
def link_to_commit_diff_line_note(note) def link_to_merge_request_diff_line_note(note)
commit = note.noteable if note.for_merge_request_diff_line? and note.diff
diff_index, diff_old_line, diff_new_line = note.line_code.split('_') link_to "#{note.diff_file_name}:L#{note.diff_new_line}", diffs_project_merge_request_path(note.project, note.noteable_id, anchor: note.line_code)
end
end
link_file = commit.diffs[diff_index.to_i].new_path def loading_more_notes?
link_line = diff_new_line params[:loading_more].present?
end
link_to "#{link_file}:L#{link_line}", project_commit_path(@project, commit, anchor: note.line_code) def loading_new_notes?
params[:loading_new].present?
end end
end end
...@@ -63,12 +63,12 @@ class Notify < ActionMailer::Base ...@@ -63,12 +63,12 @@ class Notify < ActionMailer::Base
# Note # Note
# #
def note_commit_email(recipient_id, note_id) def note_commit_email(commit_autor_email, note_id)
@note = Note.find(note_id) @note = Note.find(note_id)
@commit = @note.noteable @commit = @note.noteable
@commit = CommitDecorator.decorate(@commit) @commit = CommitDecorator.decorate(@commit)
@project = @note.project @project = @note.project
mail(to: recipient(recipient_id), subject: subject("note for commit #{@commit.short_id}", @commit.title)) mail(to: recipient(commit_autor_email), subject: subject("note for commit #{@commit.short_id}", @commit.title))
end end
def note_issue_email(recipient_id, note_id) def note_issue_email(recipient_id, note_id)
......
...@@ -69,29 +69,33 @@ module Issuable ...@@ -69,29 +69,33 @@ module Issuable
closed_changed? && !closed closed_changed? && !closed
end end
# Return the number of +1 comments (upvotes) #
def upvotes # Votes
notes.select(&:upvote?).size #
# Return the number of -1 comments (downvotes)
def downvotes
notes.select(&:downvote?).size
end end
def upvotes_in_percent def downvotes_in_percent
if votes_count.zero? if votes_count.zero?
0 0
else else
100.0 / votes_count * upvotes 100.0 - upvotes_in_percent
end end
end end
# Return the number of -1 comments (downvotes) # Return the number of +1 comments (upvotes)
def downvotes def upvotes
notes.select(&:downvote?).size notes.select(&:upvote?).size
end end
def downvotes_in_percent def upvotes_in_percent
if votes_count.zero? if votes_count.zero?
0 0
else else
100.0 - upvotes_in_percent 100.0 / votes_count * upvotes
end end
end end
......
...@@ -19,7 +19,6 @@ require 'carrierwave/orm/activerecord' ...@@ -19,7 +19,6 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator' require 'file_size_validator'
class Note < ActiveRecord::Base class Note < ActiveRecord::Base
attr_accessible :note, :noteable, :noteable_id, :noteable_type, :project_id, attr_accessible :note, :noteable, :noteable_id, :noteable_type, :project_id,
:attachment, :line_code, :commit_id :attachment, :line_code, :commit_id
...@@ -34,6 +33,7 @@ class Note < ActiveRecord::Base ...@@ -34,6 +33,7 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
validates :note, :project, presence: true validates :note, :project, presence: true
validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
validates :attachment, file_size: { maximum: 10.megabytes.to_i } validates :attachment, file_size: { maximum: 10.megabytes.to_i }
validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' }
...@@ -60,12 +60,74 @@ class Note < ActiveRecord::Base ...@@ -60,12 +60,74 @@ class Note < ActiveRecord::Base
}, without_protection: true) }, without_protection: true)
end end
def notify def commit_author
@notify ||= false @commit_author ||=
project.users.find_by_email(noteable.author_email) ||
project.users.find_by_name(noteable.author_name)
rescue
nil
end end
def notify_author def diff
@notify_author ||= false if noteable.diffs.present?
noteable.diffs.select do |d|
if d.b_path
Digest::SHA1.hexdigest(d.b_path) == diff_file_index
end
end.first
end
end
def diff_file_index
line_code.split('_')[0]
end
def diff_file_name
diff.b_path
end
def diff_new_line
line_code.split('_')[2].to_i
end
def discussion_id
@discussion_id ||= [:discussion, noteable_type.try(:underscore), noteable_id, line_code].join("-").to_sym
end
# Returns true if this is a downvote note,
# otherwise false is returned
def downvote?
votable? && (note.start_with?('-1') ||
note.start_with?(':-1:')
)
end
def for_commit?
noteable_type == "Commit"
end
def for_commit_diff_line?
for_commit? && for_diff_line?
end
def for_diff_line?
line_code.present?
end
def for_issue?
noteable_type == "Issue"
end
def for_merge_request?
noteable_type == "MergeRequest"
end
def for_merge_request_diff_line?
for_merge_request? && for_diff_line?
end
def for_wall?
noteable_type.blank?
end end
# override to return commits, which are not active record # override to return commits, which are not active record
...@@ -81,50 +143,24 @@ class Note < ActiveRecord::Base ...@@ -81,50 +143,24 @@ class Note < ActiveRecord::Base
nil nil
end end
# Check if we can notify commit author def notify
# with email about our comment @notify ||= false
#
# If commit author email exist in project
# and commit author is not passed user we can
# send email to him
#
# params:
# user - current user
#
# return:
# Boolean
#
def notify_only_author?(user)
for_commit? && commit_author &&
commit_author.email != user.email
end
def for_commit?
noteable_type == "Commit"
end
def for_diff_line?
line_code.present?
end end
def commit_author def notify_author
@commit_author ||= @notify_author ||= false
project.users.find_by_email(noteable.author_email) ||
project.users.find_by_name(noteable.author_name)
rescue
nil
end end
# Returns true if this is an upvote note, # Returns true if this is an upvote note,
# otherwise false is returned # otherwise false is returned
def upvote? def upvote?
note.start_with?('+1') || note.start_with?(':+1:') votable? && (note.start_with?('+1') ||
note.start_with?(':+1:')
)
end end
# Returns true if this is a downvote note, def votable?
# otherwise false is returned for_issue? || (for_merge_request? && !for_diff_line?)
def downvote?
note.start_with?('-1') || note.start_with?(':-1:')
end end
def noteable_type_name def noteable_type_name
......
...@@ -11,7 +11,7 @@ class NoteObserver < ActiveRecord::Observer ...@@ -11,7 +11,7 @@ class NoteObserver < ActiveRecord::Observer
notify_team(note) notify_team(note)
elsif note.notify_author elsif note.notify_author
# Notify only author of resource # Notify only author of resource
Notify.delay.note_commit_email(note.commit_author.id, note.id) Notify.delay.note_commit_email(note.noteable.author_email, note.id)
else else
# Otherwise ignore it # Otherwise ignore it
nil nil
......
...@@ -7,12 +7,10 @@ ...@@ -7,12 +7,10 @@
%span.cred #{@commit.stats.deletions} deletions %span.cred #{@commit.stats.deletions} deletions
= render "commits/diffs", diffs: @commit.diffs = render "commits/diffs", diffs: @commit.diffs
= render "notes/notes_with_form", tid: @commit.id, tt: "commit" = render "notes/notes_with_form"
= render "notes/per_line_form"
:javascript :javascript
$(function(){ $(function(){
PerLineNotes.init();
var w, h; var w, h;
$('.diff_file').each(function(){ $('.diff_file').each(function(){
$('.image.diff_removed img', this).on('load', $.proxy(function(event){ $('.image.diff_removed img', this).on('load', $.proxy(function(event){
......
...@@ -38,10 +38,10 @@ ...@@ -38,10 +38,10 @@
%br/ %br/
.diff_file_content .diff_file_content
-# Skipp all non non-supported blobs -# Skip all non-supported blobs
- next unless file.respond_to?('text?') - next unless file.respond_to?('text?')
- if file.text? - if file.text?
= render "commits/text_file", diff: diff, index: i = render "commits/text_diff", diff: diff, index: i
- elsif file.image? - elsif file.image?
- old_file = (@commit.prev_commit.tree / diff.old_path) if !@commit.prev_commit.nil? - old_file = (@commit.prev_commit.tree / diff.old_path) if !@commit.prev_commit.nil?
- if diff.renamed_file || diff.new_file || diff.deleted_file - if diff.renamed_file || diff.new_file || diff.deleted_file
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
%a.supp_diff_link Diff suppressed. Click to show %a.supp_diff_link Diff suppressed. Click to show
%table{class: "#{'hide' if too_big}"} %table{class: "#{'hide' if too_big}"}
- each_diff_line(diff.diff.lines.to_a, index) do |line, type, line_code, line_new, line_old| - each_diff_line(diff, index) do |line, type, line_code, line_new, line_old|
%tr.line_holder{ id: line_code } %tr.line_holder{ id: line_code }
- if type == "match" - if type == "match"
%td.old_line= "..." %td.old_line= "..."
...@@ -13,11 +13,11 @@ ...@@ -13,11 +13,11 @@
%td.old_line %td.old_line
= link_to raw(type == "new" ? "&nbsp;" : line_old), "##{line_code}", id: line_code = link_to raw(type == "new" ? "&nbsp;" : line_old), "##{line_code}", id: line_code
- if @comments_allowed - if @comments_allowed
= render "notes/per_line_note_link", line_code: line_code = render "notes/diff_note_link", line_code: line_code
%td.new_line= link_to raw(type == "old" ? "&nbsp;" : line_new) , "##{line_code}", id: line_code %td.new_line= link_to raw(type == "old" ? "&nbsp;" : line_new) , "##{line_code}", id: line_code
%td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line) %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line)
- if @comments_allowed - if @reply_allowed
- comments = @line_notes.select { |n| n.line_code == line_code }.sort_by(&:created_at) - comments = @line_notes.select { |n| n.line_code == line_code }.sort_by(&:created_at)
- unless comments.empty? - unless comments.empty?
= render "notes/per_line_notes_with_reply", notes: comments = render "notes/diff_notes_with_reply", notes: comments
...@@ -56,4 +56,4 @@ ...@@ -56,4 +56,4 @@
= markdown @issue.description = markdown @issue.description
.issue_notes.voting_notes#notes= render "notes/notes_with_form", tid: @issue.id, tt: "issue" .voting_notes#notes= render "notes/notes_with_form"
...@@ -12,20 +12,18 @@ ...@@ -12,20 +12,18 @@
%li.notes-tab{data: {action: 'notes'}} %li.notes-tab{data: {action: 'notes'}}
= link_to project_merge_request_path(@project, @merge_request) do = link_to project_merge_request_path(@project, @merge_request) do
%i.icon-comment %i.icon-comment
Comments Discussion
%li.diffs-tab{data: {action: 'diffs'}} %li.diffs-tab{data: {action: 'diffs'}}
= link_to diffs_project_merge_request_path(@project, @merge_request) do = link_to diffs_project_merge_request_path(@project, @merge_request) do
%i.icon-list-alt %i.icon-list-alt
Diff Diff
.notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" } .notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" }
= render("notes/notes_with_form", tid: @merge_request.id, tt: "merge_request") = render "notes/notes_with_form"
.diffs.tab-content .diffs.tab-content
= render "merge_requests/show/diffs" if @diffs = render "merge_requests/show/diffs" if @diffs
.status .status
= render "notes/per_line_form"
:javascript :javascript
var merge_request; var merge_request;
$(function(){ $(function(){
......
= render "show" = render "show"
:javascript
$(function(){
PerLineNotes.init();
});
:plain :plain
merge_request.$(".diffs").html("#{escape_javascript(render(partial: "merge_requests/show/diffs"))}"); merge_request.$(".diffs").html("#{escape_javascript(render(partial: "merge_requests/show/diffs"))}");
PerLineNotes.init();
:plain :plain
merge_request.$(".notes").html("#{escape_javascript(render "notes/notes_with_form", tid: @merge_request.id, tt: "merge_request")}"); merge_request.$(".notes").html("#{escape_javascript(render "notes/notes_with_form")}");
.note-form-holder
= form_for [@project, @note], remote: "true", multipart: true do |f|
%h3.page_title Leave a comment
-if @note.errors.any?
.alert-message.block-message.error
- @note.errors.full_messages.each do |msg|
%div= msg
= f.hidden_field :noteable_id
= f.hidden_field :commit_id
= f.hidden_field :noteable_type
= f.text_area :note, size: 255, class: 'note-text js-gfm-input'
#preview-note.preview_note.hide
.hint
.right Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
.clearfix
.row.note_advanced_opts
.span3
= f.submit 'Add Comment', class: "btn success submit_note grouped", id: "submit_note"
= link_to 'Preview', preview_project_notes_path(@project), class: 'btn grouped', id: 'preview-link'
.span4.notify_opts
%h6.left Notify via email:
= label_tag :notify do
= check_box_tag :notify, 1, @note.noteable_type != "Commit"
%span Project team
- if @note.notify_only_author?(current_user)
= label_tag :notify_author do
= check_box_tag :notify_author, 1 , @note.noteable_type == "Commit"
%span Commit author
.span5.attachments
%h6.left Attachment:
%span.file_name File name...
.input.input_file
%a.file_upload.btn.small Upload File
= f.file_field :attachment, class: "input-file"
%span.hint Any file less than 10 MB
- if note.valid?
:plain
$(".note-form-holder .error").remove();
$('.note-form-holder textarea').val("");
$('.note-form-holder #preview-link').text('Preview');
$('.note-form-holder #preview-note').hide();
$('.note-form-holder').show();
NoteList.appendNewNote(#{note.id}, "#{escape_javascript(render "notes/note", note: note)}");
- else
:plain
$(".note-form-holder").replaceWith("#{escape_javascript(render 'notes/common_form')}");
GitLab.GfmAutoComplete.setup();
- if note.valid?
:plain
// hide and reset the form
$(".per_line_form").hide();
$('.line-note-form-holder textarea').val("");
// find the reply button for this line
// (might not be there if this is the first note)
var trRpl = $("a.line_note_reply_link[data-line-code='#{note.line_code}']").closest("tr");
if (trRpl.size() == 0) {
// find the commented line ...
var trEl = $(".#{note.line_code}").parent();
// ... and insert the note and the reply button after it
trEl.after("#{escape_javascript(render "notes/per_line_reply_button", line_code: note.line_code)}");
trEl.after("#{escape_javascript(render "notes/per_line_note", note: note)}");
} else {
// instert new note before reply button
trRpl.before("#{escape_javascript(render "notes/per_line_note", note: note)}");
}
- note = @project.notes.new(@comments_target.merge({ line_code: line_code }))
= link_to "",
"javascript:;",
class: "add-diff-note js-add-diff-note-button",
data: { noteable_type: note.noteable_type,
noteable_id: note.noteable_id,
commit_id: note.commit_id,
line_code: note.line_code,
discussion_id: note.discussion_id },
title: "Add a comment to this line"
- note = notes.first # example note
%tr.notes_holder
%td.notes_line{ colspan: 2 }
%span.btn.disabled
%i.icon-comment
= notes.count
%td.notes_content
%ul.notes{ rel: note.discussion_id }
= render notes
= render "notes/discussion_reply_button", note: note
- note = discussion_notes.first
.discussion.js-details-container.js-toggler-container.open{ class: note.discussion_id }
.discussion-header
.discussion-actions
= link_to "javascript:;", class: "js-details-target turn-on js-toggler-target" do
%i.icon-eye-close
Hide discussion
= link_to "javascript:;", class: "js-details-target turn-off js-toggler-target" do
%i.icon-eye-open
Show discussion
= image_tag gravatar_icon(note.author.email), class: "avatar s32"
%div
= link_to note.author_name, project_team_member_path(@project, @project.team_member_by_id(note.author)), class: "note-author"
- if note.for_merge_request?
- if note.diff
started a discussion on this merge request diff
= link_to_merge_request_diff_line_note(note)
- else
started
%strong
%i.icon-remove
outdated
discussion on this merge request diff
- elsif note.for_commit?
started a discussion on commit
#{link_to note.noteable.short_id, project_commit_path(@project, note.noteable)}
= link_to_commit_diff_line_note(note) if note.for_diff_line?
- else
%cite.cgray started a discussion
%div
- last_note = discussion_notes.last
last updated by
= link_to last_note.author_name, project_team_member_path(@project, @project.team_member_by_id(last_note.author)), class: "note-author"
%span.discussion-last-update
= time_ago_in_words(last_note.updated_at)
ago
.discussion-body
- if note.for_diff_line?
- if note.diff
.content
.diff_file= render "notes/discussion_diff", discussion_notes: discussion_notes, note: note
- else
= link_to 'show outdated discussion', '#', class: 'js-show-outdated-discussion'
%div.hide.outdated-discussion
.content
.notes{ rel: discussion_notes.first.discussion_id }
= render discussion_notes
- else
.content
.notes{ rel: discussion_notes.first.discussion_id }
= render discussion_notes
= render "notes/discussion_reply_button", note: discussion_notes.first
-# will be shown when the other one is hidden
.discussion-hidden.content.hide
.note
%em Hidden discussion.
= link_to "javascript:;", class: "js-details-target js-toggler-target" do
%i.icon-eye-open
Show
- diff = note.diff
.diff_file_header
- if diff.deleted_file
%span= diff.old_path
- else
%span= diff.new_path
- if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode
%span.file-mode= "#{diff.a_mode}#{diff.b_mode}"
%br/
.diff_file_content
%table
- each_diff_line(diff, note.diff_file_index) do |line, type, line_code, line_new, line_old|
%tr.line_holder{ id: line_code }
- if type == "match"
%td.old_line= "..."
%td.new_line= "..."
%td.line_content.matched= line
- else
%td.old_line= raw(type == "new" ? "&nbsp;" : line_old)
%td.new_line= raw(type == "old" ? "&nbsp;" : line_new)
%td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw "#{line} &nbsp;"
- if line_code == note.line_code
= render "notes/diff_notes_with_reply", notes: discussion_notes
- break # cut off diff after notes
= link_to "javascript:;",
class: "btn reply-btn js-discussion-reply-button",
data: { noteable_type: note.noteable_type,
noteable_id: note.noteable_id,
commit_id: note.commit_id,
line_code: note.line_code,
discussion_id: note.discussion_id },
title: "Add a reply" do
%i.icon-comment
Reply
= form_for [@project, @note], remote: true, html: { multipart: true, id: nil, class: "new_note js-new-note-form" } do |f|
= note_target_fields
= f.hidden_field :commit_id
= f.hidden_field :line_code
= f.hidden_field :noteable_id
= f.hidden_field :noteable_type
.note_text_and_preview.js-toggler-container
%a.js-note-preview-button.js-toggler-target.turn-off{ href: "javascript:;", title: "Preview", data: {url: preview_project_notes_path(@project)} }
%i.icon-eye-open
%a.js-note-edit-button.js-toggler-target.turn-off{ href: "javascript:;", title: "Edit" }
%i.icon-edit
= f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on'
.note_preview.js-note-preview.turn-off
.buttons
= f.submit 'Add Comment', class: "btn comment-btn grouped js-comment-button"
%a.btn.grouped.js-close-discussion-note-form Cancel
.hint
.right Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
.clearfix
.note_options
.attachment
%h6 Attachment:
.file_name.js-attachment-filename File name...
%a.choose-btn.btn.small.js-choose-note-attachment-button Choose File ...
.hint Any file up to 10 MB
= f.file_field :attachment, class: "js-note-attachment-input"
.notify_options
%h6 Notify via email:
= label_tag :notify do
= check_box_tag :notify, 1, !@note.for_commit?
Project team
.js-notify-commit-author
= label_tag :notify_author do
= check_box_tag :notify_author, 1 , @note.for_commit?
Commit author
.clearfix
.error_message.js-errors
- note.errors.full_messages.each do |msg|
%div= msg
%li{id: dom_id(note), class: "note"} %li{ id: dom_id(note), class: dom_class(note), data: { discussion: note.discussion_id } }
= image_tag gravatar_icon(note.author.email), class: "avatar s32" .note-header
%div.note-author .note-actions
%strong= note.author_name = link_to "##{dom_id(note)}", name: dom_id(note) do
= link_to "##{dom_id(note)}", name: dom_id(note) do %i.icon-link
%cite.cgray Link here
= time_ago_in_words(note.updated_at) &nbsp;
ago - if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project)
= link_to project_note_path(@project, note), title: "Remove comment", method: :delete, confirm: 'Are you sure you want to remove comment?', remote: true, class: "danger js-note-delete" do
%i.icon-trash.cred
= image_tag gravatar_icon(note.author.email), class: "avatar s32"
= link_to note.author_name, project_team_member_path(@project, @project.team_member_by_id(note.author)), class: "note-author"
%span.note-last-update
= time_ago_in_words(note.updated_at)
ago
- unless note_for_main_target?(note) - if note.upvote?
- if note.for_commit? %span.vote.upvote.label.label-success
%span.cgray %i.icon-thumbs-up
on #{link_to note.noteable.short_id, project_commit_path(@project, note.noteable)} \+1
= link_to_commit_diff_line_note(note) if note.for_diff_line? - if note.downvote?
%span.vote.downvote.label.label-error
%i.icon-thumbs-down
\-1
-# only show vote if it's a note for the main target
- if note_for_main_target?(note)
- if note.upvote?
%span.vote.upvote.label.label-success
%i.icon-thumbs-up
\+1
- if note.downvote?
%span.vote.downvote.label.label-error
%i.icon-thumbs-down
\-1
-# remove button .note-body
- if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project)
= link_to [@project, note], confirm: 'Are you sure?', method: :delete, remote: true, class: "cred delete-note btn very_small" do
%i.icon-trash
Remove
%div.note-title
= preserve do = preserve do
= markdown(note.note) = markdown(note.note)
- if note.attachment.url - if note.attachment.url
- if note.attachment.image? - if note.attachment.image?
= image_tag note.attachment.url, class: 'thumbnail span4' = image_tag note.attachment.url, class: 'note-image-attach'
.right .attachment.right
%div.file = link_to note.attachment.url, target: "_blank" do
= link_to note.attachment_identifier, note.attachment.url, target: "_blank" %i.icon-attachment
= note.attachment_identifier
.clear .clear
- @notes.each do |note| - if @discussions.present?
- next unless note.author - @discussions.each do |discussion_notes|
= render "note", note: note - note = discussion_notes.first
- if note_for_main_target?(note)
= render discussion_notes
- else
= render 'discussion', discussion_notes: discussion_notes
- else
- @notes.each do |note|
- next unless note.author
= render 'note', note: note
%ul#notes-list %ul#notes-list.notes
%ul#new-notes-list .js-notes-busy
.notes-status
.js-main-target-form
- if can? current_user, :write_note, @project - if can? current_user, :write_note, @project
= render "notes/common_form" = render "notes/form"
:javascript :javascript
$(function(){ $(function(){
NoteList.init("#{tid}", "#{tt}", "#{project_notes_path(@project)}"); NoteList.init("#{@target_id}", "#{@target_type}", "#{project_notes_path(@project)}");
}); });
%table{style: "display:none;"}
%tr.per_line_form
%td{colspan: 3 }
.line-note-form-holder
= form_for [@project, @note], remote: "true", multipart: true do |f|
%h3.page_title Leave a note
%div.span10
-if @note.errors.any?
.alert-message.block-message.error
- @note.errors.full_messages.each do |msg|
%div= msg
= f.hidden_field :noteable_id
= f.hidden_field :commit_id
= f.hidden_field :noteable_type
= f.hidden_field :line_code
= f.text_area :note, size: 255, class: 'line-note-text js-gfm-input'
.note_actions
.buttons
= f.submit 'Add note', class: "btn save-btn submit_note submit_inline_note", id: "submit_note"
= link_to "Cancel", "#", class: "btn hide-button"
.options
%h6.left Notify via email:
.labels
= label_tag :notify do
= check_box_tag :notify, 1, @note.noteable_type != "Commit"
%span Project team
- if @note.notify_only_author?(current_user)
= label_tag :notify_author do
= check_box_tag :notify_author, 1 , @note.noteable_type == "Commit"
%span Commit author
:javascript
$(function(){
$(".per_line_form .hide-button").bind("click", function(){
$('.per_line_form').hide();
return false;
});
});
%tr.line_notes_row
%td{colspan: 3}
%ul
= render "notes/note", note: note
= link_to "", "#", class: "line_note_link", data: { line_code: line_code }, title: "Add note for this line"
- notes.each do |note|
= render "notes/per_line_note", note: note
= render "notes/per_line_reply_button", line_code: notes.first.line_code
%tr.line_notes_row.reply
%td{colspan: 3}
%i.icon-comment
= link_to "Reply", "#", class: "line_note_reply_link", data: { line_code: line_code }, title: "Add note for this line"
.js-main-target-form
- if can? current_user, :write_note, @project - if can? current_user, :write_note, @project
= render "notes/common_form" = render "notes/form"
%ul.reversed#new-notes-list %ul#new-notes-list.reversed.notes
%ul.reversed#notes-list %ul#notes-list.reversed.notes
.notes-status .notes-busy.js-notes-busy
:javascript :javascript
$(function(){ $(function(){
NoteList.init("#{tid}", "#{tt}", "#{project_notes_path(@project)}"); NoteList.init("#{@target_id}", "#{@target_type}", "#{project_notes_path(@project)}");
}); });
- if @note.line_code - if @note.valid?
= render "create_per_line_note", note: @note var noteHtml = "#{escape_javascript(render "notes/note", note: @note)}";
- else
= render "create_common_note", note: @note - if note_for_main_target?(@note)
- if @note.for_wall?
NoteList.appendNewWallNote(#{@note.id}, noteHtml);
- else
NoteList.appendNewNote(#{@note.id}, noteHtml);
- else
:plain
var firstDiscussionNoteHtml = "#{escape_javascript(render "notes/diff_notes_with_reply", notes: [@note])}";
NoteList.appendNewDiscussionNote("#{@note.discussion_id}",
firstDiscussionNoteHtml,
noteHtml);
-# Enable submit button - else
:plain var errorsHtml = "#{escape_javascript(render 'notes/form_errors', note: @note)}";
$("#submit_note").removeAttr("disabled"); - if note_for_main_target?(@note)
NoteList.errorsOnForm(errorsHtml);
- else
NoteList.errorsOnForm(errorsHtml, "#{@note.discussion_id}");
\ No newline at end of file
- unless @notes.blank? - unless @notes.blank?
var notesHtml = "#{escape_javascript(render 'notes/notes')}";
- new_note_ids = @notes.map(&:id)
- if loading_more_notes? - if loading_more_notes?
:plain NoteList.appendMoreNotes(#{new_note_ids}, notesHtml);
NoteList.appendMoreNotes(#{@notes.last.id}, "#{escape_javascript(render 'notes/notes')}");
- elsif loading_new_notes? - elsif loading_new_notes?
:plain NoteList.replaceNewNotes(#{new_note_ids}, notesHtml);
NoteList.replaceNewNotes("#{escape_javascript(render 'notes/notes')}");
- else - else
:plain NoteList.setContent(#{new_note_ids}, notesHtml);
NoteList.setContent(#{@notes.first.id}, #{@notes.last.id}, "#{escape_javascript(render 'notes/notes')}");
- else - else
- if loading_more_notes? - if loading_more_notes?
:plain NoteList.finishedLoadingMore();
NoteList.finishedLoadingMore();
%div.wall_page %div.wall_page
= render "notes/reversed_notes_with_form", tid: nil, tt: "wall" = render "notes/reversed_notes_with_form"
...@@ -8,4 +8,4 @@ ...@@ -8,4 +8,4 @@
%br %br
%div= render 'blob' %div= render 'blob'
%div#notes= render "notes/notes_with_form", tid: @snippet.id, tt: "snippet" %div#notes= render "notes/notes_with_form"
Feature: Project Comment commit Feature: Comments on commits
Background: Background:
Given I sign in as a user Given I sign in as a user
And I own project "Shop" And I own project "Shop"
Given I visit project commit page And I visit project commit page
@javascript @javascript
Scenario: I comment commit Scenario: I can comment on a commit
Given I leave a comment like "XML attached" Given I leave a comment like "XML attached"
Then I should see comment "XML attached" Then I should see a comment saying "XML attached"
@javascript
Scenario: I can't cancel the main form
Then I should not see the cancel comment button
@javascript
Scenario: I can't preview without text
Given I haven't written any comment text
Then I should not see the comment preview button
@javascript
Scenario: I can preview with text
Given I write a comment like "Nice"
Then I should see the comment preview button
@javascript
Scenario: I preview a comment
Given I preview a comment text like "Bug fixed :smile:"
Then I should see the comment preview
And I should not see the comment text field
@javascript
Scenario: I can edit after preview
Given I preview a comment text like "Bug fixed :smile:"
Then I should see the comment edit button
@javascript
Scenario: I have a reset form after posting from preview
Given I preview a comment text like "Bug fixed :smile:"
And I submit the comment
Then I should see an empty comment text field
And I should not see the comment preview
@javascript
Scenario: I can delete a comment
Given I leave a comment like "XML attached"
And I delete a comment
Then I should not see a comment saying "XML attached"
Feature: Comments on commit diffs
Background:
Given I sign in as a user
And I own project "Shop"
And I visit project commit page
@javascript
Scenario: I can access add diff comment buttons
Then I should see add a diff comment button
@javascript
Scenario: I can comment on a commit diff
Given I leave a diff comment like "Typo, please fix"
Then I should see a diff comment saying "Typo, please fix"
@javascript
Scenario: I get a temporary form for the first comment on a diff line
Given I open a diff comment form
Then I should see a temporary diff comment form
@javascript
Scenario: I have a cancel button on the diff form
Given I open a diff comment form
Then I should see the cancel comment button
@javascript
Scenario: I can cancel a diff form
Given I open a diff comment form
And I cancel the diff comment
Then I should not see the diff comment form
@javascript
Scenario: I can't open a second form for a diff line
Given I open a diff comment form
And I open a diff comment form
Then I should only see one diff form
@javascript
Scenario: I can have multiple forms
Given I open a diff comment form
And I write a diff comment like ":-1: I don't like this"
And I open another diff comment form
Then I should see a diff comment form with ":-1: I don't like this"
And I should see an empty diff comment form
@javascript
Scenario: I can preview multiple forms separately
Given I preview a diff comment text like "Should fix it :smile:"
And I preview another diff comment text like "DRY this up"
Then I should see two separate previews
@javascript
Scenario: I have a reply button in discussions
Given I leave a diff comment like "Typo, please fix"
Then I should see a discussion reply button
@javascript
Scenario: I can't preview without text
Given I open a diff comment form
And I haven't written any diff comment text
Then I should not see the diff comment preview button
@javascript
Scenario: I can preview with text
Given I open a diff comment form
And I write a diff comment like ":-1: I don't like this"
Then I should see the diff comment preview button
@javascript
Scenario: I preview a diff comment
Given I preview a diff comment text like "Should fix it :smile:"
Then I should see the diff comment preview
And I should not see the diff comment text field
@javascript
Scenario: I can edit after preview
Given I preview a diff comment text like "Should fix it :smile:"
Then I should see the diff comment edit button
@javascript
Scenario: The form gets removed after posting
Given I preview a diff comment text like "Should fix it :smile:"
And I submit the diff comment
Then I should not see the diff comment form
And I should see a discussion reply button
@javascript
Scenario: I can delete a discussion comment
Given I leave a diff comment like "Typo, please fix"
And I delete a diff comment
Then I should not see a diff comment saying "Typo, please fix"
...@@ -35,8 +35,34 @@ Feature: Project Merge Requests ...@@ -35,8 +35,34 @@ Feature: Project Merge Requests
Then I should see merge request "Wiki Feature" Then I should see merge request "Wiki Feature"
@javascript @javascript
Scenario: I comment merge request Scenario: I comment on a merge request
Given I visit merge request page "Bug NS-04" Given I visit merge request page "Bug NS-04"
And I leave a comment like "XML attached" And I leave a comment like "XML attached"
Then I should see comment "XML attached" Then I should see comment "XML attached"
@javascript
Scenario: I comment on a merge request diff
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I switch to the diff tab
And I leave a comment like "Line is wrong" on line 185 of the first file
And I switch to the merge request's comments tab
Then I should see a discussion has started on line 185
@javascript
Scenario: I comment on a line of a commit in merge request
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the first commit in the merge request
And I leave a comment like "Line is wrong" on line 185 of the first file
And I switch to the merge request's comments tab
Then I should see a discussion has started on commit bcf03b5de6c:L185
@javascript
Scenario: I comment on a commit in merge request
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the first commit in the merge request
And I leave a comment on the diff page
And I switch to the merge request's comments tab
Then I should see a discussion has started on commit bcf03b5de6c
class CommentsOnCommitDiffs < Spinach::FeatureSteps
include SharedAuthentication
include SharedDiffNote
include SharedPaths
include SharedProject
end
class ProjectCommentCommit < Spinach::FeatureSteps class CommentsOnCommits < Spinach::FeatureSteps
include SharedAuthentication include SharedAuthentication
include SharedProject
include SharedNote include SharedNote
include SharedPaths include SharedPaths
include SharedProject
end end
...@@ -4,50 +4,55 @@ class ProjectMergeRequests < Spinach::FeatureSteps ...@@ -4,50 +4,55 @@ class ProjectMergeRequests < Spinach::FeatureSteps
include SharedNote include SharedNote
include SharedPaths include SharedPaths
Then 'I should see "Bug NS-04" in merge requests' do Given 'I click link "New Merge Request"' do
page.should have_content "Bug NS-04" click_link "New Merge Request"
end end
And 'I should not see "Feature NS-03" in merge requests' do Given 'I click link "Bug NS-04"' do
page.should_not have_content "Feature NS-03" click_link "Bug NS-04"
end
Given 'I click link "All"' do
click_link "All"
end end
Given 'I click link "Closed"' do Given 'I click link "Closed"' do
click_link "Closed" click_link "Closed"
end end
Then 'I should see "Feature NS-03" in merge requests' do Then 'I should see merge request "Wiki Feature"' do
page.should have_content "Feature NS-03" page.should have_content "Wiki Feature"
end end
And 'I should not see "Bug NS-04" in merge requests' do Then 'I should see closed merge request "Bug NS-04"' do
page.should_not have_content "Bug NS-04" mr = MergeRequest.find_by_title("Bug NS-04")
mr.closed.should be_true
page.should have_content "Closed by"
end end
Given 'I click link "All"' do Then 'I should see merge request "Bug NS-04"' do
click_link "All" page.should have_content "Bug NS-04"
end end
Given 'I click link "Bug NS-04"' do Then 'I should see "Bug NS-04" in merge requests' do
click_link "Bug NS-04" page.should have_content "Bug NS-04"
end end
Then 'I should see merge request "Bug NS-04"' do Then 'I should see "Feature NS-03" in merge requests' do
page.should have_content "Bug NS-04" page.should have_content "Feature NS-03"
end end
And 'I click link "Close"' do And 'I should not see "Feature NS-03" in merge requests' do
click_link "Close" page.should_not have_content "Feature NS-03"
end end
Then 'I should see closed merge request "Bug NS-04"' do
mr = MergeRequest.find_by_title("Bug NS-04") And 'I should not see "Bug NS-04" in merge requests' do
mr.closed.should be_true page.should_not have_content "Bug NS-04"
page.should have_content "Closed by"
end end
Given 'I click link "New Merge Request"' do And 'I click link "Close"' do
click_link "New Merge Request" click_link "Close"
end end
And 'I submit new merge request "Wiki Feature"' do And 'I submit new merge request "Wiki Feature"' do
...@@ -57,24 +62,91 @@ class ProjectMergeRequests < Spinach::FeatureSteps ...@@ -57,24 +62,91 @@ class ProjectMergeRequests < Spinach::FeatureSteps
click_button "Submit merge request" click_button "Submit merge request"
end end
Then 'I should see merge request "Wiki Feature"' do
page.should have_content "Wiki Feature"
end
And 'project "Shop" have "Bug NS-04" open merge request' do And 'project "Shop" have "Bug NS-04" open merge request' do
project = Project.find_by_name("Shop") project = Project.find_by_name("Shop")
create(:merge_request, create(:merge_request,
:title => "Bug NS-04", title: "Bug NS-04",
:project => project, project: project,
:author => project.users.first) author: project.users.first)
end
And 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
project = Project.find_by_name("Shop")
create(:merge_request_with_diffs,
title: "Bug NS-05",
project: project,
author: project.users.first)
end end
And 'project "Shop" have "Feature NS-03" closed merge request' do And 'project "Shop" have "Feature NS-03" closed merge request' do
project = Project.find_by_name("Shop") project = Project.find_by_name("Shop")
create(:merge_request, create(:merge_request,
:title => "Feature NS-03", title: "Feature NS-03",
:project => project, project: project,
:author => project.users.first, author: project.users.first,
:closed => true) closed: true)
end
And 'I switch to the diff tab' do
mr = MergeRequest.find_by_title("Bug NS-05")
visit diffs_project_merge_request_path(mr.project, mr)
end
And 'I switch to the merge request\'s comments tab' do
mr = MergeRequest.find_by_title("Bug NS-05")
visit project_merge_request_path(mr.project, mr)
end
And 'I click on the first commit in the merge request' do
mr = MergeRequest.find_by_title("Bug NS-05")
click_link mr.commits.first.short_id(8)
end
And 'I leave a comment on the diff page' do
within(:xpath, "//div[@class='note-form-holder']") do
fill_in "note_note", with: "One comment to rule them all"
click_button "Add Comment"
end
end
And 'I leave a comment like "Line is wrong" on line 185 of the first file' do
save_and_open_page
within(:xpath, "//div[@class='diff_file'][1]") do
click_link "add-diff-line-note-0_185_185"
end
within(:xpath, "//div[@class='line-note-form-holder']") do
fill_in "note_note", with: "Line is wrong"
click_button "Add Comment"
end
end
Then 'I should see a discussion has started on line 185' do
mr = MergeRequest.find_by_title("Bug NS-05")
first_commit = mr.commits.first
first_diff = mr.diffs.first
page.should have_content "#{current_user.name} started a discussion on this merge request diff"
page.should have_content "#{first_diff.b_path}:L185"
page.should have_content "Line is wrong"
end
Then 'I should see a discussion has started on commit bcf03b5de6c:L185' do
mr = MergeRequest.find_by_title("Bug NS-05")
first_commit = mr.commits.first
first_diff = mr.diffs.first
page.should have_content "#{current_user.name} started a discussion on commit"
page.should have_content first_commit.short_id(8)
page.should have_content "#{first_diff.b_path}:L185"
page.should have_content "Line is wrong"
end
Then 'I should see a discussion has started on commit bcf03b5de6c' do
mr = MergeRequest.find_by_title("Bug NS-05")
first_commit = mr.st_commits.first
first_diff = mr.diffs.first
page.should have_content "#{current_user.name} started a discussion on commit"
page.should have_content first_commit.short_id(8)
page.should have_content "One comment to rule them all"
page.should_not have_content "#{first_diff.b_path}:L185"
end end
end end
module SharedDiffNote
include Spinach::DSL
Given 'I cancel the diff comment' do
within(".diff_file") do
find(".js-close-discussion-note-form").trigger("click")
end
end
Given 'I delete a diff comment' do
within(".diff_file") do
first(".js-note-delete").trigger("click")
end
end
Given 'I haven\'t written any diff comment text' do
within(".diff_file") do
fill_in "note[note]", with: ""
end
end
Given 'I leave a diff comment like "Typo, please fix"' do
find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click")
within(".diff_file") do
fill_in "note[note]", with: "Typo, please fix"
#click_button("Add Comment")
find(".js-comment-button").trigger("click")
end
end
Given 'I preview a diff comment text like "Should fix it :smile:"' do
find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click")
within(".diff_file") do
fill_in "note[note]", with: "Should fix it :smile:"
find(".js-note-preview-button").trigger("click")
end
end
Given 'I preview another diff comment text like "DRY this up"' do
find("#586fb7c4e1add2d4d24e27566ed7064680098646_57_41.line_holder .js-add-diff-note-button").trigger("click")
within(".diff_file") do
fill_in "note[note]", with: "DRY this up"
find(".js-note-preview-button").trigger("click")
end
end
Given 'I open a diff comment form' do
find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click")
end
Given 'I open another diff comment form' do
find("#586fb7c4e1add2d4d24e27566ed7064680098646_57_41.line_holder .js-add-diff-note-button").trigger("click")
end
Given 'I write a diff comment like ":-1: I don\'t like this"' do
within(".diff_file") do
fill_in "note[note]", with: ":-1: I don\'t like this"
end
end
Given 'I submit the diff comment' do
within(".diff_file") do
click_button("Add Comment")
end
end
Then 'I should not see the diff comment form' do
within(".diff_file") do
page.should_not have_css("form.new_note")
end
end
Then 'I should not see the diff comment preview button' do
within(".diff_file") do
page.should have_css(".js-note-preview-button", visible: false)
end
end
Then 'I should not see the diff comment text field' do
within(".diff_file") do
page.should have_css(".js-note-text", visible: false)
end
end
Then 'I should only see one diff form' do
within(".diff_file") do
page.should have_css("form.new_note", count: 1)
end
end
Then 'I should see a diff comment form with ":-1: I don\'t like this"' do
within(".diff_file") do
page.should have_field("note[note]", with: ":-1: I don\'t like this")
end
end
Then 'I should see a diff comment saying "Typo, please fix"' do
within(".diff_file .note") do
page.should have_content("Typo, please fix")
end
end
Then 'I should see a discussion reply button' do
within(".diff_file") do
page.should have_link("Reply")
end
end
Then 'I should see a temporary diff comment form' do
within(".diff_file") do
page.should have_css(".js-temp-notes-holder form.new_note")
end
end
Then 'I should see add a diff comment button' do
page.should have_css(".js-add-diff-note-button", visible: false)
end
Then 'I should see an empty diff comment form' do
within(".diff_file") do
page.should have_field("note[note]", with: "")
end
end
Then 'I should see the cancel comment button' do
within(".diff_file form") do
page.should have_css(".js-close-discussion-note-form", text: "Cancel")
end
end
Then 'I should see the diff comment preview' do
within(".diff_file form") do
page.should have_css(".js-note-preview", visible: false)
end
end
Then 'I should see the diff comment edit button' do
within(".diff_file") do
page.should have_css(".js-note-edit-button", visible: true)
end
end
Then 'I should see the diff comment preview button' do
within(".diff_file") do
page.should have_css(".js-note-preview-button", visible: true)
end
end
Then 'I should see two separate previews' do
within(".diff_file") do
page.should have_css(".js-note-preview", visible: true, count: 2)
page.should have_content("Should fix it")
page.should have_content("DRY this up")
end
end
end
module SharedNote module SharedNote
include Spinach::DSL include Spinach::DSL
Given 'I delete a comment' do
first(".js-note-delete").trigger("click")
end
Given 'I haven\'t written any comment text' do
within(".js-main-target-form") do
fill_in "note[note]", with: ""
end
end
Given 'I leave a comment like "XML attached"' do Given 'I leave a comment like "XML attached"' do
fill_in "note_note", :with => "XML attached" within(".js-main-target-form") do
click_button "Add Comment" fill_in "note[note]", with: "XML attached"
click_button "Add Comment"
end
end end
Then 'I should see comment "XML attached"' do Given 'I preview a comment text like "Bug fixed :smile:"' do
page.should have_content "XML attached" within(".js-main-target-form") do
fill_in "note[note]", with: "Bug fixed :smile:"
find(".js-note-preview-button").trigger("click")
end
end end
Given 'I submit the comment' do
within(".js-main-target-form") do
click_button "Add Comment"
end
end
Given 'I write a comment like "Nice"' do
within(".js-main-target-form") do
fill_in "note[note]", with: "Nice"
end
end
Then 'I should not see a comment saying "XML attached"' do
page.should_not have_css(".note")
end
Then 'I should not see the cancel comment button' do
within(".js-main-target-form") do
should_not have_link("Cancel")
end
end
Then 'I should not see the comment preview' do
within(".js-main-target-form") do
page.should have_css(".js-note-preview", visible: false)
end
end
Then 'I should not see the comment preview button' do
within(".js-main-target-form") do
page.should have_css(".js-note-preview-button", visible: false)
end
end
Then 'I should not see the comment text field' do
within(".js-main-target-form") do
page.should have_css(".js-note-text", visible: false)
end
end
Then 'I should see a comment saying "XML attached"' do
within(".note") do
page.should have_content("XML attached")
end
end
Then 'I should see an empty comment text field' do
within(".js-main-target-form") do
page.should have_field("note[note]", with: "")
end
end
Then 'I should see the comment edit button' do
within(".js-main-target-form") do
page.should have_css(".js-note-edit-button", visible: true)
end
end
Then 'I should see the comment preview' do
within(".js-main-target-form") do
page.should have_css(".js-note-preview", visible: true)
end
end
Then 'I should see the comment preview button' do
within(".js-main-target-form") do
page.should have_css(".js-note-preview-button", visible: true)
end
end
# Wall
Given 'I write new comment "my special test message"' do Given 'I write new comment "my special test message"' do
fill_in "note_note", :with => "my special test message" within(".js-main-target-form") do
click_button "Add Comment" fill_in "note[note]", with: "my special test message"
click_button "Add Comment"
end
end end
Then 'I should see project wall note "my special test message"' do Then 'I should see project wall note "my special test message"' do
......
...@@ -224,6 +224,11 @@ module SharedPaths ...@@ -224,6 +224,11 @@ module SharedPaths
visit project_merge_request_path(mr.project, mr) visit project_merge_request_path(mr.project, mr)
end end
Given 'I visit merge request page "Bug NS-05"' do
mr = MergeRequest.find_by_title("Bug NS-05")
visit project_merge_request_path(mr.project, mr)
end
And 'I visit project "Shop" merge requests page' do And 'I visit project "Shop" merge requests page' do
visit project_merge_requests_path(Project.find_by_name("Shop")) visit project_merge_requests_path(Project.find_by_name("Shop"))
end end
......
...@@ -25,22 +25,6 @@ module Gitlab ...@@ -25,22 +25,6 @@ module Gitlab
# >> gfm(":trollface:") # >> gfm(":trollface:")
# => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" /> # => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" />
module Markdown module Markdown
REFERENCE_PATTERN = %r{
(?<prefix>\W)? # Prefix
( # Reference
@(?<user>[a-zA-Z][a-zA-Z0-9_\-\.]*) # User name
|\#(?<issue>\d+) # Issue ID
|!(?<merge_request>\d+) # MR ID
|\$(?<snippet>\d+) # Snippet ID
|(?<commit>[\h]{6,40}) # Commit ID
)
(?<suffix>\W)? # Suffix
}x.freeze
TYPES = [:user, :issue, :merge_request, :snippet, :commit].freeze
EMOJI_PATTERN = %r{(:(\S+):)}.freeze
attr_reader :html_options attr_reader :html_options
# Public: Parse the provided text with GitLab-Flavored Markdown # Public: Parse the provided text with GitLab-Flavored Markdown
...@@ -96,6 +80,20 @@ module Gitlab ...@@ -96,6 +80,20 @@ module Gitlab
text text
end end
REFERENCE_PATTERN = %r{
(?<prefix>\W)? # Prefix
( # Reference
@(?<user>[a-zA-Z][a-zA-Z0-9_\-\.]*) # User name
|\#(?<issue>\d+) # Issue ID
|!(?<merge_request>\d+) # MR ID
|\$(?<snippet>\d+) # Snippet ID
|(?<commit>[\h]{6,40}) # Commit ID
)
(?<suffix>\W)? # Suffix
}x.freeze
TYPES = [:user, :issue, :merge_request, :snippet, :commit].freeze
def parse_references(text) def parse_references(text)
# parse reference links # parse reference links
text.gsub!(REFERENCE_PATTERN) do |match| text.gsub!(REFERENCE_PATTERN) do |match|
...@@ -115,11 +113,13 @@ module Gitlab ...@@ -115,11 +113,13 @@ module Gitlab
end end
end end
EMOJI_PATTERN = %r{(:(\S+):)}.freeze
def parse_emoji(text) def parse_emoji(text)
# parse emoji # parse emoji
text.gsub!(EMOJI_PATTERN) do |match| text.gsub!(EMOJI_PATTERN) do |match|
if valid_emoji?($2) if valid_emoji?($2)
image_tag("emoji/#{$2}.png", size: "20x20", class: 'emoji', title: $1, alt: $1) image_tag("emoji/#{$2}.png", class: 'emoji', title: $1, alt: $1, size: "20x20")
else else
match match
end end
......
...@@ -73,8 +73,8 @@ FactoryGirl.define do ...@@ -73,8 +73,8 @@ FactoryGirl.define do
# pick 3 commits "at random" (from bcf03b5d~3 to bcf03b5d) # pick 3 commits "at random" (from bcf03b5d~3 to bcf03b5d)
trait :with_diffs do trait :with_diffs do
target_branch "bcf03b5d~3" target_branch "master" # pretend bcf03b5d~3
source_branch "bcf03b5d" source_branch "stable" # pretend bcf03b5d
st_commits do st_commits do
[Commit.new(project.repo.commit('bcf03b5d')), [Commit.new(project.repo.commit('bcf03b5d')),
Commit.new(project.repo.commit('bcf03b5d~1')), Commit.new(project.repo.commit('bcf03b5d~1')),
...@@ -92,6 +92,32 @@ FactoryGirl.define do ...@@ -92,6 +92,32 @@ FactoryGirl.define do
factory :note do factory :note do
project project
note "Note" note "Note"
author
factory :note_on_commit, traits: [:on_commit]
factory :note_on_commit_diff, traits: [:on_commit, :on_diff]
factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
factory :note_on_merge_request, traits: [:on_merge_request]
factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff]
trait :on_commit do
commit_id "bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a"
noteable_type "Commit"
end
trait :on_diff do
line_code "0_184_184"
end
trait :on_merge_request do
noteable_id 1
noteable_type "MergeRequest"
end
trait :on_issue do
noteable_id 1
noteable_type "Issue"
end
end end
factory :event do factory :event do
......
require 'spec_helper' require 'spec_helper'
describe Issue do describe Issue do
let(:issue) { create(:issue) } it { should include_module(Votes) }
end
describe MergeRequest do
let(:merge_request) { FactoryGirl.create(:merge_request_with_diffs) }
it { should include_module(Votes) }
describe "#upvotes" do describe "#upvotes" do
it "with no notes has a 0/0 score" do it "with no notes has a 0/0 score" do
issue.upvotes.should == 0 merge_request.upvotes.should == 0
end end
it "should recognize non-+1 notes" do it "should recognize non-+1 notes" do
issue.notes << create(:note, note: "No +1 here") merge_request.notes << create(:note, note: "No +1 here")
issue.should have(1).note merge_request.should have(1).note
issue.notes.first.upvote?.should be_false merge_request.notes.first.upvote?.should be_false
issue.upvotes.should == 0 merge_request.upvotes.should == 0
end end
it "should recognize a single +1 note" do it "should recognize a single +1 note" do
issue.notes << create(:note, note: "+1 This is awesome") merge_request.notes << create(:note, note: "+1 This is awesome")
issue.upvotes.should == 1 merge_request.upvotes.should == 1
end end
it "should recognize multiple +1 notes" do it "should recognize multiple +1 notes" do
issue.notes << create(:note, note: "+1 This is awesome") merge_request.notes << create(:note, note: "+1 This is awesome")
issue.notes << create(:note, note: "+1 I want this") merge_request.notes << create(:note, note: "+1 I want this")
issue.upvotes.should == 2 merge_request.upvotes.should == 2
end end
end end
describe "#downvotes" do describe "#downvotes" do
it "with no notes has a 0/0 score" do it "with no notes has a 0/0 score" do
issue.downvotes.should == 0 merge_request.downvotes.should == 0
end end
it "should recognize non--1 notes" do it "should recognize non--1 notes" do
issue.notes << create(:note, note: "Almost got a -1") merge_request.notes << create(:note, note: "Almost got a -1")
issue.should have(1).note merge_request.should have(1).note
issue.notes.first.downvote?.should be_false merge_request.notes.first.downvote?.should be_false
issue.downvotes.should == 0 merge_request.downvotes.should == 0
end end
it "should recognize a single -1 note" do it "should recognize a single -1 note" do
issue.notes << create(:note, note: "-1 This is bad") merge_request.notes << create(:note, note: "-1 This is bad")
issue.downvotes.should == 1 merge_request.downvotes.should == 1
end end
it "should recognize multiple -1 notes" do it "should recognize multiple -1 notes" do
issue.notes << create(:note, note: "-1 This is bad") merge_request.notes << create(:note, note: "-1 This is bad")
issue.notes << create(:note, note: "-1 Away with this") merge_request.notes << create(:note, note: "-1 Away with this")
issue.downvotes.should == 2 merge_request.downvotes.should == 2
end end
end end
describe "#votes_count" do describe "#votes_count" do
it "with no notes has a 0/0 score" do it "with no notes has a 0/0 score" do
issue.votes_count.should == 0 merge_request.votes_count.should == 0
end end
it "should recognize non notes" do it "should recognize non notes" do
issue.notes << create(:note, note: "No +1 here") merge_request.notes << create(:note, note: "No +1 here")
issue.should have(1).note merge_request.should have(1).note
issue.votes_count.should == 0 merge_request.votes_count.should == 0
end end
it "should recognize a single +1 note" do it "should recognize a single +1 note" do
issue.notes << create(:note, note: "+1 This is awesome") merge_request.notes << create(:note, note: "+1 This is awesome")
issue.votes_count.should == 1 merge_request.votes_count.should == 1
end end
it "should recognize a single -1 note" do it "should recognize a single -1 note" do
issue.notes << create(:note, note: "-1 This is bad") merge_request.notes << create(:note, note: "-1 This is bad")
issue.votes_count.should == 1 merge_request.votes_count.should == 1
end end
it "should recognize multiple notes" do it "should recognize multiple notes" do
issue.notes << create(:note, note: "+1 This is awesome") merge_request.notes << create(:note, note: "+1 This is awesome")
issue.notes << create(:note, note: "-1 This is bad") merge_request.notes << create(:note, note: "-1 This is bad")
issue.notes << create(:note, note: "+1 I want this") merge_request.notes << create(:note, note: "+1 I want this")
issue.votes_count.should == 3 merge_request.votes_count.should == 3
end end
end end
describe "#upvotes_in_percent" do describe "#upvotes_in_percent" do
it "with no notes has a 0% score" do it "with no notes has a 0% score" do
issue.upvotes_in_percent.should == 0 merge_request.upvotes_in_percent.should == 0
end end
it "should count a single 1 note as 100%" do it "should count a single 1 note as 100%" do
issue.notes << create(:note, note: "+1 This is awesome") merge_request.notes << create(:note, note: "+1 This is awesome")
issue.upvotes_in_percent.should == 100 merge_request.upvotes_in_percent.should == 100
end end
it "should count multiple +1 notes as 100%" do it "should count multiple +1 notes as 100%" do
issue.notes << create(:note, note: "+1 This is awesome") merge_request.notes << create(:note, note: "+1 This is awesome")
issue.notes << create(:note, note: "+1 I want this") merge_request.notes << create(:note, note: "+1 I want this")
issue.upvotes_in_percent.should == 100 merge_request.upvotes_in_percent.should == 100
end end
it "should count fractions for multiple +1 and -1 notes correctly" do it "should count fractions for multiple +1 and -1 notes correctly" do
issue.notes << create(:note, note: "+1 This is awesome") merge_request.notes << create(:note, note: "+1 This is awesome")
issue.notes << create(:note, note: "+1 I want this") merge_request.notes << create(:note, note: "+1 I want this")
issue.notes << create(:note, note: "-1 This is bad") merge_request.notes << create(:note, note: "-1 This is bad")
issue.notes << create(:note, note: "+1 me too") merge_request.notes << create(:note, note: "+1 me too")
issue.upvotes_in_percent.should == 75 merge_request.upvotes_in_percent.should == 75
end end
end end
describe "#downvotes_in_percent" do describe "#downvotes_in_percent" do
it "with no notes has a 0% score" do it "with no notes has a 0% score" do
issue.downvotes_in_percent.should == 0 merge_request.downvotes_in_percent.should == 0
end end
it "should count a single -1 note as 100%" do it "should count a single -1 note as 100%" do
issue.notes << create(:note, note: "-1 This is bad") merge_request.notes << create(:note, note: "-1 This is bad")
issue.downvotes_in_percent.should == 100 merge_request.downvotes_in_percent.should == 100
end end
it "should count multiple -1 notes as 100%" do it "should count multiple -1 notes as 100%" do
issue.notes << create(:note, note: "-1 This is bad") merge_request.notes << create(:note, note: "-1 This is bad")
issue.notes << create(:note, note: "-1 Away with this") merge_request.notes << create(:note, note: "-1 Away with this")
issue.downvotes_in_percent.should == 100 merge_request.downvotes_in_percent.should == 100
end end
it "should count fractions for multiple +1 and -1 notes correctly" do it "should count fractions for multiple +1 and -1 notes correctly" do
issue.notes << create(:note, note: "+1 This is awesome") merge_request.notes << create(:note, note: "+1 This is awesome")
issue.notes << create(:note, note: "+1 I want this") merge_request.notes << create(:note, note: "+1 I want this")
issue.notes << create(:note, note: "-1 This is bad") merge_request.notes << create(:note, note: "-1 This is bad")
issue.notes << create(:note, note: "+1 me too") merge_request.notes << create(:note, note: "+1 me too")
issue.downvotes_in_percent.should == 25 merge_request.downvotes_in_percent.should == 25
end end
end end
end end
...@@ -38,34 +38,34 @@ describe Note do ...@@ -38,34 +38,34 @@ describe Note do
let(:project) { create(:project) } let(:project) { create(:project) }
it "recognizes a neutral note" do it "recognizes a neutral note" do
note = create(:note, note: "This is not a +1 note") note = create(:votable_note, note: "This is not a +1 note")
note.should_not be_upvote note.should_not be_upvote
note.should_not be_downvote note.should_not be_downvote
end end
it "recognizes a neutral emoji note" do it "recognizes a neutral emoji note" do
note = build(:note, note: "I would :+1: this, but I don't want to") note = build(:votable_note, note: "I would :+1: this, but I don't want to")
note.should_not be_upvote note.should_not be_upvote
note.should_not be_downvote note.should_not be_downvote
end end
it "recognizes a +1 note" do it "recognizes a +1 note" do
note = create(:note, note: "+1 for this") note = create(:votable_note, note: "+1 for this")
note.should be_upvote note.should be_upvote
end end
it "recognizes a +1 emoji as a vote" do it "recognizes a +1 emoji as a vote" do
note = build(:note, note: ":+1: for this") note = build(:votable_note, note: ":+1: for this")
note.should be_upvote note.should be_upvote
end end
it "recognizes a -1 note" do it "recognizes a -1 note" do
note = create(:note, note: "-1 for this") note = create(:votable_note, note: "-1 for this")
note.should be_downvote note.should be_downvote
end end
it "recognizes a -1 emoji as a vote" do it "recognizes a -1 emoji as a vote" do
note = build(:note, note: ":-1: for this") note = build(:votable_note, note: ":-1: for this")
note.should be_downvote note.should be_downvote
end end
end end
...@@ -74,43 +74,72 @@ describe Note do ...@@ -74,43 +74,72 @@ describe Note do
let(:commit) { project.repository.commit } let(:commit) { project.repository.commit }
describe "Commit notes" do describe "Commit notes" do
before do let!(:note) { create(:note_on_commit, note: "+1 from me") }
@note = create(:note, let!(:commit) { note.noteable }
commit_id: commit.id,
noteable_type: "Commit")
end
it "should be accessible through #noteable" do it "should be accessible through #noteable" do
@note.commit_id.should == commit.id note.commit_id.should == commit.id
@note.noteable.should be_a(Commit) note.noteable.should be_a(Commit)
@note.noteable.should == commit note.noteable.should == commit
end end
it "should save a valid note" do it "should save a valid note" do
@note.commit_id.should == commit.id note.commit_id.should == commit.id
@note.noteable == commit note.noteable == commit
end end
it "should be recognized by #for_commit?" do it "should be recognized by #for_commit?" do
@note.should be_for_commit note.should be_for_commit
end end
end
describe "Pre-line commit notes" do it "should not be votable" do
before do note.should_not be_votable
@note = create(:note,
commit_id: commit.id,
noteable_type: "Commit",
line_code: "0_16_1")
end end
end
describe "Commit diff line notes" do
let!(:note) { create(:note_on_commit_line, note: "+1 from me") }
let!(:commit) { note.noteable }
it "should save a valid note" do it "should save a valid note" do
@note.commit_id.should == commit.id note.commit_id.should == commit.id
@note.noteable.id.should == commit.id note.noteable.id.should == commit.id
end end
it "should be recognized by #for_diff_line?" do it "should be recognized by #for_diff_line?" do
@note.should be_for_diff_line note.should be_for_diff_line
end
it "should be recognized by #for_commit_diff_line?" do
note.should be_for_commit_diff_line
end
it "should not be votable" do
note.should_not be_votable
end
end
describe "Issue notes" do
let!(:note) { create(:note_on_issue, note: "+1 from me") }
it "should not be votable" do
note.should be_votable
end
end
describe "Merge request notes" do
let!(:note) { create(:note_on_merge_request, note: "+1 from me") }
it "should not be votable" do
note.should be_votable
end
end
describe "Merge request diff line notes" do
let!(:note) { create(:note_on_merge_request_line, note: "+1 from me") }
it "should not be votable" do
note.should_not be_votable
end end
end end
......
require 'spec_helper'
describe "On a merge request", js: true do
let!(:project) { create(:project) }
let!(:merge_request) { create(:merge_request, project: project) }
before do
login_as :user
project.team << [@user, :master]
visit project_merge_request_path(project, merge_request)
end
subject { page }
describe "the note form" do
# main target form creation
it { should have_css(".js-main-target-form", visible: true, count: 1) }
# button initalization
it { within(".js-main-target-form") { should have_button("Add Comment") } }
it { within(".js-main-target-form") { should_not have_link("Cancel") } }
# notifiactions
it { within(".js-main-target-form") { should have_checked_field("Project team") } }
it { within(".js-main-target-form") { should_not have_checked_field("Commit author") } }
it { within(".js-main-target-form") { should_not have_unchecked_field("Commit author") } }
describe "without text" do
it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } }
end
describe "with text" do
before do
within(".js-main-target-form") do
fill_in "note[note]", with: "This is awesome"
end
end
it { within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } }
it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } }
end
describe "with preview" do
before do
within(".js-main-target-form") do
fill_in "note[note]", with: "This is awesome"
find(".js-note-preview-button").trigger("click")
end
end
it { within(".js-main-target-form") { should have_css(".js-note-preview", text: "This is awesome", visible: true) } }
it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } }
it { within(".js-main-target-form") { should have_css(".js-note-edit-button", visible: true) } }
end
end
describe "when posting a note" do
before do
within(".js-main-target-form") do
fill_in "note[note]", with: "This is awsome!"
find(".js-note-preview-button").trigger("click")
click_button "Add Comment"
end
end
# note added
it { within(".js-main-target-form") { should have_content("This is awsome!") } }
# reset form
it { within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } }
# return from preview
it { within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } }
it { within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } }
it "should be removable" do
find(".js-note-delete").trigger("click")
should_not have_css(".note")
end
end
end
describe "On a merge request diff", js: true, focus: true do
let!(:project) { create(:project) }
let!(:merge_request) { create(:merge_request_with_diffs, project: project) }
before do
login_as :user
project.team << [@user, :master]
visit diffs_project_merge_request_path(project, merge_request)
click_link("Diff")
end
subject { page }
describe "when adding a note" do
before do
find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click")
end
describe "the notes holder" do
it { should have_css("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder") }
it { within(".js-temp-notes-holder") { should have_css(".new_note") } }
end
describe "the note form" do
# set up hidden fields correctly
it { within(".js-temp-notes-holder") { find("#note_noteable_type").value.should == "MergeRequest" } }
it { within(".js-temp-notes-holder") { find("#note_noteable_id").value.should == "" } }
it { within(".js-temp-notes-holder") { find("#note_commit_id").value.should == "bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a" } }
it { within(".js-temp-notes-holder") { find("#note_line_code").value.should == "4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185" } }
# buttons
it { should have_button("Add Comment") }
it { should have_css(".js-close-discussion-note-form", text: "Cancel") }
# notification options
it { should have_unchecked_field("Project team") }
it { should have_checked_field("Commit author") }
it "shouldn't add a second form for same row" do
find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click")
should have_css("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder form", count: 1)
end
it "should be removed when canceled" do
find(".js-close-discussion-note-form").trigger("click")
should have_no_css(".js-temp-notes-holder")
end
end
end
describe "with muliple note forms" do
before do
find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click")
find("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder .js-add-diff-note-button").trigger("click")
end
# has two line forms
it { should have_css(".js-temp-notes-holder", count: 2) }
describe "previewing them separately" do
before do
# add two separate texts and trigger previews on both
within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder") do
fill_in "note[note]", with: "One comment on line 185"
find(".js-note-preview-button").trigger("click")
end
within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") do
fill_in "note[note]", with: "Another comment on line 17"
find(".js-note-preview-button").trigger("click")
end
end
# check if previews were rendered separately
it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder") { should have_css(".js-note-preview", text: "One comment on line 185") } }
it { within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") { should have_css(".js-note-preview", text: "Another comment on line 17") } }
end
describe "posting a note" do
before do
within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") do
fill_in "note[note]", with: "Another comment on line 17"
click_button("Add Comment")
end
end
# removed form after submit
it { should have_no_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") }
# added discussion
it { should have_content("Another comment on line 17") }
it { should have_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .notes_holder") }
it { should have_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .notes_holder .note", count: 1) }
it { should have_link("Reply") }
it "should remove last note of a discussion" do
within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .notes_holder") do
find(".js-note-delete").trigger("click")
end
# removed whole discussion
should_not have_css(".note_holder")
should have_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + #342e16cbbd482ac2047dc679b2749d248cc1428f_18_18.line_holder")
end
end
end
describe "when replying to a note" do
before do
# create first note
find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder .js-add-diff-note-button").trigger("click")
within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .js-temp-notes-holder") do
fill_in "note[note]", with: "One comment on line 184"
click_button("Add Comment")
end
# create second note
within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") do
find(".js-discussion-reply-button").trigger("click")
fill_in "note[note]", with: "An additional comment in reply"
click_button("Add Comment")
end
end
# inserted note
it { should have_content("An additional comment in reply") }
it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") { should have_css(".note", count: 2) } }
# removed form after reply
it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") { should have_no_css("form") } }
it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") { should have_link("Reply") } }
end
end
describe "On merge request discussion", js: true do
describe "with merge request diff note"
describe "with commit note"
describe "with commit diff note"
end
require 'spec_helper'
describe "On the project wall", js: true do
let!(:project) { create(:project) }
let!(:commit) { project.commit("bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a") }
before do
login_as :user
project.team << [@user, :master]
visit wall_project_path(project)
end
subject { page }
describe "the note form" do
# main target form creation
it { should have_css(".js-main-target-form", visible: true, count: 1) }
# button initalization
it { within(".js-main-target-form") { should have_button("Add Comment") } }
it { within(".js-main-target-form") { should_not have_link("Cancel") } }
# notifiactions
it { within(".js-main-target-form") { should have_checked_field("Project team") } }
it { within(".js-main-target-form") { should_not have_checked_field("Commit author") } }
it { within(".js-main-target-form") { should_not have_unchecked_field("Commit author") } }
describe "without text" do
it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } }
end
describe "with text" do
before do
within(".js-main-target-form") do
fill_in "note[note]", with: "This is awesome"
end
end
it { within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } }
it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } }
end
describe "with preview" do
before do
within(".js-main-target-form") do
fill_in "note[note]", with: "This is awesome"
find(".js-note-preview-button").trigger("click")
end
end
it { within(".js-main-target-form") { should have_css(".js-note-preview", text: "This is awesome", visible: true) } }
it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } }
it { within(".js-main-target-form") { should have_css(".js-note-edit-button", visible: true) } }
end
end
describe "when posting a note" do
before do
within(".js-main-target-form") do
fill_in "note[note]", with: "This is awsome!"
find(".js-note-preview-button").trigger("click")
click_button "Add Comment"
end
end
# note added
it { within(".js-main-target-form") { should have_content("This is awsome!") } }
# reset form
it { within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } }
# return from preview
it { within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } }
it { within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } }
it "should be removable" do
find(".js-note-delete").trigger("click")
should_not have_css(".note")
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