Commit 512ffad9 authored by Dmytro Zaporozhets (DZ)'s avatar Dmytro Zaporozhets (DZ)

Merge branch '238564-graphql-mutation-to-update-incident-severity' into 'master'

Allow to update issuable severity via GraphQL

Closes #238564

See merge request gitlab-org/gitlab!40869
parents ed400ae4 565a5ad4
# frozen_string_literal: true
module Mutations
module Issues
class SetSeverity < Base
graphql_name 'IssueSetSeverity'
argument :severity, Types::IssuableSeverityEnum, required: true,
description: 'Set the incident severity level.'
def resolve(project_path:, iid:, severity:)
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
::Issues::UpdateService.new(project, current_user, severity: severity)
.execute(issue)
{
issue: issue,
errors: errors_on_object(issue)
}
end
end
end
end
...@@ -24,6 +24,7 @@ module Types ...@@ -24,6 +24,7 @@ module Types
mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetLocked mount_mutation Mutations::Issues::SetLocked
mount_mutation Mutations::Issues::SetDueDate mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::Issues::SetSeverity
mount_mutation Mutations::Issues::SetSubscription mount_mutation Mutations::Issues::SetSubscription
mount_mutation Mutations::Issues::Update mount_mutation Mutations::Issues::Update
mount_mutation Mutations::MergeRequests::Create mount_mutation Mutations::MergeRequests::Create
......
...@@ -195,6 +195,15 @@ module Issuable ...@@ -195,6 +195,15 @@ module Issuable
issuable_severity&.severity || IssuableSeverity::DEFAULT issuable_severity&.severity || IssuableSeverity::DEFAULT
end end
def update_severity(severity)
return unless incident?
severity = severity.to_s.downcase
severity = IssuableSeverity::DEFAULT unless IssuableSeverity.severities.key?(severity)
(issuable_severity || build_issuable_severity(issue_id: id)).update(severity: severity)
end
private private
def description_max_length_for_new_records_is_valid def description_max_length_for_new_records_is_valid
......
...@@ -184,10 +184,7 @@ class IssuableBaseService < BaseService ...@@ -184,10 +184,7 @@ class IssuableBaseService < BaseService
handle_quick_actions(issuable) handle_quick_actions(issuable)
filter_params(issuable) filter_params(issuable)
change_state(issuable) change_additional_attributes(issuable)
change_subscription(issuable)
change_todo(issuable)
toggle_award(issuable)
old_associations = associations_before_update(issuable) old_associations = associations_before_update(issuable)
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
...@@ -305,6 +302,14 @@ class IssuableBaseService < BaseService ...@@ -305,6 +302,14 @@ class IssuableBaseService < BaseService
issuable.title_changed? || issuable.description_changed? issuable.title_changed? || issuable.description_changed?
end end
def change_additional_attributes(issuable)
change_state(issuable)
change_severity(issuable)
change_subscription(issuable)
change_todo(issuable)
toggle_award(issuable)
end
def change_state(issuable) def change_state(issuable)
case params.delete(:state_event) case params.delete(:state_event)
when 'reopen' when 'reopen'
...@@ -314,6 +319,12 @@ class IssuableBaseService < BaseService ...@@ -314,6 +319,12 @@ class IssuableBaseService < BaseService
end end
end end
def change_severity(issuable)
if severity = params.delete(:severity)
issuable.update_severity(severity)
end
end
def change_subscription(issuable) def change_subscription(issuable)
case params.delete(:subscription_event) case params.delete(:subscription_event)
when 'subscribe' when 'subscribe'
......
---
title: Allows to update incident severity via GraphQL
merge_request: 40869
author:
type: added
...@@ -8075,6 +8075,51 @@ type IssueSetLockedPayload { ...@@ -8075,6 +8075,51 @@ type IssueSetLockedPayload {
issue: Issue issue: Issue
} }
"""
Autogenerated input type of IssueSetSeverity
"""
input IssueSetSeverityInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The IID of the issue to mutate
"""
iid: String!
"""
The project the issue to mutate is in
"""
projectPath: ID!
"""
Set the incident severity level.
"""
severity: IssuableSeverity!
}
"""
Autogenerated return type of IssueSetSeverity
"""
type IssueSetSeverityPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The issue after mutation
"""
issue: Issue
}
""" """
Autogenerated input type of IssueSetSubscription Autogenerated input type of IssueSetSubscription
""" """
...@@ -10279,6 +10324,7 @@ type Mutation { ...@@ -10279,6 +10324,7 @@ type Mutation {
issueSetEpic(input: IssueSetEpicInput!): IssueSetEpicPayload issueSetEpic(input: IssueSetEpicInput!): IssueSetEpicPayload
issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload
issueSetLocked(input: IssueSetLockedInput!): IssueSetLockedPayload issueSetLocked(input: IssueSetLockedInput!): IssueSetLockedPayload
issueSetSeverity(input: IssueSetSeverityInput!): IssueSetSeverityPayload
issueSetSubscription(input: IssueSetSubscriptionInput!): IssueSetSubscriptionPayload issueSetSubscription(input: IssueSetSubscriptionInput!): IssueSetSubscriptionPayload
issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload
jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload
......
...@@ -22367,6 +22367,136 @@ ...@@ -22367,6 +22367,136 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "IssueSetSeverityInput",
"description": "Autogenerated input type of IssueSetSeverity",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project the issue to mutate is in",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "iid",
"description": "The IID of the issue to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "severity",
"description": "Set the incident severity level.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "IssuableSeverity",
"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": "IssueSetSeverityPayload",
"description": "Autogenerated return type of IssueSetSeverity",
"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": "issue",
"description": "The issue after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "IssueSetSubscriptionInput", "name": "IssueSetSubscriptionInput",
...@@ -29904,6 +30034,33 @@ ...@@ -29904,6 +30034,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "issueSetSeverity",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "IssueSetSeverityInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "IssueSetSeverityPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "issueSetSubscription", "name": "issueSetSubscription",
"description": null, "description": null,
...@@ -1227,6 +1227,16 @@ Autogenerated return type of IssueSetLocked ...@@ -1227,6 +1227,16 @@ Autogenerated return type of IssueSetLocked
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation | | `issue` | Issue | The issue after mutation |
## IssueSetSeverityPayload
Autogenerated return type of IssueSetSeverity
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation |
## IssueSetSubscriptionPayload ## IssueSetSubscriptionPayload
Autogenerated return type of IssueSetSubscription Autogenerated return type of IssueSetSubscription
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Issues::SetSeverity do
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:incident) }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
specify { expect(described_class).to require_graphql_authorizations(:update_issue) }
describe '#resolve' do
let(:severity) { 'CRITICAL' }
let(:mutated_incident) { subject[:issue] }
subject(:resolve) { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, severity: severity) }
context 'when the user cannot update the issue' do
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can update the issue' do
before do
issue.project.add_developer(user)
end
context 'when issue type is incident' do
context 'when severity has a correct value' do
it 'updates severity' do
expect(resolve[:issue].severity).to eq('critical')
end
it 'returns no errors' do
expect(resolve[:errors]).to be_empty
end
end
context 'when severity has an unsuported value' do
let(:severity) { 'unsupported-severity' }
it 'sets severity to default' do
expect(resolve[:issue].severity).to eq(IssuableSeverity::DEFAULT)
end
it 'returns no errorsr' do
expect(resolve[:errors]).to be_empty
end
end
end
context 'when issue type is not incident' do
let!(:issue) { create(:issue) }
it 'does not updates the issue' do
expect { resolve }.not_to change { issue.updated_at }
end
end
end
end
end
...@@ -29,6 +29,7 @@ issues: ...@@ -29,6 +29,7 @@ issues:
- merge_requests_closing_issues - merge_requests_closing_issues
- metrics - metrics
- timelogs - timelogs
- issuable_severity
- issue_assignees - issue_assignees
- closed_by - closed_by
- epic_issue - epic_issue
...@@ -542,6 +543,8 @@ timelogs: ...@@ -542,6 +543,8 @@ timelogs:
- note - note
push_event_payload: push_event_payload:
- event - event
issuable_severity:
- issue
issue_assignees: issue_assignees:
- issue - issue
- assignee - assignee
......
...@@ -891,4 +891,58 @@ RSpec.describe Issuable do ...@@ -891,4 +891,58 @@ RSpec.describe Issuable do
end end
end end
end end
describe '#update_severity' do
let(:severity) { 'low' }
subject(:update_severity) { issuable.update_severity(severity) }
context 'when issuable not an incident' do
%i(issue merge_request).each do |issuable_type|
let(:issuable) { build_stubbed(issuable_type) }
it { is_expected.to be_nil }
it 'does not set severity' do
expect { subject }.not_to change(IssuableSeverity, :count)
end
end
end
context 'when issuable is an incident' do
let!(:issuable) { create(:incident) }
context 'when issuable does not have issuable severity yet' do
it 'creates new record' do
expect { update_severity }.to change { IssuableSeverity.where(issue: issuable).count }.to(1)
end
it 'sets severity to specified value' do
expect { update_severity }.to change { issuable.severity }.to('low')
end
end
context 'when issuable has an issuable severity' do
let!(:issuable_severity) { create(:issuable_severity, issue: issuable, severity: 'medium') }
it 'does not create new record' do
expect { update_severity }.not_to change(IssuableSeverity, :count)
end
it 'updates existing issuable severity' do
expect { update_severity }.to change { issuable_severity.severity }.to(severity)
end
end
context 'when severity value is unsupported' do
let(:severity) { 'unsupported-severity' }
it 'sets the severity to default value' do
update_severity
expect(issuable.issuable_severity.severity).to eq(IssuableSeverity::DEFAULT)
end
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Setting severity level of an incident' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let(:incident) { create(:incident) }
let(:project) { incident.project }
let(:input) { { severity: 'CRITICAL' } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: incident.iid.to_s
}
graphql_mutation(:issue_set_severity, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
issue {
iid
severity
}
QL
)
end
def mutation_response
graphql_mutation_response(:issue_set_severity)
end
context 'when the user is not allowed to update the incident' do
it 'returns an error' do
error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(graphql_errors).to include(a_hash_including('message' => error))
end
end
context 'when the user is allowed to update the incident' do
before do
project.add_developer(user)
end
it 'updates the issue' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('issue', 'severity')).to eq('CRITICAL')
end
end
end
...@@ -52,7 +52,8 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -52,7 +52,8 @@ RSpec.describe Issues::UpdateService, :mailer do
state_event: 'close', state_event: 'close',
label_ids: [label.id], label_ids: [label.id],
due_date: Date.tomorrow, due_date: Date.tomorrow,
discussion_locked: true discussion_locked: true,
severity: 'low'
} }
end end
...@@ -71,6 +72,24 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -71,6 +72,24 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(issue.discussion_locked).to be_truthy expect(issue.discussion_locked).to be_truthy
end end
context 'when issue type is not incident' do
it 'returns default severity' do
update_issue(opts)
expect(issue.severity).to eq(IssuableSeverity::DEFAULT)
end
end
context 'when issue type is incident' do
let(:issue) { create(:incident, project: project) }
it 'changes updates the severity' do
update_issue(opts)
expect(issue.severity).to eq('low')
end
end
it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do
issue # make sure the issue is created first so our counts are correct. issue # make sure the issue is created first so our counts are correct.
......
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