##// END OF EJS Templates
Merged r14967 to r14969 (#21488)....
Jean-Philippe Lang -
r14601:9619aa98147c
parent child
Show More
@@ -1,657 +1,663
1 1 /* Redmine - project management software
2 2 Copyright (C) 2006-2015 Jean-Philippe Lang */
3 3
4 4 function checkAll(id, checked) {
5 5 $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
6 6 }
7 7
8 8 function toggleCheckboxesBySelector(selector) {
9 9 var all_checked = true;
10 10 $(selector).each(function(index) {
11 11 if (!$(this).is(':checked')) { all_checked = false; }
12 12 });
13 13 $(selector).prop('checked', !all_checked);
14 14 }
15 15
16 16 function showAndScrollTo(id, focus) {
17 17 $('#'+id).show();
18 18 if (focus !== null) {
19 19 $('#'+focus).focus();
20 20 }
21 21 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
22 22 }
23 23
24 24 function toggleRowGroup(el) {
25 25 var tr = $(el).parents('tr').first();
26 26 var n = tr.next();
27 27 tr.toggleClass('open');
28 28 while (n.length && !n.hasClass('group')) {
29 29 n.toggle();
30 30 n = n.next('tr');
31 31 }
32 32 }
33 33
34 34 function collapseAllRowGroups(el) {
35 35 var tbody = $(el).parents('tbody').first();
36 36 tbody.children('tr').each(function(index) {
37 37 if ($(this).hasClass('group')) {
38 38 $(this).removeClass('open');
39 39 } else {
40 40 $(this).hide();
41 41 }
42 42 });
43 43 }
44 44
45 45 function expandAllRowGroups(el) {
46 46 var tbody = $(el).parents('tbody').first();
47 47 tbody.children('tr').each(function(index) {
48 48 if ($(this).hasClass('group')) {
49 49 $(this).addClass('open');
50 50 } else {
51 51 $(this).show();
52 52 }
53 53 });
54 54 }
55 55
56 56 function toggleAllRowGroups(el) {
57 57 var tr = $(el).parents('tr').first();
58 58 if (tr.hasClass('open')) {
59 59 collapseAllRowGroups(el);
60 60 } else {
61 61 expandAllRowGroups(el);
62 62 }
63 63 }
64 64
65 65 function toggleFieldset(el) {
66 66 var fieldset = $(el).parents('fieldset').first();
67 67 fieldset.toggleClass('collapsed');
68 68 fieldset.children('div').toggle();
69 69 }
70 70
71 71 function hideFieldset(el) {
72 72 var fieldset = $(el).parents('fieldset').first();
73 73 fieldset.toggleClass('collapsed');
74 74 fieldset.children('div').hide();
75 75 }
76 76
77 77 // columns selection
78 78 function moveOptions(theSelFrom, theSelTo) {
79 79 $(theSelFrom).find('option:selected').detach().prop("selected", false).appendTo($(theSelTo));
80 80 }
81 81
82 82 function moveOptionUp(theSel) {
83 83 $(theSel).find('option:selected').each(function(){
84 84 $(this).prev(':not(:selected)').detach().insertAfter($(this));
85 85 });
86 86 }
87 87
88 88 function moveOptionTop(theSel) {
89 89 $(theSel).find('option:selected').detach().prependTo($(theSel));
90 90 }
91 91
92 92 function moveOptionDown(theSel) {
93 93 $($(theSel).find('option:selected').get().reverse()).each(function(){
94 94 $(this).next(':not(:selected)').detach().insertBefore($(this));
95 95 });
96 96 }
97 97
98 98 function moveOptionBottom(theSel) {
99 99 $(theSel).find('option:selected').detach().appendTo($(theSel));
100 100 }
101 101
102 102 function initFilters() {
103 103 $('#add_filter_select').change(function() {
104 104 addFilter($(this).val(), '', []);
105 105 });
106 106 $('#filters-table td.field input[type=checkbox]').each(function() {
107 107 toggleFilter($(this).val());
108 108 });
109 109 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
110 110 toggleFilter($(this).val());
111 111 });
112 112 $('#filters-table').on('click', '.toggle-multiselect', function() {
113 113 toggleMultiSelect($(this).siblings('select'));
114 114 });
115 115 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
116 116 if (e.keyCode == 13) $(this).closest('form').submit();
117 117 });
118 118 }
119 119
120 120 function addFilter(field, operator, values) {
121 121 var fieldId = field.replace('.', '_');
122 122 var tr = $('#tr_'+fieldId);
123 123 if (tr.length > 0) {
124 124 tr.show();
125 125 } else {
126 126 buildFilterRow(field, operator, values);
127 127 }
128 128 $('#cb_'+fieldId).prop('checked', true);
129 129 toggleFilter(field);
130 130 $('#add_filter_select').val('').find('option').each(function() {
131 131 if ($(this).attr('value') == field) {
132 132 $(this).attr('disabled', true);
133 133 }
134 134 });
135 135 }
136 136
137 137 function buildFilterRow(field, operator, values) {
138 138 var fieldId = field.replace('.', '_');
139 139 var filterTable = $("#filters-table");
140 140 var filterOptions = availableFilters[field];
141 141 if (!filterOptions) return;
142 142 var operators = operatorByType[filterOptions['type']];
143 143 var filterValues = filterOptions['values'];
144 144 var i, select;
145 145
146 146 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
147 147 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
148 148 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
149 149 '<td class="values"></td>'
150 150 );
151 151 filterTable.append(tr);
152 152
153 153 select = tr.find('td.operator select');
154 154 for (i = 0; i < operators.length; i++) {
155 155 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
156 156 if (operators[i] == operator) { option.attr('selected', true); }
157 157 select.append(option);
158 158 }
159 159 select.change(function(){ toggleOperator(field); });
160 160
161 161 switch (filterOptions['type']) {
162 162 case "list":
163 163 case "list_optional":
164 164 case "list_status":
165 165 case "list_subprojects":
166 166 tr.find('td.values').append(
167 167 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
168 168 ' <span class="toggle-multiselect">&nbsp;</span></span>'
169 169 );
170 170 select = tr.find('td.values select');
171 171 if (values.length > 1) { select.attr('multiple', true); }
172 172 for (i = 0; i < filterValues.length; i++) {
173 173 var filterValue = filterValues[i];
174 174 var option = $('<option>');
175 175 if ($.isArray(filterValue)) {
176 176 option.val(filterValue[1]).text(filterValue[0]);
177 177 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
178 178 } else {
179 179 option.val(filterValue).text(filterValue);
180 180 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
181 181 }
182 182 select.append(option);
183 183 }
184 184 break;
185 185 case "date":
186 186 case "date_past":
187 187 tr.find('td.values').append(
188 188 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
189 189 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
190 190 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
191 191 );
192 192 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
193 193 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
194 194 $('#values_'+fieldId).val(values[0]);
195 195 break;
196 196 case "string":
197 197 case "text":
198 198 tr.find('td.values').append(
199 199 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
200 200 );
201 201 $('#values_'+fieldId).val(values[0]);
202 202 break;
203 203 case "relation":
204 204 tr.find('td.values').append(
205 205 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
206 206 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
207 207 );
208 208 $('#values_'+fieldId).val(values[0]);
209 209 select = tr.find('td.values select');
210 210 for (i = 0; i < allProjects.length; i++) {
211 211 var filterValue = allProjects[i];
212 212 var option = $('<option>');
213 213 option.val(filterValue[1]).text(filterValue[0]);
214 214 if (values[0] == filterValue[1]) { option.attr('selected', true); }
215 215 select.append(option);
216 216 }
217 217 break;
218 218 case "integer":
219 219 case "float":
220 220 case "tree":
221 221 tr.find('td.values').append(
222 222 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
223 223 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
224 224 );
225 225 $('#values_'+fieldId+'_1').val(values[0]);
226 226 $('#values_'+fieldId+'_2').val(values[1]);
227 227 break;
228 228 }
229 229 }
230 230
231 231 function toggleFilter(field) {
232 232 var fieldId = field.replace('.', '_');
233 233 if ($('#cb_' + fieldId).is(':checked')) {
234 234 $("#operators_" + fieldId).show().removeAttr('disabled');
235 235 toggleOperator(field);
236 236 } else {
237 237 $("#operators_" + fieldId).hide().attr('disabled', true);
238 238 enableValues(field, []);
239 239 }
240 240 }
241 241
242 242 function enableValues(field, indexes) {
243 243 var fieldId = field.replace('.', '_');
244 244 $('#tr_'+fieldId+' td.values .value').each(function(index) {
245 245 if ($.inArray(index, indexes) >= 0) {
246 246 $(this).removeAttr('disabled');
247 247 $(this).parents('span').first().show();
248 248 } else {
249 249 $(this).val('');
250 250 $(this).attr('disabled', true);
251 251 $(this).parents('span').first().hide();
252 252 }
253 253
254 254 if ($(this).hasClass('group')) {
255 255 $(this).addClass('open');
256 256 } else {
257 257 $(this).show();
258 258 }
259 259 });
260 260 }
261 261
262 262 function toggleOperator(field) {
263 263 var fieldId = field.replace('.', '_');
264 264 var operator = $("#operators_" + fieldId);
265 265 switch (operator.val()) {
266 266 case "!*":
267 267 case "*":
268 268 case "t":
269 269 case "ld":
270 270 case "w":
271 271 case "lw":
272 272 case "l2w":
273 273 case "m":
274 274 case "lm":
275 275 case "y":
276 276 case "o":
277 277 case "c":
278 278 enableValues(field, []);
279 279 break;
280 280 case "><":
281 281 enableValues(field, [0,1]);
282 282 break;
283 283 case "<t+":
284 284 case ">t+":
285 285 case "><t+":
286 286 case "t+":
287 287 case ">t-":
288 288 case "<t-":
289 289 case "><t-":
290 290 case "t-":
291 291 enableValues(field, [2]);
292 292 break;
293 293 case "=p":
294 294 case "=!p":
295 295 case "!p":
296 296 enableValues(field, [1]);
297 297 break;
298 298 default:
299 299 enableValues(field, [0]);
300 300 break;
301 301 }
302 302 }
303 303
304 304 function toggleMultiSelect(el) {
305 305 if (el.attr('multiple')) {
306 306 el.removeAttr('multiple');
307 307 el.attr('size', 1);
308 308 } else {
309 309 el.attr('multiple', true);
310 310 if (el.children().length > 10)
311 311 el.attr('size', 10);
312 312 else
313 313 el.attr('size', 4);
314 314 }
315 315 }
316 316
317 317 function showTab(name, url) {
318 318 $('#tab-content-' + name).parent().find('.tab-content').hide();
319 319 $('#tab-content-' + name).parent().find('div.tabs a').removeClass('selected');
320 320 $('#tab-content-' + name).show();
321 321 $('#tab-' + name).addClass('selected');
322 322 //replaces current URL with the "href" attribute of the current link
323 323 //(only triggered if supported by browser)
324 324 if ("replaceState" in window.history) {
325 325 window.history.replaceState(null, document.title, url);
326 326 }
327 327 return false;
328 328 }
329 329
330 330 function moveTabRight(el) {
331 331 var lis = $(el).parents('div.tabs').first().find('ul').children();
332 332 var tabsWidth = 0;
333 333 var i = 0;
334 334 lis.each(function() {
335 335 if ($(this).is(':visible')) {
336 336 tabsWidth += $(this).width() + 6;
337 337 }
338 338 });
339 339 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
340 340 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
341 341 lis.eq(i).hide();
342 342 }
343 343
344 344 function moveTabLeft(el) {
345 345 var lis = $(el).parents('div.tabs').first().find('ul').children();
346 346 var i = 0;
347 347 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
348 348 if (i > 0) {
349 349 lis.eq(i-1).show();
350 350 }
351 351 }
352 352
353 353 function displayTabsButtons() {
354 354 var lis;
355 355 var tabsWidth = 0;
356 356 var el;
357 357 $('div.tabs').each(function() {
358 358 el = $(this);
359 359 lis = el.find('ul').children();
360 360 lis.each(function(){
361 361 if ($(this).is(':visible')) {
362 362 tabsWidth += $(this).width() + 6;
363 363 }
364 364 });
365 365 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
366 366 el.find('div.tabs-buttons').hide();
367 367 } else {
368 368 el.find('div.tabs-buttons').show();
369 369 }
370 370 });
371 371 }
372 372
373 373 function setPredecessorFieldsVisibility() {
374 374 var relationType = $('#relation_relation_type');
375 375 if (relationType.val() == "precedes" || relationType.val() == "follows") {
376 376 $('#predecessor_fields').show();
377 377 } else {
378 378 $('#predecessor_fields').hide();
379 379 }
380 380 }
381 381
382 382 function showModal(id, width, title) {
383 383 var el = $('#'+id).first();
384 384 if (el.length === 0 || el.is(':visible')) {return;}
385 385 if (!title) title = el.find('h3.title').text();
386 386 // moves existing modals behind the transparent background
387 387 $(".modal").zIndex(99);
388 388 el.dialog({
389 389 width: width,
390 390 modal: true,
391 391 resizable: false,
392 392 dialogClass: 'modal',
393 393 title: title
394 394 }).on('dialogclose', function(){
395 395 $(".modal").zIndex(101);
396 396 });
397 397 el.find("input[type=text], input[type=submit]").first().focus();
398 398 }
399 399
400 400 function hideModal(el) {
401 401 var modal;
402 402 if (el) {
403 403 modal = $(el).parents('.ui-dialog-content');
404 404 } else {
405 405 modal = $('#ajax-modal');
406 406 }
407 407 modal.dialog("close");
408 408 }
409 409
410 410 function submitPreview(url, form, target) {
411 411 $.ajax({
412 412 url: url,
413 413 type: 'post',
414 414 data: $('#'+form).serialize(),
415 415 success: function(data){
416 416 $('#'+target).html(data);
417 417 }
418 418 });
419 419 }
420 420
421 421 function collapseScmEntry(id) {
422 422 $('.'+id).each(function() {
423 423 if ($(this).hasClass('open')) {
424 424 collapseScmEntry($(this).attr('id'));
425 425 }
426 426 $(this).hide();
427 427 });
428 428 $('#'+id).removeClass('open');
429 429 }
430 430
431 431 function expandScmEntry(id) {
432 432 $('.'+id).each(function() {
433 433 $(this).show();
434 434 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
435 435 expandScmEntry($(this).attr('id'));
436 436 }
437 437 });
438 438 $('#'+id).addClass('open');
439 439 }
440 440
441 441 function scmEntryClick(id, url) {
442 442 var el = $('#'+id);
443 443 if (el.hasClass('open')) {
444 444 collapseScmEntry(id);
445 445 el.addClass('collapsed');
446 446 return false;
447 447 } else if (el.hasClass('loaded')) {
448 448 expandScmEntry(id);
449 449 el.removeClass('collapsed');
450 450 return false;
451 451 }
452 452 if (el.hasClass('loading')) {
453 453 return false;
454 454 }
455 455 el.addClass('loading');
456 456 $.ajax({
457 457 url: url,
458 458 success: function(data) {
459 459 el.after(data);
460 460 el.addClass('open').addClass('loaded').removeClass('loading');
461 461 }
462 462 });
463 463 return true;
464 464 }
465 465
466 466 function randomKey(size) {
467 467 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
468 468 var key = '';
469 469 for (var i = 0; i < size; i++) {
470 470 key += chars.charAt(Math.floor(Math.random() * chars.length));
471 471 }
472 472 return key;
473 473 }
474 474
475 475 function updateIssueFrom(url) {
476 476 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
477 477 $(this).data('valuebeforeupdate', $(this).val());
478 478 });
479 479 return $.ajax({
480 480 url: url,
481 481 type: 'post',
482 482 data: $('#issue-form').serialize()
483 483 });
484 484 }
485 485
486 486 function replaceIssueFormWith(html){
487 487 var replacement = $(html);
488 488 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
489 489 var object_id = $(this).attr('id');
490 490 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
491 491 replacement.find('#'+object_id).val($(this).val());
492 492 }
493 493 });
494 494 $('#all_attributes').empty();
495 495 $('#all_attributes').prepend(replacement);
496 496 }
497 497
498 498 function updateBulkEditFrom(url) {
499 499 $.ajax({
500 500 url: url,
501 501 type: 'post',
502 502 data: $('#bulk_edit_form').serialize()
503 503 });
504 504 }
505 505
506 506 function observeAutocompleteField(fieldId, url, options) {
507 507 $(document).ready(function() {
508 508 $('#'+fieldId).autocomplete($.extend({
509 509 source: url,
510 510 minLength: 2,
511 511 search: function(){$('#'+fieldId).addClass('ajax-loading');},
512 512 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
513 513 }, options));
514 514 $('#'+fieldId).addClass('autocomplete');
515 515 });
516 516 }
517 517
518 518 function observeSearchfield(fieldId, targetId, url) {
519 519 $('#'+fieldId).each(function() {
520 520 var $this = $(this);
521 521 $this.addClass('autocomplete');
522 522 $this.attr('data-value-was', $this.val());
523 523 var check = function() {
524 524 var val = $this.val();
525 525 if ($this.attr('data-value-was') != val){
526 526 $this.attr('data-value-was', val);
527 527 $.ajax({
528 528 url: url,
529 529 type: 'get',
530 530 data: {q: $this.val()},
531 531 success: function(data){ if(targetId) $('#'+targetId).html(data); },
532 532 beforeSend: function(){ $this.addClass('ajax-loading'); },
533 533 complete: function(){ $this.removeClass('ajax-loading'); }
534 534 });
535 535 }
536 536 };
537 537 var reset = function() {
538 538 if (timer) {
539 539 clearInterval(timer);
540 540 timer = setInterval(check, 300);
541 541 }
542 542 };
543 543 var timer = setInterval(check, 300);
544 544 $this.bind('keyup click mousemove', reset);
545 545 });
546 546 }
547 547
548 548 function beforeShowDatePicker(input, inst) {
549 549 var default_date = null;
550 550 switch ($(input).attr("id")) {
551 551 case "issue_start_date" :
552 552 if ($("#issue_due_date").size() > 0) {
553 553 default_date = $("#issue_due_date").val();
554 554 }
555 555 break;
556 556 case "issue_due_date" :
557 557 if ($("#issue_start_date").size() > 0) {
558 default_date = $("#issue_start_date").val();
558 var start_date = $("#issue_start_date").val();
559 if (start_date != "") {
560 start_date = new Date(Date.parse(start_date));
561 if (start_date > new Date()) {
562 default_date = $("#issue_start_date").val();
563 }
564 }
559 565 }
560 566 break;
561 567 }
562 568 $(input).datepicker("option", "defaultDate", default_date);
563 569 }
564 570
565 571 function initMyPageSortable(list, url) {
566 572 $('#list-'+list).sortable({
567 573 connectWith: '.block-receiver',
568 574 tolerance: 'pointer',
569 575 update: function(){
570 576 $.ajax({
571 577 url: url,
572 578 type: 'post',
573 579 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
574 580 });
575 581 }
576 582 });
577 583 $("#list-top, #list-left, #list-right").disableSelection();
578 584 }
579 585
580 586 var warnLeavingUnsavedMessage;
581 587 function warnLeavingUnsaved(message) {
582 588 warnLeavingUnsavedMessage = message;
583 589 $(document).on('submit', 'form', function(){
584 590 $('textarea').removeData('changed');
585 591 });
586 592 $(document).on('change', 'textarea', function(){
587 593 $(this).data('changed', 'changed');
588 594 });
589 595 window.onbeforeunload = function(){
590 596 var warn = false;
591 597 $('textarea').blur().each(function(){
592 598 if ($(this).data('changed')) {
593 599 warn = true;
594 600 }
595 601 });
596 602 if (warn) {return warnLeavingUnsavedMessage;}
597 603 };
598 604 }
599 605
600 606 function setupAjaxIndicator() {
601 607 $(document).bind('ajaxSend', function(event, xhr, settings) {
602 608 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
603 609 $('#ajax-indicator').show();
604 610 }
605 611 });
606 612 $(document).bind('ajaxStop', function() {
607 613 $('#ajax-indicator').hide();
608 614 });
609 615 }
610 616
611 617 function hideOnLoad() {
612 618 $('.hol').hide();
613 619 }
614 620
615 621 function addFormObserversForDoubleSubmit() {
616 622 $('form[method=post]').each(function() {
617 623 if (!$(this).hasClass('multiple-submit')) {
618 624 $(this).submit(function(form_submission) {
619 625 if ($(form_submission.target).attr('data-submitted')) {
620 626 form_submission.preventDefault();
621 627 } else {
622 628 $(form_submission.target).attr('data-submitted', true);
623 629 }
624 630 });
625 631 }
626 632 });
627 633 }
628 634
629 635 function defaultFocus(){
630 636 if (($('#content :focus').length == 0) && (window.location.hash == '')) {
631 637 $('#content input[type=text], #content textarea').first().focus();
632 638 }
633 639 }
634 640
635 641 function blockEventPropagation(event) {
636 642 event.stopPropagation();
637 643 event.preventDefault();
638 644 }
639 645
640 646 function toggleDisabledOnChange() {
641 647 var checked = $(this).is(':checked');
642 648 $($(this).data('disables')).attr('disabled', checked);
643 649 $($(this).data('enables')).attr('disabled', !checked);
644 650 }
645 651 function toggleDisabledInit() {
646 652 $('input[data-disables], input[data-enables]').each(toggleDisabledOnChange);
647 653 }
648 654 $(document).ready(function(){
649 655 $('#content').on('change', 'input[data-disables], input[data-enables]', toggleDisabledOnChange);
650 656 toggleDisabledInit();
651 657 });
652 658
653 659 $(document).ready(setupAjaxIndicator);
654 660 $(document).ready(hideOnLoad);
655 661 $(document).ready(addFormObserversForDoubleSubmit);
656 662 $(document).ready(defaultFocus);
657 663
@@ -1,69 +1,74
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'capybara/rails'
20 20
21 21 Capybara.default_driver = :selenium
22 22 Capybara.register_driver :selenium do |app|
23 23 # Use the following driver definition to test locally using Chrome
24 24 # (also requires chromedriver to be in PATH)
25 25 # Capybara::Selenium::Driver.new(app, :browser => :chrome)
26 26 # Add :switches => %w[--lang=en] to force default browser locale to English
27 27 # Default for Selenium remote driver is to connect to local host on port 4444
28 28 # This can be change using :url => 'http://localhost:9195' if necessary
29 29 # PhantomJS 1.8 now directly supports Webdriver Wire API,
30 30 # simply run it with `phantomjs --webdriver 4444`
31 31 # Add :desired_capabilities => Selenium::WebDriver::Remote::Capabilities.internet_explorer)
32 32 # to run on Selenium Grid Hub with IE
33 33 Capybara::Selenium::Driver.new(app, :browser => :remote)
34 34 end
35 35
36 36 # default: 2
37 37 Capybara.default_wait_time = 2
38 38
39 39 module Redmine
40 40 module UiTest
41 41 # Base class for UI tests
42 42 class Base < ActionDispatch::IntegrationTest
43 43 include Capybara::DSL
44 44
45 45 # Stop ActiveRecord from wrapping tests in transactions
46 46 # Transactional fixtures do not work with Selenium tests, because Capybara
47 47 # uses a separate server thread, which the transactions would be hidden
48 48 self.use_transactional_fixtures = false
49 49
50 50 # Should not depend on locale since Redmine displays login page
51 51 # using default browser locale which depend on system locale for "real" browsers drivers
52 52 def log_user(login, password)
53 53 visit '/my/page'
54 54 assert_equal '/login', current_path
55 55 within('#login-form form') do
56 56 fill_in 'username', :with => login
57 57 fill_in 'password', :with => password
58 58 find('input[name=login]').click
59 59 end
60 60 assert_equal '/my/page', current_path
61 61 end
62 62
63 setup do
64 # Set the page width higher than 900 to get the full layout with sidebar
65 page.driver.browser.manage.window.resize_to(1024, 900)
66 end
67
63 68 teardown do
64 69 Capybara.reset_sessions! # Forget the (simulated) browser state
65 70 Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver
66 71 end
67 72 end
68 73 end
69 74 end
@@ -1,278 +1,293
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../base', __FILE__)
19 19
20 20 class Redmine::UiTest::IssuesTest < Redmine::UiTest::Base
21 21 fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
22 22 :trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues,
23 23 :enumerations, :custom_fields, :custom_values, :custom_fields_trackers,
24 :watchers
24 :watchers, :journals, :journal_details
25 25
26 26 def test_create_issue
27 27 log_user('jsmith', 'jsmith')
28 28 visit '/projects/ecookbook/issues/new'
29 29 within('form#issue-form') do
30 30 select 'Bug', :from => 'Tracker'
31 31 select 'Low', :from => 'Priority'
32 32 fill_in 'Subject', :with => 'new test issue'
33 33 fill_in 'Description', :with => 'new issue'
34 34 select '0 %', :from => 'Done'
35 35 fill_in 'Due date', :with => ''
36 36 fill_in 'Searchable field', :with => 'Value for field 2'
37 37 # click_button 'Create' would match both 'Create' and 'Create and continue' buttons
38 38 find('input[name=commit]').click
39 39 end
40 40
41 41 # find created issue
42 42 issue = Issue.find_by_subject("new test issue")
43 43 assert_kind_of Issue, issue
44 44
45 45 # check redirection
46 46 find 'div#flash_notice', :visible => true, :text => "Issue \##{issue.id} created."
47 47 assert_equal issue_path(:id => issue), current_path
48 48
49 49 # check issue attributes
50 50 assert_equal 'jsmith', issue.author.login
51 51 assert_equal 1, issue.project.id
52 52 assert_equal IssueStatus.find_by_name('New'), issue.status
53 53 assert_equal Tracker.find_by_name('Bug'), issue.tracker
54 54 assert_equal IssuePriority.find_by_name('Low'), issue.priority
55 55 assert_equal 'Value for field 2', issue.custom_field_value(CustomField.find_by_name('Searchable field'))
56 56 end
57 57
58 58 def test_create_issue_with_form_update
59 59 field1 = IssueCustomField.create!(
60 60 :field_format => 'string',
61 61 :name => 'Field1',
62 62 :is_for_all => true,
63 63 :trackers => Tracker.where(:id => [1, 2])
64 64 )
65 65 field2 = IssueCustomField.create!(
66 66 :field_format => 'string',
67 67 :name => 'Field2',
68 68 :is_for_all => true,
69 69 :trackers => Tracker.where(:id => 2)
70 70 )
71 71
72 72 Role.non_member.add_permission! :add_issues
73 73 Role.non_member.remove_permission! :edit_issues, :add_issue_notes
74 74
75 75 log_user('someone', 'foo')
76 76 visit '/projects/ecookbook/issues/new'
77 77 assert page.has_no_content?(field2.name)
78 78 assert page.has_content?(field1.name)
79 79
80 80 fill_in 'Subject', :with => 'New test issue'
81 81 fill_in 'Description', :with => 'New test issue description'
82 82 fill_in field1.name, :with => 'CF1 value'
83 83 select 'Low', :from => 'Priority'
84 84
85 85 # field2 should show up when changing tracker
86 86 select 'Feature request', :from => 'Tracker'
87 87 assert page.has_content?(field2.name)
88 88 assert page.has_content?(field1.name)
89 89
90 90 fill_in field2.name, :with => 'CF2 value'
91 91 assert_difference 'Issue.count' do
92 92 page.first(:button, 'Create').click
93 93 end
94 94
95 95 issue = Issue.order('id desc').first
96 96 assert_equal 'New test issue', issue.subject
97 97 assert_equal 'New test issue description', issue.description
98 98 assert_equal 'Low', issue.priority.name
99 99 assert_equal 'CF1 value', issue.custom_field_value(field1)
100 100 assert_equal 'CF2 value', issue.custom_field_value(field2)
101 101 end
102 102
103 103 def test_create_issue_with_watchers
104 104 user = User.generate!(:firstname => 'Some', :lastname => 'Watcher')
105 105 assert_equal 'Some Watcher', user.name
106 106 log_user('jsmith', 'jsmith')
107 107 visit '/projects/ecookbook/issues/new'
108 108 fill_in 'Subject', :with => 'Issue with watchers'
109 109 # Add a project member as watcher
110 110 check 'Dave Lopper'
111 111 # Search for another user
112 112 assert page.has_no_css?('form#new-watcher-form')
113 113 assert page.has_no_content?('Some Watcher')
114 114 click_link 'Search for watchers to add'
115 115 within('form#new-watcher-form') do
116 116 fill_in 'user_search', :with => 'watch'
117 117 assert page.has_content?('Some Watcher')
118 118 check 'Some Watcher'
119 119 click_button 'Add'
120 120 end
121 121 assert page.has_css?('form#issue-form')
122 122 assert page.has_css?('p#watchers_form')
123 123 using_wait_time(30) do
124 124 within('span#watchers_inputs') do
125 125 within("label#issue_watcher_user_ids_#{user.id}") do
126 126 assert has_content?('Some Watcher'), "No watcher content"
127 127 end
128 128 end
129 129 end
130 130 assert_difference 'Issue.count' do
131 131 find('input[name=commit]').click
132 132 end
133 133
134 134 issue = Issue.order('id desc').first
135 135 assert_equal ['Dave Lopper', 'Some Watcher'], issue.watcher_users.map(&:name).sort
136 136 end
137 137
138 138 def test_create_issue_start_due_date
139 139 with_settings :default_issue_start_date_to_creation_date => 0 do
140 140 log_user('jsmith', 'jsmith')
141 141 visit '/projects/ecookbook/issues/new'
142 142 assert_equal "", page.find('input#issue_start_date').value
143 143 assert_equal "", page.find('input#issue_due_date').value
144 144 page.first('p#start_date_area img').click
145 145 page.first("td.ui-datepicker-days-cell-over a").click
146 146 assert_equal Date.today.to_s, page.find('input#issue_start_date').value
147 147 page.first('p#due_date_area img').click
148 148 page.first("td.ui-datepicker-days-cell-over a").click
149 149 assert_equal Date.today.to_s, page.find('input#issue_due_date').value
150 150 end
151 151 end
152 152
153 def test_create_issue_start_due_date_default
153 def test_default_due_date_proposed_in_date_picker
154 154 log_user('jsmith', 'jsmith')
155 155 visit '/projects/ecookbook/issues/new'
156
157 # Future start date: due date should default to start date
158 fill_in 'Start date', :with => '2027-04-01'
159 fill_in 'Due date', :with => ''
160 page.first('p#due_date_area img').click
161 page.first("td.ui-datepicker-days-cell-over a").click
162 assert_equal '2027-04-01', page.find('input#issue_due_date').value
163
164 # Passed start date: due date should default to today
156 165 fill_in 'Start date', :with => '2012-04-01'
157 166 fill_in 'Due date', :with => ''
158 167 page.first('p#due_date_area img').click
159 168 page.first("td.ui-datepicker-days-cell-over a").click
160 assert_equal '2012-04-01', page.find('input#issue_due_date').value
169 assert_equal Date.today.to_s, page.find('input#issue_due_date').value
170 end
171
172 def test_default_start_date_proposed_in_date_picker
173 log_user('jsmith', 'jsmith')
174 visit '/projects/ecookbook/issues/new'
161 175
176 # Passed due date: start date should default to due date
162 177 fill_in 'Start date', :with => ''
163 178 fill_in 'Due date', :with => '2012-04-01'
164 179 page.first('p#start_date_area img').click
165 180 page.first("td.ui-datepicker-days-cell-over a").click
166 181 assert_equal '2012-04-01', page.find('input#issue_start_date').value
167 182 end
168 183
169 184 def test_preview_issue_description
170 185 log_user('jsmith', 'jsmith')
171 186 visit '/projects/ecookbook/issues/new'
172 187 within('form#issue-form') do
173 188 fill_in 'Subject', :with => 'new issue subject'
174 189 fill_in 'Description', :with => 'new issue description'
175 190 click_link 'Preview'
176 191 end
177 192 find 'div#preview fieldset', :visible => true, :text => 'new issue description'
178 193 assert_difference 'Issue.count' do
179 194 find('input[name=commit]').click
180 195 end
181 196
182 197 issue = Issue.order('id desc').first
183 198 assert_equal 'new issue description', issue.description
184 199 end
185 200
186 201 def test_update_issue_with_form_update
187 202 field = IssueCustomField.create!(
188 203 :field_format => 'string',
189 204 :name => 'Form update CF',
190 205 :is_for_all => true,
191 206 :trackers => Tracker.where(:name => 'Feature request')
192 207 )
193 208
194 209 Role.non_member.add_permission! :edit_issues
195 210 Role.non_member.remove_permission! :add_issues, :add_issue_notes
196 211
197 212 log_user('someone', 'foo')
198 213 visit '/issues/1'
199 214 assert page.has_no_content?('Form update CF')
200 215
201 216 page.first(:link, 'Edit').click
202 217 # the custom field should show up when changing tracker
203 218 select 'Feature request', :from => 'Tracker'
204 219 assert page.has_content?('Form update CF')
205 220
206 221 fill_in 'Form update', :with => 'CF value'
207 222 assert_no_difference 'Issue.count' do
208 223 page.first(:button, 'Submit').click
209 224 end
210 225
211 226 issue = Issue.find(1)
212 227 assert_equal 'CF value', issue.custom_field_value(field)
213 228 end
214 229
215 230 def test_remove_issue_watcher_from_sidebar
216 231 user = User.find(3)
217 232 Watcher.create!(:watchable => Issue.find(1), :user => user)
218 233
219 234 log_user('jsmith', 'jsmith')
220 235 visit '/issues/1'
221 236 assert page.first('#sidebar').has_content?('Watchers (1)')
222 237 assert page.first('#sidebar').has_content?(user.name)
223 238 assert_difference 'Watcher.count', -1 do
224 239 page.first('ul.watchers .user-3 a.delete').click
225 240 assert page.first('#sidebar').has_content?('Watchers (0)')
226 241 end
227 242 assert page.first('#sidebar').has_no_content?(user.name)
228 243 end
229 244
230 245 def test_watch_should_update_watchers_list
231 246 user = User.find(2)
232 247 log_user('jsmith', 'jsmith')
233 248 visit '/issues/1'
234 249 assert page.first('#sidebar').has_content?('Watchers (0)')
235 250
236 251 page.first('a.issue-1-watcher').click
237 252 assert page.first('#sidebar').has_content?('Watchers (1)')
238 253 assert page.first('#sidebar').has_content?(user.name)
239 254 end
240 255
241 256 def test_watch_issue_via_context_menu
242 257 log_user('jsmith', 'jsmith')
243 258 visit '/issues'
244 259 assert page.has_css?('tr#issue-1')
245 260 find('tr#issue-1 td.updated_on').click
246 261 page.execute_script "$('tr#issue-1 td.updated_on').trigger('contextmenu');"
247 262 assert_difference 'Watcher.count' do
248 263 within('#context-menu') do
249 264 click_link 'Watch'
250 265 end
251 266 # wait for ajax response
252 267 assert page.has_css?('#context-menu .issue-1-watcher.icon-fav')
253 268 assert page.has_css?('tr#issue-1')
254 269 end
255 270 assert Issue.find(1).watched_by?(User.find_by_login('jsmith'))
256 271 end
257 272
258 273 def test_bulk_watch_issues_via_context_menu
259 274 log_user('jsmith', 'jsmith')
260 275 visit '/issues'
261 276 assert page.has_css?('tr#issue-1')
262 277 assert page.has_css?('tr#issue-4')
263 278 find('tr#issue-1 input[type=checkbox]').click
264 279 find('tr#issue-4 input[type=checkbox]').click
265 280 page.execute_script "$('tr#issue-1 td.updated_on').trigger('contextmenu');"
266 281 assert_difference 'Watcher.count', 2 do
267 282 within('#context-menu') do
268 283 click_link 'Watch'
269 284 end
270 285 # wait for ajax response
271 286 assert page.has_css?('#context-menu .issue-bulk-watcher.icon-fav')
272 287 assert page.has_css?('tr#issue-1')
273 288 assert page.has_css?('tr#issue-4')
274 289 end
275 290 assert Issue.find(1).watched_by?(User.find_by_login('jsmith'))
276 291 assert Issue.find(4).watched_by?(User.find_by_login('jsmith'))
277 292 end
278 293 end
General Comments 0
You need to be logged in to leave comments. Login now