Skip to content

Commit 10bf440

Browse files
authored
Merge pull request #2562 from mroderick/feature/tom-select-member-search
Replace Chosen.js with TomSelect for member lookup
2 parents aea3b53 + ea3e71f commit 10bf440

6 files changed

Lines changed: 174 additions & 8 deletions

File tree

app/assets/javascripts/application.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,43 @@ $(function() {
4040
format: "HH:i",
4141
});
4242

43+
// TomSelect for admin member lookup
44+
if ($('#member_lookup_id').length) {
45+
new TomSelect('#member_lookup_id', {
46+
placeholder: 'Type to search members...',
47+
valueField: 'id',
48+
labelField: 'full_name',
49+
searchField: ['full_name', 'email'],
50+
create: false,
51+
loadThrottle: 300,
52+
shouldLoad: function(query) {
53+
return query.length >= 3;
54+
},
55+
load: function(query, callback) {
56+
fetch('/admin/members/search?q=' + encodeURIComponent(query))
57+
.then(response => response.json())
58+
.then(json => callback(json))
59+
.catch(() => callback());
60+
},
61+
render: {
62+
option: function(item, escape) {
63+
return '<div>' + escape(item.full_name) + ' <small class="text-muted">' + escape(item.email) + '</small></div>';
64+
}
65+
}
66+
});
67+
68+
$('#member_lookup_id').on('change', function() {
69+
$('#view_profile').attr('href', '/admin/members/' + $(this).val());
70+
});
71+
}
72+
73+
// Chosen for all other selects (exclude #member_lookup_id)
4374
// Chosen hides inputs and selects, which becomes problematic when they are
4475
// required: browser validation doesn't get shown to the user.
4576
// This fix places "the original input behind the Chosen input, matching the
4677
// height and width so that the warning appears in the correct position."
4778
// https://github.com/harvesthq/chosen/issues/515#issuecomment-474588057
48-
$('select').on('chosen:ready', function () {
79+
$('select').not('#member_lookup_id').on('chosen:ready', function () {
4980
var height = $(this).next('.chosen-container').height();
5081
var width = $(this).next('.chosen-container').width();
5182

@@ -57,14 +88,10 @@ $(function() {
5788
}).show();
5889
});
5990

60-
$('select').chosen({
91+
$('select').not('#member_lookup_id').chosen({
6192
allow_single_deselect: true,
6293
no_results_text: 'No results matched'
6394
});
6495

65-
$('#member_lookup_id').change(function(e) {
66-
$('#view_profile').attr('href', '/admin/members/' + $(this).val())
67-
});
68-
6996
$('[data-bs-toggle="tooltip"]').tooltip();
7097
});

app/controllers/admin/members_controller.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,25 @@ class Admin::MembersController < Admin::ApplicationController
22
before_action :set_member, only: %i[events update_subscriptions send_attendance_email send_eligibility_email]
33

44
def index
5-
@members = Member.all
5+
# @members = Member.all removed - members loaded dynamically via search
6+
end
7+
8+
def search
9+
query = params[:q].to_s.strip
10+
11+
members = if query.length >= 3
12+
Member.where(
13+
"CONCAT(name, ' ', surname) ILIKE :q OR email ILIKE :q",
14+
q: "%#{query}%"
15+
).select(:id, :name, :surname, :email, :pronouns).limit(50)
16+
else
17+
[]
18+
end
19+
20+
render json: members.as_json(
21+
only: %i[id name surname email],
22+
methods: [:full_name]
23+
)
624
end
725

826
def show
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
- content_for :head do
2+
%link{ href: 'https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.bootstrap5.min.css', rel: 'stylesheet', type: 'text/css' }
3+
%script{ src: 'https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/js/tom-select.complete.min.js' }
4+
15
.container.py-4.py-lg-5
26
.row.mb-4
37
.col
48
%h1 Members Directory
59
.row.mb-4
610
.col-12.col-md-6
7-
= select_tag 'member_lookup_id', options_for_select([['Select a member...', '']] + @members.collect{ |u| ["#{u.full_name} (#{u.email})", u.id] }), { class: 'chosen-select' }
11+
= select_tag 'member_lookup_id', nil, class: 'form-control'
812
.row
913
.col
1014
= link_to 'View Profile', '#', { class: 'btn btn-primary', id: 'view_profile' }

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
resources :announcements, only: %i[new index create edit update]
9797

9898
resources :members, only: %i[show index] do
99+
get :search, on: :collection
99100
get :events
100101
get :send_eligibility_email
101102
get :send_attendance_email
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe Admin::MembersController, type: :controller do
4+
describe 'GET #search' do
5+
let(:admin) { Fabricate(:member) }
6+
let!(:member_jane) { Fabricate(:member, name: 'Jane', surname: 'Doe', email: 'jane@example.com', pronouns: nil) }
7+
let!(:member_john) { Fabricate(:member, name: 'John', surname: 'Smith', email: 'john@test.com') }
8+
9+
before do
10+
admin.add_role(:admin)
11+
login_as_admin(admin)
12+
end
13+
14+
context 'with query less than 3 characters' do
15+
it 'returns empty array' do
16+
get :search, params: { q: 'ab' }, format: :json
17+
18+
expect(response).to have_http_status(:ok)
19+
expect(JSON.parse(response.body)).to eq([])
20+
end
21+
end
22+
23+
context 'with query 3 or more characters' do
24+
it 'returns matching members by name' do
25+
get :search, params: { q: 'Jan' }, format: :json
26+
27+
expect(response).to have_http_status(:ok)
28+
results = JSON.parse(response.body)
29+
expect(results.length).to eq(1)
30+
expect(results.first['id']).to eq(member_jane.id)
31+
expect(results.first['full_name']).to eq('Jane Doe')
32+
end
33+
34+
it 'returns matching members by email' do
35+
get :search, params: { q: 'john@tes' }, format: :json
36+
37+
expect(response).to have_http_status(:ok)
38+
results = JSON.parse(response.body)
39+
expect(results.length).to eq(1)
40+
expect(results.first['id']).to eq(member_john.id)
41+
end
42+
43+
it 'returns JSON with correct shape' do
44+
get :search, params: { q: 'Jan' }, format: :json
45+
46+
results = JSON.parse(response.body)
47+
expect(results.first.keys).to contain_exactly('id', 'name', 'surname', 'email', 'full_name')
48+
end
49+
50+
it 'limits results to 50' do
51+
51.times { |i| Fabricate(:member, name: "Test#{i}", surname: 'User', email: "test#{i}@example.com") }
52+
53+
get :search, params: { q: 'Test' }, format: :json
54+
55+
results = JSON.parse(response.body)
56+
expect(results.length).to be <= 50
57+
end
58+
end
59+
60+
context 'when not authenticated' do
61+
before { login(Fabricate(:member)) }
62+
63+
it 'redirects to login' do
64+
get :search, params: { q: 'test' }, format: :json
65+
66+
expect(response).to have_http_status(:found)
67+
end
68+
end
69+
end
70+
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe 'Admin TomSelect Member Lookup', :js, type: :feature do
4+
let(:admin) { Fabricate(:member) }
5+
let!(:member_jane) { Fabricate(:member, name: 'Jane', surname: 'Doe', email: 'jane@example.com') }
6+
let!(:member_john) { Fabricate(:member, name: 'John', surname: 'Smith', email: 'john@test.com') }
7+
8+
before do
9+
admin.add_role(:admin)
10+
login_as_admin(admin)
11+
end
12+
13+
scenario 'searching for members with TomSelect' do
14+
visit admin_members_path
15+
16+
expect(page).to have_css('.ts-wrapper', wait: 5)
17+
18+
find('.ts-control').click
19+
find('.ts-control input').send_keys('Ja')
20+
sleep 0.5
21+
22+
find('.ts-control input').send_keys('ne')
23+
24+
expect(page).to have_css('.ts-dropdown .option', wait: 5)
25+
26+
expect(page).to have_content('Jane Doe')
27+
expect(page).to have_content('jane@example.com')
28+
29+
expect(page).not_to have_content('John Smith')
30+
end
31+
32+
scenario 'selecting a member updates view profile link' do
33+
visit admin_members_path
34+
35+
expect(page).to have_css('.ts-wrapper', wait: 5)
36+
37+
find('.ts-control').click
38+
find('.ts-control input').send_keys('Jane Doe')
39+
40+
expect(page).to have_css('.ts-dropdown .option', wait: 5)
41+
42+
find('.ts-dropdown .option', text: 'Jane Doe').click
43+
44+
expect(find('#view_profile')[:href]).to include(admin_member_path(member_jane))
45+
end
46+
end

0 commit comments

Comments
 (0)