comments: multiple changes on comments navigation/display logic

- toggle are smarter, they open/hide windows accorind to actions. E.g commenting opens threads
- toggle are mor explicit
- possible to hide/show only single threads
- when channelstream is enabled comments are now live-pushed to diff with NEW marker indicator
- new UI for showing thread comments
This commit is contained in:
Milka Kuzminski 2020-10-27 10:05:00 +01:00
parent dc29bb13b7
commit 1ebb2411e9
20 changed files with 718 additions and 412 deletions

View file

@ -252,7 +252,7 @@ But please check this code
var comment = $('#comment-'+commentId);
var commentData = comment.data();
if (commentData.commentInline) {
this.createComment(comment, commentId)
this.createComment(comment, f_path, line_no, commentId)
} else {
Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
}

View file

@ -383,6 +383,9 @@ class RepoCommitsView(RepoAppView):
text = self.request.POST.get('text')
comment_type = self.request.POST.get('comment_type')
resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
f_path = self.request.POST.get('f_path')
line_no = self.request.POST.get('line')
target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
if status:
text = text or (_('Status change %(transition_icon)s %(status)s')
@ -404,8 +407,8 @@ class RepoCommitsView(RepoAppView):
repo=self.db_repo.repo_id,
user=self._rhodecode_db_user.user_id,
commit_id=current_id,
f_path=self.request.POST.get('f_path'),
line_no=self.request.POST.get('line'),
f_path=f_path,
line_no=line_no,
status_change=(ChangesetStatus.get_status_lbl(status)
if status else None),
status_change_type=status,
@ -447,19 +450,20 @@ class RepoCommitsView(RepoAppView):
# finalize, commit and redirect
Session().commit()
data = {
'target_id': h.safeid(h.safe_unicode(
self.request.POST.get('f_path'))),
}
data = {}
if comment:
comment_id = comment.comment_id
data[comment_id] = {
'target_id': target_elem_id
}
c.co = comment
c.at_version_num = 0
rendered_comment = render(
'rhodecode:templates/changeset/changeset_comment_block.mako',
self._get_template_context(c), self.request)
data.update(comment.get_dict())
data.update({'rendered_text': rendered_comment})
data[comment_id].update(comment.get_dict())
data[comment_id].update({'rendered_text': rendered_comment})
comment_broadcast_channel = channelstream.comment_channel(
self.db_repo_name, commit_obj=commit)

View file

@ -983,8 +983,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
}
return data
def _get_existing_ids(self, post_data):
return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
@classmethod
def get_comment_ids(cls, post_data):
return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
@LoginRequired()
@NotAnonymous()
@ -1022,7 +1023,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
all_comments = c.inline_comments_flat + c.comments
existing_ids = self._get_existing_ids(self.request.POST)
existing_ids = self.get_comment_ids(self.request.POST)
return _render('comments_table', all_comments, len(all_comments),
existing_ids=existing_ids)
@ -1064,7 +1065,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
.get_pull_request_resolved_todos(pull_request, include_drafts=False)
all_comments = c.unresolved_comments + c.resolved_comments
existing_ids = self._get_existing_ids(self.request.POST)
existing_ids = self.get_comment_ids(self.request.POST)
return _render('comments_table', all_comments, len(c.unresolved_comments),
todo_comments=True, existing_ids=existing_ids)
@ -1518,6 +1519,150 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
self._rhodecode_user)
raise HTTPNotFound()
def _pull_request_comments_create(self, pull_request, comments):
_ = self.request.translate
data = {}
pull_request_id = pull_request.pull_request_id
if not comments:
return
all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
for entry in comments:
c = self.load_default_context()
comment_type = entry['comment_type']
text = entry['text']
status = entry['status']
is_draft = str2bool(entry['is_draft'])
resolves_comment_id = entry['resolves_comment_id']
close_pull_request = entry['close_pull_request']
f_path = entry['f_path']
line_no = entry['line']
target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
# the logic here should work like following, if we submit close
# pr comment, use `close_pull_request_with_comment` function
# else handle regular comment logic
if close_pull_request:
# only owner or admin or person with write permissions
allowed_to_close = PullRequestModel().check_user_update(
pull_request, self._rhodecode_user)
if not allowed_to_close:
log.debug('comment: forbidden because not allowed to close '
'pull request %s', pull_request_id)
raise HTTPForbidden()
# This also triggers `review_status_change`
comment, status = PullRequestModel().close_pull_request_with_comment(
pull_request, self._rhodecode_user, self.db_repo, message=text,
auth_user=self._rhodecode_user)
Session().flush()
is_inline = comment.is_inline
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'comment',
data={'comment': comment})
else:
# regular comment case, could be inline, or one with status.
# for that one we check also permissions
# Additionally ENSURE if somehow draft is sent we're then unable to change status
allowed_to_change_status = PullRequestModel().check_user_change_status(
pull_request, self._rhodecode_user) and not is_draft
if status and allowed_to_change_status:
message = (_('Status change %(transition_icon)s %(status)s')
% {'transition_icon': '>',
'status': ChangesetStatus.get_status_lbl(status)})
text = text or message
comment = CommentsModel().create(
text=text,
repo=self.db_repo.repo_id,
user=self._rhodecode_user.user_id,
pull_request=pull_request,
f_path=f_path,
line_no=line_no,
status_change=(ChangesetStatus.get_status_lbl(status)
if status and allowed_to_change_status else None),
status_change_type=(status
if status and allowed_to_change_status else None),
comment_type=comment_type,
is_draft=is_draft,
resolves_comment_id=resolves_comment_id,
auth_user=self._rhodecode_user,
send_email=not is_draft, # skip notification for draft comments
)
is_inline = comment.is_inline
if allowed_to_change_status:
# calculate old status before we change it
old_calculated_status = pull_request.calculated_review_status()
# get status if set !
if status:
ChangesetStatusModel().set_status(
self.db_repo.repo_id,
status,
self._rhodecode_user.user_id,
comment,
pull_request=pull_request
)
Session().flush()
# this is somehow required to get access to some relationship
# loaded on comment
Session().refresh(comment)
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'comment',
data={'comment': comment})
# we now calculate the status of pull request, and based on that
# calculation we set the commits status
calculated_status = pull_request.calculated_review_status()
if old_calculated_status != calculated_status:
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'review_status_change',
data={'status': calculated_status})
comment_id = comment.comment_id
data[comment_id] = {
'target_id': target_elem_id
}
Session().flush()
c.co = comment
c.at_version_num = None
c.is_new = True
rendered_comment = render(
'rhodecode:templates/changeset/changeset_comment_block.mako',
self._get_template_context(c), self.request)
data[comment_id].update(comment.get_dict())
data[comment_id].update({'rendered_text': rendered_comment})
Session().commit()
# skip channelstream for draft comments
if all_drafts:
comment_broadcast_channel = channelstream.comment_channel(
self.db_repo_name, pull_request_obj=pull_request)
comment_data = data
comment_type = 'inline' if is_inline else 'general'
if len(data) == 1:
msg = _('posted {} new {} comment').format(len(data), comment_type)
else:
msg = _('posted {} new {} comments').format(len(data), comment_type)
channelstream.comment_channelstream_push(
self.request, comment_broadcast_channel, self._rhodecode_user, msg,
comment_data=comment_data)
return data
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
@ -1529,9 +1674,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
def pull_request_comment_create(self):
_ = self.request.translate
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
pull_request_id = pull_request.pull_request_id
pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
if pull_request.is_closed():
log.debug('comment: forbidden because pull request is closed')
@ -1543,130 +1686,17 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
log.debug('comment: forbidden because pull request is from forbidden repo')
raise HTTPForbidden()
c = self.load_default_context()
status = self.request.POST.get('changeset_status', None)
text = self.request.POST.get('text')
comment_type = self.request.POST.get('comment_type')
is_draft = str2bool(self.request.POST.get('draft'))
resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
close_pull_request = self.request.POST.get('close_pull_request')
# the logic here should work like following, if we submit close
# pr comment, use `close_pull_request_with_comment` function
# else handle regular comment logic
if close_pull_request:
# only owner or admin or person with write permissions
allowed_to_close = PullRequestModel().check_user_update(
pull_request, self._rhodecode_user)
if not allowed_to_close:
log.debug('comment: forbidden because not allowed to close '
'pull request %s', pull_request_id)
raise HTTPForbidden()
# This also triggers `review_status_change`
comment, status = PullRequestModel().close_pull_request_with_comment(
pull_request, self._rhodecode_user, self.db_repo, message=text,
auth_user=self._rhodecode_user)
Session().flush()
is_inline = comment.is_inline
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'comment',
data={'comment': comment})
else:
# regular comment case, could be inline, or one with status.
# for that one we check also permissions
# Additionally ENSURE if somehow draft is sent we're then unable to change status
allowed_to_change_status = PullRequestModel().check_user_change_status(
pull_request, self._rhodecode_user) and not is_draft
if status and allowed_to_change_status:
message = (_('Status change %(transition_icon)s %(status)s')
% {'transition_icon': '>',
'status': ChangesetStatus.get_status_lbl(status)})
text = text or message
comment = CommentsModel().create(
text=text,
repo=self.db_repo.repo_id,
user=self._rhodecode_user.user_id,
pull_request=pull_request,
f_path=self.request.POST.get('f_path'),
line_no=self.request.POST.get('line'),
status_change=(ChangesetStatus.get_status_lbl(status)
if status and allowed_to_change_status else None),
status_change_type=(status
if status and allowed_to_change_status else None),
comment_type=comment_type,
is_draft=is_draft,
resolves_comment_id=resolves_comment_id,
auth_user=self._rhodecode_user,
send_email=not is_draft, # skip notification for draft comments
)
is_inline = comment.is_inline
if allowed_to_change_status:
# calculate old status before we change it
old_calculated_status = pull_request.calculated_review_status()
# get status if set !
if status:
ChangesetStatusModel().set_status(
self.db_repo.repo_id,
status,
self._rhodecode_user.user_id,
comment,
pull_request=pull_request
)
Session().flush()
# this is somehow required to get access to some relationship
# loaded on comment
Session().refresh(comment)
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'comment',
data={'comment': comment})
# we now calculate the status of pull request, and based on that
# calculation we set the commits status
calculated_status = pull_request.calculated_review_status()
if old_calculated_status != calculated_status:
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'review_status_change',
data={'status': calculated_status})
Session().commit()
data = {
'target_id': h.safeid(h.safe_unicode(
self.request.POST.get('f_path'))),
comment_data = {
'comment_type': self.request.POST.get('comment_type'),
'text': self.request.POST.get('text'),
'status': self.request.POST.get('changeset_status', None),
'is_draft': self.request.POST.get('draft'),
'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
'close_pull_request': self.request.POST.get('close_pull_request'),
'f_path': self.request.POST.get('f_path'),
'line': self.request.POST.get('line'),
}
if comment:
c.co = comment
c.at_version_num = None
rendered_comment = render(
'rhodecode:templates/changeset/changeset_comment_block.mako',
self._get_template_context(c), self.request)
data.update(comment.get_dict())
data.update({'rendered_text': rendered_comment})
# skip channelstream for draft comments
if not is_draft:
comment_broadcast_channel = channelstream.comment_channel(
self.db_repo_name, pull_request_obj=pull_request)
comment_data = data
comment_type = 'inline' if is_inline else 'general'
channelstream.comment_channelstream_push(
self.request, comment_broadcast_channel, self._rhodecode_user,
_('posted a new {} comment').format(comment_type),
comment_data=comment_data)
data = self._pull_request_comments_create(pull_request, [comment_data])
return data

View file

@ -339,13 +339,12 @@ def comment_channelstream_push(request, comment_broadcast_channel, user, msg, **
comment_data = kwargs.pop('comment_data', {})
user_data = kwargs.pop('user_data', {})
comment_id = comment_data.get('comment_id')
comment_id = comment_data.keys()[0] if comment_data else ''
message = '<strong>{}</strong> {} #{}, {}'.format(
message = '<strong>{}</strong> {} #{}'.format(
user.username,
msg,
comment_id,
_reload_link(_('Reload page to see new comments')),
)
message_obj = {

View file

@ -1148,7 +1148,7 @@ class DiffLimitExceeded(Exception):
# NOTE(marcink): if diffs.mako change, probably this
# needs a bump to next version
CURRENT_DIFF_VERSION = 'v4'
CURRENT_DIFF_VERSION = 'v5'
def _cleanup_cache_file(cached_diff_file):

View file

@ -370,7 +370,7 @@ input[type="button"] {
color: @alert2;
&:hover {
color: darken(@alert2,30%);
color: darken(@alert2, 30%);
}
&:disabled {

View file

@ -1002,7 +1002,7 @@ input.filediff-collapse-state {
.nav-chunk {
position: absolute;
right: 20px;
margin-top: -17px;
margin-top: -15px;
}
.nav-chunk.selected {

View file

@ -4,7 +4,7 @@
// Comments
@comment-outdated-opacity: 0.6;
@comment-outdated-opacity: 1.0;
.comments {
width: 100%;
@ -64,32 +64,34 @@ tr.inline-comments div {
.comment-draft {
float: left;
margin-right: 10px;
font-weight: 600;
color: @alert3;
font-weight: 400;
color: @color-draft;
}
.comment-new {
float: left;
margin-right: 10px;
font-weight: 400;
color: @color-new;
}
.comment-label {
float: left;
padding: 0.4em 0.4em;
margin: 2px 4px 0px 0px;
display: inline-block;
padding: 0 8px 0 0;
min-height: 0;
text-align: center;
font-size: 10px;
line-height: .8em;
font-family: @text-italic;
font-style: italic;
background: #fff none;
color: @grey3;
border: 1px solid @grey4;
white-space: nowrap;
text-transform: uppercase;
min-width: 50px;
border-radius: 4px;
&.todo {
color: @color5;
@ -277,63 +279,164 @@ tr.inline-comments div {
.comment-outdated {
opacity: @comment-outdated-opacity;
}
.comment-outdated-label {
color: @grey3;
padding-right: 4px;
}
}
.inline-comments {
border-radius: @border-radius;
.comment {
margin: 0;
border-radius: @border-radius;
}
.comment-outdated {
opacity: @comment-outdated-opacity;
}
.comment-outdated-label {
color: @grey3;
padding-right: 4px;
}
.comment-inline {
&:first-child {
margin: 4px 4px 0 4px;
border-top: 1px solid @grey5;
border-bottom: 0 solid @grey5;
border-left: 1px solid @grey5;
border-right: 1px solid @grey5;
.border-radius-top(4px);
}
&:only-child {
margin: 4px 4px 0 4px;
border-top: 1px solid @grey5;
border-bottom: 0 solid @grey5;
border-left: 1px solid @grey5;
border-right: 1px solid @grey5;
.border-radius-top(4px);
}
background: white;
padding: @comment-padding @comment-padding;
border: @comment-padding solid @grey6;
margin: 0 4px 0 4px;
border-top: 0 solid @grey5;
border-bottom: 0 solid @grey5;
border-left: 1px solid @grey5;
border-right: 1px solid @grey5;
.text {
border: none;
}
.meta {
border-bottom: 1px solid @grey6;
margin: -5px 0px;
line-height: 24px;
}
}
.comment-selected {
border-left: 6px solid @comment-highlight-color;
}
.comment-inline-form-open {
display: block !important;
}
.comment-inline-form {
padding: @comment-padding;
display: none;
}
.cb-comment-add-button {
margin: @comment-padding;
.comment-inline-form-edit {
padding: 0;
margin: 0px 4px 2px 4px;
}
/* hide add comment button when form is open */
.reply-thread-container {
display: table;
width: 100%;
padding: 0px 4px 4px 4px;
}
.reply-thread-container-wrapper {
margin: 0 4px 4px 4px;
border-top: 0 solid @grey5;
border-bottom: 1px solid @grey5;
border-left: 1px solid @grey5;
border-right: 1px solid @grey5;
.border-radius-bottom(4px);
}
.reply-thread-gravatar {
display: table-cell;
width: 24px;
height: 24px;
padding-top: 10px;
padding-left: 10px;
background-color: #eeeeee;
vertical-align: top;
}
.reply-thread-reply-button {
display: table-cell;
width: 100%;
height: 33px;
padding: 3px 8px;
margin-left: 8px;
background-color: #eeeeee;
}
.reply-thread-reply-button .cb-comment-add-button {
border-radius: 4px;
width: 100%;
padding: 6px 2px;
text-align: left;
cursor: text;
color: @grey3;
}
.reply-thread-reply-button .cb-comment-add-button:hover {
background-color: white;
color: @grey2;
}
.reply-thread-last {
display: table-cell;
width: 10px;
}
/* Hide reply box when it's a first element,
can happen when drafts are saved but not shown to specific user,
or there are outdated comments hidden
*/
.reply-thread-container-wrapper:first-child:not(.comment-form-active) {
display: none;
}
.reply-thread-container-wrapper.comment-outdated {
display: none
}
/* hide add comment button when form is open */
.comment-inline-form-open ~ .cb-comment-add-button {
display: none;
}
.comment-inline-form-open {
display: block;
}
/* hide add comment button when form but no comments */
.comment-inline-form:first-child + .cb-comment-add-button {
display: none;
}
/* hide add comment button when no comments or form */
.cb-comment-add-button:first-child {
display: none;
}
/* hide add comment button when only comment is being deleted */
.comment-deleting:first-child + .cb-comment-add-button {
display: none;
}
}
/* hide add comment button when form but no comments */
.comment-inline-form:first-child + .cb-comment-add-button {
display: none;
}
}
.show-outdated-comments {
display: inline;
@ -387,23 +490,40 @@ form.comment-form {
}
.comment-footer {
position: relative;
display: table;
width: 100%;
min-height: 42px;
height: 42px;
.status_box,
.comment-status-box,
.cancel-button {
float: left;
display: inline-block;
}
.status_box {
.comment-status-box {
margin-left: 10px;
}
.action-buttons {
float: left;
display: inline-block;
display: table-cell;
padding: 5px 0 5px 2px;
}
.toolbar-text {
height: 42px;
display: table-cell;
vertical-align: bottom;
font-size: 11px;
color: @grey4;
text-align: right;
a {
color: @grey4;
}
p {
padding: 0;
margin: 0;
}
}
.action-buttons-extra {
@ -434,10 +554,6 @@ form.comment-form {
margin-right: 0;
}
.comment-footer {
margin-bottom: 50px;
margin-top: 10px;
}
}
@ -489,8 +605,8 @@ form.comment-form {
.injected_diff .comment-inline-form,
.comment-inline-form {
background-color: white;
margin-top: 10px;
margin-bottom: 20px;
margin-top: 4px;
margin-bottom: 10px;
}
.inline-form {
@ -526,9 +642,6 @@ form.comment-form {
margin: 0px;
}
.comment-inline-form .comment-footer {
margin: 10px 0px 0px 0px;
}
.hide-inline-form-button {
margin-left: 5px;
@ -554,6 +667,7 @@ comment-area-text {
.comment-area-header {
height: 35px;
border-bottom: 1px solid @grey5;
}
.comment-area-header .nav-links {
@ -561,6 +675,7 @@ comment-area-text {
flex-flow: row wrap;
-webkit-flex-flow: row wrap;
width: 100%;
border: none;
}
.comment-area-footer {
@ -629,14 +744,3 @@ comment-area-text {
border-bottom: 2px solid transparent;
}
.toolbar-text {
float: right;
font-size: 11px;
color: @grey4;
text-align: right;
a {
color: @grey4;
}
}

View file

@ -3212,8 +3212,13 @@ details:not([open]) > :not(summary) {
.sidebar-element {
margin-top: 20px;
.icon-draft {
color: @color-draft
}
}
.right-sidebar-collapsed-state {
display: flex;
flex-direction: column;
@ -3235,5 +3240,4 @@ details:not([open]) > :not(summary) {
.old-comments-marker td {
padding-top: 15px;
border-bottom: 1px solid @grey5;
}

View file

@ -47,6 +47,8 @@
// Highlight color for lines and colors
@comment-highlight-color: #ffd887;
@color-draft: darken(@alert3, 30%);
@color-new: darken(@alert1, 5%);
// FONTS
@basefontsize: 13px;

View file

@ -71,14 +71,20 @@ export class RhodecodeApp extends PolymerElement {
if (elem) {
elem.handleNotification(data);
}
}
handleComment(data) {
if (data.message.comment_id) {
if (data.message.comment_data.length !== 0) {
if (window.refreshAllComments !== undefined) {
refreshAllComments()
}
var json_data = data.message.comment_data;
if (window.commentsController !== undefined) {
window.commentsController.attachComment(json_data)
}
}
}

View file

@ -364,12 +364,15 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
postData['close_pull_request'] = true;
}
var submitSuccessCallback = function(o) {
// submitSuccess for general comments
var submitSuccessCallback = function(json_data) {
// reload page if we change status for single commit.
if (status && self.commitId) {
location.reload(true);
} else {
$('#injected_page_comments').append(o.rendered_text);
// inject newly created comments, json_data is {<comment_id>: {}}
self.attachGeneralComment(json_data)
self.resetCommentFormState();
timeagoActivate();
tooltipActivate();
@ -565,26 +568,6 @@ var CommentsController = function() {
var mainComment = '#text';
var self = this;
this.cancelComment = function (node) {
var $node = $(node);
var edit = $(this).attr('edit');
if (edit) {
var $general_comments = null;
var $inline_comments = $node.closest('div.inline-comments');
if (!$inline_comments.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 = $inline_comments.find('div.comment:hidden');
}
$comment.show();
}
$node.closest('.comment-inline-form').remove();
return false;
};
this.showVersion = function (comment_id, comment_history_id) {
var historyViewUrl = pyroutes.url(
@ -682,6 +665,35 @@ var CommentsController = function() {
return self.scrollToComment(node, -1, true);
};
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;
};
this._deleteComment = function(node) {
var $node = $(node);
var $td = $node.closest('td');
@ -751,7 +763,7 @@ var CommentsController = function() {
this.finalizeDrafts = function(commentIds) {
SwalNoAnimation.fire({
title: _ngettext('Submit {0} draft comment', 'Submit {0} draft comments', commentIds.length).format(commentIds.length),
title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
icon: 'warning',
showCancelButton: true,
confirmButtonText: _gettext('Yes, finalize drafts'),
@ -764,6 +776,7 @@ var CommentsController = function() {
};
this.toggleWideMode = function (node) {
if ($('#content').hasClass('wrapper')) {
$('#content').removeClass("wrapper");
$('#content').addClass("wide-mode-wrapper");
@ -778,18 +791,23 @@ var CommentsController = function() {
};
this.toggleComments = function(node, show) {
/**
* Turn off/on all comments in file diff
*/
this.toggleDiffComments = function(node) {
// Find closes filediff container
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');
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) {
@ -799,14 +817,33 @@ var CommentsController = function() {
}
return false;
};
}
this.toggleLineComments = function(node, show) {
var trElem = $(node).closest('tr')
if (show === true) {
// mark outdated comments as visible before the toggle;
$(trElem).find('.comment-outdated').show();
$(trElem).removeClass('hide-line-comments');
} else if (show === false) {
$(trElem).find('.comment-outdated').hide();
$(trElem).addClass('hide-line-comments');
} else {
// mark outdated comments as visible before the toggle;
$(trElem).find('.comment-outdated').show();
$(trElem).toggleClass('hide-line-comments');
}
// 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()
}
this.toggleLineComments = function(node) {
self.toggleComments(node, true);
var $node = $(node);
// mark outdated comments as visible before the toggle;
$(node.closest('tr')).find('.comment-outdated').show();
$node.closest('tr').toggleClass('hide-line-comments');
};
this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
@ -960,64 +997,58 @@ var CommentsController = function() {
return commentForm;
};
this.editComment = function(node) {
this.editComment = function(node, line_no, f_path) {
self.edit = true;
var $node = $(node);
var $td = $node.closest('td');
var $comment = $(node).closest('.comment');
var comment_id = $($comment).data('commentId');
var isDraft = $($comment).data('commentDraft');
var $form = null
var $editForm = null
var $comments = $node.closest('div.inline-comments');
var $general_comments = null;
var lineno = null;
if($comments.length){
// inline comments setup
$form = $comments.find('.comment-inline-form');
lineno = self.getLineNumber(node)
$editForm = $comments.find('.comment-inline-form');
line_no = self.getLineNumber(node)
}
else{
// general comments setup
$comments = $('#comments');
$form = $comments.find('.comment-inline-form');
lineno = $comment[0].id
$editForm = $comments.find('.comment-inline-form');
line_no = $comment[0].id
$('#cb-comment-general-form-placeholder').hide();
}
this.edit = true;
if (!$form.length) {
if ($editForm.length === 0) {
// unhide all comments if they are hidden for a proper REPLY mode
var $filediff = $node.closest('.filediff');
$filediff.removeClass('hide-comments');
var f_path = $filediff.attr('data-f-path');
// create a new HTML from template
$editForm = self.createNewFormWrapper(f_path, line_no);
if(f_path && line_no) {
$editForm.addClass('comment-inline-form-edit')
}
var tmpl = $('#cb-comment-inline-form-template').html();
tmpl = tmpl.format(escapeHtml(f_path), lineno);
$form = $(tmpl);
$comment.after($form)
$comment.after($editForm)
var _form = $($form[0]).find('form');
var _form = $($editForm[0]).find('form');
var autocompleteActions = ['as_note',];
var commentForm = this.createCommentForm(
_form, lineno, '', autocompleteActions, resolvesCommentId,
_form, line_no, '', autocompleteActions, resolvesCommentId,
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();
tooltipActivate();
$.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
form: _form,
parent: $comments,
lineno: lineno,
f_path: f_path}
);
// set a CUSTOM submit handler for inline comments.
commentForm.setHandleFormSubmit(function(o) {
// set a CUSTOM submit handler for inline comment edit action.
commentForm.setHandleFormSubmit(function(o) {
var text = commentForm.cm.getValue();
var commentType = commentForm.getCommentType();
@ -1048,7 +1079,7 @@ var CommentsController = function() {
var postData = {
'text': text,
'f_path': f_path,
'line': lineno,
'line': line_no,
'comment_type': commentType,
'draft': isDraft,
'version': version,
@ -1056,7 +1087,7 @@ var CommentsController = function() {
};
var submitSuccessCallback = function(json_data) {
$form.remove();
$editForm.remove();
$comment.show();
var postData = {
'text': text,
@ -1121,8 +1152,7 @@ var CommentsController = function() {
'commit_id': templateContext.commit_data.commit_id});
_submitAjaxPOST(
previewUrl, postData, successRenderCommit,
failRenderCommit
previewUrl, postData, successRenderCommit, failRenderCommit
);
try {
@ -1178,49 +1208,103 @@ var CommentsController = function() {
});
}
$form.addClass('comment-inline-form-open');
$editForm.addClass('comment-inline-form-open');
};
this.createComment = function(node, resolutionComment) {
var resolvesCommentId = resolutionComment || null;
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);
}
this.createComment = function(node, f_path, line_no, resolutionComment) {
self.edit = false;
var $node = $(node);
var $td = $node.closest('td');
var $form = $td.find('.comment-inline-form');
this.edit = false;
var resolvesCommentId = resolutionComment || null;
if (!$form.length) {
var $replyForm = $td.find('.comment-inline-form');
var $filediff = $node.closest('.filediff');
$filediff.removeClass('hide-comments');
var f_path = $filediff.attr('data-f-path');
var lineno = self.getLineNumber(node);
// create a new HTML from template
var tmpl = $('#cb-comment-inline-form-template').html();
tmpl = tmpl.format(escapeHtml(f_path), lineno);
$form = $(tmpl);
// 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);
var $comments = $td.find('.inline-comments');
if (!$comments.length) {
$comments = $(
$('#cb-comments-inline-container-template').html());
$td.append($comments);
// There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
if ($comments.length===0) {
var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
var $reply_container = $('#cb-comments-inline-container-template')
$reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
$td.append($($reply_container).html());
}
$td.find('.cb-comment-add-button').before($form);
// 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');
var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
var _form = $($form[0]).find('form');
var lastComment = $comments.find('.comment-inline').last();
if ($(lastComment).hasClass('comment-outdated')) {
$replyWrapper.show();
}
var _form = $($replyForm[0]).find('form');
var autocompleteActions = ['as_note', 'as_todo'];
var comment_id=null;
var commentForm = this.createCommentForm(
_form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
$.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
form: _form,
parent: $td[0],
lineno: lineno,
f_path: f_path}
);
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);
// set a CUSTOM submit handler for inline comments.
commentForm.setHandleFormSubmit(function(o) {
@ -1233,12 +1317,13 @@ var CommentsController = function() {
return;
}
if (lineno === undefined) {
alert('missing line !');
if (line_no === undefined) {
alert('Error: unable to fetch line number for this inline comment !');
return;
}
if (f_path === undefined) {
alert('missing file path !');
alert('Error: unable to fetch file path for this inline comment !');
return;
}
@ -1249,7 +1334,7 @@ var CommentsController = function() {
var postData = {
'text': text,
'f_path': f_path,
'line': lineno,
'line': line_no,
'comment_type': commentType,
'draft': isDraft,
'csrf_token': CSRF_TOKEN
@ -1258,32 +1343,32 @@ var CommentsController = function() {
postData['resolves_comment_id'] = resolvesCommentId;
}
// submitSuccess for inline commits
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);
$replyForm.remove();
$td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
//mark visually which comment was resolved
if (resolvesCommentId) {
commentForm.markCommentResolved(resolvesCommentId);
try {
// inject newly created comments, json_data is {<comment_id>: {}}
self.attachInlineComment(json_data)
//mark visually which comment was resolved
if (resolvesCommentId) {
commentForm.markCommentResolved(resolvesCommentId);
}
// run global callback on submit
commentForm.globalSubmitSuccessCallback({
draft: isDraft,
comment_id: comment_id
});
} catch (e) {
console.error(e);
}
// run global callback on submit
commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
} 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
@ -1297,19 +1382,27 @@ var CommentsController = function() {
commentForm.setActionButtonsDisabled(false);
// re trigger the linkification of next/prev navigation
linkifyComments($('.inline-comment-injected'));
timeagoActivate();
tooltipActivate();
};
var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
var prefix = "Error while submitting comment.\n"
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
ajaxErrorSwal(message);
commentForm.resetCommentFormState(text)
};
commentForm.submitAjaxPOST(
commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
});
}
$form.addClass('comment-inline-form-open');
// 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();
};
this.createResolutionComment = function(commentId){
@ -1319,9 +1412,12 @@ var CommentsController = function() {
var comment = $('#comment-'+commentId);
var commentData = comment.data();
if (commentData.commentInline) {
this.createComment(comment, commentId)
var f_path = commentData.fPath;
var line_no = commentData.lineNo;
//TODO check this if we need to give f_path/line_no
this.createComment(comment, f_path, line_no, commentId)
} else {
Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
this.createGeneralComment('general', "$placeholder", commentId)
}
return false;
@ -1347,3 +1443,8 @@ var CommentsController = function() {
};
};
window.commentHelp = function(renderer) {
var funcData = {'renderer': renderer}
return renderTemplate('commentHelpHovercard', funcData)
}

View file

@ -42,12 +42,22 @@ window.toggleElement = function (elem, target) {
var $elem = $(elem);
var $target = $(target);
if ($target.is(':visible') || $target.length === 0) {
if (target !== undefined) {
var show = $target.is(':visible') || $target.length === 0;
} else {
var show = $elem.hasClass('toggle-off')
}
if (show) {
$target.hide();
$elem.html($elem.data('toggleOn'))
$elem.addClass('toggle-on')
$elem.removeClass('toggle-off')
} else {
$target.show();
$elem.html($elem.data('toggleOff'))
$elem.addClass('toggle-off')
$elem.removeClass('toggle-on')
}
return false

View file

@ -1,7 +1,5 @@
/__MAIN_APP__ - launched when rhodecode-app element is attached to DOM
/plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed
/ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview
/ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created
/notifications - shows new event notifications
/connection_controller/subscribe - subscribes user to new channels
/connection_controller/presence - receives presence change messages

View file

@ -1,4 +1,4 @@
## this is a dummy html file for partial rendering on server and sending
## generated output via ajax after comment submit
<%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
${comment.comment_block(c.co, inline=c.co.is_inline)}
${comment.comment_block(c.co, inline=c.co.is_inline, is_new=c.is_new)}

View file

@ -10,7 +10,7 @@
%>
<%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
<%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
<%
from rhodecode.model.comment import CommentsModel
@ -40,6 +40,7 @@
data-comment-draft=${h.json.dumps(comment.draft)}
data-comment-renderer="${comment.renderer}"
data-comment-text="${comment.text | html_filters.base64,n}"
data-comment-f-path="${comment.f_path}"
data-comment-line-no="${comment.line_no}"
data-comment-inline=${h.json.dumps(inline)}
style="${'display: none;' if outdated_at_ver else ''}">
@ -47,8 +48,17 @@
<div class="meta">
<div class="comment-type-label">
% if comment.draft:
<div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">DRAFT</div>
<div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
DRAFT
</div>
% endif
% if is_new:
<div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
NEW
</div>
% endif
<div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
## TODO COMMENT
@ -176,7 +186,7 @@
% if inline:
<a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
% if outdated_at_ver:
<code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">outdated ${'v{}'.format(comment_ver)}</code>
<strong class="comment-outdated-label">outdated</strong> <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
<code class="action-divider">|</code>
% elif comment_ver:
<code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
@ -222,12 +232,13 @@
%if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
<div class="dropdown-divider"></div>
<div class="dropdown-item">
<a onclick="return Rhodecode.comments.editComment(this);" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
<a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
</div>
<div class="dropdown-item">
<a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
</div>
% if comment.draft:
## Only available in EE edition
% if comment.draft and c.rhodecode_edition_id == 'EE':
<div class="dropdown-item">
<a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
</div>
@ -391,7 +402,7 @@
<div class="comment-area-write" style="display: block;">
<div id="edit-container">
<div style="padding: 40px 0">
<div style="padding: 20px 0px 0px 0;">
${_('You need to be logged in to leave comments.')}
<a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
</div>
@ -450,7 +461,7 @@
</div>
<div class="comment-area-write" style="display: block;">
<div id="edit-container_${lineno_id}">
<div id="edit-container_${lineno_id}" style="margin-top: -1px">
<textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
</div>
<div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
@ -500,40 +511,47 @@
<input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
% if form_type == 'inline':
<input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
% if c.rhodecode_edition_id == 'EE':
## Disable the button for CE, the "real" validation is in the backend code anyway
<input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
% else:
<input class="tooltip btn btn-warning comment-button-input submit-draft-action disabled" type="submit" disabled="disabled" value="${_('Add draft')}" onclick="return false;" title="Draft comments only available in EE edition of RhodeCode">
% endif
% endif
% if review_statuses:
<div class="comment-status-box">
<select id="change_status_${lineno_id}" name="changeset_status">
<option></option> ## Placeholder
% for status, lbl in review_statuses:
<option value="${status}" data-status="${status}">${lbl}</option>
%if is_pull_request and change_status and status in ('approved', 'rejected'):
<option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
%endif
% endfor
</select>
</div>
% endif
## inline for has a file, and line-number together with cancel hide button.
% if form_type == 'inline':
<input type="hidden" name="f_path" value="{0}">
<input type="hidden" name="line" value="${lineno_id}">
<button type="button" class="tooltip cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);" title="Hide comment box">
<button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
<i class="icon-cancel-circled2"></i>
</button>
% endif
</div>
% if review_statuses:
<div class="status_box">
<select id="change_status_${lineno_id}" name="changeset_status">
<option></option> ## Placeholder
% for status, lbl in review_statuses:
<option value="${status}" data-status="${status}">${lbl}</option>
%if is_pull_request and change_status and status in ('approved', 'rejected'):
<option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
%endif
% endfor
</select>
</div>
% endif
<div class="toolbar-text">
<% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
<span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
${_('and')}
<span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
${_('actions supported.')}
<p>${_('Styling with {} is supported.').format(renderer_url)|n}
<i class="icon-info-circled tooltip-hovercard"
data-hovercard-alt="ALT"
data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
</p>
</div>
</div>

View file

@ -1,3 +1,4 @@
<%namespace name="base" file="/base/base.mako"/>
<%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
<%def name="diff_line_anchor(commit, filename, line, type)"><%
@ -74,24 +75,9 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
<div class="js-template" id="cb-comment-inline-form-template">
<div class="comment-inline-form ac">
%if c.rhodecode_user.username != h.DEFAULT_USER:
%if not c.rhodecode_user.is_default:
## render template for inline comments
${commentblock.comment_form(form_type='inline')}
%else:
${h.form('', class_='inline-form comment-form-login', method='get')}
<div class="pull-left">
<div class="comment-help pull-right">
${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
</div>
</div>
<div class="comment-button pull-right">
<button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
${_('Cancel')}
</button>
</div>
<div class="clearfix"></div>
${h.end_form()}
%endif
</div>
</div>
@ -327,7 +313,7 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
</label>
${diff_menu(filediff, use_comments=use_comments)}
<table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
<table id="file-${h.safeid(h.safe_unicode(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
## new/deleted/empty content case
% if not filediff.hunks:
@ -626,8 +612,10 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
% if use_comments:
|
<a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
<span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
<a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
data-toggle-on="${_('Hide comments')}"
data-toggle-off="${_('Show comments')}">
<span class="hide-comment-button">${_('Hide comments')}</span>
</a>
% endif
@ -637,23 +625,36 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
</%def>
<%def name="inline_comments_container(comments, active_pattern_entries=None)">
<%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
<div class="inline-comments">
%for comment in comments:
${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
%endfor
% if comments and comments[-1].outdated:
<span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
${_('Add another comment')}
</span>
% else:
<span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
${_('Add another comment')}
</span>
% endif
<%
extra_class = ''
extra_style = ''
if comments and comments[-1].outdated:
extra_class = ' comment-outdated'
extra_style = 'display: none;'
%>
<div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
<div class="reply-thread-container${extra_class}">
<div class="reply-thread-gravatar">
${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
</div>
<div class="reply-thread-reply-button">
## initial reply button, some JS logic can append here a FORM to leave a first comment.
<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
</div>
<div class="reply-thread-last"></div>
</div>
</div>
</div>
</%def>
<%!
@ -721,9 +722,9 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
%endif
%if line_old_comments_no_drafts:
% if has_outdated:
<i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
<i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
% else:
<i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
<i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
% endif
%endif
</td>
@ -737,16 +738,18 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
<a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
%endif
</td>
<% line_no = 'o{}'.format(line.original.lineno) %>
<td class="cb-content ${action_class(line.original.action)}"
data-line-no="o${line.original.lineno}"
data-line-no="${line_no}"
>
%if use_comments and line.original.lineno:
${render_add_comment_button()}
${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
%endif
<span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
%if use_comments and line.original.lineno and line_old_comments:
${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries)}
${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
%endif
</td>
@ -766,9 +769,9 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
%if line_new_comments_no_drafts:
% if has_outdated:
<i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
<i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
% else:
<i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
<i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
% endif
%endif
</div>
@ -783,22 +786,25 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
<a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
%endif
</td>
<% line_no = 'n{}'.format(line.modified.lineno) %>
<td class="cb-content ${action_class(line.modified.action)}"
data-line-no="n${line.modified.lineno}"
data-line-no="${line_no}"
>
%if use_comments and line.modified.lineno:
${render_add_comment_button()}
${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
%endif
<span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
%if use_comments and line.modified.lineno and line_new_comments:
${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries)}
%endif
% if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
<div class="nav-chunk" style="visibility: hidden">
<i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
</div>
<% chunk_count +=1 %>
% endif
%if use_comments and line.modified.lineno and line_new_comments:
${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
%endif
</td>
</tr>
%endfor
@ -830,9 +836,9 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
% if comments_no_drafts:
% if has_outdated:
<i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
<i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
% else:
<i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
<i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
% endif
% endif
</div>
@ -857,15 +863,16 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
<a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
%endif
</td>
<% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
<td class="cb-content ${action_class(action)}"
data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
data-line-no="${line_no}"
>
%if use_comments:
${render_add_comment_button()}
${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
%endif
<span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
%if use_comments and comments:
${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
%endif
</td>
</tr>
@ -886,10 +893,12 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
</%def>file changes
<%def name="render_add_comment_button()">
<button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
<%def name="render_add_comment_button(line_no='', f_path='')">
% if not c.rhodecode_user.is_default:
<button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
<span><i class="icon-comment"></i></span>
</button>
% endif
</%def>
<%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">

View file

@ -456,7 +456,7 @@ ${h.style_metatag(tag_type, tag)|n,trim}
</div>
<div class="markup-form-area-write" style="display: block;">
<div id="edit-container_${form_id}">
<div id="edit-container_${form_id}" style="margin-top: -1px">
<textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
</div>
<div id="preview-container_${form_id}" class="clearfix" style="display: none;">

View file

@ -237,6 +237,24 @@ if (show_disabled) {
</script>
<script id="ejs_commentHelpHovercard" type="text/template" class="ejsTemplate">
<div>
Use <strong>@username</strong> mention syntax to send direct notification to this RhodeCode user.<br/>
Typing / starts autocomplete for certain action, e.g set review status, or comment type. <br/>
<br/>
Use <strong>Cmd/ctrl+enter</strong> to submit comment, or <strong>Shift+Cmd/ctrl+enter</strong> to submit a draft.<br/>
<br/>
<strong>Draft comments</strong> are private to the author, and trigger no notification to others.<br/>
They are permanent until deleted, or converted to regular comments.<br/>
<br/>
<br/>
</div>
</script>
##// END OF EJS Templates
</div>

View file

@ -846,6 +846,7 @@ versionController.init();
reviewersController = new ReviewersController();
commitsController = new CommitsController();
commentsController = new CommentsController();
updateController = new UpdatePrController();
@ -1002,6 +1003,8 @@ $(document).ready(function () {
alert('okok !' + commentIds)
}
// register globally so inject comment logic can re-use it.
window.commentsController = commentsController;
})
</script>