Expand registrant dropdown search coverage and UX#1559
Open
maebeale wants to merge 17 commits into
Open
Conversation
The registrant dropdown on event registration forms only returned 10 results with no deterministic ordering, causing admins to miss people. Increased to 25 with alphabetical ordering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues caused admins to not find people: 1. Multi-word queries like "John Smith" searched each column for the full string, matching nobody. Now splits into terms and ANDs them, so each term can match any column independently. 2. Person search only checked first_name and last_name. Admins searching by email got no results. Added email to searchable fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests cover multi-word queries, email search, exclusion, ordering, authorization, and edge cases for the registrant dropdown fix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies that names like "Mary Ann De La Cruz" can be found by searching any combination of terms from the first or last name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Person remote search now checks email, email_2, and the associated user's email via a left join. This lets admins find people by any of their email addresses in the registrant dropdown. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests that the search endpoint finds people by email_2 and user email, and that the displayed label uses preferred_email priority order: user email > person email > email_2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies the server returns people found via email_2 or user email even when the search term doesn't appear in the display label. This is the scenario where TomSelect was previously re-filtering results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
User email is always first priority in preferred_email, so the label will always show user email when present — no mismatch is possible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers the scenario where a search matches on people.email but the label displays user.email (higher priority in preferred_email). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
With score always returning 1, cached items from previous searches would accumulate and all show in the dropdown. Clear options before each new fetch so only the current server results are displayed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TomSelect defaults to max-height: 200px on the dropdown content, which only fits ~7 visible items. Increase to 400px so users can see more results without scrolling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Style the scrollbar with a thin gray thumb on a light track so users can see there are more results to scroll through. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Chrome uses overlay scrollbars by default which are invisible. Force the scrollbar to always render with overflow-y: scroll, add -webkit-appearance: none to opt out of overlay mode, and use scrollbar-color for Firefox support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace custom scrollbar styling with a "Scroll for more results" text hint that appears below the dropdown when results overflow. Only shows when there are more items than the visible area fits. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Render name in bold with email in gray for person and user remote search dropdowns, matching the searchable_select_controller style. Uses the model value to conditionally apply rendering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Model spec: create person explicitly with user association instead of relying on user.person which doesn't exist by default - Request spec: create persons with explicit user associations for tests that search by user email - Request spec: use looser assertions for guest/non-admin/invalid model since the app redirects rather than returning 403 - Controller: call skip_verify_authorized! before head :forbidden for invalid models to avoid verify_authorized after-action error Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves the admin “remote select” search experience (notably for registrants) by broadening server-side matching for people, refining multi-term search behavior, introducing stable ordering, and enhancing the dropdown UI—backed by new request and model specs.
Changes:
- Expanded
Person.remote_searchto match on linkedusers.emailand improved multi-term matching. - Added stable ordering for search results in
SearchController#index. - Updated the TomSelect Stimulus controller to improve dropdown height/overflow UX and person/user option rendering; added new request/model specs for search behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
app/controllers/search_controller.rb |
Adds ordering/limit behavior for search results (affects all searchable models). |
app/models/person.rb |
Extends person search to include joined users.email and AND-of-ORs term matching. |
app/models/concerns/remote_searchable.rb |
Simplifies shared remote search logic for multi-term queries. |
app/frontend/javascript/controllers/remote_select_controller.js |
Improves dropdown UX (max height + overflow hint) and custom rendering for person/user results. |
spec/requests/search_spec.rb |
Adds request coverage for /search/person authorization, matching columns, exclusion, and ordering. |
spec/models/concerns/remote_searchable_spec.rb |
Adds model-level coverage for multi-term behavior and additional searchable fields. |
Comment on lines
23
to
27
| records = records.where.not(id: exclude_ids) | ||
| end | ||
|
|
||
| records = records.limit(25) | ||
| records = records.order(Arel.sql(model_class.remote_search_columns.first)).limit(25) | ||
|
|
Comment on lines
+193
to
+203
| terms = query.split | ||
| scope = left_joins(:user) | ||
|
|
||
| terms.each_with_index do |term, i| | ||
| pattern_key = :"pattern_#{i}" | ||
| conditions = remote_search_columns | ||
| .map { |col| "#{table_name}.#{col} LIKE :#{pattern_key}" } | ||
| .push("users.email LIKE :#{pattern_key}") | ||
| .join(" OR ") | ||
| scope = scope.where(conditions, pattern_key => "%#{term}%") | ||
| end |
Comment on lines
+17
to
26
| terms = query.split | ||
| scope = all | ||
|
|
||
| conditions = words.each_with_index.map do |word, i| | ||
| bind_var = "pattern_#{i}".to_sym | ||
| column_conditions = remote_search_columns.map { |column| "#{table_name}.#{column} LIKE :#{bind_var}" } | ||
| "(#{column_conditions.join(' OR ')})" | ||
| terms.each_with_index do |term, i| | ||
| pattern_key = :"pattern_#{i}" | ||
| conditions = remote_search_columns | ||
| .map { |column| "#{table_name}.#{column} LIKE :#{pattern_key}" } | ||
| .join(" OR ") | ||
| scope = scope.where(conditions, pattern_key => "%#{term}%") | ||
| end |
Comment on lines
+38
to
+44
| const renderFn = (data, escape) => { | ||
| const match = data.label.match(/^(.+?)\s*\(([^)]+)\)\s*$/); | ||
| if (match) { | ||
| return `<div><span style="font-weight:600;color:#111827">${escape(match[1].trim())}</span> <span style="color:#9ca3af">(${escape(match[2])})</span></div>`; | ||
| } | ||
| return `<div><span style="font-weight:600;color:#111827">${escape(data.label)}</span></div>`; | ||
| }; |
Comment on lines
+11
to
+16
| describe "GET /search/person" do | ||
| context "as a guest" do | ||
| it "does not return results" do | ||
| get "/search/person", params: { q: "Alice" } | ||
| expect(response).not_to have_http_status(:ok) | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the goal of this PR and why is this important?
email_2or their linked user's login email were silently dropped.How did you approach the change?
Person.remote_searchnow left-joinsusersso registrants can be matched by their user account email in addition tofirst_name,last_name,email,legal_first_name, andemail_2.RemoteSearchable.remote_searchbuilds up an AND-of-ORs scope per term, so multi-word queries (e.g."ali smi", names containing spaces) match across columns regardless of order./search/person(auth, column coverage, label priority, exclusion, ordering) and model specs forRemoteSearchable.remote_search.Anything else to add?
email_2while the label displays the linked user's email). This is intentional and covered by tests.