Skip to content
Snippets Groups Projects
Commit 645593e5 authored by Kushal Pandya's avatar Kushal Pandya Committed by Filipa Lacerda
Browse files

Add instant comments support

parent a5347fe5
No related branches found
No related tags found
No related merge requests found
Showing
with 607 additions and 65 deletions
Loading
Loading
@@ -43,8 +43,8 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
const $submitButton = $form.find('input[type=submit], button[type=submit]');
 
if (!$submitButton.attr('disabled')) {
$submitButton.trigger('click', [e]);
$submitButton.disable();
$form.submit();
}
});
 
Loading
Loading
Loading
Loading
@@ -35,6 +35,14 @@
});
};
 
w.gl.utils.ajaxPost = function(url, data) {
return $.ajax({
type: 'POST',
url: url,
data: data,
});
};
w.gl.utils.extractLast = function(term) {
return this.split(term).pop();
};
Loading
Loading
This diff is collapsed.
Loading
Loading
@@ -159,3 +159,31 @@ a {
.fade-in {
animation: fadeIn $fade-in-duration 1;
}
@keyframes fadeInHalf {
0% {
opacity: 0;
}
100% {
opacity: 0.5;
}
}
.fade-in-half {
animation: fadeInHalf $fade-in-duration 1;
}
@keyframes fadeInFull {
0% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.fade-in-full {
animation: fadeInFull $fade-in-duration 1;
}
Loading
Loading
@@ -57,6 +57,25 @@ ul.notes {
position: relative;
border-bottom: 1px solid $white-normal;
 
&.being-posted {
pointer-events: none;
opacity: 0.5;
.dummy-avatar {
display: inline-block;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: $kdb-border;
border: 1px solid darken($kdb-border, 25%);
}
.note-headline-light,
.fa-spinner {
margin-left: 3px;
}
}
&.note-discussion {
&.timeline-entry {
padding: 14px 10px;
Loading
Loading
@@ -687,6 +706,10 @@ ul.notes {
}
}
 
.discussion-notes .flash-container {
margin-bottom: 0;
}
// Merge request notes in diffs
.diff-file {
// Diff is side by side
Loading
Loading
.discussion-notes
%ul.notes{ data: { discussion_id: discussion.id } }
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
.flash-container
 
- if current_user
.discussion-reply-holder
Loading
Loading
Loading
Loading
@@ -9,6 +9,6 @@
.note-form-actions.clearfix
.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'
= submit_tag 'Save comment', class: 'btn btn-nr btn-save js-comment-save-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
Cancel
---
title: Add support for instantly updating comments
merge_request: 10760
author:
Loading
Loading
@@ -458,6 +458,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
click_button "Comment"
end
 
wait_for_ajax
page.within ".files>div:nth-child(2) .note-body > .note-text" do
expect(page).to have_content "Line is correct"
end
Loading
Loading
@@ -470,6 +472,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
fill_in "note_note", with: "Line is wrong on here"
click_button "Comment"
end
wait_for_ajax
end
 
step 'I should still see a comment like "Line is correct" in the second file' do
Loading
Loading
@@ -574,6 +578,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
fill_in "note_note", with: message
click_button "Comment"
end
wait_for_ajax
page.within(".notes_holder", visible: true) do
expect(page).to have_content message
end
Loading
Loading
Loading
Loading
@@ -24,6 +24,8 @@ module SharedNote
fill_in "note[note]", with: "XML attached"
click_button "Comment"
end
wait_for_ajax
end
 
step 'I preview a comment text like "Bug fixed :smile:"' do
Loading
Loading
@@ -37,6 +39,8 @@ module SharedNote
page.within(".js-main-target-form") do
click_button "Comment"
end
wait_for_ajax
end
 
step 'I write a comment like ":+1: Nice"' do
Loading
Loading
Loading
Loading
@@ -98,6 +98,7 @@ describe 'Merge requests > User posts notes', :js do
find('.btn-save').click
end
 
wait_for_ajax
find('.note').hover
find('.js-note-edit').click
 
Loading
Loading
Loading
Loading
@@ -160,6 +160,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
it 'changes target branch from a note' do
write_note("message start \n/target_branch merge-test\n message end.")
 
wait_for_ajax
expect(page).not_to have_content('/target_branch')
expect(page).to have_content('message start')
expect(page).to have_content('message end.')
Loading
Loading
Loading
Loading
@@ -362,5 +362,16 @@ require('~/lib/utils/common_utils');
gl.utils.setCiStatusFavicon(BUILD_URL);
});
});
describe('gl.utils.ajaxPost', () => {
it('should perform `$.ajax` call and do `POST` request', () => {
const requestURL = '/some/random/api';
const data = { keyname: 'value' };
const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {});
gl.utils.ajaxPost(requestURL, data);
expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST');
});
});
});
})();
Loading
Loading
@@ -26,7 +26,7 @@ import '~/notes';
 
describe('task lists', function() {
beforeEach(function() {
$('form').on('submit', function(e) {
$('.js-comment-button').on('click', function(e) {
e.preventDefault();
});
this.notes = new Notes();
Loading
Loading
@@ -60,9 +60,12 @@ import '~/notes';
reset: function() {}
});
 
$('form').on('submit', function(e) {
$('.js-comment-button').on('click', (e) => {
const $form = $(this);
e.preventDefault();
$('.js-main-target-form').trigger('ajax:success');
this.notes.addNote($form);
this.notes.reenableTargetFormSubmitButton(e);
this.notes.resetMainTargetForm(e);
});
});
 
Loading
Loading
@@ -238,8 +241,8 @@ import '~/notes';
$resultantNote = Notes.animateAppendNote(noteHTML, $notesList);
});
 
it('should have `fade-in` class', () => {
expect($resultantNote.hasClass('fade-in')).toEqual(true);
it('should have `fade-in-full` class', () => {
expect($resultantNote.hasClass('fade-in-full')).toEqual(true);
});
 
it('should append note to the notes list', () => {
Loading
Loading
@@ -269,5 +272,221 @@ import '~/notes';
expect($note.replaceWith).toHaveBeenCalledWith($updatedNote);
});
});
describe('getFormData', () => {
it('should return form metadata object from form reference', () => {
this.notes = new Notes();
const $form = $('form');
const sampleComment = 'foobar';
$form.find('textarea.js-note-text').val(sampleComment);
const { formData, formContent, formAction } = this.notes.getFormData($form);
expect(formData.indexOf(sampleComment) > -1).toBe(true);
expect(formContent).toEqual(sampleComment);
expect(formAction).toEqual($form.attr('action'));
});
});
describe('hasSlashCommands', () => {
beforeEach(() => {
this.notes = new Notes();
});
it('should return true when comment has slash commands', () => {
const sampleComment = '/wip /milestone %1.0 /merge /unassign Merging this';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
expect(hasSlashCommands).toBeTruthy();
});
it('should return false when comment does NOT have any slash commands', () => {
const sampleComment = 'Looking good, Awesome!';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
expect(hasSlashCommands).toBeFalsy();
});
});
describe('stripSlashCommands', () => {
const REGEX_SLASH_COMMANDS = /\/\w+/g;
it('should strip slash commands from the comment', () => {
this.notes = new Notes();
const sampleComment = '/wip /milestone %1.0 /merge /unassign Merging this';
const stripedComment = this.notes.stripSlashCommands(sampleComment);
expect(REGEX_SLASH_COMMANDS.test(stripedComment)).toBeFalsy();
});
});
describe('createPlaceholderNote', () => {
const sampleComment = 'foobar';
const uniqueId = 'b1234-a4567';
const currentUsername = 'root';
const currentUserFullname = 'Administrator';
beforeEach(() => {
this.notes = new Notes();
});
it('should return constructed placeholder element for regular note based on form contents', () => {
const $tempNote = this.notes.createPlaceholderNote({
formContent: sampleComment,
uniqueId,
isDiscussionNote: false,
currentUsername,
currentUserFullname
});
const $tempNoteHeader = $tempNote.find('.note-header');
expect($tempNote.prop('nodeName')).toEqual('LI');
expect($tempNote.attr('id')).toEqual(uniqueId);
$tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() {
expect($(this).attr('href')).toEqual(`/${currentUsername}`);
});
expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname);
expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`);
expect($tempNote.find('.note-body .note-text').text().trim()).toEqual(sampleComment);
});
it('should return constructed placeholder element for discussion note based on form contents', () => {
const $tempNote = this.notes.createPlaceholderNote({
formContent: sampleComment,
uniqueId,
isDiscussionNote: true,
currentUsername,
currentUserFullname
});
expect($tempNote.prop('nodeName')).toEqual('LI');
expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
});
});
describe('postComment & updateComment', () => {
const sampleComment = 'foo';
const updatedComment = 'bar';
const note = {
id: 1234,
html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
<div class="note-text">${sampleComment}</div>
</li>`,
note: sampleComment,
valid: true
};
let $form;
let $notesContainer;
beforeEach(() => {
this.notes = new Notes();
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
$form = $('form');
$notesContainer = $('ul.main-notes-list');
$form.find('textarea.js-note-text').val(sampleComment);
$('.js-comment-button').click();
});
it('should show placeholder note while new comment is being posted', () => {
expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true);
});
it('should remove placeholder note when new comment is done posting', () => {
spyOn($, 'ajax').and.callFake((options) => {
options.success(note);
expect($notesContainer.find('.note.being-posted').length).toEqual(0);
});
});
it('should show actual note element when new comment is done posting', () => {
spyOn($, 'ajax').and.callFake((options) => {
options.success(note);
expect($notesContainer.find(`#${note.id}`).length > 0).toEqual(true);
});
});
it('should reset Form when new comment is done posting', () => {
spyOn($, 'ajax').and.callFake((options) => {
options.success(note);
expect($form.find('textarea.js-note-text')).toEqual('');
});
});
it('should trigger ajax:success event on Form when new comment is done posting', () => {
spyOn($, 'ajax').and.callFake((options) => {
options.success(note);
spyOn($form, 'trigger');
expect($form.trigger).toHaveBeenCalledWith('ajax:success', [note]);
});
});
it('should show flash error message when new comment failed to be posted', () => {
spyOn($, 'ajax').and.callFake((options) => {
options.error();
expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true);
});
});
it('should refill form textarea with original comment content when new comment failed to be posted', () => {
spyOn($, 'ajax').and.callFake((options) => {
options.error();
expect($form.find('textarea.js-note-text')).toEqual(sampleComment);
});
});
it('should show updated comment as _actively being posted_ while comment being updated', () => {
spyOn($, 'ajax').and.callFake((options) => {
options.success(note);
const $noteEl = $notesContainer.find(`#note_${note.id}`);
$noteEl.find('.js-note-edit').click();
$noteEl.find('textarea.js-note-text').val(updatedComment);
$noteEl.find('.js-comment-save-button').click();
expect($noteEl.hasClass('.being-posted')).toEqual(true);
expect($noteEl.find('.note-text').text()).toEqual(updatedComment);
});
});
it('should show updated comment when comment update is done posting', () => {
spyOn($, 'ajax').and.callFake((options) => {
options.success(note);
const $noteEl = $notesContainer.find(`#note_${note.id}`);
$noteEl.find('.js-note-edit').click();
$noteEl.find('textarea.js-note-text').val(updatedComment);
$noteEl.find('.js-comment-save-button').click();
spyOn($, 'ajax').and.callFake((updateOptions) => {
const updatedNote = Object.assign({}, note);
updatedNote.note = updatedComment;
updatedNote.html = `<li class="note note-row-1234 timeline-entry" id="note_1234">
<div class="note-text">${updatedComment}</div>
</li>`;
updateOptions.success(updatedNote);
const $updatedNoteEl = $notesContainer.find(`#note_${updatedNote.id}`);
expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
expect($updatedNoteEl.find('note-text').text().trim()).toEqual(updatedComment); // Verify if comment text updated
});
});
});
it('should show flash error message when comment failed to be updated', () => {
spyOn($, 'ajax').and.callFake((options) => {
options.success(note);
const $noteEl = $notesContainer.find(`#note_${note.id}`);
$noteEl.find('.js-note-edit').click();
$noteEl.find('textarea.js-note-text').val(updatedComment);
$noteEl.find('.js-comment-save-button').click();
spyOn($, 'ajax').and.callFake((updateOptions) => {
updateOptions.error();
const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
expect($updatedNoteEl.find('note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original
expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true); // Flash error message shown
});
});
});
});
});
}).call(window);
Loading
Loading
@@ -58,6 +58,7 @@ shared_examples 'issuable record that supports slash commands in its description
expect(page).not_to have_content '/label ~bug'
expect(page).not_to have_content '/milestone %"ASAP"'
 
