diff --git a/app/components/chapter_picker_component.html.erb b/app/components/chapter_picker_component.html.erb new file mode 100644 index 000000000..7d30afa31 --- /dev/null +++ b/app/components/chapter_picker_component.html.erb @@ -0,0 +1,11 @@ +<%= text_field_tag @name, @selected, + list: datalist_id, + placeholder: @placeholder, + class: 'form-control', + autocomplete: 'off' %> + +<%= tag.datalist id: datalist_id do %> + <% @chapters.each do |chapter| %> + <%= tag.option chapter.name, value: chapter.name %> + <% end %> +<% end %> diff --git a/app/components/chapter_picker_component.rb b/app/components/chapter_picker_component.rb new file mode 100644 index 000000000..c74c52d79 --- /dev/null +++ b/app/components/chapter_picker_component.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ChapterPickerComponent < ViewComponent::Base + def initialize(name:, chapters:, selected: nil, placeholder: 'Select a chapter') + super() + @name = name + @chapters = chapters + @selected = selected + @placeholder = placeholder + end + + def datalist_id + "#{@name.parameterize}-options" + end +end diff --git a/app/controllers/admin/workshops_controller.rb b/app/controllers/admin/workshops_controller.rb index 4416c6a7b..6a7d6e2ca 100644 --- a/app/controllers/admin/workshops_controller.rb +++ b/app/controllers/admin/workshops_controller.rb @@ -25,17 +25,15 @@ def new end def create + resolve_chapter_name_to_id @workshop = Workshop.new(workshop_params) authorize(@workshop) - if workshop_type_valid? && @workshop.save assign_organisers_or_default assign_host(host_id) - redirect_to admin_workshop_path(@workshop), notice: I18n.t('admin.messages.workshop.created') else - flash[:warning] = @workshop.errors.full_messages - render 'new' + flash[:warning] = @workshop.errors.full_messages; render 'new' end end @@ -166,6 +164,14 @@ def destroy_host private + def resolve_chapter_name_to_id + chapter_value = params.dig(:workshop, :chapter_id) + return if chapter_value.blank? || chapter_value.match?(/\A\d+\z/) + + chapter = Chapter.find_by('LOWER(name) = LOWER(?)', chapter_value.strip) + params[:workshop][:chapter_id] = chapter&.id + end + def paginate_matching_invitations(query) eligible = @workshop.invitations .joins(:member) diff --git a/app/queries/sponsors_search.rb b/app/queries/sponsors_search.rb index d2478210b..45daad9a3 100644 --- a/app/queries/sponsors_search.rb +++ b/app/queries/sponsors_search.rb @@ -26,8 +26,15 @@ def by_name end def by_chapter - if chapter.present? - @sponsors = sponsors.joins(:workshops).where('workshops.chapter_id' => chapter).group('sponsors.id') - end + return if chapter.blank? + + chapter_id = chapter.to_s.match?(/\A\d+\z/) ? chapter : lookup_chapter_id + return unless chapter_id + + @sponsors = sponsors.joins(:workshops).where('workshops.chapter_id' => chapter_id).group('sponsors.id') + end + + def lookup_chapter_id + Chapter.find_by('LOWER(name) = LOWER(?)', chapter.strip)&.id end end diff --git a/app/views/admin/sponsors/index.html.haml b/app/views/admin/sponsors/index.html.haml index e8caee6be..6cd7e5adb 100644 --- a/app/views/admin/sponsors/index.html.haml +++ b/app/views/admin/sponsors/index.html.haml @@ -7,10 +7,10 @@ .col-12 .row.row-cols-md-auto.align-items-center - = simple_form_for @sponsors_search, url: admin_sponsors_path, method: :get, wrapper: :inline_form, html: { class: 'row row-cols-1 row-cols-md-auto align-items-center' } do |f| - = f.collection_select :chapter, @chapters, :id, :name, { include_blank: true, prompt: 'Select a chapter' }, { class: 'chosen-select'} - = f.input :name, required: false, label: false, placeholder: 'Filter by sponsor name', input_html: { class: 'my-2 my-md-0' } - = f.button :button, 'Filter', class: 'btn btn-primary' + = form_with model: @sponsors_search, url: admin_sponsors_path, method: :get, class: 'row row-cols-1 row-cols-md-auto align-items-center' do |f| + = render(ChapterPickerComponent.new(name: 'sponsors_search[chapter]', chapters: @chapters, selected: @sponsors_search.chapter, placeholder: 'Filter by chapter')) + = f.text_field :name, placeholder: 'Filter by sponsor name', class: 'form-control w-auto my-2 my-md-0' + = f.button 'Filter', class: 'btn btn-primary' = link_to 'Reset form', admin_sponsors_path = render partial: 'shared/pagination', locals: { pagy: @pagy, model: 'sponsor' } diff --git a/app/views/admin/workshops/_shared_form.html.haml b/app/views/admin/workshops/_shared_form.html.haml index 20fafb5f3..02029483c 100644 --- a/app/views/admin/workshops/_shared_form.html.haml +++ b/app/views/admin/workshops/_shared_form.html.haml @@ -3,7 +3,7 @@ = f.hidden_field :chapter_id - else .col-12 - = f.association :chapter, as: :select, collection: Chapter.available_to_user(current_user) + = render(ChapterPickerComponent.new(name: 'workshop[chapter_id]', chapters: Chapter.available_to_user(current_user), placeholder: 'Select a chapter')) .col-12 = f.input :local_date, label: 'Date', required: true, input_html: { value: @workshop.date_and_time.try(:strftime, '%Y-%m-%d'), type: :date } .col-12.col-md-6 diff --git a/spec/components/chapter_picker_component_spec.rb b/spec/components/chapter_picker_component_spec.rb new file mode 100644 index 000000000..b6efaf2a1 --- /dev/null +++ b/spec/components/chapter_picker_component_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' +require 'view_component/test_helpers' + +RSpec.describe ChapterPickerComponent do + include ViewComponent::TestHelpers + + let(:chapters) { Fabricate.times(3, :chapter) } + + it 'renders a text input with datalist attributes' do + render_inline described_class.new(name: 'sponsors_search[chapter]', chapters: chapters, placeholder: 'Filter by chapter') + + expect(page).to have_field('sponsors_search[chapter]') + input = page.find('input') + expect(input['placeholder']).to eq('Filter by chapter') + expect(input['autocomplete']).to eq('off') + expect(input['class']).to include('form-control') + end + + it 'renders a datalist with chapter names' do + render_inline described_class.new(name: 'sponsors_search[chapter]', chapters: chapters) + + expect(page).to have_css('datalist#sponsors_search-chapter-options') + chapters.each do |chapter| + expect(page).to have_css("option[value='#{chapter.name}']") + end + end + + it 'sanitises bracket characters in the datalist id' do + render_inline described_class.new(name: 'workshop[chapter_id]', chapters: chapters) + + expect(page).to have_css('datalist#workshop-chapter_id-options') + end + + it 'pre-fills the input when selected value is provided' do + render_inline described_class.new(name: 'sponsors_search[chapter]', chapters: chapters, selected: 'London') + + input = page.find('input') + expect(input['value']).to eq('London') + end +end diff --git a/spec/features/admin/filtering_sponsors_list_spec.rb b/spec/features/admin/filtering_sponsors_list_spec.rb index 762a6ccaa..f46b6bff1 100644 --- a/spec/features/admin/filtering_sponsors_list_spec.rb +++ b/spec/features/admin/filtering_sponsors_list_spec.rb @@ -25,5 +25,25 @@ expect(page).to have_text(sponsors.first.name) end end + + describe 'when filtering by chapter' do + let!(:chapter) { Fabricate(:chapter, name: 'London') } + let!(:workshop) { Fabricate(:workshop_no_sponsor, chapter: chapter) } + let!(:matching_sponsor) { Fabricate(:sponsor) } + + before do + Fabricate(:workshop_sponsor, workshop: workshop, sponsor: matching_sponsor) + Fabricate(:sponsor) + visit admin_sponsors_path + end + + scenario 'only sponsors for that chapter are displayed' do + fill_in 'sponsors_search[chapter]', with: 'London' + click_on 'Filter' + + expect(page).to have_css('.sponsor', count: 1) + expect(page).to have_text(matching_sponsor.name) + end + end end end diff --git a/spec/features/admin/workshops_spec.rb b/spec/features/admin/workshops_spec.rb index 4e2ef4a95..a83d3aa5b 100644 --- a/spec/features/admin/workshops_spec.rb +++ b/spec/features/admin/workshops_spec.rb @@ -48,7 +48,7 @@ scenario 'requires a host and a start and end datetime to be set' do visit new_admin_workshop_path - select chapter.name + fill_in 'workshop[chapter_id]', with: chapter.id fill_in 'Date', with: Date.current fill_in 'Begins at', with: '11:30' fill_in 'Ends at', with: '12:45' @@ -82,7 +82,7 @@ scenario 'must have a host set' do visit new_admin_workshop_path - select chapter.name + fill_in 'workshop[chapter_id]', with: chapter.id fill_in 'Date', with: Date.current fill_in 'Begins at', with: '11:30' @@ -108,7 +108,7 @@ chapter = Fabricate(:chapter, time_zone: 'Berlin') visit new_admin_workshop_path - select chapter.name + fill_in 'workshop[chapter_id]', with: chapter.id fill_in 'Date', with: Date.current fill_in 'Begins at', with: '18:30' fill_in 'Ends at', with: '20:45' @@ -130,7 +130,7 @@ check 'Virtual' - select chapter.name + fill_in 'workshop[chapter_id]', with: chapter.id fill_in 'Date', with: Date.current fill_in 'Begins at', with: '11:30' @@ -151,7 +151,7 @@ fill_in 'Student spaces', with: '10' fill_in 'Coach spaces', with: '5' - select chapter.name + fill_in 'workshop[chapter_id]', with: chapter.id fill_in 'Date', with: Date.current fill_in 'Begins at', with: '11:30' fill_in 'Ends at', with: '14:30' @@ -211,7 +211,7 @@ expect(page).to have_css('h1', text: 'New Workshop') expect(page).to have_title('New Workshop') - expect(page).to have_select('workshop_chapter_id') + expect(page).to have_field('workshop[chapter_id]') expect(page).to have_no_select('workshop_organisers') end end diff --git a/spec/queries/sponsors_search_spec.rb b/spec/queries/sponsors_search_spec.rb index 5a202c1ca..49c1c1e8d 100644 --- a/spec/queries/sponsors_search_spec.rb +++ b/spec/queries/sponsors_search_spec.rb @@ -44,6 +44,29 @@ expect(results).to contain_exactly(matching) end + it 'filters by chapter name' do + chapter = Fabricate(:chapter, name: 'London') + matching = Fabricate(:sponsor) + Fabricate(:workshop_sponsor, workshop: Fabricate(:workshop_no_sponsor, chapter: chapter), sponsor: matching) + Fabricate(:sponsor) + + results = described_class.new(name: nil, chapter: 'London').call + + expect(results).to contain_exactly(matching) + end + + it 'is case insensitive when filtering by chapter name' do + chapter = Fabricate(:chapter, name: 'London') + matching = Fabricate(:sponsor) + Fabricate(:workshop_sponsor, workshop: Fabricate(:workshop_no_sponsor, chapter: chapter), sponsor: matching) + + results = described_class.new(name: nil, chapter: 'london').call + expect(results).to contain_exactly(matching) + + results = described_class.new(name: nil, chapter: 'LONDON').call + expect(results).to contain_exactly(matching) + end + it 'filters by chapter' do chapter = Fabricate(:chapter) matching = Fabricate(:sponsor)