diff --git a/app/graphql/resolvers/organizations/projects_resolver.rb b/app/graphql/resolvers/organizations/projects_resolver.rb index 836fe0ae0597316c9317f3851b634e03f2fb6f74..310e2d8454073e8154abbed351e4dbca85f77df4 100644 --- a/app/graphql/resolvers/organizations/projects_resolver.rb +++ b/app/graphql/resolvers/organizations/projects_resolver.rb @@ -11,8 +11,21 @@ class ProjectsResolver < BaseResolver alias_method :organization, :object - def resolve - ::ProjectsFinder.new(current_user: current_user, params: { organization: organization }).execute + argument :sort, GraphQL::Types::String, + required: false, + description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \ + "for example: `id_desc` or `name_asc`", + alpha: { milestone: '16.9' } + + def resolve(**args) + project_finder_params = args.merge(organization: organization) + + if %w[path_asc path_desc].include?(project_finder_params[:sort]) && + Feature.disabled?(:project_path_sort, current_user, type: :gitlab_com_derisk) + project_finder_params.delete(:sort) + end + + ::ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute end end end diff --git a/app/models/project.rb b/app/models/project.rb index 19847978acbdd7c9a576ce6cb16103996cb31d47..7012cb7b25bcc84be66ce3ee9e7e2ba4631b4bf7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -634,6 +634,8 @@ def self.integration_association_name(name) scope :sorted_by_updated_desc, -> { reorder(self.arel_table['updated_at'].desc) } scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) } scope :sorted_by_stars_asc, -> { reorder(self.arel_table['star_count'].asc) } + scope :sorted_by_path_asc, -> { reorder(self.arel_table['path'].asc) } + scope :sorted_by_path_desc, -> { reorder(self.arel_table['path'].desc) } # Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name scope :projects_order_id_asc, -> { reorder(self.arel_table['id'].asc) } scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) } @@ -956,6 +958,10 @@ def sort_by_attribute(method) sorted_by_updated_desc when 'latest_activity_asc' sorted_by_updated_asc + when 'path_asc' + sorted_by_path_asc + when 'path_desc' + sorted_by_path_desc when 'stars_desc' sorted_by_stars_desc when 'stars_asc' diff --git a/config/feature_flags/gitlab_com_derisk/project_path_sort.yml b/config/feature_flags/gitlab_com_derisk/project_path_sort.yml new file mode 100644 index 0000000000000000000000000000000000000000..a550bbea54dba06d6bd6f9852f74e57ae7760d0d --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/project_path_sort.yml @@ -0,0 +1,9 @@ +--- +name: project_path_sort +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409312 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142390 +rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17492 +milestone: '16.9' +group: group::tenant scale +type: gitlab_com_derisk +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 27cbeeb3d9cfa985e861476b90862f59e870b4c4..1d2b8b0143e25c4211d26e8ba6e87ea4e9022da5 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -23550,7 +23550,6 @@ Active period time range for on-call rotation. | <a id="organizationname"></a>`name` **{warning-solid}** | [`String!`](#string) | **Introduced** in 16.4. **Status**: Experiment. Name of the organization. | | <a id="organizationorganizationusers"></a>`organizationUsers` **{warning-solid}** | [`OrganizationUserConnection!`](#organizationuserconnection) | **Introduced** in 16.4. **Status**: Experiment. Users with access to the organization. | | <a id="organizationpath"></a>`path` **{warning-solid}** | [`String!`](#string) | **Introduced** in 16.4. **Status**: Experiment. Path of the organization. | -| <a id="organizationprojects"></a>`projects` **{warning-solid}** | [`ProjectConnection!`](#projectconnection) | **Introduced** in 16.8. **Status**: Experiment. Projects within this organization that the user has access to. | | <a id="organizationweburl"></a>`webUrl` **{warning-solid}** | [`String!`](#string) | **Introduced** in 16.6. **Status**: Experiment. Web URL of the organization. | #### Fields with arguments @@ -23576,6 +23575,26 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="organizationgroupssearch"></a>`search` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.4. **Status**: Experiment. Search query for group name or full path. | | <a id="organizationgroupssort"></a>`sort` **{warning-solid}** | [`OrganizationGroupSort`](#organizationgroupsort) | **Introduced** in 16.4. **Status**: Experiment. Criteria to sort organization groups by. | +##### `Organization.projects` + +Projects within this organization that the user has access to. + +NOTE: +**Introduced** in 16.8. +**Status**: Experiment. + +Returns [`ProjectConnection!`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="organizationprojectssort"></a>`sort` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.9. **Status**: Experiment. Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. | + ### `OrganizationStateCounts` Represents the total number of organizations for the represented states. diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 03e7b69181fa5758a4d55343c35e7957e9bcdd16..d7037df66782be8ba8fd23d6f2ec27042e629436 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2112,6 +2112,18 @@ def has_external_wiki expect(projects).to eq([project3, project1, project2]) end + + it 'reorders the input relation by path asc' do + projects = described_class.sort_by_attribute(:path_asc) + + expect(projects).to eq([project1, project2, project3].sort_by(&:path)) + end + + it 'reorders the input relation by path desc' do + projects = described_class.sort_by_attribute(:path_desc) + + expect(projects).to eq([project1, project2, project3].sort_by(&:path).reverse) + end end describe '.with_shared_runners_enabled' do diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb index 14becd52e93fbb384e6dddcee08028931dca4ec0..37256c22c231595d72819b792170dac87ad15bcb 100644 --- a/spec/requests/api/graphql/organizations/organization_query_spec.rb +++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb @@ -4,6 +4,7 @@ RSpec.describe 'getting organization information', feature_category: :cell do include GraphqlHelpers + using RSpec::Parameterized::TableSyntax let(:query) { graphql_query_for(:organization, { id: organization.to_global_id }, organization_fields) } let(:current_user) { user } @@ -154,8 +155,6 @@ def run_query end context 'with `sort` argument' do - using RSpec::Parameterized::TableSyntax - let(:authorized_groups) { [public_group, private_group, other_group] } where(:field, :direction, :sorted_groups) do @@ -216,14 +215,46 @@ def run_query expect(projects).to contain_exactly(a_graphql_entity_for(project)) end - it_behaves_like 'sorted paginated query' do - include_context 'no sort argument' - + describe 'project sorting' do let_it_be(:another_project) { create(:project, organization: organization) { |p| p.add_developer(user) } } let_it_be(:another_project2) { create(:project, organization: organization) { |p| p.add_developer(user) } } - let(:first_param) { 2 } - let(:data_path) { [:organization, :projects] } - let(:all_records) { [another_project2, another_project, project].map { |p| global_id_of(p).to_s } } + let_it_be(:all_projects) { [another_project2, another_project, project] } + let_it_be(:first_param) { 2 } + let_it_be(:data_path) { [:organization, :projects] } + + where(:field, :direction, :sorted_projects) do + 'id' | 'asc' | lazy { all_projects.sort_by(&:id) } + 'id' | 'desc' | lazy { all_projects.sort_by(&:id).reverse } + 'name' | 'asc' | lazy { all_projects.sort_by(&:name) } + 'name' | 'desc' | lazy { all_projects.sort_by(&:name).reverse } + 'path' | 'asc' | lazy { all_projects.sort_by(&:path) } + 'path' | 'desc' | lazy { all_projects.sort_by(&:path).reverse } + end + + with_them do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { "#{field}_#{direction}" } + let(:all_records) { sorted_projects.map { |p| global_id_of(p).to_s } } + end + end + + context 'with project_path_sort disabled sorts the projects by id_desc' do + before do + stub_feature_flags(project_path_sort: false) + end + + where(:field, :direction) do + 'path' | 'asc' + 'path' | 'desc' + end + + with_them do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { "#{field}_#{direction}" } + let(:all_records) { all_projects.sort_by(&:id).reverse.map { |p| global_id_of(p).to_s } } + end + end + end end def pagination_query(params)