##// END OF EJS Templates
quick-switcher: handle errors using Swal, and create a new helper to make calls the same across whole app.
marcink -
r4339:de17b181 default
parent child Browse files
Show More
@@ -1,948 +1,950 b''
1 1 /**
2 2 * Ajax Autocomplete for jQuery, version dev
3 3 * RhodeCode additions
4 4 * (c) 2014 Tomas Kirda
5 5 * (c) 2014 Marcin Kuzminski
6 6 *
7 7 * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
8 8 * For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete
9 9 */
10 10 // Expose plugin as an AMD module if AMD loader is present:
11 11 (function (factory) {
12 12 'use strict';
13 13 if (typeof define === 'function' && define.amd) {
14 14 // AMD. Register as an anonymous module.
15 15 define(['jquery'], factory);
16 16 } else if (typeof exports === 'object' && typeof require === 'function') {
17 17 // Browserify
18 18 factory(require('jquery'));
19 19 } else {
20 20 // Browser globals
21 21 factory(jQuery);
22 22 }
23 23 }(function ($) {
24 24 'use strict';
25 25
26 26 var
27 27 utils = (function () {
28 28 return {
29 29 escapeRegExChars: function (value) {
30 30 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
31 31 },
32 32 createNode: function (containerClass) {
33 33 var div = document.createElement('div');
34 34 div.className = containerClass;
35 35 div.style.position = 'absolute';
36 36 div.style.display = 'none';
37 37 return div;
38 38 }
39 39 };
40 40 }()),
41 41
42 42 keys = {
43 43 ESC: 27,
44 44 TAB: 9,
45 45 RETURN: 13,
46 46 LEFT: 37,
47 47 UP: 38,
48 48 RIGHT: 39,
49 49 DOWN: 40
50 50 };
51 51
52 52 function Autocomplete(el, options) {
53 53 var noop = function () { },
54 54 that = this,
55 55 defaults = {
56 56 ajaxSettings: {},
57 57 autoSelectFirst: false,
58 58 appendTo: document.body,
59 59 serviceUrl: null,
60 60 lookup: null,
61 61 width: 'auto',
62 62 minChars: 1,
63 63 maxHeight: 300,
64 64 deferRequestBy: 0,
65 65 params: {},
66 66 formatResult: Autocomplete.formatResult,
67 67 lookupFilter: Autocomplete.lookupFilter,
68 68 delimiter: null,
69 69 zIndex: 9999,
70 70 type: 'GET',
71 71 noCache: false,
72 72 onSelect: noop,
73 73 onSearchStart: noop,
74 74 onSearchComplete: noop,
75 75 onSearchError: noop,
76 76 containerClass: 'autocomplete-suggestions',
77 77 tabDisabled: false,
78 78 dataType: 'text',
79 79 currentRequest: null,
80 80 triggerSelectOnValidInput: false,
81 81 preventBadQueries: true,
82 82 paramName: 'query',
83 83 transformResult: function (response) {
84 84 return typeof response === 'string' ? $.parseJSON(response) : response;
85 85 },
86 86 showNoSuggestionNotice: false,
87 87 noSuggestionNotice: _gettext('No results'),
88 88 orientation: 'bottom',
89 89 forceFixPosition: false,
90 90 replaceOnArrowKey: true
91 91 };
92 92
93 93 // Shared variables:
94 94 that.element = el;
95 95 that.el = $(el);
96 96 that.suggestions = [];
97 97 that.badQueries = [];
98 98 that.selectedIndex = -1;
99 99 that.currentValue = that.element.value;
100 100 that.intervalId = 0;
101 101 that.cachedResponse = {};
102 102 that.onChangeInterval = null;
103 103 that.onChange = null;
104 104 that.isLocal = false;
105 105 that.suggestionsContainer = null;
106 106 that.noSuggestionsContainer = null;
107 107 that.options = $.extend({}, defaults, options);
108 108 that.classes = {
109 109 selected: 'autocomplete-selected',
110 110 suggestion: 'autocomplete-suggestion'
111 111 };
112 112 that.hint = null;
113 113 that.hintValue = '';
114 114 that.selection = null;
115 115
116 116 // Initialize and set options:
117 117 that.initialize();
118 118 that.setOptions(options);
119 119 }
120 120
121 121 Autocomplete.utils = utils;
122 122
123 123 $.Autocomplete = Autocomplete;
124 124
125 125 Autocomplete.formatResult = function (suggestion, currentValue) {
126 126 var pattern = '(' + utils.escapeRegExChars(currentValue) + ')';
127 127 return suggestion.value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
128 128 };
129 129 Autocomplete.lookupFilter = function (suggestion, originalQuery, queryLowerCase) {
130 130 return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1;
131 131 };
132 132
133 133 Autocomplete.prototype = {
134 134
135 135 killerFn: null,
136 136
137 137 initialize: function () {
138 138 var that = this,
139 139 suggestionSelector = '.' + that.classes.suggestion,
140 140 selected = that.classes.selected,
141 141 options = that.options,
142 142 container;
143 143
144 144 // Remove autocomplete attribute to prevent native suggestions:
145 145 that.element.setAttribute('autocomplete', 'off');
146 146
147 147 that.killerFn = function (e) {
148 148 if ($(e.target).closest('.' + that.options.containerClass).length === 0) {
149 149 that.killSuggestions();
150 150 that.disableKillerFn();
151 151 }
152 152 };
153 153
154 154 // html() deals with many types: htmlString or Element or Array or jQuery
155 155 that.noSuggestionsContainer = $('<div class="autocomplete-no-suggestion"></div>')
156 156 .html(this.options.noSuggestionNotice).get(0);
157 157
158 158 that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass);
159 159
160 160 container = $(that.suggestionsContainer);
161 161
162 162 container.appendTo(options.appendTo);
163 163
164 164 // Only set width if it was provided:
165 165 if (options.width !== 'auto') {
166 166 container.width(options.width);
167 167 }
168 168
169 169 // Listen for mouse over event on suggestions list:
170 170 container.on('mouseover.autocomplete', suggestionSelector, function () {
171 171 that.activate($(this).data('index'));
172 172 });
173 173
174 174 // Deselect active element when mouse leaves suggestions container:
175 175 container.on('mouseout.autocomplete', function () {
176 176 that.selectedIndex = -1;
177 177 container.children('.' + selected).removeClass(selected);
178 178 });
179 179
180 180 // Listen for click event on suggestions list:
181 181 container.on('click.autocomplete', suggestionSelector, function () {
182 182 that.select($(this).data('index'));
183 183 });
184 184
185 185 that.fixPositionCapture = function () {
186 186 if (that.visible) {
187 187 that.fixPosition();
188 188 }
189 189 };
190 190
191 191 $(window).on('resize.autocomplete', that.fixPositionCapture);
192 192
193 193 that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); });
194 194 that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); });
195 195 that.el.on('blur.autocomplete', function () { that.onBlur(); });
196 196 that.el.on('focus.autocomplete', function () { that.onFocus(); });
197 197 that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); });
198 198 },
199 199
200 200 onFocus: function () {
201 201 var that = this;
202 202 that.fixPosition();
203 203 if (that.options.minChars <= that.el.val().length) {
204 204 that.onValueChange();
205 205 }
206 206 },
207 207
208 208 onBlur: function () {
209 209 this.enableKillerFn();
210 210 },
211 211
212 212 setOptions: function (suppliedOptions) {
213 213 var that = this,
214 214 options = that.options;
215 215
216 216 $.extend(options, suppliedOptions);
217 217
218 218 that.isLocal = $.isArray(options.lookup);
219 219
220 220 if (that.isLocal) {
221 221 options.lookup = that.verifySuggestionsFormat(options.lookup);
222 222 }
223 223
224 224 options.orientation = that.validateOrientation(options.orientation, 'bottom');
225 225
226 226 // Adjust height, width and z-index:
227 227 $(that.suggestionsContainer).css({
228 228 'max-height': options.maxHeight + 'px',
229 229 'width': options.width + 'px',
230 230 'z-index': options.zIndex
231 231 });
232 232 },
233 233
234 234 clearCache: function () {
235 235 this.cachedResponse = {};
236 236 this.badQueries = [];
237 237 },
238 238
239 239 clear: function () {
240 240 this.clearCache();
241 241 this.currentValue = '';
242 242 this.suggestions = [];
243 243 },
244 244
245 245 disable: function () {
246 246 var that = this;
247 247 that.disabled = true;
248 248 if (that.currentRequest) {
249 249 that.currentRequest.abort();
250 250 }
251 251 },
252 252
253 253 enable: function () {
254 254 this.disabled = false;
255 255 },
256 256
257 257 fixPosition: function () {
258 258 // Use only when container has already its content
259 259
260 260 var that = this,
261 261 $container = $(that.suggestionsContainer),
262 262 containerParent = $container.parent().get(0);
263 263 // Fix position automatically when appended to body.
264 264 // In other cases force parameter must be given.
265 265 if (containerParent !== document.body && !that.options.forceFixPosition)
266 266 return;
267 267
268 268 // Choose orientation
269 269 var orientation = that.options.orientation,
270 270 containerHeight = $container.outerHeight(),
271 271 height = that.el.outerHeight(),
272 272 offset = that.el.offset(),
273 273 styles = { 'top': offset.top, 'left': offset.left };
274 274
275 275 if (orientation == 'auto') {
276 276 var viewPortHeight = $(window).height(),
277 277 scrollTop = $(window).scrollTop(),
278 278 topOverflow = -scrollTop + offset.top - containerHeight,
279 279 bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight);
280 280
281 281 orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow)
282 282 ? 'top'
283 283 : 'bottom';
284 284 }
285 285
286 286 if (orientation === 'top') {
287 287 styles.top += -containerHeight;
288 288 } else {
289 289 styles.top += height;
290 290 }
291 291
292 292 // If container is not positioned to body,
293 293 // correct its position using offset parent offset
294 294 if(containerParent !== document.body) {
295 295 var opacity = $container.css('opacity'),
296 296 parentOffsetDiff;
297 297
298 298 if (!that.visible){
299 299 $container.css('opacity', 0).show();
300 300 }
301 301
302 302 parentOffsetDiff = $container.offsetParent().offset();
303 303 styles.top -= parentOffsetDiff.top;
304 304 styles.left -= parentOffsetDiff.left;
305 305
306 306 if (!that.visible){
307 307 $container.css('opacity', opacity).hide();
308 308 }
309 309 }
310 310
311 311 // -2px to account for suggestions border.
312 312 if (that.options.width === 'auto') {
313 313 styles.width = (that.el.outerWidth() - 2) + 'px';
314 314 }
315 315
316 316 $container.css(styles);
317 317 },
318 318
319 319 enableKillerFn: function () {
320 320 var that = this;
321 321 $(document).on('click.autocomplete', that.killerFn);
322 322 },
323 323
324 324 disableKillerFn: function () {
325 325 var that = this;
326 326 $(document).off('click.autocomplete', that.killerFn);
327 327 },
328 328
329 329 killSuggestions: function () {
330 330 var that = this;
331 331 that.stopKillSuggestions();
332 332 that.intervalId = window.setInterval(function () {
333 333 that.hide();
334 334 that.stopKillSuggestions();
335 335 }, 50);
336 336 },
337 337
338 338 stopKillSuggestions: function () {
339 339 window.clearInterval(this.intervalId);
340 340 },
341 341
342 342 isCursorAtEnd: function () {
343 343 var that = this,
344 344 valLength = that.el.val().length,
345 345 selectionStart = that.element.selectionStart,
346 346 range;
347 347
348 348 if (typeof selectionStart === 'number') {
349 349 return selectionStart === valLength;
350 350 }
351 351 if (document.selection) {
352 352 range = document.selection.createRange();
353 353 range.moveStart('character', -valLength);
354 354 return valLength === range.text.length;
355 355 }
356 356 return true;
357 357 },
358 358
359 359 onKeyPress: function (e) {
360 360 var that = this;
361 361
362 362 // If suggestions are hidden and user presses arrow down, display suggestions:
363 363 if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) {
364 364 that.suggest();
365 365 return;
366 366 }
367 367
368 368 if (that.disabled || !that.visible) {
369 369 return;
370 370 }
371 371
372 372 switch (e.which) {
373 373 case keys.ESC:
374 374 that.el.val(that.currentValue);
375 375 that.hide();
376 376 break;
377 377 case keys.RIGHT:
378 378 if (that.hint && that.options.onHint && that.isCursorAtEnd()) {
379 379 that.selectHint();
380 380 break;
381 381 }
382 382 return;
383 383 case keys.TAB:
384 384 if (that.hint && that.options.onHint) {
385 385 that.selectHint();
386 386 return;
387 387 }
388 388 // Fall through to RETURN
389 389 case keys.RETURN:
390 390 if (that.selectedIndex === -1) {
391 391 that.hide();
392 392 return;
393 393 }
394 394 that.select(that.selectedIndex);
395 395 if (e.which === keys.TAB && that.options.tabDisabled === false) {
396 396 return;
397 397 }
398 398 break;
399 399 case keys.UP:
400 400 that.moveUp();
401 401 break;
402 402 case keys.DOWN:
403 403 that.moveDown();
404 404 break;
405 405 default:
406 406 return;
407 407 }
408 408
409 409 // Cancel event if function did not return:
410 410 e.stopImmediatePropagation();
411 411 e.preventDefault();
412 412 },
413 413
414 414 onKeyUp: function (e) {
415 415 var that = this;
416 416
417 417 if (that.disabled) {
418 418 return;
419 419 }
420 420
421 421 switch (e.which) {
422 422 case keys.UP:
423 423 case keys.DOWN:
424 424 return;
425 425 }
426 426
427 427 clearInterval(that.onChangeInterval);
428 428
429 429 if (that.currentValue !== that.el.val()) {
430 430 that.findBestHint();
431 431 if (that.options.deferRequestBy > 0) {
432 432 // Defer lookup in case when value changes very quickly:
433 433 that.onChangeInterval = setInterval(function () {
434 434 that.onValueChange();
435 435 }, that.options.deferRequestBy);
436 436 } else {
437 437 that.onValueChange();
438 438 }
439 439 }
440 440 },
441 441
442 442 onValueChange: function () {
443 443 var that = this,
444 444 options = that.options,
445 445 value = that.el.val(),
446 446 query = that.getQuery(value),
447 447 index;
448 448
449 449 if (that.selection && that.currentValue !== query) {
450 450 that.selection = null;
451 451 (options.onInvalidateSelection || $.noop).call(that.element);
452 452 }
453 453
454 454 clearInterval(that.onChangeInterval);
455 455 that.currentValue = value;
456 456 that.selectedIndex = -1;
457 457
458 458 // Check existing suggestion for the match before proceeding:
459 459 if (options.triggerSelectOnValidInput) {
460 460 index = that.findSuggestionIndex(query);
461 461 if (index !== -1) {
462 462 that.select(index);
463 463 return;
464 464 }
465 465 }
466 466
467 467 if (query.length < options.minChars) {
468 468 that.hide();
469 469 } else {
470 470 that.getSuggestions(query);
471 471 }
472 472 },
473 473
474 474 findSuggestionIndex: function (query) {
475 475 var that = this,
476 476 index = -1,
477 477 queryLowerCase = query.toLowerCase();
478 478
479 479 $.each(that.suggestions, function (i, suggestion) {
480 480 if (suggestion.value.toLowerCase() === queryLowerCase) {
481 481 index = i;
482 482 return false;
483 483 }
484 484 });
485 485
486 486 return index;
487 487 },
488 488
489 489 getQuery: function (value) {
490 490 var delimiter = this.options.delimiter,
491 491 parts;
492 492
493 493 if (!delimiter) {
494 494 return value;
495 495 }
496 496 parts = value.split(delimiter);
497 497 return $.trim(parts[parts.length - 1]);
498 498 },
499 499
500 500 getSuggestionsLocal: function (query) {
501 501 var that = this,
502 502 options = that.options,
503 503 queryLowerCase = query.toLowerCase(),
504 504 data;
505 505
506 506 // re-pack the data as it was comming from AJAX
507 507 data = {
508 508 suggestions: data
509 509 };
510 510 return data;
511 511 },
512 512
513 513 getSuggestions: function (query) {
514 514 var response,
515 515 that = this,
516 516 options = that.options,
517 517 serviceUrl = options.serviceUrl,
518 518 params,
519 519 cacheKey,
520 520 ajaxSettings;
521 521
522 522 options.params[options.paramName] = query;
523 523 params = options.ignoreParams ? null : options.params;
524 524
525 525 if (that.isLocal) {
526 526 response = that.getSuggestionsLocal(query);
527 527 } else {
528 528 if ($.isFunction(serviceUrl)) {
529 529 serviceUrl = serviceUrl.call(that.element, query);
530 530 }
531 531
532 532 var callParams = {};
533 533 //make an evaluated copy of params
534 534 $.each(params, function(index, value) {
535 535 if($.isFunction(value)){
536 536 callParams[index] = value();
537 537 }
538 538 else {
539 539 callParams[index] = value;
540 540 }
541 541 });
542 542
543 543 cacheKey = serviceUrl + '?' + $.param(callParams);
544 544 response = that.cachedResponse[cacheKey];
545 545 }
546 546
547 547 if (response && $.isArray(response.suggestions)) {
548 548 that.suggestions = response.suggestions;
549 549 that.suggest();
550 550 } else if (!that.isBadQuery(query)) {
551 551 if (options.onSearchStart.call(that.element, params) === false) {
552 552 return;
553 553 }
554 554 if (that.currentRequest) {
555 555 that.currentRequest.abort();
556 556 }
557 557
558 558 ajaxSettings = {
559 559 url: serviceUrl,
560 560 data: params,
561 561 type: options.type,
562 562 dataType: options.dataType
563 563 };
564 564
565 565 $.extend(ajaxSettings, options.ajaxSettings);
566 566
567 that.currentRequest = $.ajax(ajaxSettings).done(function (data) {
567 that.currentRequest = $.ajax(ajaxSettings)
568 .done(function (data) {
568 569 var result;
569 570 that.currentRequest = null;
570 571 result = options.transformResult(data);
571 572 that.processResponse(result, query, cacheKey);
572 573 options.onSearchComplete.call(that.element, query, result.suggestions);
573 }).fail(function (jqXHR, textStatus, errorThrown) {
574 })
575 .fail(function (jqXHR, textStatus, errorThrown) {
574 576 options.onSearchError.call(that.element, query, jqXHR, textStatus, errorThrown);
575 577 });
576 578 }
577 579 },
578 580
579 581 isBadQuery: function (q) {
580 582 if (!this.options.preventBadQueries){
581 583 return false;
582 584 }
583 585
584 586 var badQueries = this.badQueries,
585 587 i = badQueries.length;
586 588
587 589 while (i--) {
588 590 if (q.indexOf(badQueries[i]) === 0) {
589 591 return true;
590 592 }
591 593 }
592 594
593 595 return false;
594 596 },
595 597
596 598 hide: function () {
597 599 var that = this;
598 600 that.visible = false;
599 601 that.selectedIndex = -1;
600 602 $(that.suggestionsContainer).hide();
601 603 that.signalHint(null);
602 604 },
603 605
604 606 suggest: function () {
605 607
606 608 var that = this,
607 609 options = that.options,
608 610 formatResult = options.formatResult,
609 611 filterResult = options.lookupFilter,
610 612 value = that.getQuery(that.currentValue),
611 613 className = that.classes.suggestion,
612 614 classSelected = that.classes.selected,
613 615 container = $(that.suggestionsContainer),
614 616 noSuggestionsContainer = $(that.noSuggestionsContainer),
615 617 beforeRender = options.beforeRender,
616 618 limit = parseInt(that.options.lookupLimit, 10),
617 619 html = '',
618 620 index;
619 621
620 622 // filter and limit given results
621 623 var filtered_suggestions = $.grep(that.suggestions, function (suggestion) {
622 624 return filterResult(suggestion, value, value.toLowerCase(), that.element);
623 625 });
624 626
625 627 if (limit && filtered_suggestions.length > limit) {
626 628 filtered_suggestions = filtered_suggestions.slice(0, limit);
627 629 }
628 630
629 631 if (filtered_suggestions.length === 0) {
630 632 this.options.showNoSuggestionNotice ? this.noSuggestions() : this.hide();
631 633 return;
632 634 }
633 635
634 636 if (options.triggerSelectOnValidInput) {
635 637 index = that.findSuggestionIndex(value);
636 638 if (index !== -1) {
637 639 that.select(index);
638 640 return;
639 641 }
640 642 }
641 643
642 644 // Build suggestions inner HTML:
643 645 $.each(filtered_suggestions, function (i, suggestion) {
644 646 html += '<div class="' + className + '" data-index="' + i + '">' + formatResult(suggestion, value, Autocomplete.formatResult, that.element) + '</div>';
645 647 });
646 648 // set internal suggestion for INDEX pick to work correctly
647 649 that.suggestions = filtered_suggestions;
648 650 this.adjustContainerWidth();
649 651
650 652 noSuggestionsContainer.detach();
651 653 container.html(html);
652 654
653 655 // Select first value by default:
654 656 if (options.autoSelectFirst) {
655 657 that.selectedIndex = 0;
656 658 container.children().first().addClass(classSelected);
657 659 }
658 660
659 661 if ($.isFunction(beforeRender)) {
660 662 beforeRender.call(that.element, container);
661 663 }
662 664
663 665 that.fixPosition();
664 666
665 667 container.show();
666 668 that.visible = true;
667 669
668 670 that.findBestHint();
669 671 },
670 672
671 673 noSuggestions: function() {
672 674 var that = this,
673 675 container = $(that.suggestionsContainer),
674 676 noSuggestionsContainer = $(that.noSuggestionsContainer);
675 677
676 678 this.adjustContainerWidth();
677 679
678 680 // Some explicit steps. Be careful here as it easy to get
679 681 // noSuggestionsContainer removed from DOM if not detached properly.
680 682 noSuggestionsContainer.detach();
681 683 container.empty(); // clean suggestions if any
682 684 container.append(noSuggestionsContainer);
683 685
684 686 that.fixPosition();
685 687
686 688 container.show();
687 689 that.visible = true;
688 690 },
689 691
690 692 adjustContainerWidth: function() {
691 693 var that = this,
692 694 options = that.options,
693 695 width,
694 696 container = $(that.suggestionsContainer);
695 697
696 698 // If width is auto, adjust width before displaying suggestions,
697 699 // because if instance was created before input had width, it will be zero.
698 700 // Also it adjusts if input width has changed.
699 701 // -2px to account for suggestions border.
700 702 if (options.width === 'auto') {
701 703 width = that.el.outerWidth() - 2;
702 704 container.width(width > 0 ? width : 300);
703 705 }
704 706 },
705 707
706 708 findBestHint: function () {
707 709 var that = this,
708 710 value = that.el.val().toLowerCase(),
709 711 bestMatch = null;
710 712
711 713 if (!value) {
712 714 return;
713 715 }
714 716
715 717 $.each(that.suggestions, function (i, suggestion) {
716 718 var foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0;
717 719 if (foundMatch) {
718 720 bestMatch = suggestion;
719 721 }
720 722 return !foundMatch;
721 723 });
722 724 that.signalHint(bestMatch);
723 725 },
724 726
725 727 signalHint: function (suggestion) {
726 728 var hintValue = '',
727 729 that = this;
728 730 if (suggestion) {
729 731 hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length);
730 732 }
731 733 if (that.hintValue !== hintValue) {
732 734 that.hintValue = hintValue;
733 735 that.hint = suggestion;
734 736 (this.options.onHint || $.noop)(hintValue);
735 737 }
736 738 },
737 739
738 740 verifySuggestionsFormat: function (suggestions) {
739 741 // If suggestions is string array, convert them to supported format:
740 742 if (suggestions.length && typeof suggestions[0] === 'string') {
741 743 return $.map(suggestions, function (value) {
742 744 return { value: value, data: null };
743 745 });
744 746 }
745 747
746 748 return suggestions;
747 749 },
748 750
749 751 validateOrientation: function(orientation, fallback) {
750 752 orientation = $.trim(orientation || '').toLowerCase();
751 753
752 754 if($.inArray(orientation, ['auto', 'bottom', 'top']) === -1){
753 755 orientation = fallback;
754 756 }
755 757
756 758 return orientation;
757 759 },
758 760
759 761 processResponse: function (result, originalQuery, cacheKey) {
760 762 var that = this,
761 763 options = that.options;
762 764
763 765 result.suggestions = that.verifySuggestionsFormat(result.suggestions);
764 766
765 767 // Cache results if cache is not disabled:
766 768 if (!options.noCache) {
767 769 that.cachedResponse[cacheKey] = result;
768 770 if (options.preventBadQueries && result.suggestions.length === 0) {
769 771 that.badQueries.push(originalQuery);
770 772 }
771 773 }
772 774
773 775 // Return if originalQuery is not matching current query:
774 776 if (originalQuery !== that.getQuery(that.currentValue)) {
775 777 return;
776 778 }
777 779
778 780 that.suggestions = result.suggestions;
779 781 that.suggest();
780 782 },
781 783
782 784 activate: function (index) {
783 785 var that = this,
784 786 activeItem,
785 787 selected = that.classes.selected,
786 788 container = $(that.suggestionsContainer),
787 789 children = container.find('.' + that.classes.suggestion);
788 790
789 791 container.find('.' + selected).removeClass(selected);
790 792
791 793 that.selectedIndex = index;
792 794
793 795 if (that.selectedIndex !== -1 && children.length > that.selectedIndex) {
794 796 activeItem = children.get(that.selectedIndex);
795 797 $(activeItem).addClass(selected);
796 798 return activeItem;
797 799 }
798 800
799 801 return null;
800 802 },
801 803
802 804 selectHint: function () {
803 805 var that = this,
804 806 i = $.inArray(that.hint, that.suggestions);
805 807 that.select(i);
806 808 },
807 809
808 810 select: function (index) {
809 811 var that = this;
810 812 that.hide();
811 813 that.onSelect(index);
812 814 },
813 815
814 816 moveUp: function () {
815 817 var that = this;
816 818
817 819 if (that.selectedIndex === -1) {
818 820 return;
819 821 }
820 822
821 823 if (that.selectedIndex === 0) {
822 824 $(that.suggestionsContainer).children().first().removeClass(that.classes.selected);
823 825 that.selectedIndex = -1;
824 826 that.el.val(that.currentValue);
825 827 that.findBestHint();
826 828 return;
827 829 }
828 830
829 831 that.adjustScroll(that.selectedIndex - 1);
830 832 },
831 833
832 834 moveDown: function () {
833 835 var that = this;
834 836
835 837 if (that.selectedIndex === (that.suggestions.length - 1)) {
836 838 return;
837 839 }
838 840
839 841 that.adjustScroll(that.selectedIndex + 1);
840 842 },
841 843
842 844 adjustScroll: function (index) {
843 845 var that = this,
844 846 activeItem = that.activate(index),
845 847 offsetTop,
846 848 upperBound,
847 849 lowerBound,
848 850 heightDelta = 25;
849 851
850 852 if (!activeItem) {
851 853 return;
852 854 }
853 855
854 856 offsetTop = activeItem.offsetTop;
855 857 upperBound = $(that.suggestionsContainer).scrollTop();
856 858 lowerBound = upperBound + that.options.maxHeight - heightDelta;
857 859
858 860 if (offsetTop < upperBound) {
859 861 $(that.suggestionsContainer).scrollTop(offsetTop);
860 862 } else if (offsetTop > lowerBound) {
861 863 $(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta);
862 864 }
863 865
864 866 if (that.options.replaceOnArrowKey) {
865 867 that.el.val(that.getValue(that.suggestions[index].value));
866 868 }
867 869 that.signalHint(null);
868 870 },
869 871
870 872 onSelect: function (index) {
871 873 var that = this,
872 874 onSelectCallback = that.options.onSelect,
873 875 suggestion = that.suggestions[index];
874 876
875 877 that.currentValue = that.getValue(suggestion.value);
876 878 var prevElem = {'value': that.el.val(),
877 879 'caret': that.element.selectionStart}
878 880
879 881 if (that.currentValue !== that.el.val()) {
880 882 that.el.val(that.currentValue);
881 883 }
882 884
883 885 that.signalHint(null);
884 886 that.suggestions = [];
885 887 that.selection = suggestion;
886 888
887 889 if ($.isFunction(onSelectCallback)) {
888 890 onSelectCallback.call(this, that.element, suggestion, prevElem);
889 891 }
890 892 },
891 893
892 894 getValue: function (value) {
893 895 var that = this,
894 896 delimiter = that.options.delimiter,
895 897 currentValue,
896 898 parts;
897 899
898 900 if (!delimiter) {
899 901 return value;
900 902 }
901 903
902 904 currentValue = that.currentValue;
903 905 parts = currentValue.split(delimiter);
904 906
905 907 if (parts.length === 1) {
906 908 return value;
907 909 }
908 910
909 911 return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value;
910 912 },
911 913
912 914 dispose: function () {
913 915 var that = this;
914 916 that.el.off('.autocomplete').removeData('autocomplete');
915 917 that.disableKillerFn();
916 918 $(window).off('resize.autocomplete', that.fixPositionCapture);
917 919 $(that.suggestionsContainer).remove();
918 920 }
919 921 };
920 922
921 923 // Create chainable jQuery plugin:
922 924 $.fn.autocomplete = $.fn.devbridgeAutocomplete = function (options, args) {
923 925 var dataKey = 'autocomplete';
924 926 // If function invoked without argument return
925 927 // instance of the first matched element:
926 928 if (arguments.length === 0) {
927 929 return this.first().data(dataKey);
928 930 }
929 931
930 932 return this.each(function () {
931 933 var inputElement = $(this),
932 934 instance = inputElement.data(dataKey);
933 935
934 936 if (typeof options === 'string') {
935 937 if (instance && typeof instance[options] === 'function') {
936 938 instance[options](args);
937 939 }
938 940 } else {
939 941 // If instance already exists, destroy it:
940 942 if (instance && instance.dispose) {
941 943 instance.dispose();
942 944 }
943 945 instance = new Autocomplete(this, options);
944 946 inputElement.data(dataKey, instance);
945 947 }
946 948 });
947 949 };
948 950 }));
@@ -1,955 +1,946 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 350 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
351 351 var prefix = "Error while submitting comment.\n"
352 352 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
353 353 ajaxErrorSwal(message);
354 354 self.resetCommentFormState(text);
355 355 };
356 356 self.submitAjaxPOST(
357 357 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
358 358 };
359 359
360 360 this.previewSuccessCallback = function(o) {
361 361 $(self.previewBoxSelector).html(o);
362 362 $(self.previewBoxSelector).removeClass('unloaded');
363 363
364 364 // swap buttons, making preview active
365 365 $(self.previewButton).parent().addClass('active');
366 366 $(self.editButton).parent().removeClass('active');
367 367
368 368 // unlock buttons
369 369 self.setActionButtonsDisabled(false);
370 370 };
371 371
372 372 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
373 373 excludeCancelBtn = excludeCancelBtn || false;
374 374 submitEvent = submitEvent || false;
375 375
376 376 $(this.editButton).prop('disabled', state);
377 377 $(this.previewButton).prop('disabled', state);
378 378
379 379 if (!excludeCancelBtn) {
380 380 $(this.cancelButton).prop('disabled', state);
381 381 }
382 382
383 383 var submitState = state;
384 384 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
385 385 // if the value of commit review status is set, we allow
386 386 // submit button, but only on Main form, isInline means inline
387 387 submitState = false
388 388 }
389 389
390 390 $(this.submitButton).prop('disabled', submitState);
391 391 if (submitEvent) {
392 392 $(this.submitButton).val(_gettext('Submitting...'));
393 393 } else {
394 394 $(this.submitButton).val(this.submitButtonText);
395 395 }
396 396
397 397 };
398 398
399 399 // lock preview/edit/submit buttons on load, but exclude cancel button
400 400 var excludeCancelBtn = true;
401 401 this.setActionButtonsDisabled(true, excludeCancelBtn);
402 402
403 403 // anonymous users don't have access to initialized CM instance
404 404 if (this.cm !== undefined){
405 405 this.cm.on('change', function(cMirror) {
406 406 if (cMirror.getValue() === "") {
407 407 self.setActionButtonsDisabled(true, excludeCancelBtn)
408 408 } else {
409 409 self.setActionButtonsDisabled(false, excludeCancelBtn)
410 410 }
411 411 });
412 412 }
413 413
414 414 $(this.editButton).on('click', function(e) {
415 415 e.preventDefault();
416 416
417 417 $(self.previewButton).parent().removeClass('active');
418 418 $(self.previewContainer).hide();
419 419
420 420 $(self.editButton).parent().addClass('active');
421 421 $(self.editContainer).show();
422 422
423 423 });
424 424
425 425 $(this.previewButton).on('click', function(e) {
426 426 e.preventDefault();
427 427 var text = self.cm.getValue();
428 428
429 429 if (text === "") {
430 430 return;
431 431 }
432 432
433 433 var postData = {
434 434 'text': text,
435 435 'renderer': templateContext.visual.default_renderer,
436 436 'csrf_token': CSRF_TOKEN
437 437 };
438 438
439 439 // lock ALL buttons on preview
440 440 self.setActionButtonsDisabled(true);
441 441
442 442 $(self.previewBoxSelector).addClass('unloaded');
443 443 $(self.previewBoxSelector).html(_gettext('Loading ...'));
444 444
445 445 $(self.editContainer).hide();
446 446 $(self.previewContainer).show();
447 447
448 448 // by default we reset state of comment preserving the text
449 449 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
450 450 var prefix = "Error while preview of comment.\n"
451 451 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
452 452 ajaxErrorSwal(message);
453 453
454 454 self.resetCommentFormState(text)
455 455 };
456 456 self.submitAjaxPOST(
457 457 self.previewUrl, postData, self.previewSuccessCallback,
458 458 previewFailCallback);
459 459
460 460 $(self.previewButton).parent().addClass('active');
461 461 $(self.editButton).parent().removeClass('active');
462 462 });
463 463
464 464 $(this.submitForm).submit(function(e) {
465 465 e.preventDefault();
466 466 var allowedToSubmit = self.isAllowedToSubmit();
467 467 if (!allowedToSubmit){
468 468 return false;
469 469 }
470 470 self.handleFormSubmit();
471 471 });
472 472
473 473 }
474 474
475 475 return CommentForm;
476 476 });
477 477
478 478 /* comments controller */
479 479 var CommentsController = function() {
480 480 var mainComment = '#text';
481 481 var self = this;
482 482
483 483 this.cancelComment = function(node) {
484 484 var $node = $(node);
485 485 var $td = $node.closest('td');
486 486 $node.closest('.comment-inline-form').remove();
487 487 return false;
488 488 };
489 489
490 490 this.getLineNumber = function(node) {
491 491 var $node = $(node);
492 492 var lineNo = $node.closest('td').attr('data-line-no');
493 493 if (lineNo === undefined && $node.data('commentInline')){
494 494 lineNo = $node.data('commentLineNo')
495 495 }
496 496
497 497 return lineNo
498 498 };
499 499
500 500 this.scrollToComment = function(node, offset, outdated) {
501 501 if (offset === undefined) {
502 502 offset = 0;
503 503 }
504 504 var outdated = outdated || false;
505 505 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
506 506
507 507 if (!node) {
508 508 node = $('.comment-selected');
509 509 if (!node.length) {
510 510 node = $('comment-current')
511 511 }
512 512 }
513 513
514 514 $wrapper = $(node).closest('div.comment');
515 515
516 516 // show hidden comment when referenced.
517 517 if (!$wrapper.is(':visible')){
518 518 $wrapper.show();
519 519 }
520 520
521 521 $comment = $(node).closest(klass);
522 522 $comments = $(klass);
523 523
524 524 $('.comment-selected').removeClass('comment-selected');
525 525
526 526 var nextIdx = $(klass).index($comment) + offset;
527 527 if (nextIdx >= $comments.length) {
528 528 nextIdx = 0;
529 529 }
530 530 var $next = $(klass).eq(nextIdx);
531 531
532 532 var $cb = $next.closest('.cb');
533 533 $cb.removeClass('cb-collapsed');
534 534
535 535 var $filediffCollapseState = $cb.closest('.filediff').prev();
536 536 $filediffCollapseState.prop('checked', false);
537 537 $next.addClass('comment-selected');
538 538 scrollToElement($next);
539 539 return false;
540 540 };
541 541
542 542 this.nextComment = function(node) {
543 543 return self.scrollToComment(node, 1);
544 544 };
545 545
546 546 this.prevComment = function(node) {
547 547 return self.scrollToComment(node, -1);
548 548 };
549 549
550 550 this.nextOutdatedComment = function(node) {
551 551 return self.scrollToComment(node, 1, true);
552 552 };
553 553
554 554 this.prevOutdatedComment = function(node) {
555 555 return self.scrollToComment(node, -1, true);
556 556 };
557 557
558 558 this._deleteComment = function(node) {
559 559 var $node = $(node);
560 560 var $td = $node.closest('td');
561 561 var $comment = $node.closest('.comment');
562 562 var comment_id = $comment.attr('data-comment-id');
563 563 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
564 564 var postData = {
565 565 'csrf_token': CSRF_TOKEN
566 566 };
567 567
568 568 $comment.addClass('comment-deleting');
569 569 $comment.hide('fast');
570 570
571 571 var success = function(response) {
572 572 $comment.remove();
573 573 return false;
574 574 };
575 575 var failure = function(jqXHR, textStatus, errorThrown) {
576 576 var prefix = "Error while deleting this comment.\n"
577 577 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
578 578 ajaxErrorSwal(message);
579 579
580 580 $comment.show('fast');
581 581 $comment.removeClass('comment-deleting');
582 582 return false;
583 583 };
584 584 ajaxPOST(url, postData, success, failure);
585 585 }
586 586
587 587 this.deleteComment = function(node) {
588 588 var $comment = $(node).closest('.comment');
589 589 var comment_id = $comment.attr('data-comment-id');
590 590
591 Swal.fire({
591 SwalNoAnimation.fire({
592 592 title: 'Delete this comment?',
593 593 icon: 'warning',
594 594 showCancelButton: true,
595 confirmButtonColor: '#84a5d2',
596 cancelButtonColor: '#e85e4d',
597 595 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
598 showClass: {
599 popup: 'swal2-noanimation',
600 backdrop: 'swal2-noanimation'
601 },
602 hideClass: {
603 popup: '',
604 backdrop: ''
605 }
596
606 597 }).then(function(result) {
607 598 if (result.value) {
608 599 self._deleteComment(node);
609 600 }
610 601 })
611 602 };
612 603
613 604 this.toggleWideMode = function (node) {
614 605 if ($('#content').hasClass('wrapper')) {
615 606 $('#content').removeClass("wrapper");
616 607 $('#content').addClass("wide-mode-wrapper");
617 608 $(node).addClass('btn-success');
618 609 return true
619 610 } else {
620 611 $('#content').removeClass("wide-mode-wrapper");
621 612 $('#content').addClass("wrapper");
622 613 $(node).removeClass('btn-success');
623 614 return false
624 615 }
625 616
626 617 };
627 618
628 619 this.toggleComments = function(node, show) {
629 620 var $filediff = $(node).closest('.filediff');
630 621 if (show === true) {
631 622 $filediff.removeClass('hide-comments');
632 623 } else if (show === false) {
633 624 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
634 625 $filediff.addClass('hide-comments');
635 626 } else {
636 627 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
637 628 $filediff.toggleClass('hide-comments');
638 629 }
639 630 return false;
640 631 };
641 632
642 633 this.toggleLineComments = function(node) {
643 634 self.toggleComments(node, true);
644 635 var $node = $(node);
645 636 // mark outdated comments as visible before the toggle;
646 637 $(node.closest('tr')).find('.comment-outdated').show();
647 638 $node.closest('tr').toggleClass('hide-line-comments');
648 639 };
649 640
650 641 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
651 642 var pullRequestId = templateContext.pull_request_data.pull_request_id;
652 643 var commitId = templateContext.commit_data.commit_id;
653 644
654 645 var commentForm = new CommentForm(
655 646 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
656 647 var cm = commentForm.getCmInstance();
657 648
658 649 if (resolvesCommentId){
659 650 var placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
660 651 }
661 652
662 653 setTimeout(function() {
663 654 // callbacks
664 655 if (cm !== undefined) {
665 656 commentForm.setPlaceholder(placeholderText);
666 657 if (commentForm.isInline()) {
667 658 cm.focus();
668 659 cm.refresh();
669 660 }
670 661 }
671 662 }, 10);
672 663
673 664 // trigger scrolldown to the resolve comment, since it might be away
674 665 // from the clicked
675 666 if (resolvesCommentId){
676 667 var actionNode = $(commentForm.resolvesActionId).offset();
677 668
678 669 setTimeout(function() {
679 670 if (actionNode) {
680 671 $('body, html').animate({scrollTop: actionNode.top}, 10);
681 672 }
682 673 }, 100);
683 674 }
684 675
685 676 // add dropzone support
686 677 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
687 678 var renderer = templateContext.visual.default_renderer;
688 679 if (renderer == 'rst') {
689 680 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
690 681 if (isRendered){
691 682 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
692 683 }
693 684 } else if (renderer == 'markdown') {
694 685 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
695 686 if (isRendered){
696 687 attachmentUrl = '!' + attachmentUrl;
697 688 }
698 689 } else {
699 690 var attachmentUrl = '{}'.format(attachmentStoreUrl);
700 691 }
701 692 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
702 693
703 694 return false;
704 695 };
705 696
706 697 //see: https://www.dropzonejs.com/#configuration
707 698 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
708 699 {'repo_name': templateContext.repo_name,
709 700 'commit_id': templateContext.commit_data.commit_id})
710 701
711 702 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
712 703 if (previewTmpl !== undefined){
713 704 var selectLink = $(formElement).find('.pick-attachment').get(0);
714 705 $(formElement).find('.comment-attachment-uploader').dropzone({
715 706 url: storeUrl,
716 707 headers: {"X-CSRF-Token": CSRF_TOKEN},
717 708 paramName: function () {
718 709 return "attachment"
719 710 }, // The name that will be used to transfer the file
720 711 clickable: selectLink,
721 712 parallelUploads: 1,
722 713 maxFiles: 10,
723 714 maxFilesize: templateContext.attachment_store.max_file_size_mb,
724 715 uploadMultiple: false,
725 716 autoProcessQueue: true, // if false queue will not be processed automatically.
726 717 createImageThumbnails: false,
727 718 previewTemplate: previewTmpl.innerHTML,
728 719
729 720 accept: function (file, done) {
730 721 done();
731 722 },
732 723 init: function () {
733 724
734 725 this.on("sending", function (file, xhr, formData) {
735 726 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
736 727 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
737 728 });
738 729
739 730 this.on("success", function (file, response) {
740 731 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
741 732 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
742 733
743 734 var isRendered = false;
744 735 var ext = file.name.split('.').pop();
745 736 var imageExts = templateContext.attachment_store.image_ext;
746 737 if (imageExts.indexOf(ext) !== -1){
747 738 isRendered = true;
748 739 }
749 740
750 741 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
751 742 });
752 743
753 744 this.on("error", function (file, errorMessage, xhr) {
754 745 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
755 746
756 747 var error = null;
757 748
758 749 if (xhr !== undefined){
759 750 var httpStatus = xhr.status + " " + xhr.statusText;
760 751 if (xhr !== undefined && xhr.status >= 500) {
761 752 error = httpStatus;
762 753 }
763 754 }
764 755
765 756 if (error === null) {
766 757 error = errorMessage.error || errorMessage || httpStatus;
767 758 }
768 759 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
769 760
770 761 });
771 762 }
772 763 });
773 764 }
774 765 return commentForm;
775 766 };
776 767
777 768 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
778 769
779 770 var tmpl = $('#cb-comment-general-form-template').html();
780 771 tmpl = tmpl.format(null, 'general');
781 772 var $form = $(tmpl);
782 773
783 774 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
784 775 var curForm = $formPlaceholder.find('form');
785 776 if (curForm){
786 777 curForm.remove();
787 778 }
788 779 $formPlaceholder.append($form);
789 780
790 781 var _form = $($form[0]);
791 782 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
792 783 var commentForm = this.createCommentForm(
793 784 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
794 785 commentForm.initStatusChangeSelector();
795 786
796 787 return commentForm;
797 788 };
798 789
799 790 this.createComment = function(node, resolutionComment) {
800 791 var resolvesCommentId = resolutionComment || null;
801 792 var $node = $(node);
802 793 var $td = $node.closest('td');
803 794 var $form = $td.find('.comment-inline-form');
804 795
805 796 if (!$form.length) {
806 797
807 798 var $filediff = $node.closest('.filediff');
808 799 $filediff.removeClass('hide-comments');
809 800 var f_path = $filediff.attr('data-f-path');
810 801 var lineno = self.getLineNumber(node);
811 802 // create a new HTML from template
812 803 var tmpl = $('#cb-comment-inline-form-template').html();
813 804 tmpl = tmpl.format(escapeHtml(f_path), lineno);
814 805 $form = $(tmpl);
815 806
816 807 var $comments = $td.find('.inline-comments');
817 808 if (!$comments.length) {
818 809 $comments = $(
819 810 $('#cb-comments-inline-container-template').html());
820 811 $td.append($comments);
821 812 }
822 813
823 814 $td.find('.cb-comment-add-button').before($form);
824 815
825 816 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
826 817 var _form = $($form[0]).find('form');
827 818 var autocompleteActions = ['as_note', 'as_todo'];
828 819 var commentForm = this.createCommentForm(
829 820 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
830 821
831 822 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
832 823 form: _form,
833 824 parent: $td[0],
834 825 lineno: lineno,
835 826 f_path: f_path}
836 827 );
837 828
838 829 // set a CUSTOM submit handler for inline comments.
839 830 commentForm.setHandleFormSubmit(function(o) {
840 831 var text = commentForm.cm.getValue();
841 832 var commentType = commentForm.getCommentType();
842 833 var resolvesCommentId = commentForm.getResolvesId();
843 834
844 835 if (text === "") {
845 836 return;
846 837 }
847 838
848 839 if (lineno === undefined) {
849 840 alert('missing line !');
850 841 return;
851 842 }
852 843 if (f_path === undefined) {
853 844 alert('missing file path !');
854 845 return;
855 846 }
856 847
857 848 var excludeCancelBtn = false;
858 849 var submitEvent = true;
859 850 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
860 851 commentForm.cm.setOption("readOnly", true);
861 852 var postData = {
862 853 'text': text,
863 854 'f_path': f_path,
864 855 'line': lineno,
865 856 'comment_type': commentType,
866 857 'csrf_token': CSRF_TOKEN
867 858 };
868 859 if (resolvesCommentId){
869 860 postData['resolves_comment_id'] = resolvesCommentId;
870 861 }
871 862
872 863 var submitSuccessCallback = function(json_data) {
873 864 $form.remove();
874 865 try {
875 866 var html = json_data.rendered_text;
876 867 var lineno = json_data.line_no;
877 868 var target_id = json_data.target_id;
878 869
879 870 $comments.find('.cb-comment-add-button').before(html);
880 871
881 872 //mark visually which comment was resolved
882 873 if (resolvesCommentId) {
883 874 commentForm.markCommentResolved(resolvesCommentId);
884 875 }
885 876
886 877 // run global callback on submit
887 878 commentForm.globalSubmitSuccessCallback();
888 879
889 880 } catch (e) {
890 881 console.error(e);
891 882 }
892 883
893 884 // re trigger the linkification of next/prev navigation
894 885 linkifyComments($('.inline-comment-injected'));
895 886 timeagoActivate();
896 887 tooltipActivate();
897 888
898 889 if (window.updateSticky !== undefined) {
899 890 // potentially our comments change the active window size, so we
900 891 // notify sticky elements
901 892 updateSticky()
902 893 }
903 894
904 895 commentForm.setActionButtonsDisabled(false);
905 896
906 897 };
907 898 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
908 899 var prefix = "Error while submitting comment.\n"
909 900 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
910 901 ajaxErrorSwal(message);
911 902 commentForm.resetCommentFormState(text)
912 903 };
913 904 commentForm.submitAjaxPOST(
914 905 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
915 906 });
916 907 }
917 908
918 909 $form.addClass('comment-inline-form-open');
919 910 };
920 911
921 912 this.createResolutionComment = function(commentId){
922 913 // hide the trigger text
923 914 $('#resolve-comment-{0}'.format(commentId)).hide();
924 915
925 916 var comment = $('#comment-'+commentId);
926 917 var commentData = comment.data();
927 918 if (commentData.commentInline) {
928 919 this.createComment(comment, commentId)
929 920 } else {
930 921 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
931 922 }
932 923
933 924 return false;
934 925 };
935 926
936 927 this.submitResolution = function(commentId){
937 928 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
938 929 var commentForm = form.get(0).CommentForm;
939 930
940 931 var cm = commentForm.getCmInstance();
941 932 var renderer = templateContext.visual.default_renderer;
942 933 if (renderer == 'rst'){
943 934 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
944 935 } else if (renderer == 'markdown') {
945 936 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
946 937 } else {
947 938 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
948 939 }
949 940
950 941 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
951 942 form.submit();
952 943 return false;
953 944 };
954 945
955 946 };
@@ -1,117 +1,97 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 generatePassword = function(length) {
20 20 if (length === undefined){
21 21 length = 8
22 22 }
23 23
24 24 var charset = "abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
25 25 var gen_pass = "";
26 26
27 27 for (var i = 0, n = charset.length; i < length; ++i) {
28 28 gen_pass += charset.charAt(Math.floor(Math.random() * n));
29 29 }
30 30 return gen_pass;
31 31 };
32 32
33 33 /**
34 34 * User autocomplete
35 35 */
36 36 var UsersAutoComplete = function(input_id, user_id) {
37 37 $('#'+input_id).autocomplete({
38 38 serviceUrl: pyroutes.url('user_autocomplete_data'),
39 39 minChars:2,
40 40 maxHeight:400,
41 41 deferRequestBy: 300, //miliseconds
42 42 showNoSuggestionNotice: true,
43 43 tabDisabled: true,
44 44 autoSelectFirst: true,
45 45 params: { user_id: user_id },
46 46 formatResult: autocompleteFormatResult,
47 47 lookupFilter: autocompleteFilterResult
48 48 });
49 49 };
50 50
51 51 var _showAuthToken = function (authTokenId, showUrl) {
52 52
53 Swal.fire({
53 SwalNoAnimation.fire({
54 54 title: _gettext('Show this authentication token?'),
55 55 showCancelButton: true,
56 confirmButtonColor: '#84a5d2',
57 cancelButtonColor: '#e85e4d',
58 showClass: {
59 popup: 'swal2-noanimation',
60 backdrop: 'swal2-noanimation'
61 },
62 hideClass: {
63 popup: '',
64 backdrop: ''
65 },
66 56 confirmButtonText: _gettext('Show'),
67 57 showLoaderOnConfirm: true,
68 58 allowOutsideClick: function () {
69 59 !Swal.isLoading()
70 60 },
71 61 preConfirm: function () {
72 62
73 63 var postData = {
74 64 'auth_token_id': authTokenId,
75 65 'csrf_token': CSRF_TOKEN
76 66 };
77 67 return new Promise(function (resolve, reject) {
78 68 $.ajax({
79 69 type: 'POST',
80 70 data: postData,
81 71 url: showUrl,
82 72 headers: {'X-PARTIAL-XHR': true}
83 73 })
84 74 .done(function (data) {
85 75 resolve(data);
86 76 })
87 77 .fail(function (jqXHR, textStatus, errorThrown) {
88 78 //reject("Failed to fetch Authentication Token")
89 79 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
90 80 ajaxErrorSwal(message);
91 81 });
92 82 })
93 83 }
94 84
95 85 })
96 86 .then(function (result) {
97 87 if (result.value) {
98 88 var tmpl = ('<code>{0}</code>' +
99 89 '<i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="{1}" title="Copy Token"></i>');
100 90
101 Swal.fire({
91 SwalNoAnimation.fire({
102 92 title: _gettext('Authentication Token'),
103 93 html: tmpl.format(result.value.auth_token, result.value.auth_token),
104 confirmButtonColor: '#84a5d2',
105 cancelButtonColor: '#e85e4d',
106 showClass: {
107 popup: 'swal2-noanimation',
108 backdrop: 'swal2-noanimation'
109 },
110 hideClass: {
111 popup: '',
112 backdrop: ''
113 },
114 94 })
115 95 }
116 96 })
117 97 } No newline at end of file
@@ -1,159 +1,175 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 /**
20 20 * turns objects into GET query string
21 21 */
22 22 var toQueryString = function(o) {
23 23 if(typeof o === 'string') {
24 24 return o;
25 25 }
26 26 if(typeof o !== 'object') {
27 27 return false;
28 28 }
29 29 var _p, _qs = [];
30 30 for(_p in o) {
31 31 _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
32 32 }
33 33 return _qs.join('&');
34 34 };
35 35
36 36 /**
37 37 * ajax call wrappers
38 38 */
39 39
40 40 var ajaxGET = function (url, success, failure) {
41 41 var sUrl = url;
42 42 var request = $.ajax({
43 43 url: sUrl,
44 44 headers: {'X-PARTIAL-XHR': true}
45 45 })
46 46 .done(function (data) {
47 47 success(data);
48 48 })
49 49 .fail(function (jqXHR, textStatus, errorThrown) {
50 50 if (failure) {
51 51 failure(jqXHR, textStatus, errorThrown);
52 52 } else {
53 53 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
54 54 ajaxErrorSwal(message);
55 55 }
56 56 });
57 57 return request;
58 58 };
59 59
60 60 var ajaxPOST = function (url, postData, success, failure) {
61 61 var sUrl = url;
62 62 var postData = toQueryString(postData);
63 63 var request = $.ajax({
64 64 type: 'POST',
65 65 url: sUrl,
66 66 data: postData,
67 67 headers: {'X-PARTIAL-XHR': true}
68 68 })
69 69 .done(function (data) {
70 70 success(data);
71 71 })
72 72 .fail(function (jqXHR, textStatus, errorThrown) {
73 73 if (failure) {
74 74 failure(jqXHR, textStatus, errorThrown);
75 75 } else {
76 76 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
77 77 ajaxErrorSwal(message);
78 78 }
79 79 });
80 80 return request;
81 81 };
82 82
83
84 SwalNoAnimation = Swal.mixin({
85 confirmButtonColor: '#84a5d2',
86 cancelButtonColor: '#e85e4d',
87 showClass: {
88 popup: 'swal2-noanimation',
89 backdrop: 'swal2-noanimation'
90 },
91 hideClass: {
92 popup: '',
93 backdrop: ''
94 },
95 })
96
97
98 /* Example usage:
99 *
100 error: function(jqXHR, textStatus, errorThrown) {
101 var prefix = "Error while fetching entries.\n"
102 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
103 ajaxErrorSwal(message);
104 }
105 *
106 * */
83 107 function formatErrorMessage(jqXHR, textStatus, errorThrown, prefix) {
84 108 if(typeof prefix === "undefined") {
85 109 prefix = ''
86 110 }
87 111
88 112 if (jqXHR.status === 0) {
89 113 return (prefix + 'Not connected.\nPlease verify your network connection.');
90 114 } else if (jqXHR.status == 401) {
91 115 return (prefix + 'Unauthorized access. [401]');
92 116 } else if (jqXHR.status == 404) {
93 117 return (prefix + 'The requested page not found. [404]');
94 118 } else if (jqXHR.status == 500) {
95 119 return (prefix + 'Internal Server Error [500].');
96 120 } else if (jqXHR.status == 503) {
97 121 return (prefix + 'Service unavailable [503].');
98 122 } else if (errorThrown === 'parsererror') {
99 123 return (prefix + 'Requested JSON parse failed.');
100 124 } else if (errorThrown === 'timeout') {
101 125 return (prefix + 'Time out error.');
102 126 } else if (errorThrown === 'abort') {
103 127 return (prefix + 'Ajax request aborted.');
104 128 } else {
105 129 return (prefix + 'Uncaught Error.\n' + jqXHR.responseText);
106 130 }
107 131 }
108 132
109 133 function ajaxErrorSwal(message) {
110 Swal.fire({
134 SwalNoAnimation.fire({
111 135 icon: 'error',
112 136 title: _gettext('Ajax Request Error'),
113 137 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
114 138 showClass: {
115 139 popup: 'swal2-noanimation',
116 140 backdrop: 'swal2-noanimation'
117 141 },
118 142 hideClass: {
119 143 popup: '',
120 144 backdrop: ''
121 145 }
122 146 })
123 147 }
124 148
125 149 /*
126 150 * use in onclick attributes e.g
127 151 * onclick="submitConfirm(event, this, _gettext('Confirm to delete '), _gettext('Confirm Delete'), 'what we delete')">
128 152 * */
129 153 function submitConfirm(event, self, question, confirmText, htmlText) {
130 154 if (htmlText === "undefined") {
131 155 htmlText = null;
132 156 }
133 157 if (confirmText === "undefined") {
134 158 confirmText = _gettext('Delete')
135 159 }
136 160 event.preventDefault();
137 161
138 Swal.fire({
162 SwalNoAnimation.fire({
139 163 title: question,
140 164 icon: 'warning',
141 165 html: htmlText,
142 showClass: {
143 popup: 'swal2-noanimation',
144 backdrop: 'swal2-noanimation'
145 },
146 hideClass: {
147 popup: '',
148 backdrop: ''
149 },
166
150 167 showCancelButton: true,
151 confirmButtonColor: '#84a5d2',
152 cancelButtonColor: '#e85e4d',
168
153 169 confirmButtonText: confirmText
154 170 }).then(function(result) {
155 171 if (result.value) {
156 172 $(self).closest("form").submit();
157 173 }
158 174 })
159 175 } No newline at end of file
@@ -1,1195 +1,1201 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%!
4 4 ## base64 filter e.g ${ example | base64 }
5 5 def base64(text):
6 6 import base64
7 7 from rhodecode.lib.helpers import safe_str
8 8 return base64.encodestring(safe_str(text))
9 9 %>
10 10
11 11 <%inherit file="root.mako"/>
12 12
13 13 <%include file="/ejs_templates/templates.html"/>
14 14
15 15 <div class="outerwrapper">
16 16 <!-- HEADER -->
17 17 <div class="header">
18 18 <div id="header-inner" class="wrapper">
19 19 <div id="logo">
20 20 <div class="logo-wrapper">
21 21 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
22 22 </div>
23 23 % if c.rhodecode_name:
24 24 <div class="branding">
25 25 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
26 26 </div>
27 27 % endif
28 28 </div>
29 29 <!-- MENU BAR NAV -->
30 30 ${self.menu_bar_nav()}
31 31 <!-- END MENU BAR NAV -->
32 32 </div>
33 33 </div>
34 34 ${self.menu_bar_subnav()}
35 35 <!-- END HEADER -->
36 36
37 37 <!-- CONTENT -->
38 38 <div id="content" class="wrapper">
39 39
40 40 <rhodecode-toast id="notifications"></rhodecode-toast>
41 41
42 42 <div class="main">
43 43 ${next.main()}
44 44 </div>
45 45 </div>
46 46 <!-- END CONTENT -->
47 47
48 48 </div>
49 49 <!-- FOOTER -->
50 50 <div id="footer">
51 51 <div id="footer-inner" class="title wrapper">
52 52 <div>
53 53 <p class="footer-link-right">
54 54 % if c.visual.show_version:
55 55 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
56 56 % endif
57 57 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
58 58 % if c.visual.rhodecode_support_url:
59 59 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
60 60 % endif
61 61 </p>
62 62 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
63 63 <p class="server-instance" style="display:${sid}">
64 64 ## display hidden instance ID if specially defined
65 65 % if c.rhodecode_instanceid:
66 66 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
67 67 % endif
68 68 </p>
69 69 </div>
70 70 </div>
71 71 </div>
72 72
73 73 <!-- END FOOTER -->
74 74
75 75 ### MAKO DEFS ###
76 76
77 77 <%def name="menu_bar_subnav()">
78 78 </%def>
79 79
80 80 <%def name="breadcrumbs(class_='breadcrumbs')">
81 81 <div class="${class_}">
82 82 ${self.breadcrumbs_links()}
83 83 </div>
84 84 </%def>
85 85
86 86 <%def name="admin_menu(active=None)">
87 87
88 88 <div id="context-bar">
89 89 <div class="wrapper">
90 90 <div class="title">
91 91 <div class="title-content">
92 92 <div class="title-main">
93 93 % if c.is_super_admin:
94 94 ${_('Super-admin Panel')}
95 95 % else:
96 96 ${_('Delegated Admin Panel')}
97 97 % endif
98 98 </div>
99 99 </div>
100 100 </div>
101 101
102 102 <ul id="context-pages" class="navigation horizontal-list">
103 103
104 104 ## super-admin case
105 105 % if c.is_super_admin:
106 106 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
107 107 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
108 108 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
109 109 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
110 110 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
111 111 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
112 112 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
113 113 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
114 114 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
115 115 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
116 116
117 117 ## delegated admin
118 118 % elif c.is_delegated_admin:
119 119 <%
120 120 repositories=c.auth_user.repositories_admin or c.can_create_repo
121 121 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
122 122 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
123 123 %>
124 124
125 125 %if repositories:
126 126 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
127 127 %endif
128 128 %if repository_groups:
129 129 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
130 130 %endif
131 131 %if user_groups:
132 132 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
133 133 %endif
134 134 % endif
135 135 </ul>
136 136
137 137 </div>
138 138 <div class="clear"></div>
139 139 </div>
140 140 </%def>
141 141
142 142 <%def name="dt_info_panel(elements)">
143 143 <dl class="dl-horizontal">
144 144 %for dt, dd, title, show_items in elements:
145 145 <dt>${dt}:</dt>
146 146 <dd title="${h.tooltip(title)}">
147 147 %if callable(dd):
148 148 ## allow lazy evaluation of elements
149 149 ${dd()}
150 150 %else:
151 151 ${dd}
152 152 %endif
153 153 %if show_items:
154 154 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
155 155 %endif
156 156 </dd>
157 157
158 158 %if show_items:
159 159 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
160 160 %for item in show_items:
161 161 <dt></dt>
162 162 <dd>${item}</dd>
163 163 %endfor
164 164 </div>
165 165 %endif
166 166
167 167 %endfor
168 168 </dl>
169 169 </%def>
170 170
171 171 <%def name="tr_info_entry(element)">
172 172 <% key, val, title, show_items = element %>
173 173
174 174 <tr>
175 175 <td style="vertical-align: top">${key}</td>
176 176 <td title="${h.tooltip(title)}">
177 177 %if callable(val):
178 178 ## allow lazy evaluation of elements
179 179 ${val()}
180 180 %else:
181 181 ${val}
182 182 %endif
183 183 %if show_items:
184 184 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
185 185 % for item in show_items:
186 186 <dt></dt>
187 187 <dd>${item}</dd>
188 188 % endfor
189 189 </div>
190 190 %endif
191 191 </td>
192 192 <td style="vertical-align: top">
193 193 %if show_items:
194 194 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
195 195 %endif
196 196 </td>
197 197 </tr>
198 198
199 199 </%def>
200 200
201 201 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
202 202 <%
203 203 if size > 16:
204 204 gravatar_class = ['gravatar','gravatar-large']
205 205 else:
206 206 gravatar_class = ['gravatar']
207 207
208 208 data_hovercard_url = ''
209 209 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
210 210
211 211 if tooltip:
212 212 gravatar_class += ['tooltip-hovercard']
213 213 if extra_class:
214 214 gravatar_class += extra_class
215 215 if tooltip and user:
216 216 if user.username == h.DEFAULT_USER:
217 217 gravatar_class.pop(-1)
218 218 else:
219 219 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
220 220 gravatar_class = ' '.join(gravatar_class)
221 221
222 222 %>
223 223 <%doc>
224 224 TODO: johbo: For now we serve double size images to make it smooth
225 225 for retina. This is how it worked until now. Should be replaced
226 226 with a better solution at some point.
227 227 </%doc>
228 228
229 229 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" />
230 230 </%def>
231 231
232 232
233 233 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
234 234 <%
235 235 email = h.email_or_none(contact)
236 236 rc_user = h.discover_user(contact)
237 237 %>
238 238
239 239 <div class="${_class}">
240 240 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
241 241 <span class="${('user user-disabled' if show_disabled else 'user')}"> ${h.link_to_user(rc_user or contact)}</span>
242 242 </div>
243 243 </%def>
244 244
245 245
246 246 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
247 247 <%
248 248 if (size > 16):
249 249 gravatar_class = 'icon-user-group-alt'
250 250 else:
251 251 gravatar_class = 'icon-user-group-alt'
252 252
253 253 if tooltip:
254 254 gravatar_class += ' tooltip-hovercard'
255 255
256 256 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
257 257 %>
258 258 <%doc>
259 259 TODO: johbo: For now we serve double size images to make it smooth
260 260 for retina. This is how it worked until now. Should be replaced
261 261 with a better solution at some point.
262 262 </%doc>
263 263
264 264 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
265 265 </%def>
266 266
267 267 <%def name="repo_page_title(repo_instance)">
268 268 <div class="title-content repo-title">
269 269
270 270 <div class="title-main">
271 271 ## SVN/HG/GIT icons
272 272 %if h.is_hg(repo_instance):
273 273 <i class="icon-hg"></i>
274 274 %endif
275 275 %if h.is_git(repo_instance):
276 276 <i class="icon-git"></i>
277 277 %endif
278 278 %if h.is_svn(repo_instance):
279 279 <i class="icon-svn"></i>
280 280 %endif
281 281
282 282 ## public/private
283 283 %if repo_instance.private:
284 284 <i class="icon-repo-private"></i>
285 285 %else:
286 286 <i class="icon-repo-public"></i>
287 287 %endif
288 288
289 289 ## repo name with group name
290 290 ${h.breadcrumb_repo_link(repo_instance)}
291 291
292 292 ## Context Actions
293 293 <div class="pull-right">
294 294 %if c.rhodecode_user.username != h.DEFAULT_USER:
295 295 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
296 296
297 297 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
298 298 % if c.repository_is_user_following:
299 299 <i class="icon-eye-off"></i>${_('Unwatch')}
300 300 % else:
301 301 <i class="icon-eye"></i>${_('Watch')}
302 302 % endif
303 303
304 304 </a>
305 305 %else:
306 306 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
307 307 %endif
308 308 </div>
309 309
310 310 </div>
311 311
312 312 ## FORKED
313 313 %if repo_instance.fork:
314 314 <p class="discreet">
315 315 <i class="icon-code-fork"></i> ${_('Fork of')}
316 316 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
317 317 </p>
318 318 %endif
319 319
320 320 ## IMPORTED FROM REMOTE
321 321 %if repo_instance.clone_uri:
322 322 <p class="discreet">
323 323 <i class="icon-code-fork"></i> ${_('Clone from')}
324 324 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
325 325 </p>
326 326 %endif
327 327
328 328 ## LOCKING STATUS
329 329 %if repo_instance.locked[0]:
330 330 <p class="locking_locked discreet">
331 331 <i class="icon-repo-lock"></i>
332 332 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
333 333 </p>
334 334 %elif repo_instance.enable_locking:
335 335 <p class="locking_unlocked discreet">
336 336 <i class="icon-repo-unlock"></i>
337 337 ${_('Repository not locked. Pull repository to lock it.')}
338 338 </p>
339 339 %endif
340 340
341 341 </div>
342 342 </%def>
343 343
344 344 <%def name="repo_menu(active=None)">
345 345 <%
346 346 ## determine if we have "any" option available
347 347 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
348 348 has_actions = can_lock
349 349
350 350 %>
351 351 % if c.rhodecode_db_repo.archived:
352 352 <div class="alert alert-warning text-center">
353 353 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
354 354 </div>
355 355 % endif
356 356
357 357 <!--- REPO CONTEXT BAR -->
358 358 <div id="context-bar">
359 359 <div class="wrapper">
360 360
361 361 <div class="title">
362 362 ${self.repo_page_title(c.rhodecode_db_repo)}
363 363 </div>
364 364
365 365 <ul id="context-pages" class="navigation horizontal-list">
366 366 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
367 367 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
368 368 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
369 369 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
370 370
371 371 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
372 372 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
373 373 <li class="${h.is_active('showpullrequest', active)}">
374 374 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
375 375 <div class="menulabel">
376 376 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
377 377 </div>
378 378 </a>
379 379 </li>
380 380 %endif
381 381
382 382 <li class="${h.is_active('artifacts', active)}">
383 383 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
384 384 <div class="menulabel">
385 385 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
386 386 </div>
387 387 </a>
388 388 </li>
389 389
390 390 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
391 391 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
392 392 %endif
393 393
394 394 <li class="${h.is_active('options', active)}">
395 395 % if has_actions:
396 396 <a class="menulink dropdown">
397 397 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
398 398 </a>
399 399 <ul class="submenu">
400 400 %if can_lock:
401 401 %if c.rhodecode_db_repo.locked[0]:
402 402 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
403 403 %else:
404 404 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
405 405 %endif
406 406 %endif
407 407 </ul>
408 408 % endif
409 409 </li>
410 410
411 411 </ul>
412 412 </div>
413 413 <div class="clear"></div>
414 414 </div>
415 415
416 416 <!--- REPO END CONTEXT BAR -->
417 417
418 418 </%def>
419 419
420 420 <%def name="repo_group_page_title(repo_group_instance)">
421 421 <div class="title-content">
422 422 <div class="title-main">
423 423 ## Repository Group icon
424 424 <i class="icon-repo-group"></i>
425 425
426 426 ## repo name with group name
427 427 ${h.breadcrumb_repo_group_link(repo_group_instance)}
428 428 </div>
429 429
430 430 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
431 431 <div class="repo-group-desc discreet">
432 432 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
433 433 </div>
434 434
435 435 </div>
436 436 </%def>
437 437
438 438
439 439 <%def name="repo_group_menu(active=None)">
440 440 <%
441 441 gr_name = c.repo_group.group_name if c.repo_group else None
442 442 # create repositories with write permission on group is set to true
443 443 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
444 444
445 445 %>
446 446
447 447
448 448 <!--- REPO GROUP CONTEXT BAR -->
449 449 <div id="context-bar">
450 450 <div class="wrapper">
451 451 <div class="title">
452 452 ${self.repo_group_page_title(c.repo_group)}
453 453 </div>
454 454
455 455 <ul id="context-pages" class="navigation horizontal-list">
456 456 <li class="${h.is_active('home', active)}">
457 457 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
458 458 </li>
459 459 % if c.is_super_admin or group_admin:
460 460 <li class="${h.is_active('settings', active)}">
461 461 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
462 462 </li>
463 463 % endif
464 464
465 465 </ul>
466 466 </div>
467 467 <div class="clear"></div>
468 468 </div>
469 469
470 470 <!--- REPO GROUP CONTEXT BAR -->
471 471
472 472 </%def>
473 473
474 474
475 475 <%def name="usermenu(active=False)">
476 476 <%
477 477 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
478 478
479 479 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
480 480 # create repositories with write permission on group is set to true
481 481
482 482 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
483 483 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
484 484 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
485 485 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
486 486
487 487 can_create_repos = c.is_super_admin or c.can_create_repo
488 488 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
489 489
490 490 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
491 491 can_create_repo_groups_in_group = c.is_super_admin or group_admin
492 492 %>
493 493
494 494 % if not_anonymous:
495 495 <%
496 496 default_target_group = dict()
497 497 if c.rhodecode_user.personal_repo_group:
498 498 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
499 499 %>
500 500
501 501 ## create action
502 502 <li>
503 503 <a href="#create-actions" onclick="return false;" class="menulink childs">
504 504 <i class="tooltip icon-plus-circled" title="${_('Create')}"></i>
505 505 </a>
506 506
507 507 <div class="action-menu submenu">
508 508
509 509 <ol>
510 510 ## scope of within a repository
511 511 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
512 512 <li class="submenu-title">${_('This Repository')}</li>
513 513 <li>
514 514 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
515 515 </li>
516 516 % if can_fork:
517 517 <li>
518 518 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
519 519 </li>
520 520 % endif
521 521 % endif
522 522
523 523 ## scope of within repository groups
524 524 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
525 525 <li class="submenu-title">${_('This Repository Group')}</li>
526 526
527 527 % if can_create_repos_in_group:
528 528 <li>
529 529 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
530 530 </li>
531 531 % endif
532 532
533 533 % if can_create_repo_groups_in_group:
534 534 <li>
535 535 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'New Repository Group')}</a>
536 536 </li>
537 537 % endif
538 538 % endif
539 539
540 540 ## personal group
541 541 % if c.rhodecode_user.personal_repo_group:
542 542 <li class="submenu-title">Personal Group</li>
543 543
544 544 <li>
545 545 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
546 546 </li>
547 547
548 548 <li>
549 549 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
550 550 </li>
551 551 % endif
552 552
553 553 ## Global actions
554 554 <li class="submenu-title">RhodeCode</li>
555 555 % if can_create_repos:
556 556 <li>
557 557 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
558 558 </li>
559 559 % endif
560 560
561 561 % if can_create_repo_groups:
562 562 <li>
563 563 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
564 564 </li>
565 565 % endif
566 566
567 567 <li>
568 568 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
569 569 </li>
570 570
571 571 </ol>
572 572
573 573 </div>
574 574 </li>
575 575
576 576 ## notifications
577 577 <li>
578 578 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
579 579 ${c.unread_notifications}
580 580 </a>
581 581 </li>
582 582 % endif
583 583
584 584 ## USER MENU
585 585 <li id="quick_login_li" class="${'active' if active else ''}">
586 586 % if c.rhodecode_user.username == h.DEFAULT_USER:
587 587 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
588 588 ${gravatar(c.rhodecode_user.email, 20)}
589 589 <span class="user">
590 590 <span>${_('Sign in')}</span>
591 591 </span>
592 592 </a>
593 593 % else:
594 594 ## logged in user
595 595 <a id="quick_login_link" class="menulink childs">
596 596 ${gravatar(c.rhodecode_user.email, 20)}
597 597 <span class="user">
598 598 <span class="menu_link_user">${c.rhodecode_user.username}</span>
599 599 <div class="show_more"></div>
600 600 </span>
601 601 </a>
602 602 ## subnav with menu for logged in user
603 603 <div class="user-menu submenu">
604 604 <div id="quick_login">
605 605 %if c.rhodecode_user.username != h.DEFAULT_USER:
606 606 <div class="">
607 607 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
608 608 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
609 609 <div class="email">${c.rhodecode_user.email}</div>
610 610 </div>
611 611 <div class="">
612 612 <ol class="links">
613 613 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
614 614 % if c.rhodecode_user.personal_repo_group:
615 615 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
616 616 % endif
617 617 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
618 618
619 619 % if c.debug_style:
620 620 <li>
621 621 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
622 622 <div class="menulabel">${_('[Style]')}</div>
623 623 </a>
624 624 </li>
625 625 % endif
626 626
627 627 ## bookmark-items
628 628 <li class="bookmark-items">
629 629 ${_('Bookmarks')}
630 630 <div class="pull-right">
631 631 <a href="${h.route_path('my_account_bookmarks')}">
632 632
633 633 <i class="icon-cog"></i>
634 634 </a>
635 635 </div>
636 636 </li>
637 637 % if not c.bookmark_items:
638 638 <li>
639 639 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
640 640 </li>
641 641 % endif
642 642 % for item in c.bookmark_items:
643 643 <li>
644 644 % if item.repository:
645 645 <div>
646 646 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
647 647 <code>${item.position}</code>
648 648 % if item.repository.repo_type == 'hg':
649 649 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
650 650 % elif item.repository.repo_type == 'git':
651 651 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
652 652 % elif item.repository.repo_type == 'svn':
653 653 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
654 654 % endif
655 655 ${(item.title or h.shorter(item.repository.repo_name, 30))}
656 656 </a>
657 657 </div>
658 658 % elif item.repository_group:
659 659 <div>
660 660 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
661 661 <code>${item.position}</code>
662 662 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
663 663 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
664 664 </a>
665 665 </div>
666 666 % else:
667 667 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
668 668 <code>${item.position}</code>
669 669 ${item.title}
670 670 </a>
671 671 % endif
672 672 </li>
673 673 % endfor
674 674
675 675 <li class="logout">
676 676 ${h.secure_form(h.route_path('logout'), request=request)}
677 677 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
678 678 ${h.end_form()}
679 679 </li>
680 680 </ol>
681 681 </div>
682 682 %endif
683 683 </div>
684 684 </div>
685 685
686 686 % endif
687 687 </li>
688 688 </%def>
689 689
690 690 <%def name="menu_items(active=None)">
691 691 <%
692 692 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
693 693 notice_display = 'none' if len(notice_messages) == 0 else ''
694 694 %>
695 695 <style>
696 696
697 697 </style>
698 698
699 699 <ul id="quick" class="main_nav navigation horizontal-list">
700 700 ## notice box for important system messages
701 701 <li style="display: ${notice_display}">
702 702 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
703 703 <div class="menulabel-notice ${notice_level}" >
704 704 ${len(notice_messages)}
705 705 </div>
706 706 </a>
707 707 </li>
708 708 <div class="notice-messages-container" style="display: none">
709 709 <div class="notice-messages">
710 710 <table class="rctable">
711 711 % for notice in notice_messages:
712 712 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
713 713 <td style="vertical-align: text-top; width: 20px">
714 714 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
715 715 </td>
716 716 <td>
717 717 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
718 718 ${notice['subject']}
719 719
720 720 <div id="notice-${notice['msg_id']}" style="display: none">
721 721 ${h.render(notice['body'], renderer='markdown')}
722 722 </div>
723 723 </td>
724 724 <td style="vertical-align: text-top; width: 35px;">
725 725 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
726 726 <i class="icon-remove icon-filled-red"></i>
727 727 </a>
728 728 </td>
729 729 </tr>
730 730
731 731 % endfor
732 732 </table>
733 733 </div>
734 734 </div>
735 735 ## Main filter
736 736 <li>
737 737 <div class="menulabel main_filter_box">
738 738 <div class="main_filter_input_box">
739 739 <ul class="searchItems">
740 740
741 741 <li class="searchTag searchTagIcon">
742 742 <i class="icon-search"></i>
743 743 </li>
744 744
745 745 % if c.template_context['search_context']['repo_id']:
746 746 <li class="searchTag searchTagFilter searchTagHidable" >
747 747 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
748 748 <span class="tag">
749 749 This repo
750 750 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
751 751 </span>
752 752 ##</a>
753 753 </li>
754 754 % elif c.template_context['search_context']['repo_group_id']:
755 755 <li class="searchTag searchTagFilter searchTagHidable">
756 756 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
757 757 <span class="tag">
758 758 This group
759 759 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
760 760 </span>
761 761 ##</a>
762 762 </li>
763 763 % endif
764 764
765 765 <li class="searchTagInput">
766 766 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
767 767 </li>
768 768 <li class="searchTag searchTagHelp">
769 769 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
770 770 </li>
771 771 </ul>
772 772 </div>
773 773 </div>
774 774
775 775 <div id="main_filter_help" style="display: none">
776 776 - Use '/' key to quickly access this field.
777 777
778 778 - Enter a name of repository, or repository group for quick search.
779 779
780 780 - Prefix query to allow special search:
781 781
782 782 user:admin, to search for usernames, always global
783 783
784 784 user_group:devops, to search for user groups, always global
785 785
786 786 pr:303, to search for pull request number, title, or description, always global
787 787
788 788 commit:efced4, to search for commits, scoped to repositories or groups
789 789
790 790 file:models.py, to search for file paths, scoped to repositories or groups
791 791
792 792 % if c.template_context['search_context']['repo_id']:
793 793 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
794 794 % elif c.template_context['search_context']['repo_group_id']:
795 795 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
796 796 % else:
797 797 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
798 798 % endif
799 799 </div>
800 800 </li>
801 801
802 802 ## ROOT MENU
803 803 <li class="${h.is_active('home', active)}">
804 804 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
805 805 <div class="menulabel">${_('Home')}</div>
806 806 </a>
807 807 </li>
808 808
809 809 %if c.rhodecode_user.username != h.DEFAULT_USER:
810 810 <li class="${h.is_active('journal', active)}">
811 811 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
812 812 <div class="menulabel">${_('Journal')}</div>
813 813 </a>
814 814 </li>
815 815 %else:
816 816 <li class="${h.is_active('journal', active)}">
817 817 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
818 818 <div class="menulabel">${_('Public journal')}</div>
819 819 </a>
820 820 </li>
821 821 %endif
822 822
823 823 <li class="${h.is_active('gists', active)}">
824 824 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
825 825 <div class="menulabel">${_('Gists')}</div>
826 826 </a>
827 827 </li>
828 828
829 829 % if c.is_super_admin or c.is_delegated_admin:
830 830 <li class="${h.is_active('admin', active)}">
831 831 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
832 832 <div class="menulabel">${_('Admin')} </div>
833 833 </a>
834 834 </li>
835 835 % endif
836 836
837 837 ## render extra user menu
838 838 ${usermenu(active=(active=='my_account'))}
839 839
840 840 </ul>
841 841
842 842 <script type="text/javascript">
843 843 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
844 844
845 845 var formatRepoResult = function(result, container, query, escapeMarkup) {
846 846 return function(data, escapeMarkup) {
847 847 if (!data.repo_id){
848 848 return data.text; // optgroup text Repositories
849 849 }
850 850
851 851 var tmpl = '';
852 852 var repoType = data['repo_type'];
853 853 var repoName = data['text'];
854 854
855 855 if(data && data.type == 'repo'){
856 856 if(repoType === 'hg'){
857 857 tmpl += '<i class="icon-hg"></i> ';
858 858 }
859 859 else if(repoType === 'git'){
860 860 tmpl += '<i class="icon-git"></i> ';
861 861 }
862 862 else if(repoType === 'svn'){
863 863 tmpl += '<i class="icon-svn"></i> ';
864 864 }
865 865 if(data['private']){
866 866 tmpl += '<i class="icon-lock" ></i> ';
867 867 }
868 868 else if(visualShowPublicIcon){
869 869 tmpl += '<i class="icon-unlock-alt"></i> ';
870 870 }
871 871 }
872 872 tmpl += escapeMarkup(repoName);
873 873 return tmpl;
874 874
875 875 }(result, escapeMarkup);
876 876 };
877 877
878 878 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
879 879 return function(data, escapeMarkup) {
880 880 if (!data.repo_group_id){
881 881 return data.text; // optgroup text Repositories
882 882 }
883 883
884 884 var tmpl = '';
885 885 var repoGroupName = data['text'];
886 886
887 887 if(data){
888 888
889 889 tmpl += '<i class="icon-repo-group"></i> ';
890 890
891 891 }
892 892 tmpl += escapeMarkup(repoGroupName);
893 893 return tmpl;
894 894
895 895 }(result, escapeMarkup);
896 896 };
897 897
898 898 var escapeRegExChars = function (value) {
899 899 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
900 900 };
901 901
902 902 var getRepoIcon = function(repo_type) {
903 903 if (repo_type === 'hg') {
904 904 return '<i class="icon-hg"></i> ';
905 905 }
906 906 else if (repo_type === 'git') {
907 907 return '<i class="icon-git"></i> ';
908 908 }
909 909 else if (repo_type === 'svn') {
910 910 return '<i class="icon-svn"></i> ';
911 911 }
912 912 return ''
913 913 };
914 914
915 915 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
916 916
917 917 if (value.split(':').length === 2) {
918 918 value = value.split(':')[1]
919 919 }
920 920
921 921 var searchType = data['type'];
922 922 var searchSubType = data['subtype'];
923 923 var valueDisplay = data['value_display'];
924 924 var valueIcon = data['value_icon'];
925 925
926 926 var pattern = '(' + escapeRegExChars(value) + ')';
927 927
928 928 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
929 929
930 930 // highlight match
931 931 if (searchType != 'text') {
932 932 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
933 933 }
934 934
935 935 var icon = '';
936 936
937 937 if (searchType === 'hint') {
938 938 icon += '<i class="icon-repo-group"></i> ';
939 939 }
940 940 // full text search/hints
941 941 else if (searchType === 'search') {
942 942 if (valueIcon === undefined) {
943 943 icon += '<i class="icon-more"></i> ';
944 944 } else {
945 945 icon += valueIcon + ' ';
946 946 }
947 947
948 948 if (searchSubType !== undefined && searchSubType == 'repo') {
949 949 valueDisplay += '<div class="pull-right tag">repository</div>';
950 950 }
951 951 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
952 952 valueDisplay += '<div class="pull-right tag">repo group</div>';
953 953 }
954 954 }
955 955 // repository
956 956 else if (searchType === 'repo') {
957 957
958 958 var repoIcon = getRepoIcon(data['repo_type']);
959 959 icon += repoIcon;
960 960
961 961 if (data['private']) {
962 962 icon += '<i class="icon-lock" ></i> ';
963 963 }
964 964 else if (visualShowPublicIcon) {
965 965 icon += '<i class="icon-unlock-alt"></i> ';
966 966 }
967 967 }
968 968 // repository groups
969 969 else if (searchType === 'repo_group') {
970 970 icon += '<i class="icon-repo-group"></i> ';
971 971 }
972 972 // user group
973 973 else if (searchType === 'user_group') {
974 974 icon += '<i class="icon-group"></i> ';
975 975 }
976 976 // user
977 977 else if (searchType === 'user') {
978 978 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
979 979 }
980 980 // pull request
981 981 else if (searchType === 'pull_request') {
982 982 icon += '<i class="icon-merge"></i> ';
983 983 }
984 984 // commit
985 985 else if (searchType === 'commit') {
986 986 var repo_data = data['repo_data'];
987 987 var repoIcon = getRepoIcon(repo_data['repository_type']);
988 988 if (repoIcon) {
989 989 icon += repoIcon;
990 990 } else {
991 991 icon += '<i class="icon-tag"></i>';
992 992 }
993 993 }
994 994 // file
995 995 else if (searchType === 'file') {
996 996 var repo_data = data['repo_data'];
997 997 var repoIcon = getRepoIcon(repo_data['repository_type']);
998 998 if (repoIcon) {
999 999 icon += repoIcon;
1000 1000 } else {
1001 1001 icon += '<i class="icon-tag"></i>';
1002 1002 }
1003 1003 }
1004 1004 // generic text
1005 1005 else if (searchType === 'text') {
1006 1006 icon = '';
1007 1007 }
1008 1008
1009 1009 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1010 1010 return tmpl.format(icon, valueDisplay);
1011 1011 };
1012 1012
1013 1013 var handleSelect = function(element, suggestion) {
1014 1014 if (suggestion.type === "hint") {
1015 1015 // we skip action
1016 1016 $('#main_filter').focus();
1017 1017 }
1018 1018 else if (suggestion.type === "text") {
1019 1019 // we skip action
1020 1020 $('#main_filter').focus();
1021 1021
1022 1022 } else {
1023 1023 window.location = suggestion['url'];
1024 1024 }
1025 1025 };
1026 1026
1027 1027 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1028 1028 if (queryLowerCase.split(':').length === 2) {
1029 1029 queryLowerCase = queryLowerCase.split(':')[1]
1030 1030 }
1031 1031 if (suggestion.type === "text") {
1032 1032 // special case we don't want to "skip" display for
1033 1033 return true
1034 1034 }
1035 1035 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1036 1036 };
1037 1037
1038 1038 var cleanContext = {
1039 1039 repo_view_type: null,
1040 1040
1041 1041 repo_id: null,
1042 1042 repo_name: "",
1043 1043
1044 1044 repo_group_id: null,
1045 1045 repo_group_name: null
1046 1046 };
1047 1047 var removeGoToFilter = function () {
1048 1048 $('.searchTagHidable').hide();
1049 1049 $('#main_filter').autocomplete(
1050 1050 'setOptions', {params:{search_context: cleanContext}});
1051 1051 };
1052 1052
1053 1053 $('#main_filter').autocomplete({
1054 1054 serviceUrl: pyroutes.url('goto_switcher_data'),
1055 1055 params: {
1056 1056 "search_context": templateContext.search_context
1057 1057 },
1058 1058 minChars:2,
1059 1059 maxHeight:400,
1060 1060 deferRequestBy: 300, //miliseconds
1061 1061 tabDisabled: true,
1062 1062 autoSelectFirst: false,
1063 1063 containerClass: 'autocomplete-qfilter-suggestions',
1064 1064 formatResult: autocompleteMainFilterFormatResult,
1065 1065 lookupFilter: autocompleteMainFilterResult,
1066 1066 onSelect: function (element, suggestion) {
1067 1067 handleSelect(element, suggestion);
1068 1068 return false;
1069 1069 },
1070 1070 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1071 1071 if (jqXHR !== 'abort') {
1072 alert("Error during search.\nError code: {0}".format(textStatus));
1073 window.location = '';
1072 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1073 SwalNoAnimation.fire({
1074 icon: 'error',
1075 title: _gettext('Error during search operation'),
1076 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1077 }).then(function(result) {
1078 window.location.reload();
1079 })
1074 1080 }
1075 1081 },
1076 1082 onSearchStart: function (params) {
1077 1083 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1078 1084 },
1079 1085 onSearchComplete: function (query, suggestions) {
1080 1086 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1081 1087 },
1082 1088 });
1083 1089
1084 1090 showMainFilterBox = function () {
1085 1091 $('#main_filter_help').toggle();
1086 1092 };
1087 1093
1088 1094 $('#main_filter').on('keydown.autocomplete', function (e) {
1089 1095
1090 1096 var BACKSPACE = 8;
1091 1097 var el = $(e.currentTarget);
1092 1098 if(e.which === BACKSPACE){
1093 1099 var inputVal = el.val();
1094 1100 if (inputVal === ""){
1095 1101 removeGoToFilter()
1096 1102 }
1097 1103 }
1098 1104 });
1099 1105
1100 1106 var dismissNotice = function(noticeId) {
1101 1107
1102 1108 var url = pyroutes.url('user_notice_dismiss',
1103 1109 {"user_id": templateContext.rhodecode_user.user_id});
1104 1110
1105 1111 var postData = {
1106 1112 'csrf_token': CSRF_TOKEN,
1107 1113 'notice_id': noticeId,
1108 1114 };
1109 1115
1110 1116 var success = function(response) {
1111 1117 $('#notice-message-' + noticeId).remove();
1112 1118 return false;
1113 1119 };
1114 1120 var failure = function(data, textStatus, xhr) {
1115 1121 alert("error processing request: " + textStatus);
1116 1122 return false;
1117 1123 };
1118 1124 ajaxPOST(url, postData, success, failure);
1119 1125 }
1120 1126 </script>
1121 1127 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1122 1128 </%def>
1123 1129
1124 1130 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1125 1131 <div class="modal-dialog">
1126 1132 <div class="modal-content">
1127 1133 <div class="modal-header">
1128 1134 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1129 1135 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1130 1136 </div>
1131 1137 <div class="modal-body">
1132 1138 <div class="block-left">
1133 1139 <table class="keyboard-mappings">
1134 1140 <tbody>
1135 1141 <tr>
1136 1142 <th></th>
1137 1143 <th>${_('Site-wide shortcuts')}</th>
1138 1144 </tr>
1139 1145 <%
1140 1146 elems = [
1141 1147 ('/', 'Use quick search box'),
1142 1148 ('g h', 'Goto home page'),
1143 1149 ('g g', 'Goto my private gists page'),
1144 1150 ('g G', 'Goto my public gists page'),
1145 1151 ('g 0-9', 'Goto bookmarked items from 0-9'),
1146 1152 ('n r', 'New repository page'),
1147 1153 ('n g', 'New gist page'),
1148 1154 ]
1149 1155 %>
1150 1156 %for key, desc in elems:
1151 1157 <tr>
1152 1158 <td class="keys">
1153 1159 <span class="key tag">${key}</span>
1154 1160 </td>
1155 1161 <td>${desc}</td>
1156 1162 </tr>
1157 1163 %endfor
1158 1164 </tbody>
1159 1165 </table>
1160 1166 </div>
1161 1167 <div class="block-left">
1162 1168 <table class="keyboard-mappings">
1163 1169 <tbody>
1164 1170 <tr>
1165 1171 <th></th>
1166 1172 <th>${_('Repositories')}</th>
1167 1173 </tr>
1168 1174 <%
1169 1175 elems = [
1170 1176 ('g s', 'Goto summary page'),
1171 1177 ('g c', 'Goto changelog page'),
1172 1178 ('g f', 'Goto files page'),
1173 1179 ('g F', 'Goto files page with file search activated'),
1174 1180 ('g p', 'Goto pull requests page'),
1175 1181 ('g o', 'Goto repository settings'),
1176 1182 ('g O', 'Goto repository access permissions settings'),
1177 1183 ]
1178 1184 %>
1179 1185 %for key, desc in elems:
1180 1186 <tr>
1181 1187 <td class="keys">
1182 1188 <span class="key tag">${key}</span>
1183 1189 </td>
1184 1190 <td>${desc}</td>
1185 1191 </tr>
1186 1192 %endfor
1187 1193 </tbody>
1188 1194 </table>
1189 1195 </div>
1190 1196 </div>
1191 1197 <div class="modal-footer">
1192 1198 </div>
1193 1199 </div><!-- /.modal-content -->
1194 1200 </div><!-- /.modal-dialog -->
1195 1201 </div><!-- /.modal -->
General Comments 0
You need to be logged in to leave comments. Login now