Commit db447fd8 authored by Michael Kozono's avatar Michael Kozono

Merge branch 'bw-board-query-by-id' into 'master'

GraphQL: Allow Group/Project Board to be queried by ID

See merge request gitlab-org/gitlab!24825
parents 88f21ac2 c7c88480
...@@ -4,7 +4,11 @@ module Resolvers ...@@ -4,7 +4,11 @@ module Resolvers
class BoardsResolver < BaseResolver class BoardsResolver < BaseResolver
type Types::BoardType, null: true type Types::BoardType, null: true
def resolve(**args) argument :id, GraphQL::ID_TYPE,
required: false,
description: 'Find a board by its ID'
def resolve(id: nil)
# The project or group could have been loaded in batch by `BatchLoader`. # The project or group could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project/group to query for boards, so # At this point we need the `id` of the project/group to query for boards, so
# make sure it's loaded and not `nil` before continuing. # make sure it's loaded and not `nil` before continuing.
...@@ -12,7 +16,17 @@ module Resolvers ...@@ -12,7 +16,17 @@ module Resolvers
return Board.none unless parent return Board.none unless parent
Boards::ListService.new(parent, context[:current_user]).execute(create_default_board: false) Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false)
rescue ActiveRecord::RecordNotFound
Board.none
end
private
def extract_board_id(gid)
return unless gid.present?
GitlabSchema.parse_gid(gid, expected_type: ::Board).model_id
end end
end end
end end
...@@ -52,6 +52,12 @@ module Types ...@@ -52,6 +52,12 @@ module Types
null: true, null: true,
description: 'Boards of the group', description: 'Boards of the group',
resolver: Resolvers::BoardsResolver resolver: Resolvers::BoardsResolver
field :board,
Types::BoardType,
null: true,
description: 'A single board of the group',
resolver: Resolvers::BoardsResolver.single
end end
end end
......
...@@ -185,6 +185,12 @@ module Types ...@@ -185,6 +185,12 @@ module Types
null: true, null: true,
description: 'Boards of the project', description: 'Boards of the project',
resolver: Resolvers::BoardsResolver resolver: Resolvers::BoardsResolver
field :board,
Types::BoardType,
null: true,
description: 'A single board of the project',
resolver: Resolvers::BoardsResolver.single
end end
end end
......
---
title: Allow group/project board to be queried by ID via GraphQL
merge_request: 24825
author:
type: added
...@@ -2770,6 +2770,16 @@ type Group { ...@@ -2770,6 +2770,16 @@ type Group {
""" """
avatarUrl: String avatarUrl: String
"""
A single board of the group
"""
board(
"""
Find a board by its ID
"""
id: ID
): Board
""" """
Boards of the group Boards of the group
""" """
...@@ -2789,6 +2799,11 @@ type Group { ...@@ -2789,6 +2799,11 @@ type Group {
""" """
first: Int first: Int
"""
Find a board by its ID
"""
id: ID
""" """
Returns the last _n_ elements from the list. Returns the last _n_ elements from the list.
""" """
...@@ -5254,6 +5269,16 @@ type Project { ...@@ -5254,6 +5269,16 @@ type Project {
""" """
avatarUrl: String avatarUrl: String
"""
A single board of the project
"""
board(
"""
Find a board by its ID
"""
id: ID
): Board
""" """
Boards of the project Boards of the project
""" """
...@@ -5273,6 +5298,11 @@ type Project { ...@@ -5273,6 +5298,11 @@ type Project {
""" """
first: Int first: Int
"""
Find a board by its ID
"""
id: ID
""" """
Returns the last _n_ elements from the list. Returns the last _n_ elements from the list.
""" """
......
...@@ -368,10 +368,43 @@ ...@@ -368,10 +368,43 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "board",
"description": "A single board of the project",
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "boards", "name": "boards",
"description": "Boards of the project", "description": "Boards of the project",
"args": [ "args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "after", "name": "after",
"description": "Returns the elements in the list that come after the specified cursor.", "description": "Returns the elements in the list that come after the specified cursor.",
...@@ -3175,10 +3208,43 @@ ...@@ -3175,10 +3208,43 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "board",
"description": "A single board of the group",
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "boards", "name": "boards",
"description": "Boards of the group", "description": "Boards of the group",
"args": [ "args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "after", "name": "after",
"description": "Returns the elements in the list that come after the specified cursor.", "description": "Returns the elements in the list that come after the specified cursor.",
......
...@@ -426,6 +426,7 @@ Autogenerated return type of EpicTreeReorder ...@@ -426,6 +426,7 @@ Autogenerated return type of EpicTreeReorder
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `autoDevopsEnabled` | Boolean | Indicates whether Auto DevOps is enabled for all projects within this group | | `autoDevopsEnabled` | Boolean | Indicates whether Auto DevOps is enabled for all projects within this group |
| `avatarUrl` | String | Avatar URL of the group | | `avatarUrl` | String | Avatar URL of the group |
| `board` | Board | A single board of the group |
| `description` | String | Description of the namespace | | `description` | String | Description of the namespace |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `emailsDisabled` | Boolean | Indicates if a group has email notifications disabled | | `emailsDisabled` | Boolean | Indicates if a group has email notifications disabled |
...@@ -801,6 +802,7 @@ Information about pagination in a connection. ...@@ -801,6 +802,7 @@ Information about pagination in a connection.
| `archived` | Boolean | Indicates the archived status of the project | | `archived` | Boolean | Indicates the archived status of the project |
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically | | `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically |
| `avatarUrl` | String | URL to avatar image file of the project | | `avatarUrl` | String | URL to avatar image file of the project |
| `board` | Board | A single board of the project |
| `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry | | `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry |
| `createdAt` | Time | Timestamp of the project creation | | `createdAt` | Time | Timestamp of the project creation |
| `description` | String | Short description of the project | | `description` | String | Short description of the project |
......
...@@ -13,7 +13,6 @@ describe 'get list of boards' do ...@@ -13,7 +13,6 @@ describe 'get list of boards' do
describe 'for a group' do describe 'for a group' do
let(:board_parent) { create(:group, :private) } let(:board_parent) { create(:group, :private) }
let(:boards_data) { graphql_data['group']['boards']['edges'] }
it_behaves_like 'group and project boards query' it_behaves_like 'group and project boards query'
end end
......
...@@ -45,6 +45,21 @@ describe Resolvers::BoardsResolver do ...@@ -45,6 +45,21 @@ describe Resolvers::BoardsResolver do
expect(resolve_boards).to eq [board1] expect(resolve_boards).to eq [board1]
end end
end end
context 'when querying for a single board' do
let(:board1) { create(:board, name: 'One', resource_parent: board_parent) }
it 'returns specified board' do
expect(resolve_boards(args: { id: global_id_of(board1) })).to eq [board1]
end
it 'returns nil if board not found' do
outside_parent = create(board_parent.class.underscore.to_sym)
outside_board = create(:board, name: 'outside board', resource_parent: outside_parent)
expect(resolve_boards(args: { id: global_id_of(outside_board) })).to eq Board.none
end
end
end end
describe '#resolve' do describe '#resolve' do
......
...@@ -9,14 +9,12 @@ describe 'get list of boards' do ...@@ -9,14 +9,12 @@ describe 'get list of boards' do
describe 'for a project' do describe 'for a project' do
let(:board_parent) { create(:project, :repository, :private) } let(:board_parent) { create(:project, :repository, :private) }
let(:boards_data) { graphql_data['project']['boards']['edges'] }
it_behaves_like 'group and project boards query' it_behaves_like 'group and project boards query'
end end
describe 'for a group' do describe 'for a group' do
let(:board_parent) { create(:group, :private) } let(:board_parent) { create(:group, :private) }
let(:boards_data) { graphql_data['group']['boards']['edges'] }
before do before do
allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false) allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false)
......
...@@ -5,6 +5,8 @@ RSpec.shared_context 'group and project boards query context' do ...@@ -5,6 +5,8 @@ RSpec.shared_context 'group and project boards query context' do
let(:current_user) { user } let(:current_user) { user }
let(:params) { '' } let(:params) { '' }
let(:board_parent_type) { board_parent.class.to_s.downcase } let(:board_parent_type) { board_parent.class.to_s.downcase }
let(:boards_data) { graphql_data[board_parent_type]['boards']['edges'] }
let(:board_data) { graphql_data[board_parent_type]['board'] }
let(:start_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['startCursor'] } let(:start_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['startCursor'] }
let(:end_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['endCursor'] } let(:end_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['endCursor'] }
...@@ -28,6 +30,18 @@ RSpec.shared_context 'group and project boards query context' do ...@@ -28,6 +30,18 @@ RSpec.shared_context 'group and project boards query context' do
) )
end end
def query_single_board(board_params = params)
graphql_query_for(
board_parent_type,
{ 'fullPath' => board_parent.full_path },
<<~BOARD
board(#{board_params}) {
#{all_graphql_fields_for('board'.classify)}
}
BOARD
)
end
def grab_names(data = boards_data) def grab_names(data = boards_data)
data.map do |board| data.map do |board|
board.dig('node', 'name') board.dig('node', 'name')
......
...@@ -89,4 +89,24 @@ RSpec.shared_examples 'group and project boards query' do ...@@ -89,4 +89,24 @@ RSpec.shared_examples 'group and project boards query' do
end end
end end
end end
context 'when querying for a single board' do
before do
board_parent.add_reporter(current_user)
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query_single_board, current_user: current_user)
end
end
it 'finds the correct board' do
board = create(:board, resource_parent: board_parent, name: 'A')
post_graphql(query_single_board("id: \"#{global_id_of(board)}\""), current_user: current_user)
expect(board_data['name']).to eq board.name
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