diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 10f38317..d53fd8f5 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,7 +3,7 @@ class UsersController < ApplicationController before_action :set_user, only: %i[ edit update destroy ] def index - @users = User.all + @users = User.all.search_with_params(user_search_params) end def new @@ -52,4 +52,9 @@ def set_user def user_params params.expect(user: [ :email, :password, :is_admin ]) end + + def user_search_params + return {} unless params[:search].present? + params.expect(search: [ :email, :is_admin, :order ]) + end end diff --git a/app/javascript/controllers/search_controller.js b/app/javascript/controllers/search_controller.js new file mode 100644 index 00000000..a630dce6 --- /dev/null +++ b/app/javascript/controllers/search_controller.js @@ -0,0 +1,15 @@ +import { Controller } from "@hotwired/stimulus"; +import { useDebounce } from "stimulus-use"; + +export default class extends Controller { + static targets = ["search"]; + static debounces = ["submit"]; + + connect() { + useDebounce(this, { wait: 300 }); + } + + submit() { + this.searchTarget.requestSubmit(); + } +} diff --git a/app/models/concerns/searcheable.rb b/app/models/concerns/searcheable.rb index 7aacf6ea..f353a249 100644 --- a/app/models/concerns/searcheable.rb +++ b/app/models/concerns/searcheable.rb @@ -33,8 +33,10 @@ def sort_order(order_from_params) order_from_params end + end - def search_with_params(params) + included do + scope :search_with_params, ->(params) do self .then { |scope| params[:state].present? ? scope.by_state(params[:state]) : scope } .then { |scope| params[:provider_id].present? ? scope.by_provider(params[:provider_id]) : scope } @@ -42,7 +44,7 @@ def search_with_params(params) .then { |scope| params[:year].present? ? scope.by_year(params[:year]) : scope } .then { |scope| params[:month].present? ? scope.by_month(params[:month]) : scope } .then { |scope| params[:query].present? ? scope.search(params[:query]) : scope } - .then { |scope| scope.order(created_at: sort_order(params[:order])) } + .then { |scope| params[:order].present? ? scope.order(created_at: sort_order(params[:order].to_sym)) : scope } end end end diff --git a/app/models/user.rb b/app/models/user.rb index 172df009..0e35ab8e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,6 +24,13 @@ class User < ApplicationRecord validates :email, presence: true, uniqueness: true, format: URI::MailTo::EMAIL_REGEXP validates :password_digest, presence: true + scope :search_with_params, ->(params) do + self + .then { |scope| params[:email].present? ? scope.where("email ILIKE ?", "%#{params[:email]}%") : scope } + .then { |scope| params[:is_admin].present? ? scope.where(is_admin: params[:is_admin]) : scope } + .then { |scope| scope.order(created_at: params[:order]&.to_sym || :desc) } + end + def topics Topic.where(provider_id: providers.pluck(:id)) end diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 53d21f9c..5c790e0d 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -1,5 +1,42 @@
+
+
+

Search

+
+
+
+ <%= form_for :search, url: users_path, method: :get, data: { controller: "search", search_target: "search", turbo_frame: "user-list", turbo_action: "advance" } do |f| %> +
+
+
+
+ <%= f.label :email %> + <%= f.text_field :email, value: params.dig(:search, :email), class: "form-control", data: { action: "input->search#submit" } %> +
+
+
+
+ <%= f.label "Role" %> + <%= f.select :is_admin, options_for_select([["Admin", "true"], ["Contributor", "false"]], params.dig(:search, :is_admin)), { prompt: "Select user role" }, class: "form-select", data: { action: "change->search#submit" } %> +
+
+
+
+ <%= f.label :order %> + <%= f.select :order, options_for_select([["By most recently added", :desc], ["By least recently added", :asc]], params.dig(:search, :order)), {}, class: "form-select", data: { action: "change->search#submit" } %> +
+
+
+ <%= link_to "Clear", users_path, class: "btn btn-light-secondary me-1 mb-1" %> +
+
+
+ <% end %> +
+
+
+

User List

