Commit 7175610f authored by Nathan Friend's avatar Nathan Friend Committed by Rémy Coutable

Add issue/MR search URLs to GraphQL releases

This commit adds a number of new URLs to the GraphQL Release type that
can be used to jump directly to an issue or MR search page pre-filtered
by the release and the state of the issue or MR.

This commit also deprecates two existing fields ("issuesUrl" and
"mergeRequestsUrl") in favor of the more specific fields added in this
commit.
parent e06b0940
...@@ -12,12 +12,25 @@ module Types ...@@ -12,12 +12,25 @@ module Types
field :self_url, GraphQL::STRING_TYPE, null: true, field :self_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the release' description: 'HTTP URL of the release'
field :merge_requests_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the merge request page filtered by this release'
field :issues_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the issues page filtered by this release'
field :edit_url, GraphQL::STRING_TYPE, null: true, field :edit_url, GraphQL::STRING_TYPE, null: true,
description: "HTTP URL of the release's edit page", description: "HTTP URL of the release's edit page",
authorize: :update_release authorize: :update_release
field :open_merge_requests_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the merge request page, filtered by this release and `state=open`'
field :merged_merge_requests_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`'
field :closed_merge_requests_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`'
field :open_issues_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=open`'
field :closed_issues_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`'
field :merge_requests_url, GraphQL::STRING_TYPE, null: true, method: :open_merge_requests_url,
description: 'HTTP URL of the merge request page filtered by this release',
deprecated: { reason: 'Use `open_merge_requests_url`', milestone: '13.6' }
field :issues_url, GraphQL::STRING_TYPE, null: true, method: :open_issues_url,
description: 'HTTP URL of the issues page filtered by this release',
deprecated: { reason: 'Use `open_issues_url`', milestone: '13.6' }
end end
end end
...@@ -23,18 +23,36 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated ...@@ -23,18 +23,36 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
project_release_url(project, release) project_release_url(project, release)
end end
def merge_requests_url def open_merge_requests_url
return unless release_mr_issue_urls_available? return unless release_mr_issue_urls_available?
project_merge_requests_url(project, params_for_issues_and_mrs) project_merge_requests_url(project, params_for_issues_and_mrs)
end end
def issues_url def merged_merge_requests_url
return unless release_mr_issue_urls_available?
project_merge_requests_url(project, params_for_issues_and_mrs(state: 'merged'))
end
def closed_merge_requests_url
return unless release_mr_issue_urls_available?
project_merge_requests_url(project, params_for_issues_and_mrs(state: 'closed'))
end
def open_issues_url
return unless release_mr_issue_urls_available? return unless release_mr_issue_urls_available?
project_issues_url(project, params_for_issues_and_mrs) project_issues_url(project, params_for_issues_and_mrs)
end end
def closed_issues_url
return unless release_mr_issue_urls_available?
project_issues_url(project, params_for_issues_and_mrs(state: 'closed'))
end
def edit_url def edit_url
return unless release_edit_page_available? return unless release_edit_page_available?
...@@ -59,8 +77,8 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated ...@@ -59,8 +77,8 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
can?(current_user, :download_code, project) can?(current_user, :download_code, project)
end end
def params_for_issues_and_mrs def params_for_issues_and_mrs(state: 'opened')
{ scope: 'all', state: 'opened', release_tag: release.tag } { scope: 'all', state: state, release_tag: release.tag }
end end
def release_mr_issue_urls_available? def release_mr_issue_urls_available?
......
---
title: Add links to GraphQL release object for searching related issues and merge
requests
merge_request: 46161
author:
type: added
...@@ -16663,20 +16663,45 @@ type ReleaseEvidenceEdge { ...@@ -16663,20 +16663,45 @@ type ReleaseEvidenceEdge {
} }
type ReleaseLinks { type ReleaseLinks {
"""
HTTP URL of the issues page, filtered by this release and `state=closed`
"""
closedIssuesUrl: String
"""
HTTP URL of the merge request page , filtered by this release and `state=closed`
"""
closedMergeRequestsUrl: String
""" """
HTTP URL of the release's edit page HTTP URL of the release's edit page
""" """
editUrl: String editUrl: String
""" """
HTTP URL of the issues page filtered by this release HTTP URL of the issues page filtered by this release. Deprecated in 13.6: Use `open_issues_url`
"""
issuesUrl: String @deprecated(reason: "Use `open_issues_url`. Deprecated in 13.6")
"""
HTTP URL of the merge request page filtered by this release. Deprecated in 13.6: Use `open_merge_requests_url`
"""
mergeRequestsUrl: String @deprecated(reason: "Use `open_merge_requests_url`. Deprecated in 13.6")
"""
HTTP URL of the merge request page , filtered by this release and `state=merged`
"""
mergedMergeRequestsUrl: String
"""
HTTP URL of the issues page, filtered by this release and `state=open`
""" """
issuesUrl: String openIssuesUrl: String
""" """
HTTP URL of the merge request page filtered by this release HTTP URL of the merge request page, filtered by this release and `state=open`
""" """
mergeRequestsUrl: String openMergeRequestsUrl: String
""" """
HTTP URL of the release HTTP URL of the release
......
...@@ -47979,6 +47979,34 @@ ...@@ -47979,6 +47979,34 @@
"name": "ReleaseLinks", "name": "ReleaseLinks",
"description": null, "description": null,
"fields": [ "fields": [
{
"name": "closedIssuesUrl",
"description": "HTTP URL of the issues page, filtered by this release and `state=closed`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "closedMergeRequestsUrl",
"description": "HTTP URL of the merge request page , filtered by this release and `state=closed`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "editUrl", "name": "editUrl",
"description": "HTTP URL of the release's edit page", "description": "HTTP URL of the release's edit page",
...@@ -47995,7 +48023,35 @@ ...@@ -47995,7 +48023,35 @@
}, },
{ {
"name": "issuesUrl", "name": "issuesUrl",
"description": "HTTP URL of the issues page filtered by this release", "description": "HTTP URL of the issues page filtered by this release. Deprecated in 13.6: Use `open_issues_url`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": true,
"deprecationReason": "Use `open_issues_url`. Deprecated in 13.6"
},
{
"name": "mergeRequestsUrl",
"description": "HTTP URL of the merge request page filtered by this release. Deprecated in 13.6: Use `open_merge_requests_url`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": true,
"deprecationReason": "Use `open_merge_requests_url`. Deprecated in 13.6"
},
{
"name": "mergedMergeRequestsUrl",
"description": "HTTP URL of the merge request page , filtered by this release and `state=merged`",
"args": [ "args": [
], ],
...@@ -48008,8 +48064,22 @@ ...@@ -48008,8 +48064,22 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "mergeRequestsUrl", "name": "openIssuesUrl",
"description": "HTTP URL of the merge request page filtered by this release", "description": "HTTP URL of the issues page, filtered by this release and `state=open`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "openMergeRequestsUrl",
"description": "HTTP URL of the merge request page, filtered by this release and `state=open`",
"args": [ "args": [
], ],
...@@ -2202,9 +2202,14 @@ Evidence for a release. ...@@ -2202,9 +2202,14 @@ Evidence for a release.
| Field | Type | Description | | Field | Type | Description |
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `closedIssuesUrl` | String | HTTP URL of the issues page, filtered by this release and `state=closed` |
| `closedMergeRequestsUrl` | String | HTTP URL of the merge request page , filtered by this release and `state=closed` |
| `editUrl` | String | HTTP URL of the release's edit page | | `editUrl` | String | HTTP URL of the release's edit page |
| `issuesUrl` | String | HTTP URL of the issues page filtered by this release | | `issuesUrl` **{warning-solid}** | String | **Deprecated:** Use `open_issues_url`. Deprecated in 13.6 |
| `mergeRequestsUrl` | String | HTTP URL of the merge request page filtered by this release | | `mergeRequestsUrl` **{warning-solid}** | String | **Deprecated:** Use `open_merge_requests_url`. Deprecated in 13.6 |
| `mergedMergeRequestsUrl` | String | HTTP URL of the merge request page , filtered by this release and `state=merged` |
| `openIssuesUrl` | String | HTTP URL of the issues page, filtered by this release and `state=open` |
| `openMergeRequestsUrl` | String | HTTP URL of the merge request page, filtered by this release and `state=open` |
| `selfUrl` | String | HTTP URL of the release | | `selfUrl` | String | HTTP URL of the release |
### ReleaseSource ### ReleaseSource
......
...@@ -30,8 +30,8 @@ module API ...@@ -30,8 +30,8 @@ module API
expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_download_code? } expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_download_code? }
expose :_links do expose :_links do
expose :self_url, as: :self, expose_nil: false expose :self_url, as: :self, expose_nil: false
expose :merge_requests_url, expose_nil: false expose :open_merge_requests_url, as: :merge_requests_url, expose_nil: false
expose :issues_url, expose_nil: false expose :open_issues_url, as: :issues_url, expose_nil: false
expose :edit_url, expose_nil: false expose :edit_url, expose_nil: false
end end
......
...@@ -8,9 +8,14 @@ RSpec.describe GitlabSchema.types['ReleaseLinks'] do ...@@ -8,9 +8,14 @@ RSpec.describe GitlabSchema.types['ReleaseLinks'] do
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ expected_fields = %w[
selfUrl selfUrl
openMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openIssuesUrl
closedIssuesUrl
editUrl
mergeRequestsUrl mergeRequestsUrl
issuesUrl issuesUrl
editUrl
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
...@@ -12,6 +12,11 @@ RSpec.describe ReleasePresenter do ...@@ -12,6 +12,11 @@ RSpec.describe ReleasePresenter do
let(:release) { create(:release, project: project) } let(:release) { create(:release, project: project) }
let(:presenter) { described_class.new(release, current_user: user) } let(:presenter) { described_class.new(release, current_user: user) }
let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
let(:opened_url_params) { { state: 'opened', **base_url_params } }
let(:merged_url_params) { { state: 'merged', **base_url_params } }
let(:closed_url_params) { { state: 'closed', **base_url_params } }
before do before do
project.add_developer(developer) project.add_developer(developer)
project.add_guest(guest) project.add_guest(guest)
...@@ -55,15 +60,63 @@ RSpec.describe ReleasePresenter do ...@@ -55,15 +60,63 @@ RSpec.describe ReleasePresenter do
subject { presenter.self_url } subject { presenter.self_url }
it 'returns its own url' do it 'returns its own url' do
is_expected.to match /#{project_release_url(project, release)}/ is_expected.to eq(project_release_url(project, release))
end
end
describe '#open_merge_requests_url' do
subject { presenter.open_merge_requests_url }
it 'returns merge requests url with state=open' do
is_expected.to eq(project_merge_requests_url(project, opened_url_params))
end
context 'when release_mr_issue_urls feature flag is disabled' do
before do
stub_feature_flags(release_mr_issue_urls: false)
end
it { is_expected.to be_nil }
end
end
describe '#merged_merge_requests_url' do
subject { presenter.merged_merge_requests_url }
it 'returns merge requests url with state=merged' do
is_expected.to eq(project_merge_requests_url(project, merged_url_params))
end
context 'when release_mr_issue_urls feature flag is disabled' do
before do
stub_feature_flags(release_mr_issue_urls: false)
end
it { is_expected.to be_nil }
end
end
describe '#closed_merge_requests_url' do
subject { presenter.closed_merge_requests_url }
it 'returns merge requests url with state=closed' do
is_expected.to eq(project_merge_requests_url(project, closed_url_params))
end
context 'when release_mr_issue_urls feature flag is disabled' do
before do
stub_feature_flags(release_mr_issue_urls: false)
end
it { is_expected.to be_nil }
end end
end end
describe '#merge_requests_url' do describe '#open_issues_url' do
subject { presenter.merge_requests_url } subject { presenter.open_issues_url }
it 'returns merge requests url' do it 'returns issues url with state=open' do
is_expected.to match /#{project_merge_requests_url(project)}/ is_expected.to eq(project_issues_url(project, opened_url_params))
end end
context 'when release_mr_issue_urls feature flag is disabled' do context 'when release_mr_issue_urls feature flag is disabled' do
...@@ -75,11 +128,11 @@ RSpec.describe ReleasePresenter do ...@@ -75,11 +128,11 @@ RSpec.describe ReleasePresenter do
end end
end end
describe '#issues_url' do describe '#closed_issues_url' do
subject { presenter.issues_url } subject { presenter.closed_issues_url }
it 'returns merge requests url' do it 'returns issues url with state=closed' do
is_expected.to match /#{project_issues_url(project)}/ is_expected.to eq(project_issues_url(project, closed_url_params))
end end
context 'when release_mr_issue_urls feature flag is disabled' do context 'when release_mr_issue_urls feature flag is disabled' do
...@@ -95,7 +148,7 @@ RSpec.describe ReleasePresenter do ...@@ -95,7 +148,7 @@ RSpec.describe ReleasePresenter do
subject { presenter.edit_url } subject { presenter.edit_url }
it 'returns release edit url' do it 'returns release edit url' do
is_expected.to match /#{edit_project_release_url(project, release)}/ is_expected.to eq(edit_project_release_url(project, release))
end end
context 'when a user is not allowed to update a release' do context 'when a user is not allowed to update a release' do
......
...@@ -13,7 +13,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do ...@@ -13,7 +13,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let_it_be(:link_filepath) { '/direct/asset/link/path' } let_it_be(:link_filepath) { '/direct/asset/link/path' }
let_it_be(:released_at) { Time.now - 1.day } let_it_be(:released_at) { Time.now - 1.day }
let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } } let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
let(:opened_url_params) { { state: 'opened', **base_url_params } }
let(:merged_url_params) { { state: 'merged', **base_url_params } }
let(:closed_url_params) { { state: 'closed', **base_url_params } }
let(:post_query) { post_graphql(query, current_user: current_user) } let(:post_query) { post_graphql(query, current_user: current_user) }
let(:path_prefix) { %w[project release] } let(:path_prefix) { %w[project release] }
let(:data) { graphql_data.dig(*path) } let(:data) { graphql_data.dig(*path) }
...@@ -180,6 +184,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do ...@@ -180,6 +184,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:release_fields) do let(:release_fields) do
query_graphql_field(:links, nil, %{ query_graphql_field(:links, nil, %{
selfUrl selfUrl
openMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openIssuesUrl
closedIssuesUrl
mergeRequestsUrl mergeRequestsUrl
issuesUrl issuesUrl
}) })
...@@ -190,8 +199,13 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do ...@@ -190,8 +199,13 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
expect(data).to eq( expect(data).to eq(
'selfUrl' => project_release_url(project, release), 'selfUrl' => project_release_url(project, release),
'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs), 'openMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs) 'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
'openIssuesUrl' => project_issues_url(project, opened_url_params),
'closedIssuesUrl' => project_issues_url(project, closed_url_params),
'mergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
'issuesUrl' => project_issues_url(project, opened_url_params)
) )
end end
end end
......
...@@ -10,6 +10,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do ...@@ -10,6 +10,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do
let_it_be(:reporter) { create(:user) } let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) } let_it_be(:developer) { create(:user) }
let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
let(:opened_url_params) { { state: 'opened', **base_url_params } }
let(:merged_url_params) { { state: 'merged', **base_url_params } }
let(:closed_url_params) { { state: 'closed', **base_url_params } }
let(:query) do let(:query) do
graphql_query_for(:project, { fullPath: project.full_path }, graphql_query_for(:project, { fullPath: project.full_path },
%{ %{
...@@ -37,6 +42,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do ...@@ -37,6 +42,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do
} }
links { links {
selfUrl selfUrl
openMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openIssuesUrl
closedIssuesUrl
mergeRequestsUrl mergeRequestsUrl
issuesUrl issuesUrl
} }
...@@ -101,8 +111,13 @@ RSpec.describe 'Query.project(fullPath).releases()' do ...@@ -101,8 +111,13 @@ RSpec.describe 'Query.project(fullPath).releases()' do
}, },
'links' => { 'links' => {
'selfUrl' => project_release_url(project, release), 'selfUrl' => project_release_url(project, release),
'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs), 'openMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs) 'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
'openIssuesUrl' => project_issues_url(project, opened_url_params),
'closedIssuesUrl' => project_issues_url(project, closed_url_params),
'mergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
'issuesUrl' => project_issues_url(project, opened_url_params)
} }
) )
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