Commit 48f11c99 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'issue_36427_2' into 'master'

Allow to update issuable health status on GraphQL

See merge request gitlab-org/gitlab!24520
parents 8bc0dd05 8bcf6759
# frozen_string_literal: true
module Mutations
module Issues
class Update < Base
graphql_name 'UpdateIssue'
# Add arguments here instead of creating separate mutations
def resolve(project_path:, iid:, **args)
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
::Issues::UpdateService.new(project, current_user, args).execute(issue)
{
issue: issue,
errors: issue.errors.full_messages
}
end
end
end
end
Mutations::Issues::Update.prepend_if_ee('::EE::Mutations::Issues::Update')
......@@ -11,6 +11,7 @@ module Types
mount_mutation Mutations::AwardEmojis::Toggle
mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::Issues::Update
mount_mutation Mutations::MergeRequests::SetLabels
mount_mutation Mutations::MergeRequests::SetLocked
mount_mutation Mutations::MergeRequests::SetMilestone
......
......@@ -1877,6 +1877,11 @@ type Epic implements Noteable {
"""
hasIssues: Boolean!
"""
Current health status. Available only when feature flag save_issuable_health_status is enabled.
"""
healthStatus: HealthStatus
"""
ID of the epic
"""
......@@ -2252,6 +2257,11 @@ type EpicIssue implements Noteable {
"""
epicIssueId: ID!
"""
Current health status. Available only when feature flag save_issuable_health_status is enabled.
"""
healthStatus: HealthStatus
"""
Global ID of the epic-issue relation
"""
......@@ -3059,6 +3069,15 @@ type GroupPermissions {
readGroup: Boolean!
}
"""
Health status of an issue or epic
"""
enum HealthStatus {
atRisk
needsAttention
onTrack
}
"""
State of a GitLab issue or merge request
"""
......@@ -3179,6 +3198,11 @@ type Issue implements Noteable {
"""
epic: Epic
"""
Current health status. Available only when feature flag save_issuable_health_status is enabled.
"""
healthStatus: HealthStatus
"""
Internal ID of the issue
"""
......@@ -4667,6 +4691,7 @@ type Mutation {
be destroyed during the update, and no Note will be returned
"""
updateImageDiffNote(input: UpdateImageDiffNoteInput!): UpdateImageDiffNotePayload
updateIssue(input: UpdateIssueInput!): UpdateIssuePayload
"""
Updates a Note. If the body of the Note contains only quick actions, the Note
......@@ -7601,6 +7626,11 @@ input UpdateEpicInput {
"""
groupPath: ID!
"""
The desired health status
"""
healthStatus: HealthStatus
"""
The iid of the epic to mutate
"""
......@@ -7697,6 +7727,51 @@ type UpdateImageDiffNotePayload {
note: Note
}
"""
Autogenerated input type of UpdateIssue
"""
input UpdateIssueInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The desired health status
"""
healthStatus: HealthStatus
"""
The iid of the issue to mutate
"""
iid: String!
"""
The project the issue to mutate is in
"""
projectPath: ID!
}
"""
Autogenerated return type of UpdateIssue
"""
type UpdateIssuePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The issue after mutation
"""
issue: Issue
}
"""
Autogenerated input type of UpdateNote
"""
......
......@@ -11162,6 +11162,35 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "HealthStatus",
"description": "Health status of an issue or epic",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "onTrack",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "needsAttention",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "atRisk",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DesignConnection",
......@@ -19453,6 +19482,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateIssue",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateIssueInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "UpdateIssuePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateNote",
"description": "Updates a Note. If the body of the Note contains only quick actions, the Note will be destroyed during the update, and no Note will be returned",
......@@ -20262,6 +20318,132 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UpdateIssuePayload",
"description": "Autogenerated return type of UpdateIssue",
"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": "Reasons why the mutation failed.",
"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",
"name": "UpdateIssueInput",
"description": "Autogenerated input type of UpdateIssue",
"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": "healthStatus",
"description": "The desired health status",
"type": {
"kind": "ENUM",
"name": "HealthStatus",
"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": "MergeRequestSetLabelsPayload",
......@@ -23906,6 +24088,16 @@
},
"defaultValue": null
},
{
"name": "healthStatus",
"description": "The desired health status",
"type": {
"kind": "ENUM",
"name": "HealthStatus",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
......
......@@ -293,6 +293,7 @@ Represents an epic.
| `group` | Group! | Group to which the epic belongs |
| `hasChildren` | Boolean! | Indicates if the epic has children |
| `hasIssues` | Boolean! | Indicates if the epic has direct issues |
| `healthStatus` | HealthStatus | Current health status. Available only when feature flag save_issuable_health_status is enabled. |
| `id` | ID! | ID of the epic |
| `iid` | ID! | Internal ID of the epic |
| `parent` | Epic | Parent epic of the epic |
......@@ -342,6 +343,7 @@ Relationship between an epic and an issue
| `dueDate` | Time | Due date of the issue |
| `epic` | Epic | Epic to which this issue belongs |
| `epicIssueId` | ID! | ID of the epic-issue relation |
| `healthStatus` | HealthStatus | Current health status. Available only when feature flag save_issuable_health_status is enabled. |
| `id` | ID | Global ID of the epic-issue relation |
| `iid` | ID! | Internal ID of the issue |
| `milestone` | Milestone | Milestone of the issue |
......@@ -461,6 +463,7 @@ Autogenerated return type of EpicTreeReorder
| `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue |
| `epic` | Epic | Epic to which this issue belongs |
| `healthStatus` | HealthStatus | Current health status. Available only when feature flag save_issuable_health_status is enabled. |
| `iid` | ID! | Internal ID of the issue |
| `milestone` | Milestone | Milestone of the issue |
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
......@@ -1240,6 +1243,16 @@ Autogenerated return type of UpdateImageDiffNote
| `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation |
## UpdateIssuePayload
Autogenerated return type of UpdateIssue
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `issue` | Issue | The issue after mutation |
## UpdateNotePayload
Autogenerated return type of UpdateNote
......
# frozen_string_literal: true
module EE
module Mutations
module Issues
module Update
extend ActiveSupport::Concern
prepended do
argument :health_status,
::Types::HealthStatusEnum,
required: false,
description: 'The desired health status'
end
end
end
end
end
......@@ -20,6 +20,12 @@ module EE
field :design_collection, ::Types::DesignManagement::DesignCollectionType, null: true,
description: 'Collection of design images associated with this issue'
field :health_status,
::Types::HealthStatusEnum,
null: true,
description: 'Current health status',
feature_flag: :save_issuable_health_status
end
end
end
......
......@@ -16,6 +16,11 @@ module Mutations
required: false,
description: 'State event for the epic'
argument :health_status,
::Types::HealthStatusEnum,
required: false,
description: 'The desired health status'
field :epic,
Types::EpicType,
null: true,
......
......@@ -125,5 +125,11 @@ module Types
resolve: -> (epic, args, ctx) do
Epics::DescendantCountService.new(epic, ctx[:current_user])
end
field :health_status,
::Types::HealthStatusEnum,
null: true,
description: 'Current health status',
feature_flag: :save_issuable_health_status
end
end
# frozen_string_literal: true
module Types
class HealthStatusEnum < BaseEnum
graphql_name 'HealthStatus'
description 'Health status of an issue or epic'
value 'onTrack', value: Issue.health_statuses.key(1)
value 'needsAttention', value: Issue.health_statuses.key(2)
value 'atRisk', value: Issue.health_statuses.key(3)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Issues::Update do
it_behaves_like 'updating health status' do
let(:resource) { create(:issue) }
let(:user) { create(:user) }
end
end
......@@ -11,7 +11,7 @@ describe GitlabSchema.types['Epic'] do
closed_at created_at updated_at children has_children has_issues
web_path web_url relation_path reference issues user_permissions
notes discussions relative_position subscribed participants
descendant_counts upvotes downvotes
descendant_counts upvotes downvotes health_status
]
end
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['HealthStatus'] do
it { expect(described_class.graphql_name).to eq('HealthStatus') }
it 'exposes all the existing epic sort orders' do
expect(described_class.values.keys).to include(*%w[onTrack needsAttention atRisk])
end
end
......@@ -10,4 +10,6 @@ describe GitlabSchema.types['Issue'] do
it { expect(described_class).to have_graphql_field(:designs) }
it { expect(described_class).to have_graphql_field(:design_collection) }
it { expect(described_class).to have_graphql_field(:health_status) }
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'Updating an Epic' do
describe Mutations::Epics::Update do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
......@@ -134,4 +134,9 @@ describe 'Updating an Epic' do
end
end
end
it_behaves_like 'updating health status' do
let(:resource) { epic }
let(:user) { current_user }
end
end
# frozen_string_literal: true
RSpec.shared_examples 'updating health status' do
let(:resource_klass) { resource.class }
let(:mutated_resource) { subject[resource_klass.underscore.to_sym] }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
let(:params) do
{ iid: resource.iid, health_status: resource_klass.health_statuses[:at_risk] }.tap do |args|
if resource.is_a?(Epic)
args[:group_path] = resource.resource_parent.full_path
else
args[:project_path] = resource.resource_parent.full_path
end
end
end
subject { mutation.resolve(params) }
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user has permission' do
before do
resource.resource_parent.add_developer(user)
end
context 'and issuable_heath_status feature is disabled' do
before do
stub_licensed_features(issuable_health_status: false, epics: true)
end
it 'does not update health status' do
expect do
subject
resource.reload
end.not_to change { resource.health_status }
end
end
context 'and issuable_health_status feature is enabled' do
before do
stub_licensed_features(issuable_health_status: true, epics: true)
end
it 'updates health status' do
expect(mutated_resource.health_status).to eq('at_risk')
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