Skip to content

Commit 88fc3db

Browse files
authored
Merge pull request #2566 from mroderick/feature/tom-select-meeting-invitations
Replace Chosen.js with TomSelect for meeting invitations
2 parents 195221d + 62f7581 commit 88fc3db

File tree

5 files changed

+80
-14
lines changed

5 files changed

+80
-14
lines changed

app/assets/javascripts/application.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,42 @@ $(function() {
7070
});
7171
}
7272

73-
// Chosen for all other selects (exclude #member_lookup_id)
73+
// TomSelect for meeting invitation member lookup
74+
if ($('#meeting_invitations_member').length) {
75+
new TomSelect('#meeting_invitations_member', {
76+
placeholder: 'Type to search members...',
77+
valueField: 'id',
78+
labelField: 'full_name',
79+
searchField: ['full_name', 'email'],
80+
create: false,
81+
loadThrottle: 300,
82+
shouldLoad: function(query) {
83+
return query.length >= 3;
84+
},
85+
load: function(query, callback) {
86+
fetch('/admin/members/search?q=' + encodeURIComponent(query))
87+
.then(response => response.json())
88+
.then(json => callback(json))
89+
.catch(() => callback());
90+
},
91+
render: {
92+
option: function(item, escape) {
93+
return '<div>' + escape(item.full_name) + ' <small class="text-muted">' + escape(item.email) + '</small></div>';
94+
},
95+
no_results: function(data, escape) {
96+
return '<div class="no-results">No members found</div>';
97+
}
98+
}
99+
});
100+
}
101+
102+
// Chosen for all other selects (exclude TomSelect fields)
74103
// Chosen hides inputs and selects, which becomes problematic when they are
75104
// required: browser validation doesn't get shown to the user.
76105
// This fix places "the original input behind the Chosen input, matching the
77106
// height and width so that the warning appears in the correct position."
78107
// https://github.com/harvesthq/chosen/issues/515#issuecomment-474588057
79-
$('select').not('#member_lookup_id').on('chosen:ready', function () {
108+
$('select').not('#member_lookup_id, #meeting_invitations_member').on('chosen:ready', function () {
80109
var height = $(this).next('.chosen-container').height();
81110
var width = $(this).next('.chosen-container').width();
82111

@@ -88,7 +117,7 @@ $(function() {
88117
}).show();
89118
});
90119

91-
$('select').not('#member_lookup_id').chosen({
120+
$('select').not('#member_lookup_id, #meeting_invitations_member').chosen({
92121
allow_single_deselect: true,
93122
no_results_text: 'No results matched'
94123
});

app/views/admin/meetings/_invitation_management.html.haml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33
.card.bg-white.border-success
44
.card-body
55
%p.mb-0
6-
<strong>#{@invitations.count}</strong> members have RSVP'd to this event.
6+
%strong #{@invitations.count}
7+
members have RSVP'd to this event.
78

89
= simple_form_for :meeting_invitations, url: admin_meeting_invitations_path do |f|
910
.row
1011
.col-6
11-
= f.select :member,
12-
Member.all.map { |u| ["#{u.full_name}", u.id] },
13-
{ include_blank: true }, { class: 'chosen-select', required: true,
14-
data: { placeholder: t('messages.invitations.select_a_member_to_rsvp') } }
12+
= f.select :member, [], { include_blank: true }, { class: 'tom-select', required: true, data: { placeholder: t('messages.invitations.select_a_member_to_rsvp') } }
1513
= f.hidden_field :meeting_id, value: @meeting.slug
1614
.col
1715
= f.button :button, 'Add', class: 'btn btn-sm btn-primary mb-0 me-2'

app/views/admin/meetings/show.html.haml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@
5454
= sanitize(@meeting.description)
5555

5656
- if @invitations.any?
57+
- content_for :head do
58+
%link{ href: 'https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.bootstrap5.min.css', rel: 'stylesheet', type: 'text/css' }
59+
%script{ src: 'https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/js/tom-select.complete.min.js' }
5760
.py-4.py-lg-5.bg-light
5861
.container#invitations
5962
= render partial: 'invitation_management'

spec/features/admin/managing_meeting_invitations_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,27 @@
88
end
99

1010
describe 'creating a new meeting invitation' do
11-
scenario 'for a member that is not already attending' do
11+
scenario 'for a member that is not already attending', :js do
1212
Fabricate(:attending_meeting_invitation, meeting: meeting)
1313
member = Fabricate(:member)
1414

1515
visit admin_meeting_path(meeting)
16-
select member.name
16+
17+
select_from_tom_select(member.full_name, from: 'meeting_invitations_member')
1718
click_on 'Add'
1819

1920
expect(page).to have_content("#{member.full_name} has been successfully added and notified via email")
2021
end
2122

22-
scenario 'for a member that is already attending' do
23+
scenario 'for a member that is already attending', :js do
2324
meeting = Fabricate(:meeting)
2425
attending_member = Fabricate(:member)
2526
Fabricate(:attending_meeting_invitation, meeting: meeting)
2627
Fabricate(:attending_meeting_invitation, meeting: meeting, member: attending_member)
2728

2829
visit admin_meeting_path(meeting)
29-
select attending_member.name
30+
31+
select_from_tom_select(attending_member.full_name, from: 'meeting_invitations_member')
3032
click_on 'Add'
3133

3234
expect(page).to have_content("#{attending_member.full_name} is already on the list!")
@@ -35,11 +37,9 @@
3537

3638
scenario 'Updating the attendance of an invitation' do
3739
meeting = Fabricate(:meeting, date_and_time: 1.day.ago)
38-
member = Fabricate(:member)
3940
Fabricate(:attending_meeting_invitation, meeting: meeting)
4041

4142
visit admin_meeting_path(meeting)
42-
4343
find('.verify-attendance').click
4444

4545
expect(page).to have_content('Updated attendance')
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
# Helper for interacting with TomSelect dropdowns in Capybara feature tests
4+
# Similar to select_from_chosen but for TomSelect remote data loading
5+
module SelectFromTomSelect
6+
# Select an item from a TomSelect dropdown
7+
# @param item_text [String] The text to select
8+
# @param from [String, Symbol] The field ID (for documentation purposes)
9+
def select_from_tom_select(item_text, from: nil)
10+
# Wait for TomSelect to initialize
11+
expect(page).to have_css('.ts-wrapper', wait: 5)
12+
13+
# Open dropdown and type search query
14+
find('.ts-control').click
15+
input = find('.ts-control input')
16+
17+
# Type first 3 characters to trigger search (shouldLoad requires >= 3)
18+
input.send_keys(item_text[0, 3])
19+
20+
# Wait for debounce (300ms) and network request
21+
sleep 0.5
22+
23+
# Type the rest if item_text is longer than 3 characters
24+
input.send_keys(item_text[3..]) if item_text.length > 3
25+
26+
# Wait for results (includes debounce + network)
27+
expect(page).to have_css('.ts-dropdown .option', wait: 5)
28+
29+
# Click the matching option
30+
find('.ts-dropdown .option', text: item_text, match: :prefer_exact).click
31+
end
32+
end
33+
34+
RSpec.configure do |config|
35+
config.include SelectFromTomSelect, type: :feature
36+
end

0 commit comments

Comments
 (0)