Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,42 @@ $(function() {
});
}

// Chosen for all other selects (exclude #member_lookup_id)
// TomSelect for meeting invitation member lookup
if ($('#meeting_invitations_member').length) {
new TomSelect('#meeting_invitations_member', {
placeholder: 'Type to search members...',
valueField: 'id',
labelField: 'full_name',
searchField: ['full_name', 'email'],
create: false,
loadThrottle: 300,
shouldLoad: function(query) {
return query.length >= 3;
},
load: function(query, callback) {
fetch('/admin/members/search?q=' + encodeURIComponent(query))
.then(response => response.json())
.then(json => callback(json))
.catch(() => callback());
},
render: {
option: function(item, escape) {
return '<div>' + escape(item.full_name) + ' <small class="text-muted">' + escape(item.email) + '</small></div>';
},
no_results: function(data, escape) {
return '<div class="no-results">No members found</div>';
}
}
});
}

// Chosen for all other selects (exclude TomSelect fields)
// Chosen hides inputs and selects, which becomes problematic when they are
// required: browser validation doesn't get shown to the user.
// This fix places "the original input behind the Chosen input, matching the
// height and width so that the warning appears in the correct position."
// https://github.com/harvesthq/chosen/issues/515#issuecomment-474588057
$('select').not('#member_lookup_id').on('chosen:ready', function () {
$('select').not('#member_lookup_id, #meeting_invitations_member').on('chosen:ready', function () {
var height = $(this).next('.chosen-container').height();
var width = $(this).next('.chosen-container').width();

Expand All @@ -88,7 +117,7 @@ $(function() {
}).show();
});

$('select').not('#member_lookup_id').chosen({
$('select').not('#member_lookup_id, #meeting_invitations_member').chosen({
allow_single_deselect: true,
no_results_text: 'No results matched'
});
Expand Down
8 changes: 3 additions & 5 deletions app/views/admin/meetings/_invitation_management.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
.card.bg-white.border-success
.card-body
%p.mb-0
<strong>#{@invitations.count}</strong> members have RSVP'd to this event.
%strong #{@invitations.count}
members have RSVP'd to this event.

= simple_form_for :meeting_invitations, url: admin_meeting_invitations_path do |f|
.row
.col-6
= f.select :member,
Member.all.map { |u| ["#{u.full_name}", u.id] },
{ include_blank: true }, { class: 'chosen-select', required: true,
data: { placeholder: t('messages.invitations.select_a_member_to_rsvp') } }
= f.select :member, [], { include_blank: true }, { class: 'tom-select', required: true, data: { placeholder: t('messages.invitations.select_a_member_to_rsvp') } }
= f.hidden_field :meeting_id, value: @meeting.slug
.col
= f.button :button, 'Add', class: 'btn btn-sm btn-primary mb-0 me-2'
Expand Down
3 changes: 3 additions & 0 deletions app/views/admin/meetings/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
= sanitize(@meeting.description)

- if @invitations.any?
- content_for :head do
%link{ href: 'https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.bootstrap5.min.css', rel: 'stylesheet', type: 'text/css' }
%script{ src: 'https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/js/tom-select.complete.min.js' }
.py-4.py-lg-5.bg-light
.container#invitations
= render partial: 'invitation_management'
12 changes: 6 additions & 6 deletions spec/features/admin/managing_meeting_invitations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,27 @@
end

describe 'creating a new meeting invitation' do
scenario 'for a member that is not already attending' do
scenario 'for a member that is not already attending', :js do
Fabricate(:attending_meeting_invitation, meeting: meeting)
member = Fabricate(:member)

visit admin_meeting_path(meeting)
select member.name

select_from_tom_select(member.full_name, from: 'meeting_invitations_member')
click_on 'Add'

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

scenario 'for a member that is already attending' do
scenario 'for a member that is already attending', :js do
meeting = Fabricate(:meeting)
attending_member = Fabricate(:member)
Fabricate(:attending_meeting_invitation, meeting: meeting)
Fabricate(:attending_meeting_invitation, meeting: meeting, member: attending_member)

visit admin_meeting_path(meeting)
select attending_member.name

select_from_tom_select(attending_member.full_name, from: 'meeting_invitations_member')
click_on 'Add'

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

scenario 'Updating the attendance of an invitation' do
meeting = Fabricate(:meeting, date_and_time: 1.day.ago)
member = Fabricate(:member)
Fabricate(:attending_meeting_invitation, meeting: meeting)

visit admin_meeting_path(meeting)

find('.verify-attendance').click

expect(page).to have_content('Updated attendance')
Expand Down
36 changes: 36 additions & 0 deletions spec/support/select_from_tom_select.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

# Helper for interacting with TomSelect dropdowns in Capybara feature tests
# Similar to select_from_chosen but for TomSelect remote data loading
module SelectFromTomSelect
# Select an item from a TomSelect dropdown
# @param item_text [String] The text to select
# @param from [String, Symbol] The field ID (for documentation purposes)
def select_from_tom_select(item_text, from: nil)
# Wait for TomSelect to initialize
expect(page).to have_css('.ts-wrapper', wait: 5)

# Open dropdown and type search query
find('.ts-control').click
input = find('.ts-control input')

# Type first 3 characters to trigger search (shouldLoad requires >= 3)
input.send_keys(item_text[0, 3])

# Wait for debounce (300ms) and network request
sleep 0.5

# Type the rest if item_text is longer than 3 characters
input.send_keys(item_text[3..]) if item_text.length > 3

# Wait for results (includes debounce + network)
expect(page).to have_css('.ts-dropdown .option', wait: 5)

# Click the matching option
find('.ts-dropdown .option', text: item_text, match: :prefer_exact).click
end
end

RSpec.configure do |config|
config.include SelectFromTomSelect, type: :feature
end