@@ -20,7 +57,7 @@ <% @users.each do |user| %> <%= user.email %> - <%= user.is_admin %> + <%= user.is_admin ? "✅" : "✖️" %> <%= link_to edit_user_path(user), class: "btn btn-secondary btn-sm pr-2" do %> Edit diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 23db340f..445d40eb 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -68,4 +68,6 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + config.include SystemHelpers, type: :system end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index e2410e26..b440b01b 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -6,6 +6,12 @@ Capybara::Selenium::Driver.new(app, browser: :chrome) end +Capybara.register_driver :selenium_chrome_headless do |app| + options = Selenium::WebDriver::Chrome::Options.new(args: %w[headless disable-gpu window-size=1400,1400]) + + Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) +end + module CapybaraPage def page Capybara.string(response.body) @@ -14,7 +20,7 @@ def page RSpec.configure do |config| config.before(:each, type: :system) do - driven_by :rack_test + driven_by :selenium_chrome_headless end config.before(:each, :debug, type: :system) do diff --git a/spec/support/system_helpers.rb b/spec/support/system_helpers.rb new file mode 100644 index 00000000..381cf252 --- /dev/null +++ b/spec/support/system_helpers.rb @@ -0,0 +1,9 @@ +module SystemHelpers + def login_as(user) + visit root_path + click_link("Sign In") + fill_in "email", with: user.email + fill_in "password", with: user.password + click_button("Sign in") + end +end diff --git a/spec/system/topic_search_spec.rb b/spec/system/topic_search_spec.rb new file mode 100644 index 00000000..0f397425 --- /dev/null +++ b/spec/system/topic_search_spec.rb @@ -0,0 +1,112 @@ +require "rails_helper" + +RSpec.describe "Topics search", type: :system do + let(:admin) { create(:user, :admin, email: "admin@mail.com") } + let(:english) { create(:language, name: "English") } + let(:spanish) { create(:language, name: "Spanish") } + let!(:spanish_active_topic) { create(:topic, language: spanish, title: "Tratamiento del resfriado", created_at: Date.new(2025, 02, 03)) } + let!(:english_active_topic) { create(:topic, language: english, title: "How to treat colds", description: "All the latest information about nasopharyngitis", created_at: Date.new(2025, 03, 04)) } + let!(:english_archived_topic) { create(:topic, :archived, language: english, title: "Obsolete", created_at: Date.new(2023, 02, 01)) } + + before do + login_as(admin) + click_link("Topics") + end + + it "shows all topics" do + expect(page).to have_text(english_active_topic.title) + expect(page).to have_text(spanish_active_topic.title) + expect(page).to have_text(english_archived_topic.title) + end + + context "when searching by title" do + it "only displays topics matching the search" do + fill_in "search_query", with: "tratamiento" + + expect(page).to have_text(spanish_active_topic.title) + expect(page).not_to have_text(english_active_topic.title) + expect(page).not_to have_text(english_archived_topic.title) + end + end + + context "when searching by description" do + it "only displays topics matching the search" do + fill_in "search_query", with: "pharyn" + + expect(page).to have_text(english_active_topic.title) + expect(page).not_to have_text(spanish_active_topic.title) + expect(page).not_to have_text(english_archived_topic.title) + end + end + + context "when searching by language" do + it "only displays topics matching the search" do + select "Spanish", from: "search_language_id" + + expect(page).to have_text(spanish_active_topic.title) + expect(page).not_to have_text(english_active_topic.title) + expect(page).not_to have_text(english_archived_topic.title) + + select "English", from: "search_language_id" + + expect(page).to have_text(english_active_topic.title) + expect(page).to have_text(english_archived_topic.title) + expect(page).not_to have_text(spanish_active_topic.title) + end + end + + context "when searching by year" do + it "only displays topics matching the search" do + select "2025", from: "search_year" + + expect(page).to have_text(spanish_active_topic.title) + expect(page).to have_text(english_active_topic.title) + expect(page).not_to have_text(english_archived_topic.title) + + select "2023", from: "search_year" + + expect(page).to have_text(english_archived_topic.title) + expect(page).not_to have_text(spanish_active_topic.title) + expect(page).not_to have_text(english_active_topic.title) + end + end + + context "when searching by month" do + it "only displays topics matching the search" do + select "2", from: "search_month" + + expect(page).to have_text(spanish_active_topic.title) + expect(page).to have_text(english_archived_topic.title) + expect(page).not_to have_text(english_active_topic.title) + + select "3", from: "search_month" + + expect(page).to have_text(english_active_topic.title) + expect(page).not_to have_text(english_archived_topic.title) + expect(page).not_to have_text(spanish_active_topic.title) + end + end + + context "when searching by state" do + it "only displays topics matching the search" do + select "active", from: "search_state" + + expect(page).to have_text(spanish_active_topic.title) + expect(page).to have_text(english_active_topic.title) + expect(page).not_to have_text(english_archived_topic.title) + + select "archived", from: "search_state" + + expect(page).to have_text(english_archived_topic.title) + expect(page).not_to have_text(spanish_active_topic.title) + expect(page).not_to have_text(english_active_topic.title) + end + end + + context "when sorting" do + it "displays users in the selected order" do + select "asc", from: "search_order" + expect(page).to have_text(/#{english_archived_topic.title}.+#{spanish_active_topic.title}.+#{english_active_topic.title}/m) + end + end +end diff --git a/spec/system/user_search_spec.rb b/spec/system/user_search_spec.rb new file mode 100644 index 00000000..fd1640a9 --- /dev/null +++ b/spec/system/user_search_spec.rb @@ -0,0 +1,49 @@ +require "rails_helper" + +RSpec.describe "User search", type: :system do + let(:admin) { create(:user, :admin, email: "admin@mail.com", created_at: 3.days.ago) } + let!(:martin) { create(:user, email: "martin@mail.com", created_at: 2.days.ago) } + let!(:rosemary) { create(:user, email: "rosemary@mail.com", created_at: 1.day.ago) } + + before do + login_as(admin) + click_link("Users") + end + + it "shows by default the users from most to least recently added" do + expect(page).to have_text(/#{rosemary.email}.+#{martin.email}.+#{admin.email}/m) + end + + context "when searching by email" do + it "only displays users matching the search" do + fill_in "search[email]", with: "mar" + + expect(page).to have_text(rosemary.email) + expect(page).to have_text(martin.email) + expect(page).not_to have_text(admin.email) + end + end + + context "when searching by role" do + it "only displays users matching the search" do + select "Admin", from: "search_is_admin" + + expect(page).to have_text(admin.email) + expect(page).not_to have_text(rosemary.email) + expect(page).not_to have_text(martin.email) + + select "Contributor", from: "search_is_admin" + + expect(page).to have_text(rosemary.email) + expect(page).to have_text(martin.email) + expect(page).not_to have_text(admin.email) + end + end + + context "when sorting" do + it "displays users in the selected order" do + select "By least recently added", from: "search_order" + expect(page).to have_text(/#{admin.email}.+#{martin.email}.+#{rosemary.email}/m) + end + end +end