Feature #43825 » preserve_checkbox_state_v2.patch
| app/assets/javascripts/application-legacy.js | ||
|---|---|---|
| 796 | 796 |
}); |
| 797 | 797 |
} |
| 798 |
function observeSearchfield(fieldId, targetId, url) {
|
|
| 798 |
function observeSearchfield(fieldId, targetId, url, options) {
|
|
| 799 | 799 |
$('#'+fieldId).each(function() {
|
| 800 | 800 |
var $this = $(this); |
| 801 | 801 |
$this.addClass('autocomplete');
|
| 802 | 802 |
$this.attr('data-value-was', $this.val());
|
| 803 |
var checkedValues = {};
|
|
| 804 |
var cbSelector = options && options.checkboxSelector; |
|
| 805 |
var $form = cbSelector ? $this.closest('form') : null;
|
|
| 806 |
var cbName = cbSelector ? $form.find(cbSelector).first().attr('name') : null;
|
|
| 807 | ||
| 808 |
function saveChecked() {
|
|
| 809 |
if (!cbSelector) return; |
|
| 810 |
$form.find(cbSelector).not('.hidden-checked-value').each(function() {
|
|
| 811 |
if ($(this).prop('checked')) {
|
|
| 812 |
checkedValues[$(this).val()] = true; |
|
| 813 |
} else {
|
|
| 814 |
delete checkedValues[$(this).val()]; |
|
| 815 |
} |
|
| 816 |
}); |
|
| 817 |
} |
|
| 818 | ||
| 819 |
function restoreChecked() {
|
|
| 820 |
if (!cbSelector) return; |
|
| 821 |
// Restore checkboxes that are visible in the current page |
|
| 822 |
$form.find(cbSelector).not('.hidden-checked-value').each(function() {
|
|
| 823 |
if (checkedValues[$(this).val()]) {
|
|
| 824 |
$(this).prop('checked', true);
|
|
| 825 |
} |
|
| 826 |
}); |
|
| 827 |
// Sync hidden inputs for checked values not visible as checkboxes |
|
| 828 |
$form.find('input.hidden-checked-value').remove();
|
|
| 829 |
if (!cbName) return; |
|
| 830 |
$.each(checkedValues, function(val) {
|
|
| 831 |
if ($form.find(cbSelector + '[value="' + val + '"]').length === 0) {
|
|
| 832 |
$form.append( |
|
| 833 |
$('<input type="hidden" class="hidden-checked-value">').attr('name', cbName).val(val)
|
|
| 834 |
); |
|
| 835 |
} |
|
| 836 |
}); |
|
| 837 |
} |
|
| 838 | ||
| 839 |
if (cbSelector) {
|
|
| 840 |
// Track checkbox changes via delegation |
|
| 841 |
$form.on('change', cbSelector, function() {
|
|
| 842 |
if ($(this).prop('checked')) {
|
|
| 843 |
checkedValues[$(this).val()] = true; |
|
| 844 |
} else {
|
|
| 845 |
delete checkedValues[$(this).val()]; |
|
| 846 |
} |
|
| 847 |
restoreChecked(); |
|
| 848 |
}); |
|
| 849 |
// Handle pagination (remote links replacing content) |
|
| 850 |
$form.on('ajax:before', 'a[data-remote]', function() {
|
|
| 851 |
saveChecked(); |
|
| 852 |
}); |
|
| 853 |
$form.on('ajax:complete', 'a[data-remote]', function() {
|
|
| 854 |
restoreChecked(); |
|
| 855 |
}); |
|
| 856 |
} |
|
| 857 | ||
| 803 | 858 |
var check = function() {
|
| 804 | 859 |
var val = $this.val(); |
| 805 | 860 |
if ($this.attr('data-value-was') != val){
|
| 806 | 861 |
$this.attr('data-value-was', val);
|
| 862 |
saveChecked(); |
|
| 807 | 863 |
$.ajax({
|
| 808 | 864 |
url: url, |
| 809 | 865 |
type: 'get', |
| 810 | 866 |
data: {q: $this.val()},
|
| 811 | 867 |
success: function(data){ if(targetId) $('#'+targetId).html(data); },
|
| 812 | 868 |
beforeSend: function(){ $this.addClass('ajax-loading'); },
|
| 813 |
complete: function(){ $this.removeClass('ajax-loading'); }
|
|
| 869 |
complete: function(){
|
|
| 870 |
$this.removeClass('ajax-loading');
|
|
| 871 |
restoreChecked(); |
|
| 872 |
} |
|
| 814 | 873 |
}); |
| 815 | 874 |
} |
| 816 | 875 |
}; |
| app/views/groups/_new_users_form.html.erb | ||
|---|---|---|
| 1 | 1 |
<fieldset class="box"> |
| 2 | 2 |
<legend><%= label_tag "user_search", l(:label_user_search) %></legend> |
| 3 | 3 |
<p><%= text_field_tag 'user_search', nil %></p> |
| 4 |
<%= javascript_tag "observeSearchfield('user_search', null, '#{ escape_javascript autocomplete_for_user_group_path(@group) }')" %>
|
|
| 4 |
<%= javascript_tag "observeSearchfield('user_search', null, '#{ escape_javascript autocomplete_for_user_group_path(@group) }', {checkboxSelector: 'input[name=\"user_ids[]\"]'})" %>
|
|
| 5 | 5 |
<div id="users"> |
| 6 | 6 |
<%= render_principals_for_new_group_users(@group) %> |
| app/views/members/_new_form.html.erb | ||
|---|---|---|
| 1 | 1 |
<fieldset class="box"> |
| 2 | 2 |
<legend><%= label_tag("principal_search", l(:label_principal_search)) %></legend>
|
| 3 | 3 |
<p><%= text_field_tag('principal_search', nil) %></p>
|
| 4 |
<%= javascript_tag "observeSearchfield('principal_search', null, '#{ escape_javascript autocomplete_project_memberships_path(@project, :format => 'js') }')" %>
|
|
| 4 |
<%= javascript_tag "observeSearchfield('principal_search', null, '#{ escape_javascript autocomplete_project_memberships_path(@project, :format => 'js') }', {checkboxSelector: 'input[name=\"membership[user_ids][]\"]'})" %>
|
|
| 5 | 5 |
<div id="principals_for_new_member"> |
| 6 | 6 |
<%= render_principals_for_new_members(@project) %> |
| 7 | 7 |
</div> |
| app/views/watchers/_new.html.erb | ||
|---|---|---|
| 34 | 34 |
:object_id => (watchables.present? ? watchables.map(&:id) : nil), |
| 35 | 35 |
:project_id => @project |
| 36 | 36 |
) |
| 37 |
)}' |
|
| 37 |
)}', |
|
| 38 |
{checkboxSelector: 'input[name=\"watcher[user_ids][]\"]'}
|
|
| 38 | 39 |
)" |
| 39 | 40 |
) %> |
| 40 | 41 |
<div id="users_for_watcher"> |
| test/system/groups_test.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
require_relative '../application_system_test_case' |
|
| 21 | ||
| 22 |
class GroupsSystemTest < ApplicationSystemTestCase |
|
| 23 |
fixtures :users, :email_addresses, :groups_users |
|
| 24 | ||
| 25 |
def test_add_user_to_group_after_search_preserves_selection |
|
| 26 |
group = Group.find(10) # A Team |
|
| 27 |
rhill = User.find_by_login('rhill')
|
|
| 28 |
someone = User.find_by_login('someone')
|
|
| 29 | ||
| 30 |
assert_not group.users.include?(rhill) |
|
| 31 |
assert_not group.users.include?(someone) |
|
| 32 | ||
| 33 |
log_user('admin', 'admin')
|
|
| 34 |
visit "/groups/#{group.id}/edit?tab=users"
|
|
| 35 | ||
| 36 |
click_on 'New user' |
|
| 37 | ||
| 38 |
within '#ajax-modal' do |
|
| 39 |
# Check Robert Hill |
|
| 40 |
find('label', text: 'Robert Hill').click
|
|
| 41 | ||
| 42 |
# Search for 'Some One' - only Some One should remain |
|
| 43 |
fill_in 'user_search', with: 'Some One' |
|
| 44 |
assert page.has_no_css?('label', text: 'Robert Hill')
|
|
| 45 | ||
| 46 |
# Check Some One while Robert Hill is hidden |
|
| 47 |
find('label', text: 'Some One').click
|
|
| 48 | ||
| 49 |
# Clear search - both should be visible and Robert Hill should still be checked |
|
| 50 |
fill_in 'user_search', with: '' |
|
| 51 |
assert page.has_css?('label', text: 'Robert Hill')
|
|
| 52 |
assert page.has_css?('label', text: 'Some One')
|
|
| 53 | ||
| 54 |
# Submit |
|
| 55 |
click_on 'Add' |
|
| 56 |
end |
|
| 57 | ||
| 58 |
# Wait for modal to close |
|
| 59 |
assert page.has_no_css?('#ajax-modal')
|
|
| 60 | ||
| 61 |
# Verify both users were added to the group |
|
| 62 |
group.reload |
|
| 63 |
assert group.users.include?(rhill), |
|
| 64 |
"Expected Robert Hill to be added to group" |
|
| 65 |
assert group.users.include?(someone), |
|
| 66 |
"Expected Some One to be added to group" |
|
| 67 |
end |
|
| 68 | ||
| 69 |
def test_add_user_to_group_submitting_while_filtered |
|
| 70 |
group = Group.find(10) # A Team |
|
| 71 |
rhill = User.find_by_login('rhill')
|
|
| 72 |
someone = User.find_by_login('someone')
|
|
| 73 | ||
| 74 |
assert_not group.users.include?(rhill) |
|
| 75 |
assert_not group.users.include?(someone) |
|
| 76 | ||
| 77 |
log_user('admin', 'admin')
|
|
| 78 |
visit "/groups/#{group.id}/edit?tab=users"
|
|
| 79 | ||
| 80 |
click_on 'New user' |
|
| 81 | ||
| 82 |
within '#ajax-modal' do |
|
| 83 |
# Check Robert Hill |
|
| 84 |
find('label', text: 'Robert Hill').click
|
|
| 85 | ||
| 86 |
# Search for 'Some One' - Robert Hill disappears |
|
| 87 |
fill_in 'user_search', with: 'Some One' |
|
| 88 |
assert page.has_no_css?('label', text: 'Robert Hill')
|
|
| 89 | ||
| 90 |
# Check Some One |
|
| 91 |
find('label', text: 'Some One').click
|
|
| 92 | ||
| 93 |
# Verify hidden input exists for Robert Hill before submitting |
|
| 94 |
rhill_id = rhill.id.to_s |
|
| 95 |
hidden_values = page.evaluate_script( |
|
| 96 |
"$('form input.hidden-checked-value').map(function(){return $(this).val()}).get()"
|
|
| 97 |
) |
|
| 98 |
assert_includes hidden_values, rhill_id, |
|
| 99 |
"Expected hidden input for Robert Hill (id=#{rhill_id}) but got: #{hidden_values.inspect}"
|
|
| 100 | ||
| 101 |
# Submit without clearing search |
|
| 102 |
click_on 'Add' |
|
| 103 |
end |
|
| 104 | ||
| 105 |
assert page.has_no_css?('#ajax-modal')
|
|
| 106 | ||
| 107 |
# Both users should be added even though Robert Hill was not visible |
|
| 108 |
group.reload |
|
| 109 |
assert group.users.include?(rhill), |
|
| 110 |
"Expected Robert Hill to be added to group" |
|
| 111 |
assert group.users.include?(someone), |
|
| 112 |
"Expected Some One to be added to group" |
|
| 113 |
end |
|
| 114 | ||
| 115 |
def test_unchecked_user_should_not_be_rechecked_after_search |
|
| 116 |
group = Group.find(10) # A Team |
|
| 117 |
rhill = User.find_by_login('rhill')
|
|
| 118 | ||
| 119 |
assert_not group.users.include?(rhill) |
|
| 120 | ||
| 121 |
log_user('admin', 'admin')
|
|
| 122 |
visit "/groups/#{group.id}/edit?tab=users"
|
|
| 123 | ||
| 124 |
click_on 'New user' |
|
| 125 | ||
| 126 |
within '#ajax-modal' do |
|
| 127 |
# Step 2: Check Robert Hill |
|
| 128 |
find('label', text: 'Robert Hill').click
|
|
| 129 | ||
| 130 |
# Step 3: Search that hides Robert Hill |
|
| 131 |
fill_in 'user_search', with: 'Some One' |
|
| 132 |
assert page.has_no_css?('label', text: 'Robert Hill')
|
|
| 133 | ||
| 134 |
# Step 4: Clear search - Robert Hill reappears and should be checked |
|
| 135 |
fill_in 'user_search', with: '' |
|
| 136 |
assert page.has_css?('label', text: 'Robert Hill')
|
|
| 137 |
rhill_cb = find('label', text: 'Robert Hill').find('input[type=checkbox]', visible: :all)
|
|
| 138 |
assert rhill_cb.checked?, "Robert Hill should be checked after clearing search" |
|
| 139 | ||
| 140 |
# Step 5: Uncheck Robert Hill |
|
| 141 |
find('label', text: 'Robert Hill').click
|
|
| 142 |
rhill_cb = find('label', text: 'Robert Hill').find('input[type=checkbox]', visible: :all)
|
|
| 143 |
assert_not rhill_cb.checked?, "Robert Hill should be unchecked after clicking again" |
|
| 144 | ||
| 145 |
# Step 6: Search that matches Robert Hill |
|
| 146 |
fill_in 'user_search', with: 'Robert Hill' |
|
| 147 |
assert page.has_css?('label', text: 'Robert Hill')
|
|
| 148 | ||
| 149 |
# Step 7: Robert Hill should still be unchecked |
|
| 150 |
rhill_cb = find('label', text: 'Robert Hill').find('input[type=checkbox]', visible: :all)
|
|
| 151 |
assert_not rhill_cb.checked?, |
|
| 152 |
"Robert Hill should NOT be re-checked after search refresh" |
|
| 153 | ||
| 154 |
# Also verify no hidden input exists for Robert Hill |
|
| 155 |
rhill_id = rhill.id.to_s |
|
| 156 |
hidden_values = page.evaluate_script( |
|
| 157 |
"$('form input.hidden-checked-value').map(function(){return $(this).val()}).get()"
|
|
| 158 |
) |
|
| 159 |
assert_not_includes hidden_values, rhill_id, |
|
| 160 |
"No hidden input should exist for unchecked Robert Hill" |
|
| 161 |
end |
|
| 162 |
end |
|
| 163 |
end |
|
| test/system/members_test.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
require_relative '../application_system_test_case' |
|
| 21 | ||
| 22 |
class MembersSystemTest < ApplicationSystemTestCase |
|
| 23 |
fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles, |
|
| 24 |
:trackers, :projects_trackers, :enabled_modules |
|
| 25 | ||
| 26 |
def test_add_member_after_search_preserves_selection |
|
| 27 |
project = Project.find('ecookbook')
|
|
| 28 |
rhill = User.find_by_login('rhill')
|
|
| 29 |
admin_user = User.find_by_login('admin')
|
|
| 30 | ||
| 31 |
assert_not project.members.map(&:user).include?(rhill) |
|
| 32 |
assert_not project.members.map(&:user).include?(admin_user) |
|
| 33 | ||
| 34 |
log_user('admin', 'admin')
|
|
| 35 |
visit '/projects/ecookbook/settings/members' |
|
| 36 | ||
| 37 |
click_on 'New member' |
|
| 38 | ||
| 39 |
within '#ajax-modal' do |
|
| 40 |
# Check Robert Hill |
|
| 41 |
find('label', text: 'Robert Hill').click
|
|
| 42 | ||
| 43 |
# Search for 'Redmine Admin' - only Redmine Admin should remain |
|
| 44 |
fill_in 'principal_search', with: 'Redmine Admin' |
|
| 45 |
assert page.has_no_css?('label', text: 'Robert Hill')
|
|
| 46 | ||
| 47 |
# Check Redmine Admin while Robert Hill is hidden |
|
| 48 |
find('label', text: 'Redmine Admin').click
|
|
| 49 | ||
| 50 |
# Clear search - both should be visible and Robert Hill should still be checked |
|
| 51 |
fill_in 'principal_search', with: '' |
|
| 52 |
assert page.has_css?('label', text: 'Robert Hill')
|
|
| 53 |
assert page.has_css?('label', text: 'Redmine Admin')
|
|
| 54 | ||
| 55 |
# Select a role |
|
| 56 |
check 'Manager' |
|
| 57 | ||
| 58 |
# Submit |
|
| 59 |
click_on 'Add' |
|
| 60 |
end |
|
| 61 | ||
| 62 |
# Wait for modal to close and page to update |
|
| 63 |
assert page.has_no_css?('#ajax-modal')
|
|
| 64 | ||
| 65 |
# Verify both users were added as members |
|
| 66 |
project.reload |
|
| 67 |
assert project.members.map(&:user).include?(rhill), |
|
| 68 |
"Expected Robert Hill to be added as member" |
|
| 69 |
assert project.members.map(&:user).include?(admin_user), |
|
| 70 |
"Expected Redmine Admin to be added as member" |
|
| 71 | ||
| 72 |
# Verify on the page |
|
| 73 |
assert page.has_css?('#tab-content-members', text: 'Robert Hill')
|
|
| 74 |
assert page.has_css?('#tab-content-members', text: 'Redmine Admin')
|
|
| 75 |
end |
|
| 76 |
end |
|
| test/system/watchers_test.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
require_relative '../application_system_test_case' |
|
| 21 | ||
| 22 |
class WatchersSystemTest < ApplicationSystemTestCase |
|
| 23 |
fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles, |
|
| 24 |
:trackers, :projects_trackers, :enabled_modules, :issues, :issue_statuses, |
|
| 25 |
:watchers |
|
| 26 | ||
| 27 |
def test_add_watcher_after_search_preserves_selection |
|
| 28 |
issue = Issue.find(1) |
|
| 29 |
jsmith = User.find_by_login('jsmith')
|
|
| 30 |
dlopper = User.find_by_login('dlopper')
|
|
| 31 | ||
| 32 |
# Clear existing watchers |
|
| 33 |
issue.watcher_users = [] |
|
| 34 | ||
| 35 |
assert_not issue.watched_by?(jsmith) |
|
| 36 |
assert_not issue.watched_by?(dlopper) |
|
| 37 | ||
| 38 |
log_user('admin', 'admin')
|
|
| 39 |
visit "/issues/#{issue.id}"
|
|
| 40 | ||
| 41 |
# Open watcher modal |
|
| 42 |
within '#sidebar' do |
|
| 43 |
click_on 'Add' |
|
| 44 |
end |
|
| 45 | ||
| 46 |
within '#ajax-modal' do |
|
| 47 |
# Check John Smith |
|
| 48 |
find('label', text: 'John Smith').click
|
|
| 49 | ||
| 50 |
# Search for 'Dave Lopper' - only Dave Lopper should remain |
|
| 51 |
fill_in 'user_search', with: 'Dave Lopper' |
|
| 52 |
assert page.has_no_css?('label', text: 'John Smith')
|
|
| 53 | ||
| 54 |
# Check Dave Lopper while John Smith is hidden |
|
| 55 |
find('label', text: 'Dave Lopper').click
|
|
| 56 | ||
| 57 |
# Clear search - both should be visible and John Smith should still be checked |
|
| 58 |
fill_in 'user_search', with: '' |
|
| 59 |
assert page.has_css?('label', text: 'John Smith')
|
|
| 60 |
assert page.has_css?('label', text: 'Dave Lopper')
|
|
| 61 | ||
| 62 |
# Submit |
|
| 63 |
click_on 'Add' |
|
| 64 |
end |
|
| 65 | ||
| 66 |
# Wait for AJAX to complete and sidebar to update |
|
| 67 |
assert page.has_css?('#sidebar', text: 'John Smith', wait: 5)
|
|
| 68 | ||
| 69 |
# Verify both users were added as watchers |
|
| 70 |
issue.reload |
|
| 71 |
assert issue.watched_by?(jsmith), |
|
| 72 |
"Expected John Smith to be added as watcher" |
|
| 73 |
assert issue.watched_by?(dlopper), |
|
| 74 |
"Expected Dave Lopper to be added as watcher" |
|
| 75 |
end |
|
| 76 |
end |
|
- « Previous
- 1
- 2
- Next »