comments.js
1645 lines
| 58.9 KiB
| application/javascript
|
JavascriptLexer
r4306 | // # Copyright (C) 2010-2020 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 += ''; | ||||
} | ||||
}; | ||||
r4633 | ||||
r1 | var linkifyComments = function(comments) { | |||
r1329 | var firstCommentId = null; | |||
if (comments) { | ||||
firstCommentId = $(comments[0]).data('comment-id'); | ||||
r1 | } | |||
r1329 | if (firstCommentId){ | |||
$('#inline-comments-counter').attr('href', '#comment-' + firstCommentId); | ||||
} | ||||
r1 | }; | |||
r1143 | ||||
r4633 | ||||
r1326 | var bindToggleButtons = function() { | |||
$('.comment-toggle').on('click', function() { | ||||
$(this).parent().nextUntil('tr.line').toggle('inline-comments'); | ||||
}); | ||||
}; | ||||
r1 | ||||
r2816 | ||||
var _submitAjaxPOST = function(url, postData, successHandler, failHandler) { | ||||
failHandler = failHandler || function() {}; | ||||
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) { | ||||
failHandler(data, textStatus, errorThrown) | ||||
}); | ||||
return request; | ||||
}; | ||||
r1 | /* Comment form for main and inline comments */ | |||
r1331 | (function(mod) { | |||
r1325 | ||||
r1331 | if (typeof exports == "object" && typeof module == "object") { | |||
// CommonJS | ||||
module.exports = mod(); | ||||
} | ||||
else { | ||||
// Plain browser env | ||||
(this || window).CommentForm = mod(); | ||||
} | ||||
r1325 | ||||
})(function() { | ||||
r1 | "use strict"; | |||
r4401 | function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) { | |||
r1325 | if (!(this instanceof CommentForm)) { | |||
r4401 | return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id); | |||
r1325 | } | |||
// bind the element instance to our Form | ||||
$(formElement).get(0).CommentForm = this; | ||||
r1 | ||||
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'); | ||||
r1324 | this.cancelButton = this.withLineNo('#cancel-btn'); | |||
this.commentType = this.withLineNo('#comment_type'); | ||||
r1 | ||||
r1325 | this.resolvesId = null; | |||
this.resolvesActionId = null; | ||||
r1445 | this.closesPr = '#close_pull_request'; | |||
r1324 | this.cmBox = this.withLineNo('#text'); | |||
r1362 | this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions); | |||
r1 | ||||
r1326 | this.statusChange = this.withLineNo('#change_status'); | |||
r1 | ||||
this.submitForm = formElement; | ||||
r4540 | ||||
this.submitButton = $(this.submitForm).find('.submit-comment-action'); | ||||
r1 | this.submitButtonText = this.submitButton.val(); | |||
r4540 | this.submitDraftButton = $(this.submitForm).find('.submit-draft-action'); | |||
this.submitDraftButtonText = this.submitDraftButton.val(); | ||||
r4401 | ||||
r1951 | this.previewUrl = pyroutes.url('repo_commit_comment_preview', | |||
{'repo_name': templateContext.repo_name, | ||||
'commit_id': templateContext.commit_data.commit_id}); | ||||
r1 | ||||
r4401 | if (edit){ | |||
r4540 | this.submitDraftButton.hide(); | |||
this.submitButtonText = _gettext('Update Comment'); | ||||
r4401 | $(this.commentType).prop('disabled', true); | |||
$(this.commentType).addClass('disabled'); | ||||
var editInfo = | ||||
r4404 | ''; | |||
r4401 | $(editInfo).insertBefore($(this.editButton).parent()); | |||
} | ||||
r1325 | if (resolvesCommentId){ | |||
this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId); | ||||
this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId); | ||||
$(this.commentType).prop('disabled', true); | ||||
$(this.commentType).addClass('disabled'); | ||||
r1326 | // disable select | |||
setTimeout(function() { | ||||
$(self.statusChange).select2('readonly', true); | ||||
}, 10); | ||||
r1325 | var resolvedInfo = ( | |||
r1344 | '<li class="resolve-action">' + | |||
r1325 | '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' + | |||
'<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' + | ||||
'</li>' | ||||
).format(resolvesCommentId, _gettext('resolve comment')); | ||||
$(resolvedInfo).insertAfter($(this.commentType).parent()); | ||||
} | ||||
// based on commitId, or pullRequestId decide where do we submit | ||||
r1 | // out data | |||
if (this.commitId){ | ||||
r4401 | var pyurl = 'repo_commit_comment_create'; | |||
if(edit){ | ||||
pyurl = 'repo_commit_comment_edit'; | ||||
} | ||||
this.submitUrl = pyroutes.url(pyurl, | ||||
r1 | {'repo_name': templateContext.repo_name, | |||
r4401 | 'commit_id': this.commitId, | |||
'comment_id': comment_id}); | ||||
r1951 | this.selfUrl = pyroutes.url('repo_commit', | |||
r1325 | {'repo_name': templateContext.repo_name, | |||
r1951 | 'commit_id': this.commitId}); | |||
r1 | ||||
} else if (this.pullRequestId) { | ||||
r4401 | var pyurl = 'pullrequest_comment_create'; | |||
if(edit){ | ||||
pyurl = 'pullrequest_comment_edit'; | ||||
} | ||||
this.submitUrl = pyroutes.url(pyurl, | ||||
r1 | {'repo_name': templateContext.repo_name, | |||
r4401 | 'pull_request_id': this.pullRequestId, | |||
'comment_id': comment_id}); | ||||
r1325 | this.selfUrl = pyroutes.url('pullrequest_show', | |||
{'repo_name': templateContext.repo_name, | ||||
'pull_request_id': this.pullRequestId}); | ||||
r1 | ||||
} else { | ||||
throw new Error( | ||||
'CommentForm requires pullRequestId, or commitId to be specified.') | ||||
} | ||||
r1326 | // FUNCTIONS and helpers | |||
var self = this; | ||||
this.isInline = function(){ | ||||
return this.lineNo && this.lineNo != 'general'; | ||||
}; | ||||
r1 | this.getCmInstance = function(){ | |||
return this.cm | ||||
}; | ||||
r1325 | this.setPlaceholder = function(placeholder) { | |||
var cm = this.getCmInstance(); | ||||
if (cm){ | ||||
cm.setOption('placeholder', placeholder); | ||||
} | ||||
}; | ||||
r1 | this.getCommentStatus = function() { | |||
return $(this.submitForm).find(this.statusChange).val(); | ||||
}; | ||||
r4540 | ||||
r1324 | this.getCommentType = function() { | |||
return $(this.submitForm).find(this.commentType).val(); | ||||
}; | ||||
r1325 | ||||
r4540 | this.getDraftState = function () { | |||
var submitterElem = $(this.submitForm).find('input[type="submit"].submitter'); | ||||
var data = $(submitterElem).data('isDraft'); | ||||
return data | ||||
} | ||||
r1325 | this.getResolvesId = function() { | |||
return $(this.submitForm).find(this.resolvesId).val() || null; | ||||
}; | ||||
r1445 | ||||
this.getClosePr = function() { | ||||
return $(this.submitForm).find(this.closesPr).val() || null; | ||||
}; | ||||
r1325 | this.markCommentResolved = function(resolvedCommentId){ | |||
r4633 | Rhodecode.comments.markCommentResolved(resolvedCommentId) | |||
r1325 | }; | |||
r1 | this.isAllowedToSubmit = function() { | |||
r4540 | var commentDisabled = $(this.submitButton).prop('disabled'); | |||
var draftDisabled = $(this.submitDraftButton).prop('disabled'); | ||||
return !commentDisabled && !draftDisabled; | ||||
r1 | }; | |||
this.initStatusChangeSelector = function(){ | ||||
var formatChangeStatus = function(state, escapeMarkup) { | ||||
var originalOption = state.element; | ||||
r3883 | var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text)); | |||
return tmpl | ||||
r1 | }; | |||
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 | ||||
}); | ||||
r4540 | ||||
r1 | $(this.submitForm).find(this.statusChange).on('change', function() { | |||
var status = self.getCommentStatus(); | ||||
r1445 | ||||
r1326 | if (status && !self.isInline()) { | |||
r1 | $(self.submitButton).prop('disabled', false); | |||
r4540 | $(self.submitDraftButton).prop('disabled', false); | |||
r1 | } | |||
r1325 | ||||
r325 | var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status); | |||
r1325 | self.setPlaceholder(placeholderText) | |||
r1 | }) | |||
}; | ||||
// 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); | ||||
r1327 | ||||
if (this.resolvesId) { | ||||
// destroy the resolve action | ||||
$(this.resolvesId).parent().remove(); | ||||
} | ||||
r1445 | // reset closingPR flag | |||
$('.close-pr-input').remove(); | ||||
r1327 | ||||
$(this.statusChange).select2('readonly', false); | ||||
r1 | }; | |||
r4540 | this.globalSubmitSuccessCallback = function(comment){ | |||
r1334 | // default behaviour is to call GLOBAL hook, if it's registered. | |||
if (window.commentFormGlobalSubmitSuccessCallback !== undefined){ | ||||
r4540 | commentFormGlobalSubmitSuccessCallback(comment); | |||
r1334 | } | |||
}; | ||||
r1331 | ||||
r1 | this.submitAjaxPOST = function(url, postData, successHandler, failHandler) { | |||
r2816 | return _submitAjaxPOST(url, postData, successHandler, failHandler); | |||
r1 | }; | |||
// overwrite a submitHandler, we need to do it for inline comments | ||||
this.setHandleFormSubmit = function(callback) { | ||||
this.handleFormSubmit = callback; | ||||
}; | ||||
r1331 | // overwrite a submitSuccessHandler | |||
this.setGlobalSubmitSuccessCallback = function(callback) { | ||||
this.globalSubmitSuccessCallback = callback; | ||||
}; | ||||
r1 | // default handler for for submit for main comments | |||
this.handleFormSubmit = function() { | ||||
var text = self.cm.getValue(); | ||||
var status = self.getCommentStatus(); | ||||
r1324 | var commentType = self.getCommentType(); | |||
r4540 | var isDraft = self.getDraftState(); | |||
r1325 | var resolvesCommentId = self.getResolvesId(); | |||
r1445 | var closePullRequest = self.getClosePr(); | |||
r1 | ||||
if (text === "" && !status) { | ||||
return; | ||||
} | ||||
var excludeCancelBtn = false; | ||||
var submitEvent = true; | ||||
self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent); | ||||
self.cm.setOption("readOnly", true); | ||||
r1326 | ||||
r1 | var postData = { | |||
'text': text, | ||||
'changeset_status': status, | ||||
r1324 | 'comment_type': commentType, | |||
r1 | 'csrf_token': CSRF_TOKEN | |||
}; | ||||
r1445 | ||||
if (resolvesCommentId) { | ||||
r1325 | postData['resolves_comment_id'] = resolvesCommentId; | |||
} | ||||
r1331 | ||||
r1445 | if (closePullRequest) { | |||
postData['close_pull_request'] = true; | ||||
} | ||||
r4543 | // submitSuccess for general comments | |||
var submitSuccessCallback = function(json_data) { | ||||
r1334 | // reload page if we change status for single commit. | |||
if (status && self.commitId) { | ||||
r1 | location.reload(true); | |||
} else { | ||||
r4543 | // inject newly created comments, json_data is {<comment_id>: {}} | |||
r4574 | Rhodecode.comments.attachGeneralComment(json_data) | |||
r4543 | ||||
r1 | self.resetCommentFormState(); | |||
timeagoActivate(); | ||||
r4026 | tooltipActivate(); | |||
r1325 | ||||
r1326 | // mark visually which comment was resolved | |||
r1325 | if (resolvesCommentId) { | |||
r1326 | self.markCommentResolved(resolvesCommentId); | |||
r1325 | } | |||
r1 | } | |||
r1331 | ||||
// run global callback on submit | ||||
r4540 | self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id}); | |||
r1331 | ||||
r1 | }; | |||
r4311 | var submitFailCallback = function(jqXHR, textStatus, errorThrown) { | |||
var prefix = "Error while submitting comment.\n" | ||||
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); | ||||
ajaxErrorSwal(message); | ||||
r1325 | self.resetCommentFormState(text); | |||
r1 | }; | |||
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; | ||||
r1444 | if (!submitEvent && this.getCommentStatus() && !self.isInline()) { | |||
r1 | // if the value of commit review status is set, we allow | |||
r1444 | // submit button, but only on Main form, isInline means inline | |||
r1 | submitState = false | |||
} | ||||
r1445 | ||||
r1 | $(this.submitButton).prop('disabled', submitState); | |||
r4540 | $(this.submitDraftButton).prop('disabled', submitState); | |||
r1 | if (submitEvent) { | |||
r4540 | var isDraft = self.getDraftState(); | |||
if (isDraft) { | ||||
$(this.submitDraftButton).val(_gettext('Saving Draft...')); | ||||
} else { | ||||
$(this.submitButton).val(_gettext('Submitting...')); | ||||
} | ||||
r1 | } else { | |||
$(this.submitButton).val(this.submitButtonText); | ||||
r4540 | $(this.submitDraftButton).val(this.submitDraftButtonText); | |||
r1 | } | |||
}; | ||||
// 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, | ||||
r1325 | 'renderer': templateContext.visual.default_renderer, | |||
r1 | '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 | ||||
r4311 | var previewFailCallback = function(jqXHR, textStatus, errorThrown) { | |||
var prefix = "Error while preview of comment.\n" | ||||
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); | ||||
ajaxErrorSwal(message); | ||||
r1 | 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; | ||||
} | ||||
r4540 | ||||
r1 | self.handleFormSubmit(); | |||
}); | ||||
} | ||||
return CommentForm; | ||||
r1325 | }); | |||
r1157 | ||||
r4408 | /* selector for comment versions */ | |||
var initVersionSelector = function(selector, initialData) { | ||||
var formatResult = function(result, container, query, escapeMarkup) { | ||||
return renderTemplate('commentVersion', { | ||||
show_disabled: true, | ||||
version: result.comment_version, | ||||
user_name: result.comment_author_username, | ||||
gravatar_url: result.comment_author_gravatar, | ||||
size: 16, | ||||
timeago_component: result.comment_created_on, | ||||
}) | ||||
}; | ||||
$(selector).select2({ | ||||
placeholder: "Edited", | ||||
containerCssClass: "drop-menu-comment-history", | ||||
dropdownCssClass: "drop-menu-dropdown", | ||||
dropdownAutoWidth: true, | ||||
minimumResultsForSearch: -1, | ||||
data: initialData, | ||||
formatResult: formatResult, | ||||
}); | ||||
$(selector).on('select2-selecting', function (e) { | ||||
// hide the mast as we later do preventDefault() | ||||
$("#select2-drop-mask").click(); | ||||
e.preventDefault(); | ||||
e.choice.action(); | ||||
}); | ||||
$(selector).on("select2-open", function() { | ||||
timeagoActivate(); | ||||
}); | ||||
}; | ||||
r1325 | /* comments controller */ | |||
var CommentsController = function() { | ||||
var mainComment = '#text'; | ||||
r1157 | var self = this; | |||
r4482 | this.showVersion = function (comment_id, comment_history_id) { | |||
r4401 | ||||
var historyViewUrl = pyroutes.url( | ||||
'repo_commit_comment_history_view', | ||||
{ | ||||
'repo_name': templateContext.repo_name, | ||||
r4717 | 'commit_id': null, // We don't need to check the commit data here... | |||
'comment_id': comment_id, | ||||
r4401 | 'comment_history_id': comment_history_id, | |||
} | ||||
); | ||||
successRenderCommit = function (data) { | ||||
r4402 | SwalNoAnimation.fire({ | |||
r4401 | html: data, | |||
title: '', | ||||
}); | ||||
}; | ||||
failRenderCommit = function () { | ||||
r4402 | SwalNoAnimation.fire({ | |||
r4404 | html: 'Error while loading comment history', | |||
r4401 | title: '', | |||
}); | ||||
}; | ||||
_submitAjaxPOST( | ||||
r4408 | historyViewUrl, {'csrf_token': CSRF_TOKEN}, | |||
successRenderCommit, | ||||
r4401 | failRenderCommit | |||
); | ||||
r4482 | }; | |||
r4401 | ||||
r1157 | this.getLineNumber = function(node) { | |||
var $node = $(node); | ||||
r2642 | var lineNo = $node.closest('td').attr('data-line-no'); | |||
r2250 | if (lineNo === undefined && $node.data('commentInline')){ | |||
lineNo = $node.data('commentLineNo') | ||||
} | ||||
return lineNo | ||||
r1192 | }; | |||
r1268 | this.scrollToComment = function(node, offset, outdated) { | |||
r1386 | if (offset === undefined) { | |||
offset = 0; | ||||
} | ||||
r1268 | 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') | ||||
} | ||||
} | ||||
r4140 | ||||
r1344 | $wrapper = $(node).closest('div.comment'); | |||
r1157 | ||||
r1344 | // show hidden comment when referenced. | |||
if (!$wrapper.is(':visible')){ | ||||
$wrapper.show(); | ||||
} | ||||
r4140 | $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); | |||
r1385 | ||||
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); | ||||
}; | ||||
r4543 | this.cancelComment = function (node) { | |||
var $node = $(node); | ||||
var edit = $(this).attr('edit'); | ||||
var $inlineComments = $node.closest('div.inline-comments'); | ||||
if (edit) { | ||||
var $general_comments = null; | ||||
if (!$inlineComments.length) { | ||||
$general_comments = $('#comments'); | ||||
var $comment = $general_comments.parent().find('div.comment:hidden'); | ||||
// show hidden general comment form | ||||
$('#cb-comment-general-form-placeholder').show(); | ||||
} else { | ||||
var $comment = $inlineComments.find('div.comment:hidden'); | ||||
} | ||||
$comment.show(); | ||||
} | ||||
var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper') | ||||
$replyWrapper.removeClass('comment-form-active'); | ||||
var lastComment = $inlineComments.find('.comment-inline').last(); | ||||
if ($(lastComment).hasClass('comment-outdated')) { | ||||
$replyWrapper.hide(); | ||||
} | ||||
$node.closest('.comment-inline-form').remove(); | ||||
return false; | ||||
}; | ||||
r4312 | this._deleteComment = function(node) { | |||
r1157 | var $node = $(node); | |||
var $td = $node.closest('td'); | ||||
var $comment = $node.closest('.comment'); | ||||
r4540 | var comment_id = $($comment).data('commentId'); | |||
var isDraft = $($comment).data('commentDraft'); | ||||
r4555 | ||||
var pullRequestId = templateContext.pull_request_data.pull_request_id; | ||||
var commitId = templateContext.commit_data.commit_id; | ||||
if (pullRequestId) { | ||||
var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId}) | ||||
} else if (commitId) { | ||||
var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId}) | ||||
} | ||||
r1157 | var postData = { | |||
'csrf_token': CSRF_TOKEN | ||||
}; | ||||
$comment.addClass('comment-deleting'); | ||||
$comment.hide('fast'); | ||||
var success = function(response) { | ||||
$comment.remove(); | ||||
r4482 | ||||
if (window.updateSticky !== undefined) { | ||||
// potentially our comments change the active window size, so we | ||||
// notify sticky elements | ||||
updateSticky() | ||||
} | ||||
r4540 | if (window.refreshAllComments !== undefined && !isDraft) { | |||
r4482 | // if we have this handler, run it, and refresh all comments boxes | |||
refreshAllComments() | ||||
} | ||||
r4562 | else if (window.refreshDraftComments !== undefined && isDraft) { | |||
// if we have this handler, run it, and refresh all comments boxes | ||||
refreshDraftComments(); | ||||
} | ||||
r1157 | return false; | |||
}; | ||||
r4482 | ||||
r4311 | var failure = function(jqXHR, textStatus, errorThrown) { | |||
var prefix = "Error while deleting this comment.\n" | ||||
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); | ||||
ajaxErrorSwal(message); | ||||
r1157 | $comment.show('fast'); | |||
$comment.removeClass('comment-deleting'); | ||||
return false; | ||||
}; | ||||
ajaxPOST(url, postData, success, failure); | ||||
r4482 | ||||
r4312 | } | |||
this.deleteComment = function(node) { | ||||
var $comment = $(node).closest('.comment'); | ||||
var comment_id = $comment.attr('data-comment-id'); | ||||
r4339 | SwalNoAnimation.fire({ | |||
r4312 | title: 'Delete this comment?', | |||
icon: 'warning', | ||||
showCancelButton: true, | ||||
r4313 | confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id), | |||
r4339 | ||||
r4312 | }).then(function(result) { | |||
if (result.value) { | ||||
self._deleteComment(node); | ||||
} | ||||
}) | ||||
r1192 | }; | |||
r4540 | this._finalizeDrafts = function(commentIds) { | |||
r4549 | ||||
r4555 | var pullRequestId = templateContext.pull_request_data.pull_request_id; | |||
var commitId = templateContext.commit_data.commit_id; | ||||
if (pullRequestId) { | ||||
var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId}) | ||||
} else if (commitId) { | ||||
var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId}) | ||||
} | ||||
// remove the drafts so we can lock them before submit. | ||||
r4549 | $.each(commentIds, function(idx, val){ | |||
$('#comment-{0}'.format(val)).remove(); | ||||
}) | ||||
var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN}; | ||||
var submitSuccessCallback = function(json_data) { | ||||
self.attachInlineComment(json_data); | ||||
if (window.refreshDraftComments !== undefined) { | ||||
// if we have this handler, run it, and refresh all comments boxes | ||||
refreshDraftComments() | ||||
} | ||||
r4683 | if (window.refreshAllComments !== undefined) { | |||
// if we have this handler, run it, and refresh all comments boxes | ||||
refreshAllComments() | ||||
} | ||||
r4549 | return false; | |||
}; | ||||
ajaxPOST(url, postData, submitSuccessCallback) | ||||
r4540 | } | |||
r4562 | this.finalizeDrafts = function(commentIds, callback) { | |||
r4540 | ||||
SwalNoAnimation.fire({ | ||||
r4543 | title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length), | |||
r4540 | icon: 'warning', | |||
showCancelButton: true, | ||||
r4549 | confirmButtonText: _gettext('Yes'), | |||
r4540 | ||||
}).then(function(result) { | ||||
if (result.value) { | ||||
r4562 | if (callback !== undefined) { | |||
callback(result) | ||||
} | ||||
r4540 | self._finalizeDrafts(commentIds); | |||
} | ||||
}) | ||||
}; | ||||
r1193 | this.toggleWideMode = function (node) { | |||
r4543 | ||||
r1193 | if ($('#content').hasClass('wrapper')) { | |||
$('#content').removeClass("wrapper"); | ||||
$('#content').addClass("wide-mode-wrapper"); | ||||
$(node).addClass('btn-success'); | ||||
r3642 | return true | |||
r1193 | } else { | |||
$('#content').removeClass("wide-mode-wrapper"); | ||||
$('#content').addClass("wrapper"); | ||||
$(node).removeClass('btn-success'); | ||||
r3642 | return false | |||
r1193 | } | |||
r3642 | ||||
r1193 | }; | |||
r4543 | /** | |||
* Turn off/on all comments in file diff | ||||
*/ | ||||
this.toggleDiffComments = function(node) { | ||||
// Find closes filediff container | ||||
r1157 | var $filediff = $(node).closest('.filediff'); | |||
r4543 | if ($(node).hasClass('toggle-on')) { | |||
var show = false; | ||||
} else if ($(node).hasClass('toggle-off')) { | ||||
var show = true; | ||||
} | ||||
// Toggle each individual comment block, so we can un-toggle single ones | ||||
$.each($filediff.find('.toggle-comment-action'), function(idx, val) { | ||||
self.toggleLineComments($(val), show) | ||||
}) | ||||
// since we change the height of the diff container that has anchor points for upper | ||||
// sticky header, we need to tell it to re-calculate those | ||||
if (window.updateSticky !== undefined) { | ||||
// potentially our comments change the active window size, so we | ||||
// notify sticky elements | ||||
updateSticky() | ||||
} | ||||
return false; | ||||
} | ||||
this.toggleLineComments = function(node, show) { | ||||
var trElem = $(node).closest('tr') | ||||
r1157 | if (show === true) { | |||
r4543 | // mark outdated comments as visible before the toggle; | |||
$(trElem).find('.comment-outdated').show(); | ||||
$(trElem).removeClass('hide-line-comments'); | ||||
r1157 | } else if (show === false) { | |||
r4543 | $(trElem).find('.comment-outdated').hide(); | |||
$(trElem).addClass('hide-line-comments'); | ||||
r1157 | } else { | |||
r4543 | // mark outdated comments as visible before the toggle; | |||
$(trElem).find('.comment-outdated').show(); | ||||
$(trElem).toggleClass('hide-line-comments'); | ||||
r1157 | } | |||
r4482 | ||||
// since we change the height of the diff container that has anchor points for upper | ||||
// sticky header, we need to tell it to re-calculate those | ||||
if (window.updateSticky !== undefined) { | ||||
// potentially our comments change the active window size, so we | ||||
// notify sticky elements | ||||
updateSticky() | ||||
} | ||||
r1192 | }; | |||
r4401 | this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){ | |||
r1326 | var pullRequestId = templateContext.pull_request_data.pull_request_id; | |||
var commitId = templateContext.commit_data.commit_id; | ||||
var commentForm = new CommentForm( | ||||
r4401 | formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id); | |||
r1326 | var cm = commentForm.getCmInstance(); | |||
if (resolvesCommentId){ | ||||
r4482 | placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId); | |||
r1326 | } | |||
setTimeout(function() { | ||||
// callbacks | ||||
if (cm !== undefined) { | ||||
commentForm.setPlaceholder(placeholderText); | ||||
if (commentForm.isInline()) { | ||||
cm.focus(); | ||||
cm.refresh(); | ||||
} | ||||
} | ||||
}, 10); | ||||
// trigger scrolldown to the resolve comment, since it might be away | ||||
// from the clicked | ||||
if (resolvesCommentId){ | ||||
var actionNode = $(commentForm.resolvesActionId).offset(); | ||||
setTimeout(function() { | ||||
if (actionNode) { | ||||
$('body, html').animate({scrollTop: actionNode.top}, 10); | ||||
} | ||||
}, 100); | ||||
} | ||||
r3973 | // add dropzone support | |||
var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) { | ||||
var renderer = templateContext.visual.default_renderer; | ||||
if (renderer == 'rst') { | ||||
var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl); | ||||
if (isRendered){ | ||||
attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl); | ||||
} | ||||
} else if (renderer == 'markdown') { | ||||
var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl); | ||||
if (isRendered){ | ||||
attachmentUrl = '!' + attachmentUrl; | ||||
} | ||||
} else { | ||||
var attachmentUrl = '{}'.format(attachmentStoreUrl); | ||||
} | ||||
cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine())); | ||||
return false; | ||||
}; | ||||
//see: https://www.dropzonejs.com/#configuration | ||||
var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload', | ||||
{'repo_name': templateContext.repo_name, | ||||
'commit_id': templateContext.commit_data.commit_id}) | ||||
r3989 | var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0); | |||
if (previewTmpl !== undefined){ | ||||
var selectLink = $(formElement).find('.pick-attachment').get(0); | ||||
$(formElement).find('.comment-attachment-uploader').dropzone({ | ||||
url: storeUrl, | ||||
headers: {"X-CSRF-Token": CSRF_TOKEN}, | ||||
paramName: function () { | ||||
return "attachment" | ||||
}, // The name that will be used to transfer the file | ||||
clickable: selectLink, | ||||
parallelUploads: 1, | ||||
maxFiles: 10, | ||||
maxFilesize: templateContext.attachment_store.max_file_size_mb, | ||||
uploadMultiple: false, | ||||
autoProcessQueue: true, // if false queue will not be processed automatically. | ||||
createImageThumbnails: false, | ||||
previewTemplate: previewTmpl.innerHTML, | ||||
r3973 | ||||
r3989 | accept: function (file, done) { | |||
done(); | ||||
}, | ||||
init: function () { | ||||
r3973 | ||||
r3989 | this.on("sending", function (file, xhr, formData) { | |||
$(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide(); | ||||
$(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show(); | ||||
}); | ||||
r3973 | ||||
r3989 | this.on("success", function (file, response) { | |||
$(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show(); | ||||
$(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide(); | ||||
r3973 | ||||
r3989 | var isRendered = false; | |||
var ext = file.name.split('.').pop(); | ||||
var imageExts = templateContext.attachment_store.image_ext; | ||||
if (imageExts.indexOf(ext) !== -1){ | ||||
isRendered = true; | ||||
} | ||||
r3973 | ||||
r3989 | insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered) | |||
}); | ||||
r3973 | ||||
r3989 | this.on("error", function (file, errorMessage, xhr) { | |||
$(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide(); | ||||
r3973 | ||||
r3989 | var error = null; | |||
r3973 | ||||
r3989 | if (xhr !== undefined){ | |||
var httpStatus = xhr.status + " " + xhr.statusText; | ||||
r3994 | if (xhr !== undefined && xhr.status >= 500) { | |||
r3989 | error = httpStatus; | |||
} | ||||
r3973 | } | |||
r3989 | if (error === null) { | |||
error = errorMessage.error || errorMessage || httpStatus; | ||||
} | ||||
$(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error)); | ||||
r3973 | ||||
r3989 | }); | |||
} | ||||
}); | ||||
} | ||||
r1326 | return commentForm; | |||
}; | ||||
r1331 | this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) { | |||
r1326 | ||||
var tmpl = $('#cb-comment-general-form-template').html(); | ||||
tmpl = tmpl.format(null, 'general'); | ||||
var $form = $(tmpl); | ||||
r1331 | var $formPlaceholder = $('#cb-comment-general-form-placeholder'); | |||
var curForm = $formPlaceholder.find('form'); | ||||
r1326 | if (curForm){ | |||
curForm.remove(); | ||||
} | ||||
r1331 | $formPlaceholder.append($form); | |||
r1326 | ||||
var _form = $($form[0]); | ||||
r1362 | var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo']; | |||
r4401 | var edit = false; | |||
var comment_id = null; | ||||
r1326 | var commentForm = this.createCommentForm( | |||
r4401 | _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id); | |||
r1326 | commentForm.initStatusChangeSelector(); | |||
r1331 | ||||
return commentForm; | ||||
r1326 | }; | |||
r4408 | ||||
r4543 | this.editComment = function(node, line_no, f_path) { | |||
self.edit = true; | ||||
r4401 | var $node = $(node); | |||
r4543 | var $td = $node.closest('td'); | |||
r4401 | var $comment = $(node).closest('.comment'); | |||
r4540 | var comment_id = $($comment).data('commentId'); | |||
var isDraft = $($comment).data('commentDraft'); | ||||
r4543 | var $editForm = null | |||
r1326 | ||||
r4401 | var $comments = $node.closest('div.inline-comments'); | |||
var $general_comments = null; | ||||
if($comments.length){ | ||||
// inline comments setup | ||||
r4543 | $editForm = $comments.find('.comment-inline-form'); | |||
line_no = self.getLineNumber(node) | ||||
r4401 | } | |||
else{ | ||||
// general comments setup | ||||
$comments = $('#comments'); | ||||
r4543 | $editForm = $comments.find('.comment-inline-form'); | |||
line_no = $comment[0].id | ||||
r4401 | $('#cb-comment-general-form-placeholder').hide(); | |||
} | ||||
r4543 | if ($editForm.length === 0) { | |||
r4401 | ||||
r4543 | // unhide all comments if they are hidden for a proper REPLY mode | |||
r4401 | var $filediff = $node.closest('.filediff'); | |||
$filediff.removeClass('hide-comments'); | ||||
r4543 | $editForm = self.createNewFormWrapper(f_path, line_no); | |||
if(f_path && line_no) { | ||||
$editForm.addClass('comment-inline-form-edit') | ||||
} | ||||
r4401 | ||||
r4543 | $comment.after($editForm) | |||
var _form = $($editForm[0]).find('form'); | ||||
r4401 | var autocompleteActions = ['as_note',]; | |||
var commentForm = this.createCommentForm( | ||||
r4543 | _form, line_no, '', autocompleteActions, resolvesCommentId, | |||
r4401 | this.edit, comment_id); | |||
var old_comment_text_binary = $comment.attr('data-comment-text'); | ||||
var old_comment_text = b64DecodeUnicode(old_comment_text_binary); | ||||
commentForm.cm.setValue(old_comment_text); | ||||
$comment.hide(); | ||||
r4543 | tooltipActivate(); | |||
r4401 | ||||
r4543 | // set a CUSTOM submit handler for inline comment edit action. | |||
commentForm.setHandleFormSubmit(function(o) { | ||||
r4401 | var text = commentForm.cm.getValue(); | |||
var commentType = commentForm.getCommentType(); | ||||
if (text === "") { | ||||
return; | ||||
} | ||||
r4408 | ||||
r4401 | if (old_comment_text == text) { | |||
r4402 | SwalNoAnimation.fire({ | |||
r4403 | title: 'Unable to edit comment', | |||
html: _gettext('Comment body was not changed.'), | ||||
r4401 | }); | |||
return; | ||||
} | ||||
var excludeCancelBtn = false; | ||||
var submitEvent = true; | ||||
commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent); | ||||
commentForm.cm.setOption("readOnly", true); | ||||
r4408 | ||||
// Read last version known | ||||
var versionSelector = $('#comment_versions_{0}'.format(comment_id)); | ||||
var version = versionSelector.data('lastVersion'); | ||||
r4401 | ||||
r4408 | if (!version) { | |||
version = 0; | ||||
r4401 | } | |||
r4408 | ||||
r4401 | var postData = { | |||
'text': text, | ||||
'f_path': f_path, | ||||
r4543 | 'line': line_no, | |||
r4401 | 'comment_type': commentType, | |||
r4540 | 'draft': isDraft, | |||
r4401 | 'version': version, | |||
r4408 | 'csrf_token': CSRF_TOKEN | |||
r4401 | }; | |||
var submitSuccessCallback = function(json_data) { | ||||
r4543 | $editForm.remove(); | |||
r4401 | $comment.show(); | |||
var postData = { | ||||
'text': text, | ||||
'renderer': $comment.attr('data-comment-renderer'), | ||||
'csrf_token': CSRF_TOKEN | ||||
}; | ||||
r4408 | /* Inject new edited version selector */ | |||
r4401 | var updateCommentVersionDropDown = function () { | |||
r4408 | var versionSelectId = '#comment_versions_'+comment_id; | |||
var preLoadVersionData = [ | ||||
{ | ||||
id: json_data['comment_version'], | ||||
text: "v{0}".format(json_data['comment_version']), | ||||
action: function () { | ||||
Rhodecode.comments.showVersion( | ||||
json_data['comment_id'], | ||||
json_data['comment_history_id'] | ||||
) | ||||
}, | ||||
comment_version: json_data['comment_version'], | ||||
comment_author_username: json_data['comment_author_username'], | ||||
comment_author_gravatar: json_data['comment_author_gravatar'], | ||||
comment_created_on: json_data['comment_created_on'], | ||||
}, | ||||
] | ||||
if ($(versionSelectId).data('select2')) { | ||||
var oldData = $(versionSelectId).data('select2').opts.data.results; | ||||
$(versionSelectId).select2("destroy"); | ||||
preLoadVersionData = oldData.concat(preLoadVersionData) | ||||
} | ||||
initVersionSelector(versionSelectId, {results: preLoadVersionData}); | ||||
r4416 | $comment.attr('data-comment-text', utf8ToB64(text)); | |||
r4408 | ||||
var versionSelector = $('#comment_versions_'+comment_id); | ||||
// set lastVersion so we know our last edit version | ||||
versionSelector.data('lastVersion', json_data['comment_version']) | ||||
versionSelector.parent().show(); | ||||
r4401 | } | |||
updateCommentVersionDropDown(); | ||||
r4408 | ||||
r4401 | // by default we reset state of comment preserving the text | |||
var failRenderCommit = function(jqXHR, textStatus, errorThrown) { | ||||
r4408 | var prefix = "Error while editing this comment.\n" | |||
r4401 | var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); | |||
ajaxErrorSwal(message); | ||||
r4408 | }; | |||
r4401 | ||||
var successRenderCommit = function(o){ | ||||
$comment.show(); | ||||
$comment[0].lastElementChild.innerHTML = o; | ||||
r4408 | }; | |||
var previewUrl = pyroutes.url( | ||||
'repo_commit_comment_preview', | ||||
{'repo_name': templateContext.repo_name, | ||||
'commit_id': templateContext.commit_data.commit_id}); | ||||
r4401 | ||||
_submitAjaxPOST( | ||||
r4543 | previewUrl, postData, successRenderCommit, failRenderCommit | |||
r4401 | ); | |||
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); | ||||
// run global callback on submit | ||||
r4540 | commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id}); | |||
r4401 | ||||
} catch (e) { | ||||
console.error(e); | ||||
} | ||||
// re trigger the linkification of next/prev navigation | ||||
linkifyComments($('.inline-comment-injected')); | ||||
timeagoActivate(); | ||||
tooltipActivate(); | ||||
if (window.updateSticky !== undefined) { | ||||
// potentially our comments change the active window size, so we | ||||
// notify sticky elements | ||||
updateSticky() | ||||
} | ||||
r4540 | if (window.refreshAllComments !== undefined && !isDraft) { | |||
r4482 | // if we have this handler, run it, and refresh all comments boxes | |||
refreshAllComments() | ||||
} | ||||
r4562 | else if (window.refreshDraftComments !== undefined && isDraft) { | |||
// if we have this handler, run it, and refresh all comments boxes | ||||
refreshDraftComments(); | ||||
} | ||||
r4482 | ||||
r4401 | commentForm.setActionButtonsDisabled(false); | |||
}; | ||||
r4482 | ||||
r4401 | var submitFailCallback = function(jqXHR, textStatus, errorThrown) { | |||
var prefix = "Error while editing comment.\n" | ||||
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); | ||||
r4408 | if (jqXHR.status == 409){ | |||
message = 'This comment was probably changed somewhere else. Please reload the content of this comment.' | ||||
ajaxErrorSwal(message, 'Comment version mismatch.'); | ||||
} else { | ||||
ajaxErrorSwal(message); | ||||
} | ||||
r4401 | commentForm.resetCommentFormState(text) | |||
}; | ||||
commentForm.submitAjaxPOST( | ||||
r4408 | commentForm.submitUrl, postData, | |||
submitSuccessCallback, | ||||
submitFailCallback); | ||||
r4401 | }); | |||
} | ||||
r4543 | $editForm.addClass('comment-inline-form-open'); | |||
r4401 | }; | |||
r4408 | ||||
r4543 | this.attachComment = function(json_data) { | |||
var self = this; | ||||
$.each(json_data, function(idx, val) { | ||||
var json_data_elem = [val] | ||||
var isInline = val.comment_f_path && val.comment_lineno | ||||
if (isInline) { | ||||
self.attachInlineComment(json_data_elem) | ||||
} else { | ||||
self.attachGeneralComment(json_data_elem) | ||||
} | ||||
}) | ||||
} | ||||
this.attachGeneralComment = function(json_data) { | ||||
$.each(json_data, function(idx, val) { | ||||
$('#injected_page_comments').append(val.rendered_text); | ||||
}) | ||||
} | ||||
this.attachInlineComment = function(json_data) { | ||||
$.each(json_data, function (idx, val) { | ||||
var line_qry = '*[data-line-no="{0}"]'.format(val.line_no); | ||||
var html = val.rendered_text; | ||||
var $inlineComments = $('#' + val.target_id) | ||||
.find(line_qry) | ||||
.find('.inline-comments'); | ||||
var lastComment = $inlineComments.find('.comment-inline').last(); | ||||
if (lastComment.length === 0) { | ||||
// first comment, we append simply | ||||
$inlineComments.find('.reply-thread-container-wrapper').before(html); | ||||
} else { | ||||
$(lastComment).after(html) | ||||
} | ||||
}) | ||||
}; | ||||
this.createNewFormWrapper = function(f_path, line_no) { | ||||
// create a new reply HTML form from template | ||||
var tmpl = $('#cb-comment-inline-form-template').html(); | ||||
tmpl = tmpl.format(escapeHtml(f_path), line_no); | ||||
return $(tmpl); | ||||
} | ||||
r4633 | this.markCommentResolved = function(commentId) { | |||
$('#comment-label-{0}'.format(commentId)).find('.resolved').show(); | ||||
$('#comment-label-{0}'.format(commentId)).find('.resolve').hide(); | ||||
}; | ||||
r4543 | this.createComment = function(node, f_path, line_no, resolutionComment) { | |||
self.edit = false; | ||||
r1157 | var $node = $(node); | |||
var $td = $node.closest('td'); | ||||
r4543 | var resolvesCommentId = resolutionComment || null; | |||
r1157 | ||||
r4543 | var $replyForm = $td.find('.comment-inline-form'); | |||
r1326 | ||||
r4543 | // if form isn't existing, we're generating a new one and injecting it. | |||
if ($replyForm.length === 0) { | ||||
// unhide/expand all comments if they are hidden for a proper REPLY mode | ||||
self.toggleLineComments($node, true); | ||||
$replyForm = self.createNewFormWrapper(f_path, line_no); | ||||
r1157 | ||||
var $comments = $td.find('.inline-comments'); | ||||
r4543 | ||||
// There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first | ||||
if ($comments.length===0) { | ||||
r4652 | var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(escapeHtml(f_path), line_no) | |||
r4543 | var $reply_container = $('#cb-comments-inline-container-template') | |||
$reply_container.find('button.cb-comment-add-button').replaceWith(replBtn); | ||||
$td.append($($reply_container).html()); | ||||
r1157 | } | |||
r4543 | // default comment button exists, so we prepend the form for leaving initial comment | |||
$td.find('.cb-comment-add-button').before($replyForm); | ||||
// set marker, that we have a open form | ||||
var $replyWrapper = $td.find('.reply-thread-container-wrapper') | ||||
$replyWrapper.addClass('comment-form-active'); | ||||
r1157 | ||||
r4543 | var lastComment = $comments.find('.comment-inline').last(); | |||
if ($(lastComment).hasClass('comment-outdated')) { | ||||
$replyWrapper.show(); | ||||
} | ||||
var _form = $($replyForm[0]).find('form'); | ||||
r1362 | var autocompleteActions = ['as_note', 'as_todo']; | |||
r4401 | var comment_id=null; | |||
r4543 | var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no); | |||
var commentForm = self.createCommentForm( | ||||
_form, line_no, placeholderText, autocompleteActions, resolvesCommentId, | ||||
self.edit, comment_id); | ||||
r1157 | ||||
// set a CUSTOM submit handler for inline comments. | ||||
commentForm.setHandleFormSubmit(function(o) { | ||||
var text = commentForm.cm.getValue(); | ||||
r1324 | var commentType = commentForm.getCommentType(); | |||
r1325 | var resolvesCommentId = commentForm.getResolvesId(); | |||
r4540 | var isDraft = commentForm.getDraftState(); | |||
r1157 | ||||
if (text === "") { | ||||
return; | ||||
} | ||||
r4543 | if (line_no === undefined) { | |||
alert('Error: unable to fetch line number for this inline comment !'); | ||||
r1157 | return; | |||
} | ||||
r4543 | ||||
r1157 | if (f_path === undefined) { | |||
r4543 | alert('Error: unable to fetch file path for this inline comment !'); | |||
r1157 | 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, | ||||
r4543 | 'line': line_no, | |||
r1324 | 'comment_type': commentType, | |||
r4540 | 'draft': isDraft, | |||
r1157 | 'csrf_token': CSRF_TOKEN | |||
}; | ||||
r1325 | if (resolvesCommentId){ | |||
postData['resolves_comment_id'] = resolvesCommentId; | ||||
} | ||||
r4543 | // submitSuccess for inline commits | |||
r1157 | var submitSuccessCallback = function(json_data) { | |||
r4543 | ||||
$replyForm.remove(); | ||||
$td.find('.reply-thread-container-wrapper').removeClass('comment-form-active'); | ||||
try { | ||||
// inject newly created comments, json_data is {<comment_id>: {}} | ||||
self.attachInlineComment(json_data) | ||||
r1157 | ||||
r4543 | //mark visually which comment was resolved | |||
if (resolvesCommentId) { | ||||
r4633 | self.markCommentResolved(resolvesCommentId); | |||
r4543 | } | |||
r1157 | ||||
r4543 | // run global callback on submit | |||
commentForm.globalSubmitSuccessCallback({ | ||||
draft: isDraft, | ||||
comment_id: comment_id | ||||
}); | ||||
} catch (e) { | ||||
console.error(e); | ||||
r1325 | } | |||
r3129 | if (window.updateSticky !== undefined) { | |||
r3126 | // potentially our comments change the active window size, so we | |||
r3129 | // notify sticky elements | |||
updateSticky() | ||||
r3126 | } | |||
r4540 | if (window.refreshAllComments !== undefined && !isDraft) { | |||
r4482 | // if we have this handler, run it, and refresh all comments boxes | |||
refreshAllComments() | ||||
} | ||||
r4562 | else if (window.refreshDraftComments !== undefined && isDraft) { | |||
// if we have this handler, run it, and refresh all comments boxes | ||||
refreshDraftComments(); | ||||
} | ||||
r4482 | ||||
r1157 | commentForm.setActionButtonsDisabled(false); | |||
r4543 | // re trigger the linkification of next/prev navigation | |||
linkifyComments($('.inline-comment-injected')); | ||||
timeagoActivate(); | ||||
tooltipActivate(); | ||||
r1157 | }; | |||
r4543 | ||||
r4311 | var submitFailCallback = function(jqXHR, textStatus, errorThrown) { | |||
var prefix = "Error while submitting comment.\n" | ||||
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); | ||||
ajaxErrorSwal(message); | ||||
r1157 | commentForm.resetCommentFormState(text) | |||
}; | ||||
r4543 | ||||
r1157 | commentForm.submitAjaxPOST( | |||
commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback); | ||||
}); | ||||
} | ||||
r4543 | // Finally "open" our reply form, since we know there are comments and we have the "attached" old form | |||
$replyForm.addClass('comment-inline-form-open'); | ||||
tooltipActivate(); | ||||
r1192 | }; | |||
r1157 | ||||
r1325 | this.createResolutionComment = function(commentId){ | |||
// hide the trigger text | ||||
$('#resolve-comment-{0}'.format(commentId)).hide(); | ||||
var comment = $('#comment-'+commentId); | ||||
var commentData = comment.data(); | ||||
r4573 | ||||
r1325 | if (commentData.commentInline) { | |||
r4573 | var f_path = commentData.commentFPath; | |||
var line_no = commentData.commentLineNo; | ||||
r4543 | this.createComment(comment, f_path, line_no, commentId) | |||
r1325 | } else { | |||
r4543 | this.createGeneralComment('general', "$placeholder", commentId) | |||
r1325 | } | |||
return false; | ||||
}; | ||||
this.submitResolution = function(commentId){ | ||||
var form = $('#resolve_comment_{0}'.format(commentId)).closest('form'); | ||||
var commentForm = form.get(0).CommentForm; | ||||
var cm = commentForm.getCmInstance(); | ||||
var renderer = templateContext.visual.default_renderer; | ||||
if (renderer == 'rst'){ | ||||
var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl); | ||||
} else if (renderer == 'markdown') { | ||||
var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl); | ||||
} else { | ||||
var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl); | ||||
} | ||||
cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl)); | ||||
form.submit(); | ||||
return false; | ||||
}; | ||||
r4633 | this.resolveTodo = function (elem, todoId) { | |||
var commentId = todoId; | ||||
SwalNoAnimation.fire({ | ||||
title: 'Resolve TODO {0}'.format(todoId), | ||||
showCancelButton: true, | ||||
confirmButtonText: _gettext('Yes'), | ||||
showLoaderOnConfirm: true, | ||||
allowOutsideClick: function () { | ||||
!Swal.isLoading() | ||||
}, | ||||
preConfirm: function () { | ||||
var comment = $('#comment-' + commentId); | ||||
var commentData = comment.data(); | ||||
var f_path = null | ||||
var line_no = null | ||||
if (commentData.commentInline) { | ||||
f_path = commentData.commentFPath; | ||||
line_no = commentData.commentLineNo; | ||||
} | ||||
var renderer = templateContext.visual.default_renderer; | ||||
var commentBoxUrl = '{1}#comment-{0}'.format(commentId); | ||||
// Pull request case | ||||
if (templateContext.pull_request_data.pull_request_id !== null) { | ||||
var commentUrl = pyroutes.url('pullrequest_comment_create', | ||||
{ | ||||
'repo_name': templateContext.repo_name, | ||||
'pull_request_id': templateContext.pull_request_data.pull_request_id, | ||||
'comment_id': commentId | ||||
}); | ||||
} else { | ||||
var commentUrl = pyroutes.url('repo_commit_comment_create', | ||||
{ | ||||
'repo_name': templateContext.repo_name, | ||||
'commit_id': templateContext.commit_data.commit_id, | ||||
'comment_id': commentId | ||||
}); | ||||
} | ||||
if (renderer === 'rst') { | ||||
commentBoxUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentUrl); | ||||
} else if (renderer === 'markdown') { | ||||
commentBoxUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentUrl); | ||||
} | ||||
var resolveText = _gettext('TODO from comment {0} was fixed.').format(commentBoxUrl); | ||||
var postData = { | ||||
text: resolveText, | ||||
comment_type: 'note', | ||||
draft: false, | ||||
csrf_token: CSRF_TOKEN, | ||||
resolves_comment_id: commentId | ||||
} | ||||
if (commentData.commentInline) { | ||||
postData['f_path'] = f_path; | ||||
postData['line'] = line_no; | ||||
} | ||||
return new Promise(function (resolve, reject) { | ||||
$.ajax({ | ||||
type: 'POST', | ||||
data: postData, | ||||
url: commentUrl, | ||||
headers: {'X-PARTIAL-XHR': true} | ||||
}) | ||||
.done(function (data) { | ||||
resolve(data); | ||||
}) | ||||
.fail(function (jqXHR, textStatus, errorThrown) { | ||||
var prefix = "Error while resolving TODO.\n" | ||||
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); | ||||
ajaxErrorSwal(message); | ||||
}); | ||||
}) | ||||
} | ||||
}) | ||||
.then(function (result) { | ||||
var success = function (json_data) { | ||||
resolvesCommentId = commentId; | ||||
var commentResolved = json_data[Object.keys(json_data)[0]] | ||||
try { | ||||
if (commentResolved.f_path) { | ||||
// inject newly created comments, json_data is {<comment_id>: {}} | ||||
self.attachInlineComment(json_data) | ||||
} else { | ||||
self.attachGeneralComment(json_data) | ||||
} | ||||
//mark visually which comment was resolved | ||||
if (resolvesCommentId) { | ||||
self.markCommentResolved(resolvesCommentId); | ||||
} | ||||
// run global callback on submit | ||||
if (window.commentFormGlobalSubmitSuccessCallback !== undefined) { | ||||
commentFormGlobalSubmitSuccessCallback({ | ||||
draft: false, | ||||
comment_id: commentId | ||||
}); | ||||
} | ||||
} catch (e) { | ||||
console.error(e); | ||||
} | ||||
if (window.updateSticky !== undefined) { | ||||
// potentially our comments change the active window size, so we | ||||
// notify sticky elements | ||||
updateSticky() | ||||
} | ||||
if (window.refreshAllComments !== undefined) { | ||||
// if we have this handler, run it, and refresh all comments boxes | ||||
refreshAllComments() | ||||
} | ||||
// re trigger the linkification of next/prev navigation | ||||
linkifyComments($('.inline-comment-injected')); | ||||
timeagoActivate(); | ||||
tooltipActivate(); | ||||
}; | ||||
if (result.value) { | ||||
$(elem).remove(); | ||||
success(result.value) | ||||
} | ||||
}) | ||||
}; | ||||
r1194 | }; | |||
r4543 | ||||
window.commentHelp = function(renderer) { | ||||
var funcData = {'renderer': renderer} | ||||
return renderTemplate('commentHelpHovercard', funcData) | ||||
r4633 | } | |||