Skip to content

Expand registrant dropdown search coverage and UX#1559

Open
maebeale wants to merge 17 commits into
mainfrom
maebeale/expand-registrant-dropdown
Open

Expand registrant dropdown search coverage and UX#1559
maebeale wants to merge 17 commits into
mainfrom
maebeale/expand-registrant-dropdown

Conversation

@maebeale

@maebeale maebeale commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

What is the goal of this PR and why is this important?

  • The registrant search dropdown was missing people who should have matched a query, making it hard for admins to find and add the right person.
  • People could only be found by columns shown in the dropdown label; matches on a person's email_2 or their linked user's login email were silently dropped.
  • The dropdown also showed too few results with no indication that more existed below the fold.

How did you approach the change?

  • Broaden person searchPerson.remote_search now left-joins users so registrants can be matched by their user account email in addition to first_name, last_name, email, legal_first_name, and email_2.
  • Simplify multi-term matchingRemoteSearchable.remote_search builds 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.
  • Stable ordering — search results are now ordered by the first search column so the dropdown returns results alphabetically.
  • Dropdown UX — taller dropdown (max-height 400px) with a "Scroll for more results" hint when the list overflows, and bold name / muted email styling for person and user dropdowns.
  • Tests — added request specs for /search/person (auth, column coverage, label priority, exclusion, ordering) and model specs for RemoteSearchable.remote_search.

Anything else to add?

  • Note: results can match on fields not shown in the label (e.g. a person matched by email_2 while the label displays the linked user's email). This is intentional and covered by tests.

maebeale and others added 17 commits March 8, 2026 07:34
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>
Copilot AI review requested due to automatic review settings June 5, 2026 13:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_search to match on linked users.email and 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 thread app/models/person.rb
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants