Commit c7395adc authored by Jarka Košanová's avatar Jarka Košanová

Add graphQL mutation for destroying issue boards

- add mutation and specs
- update the graphql documentation
- update Boards::DestroyService to use ServiceResponse
parent e550811c
# frozen_string_literal: true
module Mutations
module Boards
class Destroy < ::Mutations::BaseMutation
graphql_name 'DestroyBoard'
field :board,
Types::BoardType,
null: true,
description: 'The board after mutation'
argument :id,
::Types::GlobalIDType[::Board],
required: true,
description: 'The global ID of the board to destroy'
authorize :admin_board
def resolve(id:)
board = authorized_find!(id: id)
response = ::Boards::DestroyService.new(board.resource_parent, current_user).execute(board)
{
board: response.success? ? nil : board,
errors: response.errors
}
end
private
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::Board)
end
end
end
end
......@@ -14,6 +14,7 @@ module Types
mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle
mount_mutation Mutations::Boards::Destroy
mount_mutation Mutations::Boards::Issues::IssueMoveList
mount_mutation Mutations::Boards::Lists::Create
mount_mutation Mutations::Boards::Lists::Update
......
......@@ -3,9 +3,13 @@
module Boards
class DestroyService < Boards::BaseService
def execute(board)
return false if parent.boards.size == 1
if parent.boards.size == 1
return ServiceResponse.error(message: "The board could not be deleted, because the parent doesn't have any other boards.")
end
board.destroy!
board.destroy
ServiceResponse.success
end
end
end
---
title: Destroy issue board via GraphQL
merge_request: 40930
author:
type: added
......@@ -4340,6 +4340,41 @@ enum DesignVersionEvent {
NONE
}
"""
Autogenerated input type of DestroyBoard
"""
input DestroyBoardInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global ID of the board to destroy
"""
id: BoardID!
}
"""
Autogenerated return type of DestroyBoard
"""
type DestroyBoardPayload {
"""
The board after mutation
"""
board: Board
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
Autogenerated input type of DestroyNote
"""
......@@ -10583,6 +10618,7 @@ type Mutation {
designManagementDelete(input: DesignManagementDeleteInput!): DesignManagementDeletePayload
designManagementMove(input: DesignManagementMoveInput!): DesignManagementMovePayload
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
destroyBoard(input: DestroyBoardInput!): DestroyBoardPayload
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
......
......@@ -11999,6 +11999,108 @@
],
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DestroyBoardInput",
"description": "Autogenerated input type of DestroyBoard",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global ID of the board to destroy",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "BoardID",
"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": "DestroyBoardPayload",
"description": "Autogenerated return type of DestroyBoard",
"fields": [
{
"name": "board",
"description": "The board after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"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
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DestroyNoteInput",
......@@ -30360,6 +30462,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destroyBoard",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DestroyBoardInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DestroyBoardPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destroyNote",
"description": null,
......@@ -728,6 +728,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 |
### DestroyBoardPayload
Autogenerated return type of DestroyBoard
| Field | Type | Description |
| ----- | ---- | ----------- |
| `board` | Board | The board after mutation |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### DestroyNotePayload
Autogenerated return type of DestroyNote
......
......@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe Boards::DestroyService do
describe '#execute' do
let(:project) { create(:project) }
let(:group) { create(:group) }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
shared_examples 'remove the board' do |parent_name|
let(:parent) { public_send(parent_name) }
......@@ -17,13 +17,17 @@ RSpec.describe Boards::DestroyService do
it "removes board from #{parent_name}" do
create(:board, parent_name => parent)
expect { service.execute(board) }.to change(parent.boards, :count).by(-1)
expect do
expect(service.execute(board)).to be_success
end.to change(parent.boards, :count).by(-1)
end
end
context "when #{parent_name} have one board" do
it "does not remove board from #{parent_name}" do
expect { service.execute(board) }.not_to change(group.boards, :count)
expect do
expect(service.execute(board)).to be_error
end.not_to change(parent.boards, :count)
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Boards::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(:other_board) { create(:board, project: project) }
let(:mutation) do
variables = {
id: GitlabSchema.id_from_object(board).to_s
}
graphql_mutation(:destroy_board, variables)
end
subject { post_graphql_mutation(mutation, current_user: current_user) }
def mutation_response
graphql_mutation_response(:destroy_board)
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 board' do
expect { subject }.not_to change { Board.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 board' do
let_it_be(:board) { build_stubbed(:issue, project: project) }
it 'returns an error' do
subject
expect(graphql_errors.first['message']).to include('does not represent an instance of Board')
end
end
context 'when everything is ok' do
it 'destroys the board' do
expect { subject }.to change { Board.count }.from(2).to(1)
end
it 'returns an empty board' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response).to have_key('board')
expect(mutation_response['board']).to be_nil
end
end
context 'when there is only 1 board for the parent' do
before do
other_board.destroy!
end
it 'does not destroy the board' do
expect { subject }.not_to change { Board.count }.from(1)
end
it 'returns an error and not nil board' do
subject
expect(mutation_response['errors']).not_to be_empty
expect(mutation_response['board']).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