##// END OF EJS Templates
pull-requests: lock submit on pull request to prevent double submission on fast click.
marcink -
r2806:5a43c6a7 default
parent child Browse files
Show More
@@ -1,593 +1,593 b''
1 1 // # Copyright (C) 2010-2018 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 var prButtonLockChecks = {
21 21 'compare': false,
22 22 'reviewers': false
23 23 };
24 24
25 25 /**
26 26 * lock button until all checks and loads are made. E.g reviewer calculation
27 27 * should prevent from submitting a PR
28 28 * @param lockEnabled
29 29 * @param msg
30 30 * @param scope
31 31 */
32 32 var prButtonLock = function(lockEnabled, msg, scope) {
33 33 scope = scope || 'all';
34 34 if (scope == 'all'){
35 35 prButtonLockChecks['compare'] = !lockEnabled;
36 36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 37 } else if (scope == 'compare') {
38 38 prButtonLockChecks['compare'] = !lockEnabled;
39 39 } else if (scope == 'reviewers'){
40 40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 41 }
42 42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 43 if (lockEnabled) {
44 $('#save').attr('disabled', 'disabled');
44 $('#pr_submit').attr('disabled', 'disabled');
45 45 }
46 46 else if (checksMeet) {
47 $('#save').removeAttr('disabled');
47 $('#pr_submit').removeAttr('disabled');
48 48 }
49 49
50 50 if (msg) {
51 51 $('#pr_open_message').html(msg);
52 52 }
53 53 };
54 54
55 55
56 56 /**
57 57 Generate Title and Description for a PullRequest.
58 58 In case of 1 commits, the title and description is that one commit
59 59 in case of multiple commits, we iterate on them with max N number of commits,
60 60 and build description in a form
61 61 - commitN
62 62 - commitN+1
63 63 ...
64 64
65 65 Title is then constructed from branch names, or other references,
66 66 replacing '-' and '_' into spaces
67 67
68 68 * @param sourceRef
69 69 * @param elements
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
77 77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 80 });
81 81 // only 1 commit, use commit message as title
82 82 if (elements.length === 1) {
83 83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 84 }
85 85 else {
86 86 // use reference name
87 87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 88 }
89 89
90 90 return [title, desc]
91 91 };
92 92
93 93
94 94
95 95 ReviewersController = function () {
96 96 var self = this;
97 97 this.$reviewRulesContainer = $('#review_rules');
98 98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 99 this.forbidReviewUsers = undefined;
100 100 this.$reviewMembers = $('#review_members');
101 101 this.currentRequest = null;
102 102
103 103 this.defaultForbidReviewUsers = function() {
104 104 return [
105 105 {'username': 'default',
106 106 'user_id': templateContext.default_user.user_id}
107 107 ];
108 108 };
109 109
110 110 this.hideReviewRules = function() {
111 111 self.$reviewRulesContainer.hide();
112 112 };
113 113
114 114 this.showReviewRules = function() {
115 115 self.$reviewRulesContainer.show();
116 116 };
117 117
118 118 this.addRule = function(ruleText) {
119 119 self.showReviewRules();
120 120 return '<div>- {0}</div>'.format(ruleText)
121 121 };
122 122
123 123 this.loadReviewRules = function(data) {
124 124 // reset forbidden Users
125 125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126 126
127 127 // reset state of review rules
128 128 self.$rulesList.html('');
129 129
130 130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 131 // default rule, case for older repo that don't have any rules stored
132 132 self.$rulesList.append(
133 133 self.addRule(
134 134 _gettext('All reviewers must vote.'))
135 135 );
136 136 return self.forbidReviewUsers
137 137 }
138 138
139 139 if (data.rules.voting !== undefined) {
140 140 if (data.rules.voting < 0) {
141 141 self.$rulesList.append(
142 142 self.addRule(
143 143 _gettext('All individual reviewers must vote.'))
144 144 )
145 145 } else if (data.rules.voting === 1) {
146 146 self.$rulesList.append(
147 147 self.addRule(
148 148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 149 )
150 150
151 151 } else {
152 152 self.$rulesList.append(
153 153 self.addRule(
154 154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 155 )
156 156 }
157 157 }
158 158
159 159 if (data.rules.voting_groups !== undefined) {
160 160 $.each(data.rules.voting_groups, function(index, rule_data) {
161 161 self.$rulesList.append(
162 162 self.addRule(rule_data.text)
163 163 )
164 164 });
165 165 }
166 166
167 167 if (data.rules.use_code_authors_for_review) {
168 168 self.$rulesList.append(
169 169 self.addRule(
170 170 _gettext('Reviewers picked from source code changes.'))
171 171 )
172 172 }
173 173 if (data.rules.forbid_adding_reviewers) {
174 174 $('#add_reviewer_input').remove();
175 175 self.$rulesList.append(
176 176 self.addRule(
177 177 _gettext('Adding new reviewers is forbidden.'))
178 178 )
179 179 }
180 180 if (data.rules.forbid_author_to_review) {
181 181 self.forbidReviewUsers.push(data.rules_data.pr_author);
182 182 self.$rulesList.append(
183 183 self.addRule(
184 184 _gettext('Author is not allowed to be a reviewer.'))
185 185 )
186 186 }
187 187 if (data.rules.forbid_commit_author_to_review) {
188 188
189 189 if (data.rules_data.forbidden_users) {
190 190 $.each(data.rules_data.forbidden_users, function(index, member_data) {
191 191 self.forbidReviewUsers.push(member_data)
192 192 });
193 193
194 194 }
195 195
196 196 self.$rulesList.append(
197 197 self.addRule(
198 198 _gettext('Commit Authors are not allowed to be a reviewer.'))
199 199 )
200 200 }
201 201
202 202 return self.forbidReviewUsers
203 203 };
204 204
205 205 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
206 206
207 207 if (self.currentRequest) {
208 208 // make sure we cleanup old running requests before triggering this
209 209 // again
210 210 self.currentRequest.abort();
211 211 }
212 212
213 213 $('.calculate-reviewers').show();
214 214 // reset reviewer members
215 215 self.$reviewMembers.empty();
216 216
217 217 prButtonLock(true, null, 'reviewers');
218 218 $('#user').hide(); // hide user autocomplete before load
219 219
220 220 if (sourceRef.length !== 3 || targetRef.length !== 3) {
221 221 // don't load defaults in case we're missing some refs...
222 222 $('.calculate-reviewers').hide();
223 223 return
224 224 }
225 225
226 226 var url = pyroutes.url('repo_default_reviewers_data',
227 227 {
228 228 'repo_name': templateContext.repo_name,
229 229 'source_repo': sourceRepo,
230 230 'source_ref': sourceRef[2],
231 231 'target_repo': targetRepo,
232 232 'target_ref': targetRef[2]
233 233 });
234 234
235 235 self.currentRequest = $.get(url)
236 236 .done(function(data) {
237 237 self.currentRequest = null;
238 238
239 239 // review rules
240 240 self.loadReviewRules(data);
241 241
242 242 for (var i = 0; i < data.reviewers.length; i++) {
243 243 var reviewer = data.reviewers[i];
244 244 self.addReviewMember(
245 245 reviewer, reviewer.reasons, reviewer.mandatory);
246 246 }
247 247 $('.calculate-reviewers').hide();
248 248 prButtonLock(false, null, 'reviewers');
249 249 $('#user').show(); // show user autocomplete after load
250 250 });
251 251 };
252 252
253 253 // check those, refactor
254 254 this.removeReviewMember = function(reviewer_id, mark_delete) {
255 255 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
256 256
257 257 if(typeof(mark_delete) === undefined){
258 258 mark_delete = false;
259 259 }
260 260
261 261 if(mark_delete === true){
262 262 if (reviewer){
263 263 // now delete the input
264 264 $('#reviewer_{0} input'.format(reviewer_id)).remove();
265 265 // mark as to-delete
266 266 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
267 267 obj.addClass('to-delete');
268 268 obj.css({"text-decoration":"line-through", "opacity": 0.5});
269 269 }
270 270 }
271 271 else{
272 272 $('#reviewer_{0}'.format(reviewer_id)).remove();
273 273 }
274 274 };
275 275 this.reviewMemberEntry = function() {
276 276
277 277 };
278 278 this.addReviewMember = function(reviewer_obj, reasons, mandatory) {
279 279 var members = self.$reviewMembers.get(0);
280 280 var id = reviewer_obj.user_id;
281 281 var username = reviewer_obj.username;
282 282
283 283 var reasons = reasons || [];
284 284 var mandatory = mandatory || false;
285 285
286 286 // register IDS to check if we don't have this ID already in
287 287 var currentIds = [];
288 288 var _els = self.$reviewMembers.find('li').toArray();
289 289 for (el in _els){
290 290 currentIds.push(_els[el].id)
291 291 }
292 292
293 293 var userAllowedReview = function(userId) {
294 294 var allowed = true;
295 295 $.each(self.forbidReviewUsers, function(index, member_data) {
296 296 if (parseInt(userId) === member_data['user_id']) {
297 297 allowed = false;
298 298 return false // breaks the loop
299 299 }
300 300 });
301 301 return allowed
302 302 };
303 303
304 304 var userAllowed = userAllowedReview(id);
305 305 if (!userAllowed){
306 306 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
307 307 } else {
308 308 // only add if it's not there
309 309 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
310 310
311 311 if (alreadyReviewer) {
312 312 alert(_gettext('User `{0}` already in reviewers').format(username));
313 313 } else {
314 314 members.innerHTML += renderTemplate('reviewMemberEntry', {
315 315 'member': reviewer_obj,
316 316 'mandatory': mandatory,
317 317 'allowed_to_update': true,
318 318 'review_status': 'not_reviewed',
319 319 'review_status_label': _gettext('Not Reviewed'),
320 320 'reasons': reasons,
321 321 'create': true
322 322 });
323 323 }
324 324 }
325 325
326 326 };
327 327
328 328 this.updateReviewers = function(repo_name, pull_request_id){
329 329 var postData = $('#reviewers input').serialize();
330 330 _updatePullRequest(repo_name, pull_request_id, postData);
331 331 };
332 332
333 333 };
334 334
335 335
336 336 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
337 337 var url = pyroutes.url(
338 338 'pullrequest_update',
339 339 {"repo_name": repo_name, "pull_request_id": pull_request_id});
340 340 if (typeof postData === 'string' ) {
341 341 postData += '&csrf_token=' + CSRF_TOKEN;
342 342 } else {
343 343 postData.csrf_token = CSRF_TOKEN;
344 344 }
345 345 var success = function(o) {
346 346 window.location.reload();
347 347 };
348 348 ajaxPOST(url, postData, success);
349 349 };
350 350
351 351 /**
352 352 * PULL REQUEST update commits
353 353 */
354 354 var updateCommits = function(repo_name, pull_request_id) {
355 355 var postData = {
356 356 'update_commits': true};
357 357 _updatePullRequest(repo_name, pull_request_id, postData);
358 358 };
359 359
360 360
361 361 /**
362 362 * PULL REQUEST edit info
363 363 */
364 364 var editPullRequest = function(repo_name, pull_request_id, title, description) {
365 365 var url = pyroutes.url(
366 366 'pullrequest_update',
367 367 {"repo_name": repo_name, "pull_request_id": pull_request_id});
368 368
369 369 var postData = {
370 370 'title': title,
371 371 'description': description,
372 372 'edit_pull_request': true,
373 373 'csrf_token': CSRF_TOKEN
374 374 };
375 375 var success = function(o) {
376 376 window.location.reload();
377 377 };
378 378 ajaxPOST(url, postData, success);
379 379 };
380 380
381 381 var initPullRequestsCodeMirror = function (textAreaId) {
382 382 var ta = $(textAreaId).get(0);
383 383 var initialHeight = '100px';
384 384
385 385 // default options
386 386 var codeMirrorOptions = {
387 387 mode: "text",
388 388 lineNumbers: false,
389 389 indentUnit: 4,
390 390 theme: 'rc-input'
391 391 };
392 392
393 393 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
394 394 // marker for manually set description
395 395 codeMirrorInstance._userDefinedDesc = false;
396 396 codeMirrorInstance.setSize(null, initialHeight);
397 397 codeMirrorInstance.on("change", function(instance, changeObj) {
398 398 var height = initialHeight;
399 399 var lines = instance.lineCount();
400 400 if (lines > 6 && lines < 20) {
401 401 height = "auto"
402 402 }
403 403 else if (lines >= 20) {
404 404 height = 20 * 15;
405 405 }
406 406 instance.setSize(null, height);
407 407
408 408 // detect if the change was trigger by auto desc, or user input
409 409 changeOrigin = changeObj.origin;
410 410
411 411 if (changeOrigin === "setValue") {
412 412 cmLog.debug('Change triggered by setValue');
413 413 }
414 414 else {
415 415 cmLog.debug('user triggered change !');
416 416 // set special marker to indicate user has created an input.
417 417 instance._userDefinedDesc = true;
418 418 }
419 419
420 420 });
421 421
422 422 return codeMirrorInstance
423 423 };
424 424
425 425 /**
426 426 * Reviewer autocomplete
427 427 */
428 428 var ReviewerAutoComplete = function(inputId) {
429 429 $(inputId).autocomplete({
430 430 serviceUrl: pyroutes.url('user_autocomplete_data'),
431 431 minChars:2,
432 432 maxHeight:400,
433 433 deferRequestBy: 300, //miliseconds
434 434 showNoSuggestionNotice: true,
435 435 tabDisabled: true,
436 436 autoSelectFirst: true,
437 437 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
438 438 formatResult: autocompleteFormatResult,
439 439 lookupFilter: autocompleteFilterResult,
440 440 onSelect: function(element, data) {
441 441 var mandatory = false;
442 442 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
443 443
444 444 // add whole user groups
445 445 if (data.value_type == 'user_group') {
446 446 reasons.push(_gettext('member of "{0}"').format(data.value_display));
447 447
448 448 $.each(data.members, function(index, member_data) {
449 449 var reviewer = member_data;
450 450 reviewer['user_id'] = member_data['id'];
451 451 reviewer['gravatar_link'] = member_data['icon_link'];
452 452 reviewer['user_link'] = member_data['profile_link'];
453 453 reviewer['rules'] = [];
454 454 reviewersController.addReviewMember(reviewer, reasons, mandatory);
455 455 })
456 456 }
457 457 // add single user
458 458 else {
459 459 var reviewer = data;
460 460 reviewer['user_id'] = data['id'];
461 461 reviewer['gravatar_link'] = data['icon_link'];
462 462 reviewer['user_link'] = data['profile_link'];
463 463 reviewer['rules'] = [];
464 464 reviewersController.addReviewMember(reviewer, reasons, mandatory);
465 465 }
466 466
467 467 $(inputId).val('');
468 468 }
469 469 });
470 470 };
471 471
472 472
473 473 VersionController = function () {
474 474 var self = this;
475 475 this.$verSource = $('input[name=ver_source]');
476 476 this.$verTarget = $('input[name=ver_target]');
477 477 this.$showVersionDiff = $('#show-version-diff');
478 478
479 479 this.adjustRadioSelectors = function (curNode) {
480 480 var getVal = function (item) {
481 481 if (item == 'latest') {
482 482 return Number.MAX_SAFE_INTEGER
483 483 }
484 484 else {
485 485 return parseInt(item)
486 486 }
487 487 };
488 488
489 489 var curVal = getVal($(curNode).val());
490 490 var cleared = false;
491 491
492 492 $.each(self.$verSource, function (index, value) {
493 493 var elVal = getVal($(value).val());
494 494
495 495 if (elVal > curVal) {
496 496 if ($(value).is(':checked')) {
497 497 cleared = true;
498 498 }
499 499 $(value).attr('disabled', 'disabled');
500 500 $(value).removeAttr('checked');
501 501 $(value).css({'opacity': 0.1});
502 502 }
503 503 else {
504 504 $(value).css({'opacity': 1});
505 505 $(value).removeAttr('disabled');
506 506 }
507 507 });
508 508
509 509 if (cleared) {
510 510 // if we unchecked an active, set the next one to same loc.
511 511 $(this.$verSource).filter('[value={0}]'.format(
512 512 curVal)).attr('checked', 'checked');
513 513 }
514 514
515 515 self.setLockAction(false,
516 516 $(curNode).data('verPos'),
517 517 $(this.$verSource).filter(':checked').data('verPos')
518 518 );
519 519 };
520 520
521 521
522 522 this.attachVersionListener = function () {
523 523 self.$verTarget.change(function (e) {
524 524 self.adjustRadioSelectors(this)
525 525 });
526 526 self.$verSource.change(function (e) {
527 527 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
528 528 });
529 529 };
530 530
531 531 this.init = function () {
532 532
533 533 var curNode = self.$verTarget.filter(':checked');
534 534 self.adjustRadioSelectors(curNode);
535 535 self.setLockAction(true);
536 536 self.attachVersionListener();
537 537
538 538 };
539 539
540 540 this.setLockAction = function (state, selectedVersion, otherVersion) {
541 541 var $showVersionDiff = this.$showVersionDiff;
542 542
543 543 if (state) {
544 544 $showVersionDiff.attr('disabled', 'disabled');
545 545 $showVersionDiff.addClass('disabled');
546 546 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
547 547 }
548 548 else {
549 549 $showVersionDiff.removeAttr('disabled');
550 550 $showVersionDiff.removeClass('disabled');
551 551
552 552 if (selectedVersion == otherVersion) {
553 553 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
554 554 } else {
555 555 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
556 556 }
557 557 }
558 558
559 559 };
560 560
561 561 this.showVersionDiff = function () {
562 562 var target = self.$verTarget.filter(':checked');
563 563 var source = self.$verSource.filter(':checked');
564 564
565 565 if (target.val() && source.val()) {
566 566 var params = {
567 567 'pull_request_id': templateContext.pull_request_data.pull_request_id,
568 568 'repo_name': templateContext.repo_name,
569 569 'version': target.val(),
570 570 'from_version': source.val()
571 571 };
572 572 window.location = pyroutes.url('pullrequest_show', params)
573 573 }
574 574
575 575 return false;
576 576 };
577 577
578 578 this.toggleVersionView = function (elem) {
579 579
580 580 if (this.$showVersionDiff.is(':visible')) {
581 581 $('.version-pr').hide();
582 582 this.$showVersionDiff.hide();
583 583 $(elem).html($(elem).data('toggleOn'))
584 584 } else {
585 585 $('.version-pr').show();
586 586 this.$showVersionDiff.show();
587 587 $(elem).html($(elem).data('toggleOff'))
588 588 }
589 589
590 590 return false
591 591 }
592 592
593 593 }; No newline at end of file
@@ -1,537 +1,542 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 </div>
24 24
25 25 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
26 26
27 27 ${self.breadcrumbs()}
28 28
29 29 <div class="box pr-summary">
30 30
31 31 <div class="summary-details block-left">
32 32
33 33
34 34 <div class="pr-details-title">
35 35 ${_('Pull request summary')}
36 36 </div>
37 37
38 38 <div class="form" style="padding-top: 10px">
39 39 <!-- fields -->
40 40
41 41 <div class="fields" >
42 42
43 43 <div class="field">
44 44 <div class="label">
45 45 <label for="pullrequest_title">${_('Title')}:</label>
46 46 </div>
47 47 <div class="input">
48 48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
49 49 </div>
50 50 </div>
51 51
52 52 <div class="field">
53 53 <div class="label label-textarea">
54 54 <label for="pullrequest_desc">${_('Description')}:</label>
55 55 </div>
56 56 <div class="textarea text-area editor">
57 57 ${h.textarea('pullrequest_desc',size=30, )}
58 58 <span class="help-block">${_('Write a short description on this pull request')}</span>
59 59 </div>
60 60 </div>
61 61
62 62 <div class="field">
63 63 <div class="label label-textarea">
64 64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
65 65 </div>
66 66
67 67 ## TODO: johbo: Abusing the "content" class here to get the
68 68 ## desired effect. Should be replaced by a proper solution.
69 69
70 70 ##ORG
71 71 <div class="content">
72 72 <strong>${_('Source repository')}:</strong>
73 73 ${c.rhodecode_db_repo.description}
74 74 </div>
75 75 <div class="content">
76 76 ${h.hidden('source_repo')}
77 77 ${h.hidden('source_ref')}
78 78 </div>
79 79
80 80 ##OTHER, most Probably the PARENT OF THIS FORK
81 81 <div class="content">
82 82 ## filled with JS
83 83 <div id="target_repo_desc"></div>
84 84 </div>
85 85
86 86 <div class="content">
87 87 ${h.hidden('target_repo')}
88 88 ${h.hidden('target_ref')}
89 89 <span id="target_ref_loading" style="display: none">
90 90 ${_('Loading refs...')}
91 91 </span>
92 92 </div>
93 93 </div>
94 94
95 95 <div class="field">
96 96 <div class="label label-textarea">
97 97 <label for="pullrequest_submit"></label>
98 98 </div>
99 99 <div class="input">
100 100 <div class="pr-submit-button">
101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
101 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
102 102 </div>
103 103 <div id="pr_open_message"></div>
104 104 </div>
105 105 </div>
106 106
107 107 <div class="pr-spacing-container"></div>
108 108 </div>
109 109 </div>
110 110 </div>
111 111 <div>
112 112 ## AUTHOR
113 113 <div class="reviewers-title block-right">
114 114 <div class="pr-details-title">
115 115 ${_('Author of this pull request')}
116 116 </div>
117 117 </div>
118 118 <div class="block-right pr-details-content reviewers">
119 119 <ul class="group_members">
120 120 <li>
121 121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
122 122 </li>
123 123 </ul>
124 124 </div>
125 125
126 126 ## REVIEW RULES
127 127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
128 128 <div class="pr-details-title">
129 129 ${_('Reviewer rules')}
130 130 </div>
131 131 <div class="pr-reviewer-rules">
132 132 ## review rules will be appended here, by default reviewers logic
133 133 </div>
134 134 </div>
135 135
136 136 ## REVIEWERS
137 137 <div class="reviewers-title block-right">
138 138 <div class="pr-details-title">
139 139 ${_('Pull request reviewers')}
140 140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
141 141 </div>
142 142 </div>
143 143 <div id="reviewers" class="block-right pr-details-content reviewers">
144 144 ## members goes here, filled via JS based on initial selection !
145 145 <input type="hidden" name="__start__" value="review_members:sequence">
146 146 <ul id="review_members" class="group_members"></ul>
147 147 <input type="hidden" name="__end__" value="review_members:sequence">
148 148 <div id="add_reviewer_input" class='ac'>
149 149 <div class="reviewer_ac">
150 150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
151 151 <div id="reviewers_container"></div>
152 152 </div>
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 <div class="box">
158 158 <div>
159 159 ## overview pulled by ajax
160 160 <div id="pull_request_overview"></div>
161 161 </div>
162 162 </div>
163 163 ${h.end_form()}
164 164 </div>
165 165
166 166 <script type="text/javascript">
167 167 $(function(){
168 168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
169 169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
170 170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
171 171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
172 172
173 173 var $pullRequestForm = $('#pull_request_form');
174 var $pullRequestSubmit = $('#pr_submit', $pullRequestForm);
174 175 var $sourceRepo = $('#source_repo', $pullRequestForm);
175 176 var $targetRepo = $('#target_repo', $pullRequestForm);
176 177 var $sourceRef = $('#source_ref', $pullRequestForm);
177 178 var $targetRef = $('#target_ref', $pullRequestForm);
178 179
179 180 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
180 181 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
181 182
182 183 var targetRepo = function() { return $targetRepo.eq(0).val() };
183 184 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
184 185
185 186 var calculateContainerWidth = function() {
186 187 var maxWidth = 0;
187 188 var repoSelect2Containers = ['#source_repo', '#target_repo'];
188 189 $.each(repoSelect2Containers, function(idx, value) {
189 190 $(value).select2('container').width('auto');
190 191 var curWidth = $(value).select2('container').width();
191 192 if (maxWidth <= curWidth) {
192 193 maxWidth = curWidth;
193 194 }
194 195 $.each(repoSelect2Containers, function(idx, value) {
195 196 $(value).select2('container').width(maxWidth + 10);
196 197 });
197 198 });
198 199 };
199 200
200 201 var initRefSelection = function(selectedRef) {
201 202 return function(element, callback) {
202 203 // translate our select2 id into a text, it's a mapping to show
203 204 // simple label when selecting by internal ID.
204 205 var id, refData;
205 206 if (selectedRef === undefined || selectedRef === null) {
206 207 id = element.val();
207 208 refData = element.val().split(':');
208 209
209 210 if (refData.length !== 3){
210 211 refData = ["", "", ""]
211 212 }
212 213 } else {
213 214 id = selectedRef;
214 215 refData = selectedRef.split(':');
215 216 }
216 217
217 218 var text = refData[1];
218 219 if (refData[0] === 'rev') {
219 220 text = text.substring(0, 12);
220 221 }
221 222
222 223 var data = {id: id, text: text};
223 224 callback(data);
224 225 };
225 226 };
226 227
227 228 var formatRefSelection = function(item) {
228 229 var prefix = '';
229 230 var refData = item.id.split(':');
230 231 if (refData[0] === 'branch') {
231 232 prefix = '<i class="icon-branch"></i>';
232 233 }
233 234 else if (refData[0] === 'book') {
234 235 prefix = '<i class="icon-bookmark"></i>';
235 236 }
236 237 else if (refData[0] === 'tag') {
237 238 prefix = '<i class="icon-tag"></i>';
238 239 }
239 240
240 241 var originalOption = item.element;
241 242 return prefix + item.text;
242 243 };
243 244
244 245 // custom code mirror
245 246 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
246 247
247 248 reviewersController = new ReviewersController();
248 249
249 250 var queryTargetRepo = function(self, query) {
250 251 // cache ALL results if query is empty
251 252 var cacheKey = query.term || '__';
252 253 var cachedData = self.cachedDataSource[cacheKey];
253 254
254 255 if (cachedData) {
255 256 query.callback({results: cachedData.results});
256 257 } else {
257 258 $.ajax({
258 259 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
259 260 data: {query: query.term},
260 261 dataType: 'json',
261 262 type: 'GET',
262 263 success: function(data) {
263 264 self.cachedDataSource[cacheKey] = data;
264 265 query.callback({results: data.results});
265 266 },
266 267 error: function(data, textStatus, errorThrown) {
267 268 alert(
268 269 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
269 270 }
270 271 });
271 272 }
272 273 };
273 274
274 275 var queryTargetRefs = function(initialData, query) {
275 276 var data = {results: []};
276 277 // filter initialData
277 278 $.each(initialData, function() {
278 279 var section = this.text;
279 280 var children = [];
280 281 $.each(this.children, function() {
281 282 if (query.term.length === 0 ||
282 283 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
283 284 children.push({'id': this.id, 'text': this.text})
284 285 }
285 286 });
286 287 data.results.push({'text': section, 'children': children})
287 288 });
288 289 query.callback({results: data.results});
289 290 };
290 291
291 292 var loadRepoRefDiffPreview = function() {
292 293
293 294 var url_data = {
294 295 'repo_name': targetRepo(),
295 296 'target_repo': sourceRepo(),
296 297 'source_ref': targetRef()[2],
297 298 'source_ref_type': 'rev',
298 299 'target_ref': sourceRef()[2],
299 300 'target_ref_type': 'rev',
300 301 'merge': true,
301 302 '_': Date.now() // bypass browser caching
302 303 }; // gather the source/target ref and repo here
303 304
304 305 if (sourceRef().length !== 3 || targetRef().length !== 3) {
305 306 prButtonLock(true, "${_('Please select source and target')}");
306 307 return;
307 308 }
308 309 var url = pyroutes.url('repo_compare', url_data);
309 310
310 311 // lock PR button, so we cannot send PR before it's calculated
311 312 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
312 313
313 314 if (loadRepoRefDiffPreview._currentRequest) {
314 315 loadRepoRefDiffPreview._currentRequest.abort();
315 316 }
316 317
317 318 loadRepoRefDiffPreview._currentRequest = $.get(url)
318 319 .error(function(data, textStatus, errorThrown) {
319 320 if (textStatus !== 'abort') {
320 321 alert(
321 322 "Error while processing request.\nError code {0} ({1}).".format(
322 323 data.status, data.statusText));
323 324 }
324 325
325 326 })
326 327 .done(function(data) {
327 328 loadRepoRefDiffPreview._currentRequest = null;
328 329 $('#pull_request_overview').html(data);
329 330
330 331 var commitElements = $(data).find('tr[commit_id]');
331 332
332 333 var prTitleAndDesc = getTitleAndDescription(
333 334 sourceRef()[1], commitElements, 5);
334 335
335 336 var title = prTitleAndDesc[0];
336 337 var proposedDescription = prTitleAndDesc[1];
337 338
338 339 var useGeneratedTitle = (
339 340 $('#pullrequest_title').hasClass('autogenerated-title') ||
340 341 $('#pullrequest_title').val() === "");
341 342
342 343 if (title && useGeneratedTitle) {
343 344 // use generated title if we haven't specified our own
344 345 $('#pullrequest_title').val(title);
345 346 $('#pullrequest_title').addClass('autogenerated-title');
346 347
347 348 }
348 349
349 350 var useGeneratedDescription = (
350 351 !codeMirrorInstance._userDefinedDesc ||
351 352 codeMirrorInstance.getValue() === "");
352 353
353 354 if (proposedDescription && useGeneratedDescription) {
354 355 // set proposed content, if we haven't defined our own,
355 356 // or we don't have description written
356 357 codeMirrorInstance._userDefinedDesc = false; // reset state
357 358 codeMirrorInstance.setValue(proposedDescription);
358 359 }
359 360
360 361 var msg = '';
361 362 if (commitElements.length === 1) {
362 363 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
363 364 } else {
364 365 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
365 366 }
366 367
367 368 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
368 369
369 370 if (commitElements.length) {
370 371 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
371 372 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
372 373 }
373 374 else {
374 375 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
375 376 }
376 377
377 378
378 379 });
379 380 };
380 381
381 382 var Select2Box = function(element, overrides) {
382 383 var globalDefaults = {
383 384 dropdownAutoWidth: true,
384 385 containerCssClass: "drop-menu",
385 386 dropdownCssClass: "drop-menu-dropdown"
386 387 };
387 388
388 389 var initSelect2 = function(defaultOptions) {
389 390 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
390 391 element.select2(options);
391 392 };
392 393
393 394 return {
394 395 initRef: function() {
395 396 var defaultOptions = {
396 397 minimumResultsForSearch: 5,
397 398 formatSelection: formatRefSelection
398 399 };
399 400
400 401 initSelect2(defaultOptions);
401 402 },
402 403
403 404 initRepo: function(defaultValue, readOnly) {
404 405 var defaultOptions = {
405 406 initSelection : function (element, callback) {
406 407 var data = {id: defaultValue, text: defaultValue};
407 408 callback(data);
408 409 }
409 410 };
410 411
411 412 initSelect2(defaultOptions);
412 413
413 414 element.select2('val', defaultSourceRepo);
414 415 if (readOnly === true) {
415 416 element.select2('readonly', true);
416 417 }
417 418 }
418 419 };
419 420 };
420 421
421 422 var initTargetRefs = function(refsData, selectedRef) {
422 423
423 424 Select2Box($targetRef, {
424 425 placeholder: "${_('Select commit reference')}",
425 426 query: function(query) {
426 427 queryTargetRefs(refsData, query);
427 428 },
428 429 initSelection : initRefSelection(selectedRef)
429 430 }).initRef();
430 431
431 432 if (!(selectedRef === undefined)) {
432 433 $targetRef.select2('val', selectedRef);
433 434 }
434 435 };
435 436
436 437 var targetRepoChanged = function(repoData) {
437 438 // generate new DESC of target repo displayed next to select
438 439 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
439 440 $('#target_repo_desc').html(
440 441 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
441 442 );
442 443
443 444 // generate dynamic select2 for refs.
444 445 initTargetRefs(repoData['refs']['select2_refs'],
445 446 repoData['refs']['selected_ref']);
446 447
447 448 };
448 449
449 450 var sourceRefSelect2 = Select2Box($sourceRef, {
450 451 placeholder: "${_('Select commit reference')}",
451 452 query: function(query) {
452 453 var initialData = defaultSourceRepoData['refs']['select2_refs'];
453 454 queryTargetRefs(initialData, query)
454 455 },
455 456 initSelection: initRefSelection()
456 457 }
457 458 );
458 459
459 460 var sourceRepoSelect2 = Select2Box($sourceRepo, {
460 461 query: function(query) {}
461 462 });
462 463
463 464 var targetRepoSelect2 = Select2Box($targetRepo, {
464 465 cachedDataSource: {},
465 466 query: $.debounce(250, function(query) {
466 467 queryTargetRepo(this, query);
467 468 }),
468 469 formatResult: formatRepoResult
469 470 });
470 471
471 472 sourceRefSelect2.initRef();
472 473
473 474 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
474 475
475 476 targetRepoSelect2.initRepo(defaultTargetRepo, false);
476 477
477 478 $sourceRef.on('change', function(e){
478 479 loadRepoRefDiffPreview();
479 480 reviewersController.loadDefaultReviewers(
480 481 sourceRepo(), sourceRef(), targetRepo(), targetRef());
481 482 });
482 483
483 484 $targetRef.on('change', function(e){
484 485 loadRepoRefDiffPreview();
485 486 reviewersController.loadDefaultReviewers(
486 487 sourceRepo(), sourceRef(), targetRepo(), targetRef());
487 488 });
488 489
489 490 $targetRepo.on('change', function(e){
490 491 var repoName = $(this).val();
491 492 calculateContainerWidth();
492 493 $targetRef.select2('destroy');
493 494 $('#target_ref_loading').show();
494 495
495 496 $.ajax({
496 497 url: pyroutes.url('pullrequest_repo_refs',
497 498 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
498 499 data: {},
499 500 dataType: 'json',
500 501 type: 'GET',
501 502 success: function(data) {
502 503 $('#target_ref_loading').hide();
503 504 targetRepoChanged(data);
504 505 loadRepoRefDiffPreview();
505 506 },
506 507 error: function(data, textStatus, errorThrown) {
507 508 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
508 509 }
509 510 })
510 511
511 512 });
512 513
514 $pullRequestForm.on('submit', function(e){
515 prButtonLock(true, null, 'all');
516 });
517
513 518 prButtonLock(true, "${_('Please select source and target')}", 'all');
514 519
515 520 // auto-load on init, the target refs select2
516 521 calculateContainerWidth();
517 522 targetRepoChanged(defaultTargetRepoData);
518 523
519 524 $('#pullrequest_title').on('keyup', function(e){
520 525 $(this).removeClass('autogenerated-title');
521 526 });
522 527
523 528 % if c.default_source_ref:
524 529 // in case we have a pre-selected value, use it now
525 530 $sourceRef.select2('val', '${c.default_source_ref}');
526 531 // diff preview load
527 532 loadRepoRefDiffPreview();
528 533 // default reviewers
529 534 reviewersController.loadDefaultReviewers(
530 535 sourceRepo(), sourceRef(), targetRepo(), targetRef());
531 536 % endif
532 537
533 538 ReviewerAutoComplete('#user');
534 539 });
535 540 </script>
536 541
537 542 </%def>
General Comments 0
You need to be logged in to leave comments. Login now