##// END OF EJS Templates
comments: register different slash commands for inline vs general comments.
marcink -
r1362:da1547bc default
parent child Browse files
Show More
@@ -1,582 +1,594 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * Code Mirror
21 21 */
22 22 // global code-mirror logger;, to enable run
23 23 // Logger.get('CodeMirror').setLevel(Logger.DEBUG)
24 24
25 25 cmLog = Logger.get('CodeMirror');
26 26 cmLog.setLevel(Logger.OFF);
27 27
28 28
29 29 //global cache for inline forms
30 30 var userHintsCache = {};
31 31
32 32 // global timer, used to cancel async loading
33 33 var CodeMirrorLoadUserHintTimer;
34 34
35 35 var escapeRegExChars = function(value) {
36 36 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
37 37 };
38 38
39 39 /**
40 40 * Load hints from external source returns an array of objects in a format
41 41 * that hinting lib requires
42 42 * @returns {Array}
43 43 */
44 44 var CodeMirrorLoadUserHints = function(query, triggerHints) {
45 45 cmLog.debug('Loading mentions users via AJAX');
46 46 var _users = [];
47 47 $.ajax({
48 48 type: 'GET',
49 49 data: {query: query},
50 50 url: pyroutes.url('user_autocomplete_data'),
51 51 headers: {'X-PARTIAL-XHR': true},
52 52 async: true
53 53 })
54 54 .done(function(data) {
55 55 var tmpl = '<img class="gravatar" src="{0}"/>{1}';
56 56 $.each(data.suggestions, function(i) {
57 57 var userObj = data.suggestions[i];
58 58
59 59 if (userObj.username !== "default") {
60 60 _users.push({
61 61 text: userObj.username + " ",
62 62 org_text: userObj.username,
63 63 displayText: userObj.value_display, // search that field
64 64 // internal caches
65 65 _icon_link: userObj.icon_link,
66 66 _text: userObj.value_display,
67 67
68 68 render: function(elt, data, completion) {
69 69 var el = document.createElement('div');
70 70 el.className = "CodeMirror-hint-entry";
71 71 el.innerHTML = tmpl.format(
72 72 completion._icon_link, completion._text);
73 73 elt.appendChild(el);
74 74 }
75 75 });
76 76 }
77 77 });
78 78 cmLog.debug('Mention users loaded');
79 79 // set to global cache
80 80 userHintsCache[query] = _users;
81 81 triggerHints(userHintsCache[query]);
82 82 })
83 83 .fail(function(data, textStatus, xhr) {
84 84 alert("error processing request: " + textStatus);
85 85 });
86 86 };
87 87
88 88 /**
89 89 * filters the results based on the current context
90 90 * @param users
91 91 * @param context
92 92 * @returns {Array}
93 93 */
94 94 var CodeMirrorFilterUsers = function(users, context) {
95 95 var MAX_LIMIT = 10;
96 96 var filtered_users = [];
97 97 var curWord = context.string;
98 98
99 99 cmLog.debug('Filtering users based on query:', curWord);
100 100 $.each(users, function(i) {
101 101 var match = users[i];
102 102 var searchText = match.displayText;
103 103
104 104 if (!curWord ||
105 105 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
106 106 // reset state
107 107 match._text = match.displayText;
108 108 if (curWord) {
109 109 // do highlighting
110 110 var pattern = '(' + escapeRegExChars(curWord) + ')';
111 111 match._text = searchText.replace(
112 112 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
113 113 }
114 114
115 115 filtered_users.push(match);
116 116 }
117 117 // to not return to many results, use limit of filtered results
118 118 if (filtered_users.length > MAX_LIMIT) {
119 119 return false;
120 120 }
121 121 });
122 122
123 123 return filtered_users;
124 124 };
125 125
126 126 var CodeMirrorMentionHint = function(editor, callback, options) {
127 127 var cur = editor.getCursor();
128 128 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
129 129
130 130 // match on @ +1char
131 131 var tokenMatch = new RegExp(
132 132 '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine);
133 133
134 134 var tokenStr = '';
135 135 if (tokenMatch !== null && tokenMatch.length > 0){
136 136 tokenStr = tokenMatch[0].strip();
137 137 } else {
138 138 // skip if we didn't match our token
139 139 return;
140 140 }
141 141
142 142 var context = {
143 143 start: (cur.ch - tokenStr.length) + 1,
144 144 end: cur.ch,
145 145 string: tokenStr.slice(1),
146 146 type: null
147 147 };
148 148
149 149 // case when we put the @sign in fron of a string,
150 150 // eg <@ we put it here>sometext then we need to prepend to text
151 151 if (context.end > cur.ch) {
152 152 context.start = context.start + 1; // we add to the @ sign
153 153 context.end = cur.ch; // don't eat front part just append
154 154 context.string = context.string.slice(1, cur.ch - context.start);
155 155 }
156 156
157 157 cmLog.debug('Mention context', context);
158 158
159 159 var triggerHints = function(userHints){
160 160 return callback({
161 161 list: CodeMirrorFilterUsers(userHints, context),
162 162 from: CodeMirror.Pos(cur.line, context.start),
163 163 to: CodeMirror.Pos(cur.line, context.end)
164 164 });
165 165 };
166 166
167 167 var queryBasedHintsCache = undefined;
168 168 // if we have something in the cache, try to fetch the query based cache
169 169 if (userHintsCache !== {}){
170 170 queryBasedHintsCache = userHintsCache[context.string];
171 171 }
172 172
173 173 if (queryBasedHintsCache !== undefined) {
174 174 cmLog.debug('Users loaded from cache');
175 175 triggerHints(queryBasedHintsCache);
176 176 } else {
177 177 // this takes care for async loading, and then displaying results
178 178 // and also propagates the userHintsCache
179 179 window.clearTimeout(CodeMirrorLoadUserHintTimer);
180 180 CodeMirrorLoadUserHintTimer = setTimeout(function() {
181 181 CodeMirrorLoadUserHints(context.string, triggerHints);
182 182 }, 300);
183 183 }
184 184 };
185 185
186 186 var CodeMirrorCompleteAfter = function(cm, pred) {
187 187 var options = {
188 188 completeSingle: false,
189 189 async: true,
190 190 closeOnUnfocus: true
191 191 };
192 192 var cur = cm.getCursor();
193 193 setTimeout(function() {
194 194 if (!cm.state.completionActive) {
195 195 cmLog.debug('Trigger mentions hinting');
196 196 CodeMirror.showHint(cm, CodeMirror.hint.mentions, options);
197 197 }
198 198 }, 100);
199 199
200 200 // tell CodeMirror we didn't handle the key
201 201 // trick to trigger on a char but still complete it
202 202 return CodeMirror.Pass;
203 203 };
204 204
205 205 var initCodeMirror = function(textAreadId, resetUrl, focus, options) {
206 206 var ta = $('#' + textAreadId).get(0);
207 207 if (focus === undefined) {
208 208 focus = true;
209 209 }
210 210
211 211 // default options
212 212 var codeMirrorOptions = {
213 213 mode: "null",
214 214 lineNumbers: true,
215 215 indentUnit: 4,
216 216 autofocus: focus
217 217 };
218 218
219 219 if (options !== undefined) {
220 220 // extend with custom options
221 221 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
222 222 }
223 223
224 224 var myCodeMirror = CodeMirror.fromTextArea(ta, codeMirrorOptions);
225 225
226 226 $('#reset').on('click', function(e) {
227 227 window.location = resetUrl;
228 228 });
229 229
230 230 return myCodeMirror;
231 231 };
232 232
233 var initCommentBoxCodeMirror = function(textAreaId, triggerActions){
233 var initCommentBoxCodeMirror = function(CommentForm, textAreaId, triggerActions){
234 234 var initialHeight = 100;
235 235
236 236 if (typeof userHintsCache === "undefined") {
237 237 userHintsCache = {};
238 238 cmLog.debug('Init empty cache for mentions');
239 239 }
240 240 if (!$(textAreaId).get(0)) {
241 241 cmLog.debug('Element for textarea not found', textAreaId);
242 242 return;
243 243 }
244 244 /**
245 245 * Filter action based on typed in text
246 246 * @param actions
247 247 * @param context
248 248 * @returns {Array}
249 249 */
250 250
251 251 var filterActions = function(actions, context){
252 252
253 253 var MAX_LIMIT = 10;
254 254 var filtered_actions = [];
255 255 var curWord = context.string;
256 256
257 257 cmLog.debug('Filtering actions based on query:', curWord);
258 258 $.each(actions, function(i) {
259 259 var match = actions[i];
260 260 var searchText = match.searchText;
261 261
262 262 if (!curWord ||
263 263 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
264 264 // reset state
265 265 match._text = match.displayText;
266 266 if (curWord) {
267 267 // do highlighting
268 268 var pattern = '(' + escapeRegExChars(curWord) + ')';
269 269 match._text = searchText.replace(
270 270 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
271 271 }
272 272
273 273 filtered_actions.push(match);
274 274 }
275 275 // to not return to many results, use limit of filtered results
276 276 if (filtered_actions.length > MAX_LIMIT) {
277 277 return false;
278 278 }
279 279 });
280 280
281 281 return filtered_actions;
282 282 };
283 283
284 284 var submitForm = function(cm, pred) {
285 285 $(cm.display.input.textarea.form).submit();
286 286 return CodeMirror.Pass;
287 287 };
288 288
289 289 var completeActions = function(actions){
290 290
291 var registeredActions = [];
292 var allActions = [
293 {
294 text: "approve",
295 searchText: "status approved",
296 displayText: _gettext('Set status to Approved'),
297 hint: function(CodeMirror, data, completion) {
298 CodeMirror.replaceRange("", completion.from || data.from,
299 completion.to || data.to, "complete");
300 $(CommentForm.statusChange).select2("val", 'approved').trigger('change');
301 },
302 render: function(elt, data, completion) {
303 var el = document.createElement('div');
304 el.className = "flag_status flag_status_comment_box approved pull-left";
305 elt.appendChild(el);
306
307 el = document.createElement('span');
308 el.innerHTML = completion.displayText;
309 elt.appendChild(el);
310 }
311 },
312 {
313 text: "reject",
314 searchText: "status rejected",
315 displayText: _gettext('Set status to Rejected'),
316 hint: function(CodeMirror, data, completion) {
317 CodeMirror.replaceRange("", completion.from || data.from,
318 completion.to || data.to, "complete");
319 $(CommentForm.statusChange).select2("val", 'rejected').trigger('change');
320 },
321 render: function(elt, data, completion) {
322 var el = document.createElement('div');
323 el.className = "flag_status flag_status_comment_box rejected pull-left";
324 elt.appendChild(el);
325
326 el = document.createElement('span');
327 el.innerHTML = completion.displayText;
328 elt.appendChild(el);
329 }
330 },
331 {
332 text: "as_todo",
333 searchText: "todo comment",
334 displayText: _gettext('TODO comment'),
335 hint: function(CodeMirror, data, completion) {
336 CodeMirror.replaceRange("", completion.from || data.from,
337 completion.to || data.to, "complete");
338
339 $(CommentForm.commentType).val('todo');
340 },
341 render: function(elt, data, completion) {
342 var el = document.createElement('div');
343 el.className = "pull-left";
344 elt.appendChild(el);
345
346 el = document.createElement('span');
347 el.innerHTML = completion.displayText;
348 elt.appendChild(el);
349 }
350 },
351 {
352 text: "as_note",
353 searchText: "note comment",
354 displayText: _gettext('Note Comment'),
355 hint: function(CodeMirror, data, completion) {
356 CodeMirror.replaceRange("", completion.from || data.from,
357 completion.to || data.to, "complete");
358
359 $(CommentForm.commentType).val('note');
360 },
361 render: function(elt, data, completion) {
362 var el = document.createElement('div');
363 el.className = "pull-left";
364 elt.appendChild(el);
365
366 el = document.createElement('span');
367 el.innerHTML = completion.displayText;
368 elt.appendChild(el);
369 }
370 }
371 ];
372
373 $.each(allActions, function(index, value){
374 var actionData = allActions[index];
375 if (actions.indexOf(actionData['text']) != -1) {
376 registeredActions.push(actionData);
377 }
378 });
379
291 380 return function(cm, pred) {
292 381 var cur = cm.getCursor();
293 382 var options = {
294 closeOnUnfocus: true
383 closeOnUnfocus: true,
384 registeredActions: registeredActions
295 385 };
296 386 setTimeout(function() {
297 387 if (!cm.state.completionActive) {
298 388 cmLog.debug('Trigger actions hinting');
299 389 CodeMirror.showHint(cm, CodeMirror.hint.actions, options);
300 390 }
301 391 }, 100);
302 392
303 393 // tell CodeMirror we didn't handle the key
304 394 // trick to trigger on a char but still complete it
305 395 return CodeMirror.Pass;
306 396 }
307 397 };
308 398
309 399 var extraKeys = {
310 400 "'@'": CodeMirrorCompleteAfter,
311 401 Tab: function(cm) {
312 402 // space indent instead of TABS
313 403 var spaces = new Array(cm.getOption("indentUnit") + 1).join(" ");
314 404 cm.replaceSelection(spaces);
315 405 }
316 406 };
317 407 // submit form on Meta-Enter
318 408 if (OSType === "mac") {
319 409 extraKeys["Cmd-Enter"] = submitForm;
320 410 }
321 411 else {
322 412 extraKeys["Ctrl-Enter"] = submitForm;
323 413 }
324 414
325 415 if (triggerActions) {
326 416 // register triggerActions for this instance
327 417 extraKeys["'/'"] = completeActions(triggerActions);
328 418 }
329 419
330 420 var cm = CodeMirror.fromTextArea($(textAreaId).get(0), {
331 421 lineNumbers: false,
332 422 indentUnit: 4,
333 423 viewportMargin: 30,
334 424 // this is a trick to trigger some logic behind codemirror placeholder
335 425 // it influences styling and behaviour.
336 426 placeholder: " ",
337 427 extraKeys: extraKeys,
338 428 lineWrapping: true
339 429 });
340 430
341 431 cm.setSize(null, initialHeight);
342 432 cm.setOption("mode", DEFAULT_RENDERER);
343 433 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
344 434 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
345 435 // start listening on changes to make auto-expanded editor
346 436 cm.on("change", function(self) {
347 437 var height = initialHeight;
348 438 var lines = self.lineCount();
349 439 if ( lines > 6 && lines < 20) {
350 440 height = "auto";
351 441 }
352 442 else if (lines >= 20){
353 443 zheight = 20*15;
354 444 }
355 445 self.setSize(null, height);
356 446 });
357 447
358 448 var actionHint = function(editor, options) {
449
359 450 var cur = editor.getCursor();
360 451 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
361 452
362 453 // match only on /+1 character minimum
363 454 var tokenMatch = new RegExp('(^/\|/\)([a-zA-Z]*)$').exec(curLine);
364 455
365 456 var tokenStr = '';
366 457 if (tokenMatch !== null && tokenMatch.length > 0){
367 458 tokenStr = tokenMatch[2].strip();
368 459 }
369 460
370 461 var context = {
371 462 start: (cur.ch - tokenStr.length) - 1,
372 463 end: cur.ch,
373 464 string: tokenStr,
374 465 type: null
375 466 };
376 467
377 var actions = [
378 {
379 text: "approve",
380 searchText: "status approved",
381 displayText: _gettext('Set status to Approved'),
382 hint: function(CodeMirror, data, completion) {
383 CodeMirror.replaceRange("", completion.from || data.from,
384 completion.to || data.to, "complete");
385 $('#change_status_general').select2("val", 'approved').trigger('change');
386 },
387 render: function(elt, data, completion) {
388 var el = document.createElement('div');
389 el.className = "flag_status flag_status_comment_box approved pull-left";
390 elt.appendChild(el);
391
392 el = document.createElement('span');
393 el.innerHTML = completion.displayText;
394 elt.appendChild(el);
395 }
396 },
397 {
398 text: "reject",
399 searchText: "status rejected",
400 displayText: _gettext('Set status to Rejected'),
401 hint: function(CodeMirror, data, completion) {
402 CodeMirror.replaceRange("", completion.from || data.from,
403 completion.to || data.to, "complete");
404 $('#change_status_general').select2("val", 'rejected').trigger('change');
405 },
406 render: function(elt, data, completion) {
407 var el = document.createElement('div');
408 el.className = "flag_status flag_status_comment_box rejected pull-left";
409 elt.appendChild(el);
410
411 el = document.createElement('span');
412 el.innerHTML = completion.displayText;
413 elt.appendChild(el);
414 }
415 },
416 {
417 text: "as_todo",
418 searchText: "todo comment",
419 displayText: _gettext('TODO comment'),
420 hint: function(CodeMirror, data, completion) {
421 CodeMirror.replaceRange("", completion.from || data.from,
422 completion.to || data.to, "complete");
423 $('#comment_type_general').val('todo')
424 },
425 render: function(elt, data, completion) {
426 var el = document.createElement('div');
427 el.className = "pull-left";
428 elt.appendChild(el);
429
430 el = document.createElement('span');
431 el.innerHTML = completion.displayText;
432 elt.appendChild(el);
433 }
434 },
435 {
436 text: "as_note",
437 searchText: "note comment",
438 displayText: _gettext('Note Comment'),
439 hint: function(CodeMirror, data, completion) {
440 CodeMirror.replaceRange("", completion.from || data.from,
441 completion.to || data.to, "complete");
442 $('#comment_type_general').val('note')
443 },
444 render: function(elt, data, completion) {
445 var el = document.createElement('div');
446 el.className = "pull-left";
447 elt.appendChild(el);
448
449 el = document.createElement('span');
450 el.innerHTML = completion.displayText;
451 elt.appendChild(el);
452 }
453 }
454 ];
455
456 468 return {
457 list: filterActions(actions, context),
469 list: filterActions(options.registeredActions, context),
458 470 from: CodeMirror.Pos(cur.line, context.start),
459 471 to: CodeMirror.Pos(cur.line, context.end)
460 472 };
461 473
462 474 };
463 475 CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint);
464 476 CodeMirror.registerHelper("hint", "actions", actionHint);
465 477 return cm;
466 478 };
467 479
468 480 var setCodeMirrorMode = function(codeMirrorInstance, mode) {
469 481 CodeMirror.autoLoadMode(codeMirrorInstance, mode);
470 482 codeMirrorInstance.setOption("mode", mode);
471 483 };
472 484
473 485 var setCodeMirrorLineWrap = function(codeMirrorInstance, line_wrap) {
474 486 codeMirrorInstance.setOption("lineWrapping", line_wrap);
475 487 };
476 488
477 489 var setCodeMirrorModeFromSelect = function(
478 490 targetSelect, targetFileInput, codeMirrorInstance, callback){
479 491
480 492 $(targetSelect).on('change', function(e) {
481 493 cmLog.debug('codemirror select2 mode change event !');
482 494 var selected = e.currentTarget;
483 495 var node = selected.options[selected.selectedIndex];
484 496 var mimetype = node.value;
485 497 cmLog.debug('picked mimetype', mimetype);
486 498 var new_mode = $(node).attr('mode');
487 499 setCodeMirrorMode(codeMirrorInstance, new_mode);
488 500 cmLog.debug('set new mode', new_mode);
489 501
490 502 //propose filename from picked mode
491 503 cmLog.debug('setting mimetype', mimetype);
492 504 var proposed_ext = getExtFromMimeType(mimetype);
493 505 cmLog.debug('file input', $(targetFileInput).val());
494 506 var file_data = getFilenameAndExt($(targetFileInput).val());
495 507 var filename = file_data.filename || 'filename1';
496 508 $(targetFileInput).val(filename + proposed_ext);
497 509 cmLog.debug('proposed file', filename + proposed_ext);
498 510
499 511
500 512 if (typeof(callback) === 'function') {
501 513 try {
502 514 cmLog.debug('running callback', callback);
503 515 callback(filename, mimetype, new_mode);
504 516 } catch (err) {
505 517 console.log('failed to run callback', callback, err);
506 518 }
507 519 }
508 520 cmLog.debug('finish iteration...');
509 521 });
510 522 };
511 523
512 524 var setCodeMirrorModeFromInput = function(
513 525 targetSelect, targetFileInput, codeMirrorInstance, callback) {
514 526
515 527 // on type the new filename set mode
516 528 $(targetFileInput).on('keyup', function(e) {
517 529 var file_data = getFilenameAndExt(this.value);
518 530 if (file_data.ext === null) {
519 531 return;
520 532 }
521 533
522 534 var mimetypes = getMimeTypeFromExt(file_data.ext, true);
523 535 cmLog.debug('mimetype from file', file_data, mimetypes);
524 536 var detected_mode;
525 537 var detected_option;
526 538 for (var i in mimetypes) {
527 539 var mt = mimetypes[i];
528 540 if (!detected_mode) {
529 541 detected_mode = detectCodeMirrorMode(this.value, mt);
530 542 }
531 543
532 544 if (!detected_option) {
533 545 cmLog.debug('#mimetype option[value="{0}"]'.format(mt));
534 546 if ($(targetSelect).find('option[value="{0}"]'.format(mt)).length) {
535 547 detected_option = mt;
536 548 }
537 549 }
538 550 }
539 551
540 552 cmLog.debug('detected mode', detected_mode);
541 553 cmLog.debug('detected option', detected_option);
542 554 if (detected_mode && detected_option){
543 555
544 556 $(targetSelect).select2("val", detected_option);
545 557 setCodeMirrorMode(codeMirrorInstance, detected_mode);
546 558
547 559 if(typeof(callback) === 'function'){
548 560 try{
549 561 cmLog.debug('running callback', callback);
550 562 var filename = file_data.filename + "." + file_data.ext;
551 563 callback(filename, detected_option, detected_mode);
552 564 }catch (err){
553 565 console.log('failed to run callback', callback, err);
554 566 }
555 567 }
556 568 }
557 569
558 570 });
559 571 };
560 572
561 573 var fillCodeMirrorOptions = function(targetSelect) {
562 574 //inject new modes, based on codeMirrors modeInfo object
563 575 var modes_select = $(targetSelect);
564 576 for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
565 577 var m = CodeMirror.modeInfo[i];
566 578 var opt = new Option(m.name, m.mime);
567 579 $(opt).attr('mode', m.mode);
568 580 modes_select.append(opt);
569 581 }
570 582 };
571 583
572 584 var CodeMirrorPreviewEnable = function(edit_mode) {
573 585 // in case it a preview enabled mode enable the button
574 586 if (['markdown', 'rst', 'gfm'].indexOf(edit_mode) !== -1) {
575 587 $('#render_preview').removeClass('hidden');
576 588 }
577 589 else {
578 590 if (!$('#render_preview').hasClass('hidden')) {
579 591 $('#render_preview').addClass('hidden');
580 592 }
581 593 }
582 594 };
@@ -1,808 +1,809 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45 /* Comment form for main and inline comments */
46 46 (function(mod) {
47 47
48 48 if (typeof exports == "object" && typeof module == "object") {
49 49 // CommonJS
50 50 module.exports = mod();
51 51 }
52 52 else {
53 53 // Plain browser env
54 54 (this || window).CommentForm = mod();
55 55 }
56 56
57 57 })(function() {
58 58 "use strict";
59 59
60 60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 61 if (!(this instanceof CommentForm)) {
62 62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 63 }
64 64
65 65 // bind the element instance to our Form
66 66 $(formElement).get(0).CommentForm = this;
67 67
68 68 this.withLineNo = function(selector) {
69 69 var lineNo = this.lineNo;
70 70 if (lineNo === undefined) {
71 71 return selector
72 72 } else {
73 73 return selector + '_' + lineNo;
74 74 }
75 75 };
76 76
77 77 this.commitId = commitId;
78 78 this.pullRequestId = pullRequestId;
79 79 this.lineNo = lineNo;
80 80 this.initAutocompleteActions = initAutocompleteActions;
81 81
82 82 this.previewButton = this.withLineNo('#preview-btn');
83 83 this.previewContainer = this.withLineNo('#preview-container');
84 84
85 85 this.previewBoxSelector = this.withLineNo('#preview-box');
86 86
87 87 this.editButton = this.withLineNo('#edit-btn');
88 88 this.editContainer = this.withLineNo('#edit-container');
89 89 this.cancelButton = this.withLineNo('#cancel-btn');
90 90 this.commentType = this.withLineNo('#comment_type');
91 91
92 92 this.resolvesId = null;
93 93 this.resolvesActionId = null;
94 94
95 95 this.cmBox = this.withLineNo('#text');
96 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
96 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
97 97
98 98 this.statusChange = this.withLineNo('#change_status');
99 99
100 100 this.submitForm = formElement;
101 101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
102 102 this.submitButtonText = this.submitButton.val();
103 103
104 104 this.previewUrl = pyroutes.url('changeset_comment_preview',
105 105 {'repo_name': templateContext.repo_name});
106 106
107 107 if (resolvesCommentId){
108 108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
109 109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
110 110 $(this.commentType).prop('disabled', true);
111 111 $(this.commentType).addClass('disabled');
112 112
113 113 // disable select
114 114 setTimeout(function() {
115 115 $(self.statusChange).select2('readonly', true);
116 116 }, 10);
117 117
118 118 var resolvedInfo = (
119 119 '<li class="resolve-action">' +
120 120 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
121 121 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
122 122 '</li>'
123 123 ).format(resolvesCommentId, _gettext('resolve comment'));
124 124 $(resolvedInfo).insertAfter($(this.commentType).parent());
125 125 }
126 126
127 127 // based on commitId, or pullRequestId decide where do we submit
128 128 // out data
129 129 if (this.commitId){
130 130 this.submitUrl = pyroutes.url('changeset_comment',
131 131 {'repo_name': templateContext.repo_name,
132 132 'revision': this.commitId});
133 133 this.selfUrl = pyroutes.url('changeset_home',
134 134 {'repo_name': templateContext.repo_name,
135 135 'revision': this.commitId});
136 136
137 137 } else if (this.pullRequestId) {
138 138 this.submitUrl = pyroutes.url('pullrequest_comment',
139 139 {'repo_name': templateContext.repo_name,
140 140 'pull_request_id': this.pullRequestId});
141 141 this.selfUrl = pyroutes.url('pullrequest_show',
142 142 {'repo_name': templateContext.repo_name,
143 143 'pull_request_id': this.pullRequestId});
144 144
145 145 } else {
146 146 throw new Error(
147 147 'CommentForm requires pullRequestId, or commitId to be specified.')
148 148 }
149 149
150 150 // FUNCTIONS and helpers
151 151 var self = this;
152 152
153 153 this.isInline = function(){
154 154 return this.lineNo && this.lineNo != 'general';
155 155 };
156 156
157 157 this.getCmInstance = function(){
158 158 return this.cm
159 159 };
160 160
161 161 this.setPlaceholder = function(placeholder) {
162 162 var cm = this.getCmInstance();
163 163 if (cm){
164 164 cm.setOption('placeholder', placeholder);
165 165 }
166 166 };
167 167
168 168 this.getCommentStatus = function() {
169 169 return $(this.submitForm).find(this.statusChange).val();
170 170 };
171 171 this.getCommentType = function() {
172 172 return $(this.submitForm).find(this.commentType).val();
173 173 };
174 174
175 175 this.getResolvesId = function() {
176 176 return $(this.submitForm).find(this.resolvesId).val() || null;
177 177 };
178 178 this.markCommentResolved = function(resolvedCommentId){
179 179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
180 180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
181 181 };
182 182
183 183 this.isAllowedToSubmit = function() {
184 184 return !$(this.submitButton).prop('disabled');
185 185 };
186 186
187 187 this.initStatusChangeSelector = function(){
188 188 var formatChangeStatus = function(state, escapeMarkup) {
189 189 var originalOption = state.element;
190 190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
191 191 '<span>' + escapeMarkup(state.text) + '</span>';
192 192 };
193 193 var formatResult = function(result, container, query, escapeMarkup) {
194 194 return formatChangeStatus(result, escapeMarkup);
195 195 };
196 196
197 197 var formatSelection = function(data, container, escapeMarkup) {
198 198 return formatChangeStatus(data, escapeMarkup);
199 199 };
200 200
201 201 $(this.submitForm).find(this.statusChange).select2({
202 202 placeholder: _gettext('Status Review'),
203 203 formatResult: formatResult,
204 204 formatSelection: formatSelection,
205 205 containerCssClass: "drop-menu status_box_menu",
206 206 dropdownCssClass: "drop-menu-dropdown",
207 207 dropdownAutoWidth: true,
208 208 minimumResultsForSearch: -1
209 209 });
210 210 $(this.submitForm).find(this.statusChange).on('change', function() {
211 211 var status = self.getCommentStatus();
212 212 if (status && !self.isInline()) {
213 213 $(self.submitButton).prop('disabled', false);
214 214 }
215 215
216 216 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
217 217 self.setPlaceholder(placeholderText)
218 218 })
219 219 };
220 220
221 221 // reset the comment form into it's original state
222 222 this.resetCommentFormState = function(content) {
223 223 content = content || '';
224 224
225 225 $(this.editContainer).show();
226 226 $(this.editButton).parent().addClass('active');
227 227
228 228 $(this.previewContainer).hide();
229 229 $(this.previewButton).parent().removeClass('active');
230 230
231 231 this.setActionButtonsDisabled(true);
232 232 self.cm.setValue(content);
233 233 self.cm.setOption("readOnly", false);
234 234
235 235 if (this.resolvesId) {
236 236 // destroy the resolve action
237 237 $(this.resolvesId).parent().remove();
238 238 }
239 239
240 240 $(this.statusChange).select2('readonly', false);
241 241 };
242 242
243 243 this.globalSubmitSuccessCallback = function(){
244 244 // default behaviour is to call GLOBAL hook, if it's registered.
245 245 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
246 246 commentFormGlobalSubmitSuccessCallback()
247 247 }
248 248 };
249 249
250 250 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
251 251 failHandler = failHandler || function() {};
252 252 var postData = toQueryString(postData);
253 253 var request = $.ajax({
254 254 url: url,
255 255 type: 'POST',
256 256 data: postData,
257 257 headers: {'X-PARTIAL-XHR': true}
258 258 })
259 259 .done(function(data) {
260 260 successHandler(data);
261 261 })
262 262 .fail(function(data, textStatus, errorThrown){
263 263 alert(
264 264 "Error while submitting comment.\n" +
265 265 "Error code {0} ({1}).".format(data.status, data.statusText));
266 266 failHandler()
267 267 });
268 268 return request;
269 269 };
270 270
271 271 // overwrite a submitHandler, we need to do it for inline comments
272 272 this.setHandleFormSubmit = function(callback) {
273 273 this.handleFormSubmit = callback;
274 274 };
275 275
276 276 // overwrite a submitSuccessHandler
277 277 this.setGlobalSubmitSuccessCallback = function(callback) {
278 278 this.globalSubmitSuccessCallback = callback;
279 279 };
280 280
281 281 // default handler for for submit for main comments
282 282 this.handleFormSubmit = function() {
283 283 var text = self.cm.getValue();
284 284 var status = self.getCommentStatus();
285 285 var commentType = self.getCommentType();
286 286 var resolvesCommentId = self.getResolvesId();
287 287
288 288 if (text === "" && !status) {
289 289 return;
290 290 }
291 291
292 292 var excludeCancelBtn = false;
293 293 var submitEvent = true;
294 294 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
295 295 self.cm.setOption("readOnly", true);
296 296
297 297 var postData = {
298 298 'text': text,
299 299 'changeset_status': status,
300 300 'comment_type': commentType,
301 301 'csrf_token': CSRF_TOKEN
302 302 };
303 303 if (resolvesCommentId){
304 304 postData['resolves_comment_id'] = resolvesCommentId;
305 305 }
306 306
307 307 var submitSuccessCallback = function(o) {
308 308 // reload page if we change status for single commit.
309 309 if (status && self.commitId) {
310 310 location.reload(true);
311 311 } else {
312 312 $('#injected_page_comments').append(o.rendered_text);
313 313 self.resetCommentFormState();
314 314 timeagoActivate();
315 315
316 316 // mark visually which comment was resolved
317 317 if (resolvesCommentId) {
318 318 self.markCommentResolved(resolvesCommentId);
319 319 }
320 320 }
321 321
322 322 // run global callback on submit
323 323 self.globalSubmitSuccessCallback();
324 324
325 325 };
326 326 var submitFailCallback = function(){
327 327 self.resetCommentFormState(text);
328 328 };
329 329 self.submitAjaxPOST(
330 330 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
331 331 };
332 332
333 333 this.previewSuccessCallback = function(o) {
334 334 $(self.previewBoxSelector).html(o);
335 335 $(self.previewBoxSelector).removeClass('unloaded');
336 336
337 337 // swap buttons, making preview active
338 338 $(self.previewButton).parent().addClass('active');
339 339 $(self.editButton).parent().removeClass('active');
340 340
341 341 // unlock buttons
342 342 self.setActionButtonsDisabled(false);
343 343 };
344 344
345 345 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
346 346 excludeCancelBtn = excludeCancelBtn || false;
347 347 submitEvent = submitEvent || false;
348 348
349 349 $(this.editButton).prop('disabled', state);
350 350 $(this.previewButton).prop('disabled', state);
351 351
352 352 if (!excludeCancelBtn) {
353 353 $(this.cancelButton).prop('disabled', state);
354 354 }
355 355
356 356 var submitState = state;
357 357 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
358 358 // if the value of commit review status is set, we allow
359 359 // submit button, but only on Main form, lineNo means inline
360 360 submitState = false
361 361 }
362 362 $(this.submitButton).prop('disabled', submitState);
363 363 if (submitEvent) {
364 364 $(this.submitButton).val(_gettext('Submitting...'));
365 365 } else {
366 366 $(this.submitButton).val(this.submitButtonText);
367 367 }
368 368
369 369 };
370 370
371 371 // lock preview/edit/submit buttons on load, but exclude cancel button
372 372 var excludeCancelBtn = true;
373 373 this.setActionButtonsDisabled(true, excludeCancelBtn);
374 374
375 375 // anonymous users don't have access to initialized CM instance
376 376 if (this.cm !== undefined){
377 377 this.cm.on('change', function(cMirror) {
378 378 if (cMirror.getValue() === "") {
379 379 self.setActionButtonsDisabled(true, excludeCancelBtn)
380 380 } else {
381 381 self.setActionButtonsDisabled(false, excludeCancelBtn)
382 382 }
383 383 });
384 384 }
385 385
386 386 $(this.editButton).on('click', function(e) {
387 387 e.preventDefault();
388 388
389 389 $(self.previewButton).parent().removeClass('active');
390 390 $(self.previewContainer).hide();
391 391
392 392 $(self.editButton).parent().addClass('active');
393 393 $(self.editContainer).show();
394 394
395 395 });
396 396
397 397 $(this.previewButton).on('click', function(e) {
398 398 e.preventDefault();
399 399 var text = self.cm.getValue();
400 400
401 401 if (text === "") {
402 402 return;
403 403 }
404 404
405 405 var postData = {
406 406 'text': text,
407 407 'renderer': templateContext.visual.default_renderer,
408 408 'csrf_token': CSRF_TOKEN
409 409 };
410 410
411 411 // lock ALL buttons on preview
412 412 self.setActionButtonsDisabled(true);
413 413
414 414 $(self.previewBoxSelector).addClass('unloaded');
415 415 $(self.previewBoxSelector).html(_gettext('Loading ...'));
416 416
417 417 $(self.editContainer).hide();
418 418 $(self.previewContainer).show();
419 419
420 420 // by default we reset state of comment preserving the text
421 421 var previewFailCallback = function(){
422 422 self.resetCommentFormState(text)
423 423 };
424 424 self.submitAjaxPOST(
425 425 self.previewUrl, postData, self.previewSuccessCallback,
426 426 previewFailCallback);
427 427
428 428 $(self.previewButton).parent().addClass('active');
429 429 $(self.editButton).parent().removeClass('active');
430 430 });
431 431
432 432 $(this.submitForm).submit(function(e) {
433 433 e.preventDefault();
434 434 var allowedToSubmit = self.isAllowedToSubmit();
435 435 if (!allowedToSubmit){
436 436 return false;
437 437 }
438 438 self.handleFormSubmit();
439 439 });
440 440
441 441 }
442 442
443 443 return CommentForm;
444 444 });
445 445
446 446 /* comments controller */
447 447 var CommentsController = function() {
448 448 var mainComment = '#text';
449 449 var self = this;
450 450
451 451 this.cancelComment = function(node) {
452 452 var $node = $(node);
453 453 var $td = $node.closest('td');
454 454 $node.closest('.comment-inline-form').remove();
455 455 return false;
456 456 };
457 457
458 458 this.getLineNumber = function(node) {
459 459 var $node = $(node);
460 460 return $node.closest('td').attr('data-line-number');
461 461 };
462 462
463 463 this.scrollToComment = function(node, offset, outdated) {
464 464 var outdated = outdated || false;
465 465 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
466 466
467 467 if (!node) {
468 468 node = $('.comment-selected');
469 469 if (!node.length) {
470 470 node = $('comment-current')
471 471 }
472 472 }
473 473 $wrapper = $(node).closest('div.comment');
474 474 $comment = $(node).closest(klass);
475 475 $comments = $(klass);
476 476
477 477 // show hidden comment when referenced.
478 478 if (!$wrapper.is(':visible')){
479 479 $wrapper.show();
480 480 }
481 481
482 482 $('.comment-selected').removeClass('comment-selected');
483 483
484 484 var nextIdx = $(klass).index($comment) + offset;
485 485 if (nextIdx >= $comments.length) {
486 486 nextIdx = 0;
487 487 }
488 488 var $next = $(klass).eq(nextIdx);
489 489 var $cb = $next.closest('.cb');
490 490 $cb.removeClass('cb-collapsed');
491 491
492 492 var $filediffCollapseState = $cb.closest('.filediff').prev();
493 493 $filediffCollapseState.prop('checked', false);
494 494 $next.addClass('comment-selected');
495 495 scrollToElement($next);
496 496 return false;
497 497 };
498 498
499 499 this.nextComment = function(node) {
500 500 return self.scrollToComment(node, 1);
501 501 };
502 502
503 503 this.prevComment = function(node) {
504 504 return self.scrollToComment(node, -1);
505 505 };
506 506
507 507 this.nextOutdatedComment = function(node) {
508 508 return self.scrollToComment(node, 1, true);
509 509 };
510 510
511 511 this.prevOutdatedComment = function(node) {
512 512 return self.scrollToComment(node, -1, true);
513 513 };
514 514
515 515 this.deleteComment = function(node) {
516 516 if (!confirm(_gettext('Delete this comment?'))) {
517 517 return false;
518 518 }
519 519 var $node = $(node);
520 520 var $td = $node.closest('td');
521 521 var $comment = $node.closest('.comment');
522 522 var comment_id = $comment.attr('data-comment-id');
523 523 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
524 524 var postData = {
525 525 '_method': 'delete',
526 526 'csrf_token': CSRF_TOKEN
527 527 };
528 528
529 529 $comment.addClass('comment-deleting');
530 530 $comment.hide('fast');
531 531
532 532 var success = function(response) {
533 533 $comment.remove();
534 534 return false;
535 535 };
536 536 var failure = function(data, textStatus, xhr) {
537 537 alert("error processing request: " + textStatus);
538 538 $comment.show('fast');
539 539 $comment.removeClass('comment-deleting');
540 540 return false;
541 541 };
542 542 ajaxPOST(url, postData, success, failure);
543 543 };
544 544
545 545 this.toggleWideMode = function (node) {
546 546 if ($('#content').hasClass('wrapper')) {
547 547 $('#content').removeClass("wrapper");
548 548 $('#content').addClass("wide-mode-wrapper");
549 549 $(node).addClass('btn-success');
550 550 } else {
551 551 $('#content').removeClass("wide-mode-wrapper");
552 552 $('#content').addClass("wrapper");
553 553 $(node).removeClass('btn-success');
554 554 }
555 555 return false;
556 556 };
557 557
558 558 this.toggleComments = function(node, show) {
559 559 var $filediff = $(node).closest('.filediff');
560 560 if (show === true) {
561 561 $filediff.removeClass('hide-comments');
562 562 } else if (show === false) {
563 563 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
564 564 $filediff.addClass('hide-comments');
565 565 } else {
566 566 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
567 567 $filediff.toggleClass('hide-comments');
568 568 }
569 569 return false;
570 570 };
571 571
572 572 this.toggleLineComments = function(node) {
573 573 self.toggleComments(node, true);
574 574 var $node = $(node);
575 575 $node.closest('tr').toggleClass('hide-line-comments');
576 576 };
577 577
578 578 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
579 579 var pullRequestId = templateContext.pull_request_data.pull_request_id;
580 580 var commitId = templateContext.commit_data.commit_id;
581 581
582 582 var commentForm = new CommentForm(
583 583 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
584 584 var cm = commentForm.getCmInstance();
585 585
586 586 if (resolvesCommentId){
587 587 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
588 588 }
589 589
590 590 setTimeout(function() {
591 591 // callbacks
592 592 if (cm !== undefined) {
593 593 commentForm.setPlaceholder(placeholderText);
594 594 if (commentForm.isInline()) {
595 595 cm.focus();
596 596 cm.refresh();
597 597 }
598 598 }
599 599 }, 10);
600 600
601 601 // trigger scrolldown to the resolve comment, since it might be away
602 602 // from the clicked
603 603 if (resolvesCommentId){
604 604 var actionNode = $(commentForm.resolvesActionId).offset();
605 605
606 606 setTimeout(function() {
607 607 if (actionNode) {
608 608 $('body, html').animate({scrollTop: actionNode.top}, 10);
609 609 }
610 610 }, 100);
611 611 }
612 612
613 613 return commentForm;
614 614 };
615 615
616 616 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
617 617
618 618 var tmpl = $('#cb-comment-general-form-template').html();
619 619 tmpl = tmpl.format(null, 'general');
620 620 var $form = $(tmpl);
621 621
622 622 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
623 623 var curForm = $formPlaceholder.find('form');
624 624 if (curForm){
625 625 curForm.remove();
626 626 }
627 627 $formPlaceholder.append($form);
628 628
629 629 var _form = $($form[0]);
630 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
630 631 var commentForm = this.createCommentForm(
631 _form, lineNo, placeholderText, true, resolvesCommentId);
632 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
632 633 commentForm.initStatusChangeSelector();
633 634
634 635 return commentForm;
635 636 };
636 637
637 638 this.createComment = function(node, resolutionComment) {
638 639 var resolvesCommentId = resolutionComment || null;
639 640 var $node = $(node);
640 641 var $td = $node.closest('td');
641 642 var $form = $td.find('.comment-inline-form');
642 643
643 644 if (!$form.length) {
644 645
645 646 var $filediff = $node.closest('.filediff');
646 647 $filediff.removeClass('hide-comments');
647 648 var f_path = $filediff.attr('data-f-path');
648 649 var lineno = self.getLineNumber(node);
649 650 // create a new HTML from template
650 651 var tmpl = $('#cb-comment-inline-form-template').html();
651 652 tmpl = tmpl.format(f_path, lineno);
652 653 $form = $(tmpl);
653 654
654 655 var $comments = $td.find('.inline-comments');
655 656 if (!$comments.length) {
656 657 $comments = $(
657 658 $('#cb-comments-inline-container-template').html());
658 659 $td.append($comments);
659 660 }
660 661
661 662 $td.find('.cb-comment-add-button').before($form);
662 663
663 664 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
664 665 var _form = $($form[0]).find('form');
665
666 var autocompleteActions = ['as_note', 'as_todo'];
666 667 var commentForm = this.createCommentForm(
667 _form, lineno, placeholderText, false, resolvesCommentId);
668 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
668 669
669 670 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
670 671 form: _form,
671 672 parent: $td[0],
672 673 lineno: lineno,
673 674 f_path: f_path}
674 675 );
675 676
676 677 // set a CUSTOM submit handler for inline comments.
677 678 commentForm.setHandleFormSubmit(function(o) {
678 679 var text = commentForm.cm.getValue();
679 680 var commentType = commentForm.getCommentType();
680 681 var resolvesCommentId = commentForm.getResolvesId();
681 682
682 683 if (text === "") {
683 684 return;
684 685 }
685 686
686 687 if (lineno === undefined) {
687 688 alert('missing line !');
688 689 return;
689 690 }
690 691 if (f_path === undefined) {
691 692 alert('missing file path !');
692 693 return;
693 694 }
694 695
695 696 var excludeCancelBtn = false;
696 697 var submitEvent = true;
697 698 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
698 699 commentForm.cm.setOption("readOnly", true);
699 700 var postData = {
700 701 'text': text,
701 702 'f_path': f_path,
702 703 'line': lineno,
703 704 'comment_type': commentType,
704 705 'csrf_token': CSRF_TOKEN
705 706 };
706 707 if (resolvesCommentId){
707 708 postData['resolves_comment_id'] = resolvesCommentId;
708 709 }
709 710
710 711 var submitSuccessCallback = function(json_data) {
711 712 $form.remove();
712 713 try {
713 714 var html = json_data.rendered_text;
714 715 var lineno = json_data.line_no;
715 716 var target_id = json_data.target_id;
716 717
717 718 $comments.find('.cb-comment-add-button').before(html);
718 719
719 720 //mark visually which comment was resolved
720 721 if (resolvesCommentId) {
721 722 commentForm.markCommentResolved(resolvesCommentId);
722 723 }
723 724
724 725 // run global callback on submit
725 726 commentForm.globalSubmitSuccessCallback();
726 727
727 728 } catch (e) {
728 729 console.error(e);
729 730 }
730 731
731 732 // re trigger the linkification of next/prev navigation
732 733 linkifyComments($('.inline-comment-injected'));
733 734 timeagoActivate();
734 735 commentForm.setActionButtonsDisabled(false);
735 736
736 737 };
737 738 var submitFailCallback = function(){
738 739 commentForm.resetCommentFormState(text)
739 740 };
740 741 commentForm.submitAjaxPOST(
741 742 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
742 743 });
743 744 }
744 745
745 746 $form.addClass('comment-inline-form-open');
746 747 };
747 748
748 749 this.createResolutionComment = function(commentId){
749 750 // hide the trigger text
750 751 $('#resolve-comment-{0}'.format(commentId)).hide();
751 752
752 753 var comment = $('#comment-'+commentId);
753 754 var commentData = comment.data();
754 755 if (commentData.commentInline) {
755 756 this.createComment(comment, commentId)
756 757 } else {
757 758 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
758 759 }
759 760
760 761 return false;
761 762 };
762 763
763 764 this.submitResolution = function(commentId){
764 765 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
765 766 var commentForm = form.get(0).CommentForm;
766 767
767 768 var cm = commentForm.getCmInstance();
768 769 var renderer = templateContext.visual.default_renderer;
769 770 if (renderer == 'rst'){
770 771 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
771 772 } else if (renderer == 'markdown') {
772 773 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
773 774 } else {
774 775 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
775 776 }
776 777
777 778 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
778 779 form.submit();
779 780 return false;
780 781 };
781 782
782 783 this.renderInlineComments = function(file_comments) {
783 784 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
784 785
785 786 for (var i = 0; i < file_comments.length; i++) {
786 787 var box = file_comments[i];
787 788
788 789 var target_id = $(box).attr('target_id');
789 790
790 791 // actually comments with line numbers
791 792 var comments = box.children;
792 793
793 794 for (var j = 0; j < comments.length; j++) {
794 795 var data = {
795 796 'rendered_text': comments[j].outerHTML,
796 797 'line_no': $(comments[j]).attr('line'),
797 798 'target_id': target_id
798 799 };
799 800 }
800 801 }
801 802
802 803 // since order of injection is random, we're now re-iterating
803 804 // from correct order and filling in links
804 805 linkifyComments($('.inline-comment-injected'));
805 806 firefoxAnchorFix();
806 807 };
807 808
808 809 };
General Comments 0
You need to be logged in to leave comments. Login now