diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 344f13a2b..4de962e2f 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -99,13 +99,43 @@ $(function() {
});
}
+ // TomSelect for meeting organisers (multi-select)
+ if ($('#meeting_organisers').length) {
+ new TomSelect('#meeting_organisers', {
+ plugins: ['remove_button'],
+ 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 '
' + escape(item.full_name) + ' ' + escape(item.email) + '
';
+ },
+ no_results: function(data, escape) {
+ return 'No members found
';
+ }
+ }
+ });
+ }
+
// 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, #meeting_invitations_member').on('chosen:ready', function () {
+ $('select').not('#member_lookup_id, #meeting_invitations_member, #meeting_organisers').on('chosen:ready', function () {
var height = $(this).next('.chosen-container').height();
var width = $(this).next('.chosen-container').width();
@@ -117,7 +147,7 @@ $(function() {
}).show();
});
- $('select').not('#member_lookup_id, #meeting_invitations_member').chosen({
+ $('select').not('#member_lookup_id, #meeting_invitations_member, #meeting_organisers').chosen({
allow_single_deselect: true,
no_results_text: 'No results matched'
});
diff --git a/app/controllers/admin/meetings_controller.rb b/app/controllers/admin/meetings_controller.rb
index 9365ff4b5..6906ecf76 100644
--- a/app/controllers/admin/meetings_controller.rb
+++ b/app/controllers/admin/meetings_controller.rb
@@ -7,10 +7,10 @@ def new
def create
@meeting = Meeting.new(meeting_params)
- set_organisers(organiser_ids)
- set_chapters(chapter_ids)
if @meeting.save
+ set_organisers(organiser_ids)
+ set_chapters(chapter_ids)
redirect_to [:admin, @meeting], notice: t('admin.messages.meeting.created')
else
flash[:notice] = @meeting.errors.full_messages.join(', ')
@@ -62,10 +62,10 @@ def slug
end
def meeting_params
- params.expect(meeting: [
- :name, :description, :slug, :date_and_time, :local_date, :local_time, :local_end_time,
- :invitable, :spaces, :venue_id, :sponsor_id, :chapters
- ])
+ params.expect(meeting: %i[
+ name description slug date_and_time local_date local_time local_end_time
+ invitable spaces venue_id sponsor_id chapters
+ ])
end
def organiser_ids
diff --git a/app/views/admin/meetings/_form.html.haml b/app/views/admin/meetings/_form.html.haml
index 2da3dd6d2..5f5e74da8 100644
--- a/app/views/admin/meetings/_form.html.haml
+++ b/app/views/admin/meetings/_form.html.haml
@@ -1,3 +1,7 @@
+- 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' }
+
= simple_form_for [:admin, @meeting] do |f|
.row
.col-12
@@ -17,7 +21,11 @@
.col-12
= f.association :venue, input_html: { data: { placeholder: 'Select venue' }}, required: true
.col-12
- = f.input :organisers, collection: Member.all, label_method: :full_name, value_method: :id, selected: @meeting.organisers.map(&:id), input_html: { multiple: true }
+ = f.label :organisers
+ = f.select :organisers,
+ options_for_select(@meeting.organisers.map { |o| [o.full_name, o.id] }, @meeting.organisers.map(&:id)),
+ { include_blank: false },
+ { multiple: true, class: 'tom-select', data: { placeholder: 'Type to search members...' } }
.col-12
= f.input :chapters, collection: Chapter.all, label_method: :name, value_method: :id, selected: @meeting.chapters.map(&:id), input_html: { multiple: true }
.col-12
diff --git a/spec/features/admin/meeting_spec.rb b/spec/features/admin/meeting_spec.rb
index a54caf611..c561270da 100644
--- a/spec/features/admin/meeting_spec.rb
+++ b/spec/features/admin/meeting_spec.rb
@@ -21,8 +21,8 @@
click_on 'Save'
expect(page).to have_content('Meeting successfully created')
- expect(page.current_path)
- .to eq(admin_meeting_path("#{I18n.l(today, format: :year_month).downcase}-august-meeting-1"))
+ expect(page)
+ .to have_current_path(admin_meeting_path("#{I18n.l(today, format: :year_month).downcase}-august-meeting-1"), ignore_query: true)
expect(page).to have_content 'Invite'
end
@@ -48,18 +48,30 @@
expect(page).to have_content('Slug has already been taken')
end
- scenario 'successfully' do
+ scenario 'successfully', :js do
permissions = Fabricate(:permission, resource: meeting, name: 'organiser')
visit edit_admin_meeting_path(meeting)
- fill_in 'Name', with: "March Meeting"
- unselect permissions.members.first.full_name
+ fill_in 'Name', with: 'March Meeting'
+ remove_from_tom_select(permissions.members.first.full_name)
click_on 'Save'
expect(page).to have_content('You have successfully updated the details of this meeting')
expect(page).to have_css(%(span[title="#{permissions.members.last.full_name}"]))
- expect(page).to_not have_css(%(span[title="#{permissions.members.first.full_name}"]))
+ expect(page).not_to have_css(%(span[title="#{permissions.members.first.full_name}"]))
+ end
+
+ scenario 'adding an organiser', :js do
+ meeting = Fabricate(:meeting)
+ new_organiser = Fabricate(:member)
+
+ visit edit_admin_meeting_path(meeting)
+ select_from_tom_select(new_organiser.full_name, from: 'meeting_organisers')
+
+ click_on 'Save'
+
+ expect(page).to have_css(%(span[title="#{new_organiser.full_name}"]))
end
end
@@ -78,7 +90,7 @@
scenario 'when no format is used then it redirects to the meeting page' do
visit attendees_emails_admin_meeting_path(meeting)
- expect(page.current_path).to eq(admin_meeting_path(meeting))
+ expect(page).to have_current_path(admin_meeting_path(meeting), ignore_query: true)
end
end
@@ -88,7 +100,7 @@
meeting = Fabricate(:meeting, chapters: [chapter])
visit invite_admin_meeting_path(meeting)
- expect(page).to have_content("Invitations are being sent out")
+ expect(page).to have_content('Invitations are being sent out')
end
scenario 'does not send the invitations to banned members' do
diff --git a/spec/support/select_from_tom_select.rb b/spec/support/select_from_tom_select.rb
index 228c74ac9..7153169e1 100644
--- a/spec/support/select_from_tom_select.rb
+++ b/spec/support/select_from_tom_select.rb
@@ -29,6 +29,14 @@ def select_from_tom_select(item_text, from: nil)
# Click the matching option
find('.ts-dropdown .option', text: item_text, match: :prefer_exact).click
end
+
+ # Remove an item from a TomSelect multi-select
+ # @param item_text [String] The text of the item to remove (must match exactly)
+ def remove_from_tom_select(item_text)
+ within '.ts-wrapper' do
+ find('.item', text: item_text, match: :prefer_exact).find('.remove').click
+ end
+ end
end
RSpec.configure do |config|