comments.js
658 lines
| 22.0 KiB
| application/javascript
|
JavascriptLexer
r1271 | // # Copyright (C) 2010-2017 RhodeCode GmbH | |||
r1 | // # | |||
// # This program is free software: you can redistribute it and/or modify | ||||
// # it under the terms of the GNU Affero General Public License, version 3 | ||||
// # (only), as published by the Free Software Foundation. | ||||
// # | ||||
// # This program is distributed in the hope that it will be useful, | ||||
// # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
// # GNU General Public License for more details. | ||||
// # | ||||
// # You should have received a copy of the GNU Affero General Public License | ||||
// # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
// # | ||||
// # This program is dual-licensed. If you wish to learn more about the | ||||
// # RhodeCode Enterprise Edition, including its added features, Support services, | ||||
// # and proprietary license terms, please see https://rhodecode.com/licenses/ | ||||
var firefoxAnchorFix = function() { | ||||
// hack to make anchor links behave properly on firefox, in our inline | ||||
// comments generation when comments are injected firefox is misbehaving | ||||
// when jumping to anchor links | ||||
if (location.href.indexOf('#') > -1) { | ||||
location.href += ''; | ||||
} | ||||
}; | ||||
// returns a node from given html; | ||||
var fromHTML = function(html){ | ||||
var _html = document.createElement('element'); | ||||
_html.innerHTML = html; | ||||
return _html; | ||||
}; | ||||
var tableTr = function(cls, body){ | ||||
var _el = document.createElement('div'); | ||||
var _body = $(body).attr('id'); | ||||
var comment_id = fromHTML(body).children[0].id.split('comment-')[1]; | ||||
var id = 'comment-tr-{0}'.format(comment_id); | ||||
var _html = ('<table><tbody><tr id="{0}" class="{1}">'+ | ||||
r696 | '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+ | |||
'<td></td>'+ | ||||
r1 | '<td></td>'+ | |||
'<td></td>'+ | ||||
'<td>{2}</td>'+ | ||||
'</tr></tbody></table>').format(id, cls, body); | ||||
$(_el).html(_html); | ||||
return _el.children[0].children[0].children[0]; | ||||
}; | ||||
function bindDeleteCommentButtons() { | ||||
$('.delete-comment').one('click', function() { | ||||
var comment_id = $(this).data("comment-id"); | ||||
if (comment_id){ | ||||
deleteComment(comment_id); | ||||
} | ||||
}); | ||||
} | ||||
var deleteComment = function(comment_id) { | ||||
var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id); | ||||
var postData = { | ||||
'_method': 'delete', | ||||
'csrf_token': CSRF_TOKEN | ||||
}; | ||||
var success = function(o) { | ||||
window.location.reload(); | ||||
}; | ||||
ajaxPOST(url, postData, success); | ||||
}; | ||||
r696 | ||||
var bindToggleButtons = function() { | ||||
$('.comment-toggle').on('click', function() { | ||||
$(this).parent().nextUntil('tr.line').toggle('inline-comments'); | ||||
}); | ||||
}; | ||||
r1 | var linkifyComments = function(comments) { | |||
r1143 | /* TODO: dan: remove this - it should no longer needed */ | |||
r1 | for (var i = 0; i < comments.length; i++) { | |||
var comment_id = $(comments[i]).data('comment-id'); | ||||
var prev_comment_id = $(comments[i - 1]).data('comment-id'); | ||||
var next_comment_id = $(comments[i + 1]).data('comment-id'); | ||||
// place next/prev links | ||||
if (prev_comment_id) { | ||||
$('#prev_c_' + comment_id).show(); | ||||
$('#prev_c_' + comment_id + " a.arrow_comment_link").attr( | ||||
'href', '#comment-' + prev_comment_id).removeClass('disabled'); | ||||
} | ||||
if (next_comment_id) { | ||||
$('#next_c_' + comment_id).show(); | ||||
$('#next_c_' + comment_id + " a.arrow_comment_link").attr( | ||||
'href', '#comment-' + next_comment_id).removeClass('disabled'); | ||||
} | ||||
// place a first link to the total counter | ||||
if (i === 0) { | ||||
$('#inline-comments-counter').attr('href', '#comment-' + comment_id); | ||||
} | ||||
} | ||||
}; | ||||
r1143 | ||||
r1 | ||||
/* Comment form for main and inline comments */ | ||||
var CommentForm = (function() { | ||||
"use strict"; | ||||
function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) { | ||||
this.withLineNo = function(selector) { | ||||
var lineNo = this.lineNo; | ||||
if (lineNo === undefined) { | ||||
return selector | ||||
} else { | ||||
return selector + '_' + lineNo; | ||||
} | ||||
}; | ||||
this.commitId = commitId; | ||||
this.pullRequestId = pullRequestId; | ||||
this.lineNo = lineNo; | ||||
this.initAutocompleteActions = initAutocompleteActions; | ||||
this.previewButton = this.withLineNo('#preview-btn'); | ||||
this.previewContainer = this.withLineNo('#preview-container'); | ||||
this.previewBoxSelector = this.withLineNo('#preview-box'); | ||||
this.editButton = this.withLineNo('#edit-btn'); | ||||
this.editContainer = this.withLineNo('#edit-container'); | ||||
this.cancelButton = this.withLineNo('#cancel-btn'); | ||||
this.statusChange = '#change_status'; | ||||
this.cmBox = this.withLineNo('#text'); | ||||
this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions); | ||||
this.submitForm = formElement; | ||||
this.submitButton = $(this.submitForm).find('input[type="submit"]'); | ||||
this.submitButtonText = this.submitButton.val(); | ||||
this.previewUrl = pyroutes.url('changeset_comment_preview', | ||||
{'repo_name': templateContext.repo_name}); | ||||
// based on commitId, or pullReuqestId decide where do we submit | ||||
// out data | ||||
if (this.commitId){ | ||||
this.submitUrl = pyroutes.url('changeset_comment', | ||||
{'repo_name': templateContext.repo_name, | ||||
'revision': this.commitId}); | ||||
} else if (this.pullRequestId) { | ||||
this.submitUrl = pyroutes.url('pullrequest_comment', | ||||
{'repo_name': templateContext.repo_name, | ||||
'pull_request_id': this.pullRequestId}); | ||||
} else { | ||||
throw new Error( | ||||
'CommentForm requires pullRequestId, or commitId to be specified.') | ||||
} | ||||
this.getCmInstance = function(){ | ||||
return this.cm | ||||
}; | ||||
var self = this; | ||||
this.getCommentStatus = function() { | ||||
return $(this.submitForm).find(this.statusChange).val(); | ||||
}; | ||||
this.isAllowedToSubmit = function() { | ||||
return !$(this.submitButton).prop('disabled'); | ||||
}; | ||||
this.initStatusChangeSelector = function(){ | ||||
var formatChangeStatus = function(state, escapeMarkup) { | ||||
var originalOption = state.element; | ||||
return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' + | ||||
'<span>' + escapeMarkup(state.text) + '</span>'; | ||||
}; | ||||
var formatResult = function(result, container, query, escapeMarkup) { | ||||
return formatChangeStatus(result, escapeMarkup); | ||||
}; | ||||
var formatSelection = function(data, container, escapeMarkup) { | ||||
return formatChangeStatus(data, escapeMarkup); | ||||
}; | ||||
$(this.submitForm).find(this.statusChange).select2({ | ||||
r325 | placeholder: _gettext('Status Review'), | |||
r1 | formatResult: formatResult, | |||
formatSelection: formatSelection, | ||||
containerCssClass: "drop-menu status_box_menu", | ||||
dropdownCssClass: "drop-menu-dropdown", | ||||
dropdownAutoWidth: true, | ||||
minimumResultsForSearch: -1 | ||||
}); | ||||
$(this.submitForm).find(this.statusChange).on('change', function() { | ||||
var status = self.getCommentStatus(); | ||||
if (status && !self.lineNo) { | ||||
$(self.submitButton).prop('disabled', false); | ||||
} | ||||
//todo, fix this name | ||||
r325 | var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status); | |||
r1 | self.cm.setOption('placeholder', placeholderText); | |||
}) | ||||
}; | ||||
// reset the comment form into it's original state | ||||
this.resetCommentFormState = function(content) { | ||||
content = content || ''; | ||||
$(this.editContainer).show(); | ||||
r1281 | $(this.editButton).parent().addClass('active'); | |||
r1 | ||||
$(this.previewContainer).hide(); | ||||
r1281 | $(this.previewButton).parent().removeClass('active'); | |||
r1 | ||||
this.setActionButtonsDisabled(true); | ||||
self.cm.setValue(content); | ||||
self.cm.setOption("readOnly", false); | ||||
}; | ||||
this.submitAjaxPOST = function(url, postData, successHandler, failHandler) { | ||||
failHandler = failHandler || function() {}; | ||||
var postData = toQueryString(postData); | ||||
var request = $.ajax({ | ||||
url: url, | ||||
type: 'POST', | ||||
data: postData, | ||||
headers: {'X-PARTIAL-XHR': true} | ||||
}) | ||||
.done(function(data) { | ||||
successHandler(data); | ||||
}) | ||||
.fail(function(data, textStatus, errorThrown){ | ||||
alert( | ||||
"Error while submitting comment.\n" + | ||||
"Error code {0} ({1}).".format(data.status, data.statusText)); | ||||
failHandler() | ||||
}); | ||||
return request; | ||||
}; | ||||
// overwrite a submitHandler, we need to do it for inline comments | ||||
this.setHandleFormSubmit = function(callback) { | ||||
this.handleFormSubmit = callback; | ||||
}; | ||||
// default handler for for submit for main comments | ||||
this.handleFormSubmit = function() { | ||||
var text = self.cm.getValue(); | ||||
var status = self.getCommentStatus(); | ||||
if (text === "" && !status) { | ||||
return; | ||||
} | ||||
var excludeCancelBtn = false; | ||||
var submitEvent = true; | ||||
self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent); | ||||
self.cm.setOption("readOnly", true); | ||||
var postData = { | ||||
'text': text, | ||||
'changeset_status': status, | ||||
'csrf_token': CSRF_TOKEN | ||||
}; | ||||
var submitSuccessCallback = function(o) { | ||||
if (status) { | ||||
location.reload(true); | ||||
} else { | ||||
$('#injected_page_comments').append(o.rendered_text); | ||||
self.resetCommentFormState(); | ||||
bindDeleteCommentButtons(); | ||||
timeagoActivate(); | ||||
} | ||||
}; | ||||
var submitFailCallback = function(){ | ||||
self.resetCommentFormState(text) | ||||
}; | ||||
self.submitAjaxPOST( | ||||
self.submitUrl, postData, submitSuccessCallback, submitFailCallback); | ||||
}; | ||||
this.previewSuccessCallback = function(o) { | ||||
$(self.previewBoxSelector).html(o); | ||||
$(self.previewBoxSelector).removeClass('unloaded'); | ||||
r1281 | // swap buttons, making preview active | |||
$(self.previewButton).parent().addClass('active'); | ||||
$(self.editButton).parent().removeClass('active'); | ||||
r1 | ||||
// unlock buttons | ||||
self.setActionButtonsDisabled(false); | ||||
}; | ||||
this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) { | ||||
excludeCancelBtn = excludeCancelBtn || false; | ||||
submitEvent = submitEvent || false; | ||||
$(this.editButton).prop('disabled', state); | ||||
$(this.previewButton).prop('disabled', state); | ||||
if (!excludeCancelBtn) { | ||||
$(this.cancelButton).prop('disabled', state); | ||||
} | ||||
var submitState = state; | ||||
if (!submitEvent && this.getCommentStatus() && !this.lineNo) { | ||||
// if the value of commit review status is set, we allow | ||||
// submit button, but only on Main form, lineNo means inline | ||||
submitState = false | ||||
} | ||||
$(this.submitButton).prop('disabled', submitState); | ||||
if (submitEvent) { | ||||
r325 | $(this.submitButton).val(_gettext('Submitting...')); | |||
r1 | } else { | |||
$(this.submitButton).val(this.submitButtonText); | ||||
} | ||||
}; | ||||
// lock preview/edit/submit buttons on load, but exclude cancel button | ||||
var excludeCancelBtn = true; | ||||
this.setActionButtonsDisabled(true, excludeCancelBtn); | ||||
// anonymous users don't have access to initialized CM instance | ||||
if (this.cm !== undefined){ | ||||
this.cm.on('change', function(cMirror) { | ||||
if (cMirror.getValue() === "") { | ||||
self.setActionButtonsDisabled(true, excludeCancelBtn) | ||||
} else { | ||||
self.setActionButtonsDisabled(false, excludeCancelBtn) | ||||
} | ||||
}); | ||||
} | ||||
$(this.editButton).on('click', function(e) { | ||||
e.preventDefault(); | ||||
r1281 | $(self.previewButton).parent().removeClass('active'); | |||
r1 | $(self.previewContainer).hide(); | |||
r1281 | ||||
$(self.editButton).parent().addClass('active'); | ||||
r1 | $(self.editContainer).show(); | |||
}); | ||||
$(this.previewButton).on('click', function(e) { | ||||
e.preventDefault(); | ||||
var text = self.cm.getValue(); | ||||
if (text === "") { | ||||
return; | ||||
} | ||||
var postData = { | ||||
'text': text, | ||||
'renderer': DEFAULT_RENDERER, | ||||
'csrf_token': CSRF_TOKEN | ||||
}; | ||||
// lock ALL buttons on preview | ||||
self.setActionButtonsDisabled(true); | ||||
$(self.previewBoxSelector).addClass('unloaded'); | ||||
r325 | $(self.previewBoxSelector).html(_gettext('Loading ...')); | |||
r1281 | ||||
r1 | $(self.editContainer).hide(); | |||
$(self.previewContainer).show(); | ||||
// by default we reset state of comment preserving the text | ||||
var previewFailCallback = function(){ | ||||
self.resetCommentFormState(text) | ||||
}; | ||||
self.submitAjaxPOST( | ||||
r1281 | self.previewUrl, postData, self.previewSuccessCallback, | |||
previewFailCallback); | ||||
r1 | ||||
r1281 | $(self.previewButton).parent().addClass('active'); | |||
$(self.editButton).parent().removeClass('active'); | ||||
r1 | }); | |||
$(this.submitForm).submit(function(e) { | ||||
e.preventDefault(); | ||||
var allowedToSubmit = self.isAllowedToSubmit(); | ||||
if (!allowedToSubmit){ | ||||
return false; | ||||
} | ||||
self.handleFormSubmit(); | ||||
}); | ||||
} | ||||
return CommentForm; | ||||
})(); | ||||
r1157 | ||||
var CommentsController = function() { /* comments controller */ | ||||
var self = this; | ||||
this.cancelComment = function(node) { | ||||
var $node = $(node); | ||||
var $td = $node.closest('td'); | ||||
$node.closest('.comment-inline-form').removeClass('comment-inline-form-open'); | ||||
return false; | ||||
r1192 | }; | |||
r1157 | this.getLineNumber = function(node) { | |||
var $node = $(node); | ||||
return $node.closest('td').attr('data-line-number'); | ||||
r1192 | }; | |||
r1268 | this.scrollToComment = function(node, offset, outdated) { | |||
var outdated = outdated || false; | ||||
var klass = outdated ? 'div.comment-outdated' : 'div.comment-current'; | ||||
r1157 | if (!node) { | |||
node = $('.comment-selected'); | ||||
if (!node.length) { | ||||
node = $('comment-current') | ||||
} | ||||
} | ||||
r1268 | $comment = $(node).closest(klass); | |||
$comments = $(klass); | ||||
r1157 | ||||
$('.comment-selected').removeClass('comment-selected'); | ||||
r1268 | var nextIdx = $(klass).index($comment) + offset; | |||
r1157 | if (nextIdx >= $comments.length) { | |||
nextIdx = 0; | ||||
} | ||||
r1268 | var $next = $(klass).eq(nextIdx); | |||
r1157 | var $cb = $next.closest('.cb'); | |||
r1192 | $cb.removeClass('cb-collapsed'); | |||
r1157 | ||||
var $filediffCollapseState = $cb.closest('.filediff').prev(); | ||||
$filediffCollapseState.prop('checked', false); | ||||
$next.addClass('comment-selected'); | ||||
scrollToElement($next); | ||||
return false; | ||||
r1192 | }; | |||
r1157 | this.nextComment = function(node) { | |||
return self.scrollToComment(node, 1); | ||||
r1192 | }; | |||
r1157 | this.prevComment = function(node) { | |||
return self.scrollToComment(node, -1); | ||||
r1192 | }; | |||
r1268 | this.nextOutdatedComment = function(node) { | |||
return self.scrollToComment(node, 1, true); | ||||
}; | ||||
this.prevOutdatedComment = function(node) { | ||||
return self.scrollToComment(node, -1, true); | ||||
}; | ||||
r1157 | this.deleteComment = function(node) { | |||
if (!confirm(_gettext('Delete this comment?'))) { | ||||
return false; | ||||
} | ||||
var $node = $(node); | ||||
var $td = $node.closest('td'); | ||||
var $comment = $node.closest('.comment'); | ||||
var comment_id = $comment.attr('data-comment-id'); | ||||
var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id); | ||||
var postData = { | ||||
'_method': 'delete', | ||||
'csrf_token': CSRF_TOKEN | ||||
}; | ||||
$comment.addClass('comment-deleting'); | ||||
$comment.hide('fast'); | ||||
var success = function(response) { | ||||
$comment.remove(); | ||||
return false; | ||||
}; | ||||
var failure = function(data, textStatus, xhr) { | ||||
alert("error processing request: " + textStatus); | ||||
$comment.show('fast'); | ||||
$comment.removeClass('comment-deleting'); | ||||
return false; | ||||
}; | ||||
ajaxPOST(url, postData, success, failure); | ||||
r1192 | }; | |||
r1193 | this.toggleWideMode = function (node) { | |||
if ($('#content').hasClass('wrapper')) { | ||||
$('#content').removeClass("wrapper"); | ||||
$('#content').addClass("wide-mode-wrapper"); | ||||
$(node).addClass('btn-success'); | ||||
} else { | ||||
$('#content').removeClass("wide-mode-wrapper"); | ||||
$('#content').addClass("wrapper"); | ||||
$(node).removeClass('btn-success'); | ||||
} | ||||
return false; | ||||
}; | ||||
r1157 | this.toggleComments = function(node, show) { | |||
var $filediff = $(node).closest('.filediff'); | ||||
if (show === true) { | ||||
$filediff.removeClass('hide-comments'); | ||||
} else if (show === false) { | ||||
$filediff.find('.hide-line-comments').removeClass('hide-line-comments'); | ||||
$filediff.addClass('hide-comments'); | ||||
} else { | ||||
$filediff.find('.hide-line-comments').removeClass('hide-line-comments'); | ||||
$filediff.toggleClass('hide-comments'); | ||||
} | ||||
return false; | ||||
r1192 | }; | |||
r1157 | this.toggleLineComments = function(node) { | |||
self.toggleComments(node, true); | ||||
var $node = $(node); | ||||
$node.closest('tr').toggleClass('hide-line-comments'); | ||||
r1192 | }; | |||
r1157 | this.createComment = function(node) { | |||
var $node = $(node); | ||||
var $td = $node.closest('td'); | ||||
var $form = $td.find('.comment-inline-form'); | ||||
if (!$form.length) { | ||||
var tmpl = $('#cb-comment-inline-form-template').html(); | ||||
var $filediff = $node.closest('.filediff'); | ||||
$filediff.removeClass('hide-comments'); | ||||
var f_path = $filediff.attr('data-f-path'); | ||||
var lineno = self.getLineNumber(node); | ||||
tmpl = tmpl.format(f_path, lineno); | ||||
$form = $(tmpl); | ||||
var $comments = $td.find('.inline-comments'); | ||||
if (!$comments.length) { | ||||
$comments = $( | ||||
$('#cb-comments-inline-container-template').html()); | ||||
$td.append($comments); | ||||
} | ||||
$td.find('.cb-comment-add-button').before($form); | ||||
var pullRequestId = templateContext.pull_request_data.pull_request_id; | ||||
var commitId = templateContext.commit_data.commit_id; | ||||
var _form = $form[0]; | ||||
var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false); | ||||
var cm = commentForm.getCmInstance(); | ||||
// set a CUSTOM submit handler for inline comments. | ||||
commentForm.setHandleFormSubmit(function(o) { | ||||
var text = commentForm.cm.getValue(); | ||||
if (text === "") { | ||||
return; | ||||
} | ||||
if (lineno === undefined) { | ||||
alert('missing line !'); | ||||
return; | ||||
} | ||||
if (f_path === undefined) { | ||||
alert('missing file path !'); | ||||
return; | ||||
} | ||||
var excludeCancelBtn = false; | ||||
var submitEvent = true; | ||||
commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent); | ||||
commentForm.cm.setOption("readOnly", true); | ||||
var postData = { | ||||
'text': text, | ||||
'f_path': f_path, | ||||
'line': lineno, | ||||
'csrf_token': CSRF_TOKEN | ||||
}; | ||||
var submitSuccessCallback = function(json_data) { | ||||
$form.remove(); | ||||
try { | ||||
var html = json_data.rendered_text; | ||||
var lineno = json_data.line_no; | ||||
var target_id = json_data.target_id; | ||||
$comments.find('.cb-comment-add-button').before(html); | ||||
} catch (e) { | ||||
console.error(e); | ||||
} | ||||
// re trigger the linkification of next/prev navigation | ||||
linkifyComments($('.inline-comment-injected')); | ||||
timeagoActivate(); | ||||
bindDeleteCommentButtons(); | ||||
commentForm.setActionButtonsDisabled(false); | ||||
}; | ||||
var submitFailCallback = function(){ | ||||
commentForm.resetCommentFormState(text) | ||||
}; | ||||
commentForm.submitAjaxPOST( | ||||
commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback); | ||||
}); | ||||
setTimeout(function() { | ||||
// callbacks | ||||
if (cm !== undefined) { | ||||
r1281 | cm.setOption('placeholder', _gettext('Leave a comment on line {0}.').format(lineno)); | |||
r1157 | cm.focus(); | |||
r1281 | cm.refresh(); | |||
r1157 | } | |||
}, 10); | ||||
$.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({ | ||||
form: _form, | ||||
parent: $td[0], | ||||
lineno: lineno, | ||||
f_path: f_path} | ||||
); | ||||
} | ||||
$form.addClass('comment-inline-form-open'); | ||||
r1192 | }; | |||
r1157 | ||||
this.renderInlineComments = function(file_comments) { | ||||
show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true; | ||||
for (var i = 0; i < file_comments.length; i++) { | ||||
var box = file_comments[i]; | ||||
var target_id = $(box).attr('target_id'); | ||||
// actually comments with line numbers | ||||
var comments = box.children; | ||||
for (var j = 0; j < comments.length; j++) { | ||||
var data = { | ||||
'rendered_text': comments[j].outerHTML, | ||||
'line_no': $(comments[j]).attr('line'), | ||||
'target_id': target_id | ||||
}; | ||||
} | ||||
} | ||||
// since order of injection is random, we're now re-iterating | ||||
// from correct order and filling in links | ||||
linkifyComments($('.inline-comment-injected')); | ||||
bindDeleteCommentButtons(); | ||||
firefoxAnchorFix(); | ||||
}; | ||||
r1194 | }; | |||