diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb index b9e777f27a0538f6ea488962334e2eba608525ca..1cb030c67c37701b6bec47eeadb38d1c08f02211 100644 --- a/app/models/ci/catalog/listing.rb +++ b/app/models/ci/catalog/listing.rb @@ -14,16 +14,25 @@ def initialize(namespace, current_user) @current_user = current_user end - def resources - Ci::Catalog::Resource - .joins(:project).includes(:project) - .merge(projects_in_namespace_visible_to_user) + def resources(sort: nil) + case sort.to_s + when 'name_desc' then all_resources.order_by_name_desc + when 'name_asc' then all_resources.order_by_name_asc + else + all_resources.order_by_created_at_desc + end end private attr_reader :namespace, :current_user + def all_resources + Ci::Catalog::Resource + .joins(:project).includes(:project) + .merge(projects_in_namespace_visible_to_user) + end + def projects_in_namespace_visible_to_user Project .in_namespace(namespace.self_and_descendant_ids) diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb index bb4584aacae4bf4d23dc76d92044c498e7dac10a..f400b8fe0469672dcdd10cb8d80558a7857c4ee2 100644 --- a/app/models/ci/catalog/resource.rb +++ b/app/models/ci/catalog/resource.rb @@ -13,6 +13,9 @@ class Resource < ::ApplicationRecord belongs_to :project scope :for_projects, ->(project_ids) { where(project_id: project_ids) } + scope :order_by_created_at_desc, -> { reorder(created_at: :desc) } + scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) } + scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) } delegate :avatar_path, :description, :name, to: :project diff --git a/app/models/project.rb b/app/models/project.rb index 224193fba0830554c355d199c5c130af5a47abf5..6acc88283d88ecef84f1ef27e19f8956e9ed719a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -602,6 +602,42 @@ def self.integration_association_name(name) .or(arel_table[:storage_version].eq(nil))) end + scope :sorted_by_name_desc, -> { + keyset_order = Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :name, + column_expression: Project.arel_table[:name], + order_expression: Project.arel_table[:name].desc, + distinct: false, + nullable: :nulls_last + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :id, + order_expression: Project.arel_table[:id].desc + ) + ]) + + reorder(keyset_order) + } + + scope :sorted_by_name_asc, -> { + keyset_order = Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :name, + column_expression: Project.arel_table[:name], + order_expression: Project.arel_table[:name].asc, + distinct: false, + nullable: :nulls_last + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :id, + order_expression: Project.arel_table[:id].asc + ) + ]) + + reorder(keyset_order) + } + scope :sorted_by_updated_asc, -> { reorder(self.arel_table['updated_at'].asc) } scope :sorted_by_updated_desc, -> { reorder(self.arel_table['updated_at'].desc) } scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) } diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 9f6f141a877a4970f97ec7d345e359eeb71c40dc..2aabd11060565049fa02d3a9d81f5f02663aec8b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -74,6 +74,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | <a id="querycicatalogresourcesprojectpath"></a>`projectPath` | [`ID`](#id) | Project with the namespace catalog. | +| <a id="querycicatalogresourcessort"></a>`sort` | [`CiCatalogResourceSort`](#cicatalogresourcesort) | Sort Catalog Resources by given criteria. | ### `Query.ciConfig` @@ -23733,6 +23734,23 @@ Types of blob viewers. | <a id="blobviewerstyperich"></a>`rich` | Rich blob viewers type. | | <a id="blobviewerstypesimple"></a>`simple` | Simple blob viewers type. | +### `CiCatalogResourceSort` + +Values for sorting catalog resources. + +| Value | Description | +| ----- | ----------- | +| <a id="cicatalogresourcesortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. | +| <a id="cicatalogresourcesortcreated_desc"></a>`CREATED_DESC` | Created at descending order. | +| <a id="cicatalogresourcesortname_asc"></a>`NAME_ASC` | Name by ascending order. | +| <a id="cicatalogresourcesortname_desc"></a>`NAME_DESC` | Name by descending order. | +| <a id="cicatalogresourcesortupdated_asc"></a>`UPDATED_ASC` | Updated at ascending order. | +| <a id="cicatalogresourcesortupdated_desc"></a>`UPDATED_DESC` | Updated at descending order. | +| <a id="cicatalogresourcesortcreated_asc"></a>`created_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_ASC`. | +| <a id="cicatalogresourcesortcreated_desc"></a>`created_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_DESC`. | +| <a id="cicatalogresourcesortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. | +| <a id="cicatalogresourcesortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. | + ### `CiConfigIncludeType` Include type. diff --git a/ee/app/graphql/resolvers/ci/catalog/resources_resolver.rb b/ee/app/graphql/resolvers/ci/catalog/resources_resolver.rb index c233ab6e412446ae464ddab6f8351a2c51dfb294..a8fb43a695127e9cd232c85ded7b8dc61d08ad2b 100644 --- a/ee/app/graphql/resolvers/ci/catalog/resources_resolver.rb +++ b/ee/app/graphql/resolvers/ci/catalog/resources_resolver.rb @@ -11,14 +11,18 @@ class ResourcesResolver < BaseResolver type ::Types::Ci::Catalog::ResourceType.connection_type, null: true + argument :sort, ::Types::Ci::Catalog::ResourceSortEnum, + required: false, + description: 'Sort Catalog Resources by given criteria.' + argument :project_path, GraphQL::Types::ID, required: false, description: 'Project with the namespace catalog.' - def resolve(project_path:) + def resolve(project_path:, sort: nil) project = authorized_find!(project_path: project_path) - ::Ci::Catalog::Listing.new(project.root_namespace, context[:current_user]).resources + ::Ci::Catalog::Listing.new(project.root_namespace, context[:current_user]).resources(sort: sort) end private diff --git a/ee/app/graphql/types/ci/catalog/resource_sort_enum.rb b/ee/app/graphql/types/ci/catalog/resource_sort_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..473fb9b36cc67f34481d71500ec7abb672f12d9c --- /dev/null +++ b/ee/app/graphql/types/ci/catalog/resource_sort_enum.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + module Catalog + class ResourceSortEnum < SortEnum + graphql_name 'CiCatalogResourceSort' + description 'Values for sorting catalog resources' + + value 'NAME_ASC', 'Name by ascending order.', value: :name_asc + value 'NAME_DESC', 'Name by descending order.', value: :name_desc + end + end + end +end diff --git a/ee/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb b/ee/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb index 15888f9593fbd7b347f119100733dea40b488474..aad71e58f7d9efde78b17954ba510d1d0062d867 100644 --- a/ee/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb @@ -6,21 +6,40 @@ include GraphqlHelpers let_it_be(:namespace) { create(:group) } - let_it_be(:project_1) { create(:project, name: 'Component Repository 1', namespace: namespace) } - let_it_be(:project_2) { create(:project, name: 'Component Repository 2', namespace: namespace) } + let_it_be(:project_1) { create(:project, name: 'Z', namespace: namespace) } + let_it_be(:project_2) { create(:project, name: 'A', namespace: namespace) } + let_it_be(:project_3) { create(:project, name: 'L', namespace: namespace) } let_it_be(:resource_1) { create(:catalog_resource, project: project_1) } let_it_be(:resource_2) { create(:catalog_resource, project: project_2) } + let_it_be(:resource_3) { create(:catalog_resource, project: project_3) } let_it_be(:user) { create(:user) } describe '#resolve' do - it 'returns all CI Catalog resources visible to the current user in the namespace' do - stub_licensed_features(ci_namespace_catalog: true) - namespace.add_owner(user) + context 'with an authorized user' do + before do + stub_licensed_features(ci_namespace_catalog: true) + namespace.add_owner(user) + end + + it 'returns all CI Catalog resources visible to the current user in the namespace' do + result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path }) + + expect(result.items.count).to be(3) + expect(result.items.pluck(:name)).to contain_exactly('Z', 'A', 'L') + end + + it 'returns all resources sorted by descending created date when given no sort param' do + result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path }) - result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path }) + expect(result.items.pluck(:name)).to eq(%w[L A Z]) + end + + it 'returns all CI Catalog resources sorted by descending name when there is a sort parameter' do + result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path, sort: + 'NAME_DESC' }) - expect(result.items.count).to be(2) - expect(result.items.pluck(:name)).to contain_exactly('Component Repository 1', 'Component Repository 2') + expect(result.items.pluck(:name)).to eq(%w[Z L A]) + end end context 'when the current user cannot read the namespace catalog' do diff --git a/ee/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb b/ee/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a4a726fe71ea621d2eb1f072aa03e93858a7fdb0 --- /dev/null +++ b/ee/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CiCatalogResourceSort'], feature_category: :pipeline_composition do + it { expect(described_class.graphql_name).to eq('CiCatalogResourceSort') } + + it 'exposes all the existing catalog resource sort orders' do + expect(described_class.values.keys).to include(*%w[NAME_ASC NAME_DESC]) + end +end diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb index 93d70a3f63e2eb11ba9ff9722dbca05b52f5a666..159b70d7f8ffd0f24ca78e167f6f77288baa9865 100644 --- a/spec/models/ci/catalog/listing_spec.rb +++ b/spec/models/ci/catalog/listing_spec.rb @@ -4,8 +4,8 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do let_it_be(:namespace) { create(:group) } - let_it_be(:project_1) { create(:project, namespace: namespace) } - let_it_be(:project_2) { create(:project, namespace: namespace) } + let_it_be(:project_1) { create(:project, namespace: namespace, name: 'X Project') } + let_it_be(:project_2) { create(:project, namespace: namespace, name: 'B Project') } let_it_be(:project_3) { create(:project) } let_it_be(:user) { create(:user) } @@ -34,11 +34,32 @@ end context 'when the namespace has catalog resources' do - let!(:resource) { create(:catalog_resource, project: project_1) } - let!(:other_namespace_resource) { create(:catalog_resource, project: project_3) } + let_it_be(:resource) { create(:catalog_resource, project: project_1) } + let_it_be(:resource_2) { create(:catalog_resource, project: project_2) } + let_it_be(:other_namespace_resource) { create(:catalog_resource, project: project_3) } it 'contains only catalog resources for projects in that namespace' do - is_expected.to contain_exactly(resource) + is_expected.to contain_exactly(resource, resource_2) + end + + context 'with a sort parameter' do + subject(:resources) { list.resources(sort: sort) } + + context 'when the sort is name ascending' do + let_it_be(:sort) { :name_asc } + + it 'contains catalog resources for projects sorted by name' do + is_expected.to eq([resource_2, resource]) + end + end + + context 'when the sort is name descending' do + let_it_be(:sort) { :name_desc } + + it 'contains catalog resources for projects sorted by name' do + is_expected.to eq([resource, resource_2]) + end + end end end end diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb index a239bbad85786e1b05f0cf6c80eadef131cd2c67..dfb7b311d96316120ec471899acbb0654b19eeaa 100644 --- a/spec/models/ci/catalog/resource_spec.rb +++ b/spec/models/ci/catalog/resource_spec.rb @@ -3,8 +3,12 @@ require 'spec_helper' RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, name: 'A') } + let_it_be(:project_2) { build(:project, name: 'Z') } + let_it_be(:project_3) { build(:project, name: 'L') } let_it_be(:resource) { create(:catalog_resource, project: project) } + let_it_be(:resource_2) { create(:catalog_resource, project: project_2) } + let_it_be(:resource_3) { create(:catalog_resource, project: project_3) } let_it_be(:releases) do [ @@ -28,6 +32,30 @@ end end + describe '.order_by_created_at_desc' do + it 'returns catalog resources sorted by descending created at' do + ordered_resources = described_class.order_by_created_at_desc + + expect(ordered_resources.to_a).to eq([resource_3, resource_2, resource]) + end + end + + describe '.order_by_name_desc' do + it 'returns catalog resources sorted by descending name' do + ordered_resources = described_class.order_by_name_desc + + expect(ordered_resources.pluck(:name)).to eq(%w[Z L A]) + end + end + + describe '.order_by_name_asc' do + it 'returns catalog resources sorted by ascending name' do + ordered_resources = described_class.order_by_name_asc + + expect(ordered_resources.pluck(:name)).to eq(%w[A L Z]) + end + end + describe '#versions' do it 'returns releases ordered by released date descending' do expect(resource.versions).to eq(releases.reverse) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e9bb01f4b230302f31acf5a392878b479f437cc8..5f24f609918f16cb86eb62490e07a2dfb661e7e9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2047,6 +2047,28 @@ def has_external_wiki end end + describe 'sorting by name' do + let_it_be(:project1) { create(:project, name: 'A') } + let_it_be(:project2) { create(:project, name: 'Z') } + let_it_be(:project3) { create(:project, name: 'L') } + + context 'when using .sort_by_name_desc' do + it 'reorders the projects by descending name order' do + projects = described_class.sorted_by_name_desc + + expect(projects.pluck(:name)).to eq(%w[Z L A]) + end + end + + context 'when using .sort_by_name_asc' do + it 'reorders the projects by ascending name order' do + projects = described_class.sorted_by_name_asc + + expect(projects.pluck(:name)).to eq(%w[A L Z]) + end + end + end + describe '.with_shared_runners_enabled' do subject { described_class.with_shared_runners_enabled }