diff --git a/app/graphql/resolvers/members_resolver.rb b/app/graphql/resolvers/members_resolver.rb index 827db54134a1d4c1d6d02de654733a863e0634c5..3d7894fdd6a055b03d498ab0a9434e6310e5e01e 100644 --- a/app/graphql/resolvers/members_resolver.rb +++ b/app/graphql/resolvers/members_resolver.rb @@ -11,6 +11,10 @@ class MembersResolver < BaseResolver required: false, description: 'Search query.' + argument :sort, ::Types::MemberSortEnum, + required: false, + description: 'sort query.' + def resolve_with_lookahead(**args) authorize!(object) diff --git a/app/graphql/types/member_sort_enum.rb b/app/graphql/types/member_sort_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..f3291dda13b64a9b0f295c02a46a6c91572989e1 --- /dev/null +++ b/app/graphql/types/member_sort_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + class MemberSortEnum < SortEnum + graphql_name 'MemberSort' + description 'Values for sorting members' + + value 'ACCESS_LEVEL_ASC', 'Access level ascending order.', value: :access_level_asc + value 'ACCESS_LEVEL_DESC', 'Access level descending order.', value: :access_level_desc + value 'USER_FULL_NAME_ASC', "User's full name ascending order.", value: :name_asc + value 'USER_FULL_NAME_DESC', "User's full name descending order.", value: :name_desc + end +end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 65fb62a814f815a01a42f66b9ad786ef0839aa53..eccb004b503ed723fcad7264b06691ac6ac5877c 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -43,6 +43,33 @@ def simple_sorts } end + def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:) + reversed_direction = direction == :asc ? :desc : :asc + + # rubocop: disable GitlabSecurity/PublicSend + order = ::Gitlab::Pagination::Keyset::Order.build( + [ + ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: attribute_name, + column_expression: column, + order_expression: column.send(direction).send(nullable), + reversed_order_expression: column.send(reversed_direction).send(nullable), + order_direction: direction, + distinct: false, + add_to_projections: true, + nullable: nullable + ), + ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'id', + order_expression: arel_table['id'].desc + ) + ] + ) + # rubocop: enable GitlabSecurity/PublicSend + + order.apply_cursor_conditions(scope).reorder(order) + end + private def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: []) diff --git a/app/models/issue.rb b/app/models/issue.rb index 1de2075b456fd4c1f7038fe534793b1e651379a9..153747c75df915f6c8a4e9ef4b19a137424bf11f 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -254,32 +254,6 @@ class << self alias_method :with_state, :with_state_id alias_method :with_states, :with_state_ids - def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:) - reversed_direction = direction == :asc ? :desc : :asc - - # rubocop: disable GitlabSecurity/PublicSend - order = ::Gitlab::Pagination::Keyset::Order.build( - [ - ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: attribute_name, - column_expression: column, - order_expression: column.send(direction).send(nullable), - reversed_order_expression: column.send(reversed_direction).send(nullable), - order_direction: direction, - distinct: false, - add_to_projections: true, - nullable: nullable - ), - ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'id', - order_expression: arel_table['id'].desc - ) - ]) - # rubocop: enable GitlabSecurity/PublicSend - - order.apply_cursor_conditions(scope).order(order) - end - override :order_upvotes_desc def order_upvotes_desc reorder(upvotes_count: :desc) diff --git a/app/models/member.rb b/app/models/member.rb index 186fcd8759f252342a62e40129046901533b8410..c5351d5447bee894588529dacaf16662fbcee2d7 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -184,14 +184,85 @@ class Member < ApplicationRecord unscoped.from(distinct_members, :members) end - scope :order_name_asc, -> { left_join_users.reorder(User.arel_table[:name].asc.nulls_last) } - scope :order_name_desc, -> { left_join_users.reorder(User.arel_table[:name].desc.nulls_last) } - scope :order_recent_sign_in, -> { left_join_users.reorder(User.arel_table[:last_sign_in_at].desc.nulls_last) } - scope :order_oldest_sign_in, -> { left_join_users.reorder(User.arel_table[:last_sign_in_at].asc.nulls_last) } - scope :order_recent_last_activity, -> { left_join_users.reorder(User.arel_table[:last_activity_on].desc.nulls_last) } - scope :order_oldest_last_activity, -> { left_join_users.reorder(User.arel_table[:last_activity_on].asc.nulls_first) } - scope :order_recent_created_user, -> { left_join_users.reorder(User.arel_table[:created_at].desc.nulls_last) } - scope :order_oldest_created_user, -> { left_join_users.reorder(User.arel_table[:created_at].asc.nulls_first) } + scope :order_name_asc, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_full_name', + column: User.arel_table[:name], + direction: :asc, + nullable: :nulls_last + ) + end + + scope :order_name_desc, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_full_name', + column: User.arel_table[:name], + direction: :desc, + nullable: :nulls_last + ) + end + + scope :order_oldest_sign_in, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_sign_in_at', + column: User.arel_table[:last_sign_in_at], + direction: :asc, + nullable: :nulls_last + ) + end + + scope :order_recent_sign_in, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_sign_in_at', + column: User.arel_table[:last_sign_in_at], + direction: :desc, + nullable: :nulls_last + ) + end + + scope :order_oldest_last_activity, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_activity_on', + column: User.arel_table[:last_activity_on], + direction: :asc, + nullable: :nulls_first + ) + end + + scope :order_recent_last_activity, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_activity_on', + column: User.arel_table[:last_activity_on], + direction: :desc, + nullable: :nulls_last + ) + end + + scope :order_oldest_created_user, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_created_at', + column: User.arel_table[:created_at], + direction: :asc, + nullable: :nulls_first + ) + end + + scope :order_recent_created_user, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_created_at', + column: User.arel_table[:created_at], + direction: :desc, + nullable: :nulls_last + ) + end scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) } diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index eecdcf8b69ab5153a1e844f495631e003e18126f..7af7272af7a54c8aab79b70729f9421a4b716587 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -12679,6 +12679,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="groupgroupmembersaccesslevels"></a>`accessLevels` | [`[AccessLevelEnum!]`](#accesslevelenum) | Filter members by the given access levels. | | <a id="groupgroupmembersrelations"></a>`relations` | [`[GroupMemberRelation!]`](#groupmemberrelation) | Filter members by the given member relations. | | <a id="groupgroupmemberssearch"></a>`search` | [`String`](#string) | Search query. | +| <a id="groupgroupmemberssort"></a>`sort` | [`MemberSort`](#membersort) | sort query. | ##### `Group.issues` @@ -16594,6 +16595,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | <a id="projectprojectmembersrelations"></a>`relations` | [`[ProjectMemberRelation!]`](#projectmemberrelation) | Filter members by the given member relations. | | <a id="projectprojectmemberssearch"></a>`search` | [`String`](#string) | Search query. | +| <a id="projectprojectmemberssort"></a>`sort` | [`MemberSort`](#membersort) | sort query. | ##### `Project.release` @@ -20315,6 +20317,25 @@ Possible identifier types for a measurement. | <a id="measurementidentifierprojects"></a>`PROJECTS` | Project count. | | <a id="measurementidentifierusers"></a>`USERS` | User count. | +### `MemberSort` + +Values for sorting members. + +| Value | Description | +| ----- | ----------- | +| <a id="membersortaccess_level_asc"></a>`ACCESS_LEVEL_ASC` | Access level ascending order. | +| <a id="membersortaccess_level_desc"></a>`ACCESS_LEVEL_DESC` | Access level descending order. | +| <a id="membersortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. | +| <a id="membersortcreated_desc"></a>`CREATED_DESC` | Created at descending order. | +| <a id="membersortupdated_asc"></a>`UPDATED_ASC` | Updated at ascending order. | +| <a id="membersortupdated_desc"></a>`UPDATED_DESC` | Updated at descending order. | +| <a id="membersortuser_full_name_asc"></a>`USER_FULL_NAME_ASC` | User's full name ascending order. | +| <a id="membersortuser_full_name_desc"></a>`USER_FULL_NAME_DESC` | User's full name descending order. | +| <a id="membersortcreated_asc"></a>`created_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_ASC`. | +| <a id="membersortcreated_desc"></a>`created_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_DESC`. | +| <a id="membersortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. | +| <a id="membersortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. | + ### `MergeRequestNewState` New state to apply to a merge request. diff --git a/spec/graphql/resolvers/group_members_resolver_spec.rb b/spec/graphql/resolvers/group_members_resolver_spec.rb index bd0b487006210014d33c879b898e075f0ca6d410..d860b87875e0d7ef77a1eb53b599f8cab3e7e45e 100644 --- a/spec/graphql/resolvers/group_members_resolver_spec.rb +++ b/spec/graphql/resolvers/group_members_resolver_spec.rb @@ -2,9 +2,11 @@ require 'spec_helper' -RSpec.describe Resolvers::GroupMembersResolver do +RSpec.describe 'Resolvers::GroupMembersResolver' do include GraphqlHelpers + let(:described_class) { Resolvers::GroupMembersResolver } + specify do expect(described_class).to have_nullable_graphql_type(Types::GroupMemberType.connection_type) end diff --git a/spec/graphql/resolvers/project_members_resolver_spec.rb b/spec/graphql/resolvers/project_members_resolver_spec.rb index 2f4145b321519725909151e473f107b103eb25e4..c38cb3d157bde91d747ad7bdf4fd2ae0c15f0038 100644 --- a/spec/graphql/resolvers/project_members_resolver_spec.rb +++ b/spec/graphql/resolvers/project_members_resolver_spec.rb @@ -2,9 +2,11 @@ require 'spec_helper' -RSpec.describe Resolvers::ProjectMembersResolver do +RSpec.describe 'Resolvers::ProjectMembersResolver' do include GraphqlHelpers + let(:described_class) { Resolvers::ProjectMembersResolver } + it_behaves_like 'querying members with a group' do let_it_be(:project) { create(:project, group: group_1) } let_it_be(:resource_member) { create(:project_member, user: user_1, project: project) } diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb index f0c50e20a723bccfbd5d297ebc6a92c748253ba0..5cba8baa8298f89122879a2be7bb874280caf0c0 100644 --- a/spec/support/shared_examples/graphql/members_shared_examples.rb +++ b/spec/support/shared_examples/graphql/members_shared_examples.rb @@ -52,6 +52,15 @@ expect(subject).to contain_exactly(resource_member, group_1_member, root_group_member) end + context 'with sort options' do + let(:args) { { sort: 'name_asc' } } + + it 'searches users by user name' do + # the order is important here + expect(subject.items).to eq([root_group_member, resource_member, group_1_member]) + end + end + context 'with search' do context 'when the search term matches a user' do let(:args) { { search: 'test' } }