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