Merge branch '30458-real-time-note-edits' into 'master'
Add real-time note edits 🐿️ Closes #30458 See merge request !10837
This commit is contained in:
commit
df1c933e99
|
@ -34,9 +34,9 @@ GLForm.prototype.setupForm = function() {
|
|||
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
|
||||
new DropzoneInput(this.form);
|
||||
autosize(this.textarea);
|
||||
// form and textarea event listeners
|
||||
this.addEventListeners();
|
||||
}
|
||||
// form and textarea event listeners
|
||||
this.addEventListeners();
|
||||
gl.text.init(this.form);
|
||||
// hide discard button
|
||||
this.form.find('.js-note-discard').hide();
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
/* global ResolveService */
|
||||
/* global mrRefreshWidgetUrl */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Cookies from 'js-cookie';
|
||||
import CommentTypeToggle from './comment_type_toggle';
|
||||
|
||||
|
@ -16,6 +17,10 @@ require('vendor/jquery.caret'); // required by jquery.atwho
|
|||
require('vendor/jquery.atwho');
|
||||
require('./task_list');
|
||||
|
||||
const normalizeNewlines = function(str) {
|
||||
return str.replace(/\r\n/g, '\n');
|
||||
};
|
||||
|
||||
(function() {
|
||||
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
|
||||
|
||||
|
@ -42,13 +47,17 @@ require('./task_list');
|
|||
this.refresh = bind(this.refresh, this);
|
||||
this.keydownNoteText = bind(this.keydownNoteText, this);
|
||||
this.toggleCommitList = bind(this.toggleCommitList, this);
|
||||
|
||||
this.notes_url = notes_url;
|
||||
this.note_ids = note_ids;
|
||||
// Used to keep track of updated notes while people are editing things
|
||||
this.updatedNotesTrackingMap = {};
|
||||
this.last_fetched_at = last_fetched_at;
|
||||
this.noteable_url = document.URL;
|
||||
this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
|
||||
this.basePollingInterval = 15000;
|
||||
this.maxPollingSteps = 4;
|
||||
|
||||
this.cleanBinding();
|
||||
this.addBinding();
|
||||
this.setPollingInterval();
|
||||
|
@ -128,7 +137,7 @@ require('./task_list');
|
|||
$(document).off("click", ".js-discussion-reply-button");
|
||||
$(document).off("click", ".js-add-diff-note-button");
|
||||
$(document).off("visibilitychange");
|
||||
$(document).off("keyup", ".js-note-text");
|
||||
$(document).off("keyup input", ".js-note-text");
|
||||
$(document).off("click", ".js-note-target-reopen");
|
||||
$(document).off("click", ".js-note-target-close");
|
||||
$(document).off("click", ".js-note-discard");
|
||||
|
@ -267,20 +276,20 @@ require('./task_list');
|
|||
return this.initRefresh();
|
||||
};
|
||||
|
||||
Notes.prototype.handleCreateChanges = function(note) {
|
||||
Notes.prototype.handleCreateChanges = function(noteEntity) {
|
||||
var votesBlock;
|
||||
if (typeof note === 'undefined') {
|
||||
if (typeof noteEntity === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.commands_changes) {
|
||||
if ('merge' in note.commands_changes) {
|
||||
if (noteEntity.commands_changes) {
|
||||
if ('merge' in noteEntity.commands_changes) {
|
||||
$.get(mrRefreshWidgetUrl);
|
||||
}
|
||||
|
||||
if ('emoji_award' in note.commands_changes) {
|
||||
if ('emoji_award' in noteEntity.commands_changes) {
|
||||
votesBlock = $('.js-awards-block').eq(0);
|
||||
gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.commands_changes.emoji_award);
|
||||
gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
|
||||
return gl.awardsHandler.scrollToAwards();
|
||||
}
|
||||
}
|
||||
|
@ -292,41 +301,76 @@ require('./task_list');
|
|||
Note: for rendering inline notes use renderDiscussionNote
|
||||
*/
|
||||
|
||||
Notes.prototype.renderNote = function(note, $form) {
|
||||
var $notesList;
|
||||
if (note.discussion_html != null) {
|
||||
return this.renderDiscussionNote(note, $form);
|
||||
Notes.prototype.renderNote = function(noteEntity, $form, $notesList = $('.main-notes-list')) {
|
||||
if (noteEntity.discussion_html != null) {
|
||||
return this.renderDiscussionNote(noteEntity, $form);
|
||||
}
|
||||
|
||||
if (!note.valid) {
|
||||
if (note.errors.commands_only) {
|
||||
new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
|
||||
if (!noteEntity.valid) {
|
||||
if (noteEntity.errors.commands_only) {
|
||||
new Flash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
|
||||
this.refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isNewNote(note)) {
|
||||
this.note_ids.push(note.id);
|
||||
const $note = $notesList.find(`#note_${noteEntity.id}`);
|
||||
if (this.isNewNote(noteEntity)) {
|
||||
this.note_ids.push(noteEntity.id);
|
||||
|
||||
$notesList = window.$('ul.main-notes-list');
|
||||
Notes.animateAppendNote(note.html, $notesList);
|
||||
const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList);
|
||||
|
||||
// Update datetime format on the recent note
|
||||
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
|
||||
gl.utils.localTimeAgo($newNote.find('.js-timeago'), false);
|
||||
this.collapseLongCommitList();
|
||||
this.taskList.init();
|
||||
this.refresh();
|
||||
return this.updateNotesCount(1);
|
||||
}
|
||||
// The server can send the same update multiple times so we need to make sure to only update once per actual update.
|
||||
else if (this.isUpdatedNote(noteEntity, $note)) {
|
||||
const isEditing = $note.hasClass('is-editing');
|
||||
const initialContent = normalizeNewlines(
|
||||
$note.find('.original-note-content').text().trim()
|
||||
);
|
||||
const $textarea = $note.find('.js-note-text');
|
||||
const currentContent = $textarea.val();
|
||||
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
|
||||
const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
|
||||
const isTextareaUntouched = currentContent === initialContent || currentContent === sanitizedNoteNote;
|
||||
|
||||
if (isEditing && isTextareaUntouched) {
|
||||
$textarea.val(noteEntity.note);
|
||||
this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
|
||||
}
|
||||
else if (isEditing && !isTextareaUntouched) {
|
||||
this.putConflictEditWarningInPlace(noteEntity, $note);
|
||||
this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
|
||||
}
|
||||
else {
|
||||
const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note);
|
||||
|
||||
// Update datetime format on the recent note
|
||||
gl.utils.localTimeAgo($updatedNote.find('.js-timeago'), false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Check if note does not exists on page
|
||||
*/
|
||||
|
||||
Notes.prototype.isNewNote = function(note) {
|
||||
return $.inArray(note.id, this.note_ids) === -1;
|
||||
Notes.prototype.isNewNote = function(noteEntity) {
|
||||
return $.inArray(noteEntity.id, this.note_ids) === -1;
|
||||
};
|
||||
|
||||
Notes.prototype.isUpdatedNote = function(noteEntity, $note) {
|
||||
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
|
||||
const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
|
||||
const currentNoteText = normalizeNewlines(
|
||||
$note.find('.original-note-content').text().trim()
|
||||
);
|
||||
return sanitizedNoteNote !== currentNoteText;
|
||||
};
|
||||
|
||||
Notes.prototype.isParallelView = function() {
|
||||
|
@ -339,31 +383,31 @@ require('./task_list');
|
|||
Note: for rendering inline notes use renderDiscussionNote
|
||||
*/
|
||||
|
||||
Notes.prototype.renderDiscussionNote = function(note, $form) {
|
||||
Notes.prototype.renderDiscussionNote = function(noteEntity, $form) {
|
||||
var discussionContainer, form, row, lineType, diffAvatarContainer;
|
||||
if (!this.isNewNote(note)) {
|
||||
if (!this.isNewNote(noteEntity)) {
|
||||
return;
|
||||
}
|
||||
this.note_ids.push(note.id);
|
||||
form = $form || $(".js-discussion-note-form[data-discussion-id='" + note.discussion_id + "']");
|
||||
this.note_ids.push(noteEntity.id);
|
||||
form = $form || $(".js-discussion-note-form[data-discussion-id='" + noteEntity.discussion_id + "']");
|
||||
row = form.closest("tr");
|
||||
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
|
||||
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
|
||||
// is this the first note of discussion?
|
||||
discussionContainer = window.$(`.notes[data-discussion-id="${note.discussion_id}"]`);
|
||||
discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
|
||||
if (!discussionContainer.length) {
|
||||
discussionContainer = form.closest('.discussion').find('.notes');
|
||||
}
|
||||
if (discussionContainer.length === 0) {
|
||||
if (note.diff_discussion_html) {
|
||||
var $discussion = $(note.diff_discussion_html).renderGFM();
|
||||
if (noteEntity.diff_discussion_html) {
|
||||
var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
|
||||
|
||||
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
|
||||
// insert the note and the reply button after the temp row
|
||||
row.after($discussion);
|
||||
} else {
|
||||
// Merge new discussion HTML in
|
||||
var $notes = $discussion.find('.notes[data-discussion-id="' + note.discussion_id + '"]');
|
||||
var $notes = $discussion.find('.notes[data-discussion-id="' + noteEntity.discussion_id + '"]');
|
||||
var contentContainerClass = '.' + $notes.closest('.notes_content')
|
||||
.attr('class')
|
||||
.split(' ')
|
||||
|
@ -373,17 +417,18 @@ require('./task_list');
|
|||
}
|
||||
}
|
||||
// Init discussion on 'Discussion' page if it is merge request page
|
||||
if (window.$('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
|
||||
Notes.animateAppendNote(note.discussion_html, window.$('ul.main-notes-list'));
|
||||
const page = $('body').attr('data-page');
|
||||
if ((page && page.indexOf('projects:merge_request') === 0) || !noteEntity.diff_discussion_html) {
|
||||
Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
|
||||
}
|
||||
} else {
|
||||
// append new note to all matching discussions
|
||||
Notes.animateAppendNote(note.html, discussionContainer);
|
||||
Notes.animateAppendNote(noteEntity.html, discussionContainer);
|
||||
}
|
||||
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) {
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
|
||||
gl.diffNotesCompileComponents();
|
||||
this.renderDiscussionAvatar(diffAvatarContainer, note);
|
||||
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
|
||||
}
|
||||
|
||||
gl.utils.localTimeAgo($('.js-timeago'), false);
|
||||
|
@ -397,13 +442,13 @@ require('./task_list');
|
|||
.get(0);
|
||||
};
|
||||
|
||||
Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, note) {
|
||||
Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, noteEntity) {
|
||||
var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
|
||||
var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
|
||||
|
||||
if (!avatarHolder.length) {
|
||||
avatarHolder = document.createElement('diff-note-avatars');
|
||||
avatarHolder.setAttribute('discussion-id', note.discussion_id);
|
||||
avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id);
|
||||
|
||||
diffAvatarContainer.append(avatarHolder);
|
||||
|
||||
|
@ -550,16 +595,16 @@ require('./task_list');
|
|||
Updates the current note field.
|
||||
*/
|
||||
|
||||
Notes.prototype.updateNote = function(_xhr, note, _status) {
|
||||
Notes.prototype.updateNote = function(_xhr, noteEntity, _status) {
|
||||
var $html, $note_li;
|
||||
// Convert returned HTML to a jQuery object so we can modify it further
|
||||
$html = $(note.html);
|
||||
$html = $(noteEntity.html);
|
||||
this.revertNoteEditForm();
|
||||
gl.utils.localTimeAgo($('.js-timeago', $html));
|
||||
$html.renderGFM();
|
||||
$html.find('.js-task-list-container').taskList('enable');
|
||||
// Find the note's `li` element by ID and replace it with the updated HTML
|
||||
$note_li = $('.note-row-' + note.id);
|
||||
$note_li = $('.note-row-' + noteEntity.id);
|
||||
|
||||
$note_li.replaceWith($html);
|
||||
|
||||
|
@ -570,7 +615,7 @@ require('./task_list');
|
|||
|
||||
Notes.prototype.checkContentToAllowEditing = function($el) {
|
||||
var initialContent = $el.find('.original-note-content').text().trim();
|
||||
var currentContent = $el.find('.note-textarea').val();
|
||||
var currentContent = $el.find('.js-note-text').val();
|
||||
var isAllowed = true;
|
||||
|
||||
if (currentContent === initialContent) {
|
||||
|
@ -584,7 +629,7 @@ require('./task_list');
|
|||
gl.utils.scrollToElement($el);
|
||||
}
|
||||
|
||||
$el.find('.js-edit-warning').show();
|
||||
$el.find('.js-finish-edit-warning').show();
|
||||
isAllowed = false;
|
||||
}
|
||||
|
||||
|
@ -603,7 +648,7 @@ require('./task_list');
|
|||
var $target = $(e.target);
|
||||
var $editForm = $(this.getEditFormSelector($target));
|
||||
var $note = $target.closest('.note');
|
||||
var $currentlyEditing = $('.note.is-editting:visible');
|
||||
var $currentlyEditing = $('.note.is-editing:visible');
|
||||
|
||||
if ($currentlyEditing.length) {
|
||||
var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);
|
||||
|
@ -615,7 +660,7 @@ require('./task_list');
|
|||
|
||||
$note.find('.js-note-attachment-delete').show();
|
||||
$editForm.addClass('current-note-edit-form');
|
||||
$note.addClass('is-editting');
|
||||
$note.addClass('is-editing');
|
||||
this.putEditFormInPlace($target);
|
||||
};
|
||||
|
||||
|
@ -627,21 +672,34 @@ require('./task_list');
|
|||
|
||||
Notes.prototype.cancelEdit = function(e) {
|
||||
e.preventDefault();
|
||||
var $target = $(e.target);
|
||||
var note = $target.closest('.note');
|
||||
note.find('.js-edit-warning').hide();
|
||||
const $target = $(e.target);
|
||||
const $note = $target.closest('.note');
|
||||
const noteId = $note.attr('data-note-id');
|
||||
|
||||
this.revertNoteEditForm($target);
|
||||
return this.removeNoteEditForm(note);
|
||||
|
||||
if (this.updatedNotesTrackingMap[noteId]) {
|
||||
const $newNote = $(this.updatedNotesTrackingMap[noteId].html);
|
||||
$note.replaceWith($newNote);
|
||||
this.updatedNotesTrackingMap[noteId] = null;
|
||||
|
||||
// Update datetime format on the recent note
|
||||
gl.utils.localTimeAgo($newNote.find('.js-timeago'), false);
|
||||
}
|
||||
else {
|
||||
$note.find('.js-finish-edit-warning').hide();
|
||||
this.removeNoteEditForm($note);
|
||||
}
|
||||
};
|
||||
|
||||
Notes.prototype.revertNoteEditForm = function($target) {
|
||||
$target = $target || $('.note.is-editting:visible');
|
||||
$target = $target || $('.note.is-editing:visible');
|
||||
var selector = this.getEditFormSelector($target);
|
||||
var $editForm = $(selector);
|
||||
|
||||
$editForm.insertBefore('.notes-form');
|
||||
$editForm.find('.js-comment-button').enable();
|
||||
$editForm.find('.js-edit-warning').hide();
|
||||
$editForm.find('.js-finish-edit-warning').hide();
|
||||
};
|
||||
|
||||
Notes.prototype.getEditFormSelector = function($el) {
|
||||
|
@ -654,11 +712,11 @@ require('./task_list');
|
|||
return selector;
|
||||
};
|
||||
|
||||
Notes.prototype.removeNoteEditForm = function(note) {
|
||||
var form = note.find('.current-note-edit-form');
|
||||
note.removeClass('is-editting');
|
||||
Notes.prototype.removeNoteEditForm = function($note) {
|
||||
var form = $note.find('.current-note-edit-form');
|
||||
$note.removeClass('is-editing');
|
||||
form.removeClass('current-note-edit-form');
|
||||
form.find('.js-edit-warning').hide();
|
||||
form.find('.js-finish-edit-warning').hide();
|
||||
// Replace markdown textarea text with original note text.
|
||||
return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
|
||||
};
|
||||
|
@ -683,9 +741,9 @@ require('./task_list');
|
|||
// to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
|
||||
// where $("#noteId") would return only one.
|
||||
return function(i, el) {
|
||||
var note, notes;
|
||||
note = $(el);
|
||||
notes = note.closest(".discussion-notes");
|
||||
var $note, $notes;
|
||||
$note = $(el);
|
||||
$notes = $note.closest(".discussion-notes");
|
||||
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
||||
if (gl.diffNoteApps[noteElId]) {
|
||||
|
@ -693,18 +751,18 @@ require('./task_list');
|
|||
}
|
||||
}
|
||||
|
||||
note.remove();
|
||||
$note.remove();
|
||||
|
||||
// check if this is the last note for this line
|
||||
if (notes.find(".note").length === 0) {
|
||||
var notesTr = notes.closest("tr");
|
||||
if ($notes.find(".note").length === 0) {
|
||||
var notesTr = $notes.closest("tr");
|
||||
|
||||
// "Discussions" tab
|
||||
notes.closest(".timeline-entry").remove();
|
||||
$notes.closest(".timeline-entry").remove();
|
||||
|
||||
// The notes tr can contain multiple lists of notes, like on the parallel diff
|
||||
if (notesTr.find('.discussion-notes').length > 1) {
|
||||
notes.remove();
|
||||
$notes.remove();
|
||||
} else {
|
||||
notesTr.remove();
|
||||
}
|
||||
|
@ -723,12 +781,11 @@ require('./task_list');
|
|||
*/
|
||||
|
||||
Notes.prototype.removeAttachment = function() {
|
||||
var note;
|
||||
note = $(this).closest(".note");
|
||||
note.find(".note-attachment").remove();
|
||||
note.find(".note-body > .note-text").show();
|
||||
note.find(".note-header").show();
|
||||
return note.find(".current-note-edit-form").remove();
|
||||
const $note = $(this).closest(".note");
|
||||
$note.find(".note-attachment").remove();
|
||||
$note.find(".note-body > .note-text").show();
|
||||
$note.find(".note-header").show();
|
||||
return $note.find(".current-note-edit-form").remove();
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -1004,6 +1061,19 @@ require('./task_list');
|
|||
$editForm.find('.referenced-users').hide();
|
||||
};
|
||||
|
||||
Notes.prototype.putConflictEditWarningInPlace = function(noteEntity, $note) {
|
||||
if ($note.find('.js-conflict-edit-warning').length === 0) {
|
||||
const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
|
||||
This comment has changed since you started editing, please review the
|
||||
<a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">
|
||||
updated comment
|
||||
</a>
|
||||
to ensure information is not lost
|
||||
</div>`);
|
||||
$alert.insertAfter($note.find('.note-text'));
|
||||
}
|
||||
};
|
||||
|
||||
Notes.prototype.updateNotesCount = function(updateCount) {
|
||||
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
|
||||
};
|
||||
|
@ -1064,11 +1134,20 @@ require('./task_list');
|
|||
return $form;
|
||||
};
|
||||
|
||||
Notes.animateAppendNote = function(noteHTML, $notesList) {
|
||||
const $note = window.$(noteHTML);
|
||||
Notes.animateAppendNote = function(noteHtml, $notesList) {
|
||||
const $note = $(noteHtml);
|
||||
|
||||
$note.addClass('fade-in').renderGFM();
|
||||
$notesList.append($note);
|
||||
return $note;
|
||||
};
|
||||
|
||||
Notes.animateUpdateNote = function(noteHtml, $note) {
|
||||
const $updatedNote = $(noteHtml);
|
||||
|
||||
$updatedNote.addClass('fade-in').renderGFM();
|
||||
$note.replaceWith($updatedNote);
|
||||
return $updatedNote;
|
||||
};
|
||||
|
||||
return Notes;
|
||||
|
|
|
@ -67,7 +67,7 @@ ul.notes {
|
|||
}
|
||||
}
|
||||
|
||||
&.is-editting {
|
||||
&.is-editing {
|
||||
.note-header,
|
||||
.note-text,
|
||||
.edited-text {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
= render 'projects/notes/hints'
|
||||
|
||||
.note-form-actions.clearfix
|
||||
.settings-message.note-edit-warning.js-edit-warning
|
||||
.settings-message.note-edit-warning.js-finish-edit-warning
|
||||
Finish editing this message first!
|
||||
= submit_tag 'Save comment', class: 'btn btn-nr btn-save js-comment-button'
|
||||
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
- return if note.cross_reference_not_visible_for?(current_user)
|
||||
|
||||
- note_editable = note_editable?(note)
|
||||
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
|
||||
%li.timeline-entry{ id: dom_id(note),
|
||||
class: ["note", "note-row-#{note.id}", ('system-note' if note.system)],
|
||||
data: { author_id: note.author.id,
|
||||
editable: note_editable,
|
||||
note_id: note.id } }
|
||||
.timeline-entry-inner
|
||||
.timeline-icon
|
||||
- if note.system
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Update note edits in real-time
|
||||
merge_request:
|
||||
author:
|
|
@ -62,6 +62,8 @@ describe "GitLab Flavored Markdown", feature: true do
|
|||
project: project,
|
||||
title: "fix #{@other_issue.to_reference}",
|
||||
description: "ask #{fred.to_reference} for details")
|
||||
|
||||
@note = create(:note_on_issue, noteable: @issue, project: @issue.project, note: "Hello world")
|
||||
end
|
||||
|
||||
it "renders subject in issues#index" do
|
||||
|
@ -81,14 +83,6 @@ describe "GitLab Flavored Markdown", feature: true do
|
|||
|
||||
expect(page).to have_link(fred.to_reference)
|
||||
end
|
||||
|
||||
it "renders updated subject once edited somewhere else in issues#show" do
|
||||
visit namespace_project_issue_path(project.namespace, project, @issue)
|
||||
@issue.update(title: "fix #{@other_issue.to_reference} and update")
|
||||
|
||||
wait_for_vue_resource
|
||||
expect(page).to have_text("fix #{@other_issue.to_reference} and update")
|
||||
end
|
||||
end
|
||||
|
||||
describe "for merge requests" do
|
||||
|
|
|
@ -4,14 +4,77 @@ feature 'Issue notes polling', :feature, :js do
|
|||
let(:project) { create(:empty_project, :public) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
|
||||
before do
|
||||
visit namespace_project_issue_path(project.namespace, project, issue)
|
||||
describe 'creates' do
|
||||
before do
|
||||
visit namespace_project_issue_path(project.namespace, project, issue)
|
||||
end
|
||||
|
||||
it 'displays the new comment' do
|
||||
note = create(:note, noteable: issue, project: project, note: 'Looks good!')
|
||||
page.execute_script('notes.refresh();')
|
||||
|
||||
expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!')
|
||||
end
|
||||
end
|
||||
|
||||
it 'should display the new comment' do
|
||||
note = create(:note, noteable: issue, project: project, note: 'Looks good!')
|
||||
page.execute_script('notes.refresh();')
|
||||
describe 'updates' do
|
||||
let(:user) { create(:user) }
|
||||
let(:note_text) { "Hello World" }
|
||||
let(:updated_text) { "Bye World" }
|
||||
let!(:existing_note) { create(:note, noteable: issue, project: project, author: user, note: note_text) }
|
||||
|
||||
expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!')
|
||||
before do
|
||||
login_as(user)
|
||||
visit namespace_project_issue_path(project.namespace, project, issue)
|
||||
end
|
||||
|
||||
it 'displays the updated content' do
|
||||
expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
|
||||
|
||||
update_note(existing_note, updated_text)
|
||||
|
||||
expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
|
||||
end
|
||||
|
||||
it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do
|
||||
find("#note_#{existing_note.id} .js-note-edit").click
|
||||
|
||||
expect(page).to have_field("note[note]", with: note_text)
|
||||
|
||||
update_note(existing_note, updated_text)
|
||||
|
||||
expect(page).to have_field("note[note]", with: updated_text)
|
||||
end
|
||||
|
||||
it 'when editing but you changed some things, and an update comes in, show a warning' do
|
||||
find("#note_#{existing_note.id} .js-note-edit").click
|
||||
|
||||
expect(page).to have_field("note[note]", with: note_text)
|
||||
|
||||
find("#note_#{existing_note.id} .js-note-text").set('something random')
|
||||
|
||||
update_note(existing_note, updated_text)
|
||||
|
||||
expect(page).to have_selector(".alert")
|
||||
end
|
||||
|
||||
it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do
|
||||
find("#note_#{existing_note.id} .js-note-edit").click
|
||||
|
||||
expect(page).to have_field("note[note]", with: note_text)
|
||||
|
||||
find("#note_#{existing_note.id} .js-note-text").set('something random')
|
||||
|
||||
update_note(existing_note, updated_text)
|
||||
|
||||
find("#note_#{existing_note.id} .note-edit-cancel").click
|
||||
|
||||
expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
|
||||
end
|
||||
end
|
||||
|
||||
def update_note(note, new_text)
|
||||
note.update(note: new_text)
|
||||
page.execute_script('notes.refresh();')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
|
||||
/* global Notes */
|
||||
|
||||
require('~/notes');
|
||||
require('vendor/autosize');
|
||||
require('~/gl_form');
|
||||
require('~/lib/utils/text_utility');
|
||||
import 'vendor/autosize';
|
||||
import '~/gl_form';
|
||||
import '~/lib/utils/text_utility';
|
||||
import '~/render_gfm';
|
||||
import '~/render_math';
|
||||
import '~/notes';
|
||||
|
||||
(function() {
|
||||
window.gon || (window.gon = {});
|
||||
|
@ -80,35 +82,78 @@ require('~/lib/utils/text_utility');
|
|||
|
||||
beforeEach(() => {
|
||||
note = {
|
||||
id: 1,
|
||||
discussion_html: null,
|
||||
valid: true,
|
||||
html: '<div></div>',
|
||||
note: 'heya',
|
||||
html: '<div>heya</div>',
|
||||
};
|
||||
$notesList = jasmine.createSpyObj('$notesList', ['find']);
|
||||
$notesList = jasmine.createSpyObj('$notesList', [
|
||||
'find',
|
||||
'append',
|
||||
]);
|
||||
|
||||
notes = jasmine.createSpyObj('notes', [
|
||||
'refresh',
|
||||
'isNewNote',
|
||||
'isUpdatedNote',
|
||||
'collapseLongCommitList',
|
||||
'updateNotesCount',
|
||||
'putConflictEditWarningInPlace'
|
||||
]);
|
||||
notes.taskList = jasmine.createSpyObj('tasklist', ['init']);
|
||||
notes.note_ids = [];
|
||||
notes.updatedNotesTrackingMap = {};
|
||||
|
||||
spyOn(window, '$').and.returnValue($notesList);
|
||||
spyOn(gl.utils, 'localTimeAgo');
|
||||
spyOn(Notes, 'animateAppendNote');
|
||||
notes.isNewNote.and.returnValue(true);
|
||||
|
||||
Notes.prototype.renderNote.call(notes, note);
|
||||
spyOn(Notes, 'animateAppendNote').and.callThrough();
|
||||
spyOn(Notes, 'animateUpdateNote').and.callThrough();
|
||||
});
|
||||
|
||||
it('should query for the notes list', () => {
|
||||
expect(window.$).toHaveBeenCalledWith('ul.main-notes-list');
|
||||
describe('when adding note', () => {
|
||||
it('should call .animateAppendNote', () => {
|
||||
notes.isNewNote.and.returnValue(true);
|
||||
Notes.prototype.renderNote.call(notes, note, null, $notesList);
|
||||
|
||||
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call .animateAppendNote', () => {
|
||||
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
|
||||
describe('when note was edited', () => {
|
||||
it('should call .animateUpdateNote', () => {
|
||||
notes.isUpdatedNote.and.returnValue(true);
|
||||
const $note = $('<div>');
|
||||
$notesList.find.and.returnValue($note);
|
||||
Notes.prototype.renderNote.call(notes, note, null, $notesList);
|
||||
|
||||
expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note);
|
||||
});
|
||||
|
||||
describe('while editing', () => {
|
||||
it('should update textarea if nothing has been touched', () => {
|
||||
notes.isUpdatedNote.and.returnValue(true);
|
||||
const $note = $(`<div class="is-editing">
|
||||
<div class="original-note-content">initial</div>
|
||||
<textarea class="js-note-text">initial</textarea>
|
||||
</div>`);
|
||||
$notesList.find.and.returnValue($note);
|
||||
Notes.prototype.renderNote.call(notes, note, null, $notesList);
|
||||
|
||||
expect($note.find('.js-note-text').val()).toEqual(note.note);
|
||||
});
|
||||
|
||||
it('should call .putConflictEditWarningInPlace', () => {
|
||||
notes.isUpdatedNote.and.returnValue(true);
|
||||
const $note = $(`<div class="is-editing">
|
||||
<div class="original-note-content">initial</div>
|
||||
<textarea class="js-note-text">different</textarea>
|
||||
</div>`);
|
||||
$notesList.find.and.returnValue($note);
|
||||
Notes.prototype.renderNote.call(notes, note, null, $notesList);
|
||||
|
||||
expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -147,14 +192,12 @@ require('~/lib/utils/text_utility');
|
|||
});
|
||||
|
||||
describe('Discussion root note', () => {
|
||||
let $notesList;
|
||||
let body;
|
||||
|
||||
beforeEach(() => {
|
||||
body = jasmine.createSpyObj('body', ['attr']);
|
||||
discussionContainer = { length: 0 };
|
||||
|
||||
spyOn(window, '$').and.returnValues(discussionContainer, body, $notesList);
|
||||
$form.closest.and.returnValues(row, $form);
|
||||
$form.find.and.returnValues(discussionContainer);
|
||||
body.attr.and.returnValue('');
|
||||
|
@ -162,12 +205,8 @@ require('~/lib/utils/text_utility');
|
|||
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
|
||||
});
|
||||
|
||||
it('should query for the notes list', () => {
|
||||
expect(window.$.calls.argsFor(2)).toEqual(['ul.main-notes-list']);
|
||||
});
|
||||
|
||||
it('should call Notes.animateAppendNote', () => {
|
||||
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $notesList);
|
||||
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $('.main-notes-list'));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -175,16 +214,12 @@ require('~/lib/utils/text_utility');
|
|||
beforeEach(() => {
|
||||
discussionContainer = { length: 1 };
|
||||
|
||||
spyOn(window, '$').and.returnValues(discussionContainer);
|
||||
$form.closest.and.returnValues(row);
|
||||
$form.closest.and.returnValues(row, $form);
|
||||
$form.find.and.returnValues(discussionContainer);
|
||||
|
||||
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
|
||||
});
|
||||
|
||||
it('should query foor the discussion container', () => {
|
||||
expect(window.$).toHaveBeenCalledWith(`.notes[data-discussion-id="${note.discussion_id}"]`);
|
||||
});
|
||||
|
||||
it('should call Notes.animateAppendNote', () => {
|
||||
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer);
|
||||
});
|
||||
|
@ -193,35 +228,45 @@ require('~/lib/utils/text_utility');
|
|||
|
||||
describe('animateAppendNote', () => {
|
||||
let noteHTML;
|
||||
let $note;
|
||||
let $notesList;
|
||||
let $resultantNote;
|
||||
|
||||
beforeEach(() => {
|
||||
noteHTML = '<div></div>';
|
||||
$note = jasmine.createSpyObj('$note', ['addClass', 'renderGFM', 'removeClass']);
|
||||
$notesList = jasmine.createSpyObj('$notesList', ['append']);
|
||||
|
||||
spyOn(window, '$').and.returnValue($note);
|
||||
spyOn(window, 'setTimeout').and.callThrough();
|
||||
$note.addClass.and.returnValue($note);
|
||||
$note.renderGFM.and.returnValue($note);
|
||||
|
||||
Notes.animateAppendNote(noteHTML, $notesList);
|
||||
$resultantNote = Notes.animateAppendNote(noteHTML, $notesList);
|
||||
});
|
||||
|
||||
it('should init the note jquery object', () => {
|
||||
expect(window.$).toHaveBeenCalledWith(noteHTML);
|
||||
});
|
||||
|
||||
it('should call addClass', () => {
|
||||
expect($note.addClass).toHaveBeenCalledWith('fade-in');
|
||||
});
|
||||
it('should call renderGFM', () => {
|
||||
expect($note.renderGFM).toHaveBeenCalledWith();
|
||||
it('should have `fade-in` class', () => {
|
||||
expect($resultantNote.hasClass('fade-in')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should append note to the notes list', () => {
|
||||
expect($notesList.append).toHaveBeenCalledWith($note);
|
||||
expect($notesList.append).toHaveBeenCalledWith($resultantNote);
|
||||
});
|
||||
});
|
||||
|
||||
describe('animateUpdateNote', () => {
|
||||
let noteHTML;
|
||||
let $note;
|
||||
let $updatedNote;
|
||||
|
||||
beforeEach(() => {
|
||||
noteHTML = '<div></div>';
|
||||
$note = jasmine.createSpyObj('$note', [
|
||||
'replaceWith'
|
||||
]);
|
||||
|
||||
$updatedNote = Notes.animateUpdateNote(noteHTML, $note);
|
||||
});
|
||||
|
||||
it('should have `fade-in` class', () => {
|
||||
expect($updatedNote.hasClass('fade-in')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should call replaceWith on $note', () => {
|
||||
expect($note.replaceWith).toHaveBeenCalledWith($updatedNote);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue