Commit 88bd9bac authored by Mike Greiling's avatar Mike Greiling

Merge branch 'acet-notes-prettified' into 'master'

Prettify notes app

See merge request gitlab-org/gitlab-ce!17812
parents bcc04515 fdc9ae2e
......@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue';
import store from '../notes/stores';
export default function initMrNotes() {
new Vue({ // eslint-disable-line
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-mr-discussions',
components: {
notesApp,
},
data() {
const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset;
return {
noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData),
......@@ -28,7 +30,8 @@ export default function initMrNotes() {
},
});
new Vue({ // eslint-disable-line
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter',
components: {
discussionCounter,
......
......@@ -28,7 +28,13 @@ import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import Autosave from './autosave';
import TaskList from './task_list';
import { isInViewport, getPagePath, scrollToElement, isMetaKey, hasVueMRDiscussionsCookie } from './lib/utils/common_utils';
import {
isInViewport,
getPagePath,
scrollToElement,
isMetaKey,
hasVueMRDiscussionsCookie,
} from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
......@@ -42,9 +48,21 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export default class Notes {
static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
static initialize(
notes_url,
note_ids,
last_fetched_at,
view,
enableGFM = true,
) {
if (!this.instance) {
this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
this.instance = new Notes(
notes_url,
note_ids,
last_fetched_at,
view,
enableGFM,
);
}
}
......@@ -82,7 +100,8 @@ export default class Notes {
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.notesCountBadge ||
(this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'));
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
......@@ -93,15 +112,17 @@ export default class Notes {
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
selector: '.notes'
selector: '.notes',
});
this.collapseLongCommitList();
this.setViewType(view);
// We are in the Merge Requests page so we need another edit form for Changes tab
if (getPagePath(1) === 'merge_requests') {
$('.note-edit-form').clone()
.addClass('mr-note-edit-form').insertAfter('.note-edit-form');
$('.note-edit-form')
.clone()
.addClass('mr-note-edit-form')
.insertAfter('.note-edit-form');
}
const hash = getLocationHash();
......@@ -117,7 +138,9 @@ export default class Notes {
}
addBinding() {
this.$wrapperEl = hasVueMRDiscussionsCookie() ? $(document).find('.diffs') : $(document);
this.$wrapperEl = hasVueMRDiscussionsCookie()
? $(document).find('.diffs')
: $(document);
// Edit note link
this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
......@@ -125,27 +148,55 @@ export default class Notes {
// Reopen and close actions for Issue/MR combined with note form submit
this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment);
this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment);
this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons);
this.$wrapperEl.on(
'keyup input',
'.js-note-text',
this.updateTargetButtons,
);
// resolve a discussion
this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
// delete note attachment
this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
this.$wrapperEl.on(
'click',
'.js-note-attachment-delete',
this.removeAttachment,
);
// reset main target form when clicking discard
this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
this.$wrapperEl.on(
'change',
'.js-note-attachment-input',
this.updateFormAttachment,
);
// reply to diff/discussion notes
this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
this.$wrapperEl.on(
'click',
'.js-discussion-reply-button',
this.onReplyToDiscussionNote,
);
// add diff note
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
this.$wrapperEl.on(
'click',
'.js-add-image-diff-note-button',
this.onAddImageDiffNote,
);
// hide diff note form
this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
this.$wrapperEl.on(
'click',
'.js-close-discussion-note-form',
this.cancelDiscussionForm,
);
// toggle commit list
this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
this.$wrapperEl.on(
'click',
'.system-note-commit-list-toggler',
this.toggleCommitList,
);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
// fetch notes when tab becomes visible
......@@ -154,9 +205,21 @@ export default class Notes {
this.$wrapperEl.on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote);
this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
this.$wrapperEl.on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
this.$wrapperEl.on(
'ajax:success',
'.js-discussion-note-form',
this.addDiscussionNote,
);
this.$wrapperEl.on(
'ajax:success',
'.js-main-target-form',
this.resetMainTargetForm,
);
this.$wrapperEl.on(
'ajax:complete',
'.js-main-target-form',
this.reenableTargetFormSubmitButton,
);
// when a key is clicked on the notes
this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText);
// When the URL fragment/hash has changed, `#note_xxx`
......@@ -195,10 +258,16 @@ export default class Notes {
}
static initCommentTypeToggle(form) {
const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle');
const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu');
const dropdownTrigger = form.querySelector(
'.js-comment-type-dropdown .dropdown-toggle',
);
const dropdownList = form.querySelector(
'.js-comment-type-dropdown .dropdown-menu',
);
const noteTypeInput = form.querySelector('#note_type');
const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button');
const submitButton = form.querySelector(
'.js-comment-type-dropdown .js-comment-submit-button',
);
const closeButton = form.querySelector('.js-note-target-close');
const reopenButton = form.querySelector('.js-note-target-reopen');
......@@ -215,7 +284,13 @@ export default class Notes {
}
keydownNoteText(e) {
var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText;
var $textarea,
discussionNoteForm,
editNote,
myLastNote,
myLastNoteEditBtn,
newText,
originalText;
if (isMetaKey(e)) {
return;
}
......@@ -227,7 +302,12 @@ export default class Notes {
if ($textarea.val() !== '') {
return;
}
myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, .notes_holder, #notes'));
myLastNote = $(
`li.note[data-author-id='${
gon.current_user_id
}'][data-editable]:last`,
$textarea.closest('.note, .notes_holder, #notes'),
);
if (myLastNote.length) {
myLastNoteEditBtn = myLastNote.find('.js-note-edit');
return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
......@@ -238,7 +318,9 @@ export default class Notes {
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) {
if ($textarea.val() !== '') {
if (!confirm('Are you sure you want to cancel creating this comment?')) {
if (
!confirm('Are you sure you want to cancel creating this comment?')
) {
return;
}
}
......@@ -250,7 +332,9 @@ export default class Notes {
originalText = $textarea.closest('form').data('originalNote');
newText = $textarea.val();
if (originalText !== newText) {
if (!confirm('Are you sure you want to cancel editing this comment?')) {
if (
!confirm('Are you sure you want to cancel editing this comment?')
) {
return;
}
}
......@@ -263,11 +347,14 @@ export default class Notes {
if (Notes.interval) {
clearInterval(Notes.interval);
}
return Notes.interval = setInterval((function(_this) {
return (Notes.interval = setInterval(
(function(_this) {
return function() {
return _this.refresh();
};
})(this), this.pollingInterval);
})(this),
this.pollingInterval,
));
}
refresh() {
......@@ -283,18 +370,21 @@ export default class Notes {
this.refreshing = true;
axios.get(`${this.notes_url}?html=true`, {
axios
.get(`${this.notes_url}?html=true`, {
headers: {
'X-Last-Fetched-At': this.last_fetched_at,
},
}).then(({ data }) => {
})
.then(({ data }) => {
const notes = data.notes;
this.last_fetched_at = data.last_fetched_at;
this.setPollingInterval(data.notes.length);
$.each(notes, (i, note) => this.renderNote(note));
this.refreshing = false;
}).catch(() => {
})
.catch(() => {
this.refreshing = false;
});
}
......@@ -312,7 +402,8 @@ export default class Notes {
if (shouldReset == null) {
shouldReset = true;
}
nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
nthInterval =
this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
if (shouldReset) {
this.pollingInterval = this.basePollingInterval;
} else if (this.pollingInterval < nthInterval) {
......@@ -331,10 +422,15 @@ export default class Notes {
if ('emoji_award' in noteEntity.commands_changes) {
votesBlock = $('.js-awards-block').eq(0);
loadAwardsHandler().then((awardsHandler) => {
awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
loadAwardsHandler()
.then(awardsHandler => {
awardsHandler.addAwardToEmojiBar(
votesBlock,
noteEntity.commands_changes.emoji_award,
);
awardsHandler.scrollToAwards();
}).catch(() => {
})
.catch(() => {
// ignore
});
}
......@@ -381,11 +477,17 @@ export default class Notes {
if (!noteEntity.valid) {
if (noteEntity.errors && noteEntity.errors.commands_only) {
if (noteEntity.commands_changes &&
Object.keys(noteEntity.commands_changes).length > 0) {
if (
noteEntity.commands_changes &&
Object.keys(noteEntity.commands_changes).length > 0
) {
$notesList.find('.system-note.being-posted').remove();
}
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));
this.addFlash(
noteEntity.errors.commands_only,
'notice',
this.parentTimeline.get(0),
);
this.refresh();
}
return;
......@@ -407,28 +509,30 @@ export default class Notes {
this.setupNewNote($newNote);
this.refresh();
return this.updateNotesCount(1);
}
} else if (Notes.isUpdatedNote(noteEntity, $note)) {
// The server can send the same update multiple times so we need to make sure to only update once per actual update.
else if (Notes.isUpdatedNote(noteEntity, $note)) {
const isEditing = $note.hasClass('is-editing');
const initialContent = normalizeNewlines(
$note.find('.original-note-content').text().trim()
$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;
const isTextareaUntouched =
currentContent === initialContent ||
currentContent === sanitizedNoteNote;
if (isEditing && isTextareaUntouched) {
$textarea.val(noteEntity.note);
this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
}
else if (isEditing && !isTextareaUntouched) {
} else if (isEditing && !isTextareaUntouched) {
this.putConflictEditWarningInPlace(noteEntity, $note);
this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
}
else {
} else {
const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note);
this.setupNewNote($updatedNote);
}
......@@ -452,17 +556,31 @@ export default class Notes {
}
this.note_ids.push(noteEntity.id);
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row = (form.length || !noteEntity.discussion_line_code) ? form.closest('tr') : $(`#${noteEntity.discussion_line_code}`);
form =
$form ||
$(
`.js-discussion-note-form[data-discussion-id="${
noteEntity.discussion_id
}"]`,
);
row =
form.length || !noteEntity.discussion_line_code
? form.closest('tr')
: $(`#${noteEntity.discussion_line_code}`);
if (noteEntity.on_image) {
row = form;
}
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
diffAvatarContainer = row
.prevAll('.line_holder')
.first()
.find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
discussionContainer = $(
`.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
);
if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes');
}
......@@ -470,25 +588,42 @@ export default class Notes {
if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
if (
!this.isParallelView() ||
row.hasClass('js-temp-notes-holder') ||
noteEntity.on_image
) {
// 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="${noteEntity.discussion_id}"]`);
var contentContainerClass = '.' + $notes.closest('.notes_content')
var $notes = $discussion.find(
`.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
);
var contentContainerClass =
'.' +
$notes
.closest('.notes_content')
.attr('class')
.split(' ')
.join('.');
row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
row
.find(contentContainerClass + ' .content')
.append($notes.closest('.content').children());
}
}
// Init discussion on 'Discussion' page if it is merge request page
const page = $('body').attr('data-page');
if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) {
if (
(page && page.indexOf('projects:merge_request') !== -1) ||
!noteEntity.diff_discussion_html
) {
if (!hasVueMRDiscussionsCookie()) {
Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
Notes.animateAppendNote(
noteEntity.discussion_html,
$('.main-notes-list'),
);
}
}
} else {
......@@ -496,7 +631,10 @@ export default class Notes {
Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
if (
typeof gl.diffNotesCompileComponents !== 'undefined' &&
noteEntity.discussion_resolvable
) {
gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
......@@ -508,7 +646,8 @@ export default class Notes {
}
getLineHolder(changesDiscussionContainer) {
return $(changesDiscussionContainer).closest('.notes_holder')
return $(changesDiscussionContainer)
.closest('.notes_holder')
.prevAll('.line_holder')
.first()
.get(0);
......@@ -541,8 +680,14 @@ export default class Notes {
form.find('.js-errors').remove();
// reset text and preview
form.find('.js-md-write-button').click();
form.find('.js-note-text').val('').trigger('input');
form.find('.js-note-text').data('autosave').reset();
form
.find('.js-note-text')
.val('')
.trigger('input');
form
.find('.js-note-text')
.data('autosave')
.reset();
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
......@@ -578,7 +723,10 @@ export default class Notes {
form.find('#note_type').val('');
form.find('#note_project_id').remove();
form.find('#in_reply_to_discussion_id').remove();
form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
form
.find('.js-comment-resolve-button')
.closest('comment-and-resolve-btn')
.remove();
this.parentTimeline = form.parents('.timeline');
if (form.length) {
......@@ -632,11 +780,17 @@ export default class Notes {
} else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline.get(0));
return this.addFlash(
'Your comment could not be submitted! Please check your network connection and try again.',
'alert',
formParentTimeline.get(0),
);
}
updateNoteError($parentTimeline) {
new Flash('Your comment could not be updated! Please check your network connection and try again.');
new Flash(
'Your comment could not be updated! Please check your network connection and try again.',
);
}
/**
......@@ -685,14 +839,16 @@ export default class Notes {
}
checkContentToAllowEditing($el) {
var initialContent = $el.find('.original-note-content').text().trim();
var initialContent = $el
.find('.original-note-content')
.text()
.trim();
var currentContent = $el.find('.js-note-text').val();
var isAllowed = true;
if (currentContent === initialContent) {
this.removeNoteEditForm($el);
}
else {
} else {
var $buttons = $el.find('.note-form-actions');
var isWidgetVisible = isInViewport($el.get(0));
......@@ -754,8 +910,7 @@ export default class Notes {
this.setupNewNote($newNote);
// Now that we have taken care of the update, clear it out
delete this.updatedNotesTrackingMap[noteId];
}
else {
} else {
$note.find('.js-finish-edit-warning').hide();
this.removeNoteEditForm($note);
}
......@@ -788,7 +943,9 @@ export default class Notes {
form.removeClass('current-note-edit-form');
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('originalNote'));
return form
.find('.js-note-text')
.val(form.find('form.edit-note').data('originalNote'));
}
/**
......@@ -802,10 +959,12 @@ export default class Notes {
$note = $(e.currentTarget).closest('.note');
noteElId = $note.attr('id');
noteId = $note.attr('data-note-id');
lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]')
lineHolder = $(e.currentTarget)
.closest('.notes[data-discussion-id]')
.closest('.notes_holder')
.prev('.line_holder');
$(`.note[id="${noteElId}"]`).each((function(_this) {
$(`.note[id="${noteElId}"]`).each(
(function(_this) {
// A same note appears in the "Discussion" and in the "Changes" tab, we have
// to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
// where $('#noteId') would return only one.
......@@ -834,15 +993,21 @@ export default class Notes {
// The notes tr can contain multiple lists of notes, like on the parallel diff
// notesTr does not exist for image diffs
if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
if (
notesTr.find('.discussion-notes').length > 1 ||
notesTr.length === 0
) {
const $diffFile = $notes.closest('.diff-file');
if ($diffFile.length > 0) {
const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
const removeBadgeEvent = new CustomEvent(
'removeBadge.imageDiff',
{
detail: {
// badgeNumber's start with 1 and index starts with 0
badgeNumber: $notes.index() + 1,
},
});
},
);
$diffFile[0].dispatchEvent(removeBadgeEvent);
}
......@@ -853,7 +1018,8 @@ export default class Notes {
}
}
};
})(this));
})(this),
);
Notes.refreshVueNotes();
Notes.checkMergeRequestStatus();
......@@ -935,7 +1101,12 @@ export default class Notes {
// DiffNote
form.find('#note_position').val(dataHolder.attr('data-position'));
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancelText'));
form
.find('.js-note-discard')
.show()
.removeClass('js-note-discard')
.addClass('js-close-discussion-note-form')
.text(form.find('.js-close-discussion-note-form').data('cancelText'));
form.find('.js-note-target-close').remove();
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
......@@ -971,7 +1142,7 @@ export default class Notes {
this.toggleDiffNote({
target: $link,
lineType: link.dataset.lineType,
showReplyInput
showReplyInput,
});
}
......@@ -987,7 +1158,9 @@ export default class Notes {
// Setup comment form
let newForm;
const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
const $noteContainer = $link
.closest('.diff-viewer')
.find('.note-container');
const $form = $noteContainer.find('> .discussion-form');
if ($form.length === 0) {
......@@ -1000,13 +1173,17 @@ export default class Notes {
this.setupDiscussionNoteForm($link, newForm);
}
toggleDiffNote({
target,
lineType,
forceShow,
showReplyInput = false,
}) {
var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) {
var $link,
addForm,
hasNotes,
newForm,
noteForm,
replyButton,
row,
rowCssToAdd,
targetContent,
isDiffCommentAvatar;
$link = $(target);
row = $link.closest('tr');
const nextRow = row.next();
......@@ -1018,11 +1195,13 @@ export default class Notes {
hasNotes = nextRow.is('.notes_holder');
addForm = false;
let lineTypeSelector = '';
rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
rowCssToAdd =
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`;
rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
rowCssToAdd =
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
}
const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
let notesContent = targetRow.find(notesContentSelector);
......@@ -1050,7 +1229,9 @@ export default class Notes {
notesContent = targetRow.find(notesContentSelector);
addForm = true;
} else {
const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
const isCurrentlyShown = targetRow
.find('.content:not(:empty)')
.is(':visible');
const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
......@@ -1077,11 +1258,12 @@ export default class Notes {
row = form.closest('tr');
glForm = form.data('glForm');
glForm.destroy();
form.find('.js-note-text').data('autosave').reset();
// show the reply button (will only work for replies)
form
.prev('.discussion-reply-holder')
.show();
.find('.js-note-text')
.data('autosave')
.reset();
// show the reply button (will only work for replies)
form.prev('.discussion-reply-holder').show();
if (row.is('.js-temp-notes-holder')) {
// remove temporary row for diff lines
return row.remove();
......@@ -1122,7 +1304,9 @@ export default class Notes {
var filename, form;
form = $(this).closest('form');
// get only the basename
filename = $(this).val().replace(/^.*[\\\/]/, '');
filename = $(this)
.val()
.replace(/^.*[\\\/]/, '');
return form.find('.js-attachment-filename').text(filename);
}
......@@ -1194,12 +1378,16 @@ export default class Notes {
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm.find('form')
$editForm
.find('form')
.attr('action', `${postUrl}?html=true`)
.attr('data-remote', 'true');
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
$editForm.find('.js-note-text').focus().val(originalContent);
$editForm
.find('.js-note-text')
.focus()
.val(originalContent);
$editForm.find('.js-md-write-button').trigger('click');
$editForm.find('.referenced-users').hide();
}
......@@ -1208,7 +1396,9 @@ export default class Notes {
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">
<a href="#note_${
noteEntity.id
}" target="_blank" rel="noopener noreferrer">
updated comment
</a>
to ensure information is not lost
......@@ -1218,12 +1408,15 @@ export default class Notes {
}
updateNotesCount(updateCount) {
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
return this.notesCountBadge.text(
parseInt(this.notesCountBadge.text(), 10) + updateCount,
);
}
static renderPlaceholderComponent($container) {
const el = $container.find('.js-code-placeholder').get(0);
new Vue({ // eslint-disable-line no-new
new Vue({
// eslint-disable-line no-new
el,
components: {
SkeletonLoadingContainer,
......@@ -1248,7 +1441,9 @@ export default class Notes {
$container.find('.line_content').html(
$(`
<div class="nothing-here-block">
${__('Unable to load the diff.')} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
${__(
'Unable to load the diff.',
)} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
</div>
`),
);
......@@ -1266,7 +1461,8 @@ export default class Notes {
const fileHolder = $container.find('.file-holder');
const url = fileHolder.data('linesPath');
axios.get(url)
axios
.get(url)
.then(({ data }) => {
Notes.renderDiffContent($container, data);
})
......@@ -1277,9 +1473,14 @@ export default class Notes {
toggleCommitList(e) {
const $element = $(e.currentTarget);
const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
const $closestSystemCommitList = $element.siblings(
'.system-note-commit-list',
);
$element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
$element
.find('.fa')
.toggleClass('fa-angle-down')
.toggleClass('fa-angle-up');
$closestSystemCommitList.toggleClass('hide-shade');
}
......@@ -1289,11 +1490,17 @@ export default class Notes {
* intrusive.
*/
collapseLongCommitList() {
const systemNotes = $('#notes-list').find('li.system-note').has('ul');
const systemNotes = $('#notes-list')
.find('li.system-note')
.has('ul');
$.each(systemNotes, function(index, systemNote) {
const $systemNote = $(systemNote);
const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', '');
const headerMessage = $systemNote
.find('.note-text')
.find('p:first')
.text()
.replace(':', '');
$systemNote.find('.note-header .system-note-message').html(headerMessage);
......@@ -1301,7 +1508,9 @@ export default class Notes {
$systemNote.find('.note-text').addClass('system-note-commit-list');
$systemNote.find('.system-note-commit-list-toggler').show();
} else {
$systemNote.find('.note-text').addClass('system-note-commit-list hide-shade');
$systemNote
.find('.note-text')
.addClass('system-note-commit-list hide-shade');
}
});
}
......@@ -1319,14 +1528,10 @@ export default class Notes {
cleanForm($form) {
// Remove JS classes that are not needed here
$form
.find('.js-comment-type-dropdown')
.removeClass('btn-group');
$form.find('.js-comment-type-dropdown').removeClass('btn-group');
// Remove dropdown
$form
.find('.dropdown-menu')
.remove();
$form.find('.dropdown-menu').remove();
return $form;
}
......@@ -1345,7 +1550,11 @@ export default class Notes {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim());
const currentNoteText = normalizeNewlines(
$note.find('.original-note-content').first().text().trim()
$note
.find('.original-note-content')
.first()
.text()
.trim(),
);
return sanitizedNoteEntityText !== currentNoteText;
}
......@@ -1435,7 +1644,14 @@ export default class Notes {
* Once comment is _actually_ posted on server, we will have final element
* in response that we will show in place of this temporary element.
*/
createPlaceholderNote({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) {
createPlaceholderNote({
formContent,
uniqueId,
isDiscussionNote,
currentUsername,
currentUserFullname,
currentUserAvatar,
}) {
const discussionClass = isDiscussionNote ? 'discussion' : '';
const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
......@@ -1449,8 +1665,12 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/${_.escape(currentUsername)}">
<span class="hidden-xs">${_.escape(currentUsername)}</span>
<span class="note-headline-light">${_.escape(currentUsername)}</span>
<span class="hidden-xs">${_.escape(
currentUsername,
)}</span>
<span class="note-headline-light">${_.escape(
currentUsername,
)}</span>
</a>
</div>
</div>
......@@ -1461,11 +1681,13 @@ export default class Notes {
</div>
</div>
</div>
</li>`
</li>`,
);
$tempNote.find('.hidden-xs').text(_.escape(currentUserFullname));
$tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`);
$tempNote
.find('.note-headline-light')
.text(`@${_.escape(currentUsername)}`);
return $tempNote;
}
......@@ -1481,7 +1703,7 @@ export default class Notes {
<i>${formContent}</i>
</div>
</div>
</li>`
</li>`,
);
return $tempNote;
......@@ -1513,11 +1735,22 @@ export default class Notes {
const $submitBtn = $(e.target);
let $form = $submitBtn.parents('form');
const $closeBtn = $form.find('.js-note-target-close');
const isDiscussionNote = $submitBtn.parent().find('li.droplab-item-selected').attr('id') === 'discussion';
const isDiscussionNote =
$submitBtn
.parent()
.find('li.droplab-item-selected')
.attr('id') === 'discussion';
const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
const isDiscussionResolve = $submitBtn.hasClass(
'js-comment-resolve-button',
);
const {
formData,
formContent,
formAction,
formContentOriginal,
} = this.getFormData($form);
let noteUniqueId;
let systemNoteUniqueId;
let hasQuickActions = false;
......@@ -1547,23 +1780,30 @@ export default class Notes {
// Show placeholder note
if (tempFormContent) {
noteUniqueId = _.uniqueId('tempNote_');
$notesContainer.append(this.createPlaceholderNote({
$notesContainer.append(
this.createPlaceholderNote({
formContent: tempFormContent,
uniqueId: noteUniqueId,
isDiscussionNote,
currentUsername: gon.current_username,
currentUserFullname: gon.current_user_fullname,
currentUserAvatar: gon.current_user_avatar_url,
}));
}),
);
}
// Show placeholder system note
if (hasQuickActions) {
systemNoteUniqueId = _.uniqueId('tempSystemNote_');
$notesContainer.append(this.createPlaceholderSystemNote({
formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)),
$notesContainer.append(
this.createPlaceholderSystemNote({
formContent: this.getQuickActionDescription(
formContent,
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands),
),
uniqueId: systemNoteUniqueId,
}));
}),
);
}
// Clear the form textarea
......@@ -1577,8 +1817,9 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
axios.post(`${formAction}?html=true`, formData)
.then((res) => {
axios
.post(`${formAction}?html=true`, formData)
.then(res => {
const note = res.data;
// Submission successful! remove placeholder
......@@ -1595,7 +1836,9 @@ export default class Notes {
// Reset cached commands list when command is applied
if (hasQuickActions) {
$form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
$form
.find('textarea.js-note-text')
.trigger('clear-commands-cache.atwho');
}
// Clear previous form errors
......@@ -1640,11 +1883,14 @@ export default class Notes {
// append flash-container to the Notes list
if ($notesContainer.length) {
$notesContainer.append('<div class="flash-container" style="display: none;"></div>');
$notesContainer.append(
'<div class="flash-container" style="display: none;"></div>',
);
}
Notes.refreshVueNotes();
} else if (isMainForm) { // Check if this was main thread comment
} else if (isMainForm) {
// Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
this.addNote($form, note);
this.reenableTargetFormSubmitButton(e);
......@@ -1655,7 +1901,8 @@ export default class Notes {
}
$form.trigger('ajax:success', [note]);
}).catch(() => {
})
.catch(() => {
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove();
......@@ -1675,7 +1922,9 @@ export default class Notes {
// Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) {
const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
const replyButton = $notesContainer
.parent()
.find('.js-discussion-reply-button');
this.replyToDiscussionNote(replyButton[0]);
$form = $notesContainer.parent().find('form');
}
......@@ -1720,12 +1969,19 @@ export default class Notes {
// Show updated comment content temporarily
$noteBodyText.html(formContent);
$editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
$editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>');
$editingNote
.removeClass('is-editing fade-in-full')
.addClass('being-posted fade-in-half');
$editingNote
.find('.note-headline-meta a')
.html(
'<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>',
);
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
axios.post(`${formAction}?html=true`, formData)
axios
.post(`${formAction}?html=true`, formData)
.then(({ data }) => {
// Submission successful! render final note element
this.updateNote(data, $editingNote);
......
<script>
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
import { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
import {
capitalizeFirstCharacter,
convertToCamelCase,
} from '../../lib/utils/text_utility';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default {
export default {
name: 'CommentForm',
components: {
issueWarning,
......@@ -28,9 +31,7 @@
userAvatarLink,
loadingButton,
},
mixins: [
issuableStateMixin,
],
mixins: [issuableStateMixin],
props: {
noteableType: {
type: String,
......@@ -53,9 +54,7 @@
'getNotesData',
'openState',
]),
...mapState([
'isToggleStateButtonLoading',
]),
...mapState(['isToggleStateButtonLoading']),
noteableDisplayName() {
return this.noteableType.replace(/_/g, ' ');
},
......@@ -63,10 +62,15 @@
return this.getUserData.id;
},
commentButtonTitle() {
return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
return this.noteType === constants.COMMENT
? 'Comment'
: 'Start discussion';
},
isOpen() {
return this.openState === constants.OPENED || this.openState === constants.REOPENED;
return (
this.openState === constants.OPENED ||
this.openState === constants.REOPENED
);
},
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
......@@ -75,23 +79,17 @@
const openOrClose = this.isOpen ? 'close' : 'reopen';
if (this.note.length) {
return sprintf(
__('%{actionText} & %{openOrClose} %{noteable}'),
{
return sprintf(__('%{actionText} & %{openOrClose} %{noteable}'), {
actionText: this.commentButtonTitle,
openOrClose,
noteable: this.noteableDisplayName,
},
);
});
}
return sprintf(
__('%{openOrClose} %{noteable}'),
{
return sprintf(__('%{openOrClose} %{noteable}'), {
openOrClose: capitalizeFirstCharacter(openOrClose),
noteable: this.noteableDisplayName,
},
);
});
},
actionButtonClassNames() {
return {
......@@ -131,7 +129,9 @@
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
this.toggleIssueLocalState(
isClosed ? constants.CLOSED : constants.REOPENED,
);
});
this.initAutoSave();
......@@ -180,7 +180,7 @@
this.stopPolling();
this.saveNote(noteData)
.then((res) => {
.then(res => {
this.enableButton();
this.restartPolling();
......@@ -205,8 +205,7 @@
.catch(() => {
this.enableButton();
this.discard(false);
const msg =
`Your comment could not be submitted!
const msg = `Your comment could not be submitted!
Please check your network connection and try again.`;
Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content.
......@@ -228,7 +227,9 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false);
Flash(
sprintf(
__('Something went wrong while closing the %{issuable}. Please try again later'),
__(
'Something went wrong while closing the %{issuable}. Please try again later',
),
{ issuable: this.noteableDisplayName },
),
);
......@@ -241,7 +242,9 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false);
Flash(
sprintf(
__('Something went wrong while reopening the %{issuable}. Please try again later'),
__(
'Something went wrong while reopening the %{issuable}. Please try again later',
),
{ issuable: this.noteableDisplayName },
),
);
......@@ -278,12 +281,15 @@ Please check your network connection and try again.`;
},
initAutoSave() {
if (this.isLoggedIn) {
const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
this.autosave = new Autosave(
$(this.$refs.textarea),
['Note', noteableType, this.getNoteableData.id],
const noteableType = capitalizeFirstCharacter(
convertToCamelCase(this.noteableType),
);
this.autosave = new Autosave($(this.$refs.textarea), [
'Note',
noteableType,
this.getNoteableData.id,
]);
}
},
initTaskList() {
......@@ -299,7 +305,7 @@ Please check your network connection and try again.`;
});
},
},
};
};
</script>
<template>
......
<script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
export default {
components: {
ClipboardButton,
Icon,
......@@ -18,7 +18,7 @@
return this.diffFile.discussionPath ? 'a' : 'span';
},
},
};
};
</script>
<template>
......
<script>
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from './diff_file_header.vue';
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from './diff_file_header.vue';
export default {
export default {
components: {
DiffFileHeader,
},
......@@ -37,7 +37,11 @@
if (this.isImageDiff) {
const canCreateNote = false;
const renderCommentBadge = true;
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
imageDiffHelper.initImageDiff(
this.$refs.fileHolder,
canCreateNote,
renderCommentBadge,
);
} else {
const fileHolder = $(this.$refs.fileHolder);
this.$nextTick(() => {
......@@ -50,7 +54,7 @@
return html.outerHTML ? 'tr' : 'template';
},
},
};
};
</script>
<template>
......
<script>
import { mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility';
import { scrollToElement } from '../../lib/utils/common_utils';
import tooltip from '../../vue_shared/directives/tooltip';
import { mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility';
import { scrollToElement } from '../../lib/utils/common_utils';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
export default {
directives: {
tooltip,
},
......@@ -49,7 +49,9 @@
},
methods: {
jumpToFirstDiscussion() {
const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`);
const el = document.querySelector(
`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`,
);
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') {
......@@ -61,7 +63,7 @@
}
},
},
};
};
</script>
<template>
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Issuable from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import Issuable from '~/vue_shared/mixins/issuable';
export default {
export default {
components: {
Icon,
},
mixins: [
Issuable,
],
};
mixins: [Issuable],
};
</script>
<template>
......
<script>
import { mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
export default {
name: 'NoteActions',
directives: {
tooltip,
......@@ -70,9 +70,7 @@
},
},
computed: {
...mapGetters([
'getUserDataByProp',
]),
...mapGetters(['getUserDataByProp']),
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
......@@ -115,7 +113,7 @@
this.$emit('handleResolve');
},
},
};
};
</script>
<template>
......
<script>
export default {
export default {
name: 'NoteAttachment',
props: {
attachment: {
......@@ -7,7 +7,7 @@
required: true,
},
},
};
};
</script>
<template>
......
<script>
import { mapActions, mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
import { mapActions, mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
......@@ -30,9 +30,7 @@
},
},
computed: {
...mapGetters([
'getUserData',
]),
...mapGetters(['getUserData']),
// `this.awards` is an array with emojis but they are not grouped by emoji name. See below.
// [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ]
// This method will group emojis by their name as an Object. See below.
......@@ -79,9 +77,7 @@
this.emojiSmiley = emojiSmiley;
},
methods: {
...mapActions([
'toggleAwardRequest',
]),
...mapActions(['toggleAwardRequest']),
getAwardHTML(name) {
return glEmojiTag(name);
},
......@@ -96,30 +92,43 @@
const restrictedEmojis = ['thumbsup', 'thumbsdown'];
// Users can not add :+1: and :-1: to their own notes
if (this.getUserData.id === this.noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) {
if (
this.getUserData.id === this.noteAuthorId &&
restrictedEmojis.indexOf(awardName) > -1
) {
isAllowed = false;
}
return this.getUserData.id && isAllowed;
},
hasReactionByCurrentUser(awardList) {
return awardList.filter(award => award.user.id === this.getUserData.id).length;
return awardList.filter(award => award.user.id === this.getUserData.id)
.length;
},
awardTitle(awardsList) {
const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList);
const hasReactionByCurrentUser = this.hasReactionByCurrentUser(
awardsList,
);
const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10;
let awardList = awardsList;
// Filter myself from list if I am awarded.
if (hasReactionByCurrentUser) {
awardList = awardList.filter(award => award.user.id !== this.getUserData.id);
awardList = awardList.filter(
award => award.user.id !== this.getUserData.id,
);
}
// Get only 9-10 usernames to show in tooltip text.
const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name);
const namesToShow = awardList
.slice(0, TOOLTIP_NAME_COUNT)
.map(award => award.user.name);
// Get the remaining list to use in `and x more` text.
const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length);
const remainingAwardList = awardList.slice(
TOOLTIP_NAME_COUNT,
awardList.length,
);
// Add myself to the begining of the list so title will start with You.
if (hasReactionByCurrentUser) {
......@@ -130,14 +139,17 @@
// We have 10+ awarded user, join them with comma and add `and x more`.
if (remainingAwardList.length) {
title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`;
title = `${namesToShow.join(', ')}, and ${
remainingAwardList.length
} more.`;
} else if (namesToShow.length > 1) {
// Join all names with comma but not the last one, it will be added with and text.
title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
// If we have more than 2 users we need an extra comma before and text.
title += namesToShow.length > 2 ? ',' : '';
title += ` and ${namesToShow.slice(-1)}`; // Append and text
} else { // We have only 2 users so join them with and.
} else {
// We have only 2 users so join them with and.
title = namesToShow.join(' and ');
}
......@@ -169,11 +181,12 @@
awardName: parsedName,
};
this.toggleAwardRequest(data)
.catch(() => Flash('Something went wrong on our end.'));
this.toggleAwardRequest(data).catch(() =>
Flash('Something went wrong on our end.'),
);
},
},
};
};
</script>
<template>
......
<script>
import $ from 'jquery';
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue';
import TaskList from '../../task_list';
import autosave from '../mixins/autosave';
import $ from 'jquery';
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue';
import TaskList from '../../task_list';
import autosave from '../mixins/autosave';
export default {
export default {
components: {
noteEditedText,
noteAwardsList,
noteAttachment,
noteForm,
},
mixins: [
autosave,
],
mixins: [autosave],
props: {
note: {
type: Object,
......@@ -77,7 +75,7 @@
this.$emit('cancelFormEdition', shouldConfirm, isDirty);
},
},
};
};
</script>
<template>
......
<script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default {
export default {
name: 'EditedNoteText',
components: {
timeAgoTooltip,
......@@ -26,7 +26,7 @@
default: 'edited-text',
},
},
};
};
</script>
<template>
......
<script>
import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
export default {
export default {
name: 'IssueNoteForm',
components: {
issueWarning,
markdownField,
},
mixins: [
issuableStateMixin,
resolvable,
],
mixins: [issuableStateMixin, resolvable],
props: {
noteBody: {
type: String,
......@@ -69,7 +66,9 @@
return this.getNotesDataByProp('markdownDocsPath');
},
quickActionsDocsPath() {
return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined;
return !this.isEditing
? this.getNotesDataByProp('quickActionsDocsPath')
: undefined;
},
currentUserId() {
return this.getUserDataByProp('id');
......@@ -91,24 +90,29 @@
this.$refs.textarea.focus();
},
methods: {
...mapActions([
'toggleResolveNote',
]),
...mapActions(['toggleResolveNote']),
handleUpdate(shouldResolve) {
const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true;
this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
this.$emit(
'handleFormUpdate',
this.updatedNoteBody,
this.$refs.editNoteForm,
() => {
this.isSubmitting = false;
if (shouldResolve) {
this.resolveHandler(beforeSubmitDiscussionState);
}
});
},
);
},
editMyLastNote() {
if (this.updatedNoteBody === '') {
const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody);
const lastNoteInDiscussion = this.getDiscussionLastNote(
this.updatedNoteBody,
);
if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', {
......@@ -119,10 +123,14 @@
},
cancelHandler(shouldConfirm = false) {
// Sends information about confirm message and if the textarea has changed
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody);
this.$emit(
'cancelFormEdition',
shouldConfirm,
this.noteBody !== this.updatedNoteBody,
);
},
},
};
};
</script>
<template>
......
<script>
import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default {
export default {
components: {
timeAgoTooltip,
},
......@@ -49,9 +49,7 @@
},
},
methods: {
...mapActions([
'setTargetNoteHash',
]),
...mapActions(['setTargetNoteHash']),
handleToggle() {
this.$emit('toggleHandler');
},
......@@ -59,7 +57,7 @@
this.setTargetNoteHash(this.noteTimestampLink);
},
},
};
};
</script>
<template>
......
<script>
import { mapGetters } from 'vuex';
import { mapGetters } from 'vuex';
export default {
export default {
computed: {
...mapGetters([
'getNotesDataByProp',
]),
...mapGetters(['getNotesDataByProp']),
registerLink() {
return this.getNotesDataByProp('registerPath');
},
......@@ -13,7 +11,7 @@
return this.getNotesDataByProp('newSessionPath');
},
},
};
};
</script>
<template>
......
<script>
import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import tooltip from '../../vue_shared/directives/tooltip';
import { scrollToElement } from '../../lib/utils/common_utils';
import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import tooltip from '../../vue_shared/directives/tooltip';
import { scrollToElement } from '../../lib/utils/common_utils';
export default {
export default {
components: {
noteableNote,
diffWithNote,
......@@ -34,11 +34,7 @@
directives: {
tooltip,
},
mixins: [
autosave,
noteable,
resolvable,
],
mixins: [autosave, noteable, resolvable],
props: {
note: {
type: Object,
......@@ -99,7 +95,9 @@
return this.unresolvedDiscussions.length > 0;
},
wrapperComponent() {
return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div';
return this.discussion.diffDiscussion && this.discussion.diffFile
? diffWithNote
: 'div';
},
wrapperClass() {
return this.isDiffDiscussion ? '' : 'panel panel-default';
......@@ -151,8 +149,10 @@
},
cancelReplyForm(shouldConfirm) {
if (shouldConfirm && this.$refs.noteForm.isDirty) {
const msg = 'Are you sure you want to cancel creating this comment?';
// eslint-disable-next-line no-alert
if (!confirm('Are you sure you want to cancel creating this comment?')) {
if (!confirm(msg)) {
return;
}
}
......@@ -178,7 +178,7 @@
this.resetAutoSave();
callback();
})
.catch((err) => {
.catch(err => {
this.removePlaceholderNotes();
this.isReplying = true;
this.$nextTick(() => {
......@@ -204,7 +204,7 @@ Please check your network connection and try again.`;
}
},
},
};
};
</script>
<template>
......
<script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue';
import noteBody from './note_body.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue';
import noteBody from './note_body.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
export default {
export default {
components: {
userAvatarLink,
noteHeader,
noteActions,
noteBody,
},
mixins: [
noteable,
resolvable,
],
mixins: [noteable, resolvable],
props: {
note: {
type: Object,
......@@ -37,10 +34,7 @@
};
},
computed: {
...mapGetters([
'targetNoteHash',
'getUserData',
]),
...mapGetters(['targetNoteHash', 'getUserData']),
author() {
return this.note.author;
},
......@@ -53,7 +47,9 @@
};
},
canReportAsAbuse() {
return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
return (
this.note.report_abuse_path && this.author.id !== this.getUserData.id
);
},
noteAnchorId() {
return `note_${this.note.id}`;
......@@ -89,7 +85,9 @@
this.isDeleting = false;
})
.catch(() => {
Flash('Something went wrong while deleting your note. Please try again.');
Flash(
'Something went wrong while deleting your note. Please try again.',
);
this.isDeleting = false;
});
}
......@@ -120,7 +118,8 @@
this.isRequesting = false;
this.isEditing = true;
this.$nextTick(() => {
const msg = 'Something went wrong while editing your comment. Please try again.';
const msg =
'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
......@@ -130,7 +129,8 @@
formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
if (!confirm('Are you sure you want to cancel editing this comment?')) return;
if (!confirm('Are you sure you want to cancel editing this comment?'))
return;
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
......@@ -146,7 +146,7 @@
this.$refs.noteBody.$refs.noteForm.note.note = noteText;
},
},
};
};
</script>
<template>
......
<script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import store from '../stores/';
import * as constants from '../constants';
import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import store from '../stores/';
import * as constants from '../constants';
import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
export default {
export default {
name: 'NotesApp',
components: {
noteableNote,
......@@ -47,16 +47,14 @@
};
},
computed: {
...mapGetters([
'notes',
'getNotesDataByProp',
'discussionCount',
]),
...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
// FIXME -- @fatihacet Get this from JSON data.
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE;
return this.noteableData.merge_params
? MERGE_REQUEST_NOTEABLE_TYPE
: ISSUE_NOTEABLE_TYPE;
},
allNotes() {
if (this.isLoading) {
......@@ -79,9 +77,11 @@
const parentElement = this.$el.parentElement;
if (parentElement &&
parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', (event) => {
if (
parentElement &&
parentElement.classList.contains('js-vue-notes-event')
) {
parentElement.addEventListener('toggleAward', event => {
const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId });
});
......@@ -131,7 +131,9 @@
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
Flash('Something went wrong while fetching comments. Please try again.');
Flash(
'Something went wrong while fetching comments. Please try again.',
);
});
},
initPolling() {
......@@ -154,7 +156,7 @@
}
},
},
};
};
</script>
<template>
......
import Vue from 'vue';
import notesApp from './components/notes_app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
document.addEventListener(
'DOMContentLoaded',
() =>
new Vue({
el: '#js-vue-notes',
components: {
notesApp,
......@@ -9,13 +12,17 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
data() {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const currentUserData = parsedUserData ? {
let currentUserData = {};
if (parsedUserData) {
currentUserData = {
id: parsedUserData.id,
name: parsedUserData.name,
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
} : {};
};
}
return {
noteableData: JSON.parse(notesDataset.noteableData),
......@@ -32,4 +39,5 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
}));
}),
);
......@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default {
methods: {
initAutoSave(noteableType) {
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]);
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
'Note',
capitalizeFirstCharacter(noteableType),
this.note.id,
]);
},
resetAutoSave() {
this.autosave.reset();
......
......@@ -12,7 +12,8 @@ export default {
discussionResolved() {
const { notes, resolved } = this.note;
if (notes) { // Decide resolved state using store. Only valid for discussions.
if (notes) {
// Decide resolved state using store. Only valid for discussions.
return notes.every(note => note.resolved && !note.system);
}
......@@ -26,7 +27,9 @@ export default {
return __('Comment and resolve discussion');
}
return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion');
return this.discussionResolved
? __('Unresolve discussion')
: __('Resolve discussion');
},
},
methods: {
......@@ -42,7 +45,9 @@ export default {
})
.catch(() => {
this.isResolving = false;
const msg = __('Something went wrong while resolving this discussion. Please try again.');
const msg = __(
'Something went wrong while resolving this discussion. Please try again.',
);
Flash(msg, 'alert', this.$el);
});
},
......
......@@ -22,7 +22,9 @@ export default {
},
toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
const method = isResolved
? UNRESOLVE_NOTE_METHOD_NAME
: RESOLVE_NOTE_METHOD_NAME;
return Vue.http[method](endpoint);
},
......
......@@ -12,47 +12,57 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll;
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
export const fetchNotes = ({ commit }, path) => service
export const setNotesData = ({ commit }, data) =>
commit(types.SET_NOTES_DATA, data);
export const setNoteableData = ({ commit }, data) =>
commit(types.SET_NOTEABLE_DATA, data);
export const setUserData = ({ commit }, data) =>
commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) =>
commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) =>
commit(types.SET_INITIAL_NOTES, data);
export const setTargetNoteHash = ({ commit }, data) =>
commit(types.SET_TARGET_NOTE_HASH, data);
export const toggleDiscussion = ({ commit }, data) =>
commit(types.TOGGLE_DISCUSSION, data);
export const fetchNotes = ({ commit }, path) =>
service
.fetchNotes(path)
.then(res => res.json())
.then((res) => {
.then(res => {
commit(types.SET_INITIAL_NOTES, res);
});
export const deleteNote = ({ commit }, note) => service
.deleteNote(note.path)
.then(() => {
export const deleteNote = ({ commit }, note) =>
service.deleteNote(note.path).then(() => {
commit(types.DELETE_NOTE, note);
});
export const updateNote = ({ commit }, { endpoint, note }) => service
export const updateNote = ({ commit }, { endpoint, note }) =>
service
.updateNote(endpoint, note)
.then(res => res.json())
.then((res) => {
.then(res => {
commit(types.UPDATE_NOTE, res);
});
export const replyToDiscussion = ({ commit }, { endpoint, data }) => service
export const replyToDiscussion = ({ commit }, { endpoint, data }) =>
service
.replyToDiscussion(endpoint, data)
.then(res => res.json())
.then((res) => {
.then(res => {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
return res;
});
export const createNewNote = ({ commit }, { endpoint, data }) => service
export const createNewNote = ({ commit }, { endpoint, data }) =>
service
.createNewNote(endpoint, data)
.then(res => res.json())
.then((res) => {
.then(res => {
if (!res.errors) {
commit(types.ADD_NEW_NOTE, res);
}
......@@ -62,11 +72,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export const removePlaceholderNotes = ({ commit }) =>
commit(types.REMOVE_PLACEHOLDER_NOTES);
export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service
export const toggleResolveNote = (
{ commit },
{ endpoint, isResolved, discussion },
) =>
service
.toggleResolveNote(endpoint, isResolved)
.then(res => res.json())
.then((res) => {
const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
.then(res => {
const mutationType = discussion
? types.UPDATE_DISCUSSION
: types.UPDATE_NOTE;
commit(mutationType, res);
});
......@@ -76,7 +92,7 @@ export const closeIssue = ({ commit, dispatch, state }) => {
return service
.toggleIssueState(state.notesData.closePath)
.then(res => res.json())
.then((data) => {
.then(data => {
commit(types.CLOSE_ISSUE);
dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false);
......@@ -88,7 +104,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => {
return service
.toggleIssueState(state.notesData.reopenPath)
.then(res => res.json())
.then((data) => {
.then(data => {
commit(types.REOPEN_ISSUE);
dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false);
......@@ -99,10 +115,12 @@ export const toggleStateButtonLoading = ({ commit }, value) =>
commit(types.TOGGLE_STATE_BUTTON_LOADING, value);
export const emitStateChangedEvent = ({ commit, getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { detail: {
const event = new CustomEvent('issuable_vue_app:change', {
detail: {
data,
isClosed: getters.openState === constants.CLOSED,
} });
},
});
document.dispatchEvent(event);
};
......@@ -144,8 +162,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
}
return dispatch(methodToDispatch, noteData)
.then((res) => {
return dispatch(methodToDispatch, noteData).then(res => {
const { errors } = res;
const commandsChanges = res.commands_changes;
......@@ -161,8 +178,11 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const votesBlock = $('.js-awards-block').eq(0);
loadAwardsHandler()
.then((awardsHandler) => {
awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
.then(awardsHandler => {
awardsHandler.addAwardToEmojiBar(
votesBlock,
commandsChanges.emoji_award,
);
awardsHandler.scrollToAwards();
})
.catch(() => {
......@@ -174,7 +194,10 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
}
if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
if (
commandsChanges.spend_time != null ||
commandsChanges.time_estimate != null
) {
sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
}
}
......@@ -192,11 +215,17 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
if (resp.notes && resp.notes.length) {
const { notesById } = getters;
resp.notes.forEach((note) => {
resp.notes.forEach(note => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
} else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
} else if (
note.type === constants.DISCUSSION_NOTE ||
note.type === constants.DIFF_NOTE
) {
const discussion = utils.findNoteObjectById(
state.notes,
note.discussion_id,
);
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
......@@ -219,9 +248,12 @@ export const poll = ({ commit, state, getters }) => {
resource: service,
method: 'poll',
data: state,
successCallback: resp => resp.json()
successCallback: resp =>
resp
.json()
.then(data => pollSuccessCallBack(data, commit, state, getters)),
errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
errorCallback: () =>
Flash('Something went wrong while fetching latest comments.'),
});
if (!Visibility.hidden()) {
......@@ -248,15 +280,22 @@ export const restartPolling = () => {
};
export const fetchData = ({ commit, state, getters }) => {
const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt };
const requestData = {
endpoint: state.notesData.notesPath,
lastFetchedAt: state.lastFetchedAt,
};
service.poll(requestData)
service
.poll(requestData)
.then(resp => resp.json)
.then(data => pollSuccessCallBack(data, commit, state, getters))
.catch(() => Flash('Something went wrong while fetching latest comments.'));
};
export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, noteId }) => {
export const toggleAward = (
{ commit, state, getters, dispatch },
{ awardName, noteId },
) => {
commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
};
......
......@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
export const getUserDataByProp = state => prop =>
state.userData && state.userData[prop];
export const notesById = state => state.notes.reduce((acc, note) => {
export const notesById = state =>
state.notes.reduce((acc, note) => {
note.notes.every(n => Object.assign(acc, { [n.id]: n }));
return acc;
}, {});
}, {});
const reverseNotes = array => array.slice(0).reverse();
const isLastNote = (note, state) => !note.system &&
state.userData && note.author &&
const isLastNote = (note, state) =>
!note.system &&
state.userData &&
note.author &&
note.author.id === state.userData.id;
export const getCurrentUserLastNote = state => _.flatten(
reverseNotes(state.notes)
.map(note => reverseNotes(note.notes)),
export const getCurrentUserLastNote = state =>
_.flatten(
reverseNotes(state.notes).map(note => reverseNotes(note.notes)),
).find(el => isLastNote(el, state));
export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes)
.find(el => isLastNote(el, state));
export const getDiscussionLastNote = state => discussion =>
reverseNotes(discussion.notes).find(el => isLastNote(el, state));
export const discussionCount = (state) => {
export const discussionCount = state => {
const discussions = state.notes.filter(n => !n.individual_note);
return discussions.length;
......@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => {
return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
};
export const resolvedDiscussionsById = (state) => {
export const resolvedDiscussionsById = state => {
const map = {};
state.notes.forEach((n) => {
state.notes.forEach(n => {
if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system);
......
......@@ -7,7 +7,7 @@ export default {
[types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note;
const [exists] = state.notes.filter(n => n.id === note.discussion_id);
const isDiscussion = (type === constants.DISCUSSION_NOTE);
const isDiscussion = type === constants.DISCUSSION_NOTE;
if (!exists) {
const noteData = {
......@@ -63,13 +63,15 @@ export default {
const note = notes[i];
const children = note.notes;
if (children.length && !note.individual_note) { // remove placeholder from discussions
if (children.length && !note.individual_note) {
// remove placeholder from discussions
for (let j = children.length - 1; j >= 0; j -= 1) {
if (children[j].isPlaceholderNote) {
children.splice(j, 1);
}
}
} else if (note.isPlaceholderNote) { // remove placeholders from state root
} else if (note.isPlaceholderNote) {
// remove placeholders from state root
notes.splice(i, 1);
}
}
......@@ -89,10 +91,10 @@ export default {
[types.SET_INITIAL_NOTES](state, notesData) {
const notes = [];
notesData.forEach((note) => {
notesData.forEach(note => {
// To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => {
note.notes.forEach(n => {
notes.push({
...note,
notes: [n], // override notes array to only have one item to mimick individual_note
......@@ -103,7 +105,7 @@ export default {
notes.push({
...note,
expanded: (oldNote ? oldNote.expanded : note.expanded),
expanded: oldNote ? oldNote.expanded : note.expanded,
});
}
});
......@@ -128,7 +130,9 @@ export default {
notesArr.push({
individual_note: true,
isPlaceholderNote: true,
placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE,
placeholderType: data.isSystemNote
? constants.SYSTEM_NOTE
: constants.NOTE,
notes: [
{
body: data.noteBody,
......@@ -141,12 +145,16 @@ export default {
const { awardName, note } = data;
const { id, name, username } = state.userData;
const hasEmojiAwardedByCurrentUser = note.award_emoji
.filter(emoji => emoji.name === data.awardName && emoji.user.id === id);
const hasEmojiAwardedByCurrentUser = note.award_emoji.filter(
emoji => emoji.name === data.awardName && emoji.user.id === id,
);
if (hasEmojiAwardedByCurrentUser.length) {
// If current user has awarded this emoji, remove it.
note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1);
note.award_emoji.splice(
note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]),
1,
);
} else {
note.award_emoji.push({
name: awardName,
......
......@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
export const findNoteObjectById = (notes, id) =>
notes.filter(n => n.id === id)[0];
export const getQuickActionText = (note) => {
export const getQuickActionText = note => {
let text = 'Applying command';
const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const quickActions =
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter((command) => {
const executedCommands = quickActions.filter(command => {
const commandRegex = new RegExp(`/${command.name}`);
return commandRegex.test(note);
});
......@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
export const stripQuickActions = note =>
note.replace(REGEX_QUICK_ACTIONS, '').trim();
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