Commit 546ee070 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Add creation of iteration lists

This adds creation of iteration lists to the internal JSON endpoint, the
REST API, and GraphQL
parent 56d728a6
...@@ -2114,6 +2114,11 @@ input BoardListCreateInput { ...@@ -2114,6 +2114,11 @@ input BoardListCreateInput {
""" """
clientMutationId: String clientMutationId: String
"""
Global ID of an existing iteration
"""
iterationId: IterationID
""" """
Global ID of an existing label Global ID of an existing label
""" """
......
...@@ -5596,6 +5596,16 @@ ...@@ -5596,6 +5596,16 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "iterationId",
"description": "Global ID of an existing iteration",
"type": {
"kind": "SCALAR",
"name": "IterationID",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "assigneeId", "name": "assigneeId",
"description": "Global ID of an existing user", "description": "Global ID of an existing user",
...@@ -13,7 +13,7 @@ module EE ...@@ -13,7 +13,7 @@ module EE
override :list_creation_attrs override :list_creation_attrs
def list_creation_attrs def list_creation_attrs
additional_attrs = %i[assignee_id milestone_id] additional_attrs = %i[assignee_id milestone_id iteration_id]
additional_attrs += EE_MAX_LIMITS_PARAMS if wip_limits_available? additional_attrs += EE_MAX_LIMITS_PARAMS if wip_limits_available?
super + additional_attrs super + additional_attrs
...@@ -28,7 +28,7 @@ module EE ...@@ -28,7 +28,7 @@ module EE
override :serialization_attrs override :serialization_attrs
def serialization_attrs def serialization_attrs
super.merge(user: true, milestone: true).tap do |attrs| super.merge(user: true, milestone: true, iteration: true).tap do |attrs|
attrs[:only] += EE_MAX_LIMITS_PARAMS if wip_limits_available? attrs[:only] += EE_MAX_LIMITS_PARAMS if wip_limits_available?
end end
end end
......
...@@ -116,6 +116,8 @@ class IterationsFinder ...@@ -116,6 +116,8 @@ class IterationsFinder
end end
def by_state(items) def by_state(items)
return items unless params[:state].present?
Iteration.filter_by_state(items, params[:state]) Iteration.filter_by_state(items, params[:state])
end end
......
...@@ -12,6 +12,9 @@ module EE ...@@ -12,6 +12,9 @@ module EE
argument :milestone_id, ::Types::GlobalIDType[::Milestone], argument :milestone_id, ::Types::GlobalIDType[::Milestone],
required: false, required: false,
description: 'Global ID of an existing milestone' description: 'Global ID of an existing milestone'
argument :iteration_id, ::Types::GlobalIDType[::Iteration],
required: false,
description: 'Global ID of an existing iteration'
argument :assignee_id, ::Types::GlobalIDType[::User], argument :assignee_id, ::Types::GlobalIDType[::User],
required: false, required: false,
description: 'Global ID of an existing user' description: 'Global ID of an existing user'
...@@ -24,6 +27,7 @@ module EE ...@@ -24,6 +27,7 @@ module EE
params = super params = super
params[:milestone_id] &&= ::GitlabSchema.parse_gid(params[:milestone_id], expected_type: ::Milestone).model_id params[:milestone_id] &&= ::GitlabSchema.parse_gid(params[:milestone_id], expected_type: ::Milestone).model_id
params[:iteration_id] &&= ::GitlabSchema.parse_gid(params[:iteration_id], expected_type: ::Iteration).model_id
params[:assignee_id] &&= ::GitlabSchema.parse_gid(params[:assignee_id], expected_type: ::User).model_id params[:assignee_id] &&= ::GitlabSchema.parse_gid(params[:assignee_id], expected_type: ::User).model_id
params params
...@@ -31,7 +35,7 @@ module EE ...@@ -31,7 +35,7 @@ module EE
override :mutually_exclusive_args override :mutually_exclusive_args
def mutually_exclusive_args def mutually_exclusive_args
super + [:milestone_id, :assignee_id] super + [:milestone_id, :iteration_id, :assignee_id]
end end
end end
end end
......
...@@ -81,6 +81,10 @@ module EE ...@@ -81,6 +81,10 @@ module EE
if options.key?(:milestone) if options.key?(:milestone)
json[:milestone] = MilestoneSerializer.new.represent(milestone).as_json json[:milestone] = MilestoneSerializer.new.represent(milestone).as_json
end end
if options.key?(:iteration)
json[:iteration] = IterationSerializer.new.represent(iteration).as_json
end
end end
end end
......
# frozen_string_literal: true
class IterationSerializer < BaseSerializer
entity API::Entities::Iteration
end
...@@ -8,6 +8,42 @@ module EE ...@@ -8,6 +8,42 @@ module EE
include MaxLimits include MaxLimits
override :execute
def execute(board)
return ServiceResponse.error(message: 'iteration_board_lists feature flag is disabled') if type == :iteration && ::Feature.disabled?(:iteration_board_lists, board.resource_parent)
return license_validation_error unless valid_license?(board.resource_parent)
super
end
private
def valid_license?(parent)
license_name = case type
when :assignee
:board_assignee_lists
when :milestone
:board_milestone_lists
when :iteration
:iterations
end
license_name.nil? || parent.feature_available?(license_name)
end
def license_validation_error
message = case type
when :assignee
_('Assignee lists not available with your current license')
when :milestone
_('Milestone lists not available with your current license')
when :iteration
_('Iteration lists not available with your current license')
end
ServiceResponse.error(message: message)
end
override :type override :type
def type def type
# We don't ever expect to have more than one list # We don't ever expect to have more than one list
...@@ -16,6 +52,8 @@ module EE ...@@ -16,6 +52,8 @@ module EE
:assignee :assignee
elsif params.key?('milestone_id') elsif params.key?('milestone_id')
:milestone :milestone
elsif params.key?('iteration_id')
:iteration
else else
super super
end end
...@@ -29,6 +67,8 @@ module EE ...@@ -29,6 +67,8 @@ module EE
find_user(board) find_user(board)
when :milestone when :milestone
find_milestone(board) find_milestone(board)
when :iteration
find_iteration(board)
else else
super super
end end
...@@ -46,13 +86,16 @@ module EE ...@@ -46,13 +86,16 @@ module EE
) )
end end
private
def find_milestone(board) def find_milestone(board)
milestones = milestone_finder(board).execute milestones = milestone_finder(board).execute
milestones.find_by(id: params['milestone_id']) # rubocop: disable CodeReuse/ActiveRecord milestones.find_by(id: params['milestone_id']) # rubocop: disable CodeReuse/ActiveRecord
end end
def find_iteration(board)
parent_params = ::IterationsFinder.params_for_parent(board.resource_parent, include_ancestors: true)
::IterationsFinder.new(current_user, parent_params).find_by(id: params['iteration_id']) # rubocop: disable CodeReuse/ActiveRecord
end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def find_user(board) def find_user(board)
user_ids = user_finder(board).execute.select(:user_id) user_ids = user_finder(board).execute.select(:user_id)
......
---
name: iteration_board_lists
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49688
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/250479
milestone: '13.7'
type: development
group: group::project management
default_enabled: false
# frozen_string_literal: true
module API
module Entities
class Iteration < Grape::Entity
expose :id, :iid
expose :project_id, if: -> (entity, options) { entity&.project_id }
expose :group_id, if: -> (entity, options) { entity&.group_id }
expose :title, :description
expose :state_enum, as: :state
expose :created_at, :updated_at
expose :start_date, :due_date
end
end
end
...@@ -19,7 +19,7 @@ module API ...@@ -19,7 +19,7 @@ module API
def list_iterations_for(parent) def list_iterations_for(parent)
iterations = IterationsFinder.new(current_user, iterations_finder_params(parent)).execute iterations = IterationsFinder.new(current_user, iterations_finder_params(parent)).execute
present paginate(iterations), with: EE::API::Entities::Iteration present paginate(iterations), with: Entities::Iteration
end end
def iterations_finder_params(parent) def iterations_finder_params(parent)
...@@ -36,7 +36,7 @@ module API ...@@ -36,7 +36,7 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of project iterations' do desc 'Get a list of project iterations' do
detail 'This feature was introduced in GitLab 13.5' detail 'This feature was introduced in GitLab 13.5'
success EE::API::Entities::Iteration success Entities::Iteration
end end
params do params do
use :list_params use :list_params
...@@ -54,7 +54,7 @@ module API ...@@ -54,7 +54,7 @@ module API
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group iterations' do desc 'Get a list of group iterations' do
detail 'This feature was introduced in GitLab 13.5' detail 'This feature was introduced in GitLab 13.5'
success EE::API::Entities::Iteration success Entities::Iteration
end end
params do params do
use :list_params use :list_params
......
...@@ -7,17 +7,13 @@ module EE ...@@ -7,17 +7,13 @@ module EE
prepended do prepended do
helpers do helpers do
# Overrides API::BoardsResponses create_list_params
def create_list_params
params.slice(:label_id, :milestone_id, :assignee_id)
end
# Overrides API::BoardsResponses list_creation_params # Overrides API::BoardsResponses list_creation_params
params :list_creation_params do params :list_creation_params do
optional :label_id, type: Integer, desc: 'The ID of an existing label' optional :label_id, type: Integer, desc: 'The ID of an existing label'
optional :milestone_id, type: Integer, desc: 'The ID of an existing milestone' optional :milestone_id, type: Integer, desc: 'The ID of an existing milestone'
optional :iteration_id, type: Integer, desc: 'The ID of an assignee iteration'
optional :assignee_id, type: Integer, desc: 'The ID of an assignee' optional :assignee_id, type: Integer, desc: 'The ID of an assignee'
exactly_one_of :label_id, :milestone_id, :assignee_id exactly_one_of :label_id, :milestone_id, :iteration_id, :assignee_id
end end
# Overrides API::BoardsResponses update_params # Overrides API::BoardsResponses update_params
......
# frozen_string_literal: true
module EE
module API
module Entities
class Iteration < Grape::Entity
expose :id, :iid
expose :project_id, if: -> (entity, options) { entity&.project_id }
expose :group_id, if: -> (entity, options) { entity&.group_id }
expose :title, :description
expose :state_enum, as: :state
expose :created_at, :updated_at
expose :start_date, :due_date
end
end
end
end
...@@ -8,6 +8,7 @@ module EE ...@@ -8,6 +8,7 @@ module EE
prepended do prepended do
expose :milestone, using: ::API::Entities::Milestone, if: -> (entity, _) { entity.milestone? } expose :milestone, using: ::API::Entities::Milestone, if: -> (entity, _) { entity.milestone? }
expose :iteration, using: ::API::Entities::Iteration, if: -> (entity, _) { entity.iteration? }
expose :user, as: :assignee, using: ::API::Entities::UserSafe, if: -> (entity, _) { entity.assignee? } expose :user, as: :assignee, using: ::API::Entities::UserSafe, if: -> (entity, _) { entity.assignee? }
expose :max_issue_count, if: -> (list, _) { list.wip_limits_available? } expose :max_issue_count, if: -> (list, _) { list.wip_limits_available? }
expose :max_issue_weight, if: -> (list, _) { list.wip_limits_available? } expose :max_issue_weight, if: -> (list, _) { list.wip_limits_available? }
......
...@@ -13,7 +13,7 @@ module EE ...@@ -13,7 +13,7 @@ module EE
expose :resource_id do |event, _options| expose :resource_id do |event, _options|
event.issuable.id event.issuable.id
end end
expose :iteration, using: Entities::Iteration expose :iteration, using: ::API::Entities::Iteration
expose :action expose :action
end end
end end
......
...@@ -52,19 +52,59 @@ RSpec.describe Boards::ListsController do ...@@ -52,19 +52,59 @@ RSpec.describe Boards::ListsController do
describe 'POST create' do describe 'POST create' do
context 'with valid params' do context 'with valid params' do
context 'for label lists' do
it 'returns a successful 200 response' do it 'returns a successful 200 response' do
create_board_list user: user, board: board, label_id: label.id create_board_list user: user, board: board, label_id: label.id
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('list', dir: 'ee')
end
end end
it 'returns the created list' do context 'for iteration lists' do
create_board_list user: user, board: board, label_id: label.id let_it_be(:iteration) { create(:iteration, group: group) }
context 'when iteration_board_lists is disabled' do
before do
stub_feature_flags(iteration_board_lists: false)
end
it 'returns an error' do
create_board_list user: user, board: board, iteration_id: iteration.id
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['errors']).to eq(['iteration_board_lists feature flag is disabled'])
end
end
context 'when license is available' do
before do
stub_licensed_features(iterations: true)
end
it 'returns a successful 200 response' do
create_board_list user: user, board: board, iteration_id: iteration.id
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('list', dir: 'ee') expect(response).to match_response_schema('list', dir: 'ee')
end end
end end
context 'when license is unavailable' do
before do
stub_licensed_features(iterations: false)
end
it 'returns an error' do
create_board_list user: user, board: board, iteration_id: iteration.id
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['errors']).to eq(['Iteration lists not available with your current license'])
end
end
end
end
context 'with max issue count' do context 'with max issue count' do
context 'with licensed wip limits' do context 'with licensed wip limits' do
it 'returns the created list' do it 'returns the created list' do
...@@ -166,9 +206,9 @@ RSpec.describe Boards::ListsController do ...@@ -166,9 +206,9 @@ RSpec.describe Boards::ListsController do
end end
context 'with invalid params' do context 'with invalid params' do
context 'when label is nil' do context 'when label is empty' do
it 'returns an unprocessable entity 422 response' do it 'returns an unprocessable entity 422 response' do
create_board_list user: user, board: board, label_id: nil create_board_list user: user, board: board, label_id: ''
expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['errors']).to eq(['Label not found']) expect(json_response['errors']).to eq(['Label not found'])
...@@ -195,12 +235,12 @@ RSpec.describe Boards::ListsController do ...@@ -195,12 +235,12 @@ RSpec.describe Boards::ListsController do
end end
end end
def create_board_list(user:, board:, label_id:, params: {}) def create_board_list(user:, board:, label_id: nil, iteration_id: nil, params: {})
sign_in(user) sign_in(user)
post :create, params: { post :create, params: {
board_id: board.to_param, board_id: board.to_param,
list: { label_id: label_id }.merge(params) list: { label_id: label_id, iteration_id: iteration_id }.compact.merge!(params)
}, },
format: :json format: :json
end end
......
...@@ -18,7 +18,7 @@ RSpec.describe IterationsFinder do ...@@ -18,7 +18,7 @@ RSpec.describe IterationsFinder do
context 'without permissions' do context 'without permissions' do
context 'groups and projects' do context 'groups and projects' do
let(:params) { { project_ids: project_ids, group_ids: group.id, state: 'all' } } let(:params) { { project_ids: project_ids, group_ids: group.id } }
it 'returns iterations for groups and projects' do it 'returns iterations for groups and projects' do
expect(subject).to be_empty expect(subject).to be_empty
...@@ -34,7 +34,7 @@ RSpec.describe IterationsFinder do ...@@ -34,7 +34,7 @@ RSpec.describe IterationsFinder do
end end
context 'iterations for projects' do context 'iterations for projects' do
let(:params) { { project_ids: project_ids, state: 'all' } } let(:params) { { project_ids: project_ids } }
it 'returns iterations for projects' do it 'returns iterations for projects' do
expect(subject).to contain_exactly(iteration_from_project_1, iteration_from_project_2) expect(subject).to contain_exactly(iteration_from_project_1, iteration_from_project_2)
...@@ -42,7 +42,7 @@ RSpec.describe IterationsFinder do ...@@ -42,7 +42,7 @@ RSpec.describe IterationsFinder do
end end
context 'iterations for groups' do context 'iterations for groups' do
let(:params) { { group_ids: group.id, state: 'all' } } let(:params) { { group_ids: group.id } }
it 'returns iterations for groups' do it 'returns iterations for groups' do
expect(subject).to contain_exactly(started_group_iteration, upcoming_group_iteration) expect(subject).to contain_exactly(started_group_iteration, upcoming_group_iteration)
...@@ -50,7 +50,7 @@ RSpec.describe IterationsFinder do ...@@ -50,7 +50,7 @@ RSpec.describe IterationsFinder do
end end
context 'iterations for groups and project' do context 'iterations for groups and project' do
let(:params) { { project_ids: project_ids, group_ids: group.id, state: 'all' } } let(:params) { { project_ids: project_ids, group_ids: group.id } }
it 'returns iterations for groups and projects' do it 'returns iterations for groups and projects' do
expect(subject).to contain_exactly(started_group_iteration, upcoming_group_iteration, iteration_from_project_1, iteration_from_project_2) expect(subject).to contain_exactly(started_group_iteration, upcoming_group_iteration, iteration_from_project_1, iteration_from_project_2)
...@@ -69,8 +69,7 @@ RSpec.describe IterationsFinder do ...@@ -69,8 +69,7 @@ RSpec.describe IterationsFinder do
let(:params) do let(:params) do
{ {
project_ids: project_ids, project_ids: project_ids,
group_ids: group.id, group_ids: group.id
state: 'all'
} }
end end
...@@ -79,6 +78,12 @@ RSpec.describe IterationsFinder do ...@@ -79,6 +78,12 @@ RSpec.describe IterationsFinder do
iteration_from_project_1.close iteration_from_project_1.close
end end
it 'filters by all states' do
params[:state] = 'all'
expect(subject).to contain_exactly(started_group_iteration, upcoming_group_iteration, iteration_from_project_1, iteration_from_project_2)
end
it 'filters by started state' do it 'filters by started state' do
params[:state] = 'started' params[:state] = 'started'
...@@ -154,7 +159,7 @@ RSpec.describe IterationsFinder do ...@@ -154,7 +159,7 @@ RSpec.describe IterationsFinder do
describe '#find_by' do describe '#find_by' do
it 'finds a single iteration' do it 'finds a single iteration' do
finder = described_class.new(user, project_ids: [project_1.id], state: 'all') finder = described_class.new(user, project_ids: [project_1.id])
expect(finder.find_by(iid: iteration_from_project_1.iid)).to eq(iteration_from_project_1) expect(finder.find_by(iid: iteration_from_project_1.iid)).to eq(iteration_from_project_1)
end end
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
"$ref": "../../../../../spec/fixtures/api/schemas/list.json" "$ref": "../../../../../spec/fixtures/api/schemas/list.json"
}, },
{ {
"required": ["user"], "required": ["user", "iteration"],
"properties": { "properties": {
"user": { "user": {
"type": [ "type": [
...@@ -44,6 +44,35 @@ ...@@ -44,6 +44,35 @@
"type": "string" "type": "string"
} }
} }
},
"iteration": {
"type": [
"object",
"null"
],
"required": [
"id",
"title",
"description",
"state"
],
"properties": {
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"state": {
"type": "integer"
}
}
} }
} }
} }
......
...@@ -21,7 +21,7 @@ RSpec.describe Mutations::Boards::Lists::Create do ...@@ -21,7 +21,7 @@ RSpec.describe Mutations::Boards::Lists::Create do
end end
before do before do
stub_licensed_features(board_assignee_lists: true, board_milestone_lists: true) stub_licensed_features(board_assignee_lists: true, board_milestone_lists: true, iterations: true)
end end
subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) } subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) }
...@@ -30,13 +30,13 @@ RSpec.describe Mutations::Boards::Lists::Create do ...@@ -30,13 +30,13 @@ RSpec.describe Mutations::Boards::Lists::Create 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 backlog or labelId or milestoneId or assigneeId is required') 'one and only one of backlog or labelId or milestoneId or iterationId or assigneeId 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', milestone_id: 'some milestone', assignee_id: 'some label') } expect { mutation.ready?(board_id: 'some id', milestone_id: 'some milestone', assignee_id: 'some label') }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, .to raise_error(Gitlab::Graphql::Errors::ArgumentError,
'one and only one of backlog or labelId or milestoneId or assigneeId is required') 'one and only one of backlog or labelId or milestoneId or iterationId or assigneeId is required')
end end
end end
...@@ -49,7 +49,7 @@ RSpec.describe Mutations::Boards::Lists::Create do ...@@ -49,7 +49,7 @@ RSpec.describe Mutations::Boards::Lists::Create do
it 'returns an error' do it 'returns an error' do
stub_licensed_features(board_milestone_lists: false) stub_licensed_features(board_milestone_lists: false)
expect(subject[:errors]).to include 'List type Milestone lists not available with your current license' expect(subject[:errors]).to include 'Milestone lists not available with your current license'
end end
end end
...@@ -79,7 +79,7 @@ RSpec.describe Mutations::Boards::Lists::Create do ...@@ -79,7 +79,7 @@ RSpec.describe Mutations::Boards::Lists::Create do
it 'returns an error' do it 'returns an error' do
stub_licensed_features(board_assignee_lists: false) stub_licensed_features(board_assignee_lists: false)
expect(subject[:errors]).to include 'List type Assignee lists not available with your current license' expect(subject[:errors]).to include 'Assignee lists not available with your current license'
end end
end end
...@@ -101,6 +101,45 @@ RSpec.describe Mutations::Boards::Lists::Create do ...@@ -101,6 +101,45 @@ RSpec.describe Mutations::Boards::Lists::Create do
end end
end end
end end
describe 'iteration list' do
let(:iteration) { create(:iteration, group: group) }
let(:list_create_params) { { iteration_id: iteration.to_global_id.to_s } }
context 'when feature unavailable' do
it 'returns an error' do
stub_licensed_features(iterations: false)
expect(subject[:errors]).to include 'Iteration lists not available with your current license'
end
end
context 'when feature flag is disabled' do
it 'returns an error' do
stub_feature_flags(iteration_board_lists: false)
expect(subject[:errors]).to include 'iteration_board_lists feature flag is disabled'
end
end
it 'creates a new issue board list for the iteration' do
expect { subject }.to change { board.lists.count }.from(1).to(2)
new_list = subject[:list]
expect(new_list.title).to eq "#{iteration.title}"
expect(new_list.iteration_id).to eq iteration.id
expect(new_list.position).to eq 0
end
context 'when iteration not found' do
let(:list_create_params) { { iteration_id: "gid://gitlab/Iteration/#{non_existing_record_id}" } }
it 'returns an error' do
expect(subject[:errors]).to include 'Iteration not found'
end
end
end
end end
context 'without proper permissions' do context 'without proper permissions' do
......
...@@ -4,10 +4,15 @@ require 'spec_helper' ...@@ -4,10 +4,15 @@ require 'spec_helper'
RSpec.describe API::Boards do RSpec.describe API::Boards do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } let_it_be(:group) { create(:group, :public) }
let_it_be(:board_parent) { create(:project, :public, group: group ) }
let_it_be(:milestone) { create(:milestone, project: board_parent) } let_it_be(:milestone) { create(:milestone, project: board_parent) }
let_it_be(:board) { create(:board, project: board_parent, milestone: milestone, assignee: user) } let_it_be(:board) { create(:board, project: board_parent, milestone: milestone, assignee: user) }
before_all do
group.add_maintainer(user)
end
it_behaves_like 'multiple and scoped issue boards', "/projects/:id/boards" it_behaves_like 'multiple and scoped issue boards', "/projects/:id/boards"
before do before do
...@@ -19,6 +24,9 @@ RSpec.describe API::Boards do ...@@ -19,6 +24,9 @@ RSpec.describe API::Boards do
it_behaves_like 'milestone board list' it_behaves_like 'milestone board list'
it_behaves_like 'assignee board list' it_behaves_like 'assignee board list'
it_behaves_like 'iteration board list' do
let_it_be(:iteration) { create(:iteration, group: group) }
end
end end
context 'GET /projects/:id/boards/:board_id with special milestones' do context 'GET /projects/:id/boards/:board_id with special milestones' do
......
...@@ -9,7 +9,7 @@ RSpec.describe API::GroupBoards do ...@@ -9,7 +9,7 @@ RSpec.describe API::GroupBoards do
let_it_be(:admin) { create(:user, :admin) } let_it_be(:admin) { create(:user, :admin) }
let_it_be(:board_parent) { create(:group, :public) } let_it_be(:board_parent) { create(:group, :public) }
before do before_all do
board_parent.add_owner(user) board_parent.add_owner(user)
end end
...@@ -54,5 +54,8 @@ RSpec.describe API::GroupBoards do ...@@ -54,5 +54,8 @@ RSpec.describe API::GroupBoards do
it_behaves_like 'milestone board list' it_behaves_like 'milestone board list'
it_behaves_like 'assignee board list' it_behaves_like 'assignee board list'
it_behaves_like 'iteration board list' do
let_it_be(:iteration) { create(:iteration, group: board_parent) }
end
end end
end end
...@@ -4,8 +4,9 @@ require 'spec_helper' ...@@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe Boards::Lists::CreateService do RSpec.describe Boards::Lists::CreateService do
describe '#execute' do describe '#execute' do
let_it_be(:project) { create(:project) } let_it_be(:group) { create(:group) }
let_it_be(:board) { create(:board, project: project) } let_it_be(:project) { create(:project, group: group) }
let_it_be(:board, refind: true) { create(:board, project: project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
context 'when assignee_id param is sent' do context 'when assignee_id param is sent' do
...@@ -31,7 +32,6 @@ RSpec.describe Boards::Lists::CreateService do ...@@ -31,7 +32,6 @@ RSpec.describe Boards::Lists::CreateService do
end end
context 'when milestone_id param is sent' do context 'when milestone_id param is sent' do
let_it_be(:user) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
before_all do before_all do
...@@ -52,6 +52,56 @@ RSpec.describe Boards::Lists::CreateService do ...@@ -52,6 +52,56 @@ RSpec.describe Boards::Lists::CreateService do
end end
end end
context 'when iteration_id param is sent' do
let_it_be(:iteration) { create(:iteration, group: group) }
before_all do
group.add_developer(user)
end
subject(:service) { described_class.new(project, user, 'iteration_id' => iteration.id) }
before do
stub_licensed_features(iterations: true)
end
it 'creates an iteration list when param is valid' do
response = service.execute(board)
expect(response.success?).to eq(true)
expect(response.payload[:list].list_type).to eq('iteration')
end
context 'when iteration is from another group' do
let_it_be(:iteration) { create(:iteration) }
it 'returns an error' do
response = service.execute(board)
expect(response.success?).to eq(false)
expect(response.errors).to include('Iteration not found')
end
end
it 'returns an error when feature flag is disabled' do
stub_feature_flags(iteration_board_lists: false)
response = service.execute(board)
expect(response.success?).to eq(false)
expect(response.errors).to include('iteration_board_lists feature flag is disabled')
end
it 'returns an error when license is unavailable' do
stub_licensed_features(iterations: false)
response = service.execute(board)
expect(response.success?).to eq(false)
expect(response.errors).to include('Iteration lists not available with your current license')
end
end
context 'max limits' do context 'max limits' do
describe '#create_list_attributes' do describe '#create_list_attributes' do
shared_examples 'attribute provider for list creation' do shared_examples 'attribute provider for list creation' do
...@@ -90,7 +140,7 @@ RSpec.describe Boards::Lists::CreateService do ...@@ -90,7 +140,7 @@ RSpec.describe Boards::Lists::CreateService do
it 'contains the expected max limits' do it 'contains the expected max limits' do
service = described_class.new(project, user, params) service = described_class.new(project, user, params)
attrs = service.create_list_attributes(nil, nil, nil) attrs = service.send(:create_list_attributes, nil, nil, nil)
if wip_limits_enabled if wip_limits_enabled
expect(attrs).to include(max_issue_count: expected_max_issue_count, expect(attrs).to include(max_issue_count: expected_max_issue_count,
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'assignee board list' do RSpec.shared_examples 'assignee board list' do
before do
stub_licensed_features(board_assignee_lists: true)
end
context 'when assignee_id is sent' do context 'when assignee_id is sent' do
it 'returns 400 if user is not found' do it 'returns 400 if user is not found' do
other_user = create(:user) other_user = create(:user)
...@@ -17,12 +21,10 @@ RSpec.shared_examples 'assignee board list' do ...@@ -17,12 +21,10 @@ RSpec.shared_examples 'assignee board list' do
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response.dig('message', 'error')) expect(json_response.dig('message', 'error'))
.to eq('List type Assignee lists not available with your current license') .to eq('Assignee lists not available with your current license')
end end
it 'creates an assignee list if user is found' do it 'creates an assignee list if user is found' do
stub_licensed_features(board_assignee_lists: true)
post api(url, user), params: { assignee_id: user.id } post api(url, user), params: { assignee_id: user.id }
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
......
# frozen_string_literal: true
RSpec.shared_examples 'iteration board list' do
before do
stub_licensed_features(iterations: true)
end
context 'when iteration_id is sent' do
it 'returns 400 if iteration is not found' do
other_iteration = create(:iteration)
post api(url, user), params: { iteration_id: other_iteration.id }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response.dig('message', 'error')).to eq('Iteration not found')
end
it 'returns 400 if feature flag is disabled' do
stub_feature_flags(iteration_board_lists: false)
post api(url, user), params: { iteration_id: iteration.id }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response.dig('message', 'error')).to eq('iteration_board_lists feature flag is disabled')
end
it 'returns 400 if not licensed' do
stub_licensed_features(iterations: false)
post api(url, user), params: { iteration_id: iteration.id }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response.dig('message', 'error'))
.to eq('Iteration lists not available with your current license')
end
it 'creates an iteration list if iteration is found' do
post api(url, user), params: { iteration_id: iteration.id }
expect(response).to have_gitlab_http_status(:created)
expect(json_response.dig('iteration', 'id')).to eq(iteration.id)
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'milestone board list' do RSpec.shared_examples 'milestone board list' do
before do
stub_licensed_features(board_milestone_lists: true)
end
context 'when milestone_id is sent' do context 'when milestone_id is sent' do
it 'returns 400 if milestone is not found' do it 'returns 400 if milestone is not found' do
other_milestone = create(:milestone) other_milestone = create(:milestone)
...@@ -17,12 +21,10 @@ RSpec.shared_examples 'milestone board list' do ...@@ -17,12 +21,10 @@ RSpec.shared_examples 'milestone board list' do
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response.dig('message', 'error')) expect(json_response.dig('message', 'error'))
.to eq('List type Milestone lists not available with your current license') .to eq('Milestone lists not available with your current license')
end end
it 'creates a milestone list if milestone is found' do it 'creates a milestone list if milestone is found' do
stub_licensed_features(board_milestone_lists: true)
post api(url, user), params: { milestone_id: milestone.id } post api(url, user), params: { milestone_id: milestone.id }
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
......
...@@ -45,7 +45,7 @@ module API ...@@ -45,7 +45,7 @@ module API
def create_list def create_list
create_list_service = create_list_service =
::Boards::Lists::CreateService.new(board_parent, current_user, create_list_params) ::Boards::Lists::CreateService.new(board_parent, current_user, declared_params.compact.with_indifferent_access)
response = create_list_service.execute(board) response = create_list_service.execute(board)
...@@ -56,10 +56,6 @@ module API ...@@ -56,10 +56,6 @@ module API
end end
end end
def create_list_params
params.slice(:label_id)
end
def move_list(list) def move_list(list)
move_list_service = move_list_service =
::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i }) ::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i })
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
"id": { "type": "integer" }, "id": { "type": "integer" },
"list_type": { "list_type": {
"type": "string", "type": "string",
"enum": ["backlog", "label", "closed"] "enum": ["backlog", "label", "iteration", "closed"]
}, },
"label": { "label": {
"type": ["object", "null"], "type": ["object", "null"],
......
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