Commit d2f70ab6 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'bw-graphql-board-type' into 'master'

GraphQL: Add Board type

See merge request gitlab-org/gitlab!22497
parents df90667f a9da2b10
......@@ -46,6 +46,12 @@ module Types
field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Find milestones',
resolver: Resolvers::MilestoneResolver
field :boards,
Types::BoardType.connection_type,
null: true,
description: 'Boards of the group',
resolver: Resolvers::BoardsResolver
end
end
......
......@@ -179,6 +179,12 @@ module Types
null: true,
description: 'Paginated collection of Sentry errors on the project',
resolver: Resolvers::ErrorTracking::SentryErrorCollectionResolver
field :boards,
Types::BoardType.connection_type,
null: true,
description: 'Boards of the project',
resolver: Resolvers::BoardsResolver
end
end
......
---
title: 'GraphQL: Add Board type'
merge_request: 22497
author: Alexander Koval
type: added
......@@ -159,6 +159,61 @@ enum BlobViewersType {
simple
}
"""
Represents a project or group board
"""
type Board {
"""
ID (global ID) of the board
"""
id: ID!
"""
Name of the board
"""
name: String
"""
Weight of the board
"""
weight: Int
}
"""
The connection type for Board.
"""
type BoardConnection {
"""
A list of edges.
"""
edges: [BoardEdge]
"""
A list of nodes.
"""
nodes: [Board]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type BoardEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Board
}
type Commit {
"""
Author of the commit
......@@ -2715,6 +2770,31 @@ type Group {
"""
avatarUrl: String
"""
Boards of the group
"""
boards(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): BoardConnection
"""
Description of the namespace
"""
......@@ -5174,6 +5254,31 @@ type Project {
"""
avatarUrl: String
"""
Boards of the project
"""
boards(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): BoardConnection
"""
Indicates if the project stores Docker container images in a container registry
"""
......
......@@ -368,6 +368,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "boards",
"description": "Boards of the project",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "BoardConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "containerRegistryEnabled",
"description": "Indicates if the project stores Docker container images in a container registry",
......@@ -3122,6 +3175,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "boards",
"description": "Boards of the group",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "BoardConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "Description of the namespace",
......@@ -4322,6 +4428,177 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BoardConnection",
"description": "The connection type for Board.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BoardEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BoardEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Board",
"description": "Represents a project or group board",
"fields": [
{
"name": "id",
"description": "ID (global ID) of the board",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the board",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "weight",
"description": "Weight of the board",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Epic",
......
......@@ -49,6 +49,16 @@ An emoji awarded by a user.
| `type` | EntryType! | Type of tree entry |
| `webUrl` | String | Web URL of the blob |
## Board
Represents a project or group board
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | ID (global ID) of the board |
| `name` | String | Name of the board |
| `weight` | Int | Weight of the board |
## Commit
| Name | Type | Description |
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['Board'] do
it 'includes the ee specific fields' do
expect(described_class).to have_graphql_field('weight')
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'get list of boards' do
include GraphqlHelpers
include_context 'group and project boards query context'
before do
stub_licensed_features(multiple_group_issue_boards: true)
end
describe 'for a group' do
let(:board_parent) { create(:group, :private) }
let(:boards_data) { graphql_data['group']['boards']['edges'] }
it_behaves_like 'group and project boards query'
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['Board'] do
it { expect(described_class.graphql_name).to eq('Board') }
it { expect(described_class).to require_graphql_authorizations(:read_board) }
it 'has specific fields' do
expected_fields = %w[id name]
is_expected.to include_graphql_fields(*expected_fields)
end
end
......@@ -16,9 +16,17 @@ describe GitlabSchema.types['Group'] do
web_url avatar_url share_with_group_lock project_creation_level
subgroup_creation_level require_two_factor_authentication
two_factor_grace_period auto_devops_enabled emails_disabled
mentions_disabled parent
mentions_disabled parent boards
]
is_expected.to include_graphql_fields(*expected_fields)
end
describe 'boards field' do
subject { described_class.fields['boards'] }
it 'returns boards' do
is_expected.to have_graphql_type(Types::BoardType.connection_type)
end
end
end
......@@ -24,6 +24,7 @@ describe GitlabSchema.types['Project'] do
namespace group statistics repository merge_requests merge_request issues
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
boards
]
is_expected.to include_graphql_fields(*expected_fields)
......@@ -77,4 +78,10 @@ describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::EnvironmentType.connection_type) }
it { is_expected.to have_graphql_resolver(Resolvers::EnvironmentsResolver) }
end
describe 'boards field' do
subject { described_class.fields['boards'] }
it { is_expected.to have_graphql_type(Types::BoardType.connection_type) }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'get list of boards' do
include GraphqlHelpers
include_context 'group and project boards query context'
describe 'for a project' do
let(:board_parent) { create(:project, :repository, :private) }
let(:boards_data) { graphql_data['project']['boards']['edges'] }
it_behaves_like 'group and project boards query'
end
describe 'for a group' do
let(:board_parent) { create(:group, :private) }
let(:boards_data) { graphql_data['group']['boards']['edges'] }
before do
allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false)
end
it_behaves_like 'group and project boards query'
end
end
# frozen_string_literal: true
RSpec.shared_context 'group and project boards query context' do
let_it_be(:user) { create :user }
let(:current_user) { user }
let(:params) { '' }
let(:board_parent_type) { board_parent.class.to_s.downcase }
let(:start_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['startCursor'] }
let(:end_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['endCursor'] }
def query(board_params = params)
graphql_query_for(
board_parent_type,
{ 'fullPath' => board_parent.full_path },
<<~BOARDS
boards(#{board_params}) {
pageInfo {
startCursor
endCursor
}
edges {
node {
#{all_graphql_fields_for('boards'.classify)}
}
}
}
BOARDS
)
end
def grab_names(data = boards_data)
data.map do |board|
board.dig('node', 'name')
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'group and project boards query' do
include GraphqlHelpers
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
context 'when the user does not have access to the board parent' do
it 'returns nil' do
create(:board, resource_parent: board_parent, name: 'A')
post_graphql(query)
expect(graphql_data[board_parent_type]).to be_nil
end
end
context 'when no permission to read board' do
it 'does not return any boards' do
board_parent.add_guest(current_user)
board = create(:board, resource_parent: board_parent, name: 'A')
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user, :read_board, board).and_return(false)
post_graphql(query, current_user: current_user)
expect(boards_data).to be_empty
end
end
context 'when user can read the board parent' do
before do
board_parent.add_reporter(current_user)
end
it 'does not create a default board' do
post_graphql(query, current_user: current_user)
expect(boards_data).to be_empty
end
describe 'sorting and pagination' do
context 'when using default sorting' do
let!(:board_B) { create(:board, resource_parent: board_parent, name: 'B') }
let!(:board_C) { create(:board, resource_parent: board_parent, name: 'C') }
let!(:board_a) { create(:board, resource_parent: board_parent, name: 'a') }
let!(:board_A) { create(:board, resource_parent: board_parent, name: 'A') }
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
context 'when ascending' do
let(:boards) { [board_a, board_A, board_B, board_C] }
let(:expected_boards) do
if board_parent.multiple_issue_boards_available?
boards
else
[boards.first]
end
end
it 'sorts boards' do
expect(grab_names).to eq expected_boards.map(&:name)
end
context 'when paginating' do
let(:params) { 'first: 2' }
it 'sorts boards' do
expect(grab_names).to eq expected_boards.first(2).map(&:name)
cursored_query = query("after: \"#{end_cursor}\"")
post_graphql(cursored_query, current_user: current_user)
response_data = JSON.parse(response.body)['data'][board_parent_type]['boards']['edges']
expect(grab_names(response_data)).to eq expected_boards.drop(2).first(2).map(&:name)
end
end
end
end
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