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|