pull-requests: overhaul of the UX by adding new sidebar

- sidebar aggregates comments, todos, observers, referenced tickets, and reviewers
- live components push now comments, reviewer presence, and todo live via channelstream
- sidebar is collapsable, and stored in user session
- simplified number of information on the pull request page
- fixed and cleanup of the outdated comments and at version comments that in some cases reported wrong version assignment.
This commit is contained in:
Marcin Kuzminski 2020-09-01 12:09:40 +02:00
parent 9106e99baf
commit 9430615145
18 changed files with 1132 additions and 639 deletions

View file

@ -345,6 +345,16 @@ def includeme(config):
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
repo_route=True, repo_accepted_types=['hg', 'git'])
config.add_route(
name='pullrequest_comments',
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
repo_route=True)
config.add_route(
name='pullrequest_todos',
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
repo_route=True)
# Artifacts, (EE feature)
config.add_route(
name='repo_artifacts_list',

View file

@ -87,6 +87,7 @@ class RepoCommitsView(RepoAppView):
diff_limit = c.visual.cut_off_limit_diff
file_limit = c.visual.cut_off_limit_file
# get ranges of commit ids if preset
commit_range = commit_id_range.split('...')[:2]
@ -226,6 +227,7 @@ class RepoCommitsView(RepoAppView):
# sort comments by how they were generated
c.comments = sorted(c.comments, key=lambda x: x.comment_id)
c.at_version_num = None
if len(c.commit_ranges) == 1:
c.commit = c.commit_ranges[0]

View file

@ -265,6 +265,36 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
return diffset
def register_comments_vars(self, c, pull_request, versions):
comments_model = CommentsModel()
# GENERAL COMMENTS with versions #
q = comments_model._all_general_comments_of_pull_request(pull_request)
q = q.order_by(ChangesetComment.comment_id.asc())
general_comments = q
# pick comments we want to render at current version
c.comment_versions = comments_model.aggregate_comments(
general_comments, versions, c.at_version_num)
# INLINE COMMENTS with versions #
q = comments_model._all_inline_comments_of_pull_request(pull_request)
q = q.order_by(ChangesetComment.comment_id.asc())
inline_comments = q
c.inline_versions = comments_model.aggregate_comments(
inline_comments, versions, c.at_version_num, inline=True)
# Comments inline+general
if c.at_version:
c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
c.comments = c.comment_versions[c.at_version_num]['display']
else:
c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
c.comments = c.comment_versions[c.at_version_num]['until']
return general_comments, inline_comments
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@ -280,6 +310,8 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
pull_request_id = pull_request.pull_request_id
c.state_progressing = pull_request.is_state_changing()
c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
pull_request.target_repo.repo_name, pull_request.pull_request_id)
_new_state = {
'created': PullRequest.STATE_CREATED,
@ -300,22 +332,23 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
from_version = self.request.GET.get('from_version') or version
merge_checks = self.request.GET.get('merge_checks')
c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
force_refresh = str2bool(self.request.GET.get('force_refresh'))
c.range_diff_on = self.request.GET.get('range-diff') == "1"
# fetch global flags of ignore ws or context lines
diff_context = diffs.get_diff_context(self.request)
hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
force_refresh = str2bool(self.request.GET.get('force_refresh'))
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=version)
pr_closed = pull_request_latest.is_closed()
if pr_closed and (version or from_version):
# not allow to browse versions
# not allow to browse versions for closed PR
raise HTTPFound(h.route_path(
'pullrequest_show', repo_name=self.db_repo_name,
pull_request_id=pull_request_id))
@ -323,13 +356,13 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
versions = pull_request_display_obj.versions()
# used to store per-commit range diffs
c.changes = collections.OrderedDict()
c.range_diff_on = self.request.GET.get('range-diff') == "1"
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != 'latest'
if at_version and at_version != PullRequest.LATEST_VER
else None)
c.at_version_pos = ChangesetComment.get_index_from_version(
c.at_version_index = ChangesetComment.get_index_from_version(
c.at_version_num, versions)
(prev_pull_request_latest,
@ -340,9 +373,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
c.from_version = prev_at_version
c.from_version_num = (prev_at_version
if prev_at_version and prev_at_version != 'latest'
if prev_at_version and prev_at_version != PullRequest.LATEST_VER
else None)
c.from_version_pos = ChangesetComment.get_index_from_version(
c.from_version_index = ChangesetComment.get_index_from_version(
c.from_version_num, versions)
# define if we're in COMPARE mode or VIEW at version mode
@ -355,14 +388,17 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
raise HTTPNotFound()
c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
pull_request_at_ver)
c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
c.pull_request = pull_request_display_obj
c.renderer = pull_request_at_ver.description_renderer or c.renderer
c.pull_request_latest = pull_request_latest
if compare or (at_version and not at_version == 'latest'):
# inject latest version
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
if compare or (at_version and not at_version == PullRequest.LATEST_VER):
c.allowed_to_change_status = False
c.allowed_to_update = False
c.allowed_to_merge = False
@ -391,12 +427,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
'rules' in pull_request_latest.reviewer_data:
rules = pull_request_latest.reviewer_data['rules'] or {}
try:
c.forbid_adding_reviewers = rules.get(
'forbid_adding_reviewers')
c.forbid_author_to_review = rules.get(
'forbid_author_to_review')
c.forbid_commit_author_to_review = rules.get(
'forbid_commit_author_to_review')
c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
c.forbid_author_to_review = rules.get('forbid_author_to_review')
c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
except Exception:
pass
@ -421,41 +454,37 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
return self._get_template_context(c)
comments_model = CommentsModel()
c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
# reviewers and statuses
c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
# GENERAL COMMENTS with versions #
q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
q = q.order_by(ChangesetComment.comment_id.asc())
general_comments = q
for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
member_reviewer = h.reviewer_as_json(
member, reasons=reasons, mandatory=mandatory,
user_group=review_obj.rule_user_group_data()
)
# pick comments we want to render at current version
c.comment_versions = comments_model.aggregate_comments(
general_comments, versions, c.at_version_num)
c.comments = c.comment_versions[c.at_version_num]['until']
current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
member_reviewer['review_status'] = current_review_status
member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
member_reviewer['allowed_to_update'] = c.allowed_to_update
c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
# INLINE COMMENTS with versions #
q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
q = q.order_by(ChangesetComment.comment_id.asc())
inline_comments = q
c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
c.inline_versions = comments_model.aggregate_comments(
inline_comments, versions, c.at_version_num, inline=True)
general_comments, inline_comments = \
self.register_comments_vars(c, pull_request_latest, versions)
# TODOs
c.unresolved_comments = CommentsModel() \
.get_pull_request_unresolved_todos(pull_request)
.get_pull_request_unresolved_todos(pull_request_latest)
c.resolved_comments = CommentsModel() \
.get_pull_request_resolved_todos(pull_request)
# inject latest version
latest_ver = PullRequest.get_pr_display_object(
pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
.get_pull_request_resolved_todos(pull_request_latest)
# if we use version, then do not show later comments
# than current version
@ -522,8 +551,8 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
# empty version means latest, so we keep this to prevent
# double caching
version_normalized = version or 'latest'
from_version_normalized = from_version or 'latest'
version_normalized = version or PullRequest.LATEST_VER
from_version_normalized = from_version or PullRequest.LATEST_VER
cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
cache_file_path = diff_cache_exist(
@ -615,7 +644,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
diff_limit, file_limit, c.fulldiff,
hide_whitespace_changes, diff_context,
use_ancestor=use_ancestor
)
)
# save cached diff
if caching_enabled:
@ -719,7 +748,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
# current user review statuses for each version
c.review_versions = {}
if self._rhodecode_user.user_id in allowed_reviewers:
if self._rhodecode_user.user_id in c.allowed_reviewers:
for co in general_comments:
if co.author.user_id == self._rhodecode_user.user_id:
status = co.status_change
@ -935,6 +964,83 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
}
return data
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='pullrequest_comments', request_method='POST',
renderer='string', xhr=True)
def pullrequest_comments(self):
self.load_default_context()
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
pull_request_id = pull_request.pull_request_id
version = self.request.GET.get('version')
_render = self.request.get_partial_renderer(
'rhodecode:templates/pullrequests/pullrequest_show.mako')
c = _render.get_call_context()
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=version)
versions = pull_request_display_obj.versions()
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != PullRequest.LATEST_VER
else None)
self.register_comments_vars(c, pull_request_latest, versions)
all_comments = c.inline_comments_flat + c.comments
return _render('comments_table', all_comments, len(all_comments))
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='pullrequest_todos', request_method='POST',
renderer='string', xhr=True)
def pullrequest_todos(self):
self.load_default_context()
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
pull_request_id = pull_request.pull_request_id
version = self.request.GET.get('version')
_render = self.request.get_partial_renderer(
'rhodecode:templates/pullrequests/pullrequest_show.mako')
c = _render.get_call_context()
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=version)
versions = pull_request_display_obj.versions()
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != PullRequest.LATEST_VER
else None)
c.unresolved_comments = CommentsModel() \
.get_pull_request_unresolved_todos(pull_request)
c.resolved_comments = CommentsModel() \
.get_pull_request_resolved_todos(pull_request)
all_comments = c.unresolved_comments + c.resolved_comments
return _render('comments_table', all_comments, len(c.unresolved_comments), todo_comments=True)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
@ -1100,7 +1206,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
self.request.matchdict['pull_request_id'])
_ = self.request.translate
self.load_default_context()
c = self.load_default_context()
redirect_url = None
if pull_request.is_closed():
@ -1111,6 +1217,8 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
'redirect_url': redirect_url}
is_state_changing = pull_request.is_state_changing()
c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
pull_request.target_repo.repo_name, pull_request.pull_request_id)
# only owner or admin can update it
allowed_to_update = PullRequestModel().check_user_update(
@ -1134,7 +1242,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
return {'response': True,
'redirect_url': redirect_url}
self._update_commits(pull_request)
self._update_commits(c, pull_request)
if force_refresh:
redirect_url = h.route_path(
'pullrequest_show', repo_name=self.db_repo_name,
@ -1170,7 +1278,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
h.flash(msg, category='success')
return
def _update_commits(self, pull_request):
def _update_commits(self, c, pull_request):
_ = self.request.translate
with pull_request.set_state(PullRequest.STATE_UPDATING):
@ -1198,13 +1306,18 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
change_source=changed)
h.flash(msg, category='success')
channel = '/repo${}$/pr/{}'.format(
pull_request.target_repo.repo_name, pull_request.pull_request_id)
message = msg + (
' - <a onclick="window.location.reload()">'
'<strong>{}</strong></a>'.format(_('Reload page')))
message_obj = {
'message': message,
'level': 'success',
'topic': '/notifications'
}
channelstream.post_message(
channel, message, self._rhodecode_user.username,
c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
registry=self.request.registry)
else:
msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
@ -1474,6 +1587,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
}
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)

