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