##// END OF EJS Templates
pull-requests: fixed some xss problems with odd filenames.
milka -
r4652:9012cc2f default
parent child Browse files
Show More
@@ -1,1639 +1,1639 b''
1 1 // # Copyright (C) 2010-2020 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
29 29 var linkifyComments = function(comments) {
30 30 var firstCommentId = null;
31 31 if (comments) {
32 32 firstCommentId = $(comments[0]).data('comment-id');
33 33 }
34 34
35 35 if (firstCommentId){
36 36 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
37 37 }
38 38 };
39 39
40 40
41 41 var bindToggleButtons = function() {
42 42 $('.comment-toggle').on('click', function() {
43 43 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
44 44 });
45 45 };
46 46
47 47
48 48 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
49 49 failHandler = failHandler || function() {};
50 50 postData = toQueryString(postData);
51 51 var request = $.ajax({
52 52 url: url,
53 53 type: 'POST',
54 54 data: postData,
55 55 headers: {'X-PARTIAL-XHR': true}
56 56 })
57 57 .done(function (data) {
58 58 successHandler(data);
59 59 })
60 60 .fail(function (data, textStatus, errorThrown) {
61 61 failHandler(data, textStatus, errorThrown)
62 62 });
63 63 return request;
64 64 };
65 65
66 66
67 67 /* Comment form for main and inline comments */
68 68 (function(mod) {
69 69
70 70 if (typeof exports == "object" && typeof module == "object") {
71 71 // CommonJS
72 72 module.exports = mod();
73 73 }
74 74 else {
75 75 // Plain browser env
76 76 (this || window).CommentForm = mod();
77 77 }
78 78
79 79 })(function() {
80 80 "use strict";
81 81
82 82 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
83 83
84 84 if (!(this instanceof CommentForm)) {
85 85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
86 86 }
87 87
88 88 // bind the element instance to our Form
89 89 $(formElement).get(0).CommentForm = this;
90 90
91 91 this.withLineNo = function(selector) {
92 92 var lineNo = this.lineNo;
93 93 if (lineNo === undefined) {
94 94 return selector
95 95 } else {
96 96 return selector + '_' + lineNo;
97 97 }
98 98 };
99 99
100 100 this.commitId = commitId;
101 101 this.pullRequestId = pullRequestId;
102 102 this.lineNo = lineNo;
103 103 this.initAutocompleteActions = initAutocompleteActions;
104 104
105 105 this.previewButton = this.withLineNo('#preview-btn');
106 106 this.previewContainer = this.withLineNo('#preview-container');
107 107
108 108 this.previewBoxSelector = this.withLineNo('#preview-box');
109 109
110 110 this.editButton = this.withLineNo('#edit-btn');
111 111 this.editContainer = this.withLineNo('#edit-container');
112 112 this.cancelButton = this.withLineNo('#cancel-btn');
113 113 this.commentType = this.withLineNo('#comment_type');
114 114
115 115 this.resolvesId = null;
116 116 this.resolvesActionId = null;
117 117
118 118 this.closesPr = '#close_pull_request';
119 119
120 120 this.cmBox = this.withLineNo('#text');
121 121 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122 122
123 123 this.statusChange = this.withLineNo('#change_status');
124 124
125 125 this.submitForm = formElement;
126 126
127 127 this.submitButton = $(this.submitForm).find('.submit-comment-action');
128 128 this.submitButtonText = this.submitButton.val();
129 129
130 130 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
131 131 this.submitDraftButtonText = this.submitDraftButton.val();
132 132
133 133 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
134 134 {'repo_name': templateContext.repo_name,
135 135 'commit_id': templateContext.commit_data.commit_id});
136 136
137 137 if (edit){
138 138 this.submitDraftButton.hide();
139 139 this.submitButtonText = _gettext('Update Comment');
140 140 $(this.commentType).prop('disabled', true);
141 141 $(this.commentType).addClass('disabled');
142 142 var editInfo =
143 143 '';
144 144 $(editInfo).insertBefore($(this.editButton).parent());
145 145 }
146 146
147 147 if (resolvesCommentId){
148 148 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
149 149 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
150 150 $(this.commentType).prop('disabled', true);
151 151 $(this.commentType).addClass('disabled');
152 152
153 153 // disable select
154 154 setTimeout(function() {
155 155 $(self.statusChange).select2('readonly', true);
156 156 }, 10);
157 157
158 158 var resolvedInfo = (
159 159 '<li class="resolve-action">' +
160 160 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
161 161 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
162 162 '</li>'
163 163 ).format(resolvesCommentId, _gettext('resolve comment'));
164 164 $(resolvedInfo).insertAfter($(this.commentType).parent());
165 165 }
166 166
167 167 // based on commitId, or pullRequestId decide where do we submit
168 168 // out data
169 169 if (this.commitId){
170 170 var pyurl = 'repo_commit_comment_create';
171 171 if(edit){
172 172 pyurl = 'repo_commit_comment_edit';
173 173 }
174 174 this.submitUrl = pyroutes.url(pyurl,
175 175 {'repo_name': templateContext.repo_name,
176 176 'commit_id': this.commitId,
177 177 'comment_id': comment_id});
178 178 this.selfUrl = pyroutes.url('repo_commit',
179 179 {'repo_name': templateContext.repo_name,
180 180 'commit_id': this.commitId});
181 181
182 182 } else if (this.pullRequestId) {
183 183 var pyurl = 'pullrequest_comment_create';
184 184 if(edit){
185 185 pyurl = 'pullrequest_comment_edit';
186 186 }
187 187 this.submitUrl = pyroutes.url(pyurl,
188 188 {'repo_name': templateContext.repo_name,
189 189 'pull_request_id': this.pullRequestId,
190 190 'comment_id': comment_id});
191 191 this.selfUrl = pyroutes.url('pullrequest_show',
192 192 {'repo_name': templateContext.repo_name,
193 193 'pull_request_id': this.pullRequestId});
194 194
195 195 } else {
196 196 throw new Error(
197 197 'CommentForm requires pullRequestId, or commitId to be specified.')
198 198 }
199 199
200 200 // FUNCTIONS and helpers
201 201 var self = this;
202 202
203 203 this.isInline = function(){
204 204 return this.lineNo && this.lineNo != 'general';
205 205 };
206 206
207 207 this.getCmInstance = function(){
208 208 return this.cm
209 209 };
210 210
211 211 this.setPlaceholder = function(placeholder) {
212 212 var cm = this.getCmInstance();
213 213 if (cm){
214 214 cm.setOption('placeholder', placeholder);
215 215 }
216 216 };
217 217
218 218 this.getCommentStatus = function() {
219 219 return $(this.submitForm).find(this.statusChange).val();
220 220 };
221 221
222 222 this.getCommentType = function() {
223 223 return $(this.submitForm).find(this.commentType).val();
224 224 };
225 225
226 226 this.getDraftState = function () {
227 227 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
228 228 var data = $(submitterElem).data('isDraft');
229 229 return data
230 230 }
231 231
232 232 this.getResolvesId = function() {
233 233 return $(this.submitForm).find(this.resolvesId).val() || null;
234 234 };
235 235
236 236 this.getClosePr = function() {
237 237 return $(this.submitForm).find(this.closesPr).val() || null;
238 238 };
239 239
240 240 this.markCommentResolved = function(resolvedCommentId){
241 241 Rhodecode.comments.markCommentResolved(resolvedCommentId)
242 242 };
243 243
244 244 this.isAllowedToSubmit = function() {
245 245 var commentDisabled = $(this.submitButton).prop('disabled');
246 246 var draftDisabled = $(this.submitDraftButton).prop('disabled');
247 247 return !commentDisabled && !draftDisabled;
248 248 };
249 249
250 250 this.initStatusChangeSelector = function(){
251 251 var formatChangeStatus = function(state, escapeMarkup) {
252 252 var originalOption = state.element;
253 253 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
254 254 return tmpl
255 255 };
256 256 var formatResult = function(result, container, query, escapeMarkup) {
257 257 return formatChangeStatus(result, escapeMarkup);
258 258 };
259 259
260 260 var formatSelection = function(data, container, escapeMarkup) {
261 261 return formatChangeStatus(data, escapeMarkup);
262 262 };
263 263
264 264 $(this.submitForm).find(this.statusChange).select2({
265 265 placeholder: _gettext('Status Review'),
266 266 formatResult: formatResult,
267 267 formatSelection: formatSelection,
268 268 containerCssClass: "drop-menu status_box_menu",
269 269 dropdownCssClass: "drop-menu-dropdown",
270 270 dropdownAutoWidth: true,
271 271 minimumResultsForSearch: -1
272 272 });
273 273
274 274 $(this.submitForm).find(this.statusChange).on('change', function() {
275 275 var status = self.getCommentStatus();
276 276
277 277 if (status && !self.isInline()) {
278 278 $(self.submitButton).prop('disabled', false);
279 279 $(self.submitDraftButton).prop('disabled', false);
280 280 }
281 281
282 282 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
283 283 self.setPlaceholder(placeholderText)
284 284 })
285 285 };
286 286
287 287 // reset the comment form into it's original state
288 288 this.resetCommentFormState = function(content) {
289 289 content = content || '';
290 290
291 291 $(this.editContainer).show();
292 292 $(this.editButton).parent().addClass('active');
293 293
294 294 $(this.previewContainer).hide();
295 295 $(this.previewButton).parent().removeClass('active');
296 296
297 297 this.setActionButtonsDisabled(true);
298 298 self.cm.setValue(content);
299 299 self.cm.setOption("readOnly", false);
300 300
301 301 if (this.resolvesId) {
302 302 // destroy the resolve action
303 303 $(this.resolvesId).parent().remove();
304 304 }
305 305 // reset closingPR flag
306 306 $('.close-pr-input').remove();
307 307
308 308 $(this.statusChange).select2('readonly', false);
309 309 };
310 310
311 311 this.globalSubmitSuccessCallback = function(comment){
312 312 // default behaviour is to call GLOBAL hook, if it's registered.
313 313 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
314 314 commentFormGlobalSubmitSuccessCallback(comment);
315 315 }
316 316 };
317 317
318 318 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
319 319 return _submitAjaxPOST(url, postData, successHandler, failHandler);
320 320 };
321 321
322 322 // overwrite a submitHandler, we need to do it for inline comments
323 323 this.setHandleFormSubmit = function(callback) {
324 324 this.handleFormSubmit = callback;
325 325 };
326 326
327 327 // overwrite a submitSuccessHandler
328 328 this.setGlobalSubmitSuccessCallback = function(callback) {
329 329 this.globalSubmitSuccessCallback = callback;
330 330 };
331 331
332 332 // default handler for for submit for main comments
333 333 this.handleFormSubmit = function() {
334 334 var text = self.cm.getValue();
335 335 var status = self.getCommentStatus();
336 336 var commentType = self.getCommentType();
337 337 var isDraft = self.getDraftState();
338 338 var resolvesCommentId = self.getResolvesId();
339 339 var closePullRequest = self.getClosePr();
340 340
341 341 if (text === "" && !status) {
342 342 return;
343 343 }
344 344
345 345 var excludeCancelBtn = false;
346 346 var submitEvent = true;
347 347 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
348 348 self.cm.setOption("readOnly", true);
349 349
350 350 var postData = {
351 351 'text': text,
352 352 'changeset_status': status,
353 353 'comment_type': commentType,
354 354 'csrf_token': CSRF_TOKEN
355 355 };
356 356
357 357 if (resolvesCommentId) {
358 358 postData['resolves_comment_id'] = resolvesCommentId;
359 359 }
360 360
361 361 if (closePullRequest) {
362 362 postData['close_pull_request'] = true;
363 363 }
364 364
365 365 // submitSuccess for general comments
366 366 var submitSuccessCallback = function(json_data) {
367 367 // reload page if we change status for single commit.
368 368 if (status && self.commitId) {
369 369 location.reload(true);
370 370 } else {
371 371 // inject newly created comments, json_data is {<comment_id>: {}}
372 372 Rhodecode.comments.attachGeneralComment(json_data)
373 373
374 374 self.resetCommentFormState();
375 375 timeagoActivate();
376 376 tooltipActivate();
377 377
378 378 // mark visually which comment was resolved
379 379 if (resolvesCommentId) {
380 380 self.markCommentResolved(resolvesCommentId);
381 381 }
382 382 }
383 383
384 384 // run global callback on submit
385 385 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
386 386
387 387 };
388 388 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
389 389 var prefix = "Error while submitting comment.\n"
390 390 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
391 391 ajaxErrorSwal(message);
392 392 self.resetCommentFormState(text);
393 393 };
394 394 self.submitAjaxPOST(
395 395 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
396 396 };
397 397
398 398 this.previewSuccessCallback = function(o) {
399 399 $(self.previewBoxSelector).html(o);
400 400 $(self.previewBoxSelector).removeClass('unloaded');
401 401
402 402 // swap buttons, making preview active
403 403 $(self.previewButton).parent().addClass('active');
404 404 $(self.editButton).parent().removeClass('active');
405 405
406 406 // unlock buttons
407 407 self.setActionButtonsDisabled(false);
408 408 };
409 409
410 410 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
411 411 excludeCancelBtn = excludeCancelBtn || false;
412 412 submitEvent = submitEvent || false;
413 413
414 414 $(this.editButton).prop('disabled', state);
415 415 $(this.previewButton).prop('disabled', state);
416 416
417 417 if (!excludeCancelBtn) {
418 418 $(this.cancelButton).prop('disabled', state);
419 419 }
420 420
421 421 var submitState = state;
422 422 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
423 423 // if the value of commit review status is set, we allow
424 424 // submit button, but only on Main form, isInline means inline
425 425 submitState = false
426 426 }
427 427
428 428 $(this.submitButton).prop('disabled', submitState);
429 429 $(this.submitDraftButton).prop('disabled', submitState);
430 430
431 431 if (submitEvent) {
432 432 var isDraft = self.getDraftState();
433 433
434 434 if (isDraft) {
435 435 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
436 436 } else {
437 437 $(this.submitButton).val(_gettext('Submitting...'));
438 438 }
439 439
440 440 } else {
441 441 $(this.submitButton).val(this.submitButtonText);
442 442 $(this.submitDraftButton).val(this.submitDraftButtonText);
443 443 }
444 444
445 445 };
446 446
447 447 // lock preview/edit/submit buttons on load, but exclude cancel button
448 448 var excludeCancelBtn = true;
449 449 this.setActionButtonsDisabled(true, excludeCancelBtn);
450 450
451 451 // anonymous users don't have access to initialized CM instance
452 452 if (this.cm !== undefined){
453 453 this.cm.on('change', function(cMirror) {
454 454 if (cMirror.getValue() === "") {
455 455 self.setActionButtonsDisabled(true, excludeCancelBtn)
456 456 } else {
457 457 self.setActionButtonsDisabled(false, excludeCancelBtn)
458 458 }
459 459 });
460 460 }
461 461
462 462 $(this.editButton).on('click', function(e) {
463 463 e.preventDefault();
464 464
465 465 $(self.previewButton).parent().removeClass('active');
466 466 $(self.previewContainer).hide();
467 467
468 468 $(self.editButton).parent().addClass('active');
469 469 $(self.editContainer).show();
470 470
471 471 });
472 472
473 473 $(this.previewButton).on('click', function(e) {
474 474 e.preventDefault();
475 475 var text = self.cm.getValue();
476 476
477 477 if (text === "") {
478 478 return;
479 479 }
480 480
481 481 var postData = {
482 482 'text': text,
483 483 'renderer': templateContext.visual.default_renderer,
484 484 'csrf_token': CSRF_TOKEN
485 485 };
486 486
487 487 // lock ALL buttons on preview
488 488 self.setActionButtonsDisabled(true);
489 489
490 490 $(self.previewBoxSelector).addClass('unloaded');
491 491 $(self.previewBoxSelector).html(_gettext('Loading ...'));
492 492
493 493 $(self.editContainer).hide();
494 494 $(self.previewContainer).show();
495 495
496 496 // by default we reset state of comment preserving the text
497 497 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
498 498 var prefix = "Error while preview of comment.\n"
499 499 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
500 500 ajaxErrorSwal(message);
501 501
502 502 self.resetCommentFormState(text)
503 503 };
504 504 self.submitAjaxPOST(
505 505 self.previewUrl, postData, self.previewSuccessCallback,
506 506 previewFailCallback);
507 507
508 508 $(self.previewButton).parent().addClass('active');
509 509 $(self.editButton).parent().removeClass('active');
510 510 });
511 511
512 512 $(this.submitForm).submit(function(e) {
513 513 e.preventDefault();
514 514 var allowedToSubmit = self.isAllowedToSubmit();
515 515 if (!allowedToSubmit){
516 516 return false;
517 517 }
518 518
519 519 self.handleFormSubmit();
520 520 });
521 521
522 522 }
523 523
524 524 return CommentForm;
525 525 });
526 526
527 527 /* selector for comment versions */
528 528 var initVersionSelector = function(selector, initialData) {
529 529
530 530 var formatResult = function(result, container, query, escapeMarkup) {
531 531
532 532 return renderTemplate('commentVersion', {
533 533 show_disabled: true,
534 534 version: result.comment_version,
535 535 user_name: result.comment_author_username,
536 536 gravatar_url: result.comment_author_gravatar,
537 537 size: 16,
538 538 timeago_component: result.comment_created_on,
539 539 })
540 540 };
541 541
542 542 $(selector).select2({
543 543 placeholder: "Edited",
544 544 containerCssClass: "drop-menu-comment-history",
545 545 dropdownCssClass: "drop-menu-dropdown",
546 546 dropdownAutoWidth: true,
547 547 minimumResultsForSearch: -1,
548 548 data: initialData,
549 549 formatResult: formatResult,
550 550 });
551 551
552 552 $(selector).on('select2-selecting', function (e) {
553 553 // hide the mast as we later do preventDefault()
554 554 $("#select2-drop-mask").click();
555 555 e.preventDefault();
556 556 e.choice.action();
557 557 });
558 558
559 559 $(selector).on("select2-open", function() {
560 560 timeagoActivate();
561 561 });
562 562 };
563 563
564 564 /* comments controller */
565 565 var CommentsController = function() {
566 566 var mainComment = '#text';
567 567 var self = this;
568 568
569 569 this.showVersion = function (comment_id, comment_history_id) {
570 570
571 571 var historyViewUrl = pyroutes.url(
572 572 'repo_commit_comment_history_view',
573 573 {
574 574 'repo_name': templateContext.repo_name,
575 575 'commit_id': comment_id,
576 576 'comment_history_id': comment_history_id,
577 577 }
578 578 );
579 579 successRenderCommit = function (data) {
580 580 SwalNoAnimation.fire({
581 581 html: data,
582 582 title: '',
583 583 });
584 584 };
585 585 failRenderCommit = function () {
586 586 SwalNoAnimation.fire({
587 587 html: 'Error while loading comment history',
588 588 title: '',
589 589 });
590 590 };
591 591 _submitAjaxPOST(
592 592 historyViewUrl, {'csrf_token': CSRF_TOKEN},
593 593 successRenderCommit,
594 594 failRenderCommit
595 595 );
596 596 };
597 597
598 598 this.getLineNumber = function(node) {
599 599 var $node = $(node);
600 600 var lineNo = $node.closest('td').attr('data-line-no');
601 601 if (lineNo === undefined && $node.data('commentInline')){
602 602 lineNo = $node.data('commentLineNo')
603 603 }
604 604
605 605 return lineNo
606 606 };
607 607
608 608 this.scrollToComment = function(node, offset, outdated) {
609 609 if (offset === undefined) {
610 610 offset = 0;
611 611 }
612 612 var outdated = outdated || false;
613 613 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
614 614
615 615 if (!node) {
616 616 node = $('.comment-selected');
617 617 if (!node.length) {
618 618 node = $('comment-current')
619 619 }
620 620 }
621 621
622 622 $wrapper = $(node).closest('div.comment');
623 623
624 624 // show hidden comment when referenced.
625 625 if (!$wrapper.is(':visible')){
626 626 $wrapper.show();
627 627 }
628 628
629 629 $comment = $(node).closest(klass);
630 630 $comments = $(klass);
631 631
632 632 $('.comment-selected').removeClass('comment-selected');
633 633
634 634 var nextIdx = $(klass).index($comment) + offset;
635 635 if (nextIdx >= $comments.length) {
636 636 nextIdx = 0;
637 637 }
638 638 var $next = $(klass).eq(nextIdx);
639 639
640 640 var $cb = $next.closest('.cb');
641 641 $cb.removeClass('cb-collapsed');
642 642
643 643 var $filediffCollapseState = $cb.closest('.filediff').prev();
644 644 $filediffCollapseState.prop('checked', false);
645 645 $next.addClass('comment-selected');
646 646 scrollToElement($next);
647 647 return false;
648 648 };
649 649
650 650 this.nextComment = function(node) {
651 651 return self.scrollToComment(node, 1);
652 652 };
653 653
654 654 this.prevComment = function(node) {
655 655 return self.scrollToComment(node, -1);
656 656 };
657 657
658 658 this.nextOutdatedComment = function(node) {
659 659 return self.scrollToComment(node, 1, true);
660 660 };
661 661
662 662 this.prevOutdatedComment = function(node) {
663 663 return self.scrollToComment(node, -1, true);
664 664 };
665 665
666 666 this.cancelComment = function (node) {
667 667 var $node = $(node);
668 668 var edit = $(this).attr('edit');
669 669 var $inlineComments = $node.closest('div.inline-comments');
670 670
671 671 if (edit) {
672 672 var $general_comments = null;
673 673 if (!$inlineComments.length) {
674 674 $general_comments = $('#comments');
675 675 var $comment = $general_comments.parent().find('div.comment:hidden');
676 676 // show hidden general comment form
677 677 $('#cb-comment-general-form-placeholder').show();
678 678 } else {
679 679 var $comment = $inlineComments.find('div.comment:hidden');
680 680 }
681 681 $comment.show();
682 682 }
683 683 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
684 684 $replyWrapper.removeClass('comment-form-active');
685 685
686 686 var lastComment = $inlineComments.find('.comment-inline').last();
687 687 if ($(lastComment).hasClass('comment-outdated')) {
688 688 $replyWrapper.hide();
689 689 }
690 690
691 691 $node.closest('.comment-inline-form').remove();
692 692 return false;
693 693 };
694 694
695 695 this._deleteComment = function(node) {
696 696 var $node = $(node);
697 697 var $td = $node.closest('td');
698 698 var $comment = $node.closest('.comment');
699 699 var comment_id = $($comment).data('commentId');
700 700 var isDraft = $($comment).data('commentDraft');
701 701
702 702 var pullRequestId = templateContext.pull_request_data.pull_request_id;
703 703 var commitId = templateContext.commit_data.commit_id;
704 704
705 705 if (pullRequestId) {
706 706 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
707 707 } else if (commitId) {
708 708 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
709 709 }
710 710
711 711 var postData = {
712 712 'csrf_token': CSRF_TOKEN
713 713 };
714 714
715 715 $comment.addClass('comment-deleting');
716 716 $comment.hide('fast');
717 717
718 718 var success = function(response) {
719 719 $comment.remove();
720 720
721 721 if (window.updateSticky !== undefined) {
722 722 // potentially our comments change the active window size, so we
723 723 // notify sticky elements
724 724 updateSticky()
725 725 }
726 726
727 727 if (window.refreshAllComments !== undefined && !isDraft) {
728 728 // if we have this handler, run it, and refresh all comments boxes
729 729 refreshAllComments()
730 730 }
731 731 else if (window.refreshDraftComments !== undefined && isDraft) {
732 732 // if we have this handler, run it, and refresh all comments boxes
733 733 refreshDraftComments();
734 734 }
735 735 return false;
736 736 };
737 737
738 738 var failure = function(jqXHR, textStatus, errorThrown) {
739 739 var prefix = "Error while deleting this comment.\n"
740 740 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
741 741 ajaxErrorSwal(message);
742 742
743 743 $comment.show('fast');
744 744 $comment.removeClass('comment-deleting');
745 745 return false;
746 746 };
747 747 ajaxPOST(url, postData, success, failure);
748 748
749 749 }
750 750
751 751 this.deleteComment = function(node) {
752 752 var $comment = $(node).closest('.comment');
753 753 var comment_id = $comment.attr('data-comment-id');
754 754
755 755 SwalNoAnimation.fire({
756 756 title: 'Delete this comment?',
757 757 icon: 'warning',
758 758 showCancelButton: true,
759 759 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
760 760
761 761 }).then(function(result) {
762 762 if (result.value) {
763 763 self._deleteComment(node);
764 764 }
765 765 })
766 766 };
767 767
768 768 this._finalizeDrafts = function(commentIds) {
769 769
770 770 var pullRequestId = templateContext.pull_request_data.pull_request_id;
771 771 var commitId = templateContext.commit_data.commit_id;
772 772
773 773 if (pullRequestId) {
774 774 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
775 775 } else if (commitId) {
776 776 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
777 777 }
778 778
779 779 // remove the drafts so we can lock them before submit.
780 780 $.each(commentIds, function(idx, val){
781 781 $('#comment-{0}'.format(val)).remove();
782 782 })
783 783
784 784 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
785 785
786 786 var submitSuccessCallback = function(json_data) {
787 787 self.attachInlineComment(json_data);
788 788
789 789 if (window.refreshDraftComments !== undefined) {
790 790 // if we have this handler, run it, and refresh all comments boxes
791 791 refreshDraftComments()
792 792 }
793 793
794 794 return false;
795 795 };
796 796
797 797 ajaxPOST(url, postData, submitSuccessCallback)
798 798
799 799 }
800 800
801 801 this.finalizeDrafts = function(commentIds, callback) {
802 802
803 803 SwalNoAnimation.fire({
804 804 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
805 805 icon: 'warning',
806 806 showCancelButton: true,
807 807 confirmButtonText: _gettext('Yes'),
808 808
809 809 }).then(function(result) {
810 810 if (result.value) {
811 811 if (callback !== undefined) {
812 812 callback(result)
813 813 }
814 814 self._finalizeDrafts(commentIds);
815 815 }
816 816 })
817 817 };
818 818
819 819 this.toggleWideMode = function (node) {
820 820
821 821 if ($('#content').hasClass('wrapper')) {
822 822 $('#content').removeClass("wrapper");
823 823 $('#content').addClass("wide-mode-wrapper");
824 824 $(node).addClass('btn-success');
825 825 return true
826 826 } else {
827 827 $('#content').removeClass("wide-mode-wrapper");
828 828 $('#content').addClass("wrapper");
829 829 $(node).removeClass('btn-success');
830 830 return false
831 831 }
832 832
833 833 };
834 834
835 835 /**
836 836 * Turn off/on all comments in file diff
837 837 */
838 838 this.toggleDiffComments = function(node) {
839 839 // Find closes filediff container
840 840 var $filediff = $(node).closest('.filediff');
841 841 if ($(node).hasClass('toggle-on')) {
842 842 var show = false;
843 843 } else if ($(node).hasClass('toggle-off')) {
844 844 var show = true;
845 845 }
846 846
847 847 // Toggle each individual comment block, so we can un-toggle single ones
848 848 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
849 849 self.toggleLineComments($(val), show)
850 850 })
851 851
852 852 // since we change the height of the diff container that has anchor points for upper
853 853 // sticky header, we need to tell it to re-calculate those
854 854 if (window.updateSticky !== undefined) {
855 855 // potentially our comments change the active window size, so we
856 856 // notify sticky elements
857 857 updateSticky()
858 858 }
859 859
860 860 return false;
861 861 }
862 862
863 863 this.toggleLineComments = function(node, show) {
864 864
865 865 var trElem = $(node).closest('tr')
866 866
867 867 if (show === true) {
868 868 // mark outdated comments as visible before the toggle;
869 869 $(trElem).find('.comment-outdated').show();
870 870 $(trElem).removeClass('hide-line-comments');
871 871 } else if (show === false) {
872 872 $(trElem).find('.comment-outdated').hide();
873 873 $(trElem).addClass('hide-line-comments');
874 874 } else {
875 875 // mark outdated comments as visible before the toggle;
876 876 $(trElem).find('.comment-outdated').show();
877 877 $(trElem).toggleClass('hide-line-comments');
878 878 }
879 879
880 880 // since we change the height of the diff container that has anchor points for upper
881 881 // sticky header, we need to tell it to re-calculate those
882 882 if (window.updateSticky !== undefined) {
883 883 // potentially our comments change the active window size, so we
884 884 // notify sticky elements
885 885 updateSticky()
886 886 }
887 887
888 888 };
889 889
890 890 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
891 891 var pullRequestId = templateContext.pull_request_data.pull_request_id;
892 892 var commitId = templateContext.commit_data.commit_id;
893 893
894 894 var commentForm = new CommentForm(
895 895 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
896 896 var cm = commentForm.getCmInstance();
897 897
898 898 if (resolvesCommentId){
899 899 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
900 900 }
901 901
902 902 setTimeout(function() {
903 903 // callbacks
904 904 if (cm !== undefined) {
905 905 commentForm.setPlaceholder(placeholderText);
906 906 if (commentForm.isInline()) {
907 907 cm.focus();
908 908 cm.refresh();
909 909 }
910 910 }
911 911 }, 10);
912 912
913 913 // trigger scrolldown to the resolve comment, since it might be away
914 914 // from the clicked
915 915 if (resolvesCommentId){
916 916 var actionNode = $(commentForm.resolvesActionId).offset();
917 917
918 918 setTimeout(function() {
919 919 if (actionNode) {
920 920 $('body, html').animate({scrollTop: actionNode.top}, 10);
921 921 }
922 922 }, 100);
923 923 }
924 924
925 925 // add dropzone support
926 926 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
927 927 var renderer = templateContext.visual.default_renderer;
928 928 if (renderer == 'rst') {
929 929 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
930 930 if (isRendered){
931 931 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
932 932 }
933 933 } else if (renderer == 'markdown') {
934 934 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
935 935 if (isRendered){
936 936 attachmentUrl = '!' + attachmentUrl;
937 937 }
938 938 } else {
939 939 var attachmentUrl = '{}'.format(attachmentStoreUrl);
940 940 }
941 941 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
942 942
943 943 return false;
944 944 };
945 945
946 946 //see: https://www.dropzonejs.com/#configuration
947 947 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
948 948 {'repo_name': templateContext.repo_name,
949 949 'commit_id': templateContext.commit_data.commit_id})
950 950
951 951 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
952 952 if (previewTmpl !== undefined){
953 953 var selectLink = $(formElement).find('.pick-attachment').get(0);
954 954 $(formElement).find('.comment-attachment-uploader').dropzone({
955 955 url: storeUrl,
956 956 headers: {"X-CSRF-Token": CSRF_TOKEN},
957 957 paramName: function () {
958 958 return "attachment"
959 959 }, // The name that will be used to transfer the file
960 960 clickable: selectLink,
961 961 parallelUploads: 1,
962 962 maxFiles: 10,
963 963 maxFilesize: templateContext.attachment_store.max_file_size_mb,
964 964 uploadMultiple: false,
965 965 autoProcessQueue: true, // if false queue will not be processed automatically.
966 966 createImageThumbnails: false,
967 967 previewTemplate: previewTmpl.innerHTML,
968 968
969 969 accept: function (file, done) {
970 970 done();
971 971 },
972 972 init: function () {
973 973
974 974 this.on("sending", function (file, xhr, formData) {
975 975 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
976 976 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
977 977 });
978 978
979 979 this.on("success", function (file, response) {
980 980 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
981 981 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
982 982
983 983 var isRendered = false;
984 984 var ext = file.name.split('.').pop();
985 985 var imageExts = templateContext.attachment_store.image_ext;
986 986 if (imageExts.indexOf(ext) !== -1){
987 987 isRendered = true;
988 988 }
989 989
990 990 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
991 991 });
992 992
993 993 this.on("error", function (file, errorMessage, xhr) {
994 994 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
995 995
996 996 var error = null;
997 997
998 998 if (xhr !== undefined){
999 999 var httpStatus = xhr.status + " " + xhr.statusText;
1000 1000 if (xhr !== undefined && xhr.status >= 500) {
1001 1001 error = httpStatus;
1002 1002 }
1003 1003 }
1004 1004
1005 1005 if (error === null) {
1006 1006 error = errorMessage.error || errorMessage || httpStatus;
1007 1007 }
1008 1008 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1009 1009
1010 1010 });
1011 1011 }
1012 1012 });
1013 1013 }
1014 1014 return commentForm;
1015 1015 };
1016 1016
1017 1017 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1018 1018
1019 1019 var tmpl = $('#cb-comment-general-form-template').html();
1020 1020 tmpl = tmpl.format(null, 'general');
1021 1021 var $form = $(tmpl);
1022 1022
1023 1023 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1024 1024 var curForm = $formPlaceholder.find('form');
1025 1025 if (curForm){
1026 1026 curForm.remove();
1027 1027 }
1028 1028 $formPlaceholder.append($form);
1029 1029
1030 1030 var _form = $($form[0]);
1031 1031 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1032 1032 var edit = false;
1033 1033 var comment_id = null;
1034 1034 var commentForm = this.createCommentForm(
1035 1035 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1036 1036 commentForm.initStatusChangeSelector();
1037 1037
1038 1038 return commentForm;
1039 1039 };
1040 1040
1041 1041 this.editComment = function(node, line_no, f_path) {
1042 1042 self.edit = true;
1043 1043 var $node = $(node);
1044 1044 var $td = $node.closest('td');
1045 1045
1046 1046 var $comment = $(node).closest('.comment');
1047 1047 var comment_id = $($comment).data('commentId');
1048 1048 var isDraft = $($comment).data('commentDraft');
1049 1049 var $editForm = null
1050 1050
1051 1051 var $comments = $node.closest('div.inline-comments');
1052 1052 var $general_comments = null;
1053 1053
1054 1054 if($comments.length){
1055 1055 // inline comments setup
1056 1056 $editForm = $comments.find('.comment-inline-form');
1057 1057 line_no = self.getLineNumber(node)
1058 1058 }
1059 1059 else{
1060 1060 // general comments setup
1061 1061 $comments = $('#comments');
1062 1062 $editForm = $comments.find('.comment-inline-form');
1063 1063 line_no = $comment[0].id
1064 1064 $('#cb-comment-general-form-placeholder').hide();
1065 1065 }
1066 1066
1067 1067 if ($editForm.length === 0) {
1068 1068
1069 1069 // unhide all comments if they are hidden for a proper REPLY mode
1070 1070 var $filediff = $node.closest('.filediff');
1071 1071 $filediff.removeClass('hide-comments');
1072 1072
1073 1073 $editForm = self.createNewFormWrapper(f_path, line_no);
1074 1074 if(f_path && line_no) {
1075 1075 $editForm.addClass('comment-inline-form-edit')
1076 1076 }
1077 1077
1078 1078 $comment.after($editForm)
1079 1079
1080 1080 var _form = $($editForm[0]).find('form');
1081 1081 var autocompleteActions = ['as_note',];
1082 1082 var commentForm = this.createCommentForm(
1083 1083 _form, line_no, '', autocompleteActions, resolvesCommentId,
1084 1084 this.edit, comment_id);
1085 1085 var old_comment_text_binary = $comment.attr('data-comment-text');
1086 1086 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1087 1087 commentForm.cm.setValue(old_comment_text);
1088 1088 $comment.hide();
1089 1089 tooltipActivate();
1090 1090
1091 1091 // set a CUSTOM submit handler for inline comment edit action.
1092 1092 commentForm.setHandleFormSubmit(function(o) {
1093 1093 var text = commentForm.cm.getValue();
1094 1094 var commentType = commentForm.getCommentType();
1095 1095
1096 1096 if (text === "") {
1097 1097 return;
1098 1098 }
1099 1099
1100 1100 if (old_comment_text == text) {
1101 1101 SwalNoAnimation.fire({
1102 1102 title: 'Unable to edit comment',
1103 1103 html: _gettext('Comment body was not changed.'),
1104 1104 });
1105 1105 return;
1106 1106 }
1107 1107 var excludeCancelBtn = false;
1108 1108 var submitEvent = true;
1109 1109 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1110 1110 commentForm.cm.setOption("readOnly", true);
1111 1111
1112 1112 // Read last version known
1113 1113 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1114 1114 var version = versionSelector.data('lastVersion');
1115 1115
1116 1116 if (!version) {
1117 1117 version = 0;
1118 1118 }
1119 1119
1120 1120 var postData = {
1121 1121 'text': text,
1122 1122 'f_path': f_path,
1123 1123 'line': line_no,
1124 1124 'comment_type': commentType,
1125 1125 'draft': isDraft,
1126 1126 'version': version,
1127 1127 'csrf_token': CSRF_TOKEN
1128 1128 };
1129 1129
1130 1130 var submitSuccessCallback = function(json_data) {
1131 1131 $editForm.remove();
1132 1132 $comment.show();
1133 1133 var postData = {
1134 1134 'text': text,
1135 1135 'renderer': $comment.attr('data-comment-renderer'),
1136 1136 'csrf_token': CSRF_TOKEN
1137 1137 };
1138 1138
1139 1139 /* Inject new edited version selector */
1140 1140 var updateCommentVersionDropDown = function () {
1141 1141 var versionSelectId = '#comment_versions_'+comment_id;
1142 1142 var preLoadVersionData = [
1143 1143 {
1144 1144 id: json_data['comment_version'],
1145 1145 text: "v{0}".format(json_data['comment_version']),
1146 1146 action: function () {
1147 1147 Rhodecode.comments.showVersion(
1148 1148 json_data['comment_id'],
1149 1149 json_data['comment_history_id']
1150 1150 )
1151 1151 },
1152 1152 comment_version: json_data['comment_version'],
1153 1153 comment_author_username: json_data['comment_author_username'],
1154 1154 comment_author_gravatar: json_data['comment_author_gravatar'],
1155 1155 comment_created_on: json_data['comment_created_on'],
1156 1156 },
1157 1157 ]
1158 1158
1159 1159
1160 1160 if ($(versionSelectId).data('select2')) {
1161 1161 var oldData = $(versionSelectId).data('select2').opts.data.results;
1162 1162 $(versionSelectId).select2("destroy");
1163 1163 preLoadVersionData = oldData.concat(preLoadVersionData)
1164 1164 }
1165 1165
1166 1166 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1167 1167
1168 1168 $comment.attr('data-comment-text', utf8ToB64(text));
1169 1169
1170 1170 var versionSelector = $('#comment_versions_'+comment_id);
1171 1171
1172 1172 // set lastVersion so we know our last edit version
1173 1173 versionSelector.data('lastVersion', json_data['comment_version'])
1174 1174 versionSelector.parent().show();
1175 1175 }
1176 1176 updateCommentVersionDropDown();
1177 1177
1178 1178 // by default we reset state of comment preserving the text
1179 1179 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1180 1180 var prefix = "Error while editing this comment.\n"
1181 1181 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1182 1182 ajaxErrorSwal(message);
1183 1183 };
1184 1184
1185 1185 var successRenderCommit = function(o){
1186 1186 $comment.show();
1187 1187 $comment[0].lastElementChild.innerHTML = o;
1188 1188 };
1189 1189
1190 1190 var previewUrl = pyroutes.url(
1191 1191 'repo_commit_comment_preview',
1192 1192 {'repo_name': templateContext.repo_name,
1193 1193 'commit_id': templateContext.commit_data.commit_id});
1194 1194
1195 1195 _submitAjaxPOST(
1196 1196 previewUrl, postData, successRenderCommit, failRenderCommit
1197 1197 );
1198 1198
1199 1199 try {
1200 1200 var html = json_data.rendered_text;
1201 1201 var lineno = json_data.line_no;
1202 1202 var target_id = json_data.target_id;
1203 1203
1204 1204 $comments.find('.cb-comment-add-button').before(html);
1205 1205
1206 1206 // run global callback on submit
1207 1207 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1208 1208
1209 1209 } catch (e) {
1210 1210 console.error(e);
1211 1211 }
1212 1212
1213 1213 // re trigger the linkification of next/prev navigation
1214 1214 linkifyComments($('.inline-comment-injected'));
1215 1215 timeagoActivate();
1216 1216 tooltipActivate();
1217 1217
1218 1218 if (window.updateSticky !== undefined) {
1219 1219 // potentially our comments change the active window size, so we
1220 1220 // notify sticky elements
1221 1221 updateSticky()
1222 1222 }
1223 1223
1224 1224 if (window.refreshAllComments !== undefined && !isDraft) {
1225 1225 // if we have this handler, run it, and refresh all comments boxes
1226 1226 refreshAllComments()
1227 1227 }
1228 1228 else if (window.refreshDraftComments !== undefined && isDraft) {
1229 1229 // if we have this handler, run it, and refresh all comments boxes
1230 1230 refreshDraftComments();
1231 1231 }
1232 1232
1233 1233 commentForm.setActionButtonsDisabled(false);
1234 1234
1235 1235 };
1236 1236
1237 1237 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1238 1238 var prefix = "Error while editing comment.\n"
1239 1239 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1240 1240 if (jqXHR.status == 409){
1241 1241 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1242 1242 ajaxErrorSwal(message, 'Comment version mismatch.');
1243 1243 } else {
1244 1244 ajaxErrorSwal(message);
1245 1245 }
1246 1246
1247 1247 commentForm.resetCommentFormState(text)
1248 1248 };
1249 1249 commentForm.submitAjaxPOST(
1250 1250 commentForm.submitUrl, postData,
1251 1251 submitSuccessCallback,
1252 1252 submitFailCallback);
1253 1253 });
1254 1254 }
1255 1255
1256 1256 $editForm.addClass('comment-inline-form-open');
1257 1257 };
1258 1258
1259 1259 this.attachComment = function(json_data) {
1260 1260 var self = this;
1261 1261 $.each(json_data, function(idx, val) {
1262 1262 var json_data_elem = [val]
1263 1263 var isInline = val.comment_f_path && val.comment_lineno
1264 1264
1265 1265 if (isInline) {
1266 1266 self.attachInlineComment(json_data_elem)
1267 1267 } else {
1268 1268 self.attachGeneralComment(json_data_elem)
1269 1269 }
1270 1270 })
1271 1271
1272 1272 }
1273 1273
1274 1274 this.attachGeneralComment = function(json_data) {
1275 1275 $.each(json_data, function(idx, val) {
1276 1276 $('#injected_page_comments').append(val.rendered_text);
1277 1277 })
1278 1278 }
1279 1279
1280 1280 this.attachInlineComment = function(json_data) {
1281 1281
1282 1282 $.each(json_data, function (idx, val) {
1283 1283 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1284 1284 var html = val.rendered_text;
1285 1285 var $inlineComments = $('#' + val.target_id)
1286 1286 .find(line_qry)
1287 1287 .find('.inline-comments');
1288 1288
1289 1289 var lastComment = $inlineComments.find('.comment-inline').last();
1290 1290
1291 1291 if (lastComment.length === 0) {
1292 1292 // first comment, we append simply
1293 1293 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1294 1294 } else {
1295 1295 $(lastComment).after(html)
1296 1296 }
1297 1297
1298 1298 })
1299 1299
1300 1300 };
1301 1301
1302 1302 this.createNewFormWrapper = function(f_path, line_no) {
1303 1303 // create a new reply HTML form from template
1304 1304 var tmpl = $('#cb-comment-inline-form-template').html();
1305 1305 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1306 1306 return $(tmpl);
1307 1307 }
1308 1308
1309 1309 this.markCommentResolved = function(commentId) {
1310 1310 $('#comment-label-{0}'.format(commentId)).find('.resolved').show();
1311 1311 $('#comment-label-{0}'.format(commentId)).find('.resolve').hide();
1312 1312 };
1313 1313
1314 1314 this.createComment = function(node, f_path, line_no, resolutionComment) {
1315 1315 self.edit = false;
1316 1316 var $node = $(node);
1317 1317 var $td = $node.closest('td');
1318 1318 var resolvesCommentId = resolutionComment || null;
1319 1319
1320 1320 var $replyForm = $td.find('.comment-inline-form');
1321 1321
1322 1322 // if form isn't existing, we're generating a new one and injecting it.
1323 1323 if ($replyForm.length === 0) {
1324 1324
1325 1325 // unhide/expand all comments if they are hidden for a proper REPLY mode
1326 1326 self.toggleLineComments($node, true);
1327 1327
1328 1328 $replyForm = self.createNewFormWrapper(f_path, line_no);
1329 1329
1330 1330 var $comments = $td.find('.inline-comments');
1331 1331
1332 1332 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1333 1333 if ($comments.length===0) {
1334 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1334 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(escapeHtml(f_path), line_no)
1335 1335 var $reply_container = $('#cb-comments-inline-container-template')
1336 1336 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1337 1337 $td.append($($reply_container).html());
1338 1338 }
1339 1339
1340 1340 // default comment button exists, so we prepend the form for leaving initial comment
1341 1341 $td.find('.cb-comment-add-button').before($replyForm);
1342 1342 // set marker, that we have a open form
1343 1343 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1344 1344 $replyWrapper.addClass('comment-form-active');
1345 1345
1346 1346 var lastComment = $comments.find('.comment-inline').last();
1347 1347 if ($(lastComment).hasClass('comment-outdated')) {
1348 1348 $replyWrapper.show();
1349 1349 }
1350 1350
1351 1351 var _form = $($replyForm[0]).find('form');
1352 1352 var autocompleteActions = ['as_note', 'as_todo'];
1353 1353 var comment_id=null;
1354 1354 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1355 1355 var commentForm = self.createCommentForm(
1356 1356 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1357 1357 self.edit, comment_id);
1358 1358
1359 1359 // set a CUSTOM submit handler for inline comments.
1360 1360 commentForm.setHandleFormSubmit(function(o) {
1361 1361 var text = commentForm.cm.getValue();
1362 1362 var commentType = commentForm.getCommentType();
1363 1363 var resolvesCommentId = commentForm.getResolvesId();
1364 1364 var isDraft = commentForm.getDraftState();
1365 1365
1366 1366 if (text === "") {
1367 1367 return;
1368 1368 }
1369 1369
1370 1370 if (line_no === undefined) {
1371 1371 alert('Error: unable to fetch line number for this inline comment !');
1372 1372 return;
1373 1373 }
1374 1374
1375 1375 if (f_path === undefined) {
1376 1376 alert('Error: unable to fetch file path for this inline comment !');
1377 1377 return;
1378 1378 }
1379 1379
1380 1380 var excludeCancelBtn = false;
1381 1381 var submitEvent = true;
1382 1382 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1383 1383 commentForm.cm.setOption("readOnly", true);
1384 1384 var postData = {
1385 1385 'text': text,
1386 1386 'f_path': f_path,
1387 1387 'line': line_no,
1388 1388 'comment_type': commentType,
1389 1389 'draft': isDraft,
1390 1390 'csrf_token': CSRF_TOKEN
1391 1391 };
1392 1392 if (resolvesCommentId){
1393 1393 postData['resolves_comment_id'] = resolvesCommentId;
1394 1394 }
1395 1395
1396 1396 // submitSuccess for inline commits
1397 1397 var submitSuccessCallback = function(json_data) {
1398 1398
1399 1399 $replyForm.remove();
1400 1400 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1401 1401
1402 1402 try {
1403 1403
1404 1404 // inject newly created comments, json_data is {<comment_id>: {}}
1405 1405 self.attachInlineComment(json_data)
1406 1406
1407 1407 //mark visually which comment was resolved
1408 1408 if (resolvesCommentId) {
1409 1409 self.markCommentResolved(resolvesCommentId);
1410 1410 }
1411 1411
1412 1412 // run global callback on submit
1413 1413 commentForm.globalSubmitSuccessCallback({
1414 1414 draft: isDraft,
1415 1415 comment_id: comment_id
1416 1416 });
1417 1417
1418 1418 } catch (e) {
1419 1419 console.error(e);
1420 1420 }
1421 1421
1422 1422 if (window.updateSticky !== undefined) {
1423 1423 // potentially our comments change the active window size, so we
1424 1424 // notify sticky elements
1425 1425 updateSticky()
1426 1426 }
1427 1427
1428 1428 if (window.refreshAllComments !== undefined && !isDraft) {
1429 1429 // if we have this handler, run it, and refresh all comments boxes
1430 1430 refreshAllComments()
1431 1431 }
1432 1432 else if (window.refreshDraftComments !== undefined && isDraft) {
1433 1433 // if we have this handler, run it, and refresh all comments boxes
1434 1434 refreshDraftComments();
1435 1435 }
1436 1436
1437 1437 commentForm.setActionButtonsDisabled(false);
1438 1438
1439 1439 // re trigger the linkification of next/prev navigation
1440 1440 linkifyComments($('.inline-comment-injected'));
1441 1441 timeagoActivate();
1442 1442 tooltipActivate();
1443 1443 };
1444 1444
1445 1445 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1446 1446 var prefix = "Error while submitting comment.\n"
1447 1447 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1448 1448 ajaxErrorSwal(message);
1449 1449 commentForm.resetCommentFormState(text)
1450 1450 };
1451 1451
1452 1452 commentForm.submitAjaxPOST(
1453 1453 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1454 1454 });
1455 1455 }
1456 1456
1457 1457 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1458 1458 $replyForm.addClass('comment-inline-form-open');
1459 1459 tooltipActivate();
1460 1460 };
1461 1461
1462 1462 this.createResolutionComment = function(commentId){
1463 1463 // hide the trigger text
1464 1464 $('#resolve-comment-{0}'.format(commentId)).hide();
1465 1465
1466 1466 var comment = $('#comment-'+commentId);
1467 1467 var commentData = comment.data();
1468 1468
1469 1469 if (commentData.commentInline) {
1470 1470 var f_path = commentData.commentFPath;
1471 1471 var line_no = commentData.commentLineNo;
1472 1472 this.createComment(comment, f_path, line_no, commentId)
1473 1473 } else {
1474 1474 this.createGeneralComment('general', "$placeholder", commentId)
1475 1475 }
1476 1476
1477 1477 return false;
1478 1478 };
1479 1479
1480 1480 this.submitResolution = function(commentId){
1481 1481 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1482 1482 var commentForm = form.get(0).CommentForm;
1483 1483
1484 1484 var cm = commentForm.getCmInstance();
1485 1485 var renderer = templateContext.visual.default_renderer;
1486 1486 if (renderer == 'rst'){
1487 1487 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1488 1488 } else if (renderer == 'markdown') {
1489 1489 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1490 1490 } else {
1491 1491 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1492 1492 }
1493 1493
1494 1494 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1495 1495 form.submit();
1496 1496 return false;
1497 1497 };
1498 1498
1499 1499 this.resolveTodo = function (elem, todoId) {
1500 1500 var commentId = todoId;
1501 1501
1502 1502 SwalNoAnimation.fire({
1503 1503 title: 'Resolve TODO {0}'.format(todoId),
1504 1504 showCancelButton: true,
1505 1505 confirmButtonText: _gettext('Yes'),
1506 1506 showLoaderOnConfirm: true,
1507 1507
1508 1508 allowOutsideClick: function () {
1509 1509 !Swal.isLoading()
1510 1510 },
1511 1511 preConfirm: function () {
1512 1512 var comment = $('#comment-' + commentId);
1513 1513 var commentData = comment.data();
1514 1514
1515 1515 var f_path = null
1516 1516 var line_no = null
1517 1517 if (commentData.commentInline) {
1518 1518 f_path = commentData.commentFPath;
1519 1519 line_no = commentData.commentLineNo;
1520 1520 }
1521 1521
1522 1522 var renderer = templateContext.visual.default_renderer;
1523 1523 var commentBoxUrl = '{1}#comment-{0}'.format(commentId);
1524 1524
1525 1525 // Pull request case
1526 1526 if (templateContext.pull_request_data.pull_request_id !== null) {
1527 1527 var commentUrl = pyroutes.url('pullrequest_comment_create',
1528 1528 {
1529 1529 'repo_name': templateContext.repo_name,
1530 1530 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1531 1531 'comment_id': commentId
1532 1532 });
1533 1533 } else {
1534 1534 var commentUrl = pyroutes.url('repo_commit_comment_create',
1535 1535 {
1536 1536 'repo_name': templateContext.repo_name,
1537 1537 'commit_id': templateContext.commit_data.commit_id,
1538 1538 'comment_id': commentId
1539 1539 });
1540 1540 }
1541 1541
1542 1542 if (renderer === 'rst') {
1543 1543 commentBoxUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentUrl);
1544 1544 } else if (renderer === 'markdown') {
1545 1545 commentBoxUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentUrl);
1546 1546 }
1547 1547 var resolveText = _gettext('TODO from comment {0} was fixed.').format(commentBoxUrl);
1548 1548
1549 1549 var postData = {
1550 1550 text: resolveText,
1551 1551 comment_type: 'note',
1552 1552 draft: false,
1553 1553 csrf_token: CSRF_TOKEN,
1554 1554 resolves_comment_id: commentId
1555 1555 }
1556 1556 if (commentData.commentInline) {
1557 1557 postData['f_path'] = f_path;
1558 1558 postData['line'] = line_no;
1559 1559 }
1560 1560
1561 1561 return new Promise(function (resolve, reject) {
1562 1562 $.ajax({
1563 1563 type: 'POST',
1564 1564 data: postData,
1565 1565 url: commentUrl,
1566 1566 headers: {'X-PARTIAL-XHR': true}
1567 1567 })
1568 1568 .done(function (data) {
1569 1569 resolve(data);
1570 1570 })
1571 1571 .fail(function (jqXHR, textStatus, errorThrown) {
1572 1572 var prefix = "Error while resolving TODO.\n"
1573 1573 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1574 1574 ajaxErrorSwal(message);
1575 1575 });
1576 1576 })
1577 1577 }
1578 1578
1579 1579 })
1580 1580 .then(function (result) {
1581 1581 var success = function (json_data) {
1582 1582 resolvesCommentId = commentId;
1583 1583 var commentResolved = json_data[Object.keys(json_data)[0]]
1584 1584
1585 1585 try {
1586 1586
1587 1587 if (commentResolved.f_path) {
1588 1588 // inject newly created comments, json_data is {<comment_id>: {}}
1589 1589 self.attachInlineComment(json_data)
1590 1590 } else {
1591 1591 self.attachGeneralComment(json_data)
1592 1592 }
1593 1593
1594 1594 //mark visually which comment was resolved
1595 1595 if (resolvesCommentId) {
1596 1596 self.markCommentResolved(resolvesCommentId);
1597 1597 }
1598 1598
1599 1599 // run global callback on submit
1600 1600 if (window.commentFormGlobalSubmitSuccessCallback !== undefined) {
1601 1601 commentFormGlobalSubmitSuccessCallback({
1602 1602 draft: false,
1603 1603 comment_id: commentId
1604 1604 });
1605 1605 }
1606 1606
1607 1607 } catch (e) {
1608 1608 console.error(e);
1609 1609 }
1610 1610
1611 1611 if (window.updateSticky !== undefined) {
1612 1612 // potentially our comments change the active window size, so we
1613 1613 // notify sticky elements
1614 1614 updateSticky()
1615 1615 }
1616 1616
1617 1617 if (window.refreshAllComments !== undefined) {
1618 1618 // if we have this handler, run it, and refresh all comments boxes
1619 1619 refreshAllComments()
1620 1620 }
1621 1621 // re trigger the linkification of next/prev navigation
1622 1622 linkifyComments($('.inline-comment-injected'));
1623 1623 timeagoActivate();
1624 1624 tooltipActivate();
1625 1625 };
1626 1626
1627 1627 if (result.value) {
1628 1628 $(elem).remove();
1629 1629 success(result.value)
1630 1630 }
1631 1631 })
1632 1632 };
1633 1633
1634 1634 };
1635 1635
1636 1636 window.commentHelp = function(renderer) {
1637 1637 var funcData = {'renderer': renderer}
1638 1638 return renderTemplate('commentHelpHovercard', funcData)
1639 1639 }
@@ -1,278 +1,278 b''
1 1 <%text>
2 2 <div style="display: none">
3 3
4 4 <script>
5 5 var CG = new ColorGenerator();
6 6 </script>
7 7
8 8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
9 9
10 10 <%
11 11 if (size > 16) {
12 12 var gravatar_class = 'gravatar gravatar-large';
13 13 } else {
14 14 var gravatar_class = 'gravatar';
15 15 }
16 16
17 17 if (tooltip) {
18 18 var gravatar_class = gravatar_class + ' tooltip-hovercard';
19 19 }
20 20
21 21 var data_hovercard_alt = username;
22 22
23 23 %>
24 24
25 25 <%
26 26 if (show_disabled) {
27 27 var user_cls = 'user user-disabled';
28 28 } else {
29 29 var user_cls = 'user';
30 30 }
31 31 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
32 32 %>
33 33
34 34 <div class="rc-user">
35 35 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
36 36 <span class="<%= user_cls %>"> <%- user_link -%> </span>
37 37 </div>
38 38
39 39 </script>
40 40
41 41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
42 42 <%
43 43 if (create) {
44 44 var edit_visibility = 'visible';
45 45 } else {
46 46 var edit_visibility = 'hidden';
47 47 }
48 48
49 49 if (member.user_group && member.user_group.vote_rule) {
50 50 var reviewGroup = '<i class="icon-user-group"></i>';
51 51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
52 52 } else {
53 53 var reviewGroup = null;
54 54 var reviewGroupColor = 'transparent';
55 55 }
56 56 var rule_show = rule_show || false;
57 57
58 58 if (rule_show) {
59 59 var rule_visibility = 'table-cell';
60 60 } else {
61 61 var rule_visibility = 'none';
62 62 }
63 63
64 64 %>
65 65
66 66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67 67
68 68 <% if (create) { %>
69 69 <td style="width: 1px"></td>
70 70 <% } else { %>
71 71 <td style="width: 20px">
72 72 <div class="tooltip presence-state" style="display: none; position: absolute; left: 2px" title="This users is currently at this page">
73 73 <i class="icon-eye" style="color: #0ac878"></i>
74 74 </div>
75 75 <% if (role === 'reviewer') { %>
76 76 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
77 77 <i class="icon-circle review-status-<%= review_status %>"></i>
78 78 </div>
79 79 <% } else if (role === 'observer') { %>
80 80 <div class="tooltip" title="Observer without voting right.">
81 81 <i class="icon-circle-thin"></i>
82 82 </div>
83 83 <% } %>
84 84 </td>
85 85 <% } %>
86 86
87 87
88 88 <% if (mandatory) { %>
89 89 <td style="text-align: right;width: 10px;">
90 90 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
91 91 <i class="icon-lock"></i>
92 92 </div>
93 93 </td>
94 94
95 95 <% } else { %>
96 96 <td style="text-align: right;width: 10px;">
97 97 <% if (allowed_to_update) { %>
98 98 <div class="<%=role %>_member_remove" onclick="reviewersController.removeMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
99 99 <i class="icon-remove" style="color: #e85e4d;"></i>
100 100 </div>
101 101 <% } %>
102 102 </td>
103 103 <% } %>
104 104
105 105 <td>
106 106 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
107 107 <%-
108 108 renderTemplate('gravatarWithUser', {
109 109 'size': 16,
110 110 'show_disabled': false,
111 111 'tooltip': true,
112 112 'username': member.username,
113 113 'user_id': member.user_id,
114 114 'user_link': member.user_link,
115 115 'gravatar_url': member.gravatar_link
116 116 })
117 117 %>
118 118 </div>
119 119 <% if (reviewGroup !== null) { %>
120 120 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
121 121 <%- reviewGroup %>
122 122 </span>
123 123 <% } %>
124 124 </td>
125 125
126 126 </tr>
127 127
128 128 <tr id="reviewer_<%= member.user_id %>_rules">
129 129 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
130 130 <input type="hidden" name="__start__" value="reviewer:mapping">
131 131
132 132 <%if (member.user_group && member.user_group.vote_rule) { %>
133 133 <div class="reviewer_reason">
134 134
135 135 <%if (member.user_group.vote_rule == -1) {%>
136 136 - group votes required: ALL
137 137 <%} else {%>
138 138 - group votes required: <%= member.user_group.vote_rule %>
139 139 <%}%>
140 140 </div>
141 141 <%} %>
142 142
143 143 <input type="hidden" name="__start__" value="reasons:sequence">
144 144 <% for (var i = 0; i < reasons.length; i++) { %>
145 145 <% var reason = reasons[i] %>
146 146 <div class="reviewer_reason">- <%= reason %></div>
147 147 <input type="hidden" name="reason" value="<%= reason %>">
148 148 <% } %>
149 149 <input type="hidden" name="__end__" value="reasons:sequence">
150 150
151 151 <input type="hidden" name="__start__" value="rules:sequence">
152 152 <% for (var i = 0; i < member.rules.length; i++) { %>
153 153 <% var rule = member.rules[i] %>
154 154 <input type="hidden" name="rule_id" value="<%= rule %>">
155 155 <% } %>
156 156 <input type="hidden" name="__end__" value="rules:sequence">
157 157
158 158 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
159 159 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
160 160 <input type="hidden" name="role" value="<%= role %>"/>
161 161
162 162 <input type="hidden" name="__end__" value="reviewer:mapping">
163 163 </td>
164 164 </tr>
165 165
166 166 </script>
167 167
168 168 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
169 169
170 170 <%
171 171 if (size > 16) {
172 172 var gravatar_class = 'gravatar gravatar-large';
173 173 } else {
174 174 var gravatar_class = 'gravatar';
175 175 }
176 176
177 177 %>
178 178
179 179 <%
180 180 if (show_disabled) {
181 181 var user_cls = 'user user-disabled';
182 182 } else {
183 183 var user_cls = 'user';
184 184 }
185 185
186 186 %>
187 187
188 188 <div style='line-height: 20px'>
189 189 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
190 190 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
191 191 </div>
192 192
193 193 </script>
194 194
195 195
196 196 <script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
197 197
198 198 <div>
199 199
200 200 <% if (is_todo) { %>
201 201 <% if (inline) { %>
202 202 <strong>Inline</strong> TODO (<code>#<%- comment_id -%></code>) on line: <%= line_no %>
203 203 <% if (version_info) { %>
204 204 <%= version_info %>
205 205 <% } %>
206 206 <br/>
207 207 File: <code><%- file_name -%></code>
208 208 <% } else { %>
209 209 <% if (review_status) { %>
210 210 <i class="icon-circle review-status-<%= review_status %>"></i>
211 211 <% } %>
212 212 <strong>General</strong> TODO (<code>#<%- comment_id -%></code>)
213 213 <% if (version_info) { %>
214 214 <%= version_info %>
215 215 <% } %>
216 216 <% } %>
217 217 <% } else { %>
218 218 <% if (inline) { %>
219 219 <strong>Inline</strong> comment (<code>#<%- comment_id -%></code>) on line: <%= line_no %>
220 220 <% if (version_info) { %>
221 221 <%= version_info %>
222 222 <% } %>
223 223 <br/>
224 File: <code><%- file_name -%></code>
224 File: <code><%= file_name -%></code>
225 225 <% } else { %>
226 226 <% if (review_status) { %>
227 227 <i class="icon-circle review-status-<%= review_status %>"></i>
228 228 <% } %>
229 229 <strong>General</strong> comment (<code>#<%- comment_id -%></code>)
230 230 <% if (version_info) { %>
231 231 <%= version_info %>
232 232 <% } %>
233 233 <% } %>
234 234 <% } %>
235 235 <br/>
236 236 Created:
237 237 <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
238 238
239 239 <% if (is_todo) { %>
240 240 <div style="text-align: center; padding-top: 5px">
241 241 <a class="btn btn-sm" href="#resolveTodo<%- comment_id -%>" onclick="Rhodecode.comments.resolveTodo(this, '<%- comment_id -%>'); return false">
242 242 <strong>Resolve TODO</strong>
243 243 </a>
244 244 </div>
245 245 <% } %>
246 246
247 247 </div>
248 248
249 249 </script>
250 250
251 251 <script id="ejs_commentHelpHovercard" type="text/template" class="ejsTemplate">
252 252
253 253 <div>
254 254 Use <strong>@username</strong> mention syntax to send direct notification to this RhodeCode user.<br/>
255 255 Typing / starts autocomplete for certain action, e.g set review status, or comment type. <br/>
256 256 <br/>
257 257 Use <strong>Cmd/ctrl+enter</strong> to submit comment, or <strong>Shift+Cmd/ctrl+enter</strong> to submit a draft.<br/>
258 258 <br/>
259 259 <strong>Draft comments</strong> are private to the author, and trigger no notification to others.<br/>
260 260 They are permanent until deleted, or converted to regular comments.<br/>
261 261 <br/>
262 262 <br/>
263 263 </div>
264 264
265 265 </script>
266 266
267 267
268 268
269 269 ##// END OF EJS Templates
270 270 </div>
271 271
272 272
273 273 <script>
274 274 // registers the templates into global cache
275 275 registerTemplates();
276 276 </script>
277 277
278 278 </%text>
General Comments 0
You need to be logged in to leave comments. Login now