Commit bca895fc authored by Natalia Tepluhina's avatar Natalia Tepluhina Committed by Heinrich Lee Yu

Add ability to scope board to iteration on creation

Unified some of the common arguments for board create and update
both on FOSS and EE. On FOSS we only have board `name`, hideBacklogList
and hideClosedList params available, other scope options are EE only
parent a741fa4a
# frozen_string_literal: true
module Mutations
module Boards
module CommonMutationArguments
extend ActiveSupport::Concern
included do
argument :name,
GraphQL::STRING_TYPE,
required: false,
description: 'The board name.'
argument :hide_backlog_list,
GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::BoardType, :hide_backlog_list)
argument :hide_closed_list,
GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::BoardType, :hide_closed_list)
end
end
end
end
...@@ -7,36 +7,18 @@ module Mutations ...@@ -7,36 +7,18 @@ module Mutations
graphql_name 'CreateBoard' graphql_name 'CreateBoard'
include Mutations::Boards::CommonMutationArguments
field :board, field :board,
Types::BoardType, Types::BoardType,
null: true, null: true,
description: 'The board after mutation.' description: 'The board after mutation.'
argument :name,
GraphQL::STRING_TYPE,
required: false,
description: 'The board name.'
argument :assignee_id,
GraphQL::STRING_TYPE,
required: false,
description: 'The ID of the user to be assigned to the board.'
argument :milestone_id,
Types::GlobalIDType[Milestone],
required: false,
description: 'The ID of the milestone to be assigned to the board.'
argument :weight,
GraphQL::BOOLEAN_TYPE,
required: false,
description: 'The weight of the board.'
argument :label_ids,
[Types::GlobalIDType[Label]],
required: false,
description: 'The IDs of labels to be added to the board.'
authorize :admin_board authorize :admin_board
def resolve(args) def resolve(args)
board_parent = authorized_resource_parent_find!(args) board_parent = authorized_resource_parent_find!(args)
response = ::Boards::CreateService.new(board_parent, current_user, args).execute response = ::Boards::CreateService.new(board_parent, current_user, args).execute
{ {
...@@ -47,3 +29,5 @@ module Mutations ...@@ -47,3 +29,5 @@ module Mutations
end end
end end
end end
Mutations::Boards::Create.prepend_if_ee('::EE::Mutations::Boards::Create')
# frozen_string_literal: true
module Mutations
module Boards
class Update < ::Mutations::BaseMutation
graphql_name 'UpdateBoard'
include Mutations::Boards::CommonMutationArguments
argument :id,
::Types::GlobalIDType[::Board],
required: true,
description: 'The board global ID.'
field :board,
Types::BoardType,
null: true,
description: 'The board after mutation.'
authorize :admin_board
def resolve(id:, **args)
board = authorized_find!(id: id)
::Boards::UpdateService.new(board.resource_parent, current_user, args).execute(board)
{
board: board,
errors: errors_on_object(board)
}
end
def find_object(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Board].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
end
end
Mutations::Boards::Update.prepend_if_ee('::EE::Mutations::Boards::Update')
...@@ -12,6 +12,12 @@ module Types ...@@ -12,6 +12,12 @@ module Types
field :name, type: GraphQL::STRING_TYPE, null: true, field :name, type: GraphQL::STRING_TYPE, null: true,
description: 'Name of the board' description: 'Name of the board'
field :hide_backlog_list, type: GraphQL::BOOLEAN_TYPE, null: true,
description: 'Whether or not backlog list is hidden'
field :hide_closed_list, type: GraphQL::BOOLEAN_TYPE, null: true,
description: 'Whether or not closed list is hidden'
field :lists, field :lists,
Types::BoardListType.connection_type, Types::BoardListType.connection_type,
null: true, null: true,
......
---
title: Allow updating `hideBacklogList` and `hideClosedList` board attributes
merge_request: 49947
author:
type: changed
...@@ -3975,11 +3975,6 @@ type CreateAnnotationPayload { ...@@ -3975,11 +3975,6 @@ type CreateAnnotationPayload {
Autogenerated input type of CreateBoard Autogenerated input type of CreateBoard
""" """
input CreateBoardInput { input CreateBoardInput {
"""
The ID of the user to be assigned to the board.
"""
assigneeId: String
""" """
A unique identifier for the client performing the mutation. A unique identifier for the client performing the mutation.
""" """
...@@ -3991,14 +3986,14 @@ input CreateBoardInput { ...@@ -3991,14 +3986,14 @@ input CreateBoardInput {
groupPath: ID groupPath: ID
""" """
The IDs of labels to be added to the board. Whether or not backlog list is hidden
""" """
labelIds: [LabelID!] hideBacklogList: Boolean
""" """
The ID of the milestone to be assigned to the board. Whether or not closed list is hidden
""" """
milestoneId: MilestoneID hideClosedList: Boolean
""" """
The board name. The board name.
...@@ -4009,11 +4004,6 @@ input CreateBoardInput { ...@@ -4009,11 +4004,6 @@ input CreateBoardInput {
The project full path the resource is associated with The project full path the resource is associated with
""" """
projectPath: ID projectPath: ID
"""
The weight of the board.
"""
weight: Boolean
} }
""" """
...@@ -23399,11 +23389,6 @@ type UpdateBoardEpicUserPreferencesPayload { ...@@ -23399,11 +23389,6 @@ type UpdateBoardEpicUserPreferencesPayload {
Autogenerated input type of UpdateBoard Autogenerated input type of UpdateBoard
""" """
input UpdateBoardInput { input UpdateBoardInput {
"""
The ID of user to be assigned to the board
"""
assigneeId: UserID
""" """
A unique identifier for the client performing the mutation. A unique identifier for the client performing the mutation.
""" """
...@@ -23420,39 +23405,14 @@ input UpdateBoardInput { ...@@ -23420,39 +23405,14 @@ input UpdateBoardInput {
hideClosedList: Boolean hideClosedList: Boolean
""" """
The board global ID The board global ID.
""" """
id: BoardID! id: BoardID!
""" """
The ID of iteration to be assigned to the board. The board name.
"""
iterationId: IterationID
"""
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
"""
milestoneId: MilestoneID
"""
Name of the board
""" """
name: String name: String
"""
The weight value to be assigned to the board
"""
weight: Int
} }
""" """
...@@ -23505,7 +23465,7 @@ Autogenerated return type of UpdateBoard ...@@ -23505,7 +23465,7 @@ Autogenerated return type of UpdateBoard
""" """
type UpdateBoardPayload { type UpdateBoardPayload {
""" """
The board after mutation The board after mutation.
""" """
board: Board board: Board
......
...@@ -10945,28 +10945,8 @@ ...@@ -10945,28 +10945,8 @@
"defaultValue": null "defaultValue": null
}, },
{ {
"name": "assigneeId", "name": "hideBacklogList",
"description": "The ID of the user to be assigned to the board.", "description": "Whether or not backlog list is hidden",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "milestoneId",
"description": "The ID of the milestone to be assigned to the board.",
"type": {
"kind": "SCALAR",
"name": "MilestoneID",
"ofType": null
},
"defaultValue": null
},
{
"name": "weight",
"description": "The weight of the board.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "Boolean", "name": "Boolean",
...@@ -10975,20 +10955,12 @@ ...@@ -10975,20 +10955,12 @@
"defaultValue": null "defaultValue": null
}, },
{ {
"name": "labelIds", "name": "hideClosedList",
"description": "The IDs of labels to be added to the board.", "description": "Whether or not closed list is hidden",
"type": { "type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "LabelID", "name": "Boolean",
"ofType": null "ofType": null
}
}
}, },
"defaultValue": null "defaultValue": null
}, },
...@@ -68541,23 +68513,9 @@ ...@@ -68541,23 +68513,9 @@
"description": "Autogenerated input type of UpdateBoard", "description": "Autogenerated input type of UpdateBoard",
"fields": null, "fields": null,
"inputFields": [ "inputFields": [
{
"name": "id",
"description": "The board global ID",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "BoardID",
"ofType": null
}
},
"defaultValue": null
},
{ {
"name": "name", "name": "name",
"description": "Name of the board", "description": "The board name.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -68586,78 +68544,16 @@ ...@@ -68586,78 +68544,16 @@
"defaultValue": null "defaultValue": null
}, },
{ {
"name": "assigneeId", "name": "id",
"description": "The ID of user to be assigned to the board", "description": "The board global ID.",
"type": {
"kind": "SCALAR",
"name": "UserID",
"ofType": null
},
"defaultValue": null
},
{
"name": "milestoneId",
"description": "The ID of milestone to be assigned to the board",
"type": {
"kind": "SCALAR",
"name": "MilestoneID",
"ofType": null
},
"defaultValue": null
},
{
"name": "iterationId",
"description": "The ID of iteration to be assigned to the board.",
"type": {
"kind": "SCALAR",
"name": "IterationID",
"ofType": null
},
"defaultValue": null
},
{
"name": "weight",
"description": "The weight value to be assigned to the board",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": 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": { "type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "LabelID", "name": "BoardID",
"ofType": null "ofType": null
} }
}
}, },
"defaultValue": null "defaultValue": null
}, },
...@@ -68805,7 +68701,7 @@ ...@@ -68805,7 +68701,7 @@
"fields": [ "fields": [
{ {
"name": "board", "name": "board",
"description": "The board after mutation", "description": "The board after mutation.",
"args": [ "args": [
], ],
...@@ -3584,7 +3584,7 @@ Autogenerated return type of UpdateBoard. ...@@ -3584,7 +3584,7 @@ Autogenerated return type of UpdateBoard.
| Field | Type | Description | | Field | Type | Description |
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `board` | Board | The board after mutation | | `board` | Board | The board after mutation. |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
......
# frozen_string_literal: true
module EE
module Mutations
module Boards
module Create
include Mutations::Boards::ScopedBoardMutation
end
end
end
end
# frozen_string_literal: true
module EE
module Mutations
module Boards
module ScopedBoardArguments
extend ActiveSupport::Concern
included do
argument :assignee_id,
::Types::GlobalIDType[::User],
required: false,
loads: ::Types::UserType,
description: 'The ID of user to be assigned to the board.'
# Cannot pre-load ::Types::MilestoneType because we are also assigning values like:
# ::Timebox::None(0), ::Timebox::Upcoming(-2) or ::Timebox::Started(-3), that cannot be resolved to a DB record.
argument :milestone_id,
::Types::GlobalIDType[::Milestone],
required: false,
description: 'The ID of milestone to be assigned to the board.'
# Cannot pre-load ::Types::IterationType because we are also assigning values like:
# ::Iteration::Predefined::None(0) or ::Iteration::Predefined::Current(-4), that cannot be resolved to a DB record.
argument :iteration_id,
::Types::GlobalIDType[::Iteration],
required: false,
description: 'The ID of iteration to be assigned to the board.'
argument :weight,
GraphQL::INT_TYPE,
required: false,
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.'
end
end
end
end
end
# frozen_string_literal: true
module EE
module Mutations
module Boards
module ScopedBoardMutation
extend ActiveSupport::Concern
prepended do
include ScopedBoardArguments
end
def resolve(**args)
parsed_params = parse_arguments(args)
super(parsed_params)
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
def parse_arguments(args = {})
if args[:assignee_id]
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
args[:assignee_id] = ::Types::GlobalIDType[::User].coerce_isolated_input(args[:assignee_id])
args[:assignee_id] = args[:assignee_id].model_id
end
if args[:milestone_id]
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
args[:milestone_id] = ::Types::GlobalIDType[::Milestone].coerce_isolated_input(args[:milestone_id])
args[:milestone_id] = args[:milestone_id].model_id
end
args[:label_ids] &&= args[:label_ids].map do |label_id|
::GitlabSchema.parse_gid(label_id, expected_type: ::Label).model_id
end
# we need this because we also pass `gid://gitlab/Iteration/-4` or `gid://gitlab/Iteration/-4`
# as `iteration_id` when we scope board to `Iteration::Predefined::Current` or `Iteration::Predefined::None`
args[:iteration_id] = args[:iteration_id].model_id if args[:iteration_id]
args
end
def mutually_exclusive_args
[:labels, :label_ids]
end
end
end
end
end
# frozen_string_literal: true
module EE
module Mutations
module Boards
module Update
include Mutations::Boards::ScopedBoardMutation
end
end
end
end
...@@ -14,12 +14,6 @@ module EE ...@@ -14,12 +14,6 @@ module EE
resolver: ::Resolvers::BoardGroupings::EpicsResolver, resolver: ::Resolvers::BoardGroupings::EpicsResolver,
complexity: 5 complexity: 5
field :hide_backlog_list, type: GraphQL::BOOLEAN_TYPE, null: true,
description: 'Whether or not backlog list is hidden'
field :hide_closed_list, type: GraphQL::BOOLEAN_TYPE, null: true,
description: 'Whether or not closed list is hidden'
field :labels, ::Types::LabelType.connection_type, null: true, field :labels, ::Types::LabelType.connection_type, null: true,
description: 'Labels of the board' description: 'Labels of the board'
......
# frozen_string_literal: true
module Mutations
module Boards
class Update < ::Mutations::BaseMutation
graphql_name 'UpdateBoard'
argument :id,
::Types::GlobalIDType[::Board],
required: true,
description: 'The board global ID'
argument :name,
GraphQL::STRING_TYPE,
required: false,
description: copy_field_description(Types::BoardType, :name)
argument :hide_backlog_list,
GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::BoardType, :hide_backlog_list)
argument :hide_closed_list,
GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::BoardType, :hide_closed_list)
argument :assignee_id,
::Types::GlobalIDType[::User],
required: false,
loads: ::Types::UserType,
description: 'The ID of user to be assigned to the board'
# Cannot pre-load ::Types::MilestoneType because we are also assigning values like:
# ::Timebox::None(0), ::Timebox::Upcoming(-2) or ::Timebox::Started(-3), that cannot be resolved to a DB record.
argument :milestone_id,
::Types::GlobalIDType[::Milestone],
required: false,
description: 'The ID of milestone to be assigned to the board'
# Cannot pre-load ::Types::IterationType because we are also assigning values like:
# ::Iteration::Predefined::None(0) or ::Iteration::Predefined::Current(-4), that cannot be resolved to a DB record.
argument :iteration_id,
::Types::GlobalIDType[::Iteration],
required: false,
description: 'The ID of iteration to be assigned to the board.'
argument :weight,
GraphQL::INT_TYPE,
required: false,
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,
Types::BoardType,
null: true,
description: "The board after mutation"
authorize :admin_board
def resolve(id:, assignee: nil, **args)
board = authorized_find!(id: id)
parsed_params = parse_arguments(args)
::Boards::UpdateService.new(board.resource_parent, current_user, parsed_params).execute(board)
{
board: board,
errors: errors_on_object(board)
}
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
def find_object(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Board].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def parse_arguments(args = {})
if args[:assignee_id]
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
args[:assignee_id] = ::Types::GlobalIDType[::User].coerce_isolated_input(args[:assignee_id])
args[:assignee_id] = args[:assignee_id].model_id
end
if args[:milestone_id]
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
args[:milestone_id] = ::Types::GlobalIDType[::Milestone].coerce_isolated_input(args[:milestone_id])
args[:milestone_id] = args[:milestone_id].model_id
end
args[:label_ids] &&= args[:label_ids].map do |label_id|
::GitlabSchema.parse_gid(label_id, expected_type: ::Label).model_id
end
# we need this because we also pass `gid://gitlab/Iteration/-4` or `gid://gitlab/Iteration/-4`
# as `iteration_id` when we scope board to `Iteration::Predefined::Current` or `Iteration::Predefined::None`
args[:iteration_id] = args[:iteration_id].model_id if args[:iteration_id]
args
end
def mutually_exclusive_args
[:labels, :label_ids]
end
end
end
end
---
title: Add `hideBacklogList` and `hideClosedList` and `iteration_id` to `createBoard` mutation input
merge_request: 49947
author:
type: changed
...@@ -40,6 +40,30 @@ RSpec.shared_examples 'boards create mutation' do ...@@ -40,6 +40,30 @@ RSpec.shared_examples 'boards create mutation' do
end end
end end
context 'when hide_backlog_list parameter is true' do
before do
params[:hide_backlog_list] = true
end
it 'returns the board with correct hide_backlog_list field' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['board']['hideBacklogList']).to eq(true)
end
end
context 'when hide_closed_list parameter is true' do
before do
params[:hide_closed_list] = true
end
it 'returns the board with correct hide_closed_list field' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['board']['hideClosedList']).to eq(true)
end
end
context 'when the Boards::CreateService returns an error response' do context 'when the Boards::CreateService returns an error response' do
before do before do
allow_next_instance_of(Boards::CreateService) do |service| allow_next_instance_of(Boards::CreateService) do |service|
......
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