Commit c957a14e authored by Brett Walker's avatar Brett Walker

Create board list with milestone or assignee

using GraphQL
parent 7f5f7670
...@@ -8,7 +8,7 @@ module Mutations ...@@ -8,7 +8,7 @@ module Mutations
argument :board_id, ::Types::GlobalIDType[::Board], argument :board_id, ::Types::GlobalIDType[::Board],
required: true, required: true,
description: 'The Global ID of the issue board to mutate' description: 'Global ID of the issue board to mutate'
field :list, field :list,
Types::BoardListType, Types::BoardListType,
......
...@@ -12,7 +12,7 @@ module Mutations ...@@ -12,7 +12,7 @@ module Mutations
argument :label_id, ::Types::GlobalIDType[::Label], argument :label_id, ::Types::GlobalIDType[::Label],
required: false, required: false,
description: 'ID of an existing label' description: 'Global ID of an existing label'
def ready?(**args) def ready?(**args)
if args.slice(*mutually_exclusive_args).size != 1 if args.slice(*mutually_exclusive_args).size != 1
...@@ -39,6 +39,7 @@ module Mutations ...@@ -39,6 +39,7 @@ module Mutations
private private
# Overridden in EE
def authorize_list_type_resource!(board, params) def authorize_list_type_resource!(board, params)
return unless params[:label_id] return unless params[:label_id]
...@@ -57,13 +58,15 @@ module Mutations ...@@ -57,13 +58,15 @@ module Mutations
create_list_service.execute(board) create_list_service.execute(board)
end end
# Overridden in EE
def create_list_params(args) def create_list_params(args)
params = args.slice(*mutually_exclusive_args).with_indifferent_access params = args.slice(*mutually_exclusive_args).with_indifferent_access
params[:label_id] = GitlabSchema.parse_gid(params[:label_id]).model_id if params[:label_id] params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id
params params
end end
# Overridden in EE
def mutually_exclusive_args def mutually_exclusive_args
[:backlog, :label_id] [:backlog, :label_id]
end end
...@@ -71,3 +74,5 @@ module Mutations ...@@ -71,3 +74,5 @@ module Mutations
end end
end end
end end
Mutations::Boards::Lists::Create.prepend_if_ee('::EE::Mutations::Boards::Lists::Create')
...@@ -1309,13 +1309,18 @@ type BoardListConnection { ...@@ -1309,13 +1309,18 @@ type BoardListConnection {
Autogenerated input type of BoardListCreate Autogenerated input type of BoardListCreate
""" """
input BoardListCreateInput { input BoardListCreateInput {
"""
Global ID of an existing user
"""
assigneeId: UserID
""" """
Create the backlog list Create the backlog list
""" """
backlog: Boolean backlog: Boolean
""" """
The Global ID of the issue board to mutate Global ID of the issue board to mutate
""" """
boardId: BoardID! boardId: BoardID!
...@@ -1325,9 +1330,14 @@ input BoardListCreateInput { ...@@ -1325,9 +1330,14 @@ input BoardListCreateInput {
clientMutationId: String clientMutationId: String
""" """
ID of an existing label Global ID of an existing label
""" """
labelId: LabelID labelId: LabelID
"""
Global ID of an existing milestone
"""
milestoneId: MilestoneID
} }
""" """
...@@ -17115,6 +17125,11 @@ type UserEdge { ...@@ -17115,6 +17125,11 @@ type UserEdge {
node: User node: User
} }
"""
Identifier of User
"""
scalar UserID
type UserPermissions { type UserPermissions {
""" """
Indicates the user can perform `create_snippet` on this resource Indicates the user can perform `create_snippet` on this resource
......
...@@ -3505,7 +3505,7 @@ ...@@ -3505,7 +3505,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "boardId", "name": "boardId",
"description": "The Global ID of the issue board to mutate", "description": "Global ID of the issue board to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -3529,7 +3529,7 @@ ...@@ -3529,7 +3529,7 @@
}, },
{ {
"name": "labelId", "name": "labelId",
"description": "ID of an existing label", "description": "Global ID of an existing label",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "LabelID", "name": "LabelID",
...@@ -3537,6 +3537,26 @@ ...@@ -3537,6 +3537,26 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "milestoneId",
"description": "Global ID of an existing milestone",
"type": {
"kind": "SCALAR",
"name": "MilestoneID",
"ofType": null
},
"defaultValue": null
},
{
"name": "assigneeId",
"description": "Global ID of an existing user",
"type": {
"kind": "SCALAR",
"name": "UserID",
"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.",
...@@ -50327,6 +50347,16 @@ ...@@ -50327,6 +50347,16 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "SCALAR",
"name": "UserID",
"description": "Identifier of User",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "UserPermissions", "name": "UserPermissions",
# frozen_string_literal: true
module EE
module Mutations
module Boards
module Lists
module Create
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
argument :milestone_id, ::Types::GlobalIDType[::Milestone],
required: false,
description: 'Global ID of an existing milestone'
argument :assignee_id, ::Types::GlobalIDType[::User],
required: false,
description: 'Global ID of an existing user'
end
private
# rubocop: disable CodeReuse/ActiveRecord
override :authorize_list_type_resource!
def authorize_list_type_resource!(board, params)
super
if params[:milestone_id]
milestones = ::Boards::MilestonesFinder.new(board, current_user).execute
unless milestones.where(id: params[:milestone_id]).exists?
raise ::Gitlab::Graphql::Errors::ArgumentError, 'Milestone not found!'
end
end
if params[:assignee_id]
users = ::Boards::UsersFinder.new(board, current_user).execute
unless users.where(user_id: params[:assignee_id]).exists?
raise ::Gitlab::Graphql::Errors::ArgumentError, 'User not found!'
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
override :create_list_params
def create_list_params(args)
params = super
params[:milestone_id] &&= ::GitlabSchema.parse_gid(params[:milestone_id], expected_type: ::Milestone).model_id
params[:assignee_id] &&= ::GitlabSchema.parse_gid(params[:assignee_id], expected_type: ::User).model_id
params
end
override :mutually_exclusive_args
def mutually_exclusive_args
super + [:milestone_id, :assignee_id]
end
end
end
end
end
end
---
title: Allow milestone and assignee board lists to be created using GraphQL
merge_request: 40551
author:
type: changed
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Boards::Lists::Create do
include GraphqlHelpers
let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:board, group: group) }
let_it_be(:milestone) { create(:milestone, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:current_user) { user }
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
let(:list_create_params) { {} }
before_all do
group.add_reporter(user)
group.add_guest(guest)
end
before do
stub_licensed_features(board_assignee_lists: true, board_milestone_lists: true)
end
subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) }
describe '#ready?' do
it 'raises an error if required arguments are missing' do
expect { mutation.ready?({ board_id: 'some id' }) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError,
'one and only one of backlog or labelId or milestoneId or assigneeId is required')
end
it 'raises an error if too many required arguments are specified' do
expect { mutation.ready?({ board_id: 'some id', milestone_id: 'some milestone', assignee_id: 'some label' }) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError,
'one and only one of backlog or labelId or milestoneId or assigneeId is required')
end
end
describe '#resolve' do
context 'with proper permissions' do
describe 'milestone list' do
let(:list_create_params) { { milestone_id: milestone.to_global_id.to_s } }
context 'when feature unavailable' do
it 'returns an error' do
stub_licensed_features(board_milestone_lists: false)
expect(subject[:errors]).to include 'List type Milestone lists not available with your current license'
end
end
it 'creates a new issue board list for milestones' do
expect { subject }.to change { board.lists.count }.from(1).to(2)
new_list = subject[:list]
expect(new_list.title).to eq milestone.title
expect(new_list.milestone_id).to eq milestone.id
expect(new_list.position).to eq 0
end
context 'when milestone not found' do
let(:list_create_params) { { milestone_id: "gid://gitlab/Milestone/#{non_existing_record_id}" } }
it 'raises an error' do
expect { subject }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Milestone not found!')
end
end
end
describe 'assignee list' do
let(:list_create_params) { { assignee_id: guest.to_global_id.to_s } }
context 'when feature unavailable' do
it 'returns an error' do
stub_licensed_features(board_assignee_lists: false)
expect(subject[:errors]).to include 'List type Assignee lists not available with your current license'
end
end
it 'creates a new issue board list for assignees' do
expect { subject }.to change { board.lists.count }.from(1).to(2)
new_list = subject[:list]
expect(new_list.title).to eq "@#{guest.username}"
expect(new_list.user_id).to eq guest.id
expect(new_list.position).to eq 0
end
context 'when user not found' do
let(:list_create_params) { { assignee_id: "gid://gitlab/User/#{non_existing_record_id}" } }
it 'raises an error' do
expect { subject }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'User not found!')
end
end
end
end
context 'without proper permissions' do
let(:current_user) { guest }
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Create a milestone or assignee board list' do
include GraphqlHelpers
let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:board, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:milestone) { create(:milestone, group: group) }
let(:current_user) { user }
let(:mutation) { graphql_mutation(:board_list_create, input) }
let(:mutation_response) { graphql_mutation_response(:board_list_create) }
before do
stub_licensed_features(board_assignee_lists: true, board_milestone_lists: true)
end
context 'the user is not allowed to read board lists' do
let(:input) { { board_id: board.to_global_id.to_s, milestone_id: milestone.to_global_id.to_s } }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to admin board lists' do
before do
group.add_reporter(current_user)
group.add_guest(guest)
end
describe 'milestone list' do
let(:input) { { board_id: board.to_global_id.to_s, milestone_id: milestone.to_global_id.to_s } }
it 'creates the list' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['list'])
.to include('position' => 0, 'listType' => 'milestone',
'milestone' => include('title' => milestone.title))
end
end
describe 'assignee list' do
let!(:input) { { board_id: board.to_global_id.to_s, assignee_id: guest.to_global_id.to_s } }
it 'creates the list' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['list'])
.to include('position' => 0, 'listType' => 'assignee',
'assignee' => include('id' => guest.to_global_id.to_s))
end
end
end
end
...@@ -24,14 +24,12 @@ RSpec.describe Mutations::Boards::Lists::Create do ...@@ -24,14 +24,12 @@ RSpec.describe Mutations::Boards::Lists::Create do
describe '#ready?' do describe '#ready?' do
it 'raises an error if required arguments are missing' do it 'raises an error if required arguments are missing' do
expect { mutation.ready?({ board_id: 'some id' }) } expect { mutation.ready?({ board_id: 'some id' }) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
'one and only one of backlog or labelId is required')
end end
it 'raises an error if too many required arguments are specified' do it 'raises an error if too many required arguments are specified' do
expect { mutation.ready?({ board_id: 'some id', backlog: true, label_id: 'some label' }) } expect { mutation.ready?({ board_id: 'some id', backlog: true, label_id: 'some label' }) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
'one and only one of backlog or labelId is required')
end end
end end
...@@ -66,6 +64,15 @@ RSpec.describe Mutations::Boards::Lists::Create do ...@@ -66,6 +64,15 @@ RSpec.describe Mutations::Boards::Lists::Create do
expect(new_list.title).to eq dev_label.title expect(new_list.title).to eq dev_label.title
expect(new_list.position).to eq 0 expect(new_list.position).to eq 0
end end
context 'when label not found' do
let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } }
it 'raises an error' do
expect { subject }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Label not found!')
end
end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Create a label or backlog board list' do
include GraphqlHelpers
let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:board, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:dev_label) do
create(:group_label, title: 'Development', color: '#FFAABB', group: group)
end
let(:current_user) { user }
let(:mutation) { graphql_mutation(:board_list_create, input) }
let(:mutation_response) { graphql_mutation_response(:board_list_create) }
context 'the user is not allowed to read board lists' do
let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to admin board lists' do
before do
group.add_reporter(current_user)
end
describe 'backlog list' do
let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
it 'creates the list' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['list'])
.to include('position' => nil, 'listType' => 'backlog')
end
end
describe 'label list' do
let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } }
it 'creates the list' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['list'])
.to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development'))
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