wait_for_ajax
issuable.reload
note = issuable.notes.user.first
 
Loading
Loading
Loading
Loading
@@ -8,6 +8,7 @@ shared_examples 'issuable time tracker' do
it 'updates the sidebar component when estimate is added' do
submit_time('/estimate 3w 1d 1h')
 
wait_for_ajax
page.within '.time-tracking-estimate-only-pane' do
expect(page).to have_content '3w 1d 1h'
end
Loading
Loading
@@ -16,6 +17,7 @@ shared_examples 'issuable time tracker' do
it 'updates the sidebar component when spent is added' do
submit_time('/spend 3w 1d 1h')
 
wait_for_ajax
page.within '.time-tracking-spend-only-pane' do
expect(page).to have_content '3w 1d 1h'
end
Loading
Loading
@@ -25,6 +27,7 @@ shared_examples 'issuable time tracker' do
submit_time('/estimate 3w 1d 1h')
submit_time('/spend 3w 1d 1h')
 
wait_for_ajax
page.within '.time-tracking-comparison-pane' do
expect(page).to have_content '3w 1d 1h'
end
Loading
Loading
@@ -34,6 +37,7 @@ shared_examples 'issuable time tracker' do
submit_time('/estimate 3w 1d 1h')
submit_time('/remove_estimate')
 
wait_for_ajax
page.within '#issuable-time-tracker' do
expect(page).to have_content 'No estimate or time spent'
end
Loading
Loading
@@ -43,6 +47,7 @@ shared_examples 'issuable time tracker' do
submit_time('/spend 3w 1d 1h')
submit_time('/remove_time_spent')
 
wait_for_ajax
page.within '#issuable-time-tracker' do
expect(page).to have_content 'No estimate or time spent'
end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment