Commit 7397291c authored by Jarka Košanová's avatar Jarka Košanová

Add graphQL mutation for destroying board lists

- add mutation and specs
- update the graphql documentation
- update Boards::Lists::DestroyService to use ServiceResponse
parent d85e7fc8
......@@ -42,7 +42,7 @@ module Boards
list = board.lists.destroyable.find(params[:id])
service = Boards::Lists::DestroyService.new(board_parent, current_user)
if service.execute(list)
if service.execute(list).success?
head :ok
else
head :unprocessable_entity
......
# frozen_string_literal: true
module Mutations
module Boards
module Lists
class Destroy < ::Mutations::BaseMutation
graphql_name 'DestroyBoardList'
field :list,
Types::BoardListType,
null: true,
description: 'The list after mutation.'
argument :list_id, ::Types::GlobalIDType[::List],
required: true,
loads: Types::BoardListType,
description: 'Global ID of the list to destroy. Only label lists are accepted.'
def resolve(list:)
raise_resource_not_available_error! unless can_admin_list?(list)
response = ::Boards::Lists::DestroyService.new(list.board.resource_parent, current_user)
.execute(list)
{
list: response.success? ? nil : list,
errors: response.errors
}
end
private
def can_admin_list?(list)
return false unless list.present?
Ability.allowed?(current_user, :admin_list, list.board)
end
end
end
end
end
......@@ -18,6 +18,7 @@ module Types
mount_mutation Mutations::Boards::Issues::IssueMoveList
mount_mutation Mutations::Boards::Lists::Create
mount_mutation Mutations::Boards::Lists::Update
mount_mutation Mutations::Boards::Lists::Destroy
mount_mutation Mutations::Branches::Create, calls_gitaly: true
mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::Discussions::ToggleResolve
......
......@@ -4,7 +4,9 @@ module Boards
module Lists
class DestroyService < Boards::BaseService
def execute(list)
return false unless list.destroyable?
unless list.destroyable?
return ServiceResponse.error(message: "The list cannot be destroyed. Only label lists can be destroyed.")
end
@board = list.board
......@@ -12,6 +14,8 @@ module Boards
decrement_higher_lists(list)
remove_list(list)
end
ServiceResponse.success
end
private
......@@ -26,7 +30,7 @@ module Boards
# rubocop: enable CodeReuse/ActiveRecord
def remove_list(list)
list.destroy
list.destroy!
end
end
end
......
---
title: Destroy issue board list via GraphQL
merge_request: 43081
author:
type: added
......@@ -4495,6 +4495,41 @@ input DestroyBoardInput {
id: BoardID!
}
"""
Autogenerated input type of DestroyBoardList
"""
input DestroyBoardListInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Global ID of the list to destroy. Only label lists are accepted.
"""
listId: ListID!
}
"""
Autogenerated return type of DestroyBoardList
"""
type DestroyBoardListPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The list after mutation.
"""
list: BoardList
}
"""
Autogenerated return type of DestroyBoard
"""
......@@ -9280,6 +9315,11 @@ Identifier of Label
"""
scalar LabelID
"""
Identifier of List
"""
scalar ListID
"""
List limit metric setting
"""
......@@ -10835,6 +10875,7 @@ type Mutation {
designManagementMove(input: DesignManagementMoveInput!): DesignManagementMovePayload
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
destroyBoard(input: DestroyBoardInput!): DestroyBoardPayload
destroyBoardList(input: DestroyBoardListInput!): DestroyBoardListPayload
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
......
......@@ -12400,6 +12400,108 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DestroyBoardListInput",
"description": "Autogenerated input type of DestroyBoardList",
"fields": null,
"inputFields": [
{
"name": "listId",
"description": "Global ID of the list to destroy. Only label lists are accepted.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ListID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DestroyBoardListPayload",
"description": "Autogenerated return type of DestroyBoardList",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "list",
"description": "The list after mutation.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "BoardList",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DestroyBoardPayload",
......@@ -25732,6 +25834,16 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "ListID",
"description": "Identifier of List",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "ListLimitMetric",
......@@ -31091,6 +31203,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destroyBoardList",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DestroyBoardListInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DestroyBoardListPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destroyNote",
"description": null,
......@@ -740,6 +740,16 @@ A specific version in which designs were added, modified or deleted.
| `id` | ID! | ID of the design version |
| `sha` | ID! | SHA of the design version |
### DestroyBoardListPayload
Autogenerated return type of DestroyBoardList.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `list` | BoardList | The list after mutation. |
### DestroyBoardPayload
Autogenerated return type of DestroyBoard.
......
......@@ -45,7 +45,7 @@ module API
def destroy_list(list)
destroy_conditionally!(list) do |list|
service = ::Boards::Lists::DestroyService.new(board_parent, current_user)
unless service.execute(list)
if service.execute(list).error?
render_api_error!({ error: 'List could not be deleted!' }, 400)
end
end
......
......@@ -260,6 +260,17 @@ RSpec.describe Boards::ListsController do
end
end
context 'with an error service response' do
it 'returns an unprocessable entity response' do
allow(Boards::Lists::DestroyService).to receive(:new)
.and_return(double(execute: ServiceResponse.error(message: 'error')))
remove_board_list user: user, board: board, list: planning
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
def remove_board_list(user:, board:, list:)
sign_in(user)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Boards::Lists::Destroy do
include GraphqlHelpers
let_it_be(:current_user, reload: true) { create(:user) }
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:list) { create(:list, board: board) }
let(:mutation) do
variables = {
list_id: GitlabSchema.id_from_object(list).to_s
}
graphql_mutation(:destroy_board_list, variables)
end
subject { post_graphql_mutation(mutation, current_user: current_user) }
def mutation_response
graphql_mutation_response(:destroy_board_list)
end
context 'when the user does not have permission' do
it_behaves_like 'a mutation that returns a top-level access error'
it 'does not destroy the list' do
expect { subject }.not_to change { List.count }
end
end
context 'when the user has permission' do
before do
project.add_maintainer(current_user)
end
context 'when given id is not for a list' do
let_it_be(:list) { build_stubbed(:issue, project: project) }
it 'returns an error' do
subject
expect(graphql_errors.first['message']).to include('does not represent an instance of List')
end
end
context 'when everything is ok' do
it 'destroys the list' do
expect { subject }.to change { List.count }.from(2).to(1)
end
it 'returns an empty list' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response).to have_key('list')
expect(mutation_response['list']).to be_nil
end
end
context 'when the list is not destroyable' do
let_it_be(:list) { create(:list, board: board, list_type: :backlog) }
it 'does not destroy the list' do
expect { subject }.not_to change { List.count }.from(3)
end
it 'returns an error and not nil list' do
subject
expect(mutation_response['errors']).not_to be_empty
expect(mutation_response['list']).not_to be_nil
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