##// END OF EJS Templates
comments: refresh on draft submit.
milka -
r4683:f7898de5 stable
parent child Browse files
Show More
@@ -1,1639 +1,1644 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 if (window.refreshAllComments !== undefined) {
795 // if we have this handler, run it, and refresh all comments boxes
796 refreshAllComments()
797 }
798
794 799 return false;
795 800 };
796 801
797 802 ajaxPOST(url, postData, submitSuccessCallback)
798 803
799 804 }
800 805
801 806 this.finalizeDrafts = function(commentIds, callback) {
802 807
803 808 SwalNoAnimation.fire({
804 809 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
805 810 icon: 'warning',
806 811 showCancelButton: true,
807 812 confirmButtonText: _gettext('Yes'),
808 813
809 814 }).then(function(result) {
810 815 if (result.value) {
811 816 if (callback !== undefined) {
812 817 callback(result)
813 818 }
814 819 self._finalizeDrafts(commentIds);
815 820 }
816 821 })
817 822 };
818 823
819 824 this.toggleWideMode = function (node) {
820 825
821 826 if ($('#content').hasClass('wrapper')) {
822 827 $('#content').removeClass("wrapper");
823 828 $('#content').addClass("wide-mode-wrapper");
824 829 $(node).addClass('btn-success');
825 830 return true
826 831 } else {
827 832 $('#content').removeClass("wide-mode-wrapper");
828 833 $('#content').addClass("wrapper");
829 834 $(node).removeClass('btn-success');
830 835 return false
831 836 }
832 837
833 838 };
834 839
835 840 /**
836 841 * Turn off/on all comments in file diff
837 842 */
838 843 this.toggleDiffComments = function(node) {
839 844 // Find closes filediff container
840 845 var $filediff = $(node).closest('.filediff');
841 846 if ($(node).hasClass('toggle-on')) {
842 847 var show = false;
843 848 } else if ($(node).hasClass('toggle-off')) {
844 849 var show = true;
845 850 }
846 851
847 852 // Toggle each individual comment block, so we can un-toggle single ones
848 853 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
849 854 self.toggleLineComments($(val), show)
850 855 })
851 856
852 857 // since we change the height of the diff container that has anchor points for upper
853 858 // sticky header, we need to tell it to re-calculate those
854 859 if (window.updateSticky !== undefined) {
855 860 // potentially our comments change the active window size, so we
856 861 // notify sticky elements
857 862 updateSticky()
858 863 }
859 864
860 865 return false;
861 866 }
862 867
863 868 this.toggleLineComments = function(node, show) {
864 869
865 870 var trElem = $(node).closest('tr')
866 871
867 872 if (show === true) {
868 873 // mark outdated comments as visible before the toggle;
869 874 $(trElem).find('.comment-outdated').show();
870 875 $(trElem).removeClass('hide-line-comments');
871 876 } else if (show === false) {
872 877 $(trElem).find('.comment-outdated').hide();
873 878 $(trElem).addClass('hide-line-comments');
874 879 } else {
875 880 // mark outdated comments as visible before the toggle;
876 881 $(trElem).find('.comment-outdated').show();
877 882 $(trElem).toggleClass('hide-line-comments');
878 883 }
879 884
880 885 // since we change the height of the diff container that has anchor points for upper
881 886 // sticky header, we need to tell it to re-calculate those
882 887 if (window.updateSticky !== undefined) {
883 888 // potentially our comments change the active window size, so we
884 889 // notify sticky elements
885 890 updateSticky()
886 891 }
887 892
888 893 };
889 894
890 895 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
891 896 var pullRequestId = templateContext.pull_request_data.pull_request_id;
892 897 var commitId = templateContext.commit_data.commit_id;
893 898
894 899 var commentForm = new CommentForm(
895 900 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
896 901 var cm = commentForm.getCmInstance();
897 902
898 903 if (resolvesCommentId){
899 904 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
900 905 }
901 906
902 907 setTimeout(function() {
903 908 // callbacks
904 909 if (cm !== undefined) {
905 910 commentForm.setPlaceholder(placeholderText);
906 911 if (commentForm.isInline()) {
907 912 cm.focus();
908 913 cm.refresh();
909 914 }
910 915 }
911 916 }, 10);
912 917
913 918 // trigger scrolldown to the resolve comment, since it might be away
914 919 // from the clicked
915 920 if (resolvesCommentId){
916 921 var actionNode = $(commentForm.resolvesActionId).offset();
917 922
918 923 setTimeout(function() {
919 924 if (actionNode) {
920 925 $('body, html').animate({scrollTop: actionNode.top}, 10);
921 926 }
922 927 }, 100);
923 928 }
924 929
925 930 // add dropzone support
926 931 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
927 932 var renderer = templateContext.visual.default_renderer;
928 933 if (renderer == 'rst') {
929 934 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
930 935 if (isRendered){
931 936 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
932 937 }
933 938 } else if (renderer == 'markdown') {
934 939 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
935 940 if (isRendered){
936 941 attachmentUrl = '!' + attachmentUrl;
937 942 }
938 943 } else {
939 944 var attachmentUrl = '{}'.format(attachmentStoreUrl);
940 945 }
941 946 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
942 947
943 948 return false;
944 949 };
945 950
946 951 //see: https://www.dropzonejs.com/#configuration
947 952 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
948 953 {'repo_name': templateContext.repo_name,
949 954 'commit_id': templateContext.commit_data.commit_id})
950 955
951 956 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
952 957 if (previewTmpl !== undefined){
953 958 var selectLink = $(formElement).find('.pick-attachment').get(0);
954 959 $(formElement).find('.comment-attachment-uploader').dropzone({
955 960 url: storeUrl,
956 961 headers: {"X-CSRF-Token": CSRF_TOKEN},
957 962 paramName: function () {
958 963 return "attachment"
959 964 }, // The name that will be used to transfer the file
960 965 clickable: selectLink,
961 966 parallelUploads: 1,
962 967 maxFiles: 10,
963 968 maxFilesize: templateContext.attachment_store.max_file_size_mb,
964 969 uploadMultiple: false,
965 970 autoProcessQueue: true, // if false queue will not be processed automatically.
966 971 createImageThumbnails: false,
967 972 previewTemplate: previewTmpl.innerHTML,
968 973
969 974 accept: function (file, done) {
970 975 done();
971 976 },
972 977 init: function () {
973 978
974 979 this.on("sending", function (file, xhr, formData) {
975 980 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
976 981 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
977 982 });
978 983
979 984 this.on("success", function (file, response) {
980 985 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
981 986 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
982 987
983 988 var isRendered = false;
984 989 var ext = file.name.split('.').pop();
985 990 var imageExts = templateContext.attachment_store.image_ext;
986 991 if (imageExts.indexOf(ext) !== -1){
987 992 isRendered = true;
988 993 }
989 994
990 995 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
991 996 });
992 997
993 998 this.on("error", function (file, errorMessage, xhr) {
994 999 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
995 1000
996 1001 var error = null;
997 1002
998 1003 if (xhr !== undefined){
999 1004 var httpStatus = xhr.status + " " + xhr.statusText;
1000 1005 if (xhr !== undefined && xhr.status >= 500) {
1001 1006 error = httpStatus;
1002 1007 }
1003 1008 }
1004 1009
1005 1010 if (error === null) {
1006 1011 error = errorMessage.error || errorMessage || httpStatus;
1007 1012 }
1008 1013 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1009 1014
1010 1015 });
1011 1016 }
1012 1017 });
1013 1018 }
1014 1019 return commentForm;
1015 1020 };
1016 1021
1017 1022 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1018 1023
1019 1024 var tmpl = $('#cb-comment-general-form-template').html();
1020 1025 tmpl = tmpl.format(null, 'general');
1021 1026 var $form = $(tmpl);
1022 1027
1023 1028 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1024 1029 var curForm = $formPlaceholder.find('form');
1025 1030 if (curForm){
1026 1031 curForm.remove();
1027 1032 }
1028 1033 $formPlaceholder.append($form);
1029 1034
1030 1035 var _form = $($form[0]);
1031 1036 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1032 1037 var edit = false;
1033 1038 var comment_id = null;
1034 1039 var commentForm = this.createCommentForm(
1035 1040 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1036 1041 commentForm.initStatusChangeSelector();
1037 1042
1038 1043 return commentForm;
1039 1044 };
1040 1045
1041 1046 this.editComment = function(node, line_no, f_path) {
1042 1047 self.edit = true;
1043 1048 var $node = $(node);
1044 1049 var $td = $node.closest('td');
1045 1050
1046 1051 var $comment = $(node).closest('.comment');
1047 1052 var comment_id = $($comment).data('commentId');
1048 1053 var isDraft = $($comment).data('commentDraft');
1049 1054 var $editForm = null
1050 1055
1051 1056 var $comments = $node.closest('div.inline-comments');
1052 1057 var $general_comments = null;
1053 1058
1054 1059 if($comments.length){
1055 1060 // inline comments setup
1056 1061 $editForm = $comments.find('.comment-inline-form');
1057 1062 line_no = self.getLineNumber(node)
1058 1063 }
1059 1064 else{
1060 1065 // general comments setup
1061 1066 $comments = $('#comments');
1062 1067 $editForm = $comments.find('.comment-inline-form');
1063 1068 line_no = $comment[0].id
1064 1069 $('#cb-comment-general-form-placeholder').hide();
1065 1070 }
1066 1071
1067 1072 if ($editForm.length === 0) {
1068 1073
1069 1074 // unhide all comments if they are hidden for a proper REPLY mode
1070 1075 var $filediff = $node.closest('.filediff');
1071 1076 $filediff.removeClass('hide-comments');
1072 1077
1073 1078 $editForm = self.createNewFormWrapper(f_path, line_no);
1074 1079 if(f_path && line_no) {
1075 1080 $editForm.addClass('comment-inline-form-edit')
1076 1081 }
1077 1082
1078 1083 $comment.after($editForm)
1079 1084
1080 1085 var _form = $($editForm[0]).find('form');
1081 1086 var autocompleteActions = ['as_note',];
1082 1087 var commentForm = this.createCommentForm(
1083 1088 _form, line_no, '', autocompleteActions, resolvesCommentId,
1084 1089 this.edit, comment_id);
1085 1090 var old_comment_text_binary = $comment.attr('data-comment-text');
1086 1091 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1087 1092 commentForm.cm.setValue(old_comment_text);
1088 1093 $comment.hide();
1089 1094 tooltipActivate();
1090 1095
1091 1096 // set a CUSTOM submit handler for inline comment edit action.
1092 1097 commentForm.setHandleFormSubmit(function(o) {
1093 1098 var text = commentForm.cm.getValue();
1094 1099 var commentType = commentForm.getCommentType();
1095 1100
1096 1101 if (text === "") {
1097 1102 return;
1098 1103 }
1099 1104
1100 1105 if (old_comment_text == text) {
1101 1106 SwalNoAnimation.fire({
1102 1107 title: 'Unable to edit comment',
1103 1108 html: _gettext('Comment body was not changed.'),
1104 1109 });
1105 1110 return;
1106 1111 }
1107 1112 var excludeCancelBtn = false;
1108 1113 var submitEvent = true;
1109 1114 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1110 1115 commentForm.cm.setOption("readOnly", true);
1111 1116
1112 1117 // Read last version known
1113 1118 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1114 1119 var version = versionSelector.data('lastVersion');
1115 1120
1116 1121 if (!version) {
1117 1122 version = 0;
1118 1123 }
1119 1124
1120 1125 var postData = {
1121 1126 'text': text,
1122 1127 'f_path': f_path,
1123 1128 'line': line_no,
1124 1129 'comment_type': commentType,
1125 1130 'draft': isDraft,
1126 1131 'version': version,
1127 1132 'csrf_token': CSRF_TOKEN
1128 1133 };
1129 1134
1130 1135 var submitSuccessCallback = function(json_data) {
1131 1136 $editForm.remove();
1132 1137 $comment.show();
1133 1138 var postData = {
1134 1139 'text': text,
1135 1140 'renderer': $comment.attr('data-comment-renderer'),
1136 1141 'csrf_token': CSRF_TOKEN
1137 1142 };
1138 1143
1139 1144 /* Inject new edited version selector */
1140 1145 var updateCommentVersionDropDown = function () {
1141 1146 var versionSelectId = '#comment_versions_'+comment_id;
1142 1147 var preLoadVersionData = [
1143 1148 {
1144 1149 id: json_data['comment_version'],
1145 1150 text: "v{0}".format(json_data['comment_version']),
1146 1151 action: function () {
1147 1152 Rhodecode.comments.showVersion(
1148 1153 json_data['comment_id'],
1149 1154 json_data['comment_history_id']
1150 1155 )
1151 1156 },
1152 1157 comment_version: json_data['comment_version'],
1153 1158 comment_author_username: json_data['comment_author_username'],
1154 1159 comment_author_gravatar: json_data['comment_author_gravatar'],
1155 1160 comment_created_on: json_data['comment_created_on'],
1156 1161 },
1157 1162 ]
1158 1163
1159 1164
1160 1165 if ($(versionSelectId).data('select2')) {
1161 1166 var oldData = $(versionSelectId).data('select2').opts.data.results;
1162 1167 $(versionSelectId).select2("destroy");
1163 1168 preLoadVersionData = oldData.concat(preLoadVersionData)
1164 1169 }
1165 1170
1166 1171 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1167 1172
1168 1173 $comment.attr('data-comment-text', utf8ToB64(text));
1169 1174
1170 1175 var versionSelector = $('#comment_versions_'+comment_id);
1171 1176
1172 1177 // set lastVersion so we know our last edit version
1173 1178 versionSelector.data('lastVersion', json_data['comment_version'])
1174 1179 versionSelector.parent().show();
1175 1180 }
1176 1181 updateCommentVersionDropDown();
1177 1182
1178 1183 // by default we reset state of comment preserving the text
1179 1184 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1180 1185 var prefix = "Error while editing this comment.\n"
1181 1186 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1182 1187 ajaxErrorSwal(message);
1183 1188 };
1184 1189
1185 1190 var successRenderCommit = function(o){
1186 1191 $comment.show();
1187 1192 $comment[0].lastElementChild.innerHTML = o;
1188 1193 };
1189 1194
1190 1195 var previewUrl = pyroutes.url(
1191 1196 'repo_commit_comment_preview',
1192 1197 {'repo_name': templateContext.repo_name,
1193 1198 'commit_id': templateContext.commit_data.commit_id});
1194 1199
1195 1200 _submitAjaxPOST(
1196 1201 previewUrl, postData, successRenderCommit, failRenderCommit
1197 1202 );
1198 1203
1199 1204 try {
1200 1205 var html = json_data.rendered_text;
1201 1206 var lineno = json_data.line_no;
1202 1207 var target_id = json_data.target_id;
1203 1208
1204 1209 $comments.find('.cb-comment-add-button').before(html);
1205 1210
1206 1211 // run global callback on submit
1207 1212 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1208 1213
1209 1214 } catch (e) {
1210 1215 console.error(e);
1211 1216 }
1212 1217
1213 1218 // re trigger the linkification of next/prev navigation
1214 1219 linkifyComments($('.inline-comment-injected'));
1215 1220 timeagoActivate();
1216 1221 tooltipActivate();
1217 1222
1218 1223 if (window.updateSticky !== undefined) {
1219 1224 // potentially our comments change the active window size, so we
1220 1225 // notify sticky elements
1221 1226 updateSticky()
1222 1227 }
1223 1228
1224 1229 if (window.refreshAllComments !== undefined && !isDraft) {
1225 1230 // if we have this handler, run it, and refresh all comments boxes
1226 1231 refreshAllComments()
1227 1232 }
1228 1233 else if (window.refreshDraftComments !== undefined && isDraft) {
1229 1234 // if we have this handler, run it, and refresh all comments boxes
1230 1235 refreshDraftComments();
1231 1236 }
1232 1237
1233 1238 commentForm.setActionButtonsDisabled(false);
1234 1239
1235 1240 };
1236 1241
1237 1242 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1238 1243 var prefix = "Error while editing comment.\n"
1239 1244 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1240 1245 if (jqXHR.status == 409){
1241 1246 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1242 1247 ajaxErrorSwal(message, 'Comment version mismatch.');
1243 1248 } else {
1244 1249 ajaxErrorSwal(message);
1245 1250 }
1246 1251
1247 1252 commentForm.resetCommentFormState(text)
1248 1253 };
1249 1254 commentForm.submitAjaxPOST(
1250 1255 commentForm.submitUrl, postData,
1251 1256 submitSuccessCallback,
1252 1257 submitFailCallback);
1253 1258 });
1254 1259 }
1255 1260
1256 1261 $editForm.addClass('comment-inline-form-open');
1257 1262 };
1258 1263
1259 1264 this.attachComment = function(json_data) {
1260 1265 var self = this;
1261 1266 $.each(json_data, function(idx, val) {
1262 1267 var json_data_elem = [val]
1263 1268 var isInline = val.comment_f_path && val.comment_lineno
1264 1269
1265 1270 if (isInline) {
1266 1271 self.attachInlineComment(json_data_elem)
1267 1272 } else {
1268 1273 self.attachGeneralComment(json_data_elem)
1269 1274 }
1270 1275 })
1271 1276
1272 1277 }
1273 1278
1274 1279 this.attachGeneralComment = function(json_data) {
1275 1280 $.each(json_data, function(idx, val) {
1276 1281 $('#injected_page_comments').append(val.rendered_text);
1277 1282 })
1278 1283 }
1279 1284
1280 1285 this.attachInlineComment = function(json_data) {
1281 1286
1282 1287 $.each(json_data, function (idx, val) {
1283 1288 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1284 1289 var html = val.rendered_text;
1285 1290 var $inlineComments = $('#' + val.target_id)
1286 1291 .find(line_qry)
1287 1292 .find('.inline-comments');
1288 1293
1289 1294 var lastComment = $inlineComments.find('.comment-inline').last();
1290 1295
1291 1296 if (lastComment.length === 0) {
1292 1297 // first comment, we append simply
1293 1298 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1294 1299 } else {
1295 1300 $(lastComment).after(html)
1296 1301 }
1297 1302
1298 1303 })
1299 1304
1300 1305 };
1301 1306
1302 1307 this.createNewFormWrapper = function(f_path, line_no) {
1303 1308 // create a new reply HTML form from template
1304 1309 var tmpl = $('#cb-comment-inline-form-template').html();
1305 1310 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1306 1311 return $(tmpl);
1307 1312 }
1308 1313
1309 1314 this.markCommentResolved = function(commentId) {
1310 1315 $('#comment-label-{0}'.format(commentId)).find('.resolved').show();
1311 1316 $('#comment-label-{0}'.format(commentId)).find('.resolve').hide();
1312 1317 };
1313 1318
1314 1319 this.createComment = function(node, f_path, line_no, resolutionComment) {
1315 1320 self.edit = false;
1316 1321 var $node = $(node);
1317 1322 var $td = $node.closest('td');
1318 1323 var resolvesCommentId = resolutionComment || null;
1319 1324
1320 1325 var $replyForm = $td.find('.comment-inline-form');
1321 1326
1322 1327 // if form isn't existing, we're generating a new one and injecting it.
1323 1328 if ($replyForm.length === 0) {
1324 1329
1325 1330 // unhide/expand all comments if they are hidden for a proper REPLY mode
1326 1331 self.toggleLineComments($node, true);
1327 1332
1328 1333 $replyForm = self.createNewFormWrapper(f_path, line_no);
1329 1334
1330 1335 var $comments = $td.find('.inline-comments');
1331 1336
1332 1337 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1333 1338 if ($comments.length===0) {
1334 1339 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 1340 var $reply_container = $('#cb-comments-inline-container-template')
1336 1341 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1337 1342 $td.append($($reply_container).html());
1338 1343 }
1339 1344
1340 1345 // default comment button exists, so we prepend the form for leaving initial comment
1341 1346 $td.find('.cb-comment-add-button').before($replyForm);
1342 1347 // set marker, that we have a open form
1343 1348 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1344 1349 $replyWrapper.addClass('comment-form-active');
1345 1350
1346 1351 var lastComment = $comments.find('.comment-inline').last();
1347 1352 if ($(lastComment).hasClass('comment-outdated')) {
1348 1353 $replyWrapper.show();
1349 1354 }
1350 1355
1351 1356 var _form = $($replyForm[0]).find('form');
1352 1357 var autocompleteActions = ['as_note', 'as_todo'];
1353 1358 var comment_id=null;
1354 1359 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1355 1360 var commentForm = self.createCommentForm(
1356 1361 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1357 1362 self.edit, comment_id);
1358 1363
1359 1364 // set a CUSTOM submit handler for inline comments.
1360 1365 commentForm.setHandleFormSubmit(function(o) {
1361 1366 var text = commentForm.cm.getValue();
1362 1367 var commentType = commentForm.getCommentType();
1363 1368 var resolvesCommentId = commentForm.getResolvesId();
1364 1369 var isDraft = commentForm.getDraftState();
1365 1370
1366 1371 if (text === "") {
1367 1372 return;
1368 1373 }
1369 1374
1370 1375 if (line_no === undefined) {
1371 1376 alert('Error: unable to fetch line number for this inline comment !');
1372 1377 return;
1373 1378 }
1374 1379
1375 1380 if (f_path === undefined) {
1376 1381 alert('Error: unable to fetch file path for this inline comment !');
1377 1382 return;
1378 1383 }
1379 1384
1380 1385 var excludeCancelBtn = false;
1381 1386 var submitEvent = true;
1382 1387 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1383 1388 commentForm.cm.setOption("readOnly", true);
1384 1389 var postData = {
1385 1390 'text': text,
1386 1391 'f_path': f_path,
1387 1392 'line': line_no,
1388 1393 'comment_type': commentType,
1389 1394 'draft': isDraft,
1390 1395 'csrf_token': CSRF_TOKEN
1391 1396 };
1392 1397 if (resolvesCommentId){
1393 1398 postData['resolves_comment_id'] = resolvesCommentId;
1394 1399 }
1395 1400
1396 1401 // submitSuccess for inline commits
1397 1402 var submitSuccessCallback = function(json_data) {
1398 1403
1399 1404 $replyForm.remove();
1400 1405 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1401 1406
1402 1407 try {
1403 1408
1404 1409 // inject newly created comments, json_data is {<comment_id>: {}}
1405 1410 self.attachInlineComment(json_data)
1406 1411
1407 1412 //mark visually which comment was resolved
1408 1413 if (resolvesCommentId) {
1409 1414 self.markCommentResolved(resolvesCommentId);
1410 1415 }
1411 1416
1412 1417 // run global callback on submit
1413 1418 commentForm.globalSubmitSuccessCallback({
1414 1419 draft: isDraft,
1415 1420 comment_id: comment_id
1416 1421 });
1417 1422
1418 1423 } catch (e) {
1419 1424 console.error(e);
1420 1425 }
1421 1426
1422 1427 if (window.updateSticky !== undefined) {
1423 1428 // potentially our comments change the active window size, so we
1424 1429 // notify sticky elements
1425 1430 updateSticky()
1426 1431 }
1427 1432
1428 1433 if (window.refreshAllComments !== undefined && !isDraft) {
1429 1434 // if we have this handler, run it, and refresh all comments boxes
1430 1435 refreshAllComments()
1431 1436 }
1432 1437 else if (window.refreshDraftComments !== undefined && isDraft) {
1433 1438 // if we have this handler, run it, and refresh all comments boxes
1434 1439 refreshDraftComments();
1435 1440 }
1436 1441
1437 1442 commentForm.setActionButtonsDisabled(false);
1438 1443
1439 1444 // re trigger the linkification of next/prev navigation
1440 1445 linkifyComments($('.inline-comment-injected'));
1441 1446 timeagoActivate();
1442 1447 tooltipActivate();
1443 1448 };
1444 1449
1445 1450 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1446 1451 var prefix = "Error while submitting comment.\n"
1447 1452 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1448 1453 ajaxErrorSwal(message);
1449 1454 commentForm.resetCommentFormState(text)
1450 1455 };
1451 1456
1452 1457 commentForm.submitAjaxPOST(
1453 1458 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1454 1459 });
1455 1460 }
1456 1461
1457 1462 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1458 1463 $replyForm.addClass('comment-inline-form-open');
1459 1464 tooltipActivate();
1460 1465 };
1461 1466
1462 1467 this.createResolutionComment = function(commentId){
1463 1468 // hide the trigger text
1464 1469 $('#resolve-comment-{0}'.format(commentId)).hide();
1465 1470
1466 1471 var comment = $('#comment-'+commentId);
1467 1472 var commentData = comment.data();
1468 1473
1469 1474 if (commentData.commentInline) {
1470 1475 var f_path = commentData.commentFPath;
1471 1476 var line_no = commentData.commentLineNo;
1472 1477 this.createComment(comment, f_path, line_no, commentId)
1473 1478 } else {
1474 1479 this.createGeneralComment('general', "$placeholder", commentId)
1475 1480 }
1476 1481
1477 1482 return false;
1478 1483 };
1479 1484
1480 1485 this.submitResolution = function(commentId){
1481 1486 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1482 1487 var commentForm = form.get(0).CommentForm;
1483 1488
1484 1489 var cm = commentForm.getCmInstance();
1485 1490 var renderer = templateContext.visual.default_renderer;
1486 1491 if (renderer == 'rst'){
1487 1492 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1488 1493 } else if (renderer == 'markdown') {
1489 1494 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1490 1495 } else {
1491 1496 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1492 1497 }
1493 1498
1494 1499 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1495 1500 form.submit();
1496 1501 return false;
1497 1502 };
1498 1503
1499 1504 this.resolveTodo = function (elem, todoId) {
1500 1505 var commentId = todoId;
1501 1506
1502 1507 SwalNoAnimation.fire({
1503 1508 title: 'Resolve TODO {0}'.format(todoId),
1504 1509 showCancelButton: true,
1505 1510 confirmButtonText: _gettext('Yes'),
1506 1511 showLoaderOnConfirm: true,
1507 1512
1508 1513 allowOutsideClick: function () {
1509 1514 !Swal.isLoading()
1510 1515 },
1511 1516 preConfirm: function () {
1512 1517 var comment = $('#comment-' + commentId);
1513 1518 var commentData = comment.data();
1514 1519
1515 1520 var f_path = null
1516 1521 var line_no = null
1517 1522 if (commentData.commentInline) {
1518 1523 f_path = commentData.commentFPath;
1519 1524 line_no = commentData.commentLineNo;
1520 1525 }
1521 1526
1522 1527 var renderer = templateContext.visual.default_renderer;
1523 1528 var commentBoxUrl = '{1}#comment-{0}'.format(commentId);
1524 1529
1525 1530 // Pull request case
1526 1531 if (templateContext.pull_request_data.pull_request_id !== null) {
1527 1532 var commentUrl = pyroutes.url('pullrequest_comment_create',
1528 1533 {
1529 1534 'repo_name': templateContext.repo_name,
1530 1535 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1531 1536 'comment_id': commentId
1532 1537 });
1533 1538 } else {
1534 1539 var commentUrl = pyroutes.url('repo_commit_comment_create',
1535 1540 {
1536 1541 'repo_name': templateContext.repo_name,
1537 1542 'commit_id': templateContext.commit_data.commit_id,
1538 1543 'comment_id': commentId
1539 1544 });
1540 1545 }
1541 1546
1542 1547 if (renderer === 'rst') {
1543 1548 commentBoxUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentUrl);
1544 1549 } else if (renderer === 'markdown') {
1545 1550 commentBoxUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentUrl);
1546 1551 }
1547 1552 var resolveText = _gettext('TODO from comment {0} was fixed.').format(commentBoxUrl);
1548 1553
1549 1554 var postData = {
1550 1555 text: resolveText,
1551 1556 comment_type: 'note',
1552 1557 draft: false,
1553 1558 csrf_token: CSRF_TOKEN,
1554 1559 resolves_comment_id: commentId
1555 1560 }
1556 1561 if (commentData.commentInline) {
1557 1562 postData['f_path'] = f_path;
1558 1563 postData['line'] = line_no;
1559 1564 }
1560 1565
1561 1566 return new Promise(function (resolve, reject) {
1562 1567 $.ajax({
1563 1568 type: 'POST',
1564 1569 data: postData,
1565 1570 url: commentUrl,
1566 1571 headers: {'X-PARTIAL-XHR': true}
1567 1572 })
1568 1573 .done(function (data) {
1569 1574 resolve(data);
1570 1575 })
1571 1576 .fail(function (jqXHR, textStatus, errorThrown) {
1572 1577 var prefix = "Error while resolving TODO.\n"
1573 1578 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1574 1579 ajaxErrorSwal(message);
1575 1580 });
1576 1581 })
1577 1582 }
1578 1583
1579 1584 })
1580 1585 .then(function (result) {
1581 1586 var success = function (json_data) {
1582 1587 resolvesCommentId = commentId;
1583 1588 var commentResolved = json_data[Object.keys(json_data)[0]]
1584 1589
1585 1590 try {
1586 1591
1587 1592 if (commentResolved.f_path) {
1588 1593 // inject newly created comments, json_data is {<comment_id>: {}}
1589 1594 self.attachInlineComment(json_data)
1590 1595 } else {
1591 1596 self.attachGeneralComment(json_data)
1592 1597 }
1593 1598
1594 1599 //mark visually which comment was resolved
1595 1600 if (resolvesCommentId) {
1596 1601 self.markCommentResolved(resolvesCommentId);
1597 1602 }
1598 1603
1599 1604 // run global callback on submit
1600 1605 if (window.commentFormGlobalSubmitSuccessCallback !== undefined) {
1601 1606 commentFormGlobalSubmitSuccessCallback({
1602 1607 draft: false,
1603 1608 comment_id: commentId
1604 1609 });
1605 1610 }
1606 1611
1607 1612 } catch (e) {
1608 1613 console.error(e);
1609 1614 }
1610 1615
1611 1616 if (window.updateSticky !== undefined) {
1612 1617 // potentially our comments change the active window size, so we
1613 1618 // notify sticky elements
1614 1619 updateSticky()
1615 1620 }
1616 1621
1617 1622 if (window.refreshAllComments !== undefined) {
1618 1623 // if we have this handler, run it, and refresh all comments boxes
1619 1624 refreshAllComments()
1620 1625 }
1621 1626 // re trigger the linkification of next/prev navigation
1622 1627 linkifyComments($('.inline-comment-injected'));
1623 1628 timeagoActivate();
1624 1629 tooltipActivate();
1625 1630 };
1626 1631
1627 1632 if (result.value) {
1628 1633 $(elem).remove();
1629 1634 success(result.value)
1630 1635 }
1631 1636 })
1632 1637 };
1633 1638
1634 1639 };
1635 1640
1636 1641 window.commentHelp = function(renderer) {
1637 1642 var funcData = {'renderer': renderer}
1638 1643 return renderTemplate('commentHelpHovercard', funcData)
1639 1644 }
General Comments 1
Under Review
author

Auto status change to "Under Review"

You need to be logged in to leave comments. Login now