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