View file

@ -810,8 +810,7 @@ import tzlocal
local_timezone = tzlocal.get_localzone()
def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
title = value or format_date(datetime_iso)
def get_timezone(datetime_iso, time_is_local=False):
tzinfo = '+00:00'
# detect if we have a timezone info, otherwise, add it
@ -822,6 +821,12 @@ def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
timezone = force_timezone or local_timezone
offset = timezone.localize(datetime_iso).strftime('%z')
tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
return tzinfo
def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
title = value or format_date(datetime_iso)
tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
return literal(
'<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
@ -1650,17 +1655,18 @@ def get_active_pattern_entries(repo_name):
pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
allowed_link_formats = [
'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
allowed_formats = ['html', 'rst', 'markdown',
'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
if link_format not in allowed_formats:
if link_format not in allowed_link_formats:
raise ValueError('Link format can be only one of:{} got {}'.format(
allowed_formats, link_format))
allowed_link_formats, link_format))
if active_entries is None:
log.debug('Fetch active patterns for repo: %s', repo_name)
log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
active_entries = get_active_pattern_entries(repo_name)
issues_data = []
@ -1718,7 +1724,8 @@ def process_patterns(text_string, repo_name, link_format='html', active_entries=
return new_text, issues_data
def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
issues_container=None):
"""
Parses given text message and makes proper links.
issues are linked to given issue-server, and rest is a commit link
@ -1741,6 +1748,9 @@ def urlify_commit_message(commit_text, repository=None, active_pattern_entries=N
new_text, issues = process_patterns(new_text, repository or '',
active_entries=active_pattern_entries)
if issues_container is not None:
issues_container.extend(issues)
return literal(new_text)
@ -1781,7 +1791,7 @@ def renderer_from_filename(filename, exclude=None):
def render(source, renderer='rst', mentions=False, relative_urls=None,
repo_name=None, active_pattern_entries=None):
repo_name=None, active_pattern_entries=None, issues_container=None):
def maybe_convert_relative_links(html_source):
if relative_urls:
@ -1798,6 +1808,8 @@ def render(source, renderer='rst', mentions=False, relative_urls=None,
source, issues = process_patterns(
source, repo_name, link_format='rst',
active_entries=active_pattern_entries)
if issues_container is not None:
issues_container.extend(issues)
return literal(
'<div class="rst-block">%s</div>' %
@ -1810,6 +1822,8 @@ def render(source, renderer='rst', mentions=False, relative_urls=None,
source, issues = process_patterns(
source, repo_name, link_format='markdown',
active_entries=active_pattern_entries)
if issues_container is not None:
issues_container.extend(issues)
return literal(
'<div class="markdown-block">%s</div>' %

View file

@ -91,8 +91,7 @@ class CommentsModel(BaseModel):
# group by versions, and count until, and display objects
comment_groups = collections.defaultdict(list)
[comment_groups[
_co.pull_request_version_id].append(_co) for _co in comments]
[comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
def yield_comments(pos):
for co in comment_groups[pos]:
@ -456,38 +455,54 @@ class CommentsModel(BaseModel):
else:
action = 'repo.commit.comment.create'
comment_id = comment.comment_id
comment_data = comment.get_api_data()
self._log_audit_action(
action, {'data': comment_data}, auth_user, comment)
msg_url = ''
channel = None
if commit_obj:
msg_url = commit_comment_url
repo_name = repo.repo_name
channel = u'/repo${}$/commit/{}'.format(
repo_name,
commit_obj.raw_id
)
elif pull_request_obj:
msg_url = pr_comment_url
repo_name = pr_target_repo.repo_name
channel = u'/repo${}$/pr/{}'.format(
repo_name,
pull_request_id
pull_request_obj.pull_request_id
)
message = '<strong>{}</strong> {} - ' \
'<a onclick="window.location=\'{}\';' \
'window.location.reload()">' \
'<strong>{}</strong></a>'
message = message.format(
user.username, _('made a comment'), msg_url,
_('Show it now'))
if channel:
username = user.username
message = '<strong>{}</strong> {} #{}, {}'
message = message.format(
username,
_('posted a new comment'),
comment_id,
_('Refresh the page to see new comments.'))
channelstream.post_message(
channel, message, user.username,
registry=get_current_registry())
message_obj = {
'message': message,
'level': 'success',
'topic': '/notifications'
}
channelstream.post_message(
channel, message_obj, user.username,
registry=get_current_registry())
message_obj = {
'message': None,
'user': username,
'comment_id': comment_id,
'topic': '/comment'
}
channelstream.post_message(
channel, message_obj, user.username,
registry=get_current_registry())
return comment
@ -642,15 +657,15 @@ class CommentsModel(BaseModel):
return self._group_comments_by_path_and_line_number(q)
def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
version=None):
inline_cnt = 0
version=None):
inline_comms = []
for fname, per_line_comments in inline_comments.iteritems():
for lno, comments in per_line_comments.iteritems():
for comm in comments:
if not comm.outdated_at_version(version) and skip_outdated:
inline_cnt += 1
inline_comms.append(comm)
return inline_cnt
return inline_comms
def get_outdated_comments(self, repo_id, pull_request):
# TODO: johbo: Remove `repo_id`, it is not needed to find the comments

View file

@ -3821,16 +3821,35 @@ class ChangesetComment(Base, BaseModel):
"""
Checks if comment is outdated for given pull request version
"""
return self.outdated and self.pull_request_version_id != version
def version_check():
return self.pull_request_version_id and self.pull_request_version_id != version
if self.is_inline:
return self.outdated and version_check()
else:
# general comments don't have .outdated set, also latest don't have a version
return version_check()
def outdated_at_version_js(self, version):
"""
Checks if comment is outdated for given pull request version
"""
return json.dumps(self.outdated_at_version(version))
def older_than_version(self, version):
"""
Checks if comment is made from previous version than given
"""
if version is None:
return self.pull_request_version_id is not None
return self.pull_request_version != version
return self.pull_request_version_id < version
return self.pull_request_version < version
def older_than_version_js(self, version):
"""
Checks if comment is made from previous version than given
"""
return json.dumps(self.older_than_version(version))
@property
def commit_id(self):
@ -4327,6 +4346,7 @@ class PullRequest(Base, _PullRequestBase):
__table_args__ = (
base_table_args,
)
LATEST_VER = 'latest'
pull_request_id = Column(
'pull_request_id', Integer(), nullable=False, primary_key=True)
@ -4385,6 +4405,10 @@ class PullRequest(Base, _PullRequestBase):
def pull_request_version_id(self):
return getattr(pull_request_obj, 'pull_request_version_id', None)
@property
def pull_request_last_version(self):
return pull_request_obj.pull_request_last_version
attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
attrs.author = StrictAttributeDict(
@ -4449,6 +4473,10 @@ class PullRequest(Base, _PullRequestBase):
"""
return self.versions.count() + 1
@property
def pull_request_last_version(self):
return self.versions_count
class PullRequestVersion(Base, _PullRequestBase):
__tablename__ = 'pull_request_versions'

View file

@ -240,14 +240,14 @@ div.markdown-block ol {
div.markdown-block ul.checkbox li,
div.markdown-block ol.checkbox li {
list-style: none !important;
margin: 6px !important;
margin: 0px !important;
padding: 0 !important;
}
div.markdown-block ul li,
div.markdown-block ol li {
list-style: disc !important;
margin: 6px !important;
margin: 0px !important;
padding: 0 !important;
}

View file

@ -1510,18 +1510,14 @@ table.integrations {
min-height: 55px;
}
.reviewers_member {
width: 100%;
overflow: auto;
}
.reviewer_reason {
padding-left: 20px;
line-height: 1.5em;
}
.reviewer_status {
display: inline-block;
width: 25px;
min-width: 25px;
width: 20px;
min-width: 20px;
height: 1.2em;
line-height: 1em;
}
@ -1544,23 +1540,17 @@ table.integrations {
}
.reviewer_member_mandatory {
position: absolute;
left: 15px;
top: 8px;
width: 16px;
font-size: 11px;
margin: 0;
padding: 0;
color: black;
opacity: 0.4;
}
.reviewer_member_mandatory_remove,
.reviewer_member_remove {
position: absolute;
right: 0;
top: 0;
width: 16px;
margin-bottom: 10px;
padding: 0;
color: black;
}
@ -1617,7 +1607,8 @@ table.integrations {
.td-todo-number {
text-align: left;
white-space: nowrap;
width: 15%;
width: 1%;
padding-right: 2px;
}
.td-todo-gravatar {
@ -1641,10 +1632,13 @@ table.integrations {
text-overflow: ellipsis;
}
table.group_members {
width: 100%
}
.group_members {
margin-top: 0;
padding: 0;
list-style: outside none none;
img {
height: @gravatar-size;

View file

@ -246,6 +246,8 @@ function registerRCRoutes() {
pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
pyroutes.register('pullrequest_comment_edit', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/edit', ['repo_name', 'pull_request_id', 'comment_id']);
pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);

View file

@ -28,9 +28,12 @@ export class RhodecodeApp extends PolymerElement {
super.connectedCallback();
ccLog.debug('rhodeCodeApp created');
$.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
$.Topic('/comment').subscribe(this.handleComment.bind(this));
$.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
$.Topic('/connection_controller/subscribe').subscribe(
this.subscribeToChannelTopic.bind(this));
this.subscribeToChannelTopic.bind(this)
);
// this event can be used to coordinate plugins to do their
// initialization before channelstream is kicked off
$.Topic('/__MAIN_APP__').publish({});
@ -71,6 +74,14 @@ export class RhodecodeApp extends PolymerElement {
}
handleComment(data) {
if (data.message.comment_id) {
if (window.refreshAllComments !== undefined) {
refreshAllComments()
}
}
}
faviconUpdate(data) {
this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
}
@ -95,6 +106,7 @@ export class RhodecodeApp extends PolymerElement {
}
// append any additional channels registered in other plugins
$.Topic('/connection_controller/subscribe').processPrepared();
channelstreamConnection.connect();
}
}
@ -157,8 +169,7 @@ export class RhodecodeApp extends PolymerElement {
handleConnected(event) {
var channelstreamConnection = this.getChannelStreamConnection();
channelstreamConnection.set('channelsState',
event.detail.channels_info);
channelstreamConnection.set('channelsState', event.detail.channels_info);
channelstreamConnection.set('userState', event.detail.state);
channelstreamConnection.set('channels', event.detail.channels);
this.propagageChannelsState();

View file

@ -677,7 +677,9 @@ var feedLifetimeOptions = function(query, initialData){
query.callback(data);
};
/*
* Retrievew via templateContext.session_attrs.key
* */
var storeUserSessionAttr = function (key, val) {
var postData = {

View file

@ -558,7 +558,7 @@ var CommentsController = function() {
return false;
};
this.showVersion = function (comment_id, comment_history_id) {
this.showVersion = function (comment_id, comment_history_id) {
var historyViewUrl = pyroutes.url(
'repo_commit_comment_history_view',
@ -585,7 +585,7 @@ var CommentsController = function() {
successRenderCommit,
failRenderCommit
);
};
};
this.getLineNumber = function(node) {
var $node = $(node);
@ -670,8 +670,20 @@ var CommentsController = function() {
var success = function(response) {
$comment.remove();
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()
}
return false;
};
var failure = function(jqXHR, textStatus, errorThrown) {
var prefix = "Error while deleting this comment.\n"
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
@ -682,6 +694,9 @@ var CommentsController = function() {
return false;
};
ajaxPOST(url, postData, success, failure);
}
this.deleteComment = function(node) {
@ -727,6 +742,15 @@ var CommentsController = function() {
$filediff.find('.hide-line-comments').removeClass('hide-line-comments');
$filediff.toggleClass('hide-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()
}
return false;
};
@ -747,7 +771,7 @@ var CommentsController = function() {
var cm = commentForm.getCmInstance();
if (resolvesCommentId){
var placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
}
setTimeout(function() {
@ -1077,9 +1101,15 @@ var CommentsController = function() {
updateSticky()
}
if (window.refreshAllComments !== undefined) {
// if we have this handler, run it, and refresh all comments boxes
refreshAllComments()
}
commentForm.setActionButtonsDisabled(false);
};
var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
var prefix = "Error while editing comment.\n"
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
@ -1209,6 +1239,11 @@ var CommentsController = function() {
updateSticky()
}
if (window.refreshAllComments !== undefined) {
// if we have this handler, run it, and refresh all comments boxes
refreshAllComments()
}
commentForm.setActionButtonsDisabled(false);
};

View file

@ -98,10 +98,13 @@ ReviewersController = function () {
var self = this;
this.$reviewRulesContainer = $('#review_rules');
this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
this.$userRule = $('.pr-user-rule-container');
this.forbidReviewUsers = undefined;
this.$reviewMembers = $('#review_members');
this.currentRequest = null;
this.diffData = null;
this.enabledRules = [];
//dummy handler, we might register our own later
this.diffDataHandler = function(data){};
@ -116,14 +119,17 @@ ReviewersController = function () {
this.hideReviewRules = function () {
self.$reviewRulesContainer.hide();
$(self.$userRule.selector).hide();
};
this.showReviewRules = function () {
self.$reviewRulesContainer.show();
$(self.$userRule.selector).show();
};
this.addRule = function (ruleText) {
self.showReviewRules();
self.enabledRules.push(ruleText);
return '<div>- {0}</div>'.format(ruleText)
};
@ -179,6 +185,7 @@ ReviewersController = function () {
_gettext('Reviewers picked from source code changes.'))
)
}
if (data.rules.forbid_adding_reviewers) {
$('#add_reviewer_input').remove();
self.$rulesList.append(
@ -186,6 +193,7 @@ ReviewersController = function () {
_gettext('Adding new reviewers is forbidden.'))
)
}
if (data.rules.forbid_author_to_review) {
self.forbidReviewUsers.push(data.rules_data.pr_author);
self.$rulesList.append(
@ -193,6 +201,7 @@ ReviewersController = function () {
_gettext('Author is not allowed to be a reviewer.'))
)
}
if (data.rules.forbid_commit_author_to_review) {
if (data.rules_data.forbidden_users) {
@ -208,6 +217,12 @@ ReviewersController = function () {
)
}
// we don't have any rules set, so we inform users about it
if (self.enabledRules.length === 0) {
self.addRule(
_gettext('No review rules set.'))
}
return self.forbidReviewUsers
};
@ -347,11 +362,12 @@ ReviewersController = function () {
members.innerHTML += renderTemplate('reviewMemberEntry', {
'member': reviewer_obj,
'mandatory': mandatory,
'reasons': reasons,
'allowed_to_update': true,
'review_status': 'not_reviewed',
'review_status_label': _gettext('Not Reviewed'),
'reasons': reasons,
'create': true
'user_group': reviewer_obj.user_group,
'create': true,
});
tooltipActivate();
}
@ -600,7 +616,7 @@ VersionController = function () {
var $elem = $(elem);
var $target = $(target);
if ($target.is(':visible')) {
if ($target.is(':visible') || $target.length === 0) {
$target.hide();
$elem.html($elem.data('toggleOn'))
} else {

View file

@ -10,12 +10,14 @@
<%namespace name="base" file="/base/base.mako"/>
<%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
<% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
<% comment_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
<% latest_ver = len(getattr(c, 'versions', [])) %>
% if inline:
<% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
<% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
% else:
<% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
<% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
% endif
<div class="comment
@ -153,38 +155,38 @@
</div>
%endif
<a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
<a class="permalink" href="#comment-${comment.comment_id}">&para; #${comment.comment_id}</a>
<div class="comment-links-block">
% 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(pr_index_ver, latest_ver)}">
outdated ${'v{}'.format(pr_index_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>
% elif pr_index_ver:
<code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
${'v{}'.format(pr_index_ver)} |
% 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>
% endif
</a>
% else:
% if pr_index_ver:
% if comment_ver:
% if comment.outdated:
<a class="pr-version"
href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
>
${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
</a> |
% else:
<a class="tooltip pr-version"
title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}"
title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
>
<code class="pr-version-num">
${'v{}'.format(pr_index_ver)}
${'v{}'.format(comment_ver)}
</code>
</a> |
% endif

View file

@ -21,8 +21,9 @@
## to speed up lookups cache some functions before the loop
<%
active_patterns = h.get_active_pattern_entries(c.repo_name)
urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns)
urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns, issues_container=getattr(c, 'referenced_commit_issues', None))
%>
%for commit in c.commit_ranges:
<tr id="row-${commit.raw_id}"
commit_id="${commit.raw_id}"

View file

@ -1,6 +1,10 @@
<%text>
<div style="display: none">
<script>
var CG = new ColorGenerator();
</script>
<script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
<%
@ -34,10 +38,6 @@ var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
</script>
<script>
var CG = new ColorGenerator();
</script>
<script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
<%
if (create) {
@ -47,26 +47,24 @@ var CG = new ColorGenerator();
}
if (member.user_group && member.user_group.vote_rule) {
var groupStyle = 'border-right: 2px solid '+CG.asRGB(CG.getColor(member.user_group.vote_rule));
var reviewGroup = '<i class="icon-user-group"></i>';
var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
} else {
var groupStyle = 'border-right: 2px solid transparent';
var reviewGroup = null;
var reviewGroupColor = 'transparent';
}
%>
<li id="reviewer_<%= member.user_id %>" class="reviewer_entry" style="<%= groupStyle%>" tooltip="Review Group">
<tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
<div class="reviewers_member">
<td style="width: 20px">
<div class="reviewer_status tooltip" title="<%= review_status_label %>">
<i class="icon-circle review-status-<%= review_status %>"></i>
</div>
<div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
<% if (mandatory) { %>
<div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
<i class="icon-lock"></i>
</div>
<% } %>
</td>
<td>
<div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
<%-
renderTemplate('gravatarWithUser', {
'size': 16,
@ -78,12 +76,44 @@ var CG = new ColorGenerator();
'gravatar_url': member.gravatar_link
})
%>
<span class="tooltip presence-state" style="display: none" title="This users is currently at this page">
<i class="icon-eye" style="color: #0ac878"></i>
</span>
</div>
</td>
<td style="width: 10px">
<% if (reviewGroup !== null) { %>
<span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
<%- reviewGroup %>
</span>
<% } %>
</td>
<% if (mandatory) { %>
<td style="text-align: right;width: 10px;">
<div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
<i class="icon-lock"></i>
</div>
</td>
<% } else { %>
<td>
<% if (allowed_to_update) { %>
<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
<i class="icon-remove"></i>
</div>
<% } %>
</td>
<% } %>
</tr>
<tr>
<td colspan="4" style="display: none" class="pr-user-rule-container">
<input type="hidden" name="__start__" value="reviewer:mapping">
<%if (member.user_group && member.user_group.vote_rule) {%>
<%if (member.user_group && member.user_group.vote_rule) { %>
<div class="reviewer_reason">
<%if (member.user_group.vote_rule == -1) {%>
@ -92,7 +122,7 @@ var CG = new ColorGenerator();
- group votes required: <%= member.user_group.vote_rule %>
<%}%>
</div>
<%}%>
<%} %>
<input type="hidden" name="__start__" value="reasons:sequence">
<% for (var i = 0; i < reasons.length; i++) { %>
@ -100,37 +130,24 @@ var CG = new ColorGenerator();
<div class="reviewer_reason">- <%= reason %></div>
<input type="hidden" name="reason" value="<%= reason %>">
<% } %>
<input type="hidden" name="__end__" value="reasons:sequence">
<input type="hidden" name="__end__" value="reasons:sequence">
<input type="hidden" name="__start__" value="rules:sequence">
<input type="hidden" name="__start__" value="rules:sequence">
<% for (var i = 0; i < member.rules.length; i++) { %>
<% var rule = member.rules[i] %>
<input type="hidden" name="rule_id" value="<%= rule %>">
<% } %>
<input type="hidden" name="__end__" value="rules:sequence">
<input type="hidden" name="__end__" value="rules:sequence">
<input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
<input type="hidden" name="mandatory" value="<%= mandatory %>"/>
<input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
<input type="hidden" name="mandatory" value="<%= mandatory %>"/>
<input type="hidden" name="__end__" value="reviewer:mapping">
<% if (mandatory) { %>
<div class="reviewer_member_mandatory_remove" style="visibility: <%= edit_visibility %>;">
<i class="icon-remove"></i>
</div>
<% } else { %>
<% if (allowed_to_update) { %>
<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
<i class="icon-remove" ></i>
</div>
<% } %>
<% } %>
</div>
</li>
</td>
</tr>
</script>
<script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
<%
@ -158,7 +175,6 @@ if (show_disabled) {
</script>
</div>
<script>

View file

@ -360,13 +360,13 @@ ${self.plaintext_footer()}
div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
list-style: none !important;
margin: 6px !important;
margin: 0px !important;
padding: 0 !important
}
div.markdown-block ul li, div.markdown-block ol li {
list-style: disc !important;
margin: 6px !important;
margin: 0px !important;
padding: 0 !important
}

File diff suppressed because it is too large Load diff