##// END OF EJS Templates
comments: added better alerts for failed commenting operations.
marcink -
r4311:a06710aa default
parent child Browse files
Show More
@@ -1,931 +1,932 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) {
84 84 if (!(this instanceof CommentForm)) {
85 85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
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 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 127 this.submitButtonText = this.submitButton.val();
128 128
129 129 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
130 130 {'repo_name': templateContext.repo_name,
131 131 'commit_id': templateContext.commit_data.commit_id});
132 132
133 133 if (resolvesCommentId){
134 134 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
135 135 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
136 136 $(this.commentType).prop('disabled', true);
137 137 $(this.commentType).addClass('disabled');
138 138
139 139 // disable select
140 140 setTimeout(function() {
141 141 $(self.statusChange).select2('readonly', true);
142 142 }, 10);
143 143
144 144 var resolvedInfo = (
145 145 '<li class="resolve-action">' +
146 146 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
147 147 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
148 148 '</li>'
149 149 ).format(resolvesCommentId, _gettext('resolve comment'));
150 150 $(resolvedInfo).insertAfter($(this.commentType).parent());
151 151 }
152 152
153 153 // based on commitId, or pullRequestId decide where do we submit
154 154 // out data
155 155 if (this.commitId){
156 156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
157 157 {'repo_name': templateContext.repo_name,
158 158 'commit_id': this.commitId});
159 159 this.selfUrl = pyroutes.url('repo_commit',
160 160 {'repo_name': templateContext.repo_name,
161 161 'commit_id': this.commitId});
162 162
163 163 } else if (this.pullRequestId) {
164 164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
165 165 {'repo_name': templateContext.repo_name,
166 166 'pull_request_id': this.pullRequestId});
167 167 this.selfUrl = pyroutes.url('pullrequest_show',
168 168 {'repo_name': templateContext.repo_name,
169 169 'pull_request_id': this.pullRequestId});
170 170
171 171 } else {
172 172 throw new Error(
173 173 'CommentForm requires pullRequestId, or commitId to be specified.')
174 174 }
175 175
176 176 // FUNCTIONS and helpers
177 177 var self = this;
178 178
179 179 this.isInline = function(){
180 180 return this.lineNo && this.lineNo != 'general';
181 181 };
182 182
183 183 this.getCmInstance = function(){
184 184 return this.cm
185 185 };
186 186
187 187 this.setPlaceholder = function(placeholder) {
188 188 var cm = this.getCmInstance();
189 189 if (cm){
190 190 cm.setOption('placeholder', placeholder);
191 191 }
192 192 };
193 193
194 194 this.getCommentStatus = function() {
195 195 return $(this.submitForm).find(this.statusChange).val();
196 196 };
197 197 this.getCommentType = function() {
198 198 return $(this.submitForm).find(this.commentType).val();
199 199 };
200 200
201 201 this.getResolvesId = function() {
202 202 return $(this.submitForm).find(this.resolvesId).val() || null;
203 203 };
204 204
205 205 this.getClosePr = function() {
206 206 return $(this.submitForm).find(this.closesPr).val() || null;
207 207 };
208 208
209 209 this.markCommentResolved = function(resolvedCommentId){
210 210 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
211 211 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
212 212 };
213 213
214 214 this.isAllowedToSubmit = function() {
215 215 return !$(this.submitButton).prop('disabled');
216 216 };
217 217
218 218 this.initStatusChangeSelector = function(){
219 219 var formatChangeStatus = function(state, escapeMarkup) {
220 220 var originalOption = state.element;
221 221 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
222 222 return tmpl
223 223 };
224 224 var formatResult = function(result, container, query, escapeMarkup) {
225 225 return formatChangeStatus(result, escapeMarkup);
226 226 };
227 227
228 228 var formatSelection = function(data, container, escapeMarkup) {
229 229 return formatChangeStatus(data, escapeMarkup);
230 230 };
231 231
232 232 $(this.submitForm).find(this.statusChange).select2({
233 233 placeholder: _gettext('Status Review'),
234 234 formatResult: formatResult,
235 235 formatSelection: formatSelection,
236 236 containerCssClass: "drop-menu status_box_menu",
237 237 dropdownCssClass: "drop-menu-dropdown",
238 238 dropdownAutoWidth: true,
239 239 minimumResultsForSearch: -1
240 240 });
241 241 $(this.submitForm).find(this.statusChange).on('change', function() {
242 242 var status = self.getCommentStatus();
243 243
244 244 if (status && !self.isInline()) {
245 245 $(self.submitButton).prop('disabled', false);
246 246 }
247 247
248 248 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
249 249 self.setPlaceholder(placeholderText)
250 250 })
251 251 };
252 252
253 253 // reset the comment form into it's original state
254 254 this.resetCommentFormState = function(content) {
255 255 content = content || '';
256 256
257 257 $(this.editContainer).show();
258 258 $(this.editButton).parent().addClass('active');
259 259
260 260 $(this.previewContainer).hide();
261 261 $(this.previewButton).parent().removeClass('active');
262 262
263 263 this.setActionButtonsDisabled(true);
264 264 self.cm.setValue(content);
265 265 self.cm.setOption("readOnly", false);
266 266
267 267 if (this.resolvesId) {
268 268 // destroy the resolve action
269 269 $(this.resolvesId).parent().remove();
270 270 }
271 271 // reset closingPR flag
272 272 $('.close-pr-input').remove();
273 273
274 274 $(this.statusChange).select2('readonly', false);
275 275 };
276 276
277 277 this.globalSubmitSuccessCallback = function(){
278 278 // default behaviour is to call GLOBAL hook, if it's registered.
279 279 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
280 280 commentFormGlobalSubmitSuccessCallback()
281 281 }
282 282 };
283 283
284 284 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
285 285 return _submitAjaxPOST(url, postData, successHandler, failHandler);
286 286 };
287 287
288 288 // overwrite a submitHandler, we need to do it for inline comments
289 289 this.setHandleFormSubmit = function(callback) {
290 290 this.handleFormSubmit = callback;
291 291 };
292 292
293 293 // overwrite a submitSuccessHandler
294 294 this.setGlobalSubmitSuccessCallback = function(callback) {
295 295 this.globalSubmitSuccessCallback = callback;
296 296 };
297 297
298 298 // default handler for for submit for main comments
299 299 this.handleFormSubmit = function() {
300 300 var text = self.cm.getValue();
301 301 var status = self.getCommentStatus();
302 302 var commentType = self.getCommentType();
303 303 var resolvesCommentId = self.getResolvesId();
304 304 var closePullRequest = self.getClosePr();
305 305
306 306 if (text === "" && !status) {
307 307 return;
308 308 }
309 309
310 310 var excludeCancelBtn = false;
311 311 var submitEvent = true;
312 312 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
313 313 self.cm.setOption("readOnly", true);
314 314
315 315 var postData = {
316 316 'text': text,
317 317 'changeset_status': status,
318 318 'comment_type': commentType,
319 319 'csrf_token': CSRF_TOKEN
320 320 };
321 321
322 322 if (resolvesCommentId) {
323 323 postData['resolves_comment_id'] = resolvesCommentId;
324 324 }
325 325
326 326 if (closePullRequest) {
327 327 postData['close_pull_request'] = true;
328 328 }
329 329
330 330 var submitSuccessCallback = function(o) {
331 331 // reload page if we change status for single commit.
332 332 if (status && self.commitId) {
333 333 location.reload(true);
334 334 } else {
335 335 $('#injected_page_comments').append(o.rendered_text);
336 336 self.resetCommentFormState();
337 337 timeagoActivate();
338 338 tooltipActivate();
339 339
340 340 // mark visually which comment was resolved
341 341 if (resolvesCommentId) {
342 342 self.markCommentResolved(resolvesCommentId);
343 343 }
344 344 }
345 345
346 346 // run global callback on submit
347 347 self.globalSubmitSuccessCallback();
348 348
349 349 };
350 var submitFailCallback = function(data) {
351 alert(
352 "Error while submitting comment.\n" +
353 "Error code {0} ({1}).".format(data.status, data.statusText)
354 );
350 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
351 var prefix = "Error while submitting comment.\n"
352 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
353 ajaxErrorSwal(message);
355 354 self.resetCommentFormState(text);
356 355 };
357 356 self.submitAjaxPOST(
358 357 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
359 358 };
360 359
361 360 this.previewSuccessCallback = function(o) {
362 361 $(self.previewBoxSelector).html(o);
363 362 $(self.previewBoxSelector).removeClass('unloaded');
364 363
365 364 // swap buttons, making preview active
366 365 $(self.previewButton).parent().addClass('active');
367 366 $(self.editButton).parent().removeClass('active');
368 367
369 368 // unlock buttons
370 369 self.setActionButtonsDisabled(false);
371 370 };
372 371
373 372 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
374 373 excludeCancelBtn = excludeCancelBtn || false;
375 374 submitEvent = submitEvent || false;
376 375
377 376 $(this.editButton).prop('disabled', state);
378 377 $(this.previewButton).prop('disabled', state);
379 378
380 379 if (!excludeCancelBtn) {
381 380 $(this.cancelButton).prop('disabled', state);
382 381 }
383 382
384 383 var submitState = state;
385 384 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
386 385 // if the value of commit review status is set, we allow
387 386 // submit button, but only on Main form, isInline means inline
388 387 submitState = false
389 388 }
390 389
391 390 $(this.submitButton).prop('disabled', submitState);
392 391 if (submitEvent) {
393 392 $(this.submitButton).val(_gettext('Submitting...'));
394 393 } else {
395 394 $(this.submitButton).val(this.submitButtonText);
396 395 }
397 396
398 397 };
399 398
400 399 // lock preview/edit/submit buttons on load, but exclude cancel button
401 400 var excludeCancelBtn = true;
402 401 this.setActionButtonsDisabled(true, excludeCancelBtn);
403 402
404 403 // anonymous users don't have access to initialized CM instance
405 404 if (this.cm !== undefined){
406 405 this.cm.on('change', function(cMirror) {
407 406 if (cMirror.getValue() === "") {
408 407 self.setActionButtonsDisabled(true, excludeCancelBtn)
409 408 } else {
410 409 self.setActionButtonsDisabled(false, excludeCancelBtn)
411 410 }
412 411 });
413 412 }
414 413
415 414 $(this.editButton).on('click', function(e) {
416 415 e.preventDefault();
417 416
418 417 $(self.previewButton).parent().removeClass('active');
419 418 $(self.previewContainer).hide();
420 419
421 420 $(self.editButton).parent().addClass('active');
422 421 $(self.editContainer).show();
423 422
424 423 });
425 424
426 425 $(this.previewButton).on('click', function(e) {
427 426 e.preventDefault();
428 427 var text = self.cm.getValue();
429 428
430 429 if (text === "") {
431 430 return;
432 431 }
433 432
434 433 var postData = {
435 434 'text': text,
436 435 'renderer': templateContext.visual.default_renderer,
437 436 'csrf_token': CSRF_TOKEN
438 437 };
439 438
440 439 // lock ALL buttons on preview
441 440 self.setActionButtonsDisabled(true);
442 441
443 442 $(self.previewBoxSelector).addClass('unloaded');
444 443 $(self.previewBoxSelector).html(_gettext('Loading ...'));
445 444
446 445 $(self.editContainer).hide();
447 446 $(self.previewContainer).show();
448 447
449 448 // by default we reset state of comment preserving the text
450 var previewFailCallback = function(data){
451 alert(
452 "Error while preview of comment.\n" +
453 "Error code {0} ({1}).".format(data.status, data.statusText)
454 );
449 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
450 var prefix = "Error while preview of comment.\n"
451 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
452 ajaxErrorSwal(message);
453
455 454 self.resetCommentFormState(text)
456 455 };
457 456 self.submitAjaxPOST(
458 457 self.previewUrl, postData, self.previewSuccessCallback,
459 458 previewFailCallback);
460 459
461 460 $(self.previewButton).parent().addClass('active');
462 461 $(self.editButton).parent().removeClass('active');
463 462 });
464 463
465 464 $(this.submitForm).submit(function(e) {
466 465 e.preventDefault();
467 466 var allowedToSubmit = self.isAllowedToSubmit();
468 467 if (!allowedToSubmit){
469 468 return false;
470 469 }
471 470 self.handleFormSubmit();
472 471 });
473 472
474 473 }
475 474
476 475 return CommentForm;
477 476 });
478 477
479 478 /* comments controller */
480 479 var CommentsController = function() {
481 480 var mainComment = '#text';
482 481 var self = this;
483 482
484 483 this.cancelComment = function(node) {
485 484 var $node = $(node);
486 485 var $td = $node.closest('td');
487 486 $node.closest('.comment-inline-form').remove();
488 487 return false;
489 488 };
490 489
491 490 this.getLineNumber = function(node) {
492 491 var $node = $(node);
493 492 var lineNo = $node.closest('td').attr('data-line-no');
494 493 if (lineNo === undefined && $node.data('commentInline')){
495 494 lineNo = $node.data('commentLineNo')
496 495 }
497 496
498 497 return lineNo
499 498 };
500 499
501 500 this.scrollToComment = function(node, offset, outdated) {
502 501 if (offset === undefined) {
503 502 offset = 0;
504 503 }
505 504 var outdated = outdated || false;
506 505 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
507 506
508 507 if (!node) {
509 508 node = $('.comment-selected');
510 509 if (!node.length) {
511 510 node = $('comment-current')
512 511 }
513 512 }
514 513
515 514 $wrapper = $(node).closest('div.comment');
516 515
517 516 // show hidden comment when referenced.
518 517 if (!$wrapper.is(':visible')){
519 518 $wrapper.show();
520 519 }
521 520
522 521 $comment = $(node).closest(klass);
523 522 $comments = $(klass);
524 523
525 524 $('.comment-selected').removeClass('comment-selected');
526 525
527 526 var nextIdx = $(klass).index($comment) + offset;
528 527 if (nextIdx >= $comments.length) {
529 528 nextIdx = 0;
530 529 }
531 530 var $next = $(klass).eq(nextIdx);
532 531
533 532 var $cb = $next.closest('.cb');
534 533 $cb.removeClass('cb-collapsed');
535 534
536 535 var $filediffCollapseState = $cb.closest('.filediff').prev();
537 536 $filediffCollapseState.prop('checked', false);
538 537 $next.addClass('comment-selected');
539 538 scrollToElement($next);
540 539 return false;
541 540 };
542 541
543 542 this.nextComment = function(node) {
544 543 return self.scrollToComment(node, 1);
545 544 };
546 545
547 546 this.prevComment = function(node) {
548 547 return self.scrollToComment(node, -1);
549 548 };
550 549
551 550 this.nextOutdatedComment = function(node) {
552 551 return self.scrollToComment(node, 1, true);
553 552 };
554 553
555 554 this.prevOutdatedComment = function(node) {
556 555 return self.scrollToComment(node, -1, true);
557 556 };
558 557
559 558 this.deleteComment = function(node) {
560 559 if (!confirm(_gettext('Delete this comment?'))) {
561 560 return false;
562 561 }
563 562 var $node = $(node);
564 563 var $td = $node.closest('td');
565 564 var $comment = $node.closest('.comment');
566 565 var comment_id = $comment.attr('data-comment-id');
567 566 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
568 567 var postData = {
569 568 'csrf_token': CSRF_TOKEN
570 569 };
571 570
572 571 $comment.addClass('comment-deleting');
573 572 $comment.hide('fast');
574 573
575 574 var success = function(response) {
576 575 $comment.remove();
577 576 return false;
578 577 };
579 var failure = function(data, textStatus, xhr) {
580 alert("error processing request: " + textStatus);
578 var failure = function(jqXHR, textStatus, errorThrown) {
579 var prefix = "Error while deleting this comment.\n"
580 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
581 ajaxErrorSwal(message);
582
581 583 $comment.show('fast');
582 584 $comment.removeClass('comment-deleting');
583 585 return false;
584 586 };
585 587 ajaxPOST(url, postData, success, failure);
586 588 };
587 589
588 590 this.toggleWideMode = function (node) {
589 591 if ($('#content').hasClass('wrapper')) {
590 592 $('#content').removeClass("wrapper");
591 593 $('#content').addClass("wide-mode-wrapper");
592 594 $(node).addClass('btn-success');
593 595 return true
594 596 } else {
595 597 $('#content').removeClass("wide-mode-wrapper");
596 598 $('#content').addClass("wrapper");
597 599 $(node).removeClass('btn-success');
598 600 return false
599 601 }
600 602
601 603 };
602 604
603 605 this.toggleComments = function(node, show) {
604 606 var $filediff = $(node).closest('.filediff');
605 607 if (show === true) {
606 608 $filediff.removeClass('hide-comments');
607 609 } else if (show === false) {
608 610 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
609 611 $filediff.addClass('hide-comments');
610 612 } else {
611 613 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
612 614 $filediff.toggleClass('hide-comments');
613 615 }
614 616 return false;
615 617 };
616 618
617 619 this.toggleLineComments = function(node) {
618 620 self.toggleComments(node, true);
619 621 var $node = $(node);
620 622 // mark outdated comments as visible before the toggle;
621 623 $(node.closest('tr')).find('.comment-outdated').show();
622 624 $node.closest('tr').toggleClass('hide-line-comments');
623 625 };
624 626
625 627 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
626 628 var pullRequestId = templateContext.pull_request_data.pull_request_id;
627 629 var commitId = templateContext.commit_data.commit_id;
628 630
629 631 var commentForm = new CommentForm(
630 632 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
631 633 var cm = commentForm.getCmInstance();
632 634
633 635 if (resolvesCommentId){
634 636 var placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
635 637 }
636 638
637 639 setTimeout(function() {
638 640 // callbacks
639 641 if (cm !== undefined) {
640 642 commentForm.setPlaceholder(placeholderText);
641 643 if (commentForm.isInline()) {
642 644 cm.focus();
643 645 cm.refresh();
644 646 }
645 647 }
646 648 }, 10);
647 649
648 650 // trigger scrolldown to the resolve comment, since it might be away
649 651 // from the clicked
650 652 if (resolvesCommentId){
651 653 var actionNode = $(commentForm.resolvesActionId).offset();
652 654
653 655 setTimeout(function() {
654 656 if (actionNode) {
655 657 $('body, html').animate({scrollTop: actionNode.top}, 10);
656 658 }
657 659 }, 100);
658 660 }
659 661
660 662 // add dropzone support
661 663 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
662 664 var renderer = templateContext.visual.default_renderer;
663 665 if (renderer == 'rst') {
664 666 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
665 667 if (isRendered){
666 668 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
667 669 }
668 670 } else if (renderer == 'markdown') {
669 671 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
670 672 if (isRendered){
671 673 attachmentUrl = '!' + attachmentUrl;
672 674 }
673 675 } else {
674 676 var attachmentUrl = '{}'.format(attachmentStoreUrl);
675 677 }
676 678 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
677 679
678 680 return false;
679 681 };
680 682
681 683 //see: https://www.dropzonejs.com/#configuration
682 684 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
683 685 {'repo_name': templateContext.repo_name,
684 686 'commit_id': templateContext.commit_data.commit_id})
685 687
686 688 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
687 689 if (previewTmpl !== undefined){
688 690 var selectLink = $(formElement).find('.pick-attachment').get(0);
689 691 $(formElement).find('.comment-attachment-uploader').dropzone({
690 692 url: storeUrl,
691 693 headers: {"X-CSRF-Token": CSRF_TOKEN},
692 694 paramName: function () {
693 695 return "attachment"
694 696 }, // The name that will be used to transfer the file
695 697 clickable: selectLink,
696 698 parallelUploads: 1,
697 699 maxFiles: 10,
698 700 maxFilesize: templateContext.attachment_store.max_file_size_mb,
699 701 uploadMultiple: false,
700 702 autoProcessQueue: true, // if false queue will not be processed automatically.
701 703 createImageThumbnails: false,
702 704 previewTemplate: previewTmpl.innerHTML,
703 705
704 706 accept: function (file, done) {
705 707 done();
706 708 },
707 709 init: function () {
708 710
709 711 this.on("sending", function (file, xhr, formData) {
710 712 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
711 713 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
712 714 });
713 715
714 716 this.on("success", function (file, response) {
715 717 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
716 718 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
717 719
718 720 var isRendered = false;
719 721 var ext = file.name.split('.').pop();
720 722 var imageExts = templateContext.attachment_store.image_ext;
721 723 if (imageExts.indexOf(ext) !== -1){
722 724 isRendered = true;
723 725 }
724 726
725 727 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
726 728 });
727 729
728 730 this.on("error", function (file, errorMessage, xhr) {
729 731 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
730 732
731 733 var error = null;
732 734
733 735 if (xhr !== undefined){
734 736 var httpStatus = xhr.status + " " + xhr.statusText;
735 737 if (xhr !== undefined && xhr.status >= 500) {
736 738 error = httpStatus;
737 739 }
738 740 }
739 741
740 742 if (error === null) {
741 743 error = errorMessage.error || errorMessage || httpStatus;
742 744 }
743 745 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
744 746
745 747 });
746 748 }
747 749 });
748 750 }
749 751 return commentForm;
750 752 };
751 753
752 754 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
753 755
754 756 var tmpl = $('#cb-comment-general-form-template').html();
755 757 tmpl = tmpl.format(null, 'general');
756 758 var $form = $(tmpl);
757 759
758 760 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
759 761 var curForm = $formPlaceholder.find('form');
760 762 if (curForm){
761 763 curForm.remove();
762 764 }
763 765 $formPlaceholder.append($form);
764 766
765 767 var _form = $($form[0]);
766 768 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
767 769 var commentForm = this.createCommentForm(
768 770 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
769 771 commentForm.initStatusChangeSelector();
770 772
771 773 return commentForm;
772 774 };
773 775
774 776 this.createComment = function(node, resolutionComment) {
775 777 var resolvesCommentId = resolutionComment || null;
776 778 var $node = $(node);
777 779 var $td = $node.closest('td');
778 780 var $form = $td.find('.comment-inline-form');
779 781
780 782 if (!$form.length) {
781 783
782 784 var $filediff = $node.closest('.filediff');
783 785 $filediff.removeClass('hide-comments');
784 786 var f_path = $filediff.attr('data-f-path');
785 787 var lineno = self.getLineNumber(node);
786 788 // create a new HTML from template
787 789 var tmpl = $('#cb-comment-inline-form-template').html();
788 790 tmpl = tmpl.format(escapeHtml(f_path), lineno);
789 791 $form = $(tmpl);
790 792
791 793 var $comments = $td.find('.inline-comments');
792 794 if (!$comments.length) {
793 795 $comments = $(
794 796 $('#cb-comments-inline-container-template').html());
795 797 $td.append($comments);
796 798 }
797 799
798 800 $td.find('.cb-comment-add-button').before($form);
799 801
800 802 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
801 803 var _form = $($form[0]).find('form');
802 804 var autocompleteActions = ['as_note', 'as_todo'];
803 805 var commentForm = this.createCommentForm(
804 806 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
805 807
806 808 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
807 809 form: _form,
808 810 parent: $td[0],
809 811 lineno: lineno,
810 812 f_path: f_path}
811 813 );
812 814
813 815 // set a CUSTOM submit handler for inline comments.
814 816 commentForm.setHandleFormSubmit(function(o) {
815 817 var text = commentForm.cm.getValue();
816 818 var commentType = commentForm.getCommentType();
817 819 var resolvesCommentId = commentForm.getResolvesId();
818 820
819 821 if (text === "") {
820 822 return;
821 823 }
822 824
823 825 if (lineno === undefined) {
824 826 alert('missing line !');
825 827 return;
826 828 }
827 829 if (f_path === undefined) {
828 830 alert('missing file path !');
829 831 return;
830 832 }
831 833
832 834 var excludeCancelBtn = false;
833 835 var submitEvent = true;
834 836 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
835 837 commentForm.cm.setOption("readOnly", true);
836 838 var postData = {
837 839 'text': text,
838 840 'f_path': f_path,
839 841 'line': lineno,
840 842 'comment_type': commentType,
841 843 'csrf_token': CSRF_TOKEN
842 844 };
843 845 if (resolvesCommentId){
844 846 postData['resolves_comment_id'] = resolvesCommentId;
845 847 }
846 848
847 849 var submitSuccessCallback = function(json_data) {
848 850 $form.remove();
849 851 try {
850 852 var html = json_data.rendered_text;
851 853 var lineno = json_data.line_no;
852 854 var target_id = json_data.target_id;
853 855
854 856 $comments.find('.cb-comment-add-button').before(html);
855 857
856 858 //mark visually which comment was resolved
857 859 if (resolvesCommentId) {
858 860 commentForm.markCommentResolved(resolvesCommentId);
859 861 }
860 862
861 863 // run global callback on submit
862 864 commentForm.globalSubmitSuccessCallback();
863 865
864 866 } catch (e) {
865 867 console.error(e);
866 868 }
867 869
868 870 // re trigger the linkification of next/prev navigation
869 871 linkifyComments($('.inline-comment-injected'));
870 872 timeagoActivate();
871 873 tooltipActivate();
872 874
873 875 if (window.updateSticky !== undefined) {
874 876 // potentially our comments change the active window size, so we
875 877 // notify sticky elements
876 878 updateSticky()
877 879 }
878 880
879 881 commentForm.setActionButtonsDisabled(false);
880 882
881 883 };
882 var submitFailCallback = function(data){
883 alert(
884 "Error while submitting comment.\n" +
885 "Error code {0} ({1}).".format(data.status, data.statusText)
886 );
884 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
885 var prefix = "Error while submitting comment.\n"
886 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
887 ajaxErrorSwal(message);
887 888 commentForm.resetCommentFormState(text)
888 889 };
889 890 commentForm.submitAjaxPOST(
890 891 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
891 892 });
892 893 }
893 894
894 895 $form.addClass('comment-inline-form-open');
895 896 };
896 897
897 898 this.createResolutionComment = function(commentId){
898 899 // hide the trigger text
899 900 $('#resolve-comment-{0}'.format(commentId)).hide();
900 901
901 902 var comment = $('#comment-'+commentId);
902 903 var commentData = comment.data();
903 904 if (commentData.commentInline) {
904 905 this.createComment(comment, commentId)
905 906 } else {
906 907 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
907 908 }
908 909
909 910 return false;
910 911 };
911 912
912 913 this.submitResolution = function(commentId){
913 914 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
914 915 var commentForm = form.get(0).CommentForm;
915 916
916 917 var cm = commentForm.getCmInstance();
917 918 var renderer = templateContext.visual.default_renderer;
918 919 if (renderer == 'rst'){
919 920 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
920 921 } else if (renderer == 'markdown') {
921 922 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
922 923 } else {
923 924 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
924 925 }
925 926
926 927 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
927 928 form.submit();
928 929 return false;
929 930 };
930 931
931 932 };
General Comments 0
You need to be logged in to leave comments. Login now