##// END OF EJS Templates
comments: fixed compare view comments.
marcink -
r1331:350c3c75 default
parent child Browse files
Show More
@@ -1,480 +1,483 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 commit controller for RhodeCode showing changes between commits
23 23 """
24 24
25 25 import logging
26 26
27 27 from collections import defaultdict
28 28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29 29
30 30 from pylons import tmpl_context as c, request, response
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import diffs, codeblocks
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils import action_logger, jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import CommentsModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.repo import RepoModel
52 52
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 def _update_with_GET(params, GET):
58 58 for k in ['diff1', 'diff2', 'diff']:
59 59 params[k] += GET.getall(k)
60 60
61 61
62 62 def get_ignore_ws(fid, GET):
63 63 ig_ws_global = GET.get('ignorews')
64 64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 65 if ig_ws:
66 66 try:
67 67 return int(ig_ws[0].split(':')[-1])
68 68 except Exception:
69 69 pass
70 70 return ig_ws_global
71 71
72 72
73 73 def _ignorews_url(GET, fileid=None):
74 74 fileid = str(fileid) if fileid else None
75 75 params = defaultdict(list)
76 76 _update_with_GET(params, GET)
77 77 label = _('Show whitespace')
78 78 tooltiplbl = _('Show whitespace for all diffs')
79 79 ig_ws = get_ignore_ws(fileid, GET)
80 80 ln_ctx = get_line_ctx(fileid, GET)
81 81
82 82 if ig_ws is None:
83 83 params['ignorews'] += [1]
84 84 label = _('Ignore whitespace')
85 85 tooltiplbl = _('Ignore whitespace for all diffs')
86 86 ctx_key = 'context'
87 87 ctx_val = ln_ctx
88 88
89 89 # if we have passed in ln_ctx pass it along to our params
90 90 if ln_ctx:
91 91 params[ctx_key] += [ctx_val]
92 92
93 93 if fileid:
94 94 params['anchor'] = 'a_' + fileid
95 95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96 96
97 97
98 98 def get_line_ctx(fid, GET):
99 99 ln_ctx_global = GET.get('context')
100 100 if fid:
101 101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 102 else:
103 103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 105 if ln_ctx:
106 106 ln_ctx = [ln_ctx]
107 107
108 108 if ln_ctx:
109 109 retval = ln_ctx[0].split(':')[-1]
110 110 else:
111 111 retval = ln_ctx_global
112 112
113 113 try:
114 114 return int(retval)
115 115 except Exception:
116 116 return 3
117 117
118 118
119 119 def _context_url(GET, fileid=None):
120 120 """
121 121 Generates a url for context lines.
122 122
123 123 :param fileid:
124 124 """
125 125
126 126 fileid = str(fileid) if fileid else None
127 127 ig_ws = get_ignore_ws(fileid, GET)
128 128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129 129
130 130 params = defaultdict(list)
131 131 _update_with_GET(params, GET)
132 132
133 133 if ln_ctx > 0:
134 134 params['context'] += [ln_ctx]
135 135
136 136 if ig_ws:
137 137 ig_ws_key = 'ignorews'
138 138 ig_ws_val = 1
139 139 params[ig_ws_key] += [ig_ws_val]
140 140
141 141 lbl = _('Increase context')
142 142 tooltiplbl = _('Increase context for all diffs')
143 143
144 144 if fileid:
145 145 params['anchor'] = 'a_' + fileid
146 146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147 147
148 148
149 149 class ChangesetController(BaseRepoController):
150 150
151 151 def __before__(self):
152 152 super(ChangesetController, self).__before__()
153 153 c.affected_files_cut_off = 60
154 154
155 155 def _index(self, commit_id_range, method):
156 156 c.ignorews_url = _ignorews_url
157 157 c.context_url = _context_url
158 158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159 159
160 160 # fetch global flags of ignore ws or context lines
161 161 context_lcl = get_line_ctx('', request.GET)
162 162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163 163
164 164 # diff_limit will cut off the whole diff if the limit is applied
165 165 # otherwise it will just hide the big files from the front-end
166 166 diff_limit = self.cut_off_limit_diff
167 167 file_limit = self.cut_off_limit_file
168 168
169 169 # get ranges of commit ids if preset
170 170 commit_range = commit_id_range.split('...')[:2]
171 171
172 172 try:
173 173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 174 'message', 'parents']
175 175
176 176 if len(commit_range) == 2:
177 177 commits = c.rhodecode_repo.get_commits(
178 178 start_id=commit_range[0], end_id=commit_range[1],
179 179 pre_load=pre_load)
180 180 commits = list(commits)
181 181 else:
182 182 commits = [c.rhodecode_repo.get_commit(
183 183 commit_id=commit_id_range, pre_load=pre_load)]
184 184
185 185 c.commit_ranges = commits
186 186 if not c.commit_ranges:
187 187 raise RepositoryError(
188 188 'The commit range returned an empty result')
189 189 except CommitDoesNotExistError:
190 190 msg = _('No such commit exists for this repository')
191 191 h.flash(msg, category='error')
192 192 raise HTTPNotFound()
193 193 except Exception:
194 194 log.exception("General failure")
195 195 raise HTTPNotFound()
196 196
197 197 c.changes = OrderedDict()
198 198 c.lines_added = 0
199 199 c.lines_deleted = 0
200 200
201 201 # auto collapse if we have more than limit
202 202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204 204
205 205 c.commit_statuses = ChangesetStatus.STATUSES
206 206 c.inline_comments = []
207 207 c.files = []
208 208
209 209 c.statuses = []
210 210 c.comments = []
211 211 if len(c.commit_ranges) == 1:
212 212 commit = c.commit_ranges[0]
213 213 c.comments = CommentsModel().get_comments(
214 214 c.rhodecode_db_repo.repo_id,
215 215 revision=commit.raw_id)
216 216 c.statuses.append(ChangesetStatusModel().get_status(
217 217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 218 # comments from PR
219 219 statuses = ChangesetStatusModel().get_statuses(
220 220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 221 with_revisions=True)
222 222 prs = set(st.pull_request for st in statuses
223 223 if st.pull_request is not None)
224 224 # from associated statuses, check the pull requests, and
225 225 # show comments from them
226 226 for pr in prs:
227 227 c.comments.extend(pr.comments)
228 228
229 229 # Iterate over ranges (default commit view is always one commit)
230 230 for commit in c.commit_ranges:
231 231 c.changes[commit.raw_id] = []
232 232
233 233 commit2 = commit
234 234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
235 235
236 236 _diff = c.rhodecode_repo.get_diff(
237 237 commit1, commit2,
238 238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
239 239 diff_processor = diffs.DiffProcessor(
240 240 _diff, format='newdiff', diff_limit=diff_limit,
241 241 file_limit=file_limit, show_full_diff=fulldiff)
242 242
243 243 commit_changes = OrderedDict()
244 244 if method == 'show':
245 245 _parsed = diff_processor.prepare()
246 246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
247 247
248 248 _parsed = diff_processor.prepare()
249 249
250 250 def _node_getter(commit):
251 251 def get_node(fname):
252 252 try:
253 253 return commit.get_node(fname)
254 254 except NodeDoesNotExistError:
255 255 return None
256 256 return get_node
257 257
258 258 inline_comments = CommentsModel().get_inline_comments(
259 259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
260 260 c.inline_cnt = CommentsModel().get_inline_comments_count(
261 261 inline_comments)
262 262
263 263 diffset = codeblocks.DiffSet(
264 264 repo_name=c.repo_name,
265 265 source_node_getter=_node_getter(commit1),
266 266 target_node_getter=_node_getter(commit2),
267 267 comments=inline_comments
268 268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
269 269 c.changes[commit.raw_id] = diffset
270 270 else:
271 271 # downloads/raw we only need RAW diff nothing else
272 272 diff = diff_processor.as_raw()
273 273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
274 274
275 275 # sort comments by how they were generated
276 276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
277 277
278 278
279 279 if len(c.commit_ranges) == 1:
280 280 c.commit = c.commit_ranges[0]
281 281 c.parent_tmpl = ''.join(
282 282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
283 283 if method == 'download':
284 284 response.content_type = 'text/plain'
285 285 response.content_disposition = (
286 286 'attachment; filename=%s.diff' % commit_id_range[:12])
287 287 return diff
288 288 elif method == 'patch':
289 289 response.content_type = 'text/plain'
290 290 c.diff = safe_unicode(diff)
291 291 return render('changeset/patch_changeset.mako')
292 292 elif method == 'raw':
293 293 response.content_type = 'text/plain'
294 294 return diff
295 295 elif method == 'show':
296 296 if len(c.commit_ranges) == 1:
297 297 return render('changeset/changeset.mako')
298 298 else:
299 299 c.ancestor = None
300 300 c.target_repo = c.rhodecode_db_repo
301 301 return render('changeset/changeset_range.mako')
302 302
303 303 @LoginRequired()
304 304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
305 305 'repository.admin')
306 306 def index(self, revision, method='show'):
307 307 return self._index(revision, method=method)
308 308
309 309 @LoginRequired()
310 310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 311 'repository.admin')
312 312 def changeset_raw(self, revision):
313 313 return self._index(revision, method='raw')
314 314
315 315 @LoginRequired()
316 316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 317 'repository.admin')
318 318 def changeset_patch(self, revision):
319 319 return self._index(revision, method='patch')
320 320
321 321 @LoginRequired()
322 322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 323 'repository.admin')
324 324 def changeset_download(self, revision):
325 325 return self._index(revision, method='download')
326 326
327 327 @LoginRequired()
328 328 @NotAnonymous()
329 329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 330 'repository.admin')
331 331 @auth.CSRFRequired()
332 332 @jsonify
333 333 def comment(self, repo_name, revision):
334 334 commit_id = revision
335 335 status = request.POST.get('changeset_status', None)
336 336 text = request.POST.get('text')
337 337 comment_type = request.POST.get('comment_type')
338 338 resolves_comment_id = request.POST.get('resolves_comment_id', None)
339 339
340 340 if status:
341 341 text = text or (_('Status change %(transition_icon)s %(status)s')
342 342 % {'transition_icon': '>',
343 343 'status': ChangesetStatus.get_status_lbl(status)})
344 344
345 multi_commit_ids = filter(
346 lambda s: s not in ['', None],
347 request.POST.get('commit_ids', '').split(','),)
345 multi_commit_ids = []
346 for _commit_id in request.POST.get('commit_ids', '').split(','):
347 if _commit_id not in ['', None, EmptyCommit.raw_id]:
348 if _commit_id not in multi_commit_ids:
349 multi_commit_ids.append(_commit_id)
348 350
349 351 commit_ids = multi_commit_ids or [commit_id]
352
350 353 comment = None
351 354 for current_id in filter(None, commit_ids):
352 355 c.co = comment = CommentsModel().create(
353 356 text=text,
354 357 repo=c.rhodecode_db_repo.repo_id,
355 358 user=c.rhodecode_user.user_id,
356 359 commit_id=current_id,
357 360 f_path=request.POST.get('f_path'),
358 361 line_no=request.POST.get('line'),
359 362 status_change=(ChangesetStatus.get_status_lbl(status)
360 363 if status else None),
361 364 status_change_type=status,
362 365 comment_type=comment_type,
363 366 resolves_comment_id=resolves_comment_id
364 367 )
365 368 c.inline_comment = True if comment.line_no else False
366 369
367 370 # get status if set !
368 371 if status:
369 372 # if latest status was from pull request and it's closed
370 373 # disallow changing status !
371 374 # dont_allow_on_closed_pull_request = True !
372 375
373 376 try:
374 377 ChangesetStatusModel().set_status(
375 378 c.rhodecode_db_repo.repo_id,
376 379 status,
377 380 c.rhodecode_user.user_id,
378 381 comment,
379 382 revision=current_id,
380 383 dont_allow_on_closed_pull_request=True
381 384 )
382 385 except StatusChangeOnClosedPullRequestError:
383 386 msg = _('Changing the status of a commit associated with '
384 387 'a closed pull request is not allowed')
385 388 log.exception(msg)
386 389 h.flash(msg, category='warning')
387 390 return redirect(h.url(
388 391 'changeset_home', repo_name=repo_name,
389 392 revision=current_id))
390 393
391 394 # finalize, commit and redirect
392 395 Session().commit()
393 396
394 397 data = {
395 398 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
396 399 }
397 400 if comment:
398 401 data.update(comment.get_dict())
399 402 data.update({'rendered_text':
400 403 render('changeset/changeset_comment_block.mako')})
401 404
402 405 return data
403 406
404 407 @LoginRequired()
405 408 @NotAnonymous()
406 409 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
407 410 'repository.admin')
408 411 @auth.CSRFRequired()
409 412 def preview_comment(self):
410 413 # Technically a CSRF token is not needed as no state changes with this
411 414 # call. However, as this is a POST is better to have it, so automated
412 415 # tools don't flag it as potential CSRF.
413 416 # Post is required because the payload could be bigger than the maximum
414 417 # allowed by GET.
415 418 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
416 419 raise HTTPBadRequest()
417 420 text = request.POST.get('text')
418 421 renderer = request.POST.get('renderer') or 'rst'
419 422 if text:
420 423 return h.render(text, renderer=renderer, mentions=True)
421 424 return ''
422 425
423 426 @LoginRequired()
424 427 @NotAnonymous()
425 428 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
426 429 'repository.admin')
427 430 @auth.CSRFRequired()
428 431 @jsonify
429 432 def delete_comment(self, repo_name, comment_id):
430 433 comment = ChangesetComment.get(comment_id)
431 434 if not comment:
432 435 log.debug('Comment with id:%s not found, skipping', comment_id)
433 436 # comment already deleted in another call probably
434 437 return True
435 438
436 439 owner = (comment.author.user_id == c.rhodecode_user.user_id)
437 440 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
438 441 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
439 442 CommentsModel().delete(comment=comment)
440 443 Session().commit()
441 444 return True
442 445 else:
443 446 raise HTTPForbidden()
444 447
445 448 @LoginRequired()
446 449 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
447 450 'repository.admin')
448 451 @jsonify
449 452 def changeset_info(self, repo_name, revision):
450 453 if request.is_xhr:
451 454 try:
452 455 return c.rhodecode_repo.get_commit(commit_id=revision)
453 456 except CommitDoesNotExistError as e:
454 457 return EmptyCommit(message=str(e))
455 458 else:
456 459 raise HTTPBadRequest()
457 460
458 461 @LoginRequired()
459 462 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
460 463 'repository.admin')
461 464 @jsonify
462 465 def changeset_children(self, repo_name, revision):
463 466 if request.is_xhr:
464 467 commit = c.rhodecode_repo.get_commit(commit_id=revision)
465 468 result = {"results": commit.children}
466 469 return result
467 470 else:
468 471 raise HTTPBadRequest()
469 472
470 473 @LoginRequired()
471 474 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
472 475 'repository.admin')
473 476 @jsonify
474 477 def changeset_parents(self, repo_name, revision):
475 478 if request.is_xhr:
476 479 commit = c.rhodecode_repo.get_commit(commit_id=revision)
477 480 result = {"results": commit.parents}
478 481 return result
479 482 else:
480 483 raise HTTPBadRequest()
@@ -1,774 +1,796 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45 /* Comment form for main and inline comments */
46 (function(mod) {
46 47
47 (function(mod) {
48 if (typeof exports == "object" && typeof module == "object") // CommonJS
48 if (typeof exports == "object" && typeof module == "object") {
49 // CommonJS
49 50 module.exports = mod();
50 else // Plain browser env
51 }
52 else {
53 // Plain browser env
51 54 (this || window).CommentForm = mod();
55 }
52 56
53 57 })(function() {
54 58 "use strict";
55 59
56 60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
57 61 if (!(this instanceof CommentForm)) {
58 62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
59 63 }
60 64
61 65 // bind the element instance to our Form
62 66 $(formElement).get(0).CommentForm = this;
63 67
64 68 this.withLineNo = function(selector) {
65 69 var lineNo = this.lineNo;
66 70 if (lineNo === undefined) {
67 71 return selector
68 72 } else {
69 73 return selector + '_' + lineNo;
70 74 }
71 75 };
72 76
73 77 this.commitId = commitId;
74 78 this.pullRequestId = pullRequestId;
75 79 this.lineNo = lineNo;
76 80 this.initAutocompleteActions = initAutocompleteActions;
77 81
78 82 this.previewButton = this.withLineNo('#preview-btn');
79 83 this.previewContainer = this.withLineNo('#preview-container');
80 84
81 85 this.previewBoxSelector = this.withLineNo('#preview-box');
82 86
83 87 this.editButton = this.withLineNo('#edit-btn');
84 88 this.editContainer = this.withLineNo('#edit-container');
85 89 this.cancelButton = this.withLineNo('#cancel-btn');
86 90 this.commentType = this.withLineNo('#comment_type');
87 91
88 92 this.resolvesId = null;
89 93 this.resolvesActionId = null;
90 94
91 95 this.cmBox = this.withLineNo('#text');
92 96 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
93 97
94 98 this.statusChange = this.withLineNo('#change_status');
95 99
96 100 this.submitForm = formElement;
97 101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
98 102 this.submitButtonText = this.submitButton.val();
99 103
100 104 this.previewUrl = pyroutes.url('changeset_comment_preview',
101 105 {'repo_name': templateContext.repo_name});
102 106
103 107 if (resolvesCommentId){
104 108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
105 109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
106 110 $(this.commentType).prop('disabled', true);
107 111 $(this.commentType).addClass('disabled');
108 112
109 113 // disable select
110 114 setTimeout(function() {
111 115 $(self.statusChange).select2('readonly', true);
112 116 }, 10);
113 117
114 118 var resolvedInfo = (
115 119 '<li class="">' +
116 120 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
117 121 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
118 122 '</li>'
119 123 ).format(resolvesCommentId, _gettext('resolve comment'));
120 124 $(resolvedInfo).insertAfter($(this.commentType).parent());
121 125 }
122 126
123 127 // based on commitId, or pullRequestId decide where do we submit
124 128 // out data
125 129 if (this.commitId){
126 130 this.submitUrl = pyroutes.url('changeset_comment',
127 131 {'repo_name': templateContext.repo_name,
128 132 'revision': this.commitId});
129 133 this.selfUrl = pyroutes.url('changeset_home',
130 134 {'repo_name': templateContext.repo_name,
131 135 'revision': this.commitId});
132 136
133 137 } else if (this.pullRequestId) {
134 138 this.submitUrl = pyroutes.url('pullrequest_comment',
135 139 {'repo_name': templateContext.repo_name,
136 140 'pull_request_id': this.pullRequestId});
137 141 this.selfUrl = pyroutes.url('pullrequest_show',
138 142 {'repo_name': templateContext.repo_name,
139 143 'pull_request_id': this.pullRequestId});
140 144
141 145 } else {
142 146 throw new Error(
143 147 'CommentForm requires pullRequestId, or commitId to be specified.')
144 148 }
145 149
146 150 // FUNCTIONS and helpers
147 151 var self = this;
148 152
149 153 this.isInline = function(){
150 154 return this.lineNo && this.lineNo != 'general';
151 155 };
152 156
153 157 this.getCmInstance = function(){
154 158 return this.cm
155 159 };
156 160
157 161 this.setPlaceholder = function(placeholder) {
158 162 var cm = this.getCmInstance();
159 163 if (cm){
160 164 cm.setOption('placeholder', placeholder);
161 165 }
162 166 };
163 167
164 168 this.getCommentStatus = function() {
165 169 return $(this.submitForm).find(this.statusChange).val();
166 170 };
167 171 this.getCommentType = function() {
168 172 return $(this.submitForm).find(this.commentType).val();
169 173 };
170 174
171 175 this.getResolvesId = function() {
172 176 return $(this.submitForm).find(this.resolvesId).val() || null;
173 177 };
174 178 this.markCommentResolved = function(resolvedCommentId){
175 179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
176 180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
177 181 };
178 182
179 183 this.isAllowedToSubmit = function() {
180 184 return !$(this.submitButton).prop('disabled');
181 185 };
182 186
183 187 this.initStatusChangeSelector = function(){
184 188 var formatChangeStatus = function(state, escapeMarkup) {
185 189 var originalOption = state.element;
186 190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
187 191 '<span>' + escapeMarkup(state.text) + '</span>';
188 192 };
189 193 var formatResult = function(result, container, query, escapeMarkup) {
190 194 return formatChangeStatus(result, escapeMarkup);
191 195 };
192 196
193 197 var formatSelection = function(data, container, escapeMarkup) {
194 198 return formatChangeStatus(data, escapeMarkup);
195 199 };
196 200
197 201 $(this.submitForm).find(this.statusChange).select2({
198 202 placeholder: _gettext('Status Review'),
199 203 formatResult: formatResult,
200 204 formatSelection: formatSelection,
201 205 containerCssClass: "drop-menu status_box_menu",
202 206 dropdownCssClass: "drop-menu-dropdown",
203 207 dropdownAutoWidth: true,
204 208 minimumResultsForSearch: -1
205 209 });
206 210 $(this.submitForm).find(this.statusChange).on('change', function() {
207 211 var status = self.getCommentStatus();
208 212 if (status && !self.isInline()) {
209 213 $(self.submitButton).prop('disabled', false);
210 214 }
211 215
212 216 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
213 217 self.setPlaceholder(placeholderText)
214 218 })
215 219 };
216 220
217 221 // reset the comment form into it's original state
218 222 this.resetCommentFormState = function(content) {
219 223 content = content || '';
220 224
221 225 $(this.editContainer).show();
222 226 $(this.editButton).parent().addClass('active');
223 227
224 228 $(this.previewContainer).hide();
225 229 $(this.previewButton).parent().removeClass('active');
226 230
227 231 this.setActionButtonsDisabled(true);
228 232 self.cm.setValue(content);
229 233 self.cm.setOption("readOnly", false);
230 234
231 235 if (this.resolvesId) {
232 236 // destroy the resolve action
233 237 $(this.resolvesId).parent().remove();
234 238 }
235 239
236 240 $(this.statusChange).select2('readonly', false);
237 241 };
238 242
243 this.globalSubmitSuccessCallback = function(){};
244
239 245 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
240 246 failHandler = failHandler || function() {};
241 247 var postData = toQueryString(postData);
242 248 var request = $.ajax({
243 249 url: url,
244 250 type: 'POST',
245 251 data: postData,
246 252 headers: {'X-PARTIAL-XHR': true}
247 253 })
248 254 .done(function(data) {
249 255 successHandler(data);
250 256 })
251 257 .fail(function(data, textStatus, errorThrown){
252 258 alert(
253 259 "Error while submitting comment.\n" +
254 260 "Error code {0} ({1}).".format(data.status, data.statusText));
255 261 failHandler()
256 262 });
257 263 return request;
258 264 };
259 265
260 266 // overwrite a submitHandler, we need to do it for inline comments
261 267 this.setHandleFormSubmit = function(callback) {
262 268 this.handleFormSubmit = callback;
263 269 };
264 270
271 // overwrite a submitSuccessHandler
272 this.setGlobalSubmitSuccessCallback = function(callback) {
273 this.globalSubmitSuccessCallback = callback;
274 };
275
265 276 // default handler for for submit for main comments
266 277 this.handleFormSubmit = function() {
267 278 var text = self.cm.getValue();
268 279 var status = self.getCommentStatus();
269 280 var commentType = self.getCommentType();
270 281 var resolvesCommentId = self.getResolvesId();
271 282
272 283 if (text === "" && !status) {
273 284 return;
274 285 }
275 286
276 287 var excludeCancelBtn = false;
277 288 var submitEvent = true;
278 289 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
279 290 self.cm.setOption("readOnly", true);
280 291
281 292 var postData = {
282 293 'text': text,
283 294 'changeset_status': status,
284 295 'comment_type': commentType,
285 296 'csrf_token': CSRF_TOKEN
286 297 };
287 298 if (resolvesCommentId){
288 299 postData['resolves_comment_id'] = resolvesCommentId;
289 300 }
301
290 302 var submitSuccessCallback = function(o) {
291 303 if (status) {
292 304 location.reload(true);
293 305 } else {
294 306 $('#injected_page_comments').append(o.rendered_text);
295 307 self.resetCommentFormState();
296 308 timeagoActivate();
297 309
298 310 // mark visually which comment was resolved
299 311 if (resolvesCommentId) {
300 312 self.markCommentResolved(resolvesCommentId);
301 313 }
302 314 }
315
316 // run global callback on submit
317 self.globalSubmitSuccessCallback();
318
303 319 };
304 320 var submitFailCallback = function(){
305 321 self.resetCommentFormState(text);
306 322 };
307 323 self.submitAjaxPOST(
308 324 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
309 325 };
310 326
311 327 this.previewSuccessCallback = function(o) {
312 328 $(self.previewBoxSelector).html(o);
313 329 $(self.previewBoxSelector).removeClass('unloaded');
314 330
315 331 // swap buttons, making preview active
316 332 $(self.previewButton).parent().addClass('active');
317 333 $(self.editButton).parent().removeClass('active');
318 334
319 335 // unlock buttons
320 336 self.setActionButtonsDisabled(false);
321 337 };
322 338
323 339 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
324 340 excludeCancelBtn = excludeCancelBtn || false;
325 341 submitEvent = submitEvent || false;
326 342
327 343 $(this.editButton).prop('disabled', state);
328 344 $(this.previewButton).prop('disabled', state);
329 345
330 346 if (!excludeCancelBtn) {
331 347 $(this.cancelButton).prop('disabled', state);
332 348 }
333 349
334 350 var submitState = state;
335 351 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
336 352 // if the value of commit review status is set, we allow
337 353 // submit button, but only on Main form, lineNo means inline
338 354 submitState = false
339 355 }
340 356 $(this.submitButton).prop('disabled', submitState);
341 357 if (submitEvent) {
342 358 $(this.submitButton).val(_gettext('Submitting...'));
343 359 } else {
344 360 $(this.submitButton).val(this.submitButtonText);
345 361 }
346 362
347 363 };
348 364
349 365 // lock preview/edit/submit buttons on load, but exclude cancel button
350 366 var excludeCancelBtn = true;
351 367 this.setActionButtonsDisabled(true, excludeCancelBtn);
352 368
353 369 // anonymous users don't have access to initialized CM instance
354 370 if (this.cm !== undefined){
355 371 this.cm.on('change', function(cMirror) {
356 372 if (cMirror.getValue() === "") {
357 373 self.setActionButtonsDisabled(true, excludeCancelBtn)
358 374 } else {
359 375 self.setActionButtonsDisabled(false, excludeCancelBtn)
360 376 }
361 377 });
362 378 }
363 379
364 380 $(this.editButton).on('click', function(e) {
365 381 e.preventDefault();
366 382
367 383 $(self.previewButton).parent().removeClass('active');
368 384 $(self.previewContainer).hide();
369 385
370 386 $(self.editButton).parent().addClass('active');
371 387 $(self.editContainer).show();
372 388
373 389 });
374 390
375 391 $(this.previewButton).on('click', function(e) {
376 392 e.preventDefault();
377 393 var text = self.cm.getValue();
378 394
379 395 if (text === "") {
380 396 return;
381 397 }
382 398
383 399 var postData = {
384 400 'text': text,
385 401 'renderer': templateContext.visual.default_renderer,
386 402 'csrf_token': CSRF_TOKEN
387 403 };
388 404
389 405 // lock ALL buttons on preview
390 406 self.setActionButtonsDisabled(true);
391 407
392 408 $(self.previewBoxSelector).addClass('unloaded');
393 409 $(self.previewBoxSelector).html(_gettext('Loading ...'));
394 410
395 411 $(self.editContainer).hide();
396 412 $(self.previewContainer).show();
397 413
398 414 // by default we reset state of comment preserving the text
399 415 var previewFailCallback = function(){
400 416 self.resetCommentFormState(text)
401 417 };
402 418 self.submitAjaxPOST(
403 419 self.previewUrl, postData, self.previewSuccessCallback,
404 420 previewFailCallback);
405 421
406 422 $(self.previewButton).parent().addClass('active');
407 423 $(self.editButton).parent().removeClass('active');
408 424 });
409 425
410 426 $(this.submitForm).submit(function(e) {
411 427 e.preventDefault();
412 428 var allowedToSubmit = self.isAllowedToSubmit();
413 429 if (!allowedToSubmit){
414 430 return false;
415 431 }
416 432 self.handleFormSubmit();
417 433 });
418 434
419 435 }
420 436
421 437 return CommentForm;
422 438 });
423 439
424 440 /* comments controller */
425 441 var CommentsController = function() {
426 442 var mainComment = '#text';
427 443 var self = this;
428 444
429 445 this.cancelComment = function(node) {
430 446 var $node = $(node);
431 447 var $td = $node.closest('td');
432 448 $node.closest('.comment-inline-form').remove();
433 449 return false;
434 450 };
435 451
436 452 this.getLineNumber = function(node) {
437 453 var $node = $(node);
438 454 return $node.closest('td').attr('data-line-number');
439 455 };
440 456
441 457 this.scrollToComment = function(node, offset, outdated) {
442 458 var outdated = outdated || false;
443 459 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
444 460
445 461 if (!node) {
446 462 node = $('.comment-selected');
447 463 if (!node.length) {
448 464 node = $('comment-current')
449 465 }
450 466 }
451 467 $comment = $(node).closest(klass);
452 468 $comments = $(klass);
453 469
454 470 $('.comment-selected').removeClass('comment-selected');
455 471
456 472 var nextIdx = $(klass).index($comment) + offset;
457 473 if (nextIdx >= $comments.length) {
458 474 nextIdx = 0;
459 475 }
460 476 var $next = $(klass).eq(nextIdx);
461 477 var $cb = $next.closest('.cb');
462 478 $cb.removeClass('cb-collapsed');
463 479
464 480 var $filediffCollapseState = $cb.closest('.filediff').prev();
465 481 $filediffCollapseState.prop('checked', false);
466 482 $next.addClass('comment-selected');
467 483 scrollToElement($next);
468 484 return false;
469 485 };
470 486
471 487 this.nextComment = function(node) {
472 488 return self.scrollToComment(node, 1);
473 489 };
474 490
475 491 this.prevComment = function(node) {
476 492 return self.scrollToComment(node, -1);
477 493 };
478 494
479 495 this.nextOutdatedComment = function(node) {
480 496 return self.scrollToComment(node, 1, true);
481 497 };
482 498
483 499 this.prevOutdatedComment = function(node) {
484 500 return self.scrollToComment(node, -1, true);
485 501 };
486 502
487 503 this.deleteComment = function(node) {
488 504 if (!confirm(_gettext('Delete this comment?'))) {
489 505 return false;
490 506 }
491 507 var $node = $(node);
492 508 var $td = $node.closest('td');
493 509 var $comment = $node.closest('.comment');
494 510 var comment_id = $comment.attr('data-comment-id');
495 511 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
496 512 var postData = {
497 513 '_method': 'delete',
498 514 'csrf_token': CSRF_TOKEN
499 515 };
500 516
501 517 $comment.addClass('comment-deleting');
502 518 $comment.hide('fast');
503 519
504 520 var success = function(response) {
505 521 $comment.remove();
506 522 return false;
507 523 };
508 524 var failure = function(data, textStatus, xhr) {
509 525 alert("error processing request: " + textStatus);
510 526 $comment.show('fast');
511 527 $comment.removeClass('comment-deleting');
512 528 return false;
513 529 };
514 530 ajaxPOST(url, postData, success, failure);
515 531 };
516 532
517 533 this.toggleWideMode = function (node) {
518 534 if ($('#content').hasClass('wrapper')) {
519 535 $('#content').removeClass("wrapper");
520 536 $('#content').addClass("wide-mode-wrapper");
521 537 $(node).addClass('btn-success');
522 538 } else {
523 539 $('#content').removeClass("wide-mode-wrapper");
524 540 $('#content').addClass("wrapper");
525 541 $(node).removeClass('btn-success');
526 542 }
527 543 return false;
528 544 };
529 545
530 546 this.toggleComments = function(node, show) {
531 547 var $filediff = $(node).closest('.filediff');
532 548 if (show === true) {
533 549 $filediff.removeClass('hide-comments');
534 550 } else if (show === false) {
535 551 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
536 552 $filediff.addClass('hide-comments');
537 553 } else {
538 554 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
539 555 $filediff.toggleClass('hide-comments');
540 556 }
541 557 return false;
542 558 };
543 559
544 560 this.toggleLineComments = function(node) {
545 561 self.toggleComments(node, true);
546 562 var $node = $(node);
547 563 $node.closest('tr').toggleClass('hide-line-comments');
548 564 };
549 565
550 566 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
551 567 var pullRequestId = templateContext.pull_request_data.pull_request_id;
552 568 var commitId = templateContext.commit_data.commit_id;
553 569
554 570 var commentForm = new CommentForm(
555 571 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
556 572 var cm = commentForm.getCmInstance();
557 573
558 574 if (resolvesCommentId){
559 575 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
560 576 }
561 577
562 578 setTimeout(function() {
563 579 // callbacks
564 580 if (cm !== undefined) {
565 581 commentForm.setPlaceholder(placeholderText);
566 582 if (commentForm.isInline()) {
567 583 cm.focus();
568 584 cm.refresh();
569 585 }
570 586 }
571 587 }, 10);
572 588
573 589 // trigger scrolldown to the resolve comment, since it might be away
574 590 // from the clicked
575 591 if (resolvesCommentId){
576 592 var actionNode = $(commentForm.resolvesActionId).offset();
577 593
578 594 setTimeout(function() {
579 595 if (actionNode) {
580 596 $('body, html').animate({scrollTop: actionNode.top}, 10);
581 597 }
582 598 }, 100);
583 599 }
584 600
585 601 return commentForm;
586 602 };
587 603
588 604 this.createGeneralComment = function(lineNo, placeholderText, resolvesCommentId){
589 605
590 606 var tmpl = $('#cb-comment-general-form-template').html();
591 607 tmpl = tmpl.format(null, 'general');
592 608 var $form = $(tmpl);
593 609
594 var curForm = $('#cb-comment-general-form-placeholder').find('form');
610 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
611 var curForm = $formPlaceholder.find('form');
595 612 if (curForm){
596 613 curForm.remove();
597 614 }
598 $('#cb-comment-general-form-placeholder').append($form);
615 $formPlaceholder.append($form);
599 616
600 617 var _form = $($form[0]);
601 618 var commentForm = this.createCommentForm(
602 619 _form, lineNo, placeholderText, true, resolvesCommentId);
603 620 commentForm.initStatusChangeSelector();
621
622 return commentForm;
604 623 };
605 624
606 625 this.createComment = function(node, resolutionComment) {
607 626 var resolvesCommentId = resolutionComment || null;
608 627 var $node = $(node);
609 628 var $td = $node.closest('td');
610 629 var $form = $td.find('.comment-inline-form');
611 630
612 631 if (!$form.length) {
613 632
614 633 var $filediff = $node.closest('.filediff');
615 634 $filediff.removeClass('hide-comments');
616 635 var f_path = $filediff.attr('data-f-path');
617 636 var lineno = self.getLineNumber(node);
618 637 // create a new HTML from template
619 638 var tmpl = $('#cb-comment-inline-form-template').html();
620 639 tmpl = tmpl.format(f_path, lineno);
621 640 $form = $(tmpl);
622 641
623 642 var $comments = $td.find('.inline-comments');
624 643 if (!$comments.length) {
625 644 $comments = $(
626 645 $('#cb-comments-inline-container-template').html());
627 646 $td.append($comments);
628 647 }
629 648
630 649 $td.find('.cb-comment-add-button').before($form);
631 650
632 651 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
633 652 var _form = $($form[0]).find('form');
634 653
635 654 var commentForm = this.createCommentForm(
636 655 _form, lineno, placeholderText, false, resolvesCommentId);
637 656
638 657 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
639 658 form: _form,
640 659 parent: $td[0],
641 660 lineno: lineno,
642 661 f_path: f_path}
643 662 );
644 663
645 664 // set a CUSTOM submit handler for inline comments.
646 665 commentForm.setHandleFormSubmit(function(o) {
647 666 var text = commentForm.cm.getValue();
648 667 var commentType = commentForm.getCommentType();
649 668 var resolvesCommentId = commentForm.getResolvesId();
650 669
651 670 if (text === "") {
652 671 return;
653 672 }
654 673
655 674 if (lineno === undefined) {
656 675 alert('missing line !');
657 676 return;
658 677 }
659 678 if (f_path === undefined) {
660 679 alert('missing file path !');
661 680 return;
662 681 }
663 682
664 683 var excludeCancelBtn = false;
665 684 var submitEvent = true;
666 685 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
667 686 commentForm.cm.setOption("readOnly", true);
668 687 var postData = {
669 688 'text': text,
670 689 'f_path': f_path,
671 690 'line': lineno,
672 691 'comment_type': commentType,
673 692 'csrf_token': CSRF_TOKEN
674 693 };
675 694 if (resolvesCommentId){
676 695 postData['resolves_comment_id'] = resolvesCommentId;
677 696 }
678 697
679 698 var submitSuccessCallback = function(json_data) {
680 699 $form.remove();
681 700 try {
682 701 var html = json_data.rendered_text;
683 702 var lineno = json_data.line_no;
684 703 var target_id = json_data.target_id;
685 704
686 705 $comments.find('.cb-comment-add-button').before(html);
687 706
688 707 //mark visually which comment was resolved
689 708 if (resolvesCommentId) {
690 709 commentForm.markCommentResolved(resolvesCommentId);
691 710 }
692 711
712 // run global callback on submit
713 commentForm.globalSubmitSuccessCallback();
714
693 715 } catch (e) {
694 716 console.error(e);
695 717 }
696 718
697 719 // re trigger the linkification of next/prev navigation
698 720 linkifyComments($('.inline-comment-injected'));
699 721 timeagoActivate();
700 722 commentForm.setActionButtonsDisabled(false);
701 723
702 724 };
703 725 var submitFailCallback = function(){
704 726 commentForm.resetCommentFormState(text)
705 727 };
706 728 commentForm.submitAjaxPOST(
707 729 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
708 730 });
709 731 }
710 732
711 733 $form.addClass('comment-inline-form-open');
712 734 };
713 735
714 736 this.createResolutionComment = function(commentId){
715 737 // hide the trigger text
716 738 $('#resolve-comment-{0}'.format(commentId)).hide();
717 739
718 740 var comment = $('#comment-'+commentId);
719 741 var commentData = comment.data();
720 742 if (commentData.commentInline) {
721 743 this.createComment(comment, commentId)
722 744 } else {
723 745 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
724 746 }
725 747
726 748 return false;
727 749 };
728 750
729 751 this.submitResolution = function(commentId){
730 752 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
731 753 var commentForm = form.get(0).CommentForm;
732 754
733 755 var cm = commentForm.getCmInstance();
734 756 var renderer = templateContext.visual.default_renderer;
735 757 if (renderer == 'rst'){
736 758 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
737 759 } else if (renderer == 'markdown') {
738 760 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
739 761 } else {
740 762 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
741 763 }
742 764
743 765 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
744 766 form.submit();
745 767 return false;
746 768 };
747 769
748 770 this.renderInlineComments = function(file_comments) {
749 771 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
750 772
751 773 for (var i = 0; i < file_comments.length; i++) {
752 774 var box = file_comments[i];
753 775
754 776 var target_id = $(box).attr('target_id');
755 777
756 778 // actually comments with line numbers
757 779 var comments = box.children;
758 780
759 781 for (var j = 0; j < comments.length; j++) {
760 782 var data = {
761 783 'rendered_text': comments[j].outerHTML,
762 784 'line_no': $(comments[j]).attr('line'),
763 785 'target_id': target_id
764 786 };
765 787 }
766 788 }
767 789
768 790 // since order of injection is random, we're now re-iterating
769 791 // from correct order and filling in links
770 792 linkifyComments($('.inline-comment-injected'));
771 793 firefoxAnchorFix();
772 794 };
773 795
774 796 };
@@ -1,348 +1,400 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10 10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
11 11
12 12 <div class="comment
13 13 ${'comment-inline' if inline else 'comment-general'}
14 14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
15 15 id="comment-${comment.comment_id}"
16 16 line="${comment.line_no}"
17 17 data-comment-id="${comment.comment_id}"
18 18 data-comment-type="${comment.comment_type}"
19 19 data-comment-inline=${h.json.dumps(inline)}
20 20 style="${'display: none;' if outdated_at_ver else ''}">
21 21
22 22 <div class="meta">
23 23 <div class="comment-type-label">
24 24 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
25 25 % if comment.comment_type == 'todo':
26 26 % if comment.resolved:
27 27 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
28 28 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
29 29 </div>
30 30 % else:
31 31 <div class="resolved tooltip" style="display: none">
32 32 <span>${comment.comment_type}</span>
33 33 </div>
34 34 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
35 35 ${comment.comment_type}
36 36 </div>
37 37 % endif
38 38 % else:
39 39 % if comment.resolved_comment:
40 40 fix
41 41 % else:
42 42 ${comment.comment_type or 'note'}
43 43 % endif
44 44 % endif
45 45 </div>
46 46 </div>
47 47
48 48 <div class="author ${'author-inline' if inline else 'author-general'}">
49 49 ${base.gravatar_with_user(comment.author.email, 16)}
50 50 </div>
51 51 <div class="date">
52 52 ${h.age_component(comment.modified_at, time_is_local=True)}
53 53 </div>
54 54 % if inline:
55 55 <span></span>
56 56 % else:
57 57 <div class="status-change">
58 58 % if comment.pull_request:
59 59 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
60 60 % if comment.status_change:
61 61 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
62 62 % else:
63 63 ${_('pull request #%s') % comment.pull_request.pull_request_id}
64 64 % endif
65 65 </a>
66 66 % else:
67 67 % if comment.status_change:
68 68 ${_('Status change on commit')}:
69 69 % endif
70 70 % endif
71 71 </div>
72 72 % endif
73 73
74 74 % if comment.status_change:
75 75 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
76 76 <div title="${_('Commit status')}" class="changeset-status-lbl">
77 77 ${comment.status_change[0].status_lbl}
78 78 </div>
79 79 % endif
80 80
81 81 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
82 82
83 83 <div class="comment-links-block">
84 84
85 85 % if inline:
86 86 % if outdated_at_ver:
87 87 <div class="pr-version-inline">
88 88 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
89 89 <code class="pr-version-num">
90 90 outdated ${'v{}'.format(pr_index_ver)}
91 91 </code>
92 92 </a>
93 93 </div>
94 94 |
95 95 % endif
96 96 % else:
97 97 % if comment.pull_request_version_id and pr_index_ver:
98 98 |
99 99 <div class="pr-version">
100 100 % if comment.outdated:
101 101 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
102 102 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
103 103 </a>
104 104 % else:
105 105 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
106 106 <a href="${h.url('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)}">
107 107 <code class="pr-version-num">
108 108 ${'v{}'.format(pr_index_ver)}
109 109 </code>
110 110 </a>
111 111 </div>
112 112 % endif
113 113 </div>
114 114 % endif
115 115 % endif
116 116
117 117 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
118 118 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
119 119 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
120 120 ## permissions to delete
121 121 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
122 122 ## TODO: dan: add edit comment here
123 123 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
124 124 %else:
125 125 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
126 126 %endif
127 127 %else:
128 128 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
129 129 %endif
130 130
131 131 %if not outdated_at_ver:
132 132 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
133 133 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
134 134 %endif
135 135
136 136 </div>
137 137 </div>
138 138 <div class="text">
139 139 ${comment.render(mentions=True)|n}
140 140 </div>
141 141
142 142 </div>
143 143 </%def>
144 144
145 145 ## generate main comments
146 146 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
147 147 <div id="comments">
148 148 %for comment in c.comments:
149 149 <div id="comment-tr-${comment.comment_id}">
150 150 ## only render comments that are not from pull request, or from
151 151 ## pull request and a status change
152 152 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
153 153 ${comment_block(comment)}
154 154 %endif
155 155 </div>
156 156 %endfor
157 157 ## to anchor ajax comments
158 158 <div id="injected_page_comments"></div>
159 159 </div>
160 160 </%def>
161 161
162 162
163 163 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
164 164
165 165 ## merge status, and merge action
166 166 %if is_pull_request:
167 167 <div class="pull-request-merge">
168 168 %if c.allowed_to_merge:
169 169 <div class="pull-request-wrap">
170 170 <div class="pull-right">
171 171 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
172 172 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
173 173 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
174 174 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
175 175 ${h.end_form()}
176 176 </div>
177 177 </div>
178 178 %else:
179 179 <div class="pull-request-wrap">
180 180 <div class="pull-right">
181 181 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
182 182 </div>
183 183 </div>
184 184 %endif
185 185 </div>
186 186 %endif
187 187
188 188 <div class="comments">
189 189 <%
190 190 if is_pull_request:
191 191 placeholder = _('Leave a comment on this Pull Request.')
192 192 elif is_compare:
193 placeholder = _('Leave a comment on all commits in this range.')
193 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
194 194 else:
195 195 placeholder = _('Leave a comment on this Commit.')
196 196 %>
197 197
198 198 % if c.rhodecode_user.username != h.DEFAULT_USER:
199 199 <div class="js-template" id="cb-comment-general-form-template">
200 200 ## template generated for injection
201 201 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
202 202 </div>
203 203
204 204 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
205 205 ## inject form here
206 206 </div>
207 207 <script type="text/javascript">
208 208 var lineNo = 'general';
209 209 var resolvesCommentId = null;
210 Rhodecode.comments.createGeneralComment(lineNo, "${placeholder}", resolvesCommentId)
210 var generalCommentForm = Rhodecode.comments.createGeneralComment(
211 lineNo, "${placeholder}", resolvesCommentId);
212
213 // set custom success callback on rangeCommit
214 % if is_compare:
215 generalCommentForm.setHandleFormSubmit(function(o) {
216 var self = generalCommentForm;
217
218 var text = self.cm.getValue();
219 var status = self.getCommentStatus();
220 var commentType = self.getCommentType();
221
222 if (text === "" && !status) {
223 return;
224 }
225
226 // we can pick which commits we want to make the comment by
227 // selecting them via click on preview pane, this will alter the hidden inputs
228 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
229
230 var commitIds = [];
231 $('#changeset_compare_view_content .compare_select').each(function(el) {
232 var commitId = this.id.replace('row-', '');
233 if ($(this).hasClass('hl') || !cherryPicked) {
234 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
235 commitIds.push(commitId);
236 } else {
237 $("input[data-commit-id='{0}']".format(commitId)).val('')
238 }
239 });
240
241 self.setActionButtonsDisabled(true);
242 self.cm.setOption("readOnly", true);
243 var postData = {
244 'text': text,
245 'changeset_status': status,
246 'comment_type': commentType,
247 'commit_ids': commitIds,
248 'csrf_token': CSRF_TOKEN
249 };
250
251 var submitSuccessCallback = function(o) {
252 location.reload(true);
253 };
254 var submitFailCallback = function(){
255 self.resetCommentFormState(text)
256 };
257 self.submitAjaxPOST(
258 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
259 });
260 % endif
261
262
211 263 </script>
212 264 % else:
213 265 ## form state when not logged in
214 266 <div class="comment-form ac">
215 267
216 268 <div class="comment-area">
217 269 <div class="comment-area-header">
218 270 <ul class="nav-links clearfix">
219 271 <li class="active">
220 272 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
221 273 </li>
222 274 <li class="">
223 275 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
224 276 </li>
225 277 </ul>
226 278 </div>
227 279
228 280 <div class="comment-area-write" style="display: block;">
229 281 <div id="edit-container">
230 282 <div style="padding: 40px 0">
231 283 ${_('You need to be logged in to leave comments.')}
232 284 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
233 285 </div>
234 286 </div>
235 287 <div id="preview-container" class="clearfix" style="display: none;">
236 288 <div id="preview-box" class="preview-box"></div>
237 289 </div>
238 290 </div>
239 291
240 292 <div class="comment-area-footer">
241 293 <div class="toolbar">
242 294 <div class="toolbar-text">
243 295 </div>
244 296 </div>
245 297 </div>
246 298 </div>
247 299
248 300 <div class="comment-footer">
249 301 </div>
250 302
251 303 </div>
252 304 % endif
253 305
254 306 <script type="text/javascript">
255 307 bindToggleButtons();
256 308 </script>
257 309 </div>
258 310 </%def>
259 311
260 312
261 313 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
262 314 ## comment injected based on assumption that user is logged in
263 315
264 316 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
265 317
266 318 <div class="comment-area">
267 319 <div class="comment-area-header">
268 320 <ul class="nav-links clearfix">
269 321 <li class="active">
270 322 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
271 323 </li>
272 324 <li class="">
273 325 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
274 326 </li>
275 327 <li class="pull-right">
276 328 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
277 329 % for val in c.visual.comment_types:
278 330 <option value="${val}">${val.upper()}</option>
279 331 % endfor
280 332 </select>
281 333 </li>
282 334 </ul>
283 335 </div>
284 336
285 337 <div class="comment-area-write" style="display: block;">
286 338 <div id="edit-container_${lineno_id}">
287 339 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
288 340 </div>
289 341 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
290 342 <div id="preview-box_${lineno_id}" class="preview-box"></div>
291 343 </div>
292 344 </div>
293 345
294 346 <div class="comment-area-footer">
295 347 <div class="toolbar">
296 348 <div class="toolbar-text">
297 349 ${(_('Comments parsed using %s syntax with %s support.') % (
298 350 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
299 351 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
300 352 )
301 353 )|n}
302 354 </div>
303 355 </div>
304 356 </div>
305 357 </div>
306 358
307 359 <div class="comment-footer">
308 360
309 361 % if review_statuses:
310 362 <div class="status_box">
311 363 <select id="change_status_${lineno_id}" name="changeset_status">
312 364 <option></option> ## Placeholder
313 365 % for status, lbl in review_statuses:
314 366 <option value="${status}" data-status="${status}">${lbl}</option>
315 367 %if is_pull_request and change_status and status in ('approved', 'rejected'):
316 368 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
317 369 %endif
318 370 % endfor
319 371 </select>
320 372 </div>
321 373 % endif
322 374
323 375 ## inject extra inputs into the form
324 376 % if form_extras and isinstance(form_extras, (list, tuple)):
325 377 <div id="comment_form_extras">
326 378 % for form_ex_el in form_extras:
327 379 ${form_ex_el|n}
328 380 % endfor
329 381 </div>
330 382 % endif
331 383
332 384 <div class="action-buttons">
333 385 ## inline for has a file, and line-number together with cancel hide button.
334 386 % if form_type == 'inline':
335 387 <input type="hidden" name="f_path" value="{0}">
336 388 <input type="hidden" name="line" value="${lineno_id}">
337 389 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
338 390 ${_('Cancel')}
339 391 </button>
340 392 % endif
341 393 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
342 394
343 395 </div>
344 396 </div>
345 397
346 398 </form>
347 399
348 400 </%def> No newline at end of file
@@ -1,377 +1,333 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
4 4
5 5 <%def name="title()">
6 6 %if c.compare_home:
7 7 ${_('%s Compare') % c.repo_name}
8 8 %else:
9 9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
10 10 %endif
11 11 %if c.rhodecode_name:
12 12 &middot; ${h.branding(c.rhodecode_name)}
13 13 %endif
14 14 </%def>
15 15
16 16 <%def name="breadcrumbs_links()">
17 17 ${ungettext('%s commit','%s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
18 18 </%def>
19 19
20 20 <%def name="menu_bar_nav()">
21 21 ${self.menu_items(active='repositories')}
22 22 </%def>
23 23
24 24 <%def name="menu_bar_subnav()">
25 25 ${self.repo_menu(active='compare')}
26 26 </%def>
27 27
28 28 <%def name="main()">
29 29 <script type="text/javascript">
30 30 // set fake commitId on this commit-range page
31 31 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
32 32 </script>
33 33
34 34 <div class="box">
35 35 <div class="title">
36 36 ${self.repo_page_title(c.rhodecode_db_repo)}
37 37 </div>
38 38
39 39 <div class="summary changeset">
40 40 <div class="summary-detail">
41 41 <div class="summary-detail-header">
42 42 <span class="breadcrumbs files_location">
43 43 <h4>
44 44 ${_('Compare Commits')}
45 45 % if c.file_path:
46 46 ${_('for file')} <a href="#${'a_' + h.FID('',c.file_path)}">${c.file_path}</a>
47 47 % endif
48 48
49 49 % if c.commit_ranges:
50 50 <code>
51 51 r${c.source_commit.revision}:${h.short_id(c.source_commit.raw_id)}...r${c.target_commit.revision}:${h.short_id(c.target_commit.raw_id)}
52 52 </code>
53 53 % endif
54 54 </h4>
55 55 </span>
56 56 </div>
57 57
58 58 <div class="fieldset">
59 59 <div class="left-label">
60 60 ${_('Target')}:
61 61 </div>
62 62 <div class="right-content">
63 63 <div>
64 64 <div class="code-header" >
65 65 <div class="compare_header">
66 66 ## The hidden elements are replaced with a select2 widget
67 67 ${h.hidden('compare_source')}
68 68 </div>
69 69 </div>
70 70 </div>
71 71 </div>
72 72 </div>
73 73
74 74 <div class="fieldset">
75 75 <div class="left-label">
76 76 ${_('Source')}:
77 77 </div>
78 78 <div class="right-content">
79 79 <div>
80 80 <div class="code-header" >
81 81 <div class="compare_header">
82 82 ## The hidden elements are replaced with a select2 widget
83 83 ${h.hidden('compare_target')}
84 84 </div>
85 85 </div>
86 86 </div>
87 87 </div>
88 88 </div>
89 89
90 90 <div class="fieldset">
91 91 <div class="left-label">
92 92 ${_('Actions')}:
93 93 </div>
94 94 <div class="right-content">
95 95 <div>
96 96 <div class="code-header" >
97 97 <div class="compare_header">
98 98
99 99 <div class="compare-buttons">
100 100 % if c.compare_home:
101 101 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
102 102
103 103 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
104 104 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
105 105 <div id="changeset_compare_view_content">
106 106 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
107 107 </div>
108 108
109 109 % elif c.preview_mode:
110 110 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
111 111 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
112 112 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
113 113
114 114 % else:
115 115 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
116 116 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
117 117
118 118 ## allow comment only if there are commits to comment on
119 119 % if c.diffset and c.diffset.files and c.commit_ranges:
120 120 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
121 121 % else:
122 122 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
123 123 % endif
124 124 % endif
125 125 </div>
126 126 </div>
127 127 </div>
128 128 </div>
129 129 </div>
130 130 </div>
131 131
132 132 <%doc>
133 133 ##TODO(marcink): implement this and diff menus
134 134 <div class="fieldset">
135 135 <div class="left-label">
136 136 ${_('Diff options')}:
137 137 </div>
138 138 <div class="right-content">
139 139 <div class="diff-actions">
140 140 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
141 141 ${_('Raw Diff')}
142 142 </a>
143 143 |
144 144 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
145 145 ${_('Patch Diff')}
146 146 </a>
147 147 |
148 148 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision='?',diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
149 149 ${_('Download Diff')}
150 150 </a>
151 151 </div>
152 152 </div>
153 153 </div>
154 154 </%doc>
155 155
156 156 ## commit status form
157 157 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
158 158 <div class="left-label">
159 159 ${_('Commit status')}:
160 160 </div>
161 161 <div class="right-content">
162 162 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
163 163 ## main comment form and it status
164 164 <%
165 165 def revs(_revs):
166 166 form_inputs = []
167 167 for cs in _revs:
168 168 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
169 169 form_inputs.append(tmpl)
170 170 return form_inputs
171 171 %>
172 172 <div>
173 173 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
174 <script type="text/javascript">
175
176 mainCommentForm.setHandleFormSubmit(function(o) {
177 var text = mainCommentForm.cm.getValue();
178 var status = mainCommentForm.getCommentStatus();
179
180 if (text === "" && !status) {
181 return;
182 }
183
184 // we can pick which commits we want to make the comment by
185 // selecting them via click on preview pane, this will alter the hidden inputs
186 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
187
188 var commitIds = [];
189 $('#changeset_compare_view_content .compare_select').each(function(el) {
190 var commitId = this.id.replace('row-', '');
191 if ($(this).hasClass('hl') || !cherryPicked) {
192 $("input[data-commit-id='{0}']".format(commitId)).val(commitId)
193 commitIds.push(commitId);
194 } else {
195 $("input[data-commit-id='{0}']".format(commitId)).val('')
196 }
197 });
198
199 mainCommentForm.setActionButtonsDisabled(true);
200 mainCommentForm.cm.setOption("readOnly", true);
201 var postData = {
202 'text': text,
203 'changeset_status': status,
204 'commit_ids': commitIds,
205 'csrf_token': CSRF_TOKEN
206 };
207
208 var submitSuccessCallback = function(o) {
209 location.reload(true);
210 };
211 var submitFailCallback = function(){
212 mainCommentForm.resetCommentFormState(text)
213 };
214 mainCommentForm.submitAjaxPOST(
215 mainCommentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
216 });
217 </script>
218 174 </div>
219 175 </div>
220 176 </div>
221 177
222 178 </div> <!-- end summary-detail -->
223 179 </div> <!-- end summary -->
224 180
225 181 ## use JS script to load it quickly before potentially large diffs render long time
226 182 ## this prevents from situation when large diffs block rendering of select2 fields
227 183 <script type="text/javascript">
228 184
229 185 var cache = {};
230 186
231 187 var formatSelection = function(repoName){
232 188 return function(data, container, escapeMarkup) {
233 189 var selection = data ? this.text(data) : "";
234 190 return escapeMarkup('{0}@{1}'.format(repoName, selection));
235 191 }
236 192 };
237 193
238 194 var feedCompareData = function(query, cachedValue){
239 195 var data = {results: []};
240 196 //filter results
241 197 $.each(cachedValue.results, function() {
242 198 var section = this.text;
243 199 var children = [];
244 200 $.each(this.children, function() {
245 201 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
246 202 children.push({
247 203 'id': this.id,
248 204 'text': this.text,
249 205 'type': this.type
250 206 })
251 207 }
252 208 });
253 209 data.results.push({
254 210 'text': section,
255 211 'children': children
256 212 })
257 213 });
258 214 //push the typed in changeset
259 215 data.results.push({
260 216 'text': _gettext('specify commit'),
261 217 'children': [{
262 218 'id': query.term,
263 219 'text': query.term,
264 220 'type': 'rev'
265 221 }]
266 222 });
267 223 query.callback(data);
268 224 };
269 225
270 226 var loadCompareData = function(repoName, query, cache){
271 227 $.ajax({
272 228 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
273 229 data: {},
274 230 dataType: 'json',
275 231 type: 'GET',
276 232 success: function(data) {
277 233 cache[repoName] = data;
278 234 query.callback({results: data.results});
279 235 }
280 236 })
281 237 };
282 238
283 239 var enable_fields = ${"false" if c.preview_mode else "true"};
284 240 $("#compare_source").select2({
285 241 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
286 242 containerCssClass: "drop-menu",
287 243 dropdownCssClass: "drop-menu-dropdown",
288 244 formatSelection: formatSelection("${c.source_repo.repo_name}"),
289 245 dropdownAutoWidth: true,
290 246 query: function(query) {
291 247 var repoName = '${c.source_repo.repo_name}';
292 248 var cachedValue = cache[repoName];
293 249
294 250 if (cachedValue){
295 251 feedCompareData(query, cachedValue);
296 252 }
297 253 else {
298 254 loadCompareData(repoName, query, cache);
299 255 }
300 256 }
301 257 }).select2("enable", enable_fields);
302 258
303 259 $("#compare_target").select2({
304 260 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
305 261 dropdownAutoWidth: true,
306 262 containerCssClass: "drop-menu",
307 263 dropdownCssClass: "drop-menu-dropdown",
308 264 formatSelection: formatSelection("${c.target_repo.repo_name}"),
309 265 query: function(query) {
310 266 var repoName = '${c.target_repo.repo_name}';
311 267 var cachedValue = cache[repoName];
312 268
313 269 if (cachedValue){
314 270 feedCompareData(query, cachedValue);
315 271 }
316 272 else {
317 273 loadCompareData(repoName, query, cache);
318 274 }
319 275 }
320 276 }).select2("enable", enable_fields);
321 277 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
322 278 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
323 279
324 280 $('#compare_revs').on('click', function(e) {
325 281 var source = $('#compare_source').select2('data') || initial_compare_source;
326 282 var target = $('#compare_target').select2('data') || initial_compare_target;
327 283 if (source && target) {
328 284 var url_data = {
329 285 repo_name: "${c.repo_name}",
330 286 source_ref: source.id,
331 287 source_ref_type: source.type,
332 288 target_ref: target.id,
333 289 target_ref_type: target.type
334 290 };
335 291 window.location = pyroutes.url('compare_url', url_data);
336 292 }
337 293 });
338 294 $('#compare_changeset_status_toggle').on('click', function(e) {
339 295 $('#compare_changeset_status').toggle();
340 296 });
341 297
342 298 </script>
343 299
344 300 ## table diff data
345 301 <div class="table">
346 302
347 303
348 304 % if not c.compare_home:
349 305 <div id="changeset_compare_view_content">
350 306 <div class="pull-left">
351 307 <div class="btn-group">
352 308 <a
353 309 class="btn"
354 310 href="#"
355 311 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
356 312 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
357 313 </a>
358 314 <a
359 315 class="btn"
360 316 href="#"
361 317 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
362 318 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
363 319 </a>
364 320 </div>
365 321 </div>
366 322 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
367 323 ## commit compare generated below
368 324 <%include file="compare_commits.mako"/>
369 325 ${cbdiffs.render_diffset_menu()}
370 326 ${cbdiffs.render_diffset(c.diffset)}
371 327 </div>
372 328 % endif
373 329
374 330 </div>
375 331 </div>
376 332
377 333 </%def> No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now