-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Sort selected users first in filter dropdowns #2747
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
9940622
73d9492
a87180e
528e9c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,6 +28,21 @@ def users | |||||||||||||||||||||||||||||||||||||||||||||
| @users ||= account.users.active.alphabetically | ||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Returns users sorted with selected assignees first | ||||||||||||||||||||||||||||||||||||||||||||||
| def users_for_assignee_filter | ||||||||||||||||||||||||||||||||||||||||||||||
| @users_for_assignee_filter ||= sort_users_by_selection(filter.assignees) | ||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Returns users sorted with selected creators first | ||||||||||||||||||||||||||||||||||||||||||||||
| def users_for_creator_filter | ||||||||||||||||||||||||||||||||||||||||||||||
| @users_for_creator_filter ||= sort_users_by_selection(filter.creators) | ||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Returns users sorted with selected closers first | ||||||||||||||||||||||||||||||||||||||||||||||
| def users_for_closer_filter | ||||||||||||||||||||||||||||||||||||||||||||||
| @users_for_closer_filter ||= sort_users_by_selection(filter.closers) | ||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def filters | ||||||||||||||||||||||||||||||||||||||||||||||
| @filters ||= user.filters.all | ||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -82,4 +97,14 @@ def cache_key | |||||||||||||||||||||||||||||||||||||||||||||
| def account | ||||||||||||||||||||||||||||||||||||||||||||||
| user.account | ||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def sort_users_by_selection(selected_users) | ||||||||||||||||||||||||||||||||||||||||||||||
| all_users = users.to_a | ||||||||||||||||||||||||||||||||||||||||||||||
| selected_ids = selected_users.pluck(:id).to_set | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| selected = all_users.select { |u| selected_ids.include?(u.id) } | ||||||||||||||||||||||||||||||||||||||||||||||
| unselected = all_users.reject { |u| selected_ids.include?(u.id) } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
| selected_ids = selected_users.pluck(:id).to_set | |
| selected = all_users.select { |u| selected_ids.include?(u.id) } | |
| unselected = all_users.reject { |u| selected_ids.include?(u.id) } | |
| selected_array = selected_users.to_a | |
| selected_ids = selected_array.map(&:id).to_set | |
| selected, unselected = all_users.partition { |u| selected_ids.include?(u.id) } |
Outdated
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sort_users_by_selection scans all_users twice via select + reject. Consider using partition (single pass) to split selected vs unselected for the same result with less work, especially when accounts have many active users.
| selected = all_users.select { |u| selected_ids.include?(u.id) } | |
| unselected = all_users.reject { |u| selected_ids.include?(u.id) } | |
| selected, unselected = all_users.partition { |u| selected_ids.include?(u.id) } |
Outdated
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are trailing spaces on the blank lines in this block (will be flagged by RuboCop's trailing whitespace checks). Please remove the whitespace on empty lines.
| selected = all_users.select { |u| selected_ids.include?(u.id) } | |
| unselected = all_users.reject { |u| selected_ids.include?(u.id) } | |
| selected = all_users.select { |u| selected_ids.include?(u.id) } | |
| unselected = all_users.reject { |u| selected_ids.include?(u.id) } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require "test_helper" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class User::FilteringTest < ActiveSupport::TestCase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setup do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @user = users(:david) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @filter = @user.filters.create! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| test "users_for_assignee_filter sorts selected assignees first" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Create some test users | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alice = @user.account.users.create!(name: "Alice", email_address: "alice@test.com") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bob = @user.account.users.create!(name: "Bob", email_address: "bob@test.com") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| charlie = @user.account.users.create!(name: "Charlie", email_address: "charlie@test.com") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Select Bob and Charlie as assignees | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @filter.assignees = [charlie, bob] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @filter.save! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filtering = User::Filtering.new(@user, @filter) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = filtering.users_for_assignee_filter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Selected users (Bob, Charlie) should appear first, both alphabetically sorted | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_equal "Bob", result[0].name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_equal "Charlie", result[1].name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Alice (unselected) should appear after selected users | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_equal "Alice", result.find { |u| u.name == "Alice" }.name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert result.index { |u| u.name == "Alice" } > 1, "Unselected user should appear after selected users" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| test "users_for_creator_filter sorts selected creators first" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alice = @user.account.users.create!(name: "Alice", email_address: "alice@test.com") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bob = @user.account.users.create!(name: "Bob", email_address: "bob@test.com") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+42
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @filter.creators = [bob] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @filter.save! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filtering = User::Filtering.new(@user, @filter) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = filtering.users_for_creator_filter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_equal "Bob", result.first.name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+49
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_equal "Bob", result.first.name | |
| # Selected creators should appear first | |
| assert_equal "Bob", result.first.name | |
| # Partition result into selected and unselected users | |
| selected_creators = result.select { |u| @filter.creators.include?(u) } | |
| unselected_creators = result.reject { |u| @filter.creators.include?(u) } | |
| # All selected creators should come before any unselected creators | |
| if selected_creators.any? && unselected_creators.any? | |
| last_selected_index = result.index(selected_creators.last) | |
| first_unselected_index = result.index(unselected_creators.first) | |
| assert last_selected_index < first_unselected_index, | |
| "Selected creators should all appear before unselected creators" | |
| end | |
| # Selected creators should be alphabetically sorted | |
| selected_names = selected_creators.map(&:name) | |
| assert_equal selected_names.sort_by(&:downcase), selected_names, | |
| "Selected creators should be alphabetically ordered" | |
| # Unselected creators should also be alphabetically sorted | |
| unselected_names = unselected_creators.map(&:name) | |
| assert_equal unselected_names.sort_by(&:downcase), unselected_names, | |
| "Unselected creators should be alphabetically ordered" |
Outdated
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue: email_address is not a User attribute in this app; it’s on Identity. Create an Identity record and associate it via identity: when creating the user for this test.
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test also uses Identity.find_or_create_by! with a static email address. Prefer Identity.create! (or a helper generating unique emails) to avoid cross-test coupling and to keep test data clearly owned by the test.
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test only asserts that the selected user is first, but it doesn't verify that unselected users come after the selected group and remain alphabetically ordered. Adding those assertions would better lock in the intended behavior for the closer dropdown.
| assert_equal "Alice", result.first.name | |
| # Selected users (closers) should appear first | |
| assert_equal "Alice", result.first.name | |
| # Verify that all selected closers appear before any unselected users | |
| selected_users = result.select { |u| @filter.closers.include?(u) } | |
| unselected_users = result.reject { |u| @filter.closers.include?(u) } | |
| if selected_users.any? && unselected_users.any? | |
| selected_indices = selected_users.map { |u| result.index(u) } | |
| unselected_indices = unselected_users.map { |u| result.index(u) } | |
| assert selected_indices.max < unselected_indices.min, | |
| "Selected closers should come before all unselected users" | |
| end | |
| # Unselected users should remain alphabetically ordered | |
| unselected_names = unselected_users.map(&:name) | |
| assert_equal unselected_names.sort_by(&:downcase), | |
| unselected_names, | |
| "Unselected users should be alphabetically ordered" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned about performance here, this will load all user records into memory and then filter them there. This could cause problems on larger accounts.
Did you consider plucking the IDs of the selected users and then ordering them using a case statement in SQL?
It's more work, but it will be more efficient than loading all users into memory to filter out a few.
This could be a scope so you wouldn't need separate methods for each filter.