Commit 02b38723 authored by Alexandru Croitor's avatar Alexandru Croitor

Update and expose board labels in graphql

Allow for boards to be updated with labels as well as expose board
labels in graphql.
parent 09adf1cd
...@@ -1072,6 +1072,31 @@ type Board { ...@@ -1072,6 +1072,31 @@ type Board {
""" """
id: ID! id: ID!
"""
Labels of the board
"""
labels(
"""
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
): LabelConnection
""" """
Lists of the board Lists of the board
""" """
...@@ -18763,6 +18788,16 @@ input UpdateBoardInput { ...@@ -18763,6 +18788,16 @@ input UpdateBoardInput {
""" """
id: ID! id: ID!
"""
The IDs of labels to be added to the board.
"""
labelIds: [LabelID!]
"""
Labels of the issue
"""
labels: [String!]
""" """
The id of milestone to be assigned to the board. The id of milestone to be assigned to the board.
""" """
......
...@@ -2840,6 +2840,59 @@ ...@@ -2840,6 +2840,59 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "labels",
"description": "Labels of the board",
"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": "LabelConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "lists", "name": "lists",
"description": "Lists of the board", "description": "Lists of the board",
...@@ -54769,6 +54822,42 @@ ...@@ -54769,6 +54822,42 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "labels",
"description": "Labels of the issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "labelIds",
"description": "The IDs of labels to be added to the board.",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "LabelID",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "clientMutationId", "name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.", "description": "A unique identifier for the client performing the mutation.",
...@@ -9,8 +9,10 @@ module EE ...@@ -9,8 +9,10 @@ module EE
field :assignee, type: ::Types::UserType, null: true, field :assignee, type: ::Types::UserType, null: true,
description: 'The board assignee.' description: 'The board assignee.'
field :milestone, type: ::Types::MilestoneType, null: true, field :epics, ::Types::Boards::BoardEpicType.connection_type, null: true,
description: 'The board milestone.' description: 'Epics associated with board issues.',
resolver: ::Resolvers::BoardGroupings::EpicsResolver,
complexity: 5
field :hide_backlog_list, type: GraphQL::BOOLEAN_TYPE, null: true, field :hide_backlog_list, type: GraphQL::BOOLEAN_TYPE, null: true,
description: 'Whether or not backlog list is hidden.' description: 'Whether or not backlog list is hidden.'
...@@ -18,13 +20,14 @@ module EE ...@@ -18,13 +20,14 @@ module EE
field :hide_closed_list, type: GraphQL::BOOLEAN_TYPE, null: true, field :hide_closed_list, type: GraphQL::BOOLEAN_TYPE, null: true,
description: 'Whether or not closed list is hidden.' description: 'Whether or not closed list is hidden.'
field :labels, ::Types::LabelType.connection_type, null: true,
description: 'Labels of the board'
field :milestone, type: ::Types::MilestoneType, null: true,
description: 'The board milestone.'
field :weight, type: GraphQL::INT_TYPE, null: true, field :weight, type: GraphQL::INT_TYPE, null: true,
description: 'Weight of the board.' description: 'Weight of the board.'
field :epics, ::Types::Boards::BoardEpicType.connection_type, null: true,
description: 'Epics associated with board issues.',
resolver: ::Resolvers::BoardGroupings::EpicsResolver,
complexity: 5
end end
end end
end end
......
...@@ -41,6 +41,14 @@ module Mutations ...@@ -41,6 +41,14 @@ module Mutations
required: false, required: false,
description: 'The weight value to be assigned to the board.' description: 'The weight value to be assigned to the board.'
argument :labels, [GraphQL::STRING_TYPE],
required: false,
description: copy_field_description(Types::IssueType, :labels)
argument :label_ids, [::Types::GlobalIDType[::Label]],
required: false,
description: 'The IDs of labels to be added to the board.'
field :board, field :board,
Types::BoardType, Types::BoardType,
null: true, null: true,
...@@ -61,6 +69,15 @@ module Mutations ...@@ -61,6 +69,15 @@ module Mutations
} }
end end
def ready?(**args)
if args.slice(*mutually_exclusive_args).size > 1
arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
end
super
end
private private
def find_object(id:) def find_object(id:)
...@@ -76,8 +93,16 @@ module Mutations ...@@ -76,8 +93,16 @@ module Mutations
args[:milestone_id] = GitlabSchema.parse_gid(args[:milestone_id], expected_type: ::Milestone).model_id args[:milestone_id] = GitlabSchema.parse_gid(args[:milestone_id], expected_type: ::Milestone).model_id
end end
args[:label_ids] &&= args[:label_ids].map do |label_id|
::GitlabSchema.parse_gid(label_id, expected_type: ::Label).model_id
end
args args
end end
def mutually_exclusive_args
[:labels, :label_ids]
end
end end
end end
end end
---
title: Update and expose board labels trough GraphQL API
merge_request: 44204
author:
type: added
...@@ -4,6 +4,8 @@ require 'spec_helper' ...@@ -4,6 +4,8 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Board'] do RSpec.describe GitlabSchema.types['Board'] do
it 'includes the ee specific fields' do it 'includes the ee specific fields' do
expect(described_class).to have_graphql_fields(:weight, :epics).at_least expect(described_class).to have_graphql_fields(
:assignee, :epics, :hide_backlog_list, :hide_closed_list, :labels, :milestone, :weight
).at_least
end end
end end
...@@ -7,26 +7,31 @@ RSpec.describe Mutations::Boards::Update do ...@@ -7,26 +7,31 @@ RSpec.describe Mutations::Boards::Update do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:board) { create(:board, project: project) } let_it_be(:board) { create(:board, project: project) }
let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
let(:new_labels) { %w(new_label1 new_label2) }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let(:mutated_board) { subject[:board] } let(:mutated_board) { subject[:board] }
specify { expect(described_class).to require_graphql_authorizations(:admin_board) } let(:mutation_params) do
{
id: board.to_global_id,
name: 'Test board 1',
hide_backlog_list: true,
hide_closed_list: true,
weight: 3,
assignee_id: user.to_global_id,
milestone_id: milestone.to_global_id,
label_ids: [label1.to_global_id, label2.to_global_id]
}
end
describe '#resolve' do subject { mutation.resolve(mutation_params) }
let(:mutation_params) do
{
id: board.to_global_id,
name: 'Test board 1',
hide_backlog_list: true,
hide_closed_list: true,
weight: 3,
assignee_id: user.to_global_id,
milestone_id: milestone.to_global_id
}
end
subject { mutation.resolve(mutation_params) } specify { expect(described_class).to require_graphql_authorizations(:admin_board) }
describe '#resolve' do
context 'when the user cannot admin the board' do context 'when the user cannot admin the board' do
it 'raises an error' do it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
...@@ -45,13 +50,40 @@ RSpec.describe Mutations::Boards::Update do ...@@ -45,13 +50,40 @@ RSpec.describe Mutations::Boards::Update do
hide_closed_list: true, hide_closed_list: true,
weight: 3, weight: 3,
assignee: user, assignee: user,
milestone: milestone milestone: milestone,
labels: [label1, label2]
} }
subject subject
expect(board.reload).to have_attributes(expected_attributes) expect(board.reload).to have_attributes(expected_attributes)
end end
context 'when passing labels param' do
before do
mutation_params.delete(:label_ids)
mutation_params.merge!(labels: new_labels)
end
it 'updates board with correct labels' do
subject
expect(board.reload.labels.pluck(:title)).to eq(new_labels)
end
end
end
end
describe '#ready' do
context 'when passing both labels & label_ids param' do
before do
mutation_params.merge!(labels: new_labels)
end
it 'raises exception when mutually exclusive params are given' do
expect { mutation.ready?(mutation_params) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
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