Commit 4e919e4c authored by Jarka Košanová's avatar Jarka Košanová

Merge branch '300985-fetch-recent-issue-boards-using-graphql' into 'master'

Allow querying recent boards in a group or project

See merge request gitlab-org/gitlab!78076
parents d316eaa0 5b026dd5
...@@ -14,7 +14,7 @@ module MultipleBoardsActions ...@@ -14,7 +14,7 @@ module MultipleBoardsActions
end end
def recent def recent
recent_visits = ::Boards::VisitsFinder.new(parent, current_user).latest(4) recent_visits = ::Boards::VisitsFinder.new(parent, current_user).latest(Board::RECENT_BOARDS_SIZE)
recent_boards = recent_visits.map(&:board) recent_boards = recent_visits.map(&:board)
render json: serialize_as_json(recent_boards) render json: serialize_as_json(recent_boards)
......
# frozen_string_literal: true
module Resolvers
class RecentBoardsResolver < BaseResolver
type Types::BoardType, null: true
def resolve
parent = object.respond_to?(:sync) ? object.sync : object
return Board.none unless parent
recent_visits =
::Boards::VisitsFinder.new(parent, current_user).latest(Board::RECENT_BOARDS_SIZE)
recent_visits&.map(&:board) || []
end
end
end
...@@ -94,6 +94,12 @@ module Types ...@@ -94,6 +94,12 @@ module Types
max_page_size: 2000, max_page_size: 2000,
resolver: Resolvers::BoardsResolver resolver: Resolvers::BoardsResolver
field :recent_issue_boards,
Types::BoardType.connection_type,
null: true,
description: 'List of recently visited boards of the group. Maximum size is 4.',
resolver: Resolvers::RecentBoardsResolver
field :board, field :board,
Types::BoardType, Types::BoardType,
null: true, null: true,
......
...@@ -231,6 +231,12 @@ module Types ...@@ -231,6 +231,12 @@ module Types
max_page_size: 2000, max_page_size: 2000,
resolver: Resolvers::BoardsResolver resolver: Resolvers::BoardsResolver
field :recent_issue_boards,
Types::BoardType.connection_type,
null: true,
description: 'List of recently visited boards of the project. Maximum size is 4.',
resolver: Resolvers::RecentBoardsResolver
field :board, field :board,
Types::BoardType, Types::BoardType,
null: true, null: true,
......
# frozen_string_literal: true # frozen_string_literal: true
class Board < ApplicationRecord class Board < ApplicationRecord
RECENT_BOARDS_SIZE = 4
belongs_to :group belongs_to :group
belongs_to :project belongs_to :project
......
...@@ -10770,6 +10770,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -10770,6 +10770,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupparent"></a>`parent` | [`Group`](#group) | Parent group. | | <a id="groupparent"></a>`parent` | [`Group`](#group) | Parent group. |
| <a id="grouppath"></a>`path` | [`String!`](#string) | Path of the namespace. | | <a id="grouppath"></a>`path` | [`String!`](#string) | Path of the namespace. |
| <a id="groupprojectcreationlevel"></a>`projectCreationLevel` | [`String`](#string) | Permission level required to create projects in the group. | | <a id="groupprojectcreationlevel"></a>`projectCreationLevel` | [`String`](#string) | Permission level required to create projects in the group. |
| <a id="grouprecentissueboards"></a>`recentIssueBoards` | [`BoardConnection`](#boardconnection) | List of recently visited boards of the group. Maximum size is 4. (see [Connections](#connections)) |
| <a id="grouprepositorysizeexcessprojectcount"></a>`repositorySizeExcessProjectCount` | [`Int!`](#int) | Number of projects in the root namespace where the repository size exceeds the limit. | | <a id="grouprepositorysizeexcessprojectcount"></a>`repositorySizeExcessProjectCount` | [`Int!`](#int) | Number of projects in the root namespace where the repository size exceeds the limit. |
| <a id="grouprequestaccessenabled"></a>`requestAccessEnabled` | [`Boolean`](#boolean) | Indicates if users can request access to namespace. | | <a id="grouprequestaccessenabled"></a>`requestAccessEnabled` | [`Boolean`](#boolean) | Indicates if users can request access to namespace. |
| <a id="grouprequiretwofactorauthentication"></a>`requireTwoFactorAuthentication` | [`Boolean`](#boolean) | Indicates if all users in this group are required to set up two-factor authentication. | | <a id="grouprequiretwofactorauthentication"></a>`requireTwoFactorAuthentication` | [`Boolean`](#boolean) | Indicates if all users in this group are required to set up two-factor authentication. |
...@@ -13256,6 +13257,7 @@ Represents vulnerability finding of a security report on the pipeline. ...@@ -13256,6 +13257,7 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="projectprintingmergerequestlinkenabled"></a>`printingMergeRequestLinkEnabled` | [`Boolean`](#boolean) | Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line. | | <a id="projectprintingmergerequestlinkenabled"></a>`printingMergeRequestLinkEnabled` | [`Boolean`](#boolean) | Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line. |
| <a id="projectpublicjobs"></a>`publicJobs` | [`Boolean`](#boolean) | Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts. | | <a id="projectpublicjobs"></a>`publicJobs` | [`Boolean`](#boolean) | Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts. |
| <a id="projectpushrules"></a>`pushRules` | [`PushRules`](#pushrules) | Project's push rules settings. | | <a id="projectpushrules"></a>`pushRules` | [`PushRules`](#pushrules) | Project's push rules settings. |
| <a id="projectrecentissueboards"></a>`recentIssueBoards` | [`BoardConnection`](#boardconnection) | List of recently visited boards of the project. Maximum size is 4. (see [Connections](#connections)) |
| <a id="projectremovesourcebranchaftermerge"></a>`removeSourceBranchAfterMerge` | [`Boolean`](#boolean) | Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project. | | <a id="projectremovesourcebranchaftermerge"></a>`removeSourceBranchAfterMerge` | [`Boolean`](#boolean) | Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project. |
| <a id="projectrepository"></a>`repository` | [`Repository`](#repository) | Git repository of the project. | | <a id="projectrepository"></a>`repository` | [`Repository`](#repository) | Git repository of the project. |
| <a id="projectrepositorysizeexcess"></a>`repositorySizeExcess` | [`Float`](#float) | Size of repository that exceeds the limit in bytes. | | <a id="projectrepositorysizeexcess"></a>`repositorySizeExcess` | [`Float`](#float) | Size of repository that exceeds the limit in bytes. |
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::RecentBoardsResolver do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
shared_examples_for 'group and project recent boards resolver' do
let_it_be(:board1) { create(:board, name: 'One', resource_parent: board_parent) }
let_it_be(:board2) { create(:board, name: 'Two', resource_parent: board_parent) }
before do
[board1, board2].each { |board| visit_board(board, board_parent) }
end
it 'calls ::Boards::VisitsFinder' do
expect_any_instance_of(::Boards::VisitsFinder) do |finder|
expect(finder).to receive(:latest)
end
resolve_recent_boards
end
it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new { resolve_recent_boards }
board3 = create(:board, resource_parent: board_parent)
visit_board(board3, board_parent)
expect { resolve_recent_boards(args: {}) }.not_to exceed_query_limit(control)
end
it 'returns most recent visited boards' do
expect(resolve_recent_boards).to match_array [board2, board1]
end
it 'returns a set number of boards' do
stub_const('Board::RECENT_BOARDS_SIZE', 1)
expect(resolve_recent_boards).to match_array [board2]
end
end
describe '#resolve' do
context 'when there is no parent' do
let_it_be(:board_parent) { nil }
it 'returns none if parent is nil' do
expect(resolve_recent_boards).to eq(Board.none)
end
end
context 'when project boards' do
let_it_be(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
it_behaves_like 'group and project recent boards resolver'
end
context 'when group boards' do
let_it_be(:board_parent) { create(:group) }
it_behaves_like 'group and project recent boards resolver'
end
end
def resolve_recent_boards(args: {})
resolve(described_class, obj: board_parent, args: args, ctx: { current_user: user })
end
def visit_board(board, parent)
if parent.is_a?(Group)
create(:board_group_recent_visit, group: parent, board: board, user: user)
else
create(:board_project_recent_visit, project: parent, board: board, user: user)
end
end
end
...@@ -23,6 +23,7 @@ RSpec.describe GitlabSchema.types['Group'] do ...@@ -23,6 +23,7 @@ RSpec.describe GitlabSchema.types['Group'] do
dependency_proxy_blob_count dependency_proxy_total_size dependency_proxy_blob_count dependency_proxy_total_size
dependency_proxy_image_prefix dependency_proxy_image_ttl_policy dependency_proxy_image_prefix dependency_proxy_image_ttl_policy
shared_runners_setting timelogs organizations contacts work_item_types shared_runners_setting timelogs organizations contacts work_item_types
recent_issue_boards
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
...@@ -35,6 +35,7 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -35,6 +35,7 @@ RSpec.describe GitlabSchema.types['Project'] do
pipeline_analytics squash_read_only sast_ci_configuration pipeline_analytics squash_read_only sast_ci_configuration
cluster_agent cluster_agents agent_configurations cluster_agent cluster_agents agent_configurations
ci_template timelogs merge_commit_template squash_commit_template work_item_types ci_template timelogs merge_commit_template squash_commit_template work_item_types
recent_issue_boards
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
...@@ -16,6 +16,10 @@ RSpec.describe Board do ...@@ -16,6 +16,10 @@ RSpec.describe Board do
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
end end
describe 'constants' do
it { expect(described_class::RECENT_BOARDS_SIZE).to be_a(Integer) }
end
describe '#order_by_name_asc' do describe '#order_by_name_asc' do
let!(:board_B) { create(:board, project: project, name: 'B') } let!(:board_B) { create(:board, project: project, name: 'B') }
let!(:board_C) { create(:board, project: project, name: 'C') } let!(:board_C) { create(:board, project: project, name: 'C') }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting group recent issue boards' do
include GraphqlHelpers
it_behaves_like 'querying a GraphQL type recent boards' do
let_it_be(:user) { create(:user) }
let_it_be(:parent) { create(:group, :public) }
let_it_be(:board) { create(:board, resource_parent: parent, name: 'test group board') }
let(:board_type) { 'group' }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting project recent issue boards' do
include GraphqlHelpers
it_behaves_like 'querying a GraphQL type recent boards' do
let_it_be(:user) { create(:user) }
let_it_be(:parent) { create(:project, :public, namespace: user.namespace) }
let_it_be(:board) { create(:board, resource_parent: parent, name: 'test project board') }
let(:board_type) { 'project' }
end
end
# frozen_string_literal: true
RSpec.shared_examples 'querying a GraphQL type recent boards' do
describe 'Get list of recently visited boards' do
let(:boards_data) { graphql_data[board_type]['recentIssueBoards']['nodes'] }
context 'when the request is correct' do
before do
visit_board
parent.add_reporter(user)
post_graphql(query, current_user: user)
end
it_behaves_like 'a working graphql query'
it 'returns recent boards for user successfully' do
expect(response).to have_gitlab_http_status(:ok)
expect(graphql_errors).to be_nil
expect(boards_data.size).to eq(1)
expect(boards_data[0]['name']).to eq(board.name)
end
end
context 'when requests has errors' do
context 'when there are no recently visited boards' do
it 'returns empty result' do
post_graphql(query, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(graphql_errors).to be_nil
expect(boards_data).to be_empty
end
end
end
end
def query(query_params: {}, full_path: parent.full_path)
board_nodes = <<~NODE
nodes {
name
}
NODE
graphql_query_for(
board_type.to_sym,
{ full_path: full_path },
query_graphql_field(:recent_issue_boards, query_params, board_nodes)
)
end
def visit_board
if board_type == 'group'
create(:board_group_recent_visit, group: parent, board: board, user: user)
else
create(:board_project_recent_visit, project: parent, board: board, user: user)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment