diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 3c488fd2d88aecdd85b6cdb5a7f1a5bc764776d3..f4ba752a6a1c376edb24e14be37271c36d64eca8 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -11827,6 +11827,7 @@ Relationship between an epic and an issue. | <a id="epicissueepicissueid"></a>`epicIssueId` | [`ID!`](#id) | ID of the epic-issue relation. | | <a id="epicissueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. | | <a id="epicissueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. | +| <a id="epicissuehasepic"></a>`hasEpic` | [`Boolean!`](#boolean) | Indicates if the issue belongs to an epic. Can return true and not show an associated epic when the user has no access to the epic. | | <a id="epicissuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. | | <a id="epicissuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. | | <a id="epicissuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. | @@ -13332,6 +13333,7 @@ Describes an issuable resource link for incident issues. | <a id="issueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. | | <a id="issueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. | | <a id="issueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. | +| <a id="issuehasepic"></a>`hasEpic` | [`Boolean!`](#boolean) | Indicates if the issue belongs to an epic. Can return true and not show an associated epic when the user has no access to the epic. | | <a id="issuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. | | <a id="issuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. | | <a id="issuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. | diff --git a/ee/app/graphql/ee/types/issue_type.rb b/ee/app/graphql/ee/types/issue_type.rb index 6dd1967b12f82f2366d9618f50fd4eeeb4d2b923..d5738edad13517b7463ff7fb74b64ebc6e4eec62 100644 --- a/ee/app/graphql/ee/types/issue_type.rb +++ b/ee/app/graphql/ee/types/issue_type.rb @@ -8,6 +8,12 @@ module IssueType prepended do field :epic, ::Types::EpicType, null: true, description: 'Epic to which this issue belongs.' + field :has_epic, GraphQL::Types::Boolean, + null: false, + description: "Indicates if the issue belongs to an epic. + Can return true and not show an associated epic when the user has no access to the epic.", + method: :has_epic? + field :iteration, ::Types::IterationType, null: true, description: 'Iteration of the issue.' field :weight, GraphQL::Types::Int, null: true, description: 'Weight of the issue.' diff --git a/ee/app/models/ee/issue.rb b/ee/app/models/ee/issue.rb index 8e1bd31ca3434e42184f19881d7c9218ce461f33..5419fb3145afc510704a26d129503ba5fe70c4a3 100644 --- a/ee/app/models/ee/issue.rb +++ b/ee/app/models/ee/issue.rb @@ -318,6 +318,10 @@ def update_upvotes_count super end + def has_epic? + epic_issue.present? + end + private def blocking_issues_ids diff --git a/ee/spec/graphql/types/issue_type_spec.rb b/ee/spec/graphql/types/issue_type_spec.rb index b5a41ddf246b9d342f7f3d538a700e9511cb5b5b..5ef27564ff7bfa52f477d86b5659862925e634d3 100644 --- a/ee/spec/graphql/types/issue_type_spec.rb +++ b/ee/spec/graphql/types/issue_type_spec.rb @@ -4,6 +4,7 @@ RSpec.describe GitlabSchema.types['Issue'] do it { expect(described_class).to have_graphql_field(:epic) } + it { expect(described_class).to have_graphql_field(:has_epic) } it { expect(described_class).to have_graphql_field(:iteration) } it { expect(described_class).to have_graphql_field(:weight) } it { expect(described_class).to have_graphql_field(:health_status) } diff --git a/ee/spec/models/issue_spec.rb b/ee/spec/models/issue_spec.rb index 71ada60898a91574a333d402240b45f47b07c4b8..226508135944306c62e704a745c3df5310f87a4f 100644 --- a/ee/spec/models/issue_spec.rb +++ b/ee/spec/models/issue_spec.rb @@ -1258,4 +1258,22 @@ end end end + + describe "#has_epic?" do + let(:issue) { build(:issue, epic: epic) } + + subject(:has_epic) { issue.has_epic? } + + context 'when when there is no associated epic' do + let(:epic) { nil } + + it { is_expected.to eq false } + end + + context 'when when there is an associated epic' do + let(:epic) { build(:epic) } + + it { is_expected.to eq true } + end + end end diff --git a/ee/spec/requests/api/graphql/issue/issue_spec.rb b/ee/spec/requests/api/graphql/issue/issue_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..26dc495f1212cc90e4b67d77bbd32cca956e481e --- /dev/null +++ b/ee/spec/requests/api/graphql/issue/issue_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.issue(id)' do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:epic) { create(:epic, :confidential, group: group) } + + let(:issue) { create(:issue, :confidential, project: project, epic: epic) } + let(:current_user) { create(:user) } + let(:issue_params) { { 'id' => global_id_of(issue) } } + let(:issue_data) { graphql_data['issue'] } + let(:issue_fields) { ['hasEpic', 'epic { id }'] } + + let(:query) do + graphql_query_for('issue', issue_params, issue_fields) + end + + before do + stub_licensed_features(epics: true) + end + + context 'when user has no access to the epic' do + before do + project.add_developer(current_user) + end + + context 'when there is an epic' do + it 'returns null for epic and hasEpic is `true`' do + post_graphql(query, current_user: current_user) + + expect(issue_data['hasEpic']).to eq(true) + expect(issue_data['epic']).to be_nil + end + end + + context 'when there is no epic' do + let_it_be(:epic) { nil } + + it 'returns null for epic and hasEpic is `false`' do + post_graphql(query, current_user: current_user) + + expect(issue_data['hasEpic']).to eq(false) + expect(issue_data['epic']).to be_nil + end + end + end + + context 'when user has access to the epic' do + before do + group.add_developer(current_user) + end + + it 'returns epic and hasEpic is `true`' do + post_graphql(query, current_user: current_user) + + expect(issue_data['hasEpic']).to eq(true) + expect(issue_data['epic']).to be_present + end + end +end