Commit bda18c88 authored by Mario de la Ossa's avatar Mario de la Ossa

GraphQL Iterations - add scopedPath and scopedUrl

These two new fields are meant to be used inside a project query in
order to return a path scoped to the project that looks like
/path/to/project/-/iterations/inherited/iteration_id
parent 2e6e20a9
......@@ -7806,6 +7806,16 @@ type Iteration {
"""
iid: ID!
"""
Web path of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts
"""
scopedPath: String
"""
Web URL of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts
"""
scopedUrl: String
"""
Timestamp of the iteration start date
"""
......
......@@ -21511,6 +21511,34 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scopedPath",
"description": "Web path of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scopedUrl",
"description": "Web URL of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "startDate",
"description": "Timestamp of the iteration start date",
......@@ -1191,6 +1191,8 @@ Represents an iteration object.
| `dueDate` | Time | Timestamp of the iteration due date |
| `id` | ID! | ID of the iteration |
| `iid` | ID! | Internal ID of the iteration |
| `scopedPath` | String | Web path of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts |
| `scopedUrl` | String | Web URL of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts |
| `startDate` | Time | Timestamp of the iteration start date |
| `state` | IterationState! | State of the iteration |
| `title` | String! | Title of the iteration |
......
# frozen_string_literal: true
class Projects::Iterations::InheritedController < Projects::ApplicationController
before_action :check_iterations_available!
before_action :authorize_show_iteration!
def show; end
private
def check_iterations_available!
render_404 unless project.feature_available?(:iterations)
end
def authorize_show_iteration!
render_404 unless can?(current_user, :read_iteration, project)
end
end
......@@ -32,6 +32,9 @@ module Resolvers
iterations = IterationsFinder.new(context[:current_user], iterations_finder_params(args)).execute
# Necessary for scopedPath computation in IterationPresenter
context[:parent_object] = parent
Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(iterations)
end
......
......@@ -31,6 +31,12 @@ module Types
field :web_url, GraphQL::STRING_TYPE, null: false, method: :iteration_url,
description: 'Web URL of the iteration'
field :scoped_path, GraphQL::STRING_TYPE, null: true, method: :scoped_iteration_path, extras: [:parent],
description: 'Web path of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts'
field :scoped_url, GraphQL::STRING_TYPE, null: true, method: :scoped_iteration_url, extras: [:parent],
description: 'Web URL of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts'
field :due_date, Types::TimeType, null: true,
description: 'Timestamp of the iteration due date'
......
......@@ -19,5 +19,17 @@ module EE
project_url(iteration.project, *args)
end
end
def inherited_iteration_path(project, iteration, *args)
return unless iteration.group_timebox?
project_iterations_inherited_path(project, iteration.id, *args)
end
def inherited_iteration_url(project, iteration, *args)
return unless iteration.group_timebox?
project_iterations_inherited_url(project, iteration.id, *args)
end
end
end
......@@ -10,4 +10,16 @@ class IterationPresenter < Gitlab::View::Presenter::Delegated
def iteration_url
url_builder.build(iteration)
end
def scoped_iteration_path(parent:)
return unless parent[:parent_object]&.is_a?(Project)
url_builder.inherited_iteration_path(parent[:parent_object], iteration)
end
def scoped_iteration_url(parent:)
return unless parent[:parent_object]&.is_a?(Project)
url_builder.inherited_iteration_url(parent[:parent_object], iteration)
end
end
- add_to_breadcrumbs _("Iterations"), project_iterations_path(@project)
- breadcrumb_title params[:id]
- page_title _("Iteration")
- if Feature.enabled?(:project_iterations, @project.group)
.js-iteration{ data: { full_path: @project.group.full_path,
can_edit: can?(current_user, :admin_iteration, @project).to_s,
iteration_id: params[:id],
preview_markdown_path: preview_markdown_path(@project) } }
---
title: Expose scopedPath and scopedUrl to Iteration type in GraphQL
merge_request: 39543
author:
type: added
......@@ -108,6 +108,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
resources :iterations, only: [:index, :show], constraints: { id: /\d+/ }
namespace :iterations do
resources :inherited, only: [:show], constraints: { id: /\d+/ }
end
end
# End of the /-/ scope.
......
......@@ -44,4 +44,94 @@ RSpec.describe 'Querying an Iteration' do
expect(graphql_errors).to include(a_hash_including('message' => "Field 'iteration' is missing required arguments: id"))
end
end
describe 'scoped path' do
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:project_iteration) { create(:iteration, :skip_project_validation, project: project) }
shared_examples 'scoped path' do
let(:iteration_nodes) do
nodes = <<~NODES
nodes {
scopedPath
scopedUrl
}
NODES
query_graphql_field('iterations', { id: queried_iteration.id }, nodes)
end
before_all do
group.add_guest(current_user)
end
specify do
expect(subject).to include('scopedPath' => expected_scope_path, 'scopedUrl' => expected_scope_url)
end
end
context 'inside a project context' do
subject { graphql_data['project']['iterations']['nodes'].first }
let(:query) do
graphql_query_for('project', { full_path: project.full_path }, iteration_nodes)
end
describe 'group-owned iteration' do
it_behaves_like 'scoped path' do
let(:queried_iteration) { iteration }
let(:expected_scope_path) { project_iterations_inherited_path(project, iteration.id) }
let(:expected_scope_url) { /#{expected_scope_path}$/ }
end
end
describe 'project-owned iteration' do
it_behaves_like 'scoped path' do
let(:queried_iteration) { project_iteration }
let(:expected_scope_path) { nil }
let(:expected_scope_url) { nil }
end
end
end
context 'inside a group context' do
subject { graphql_data['group']['iterations']['nodes'].first }
let(:query) do
graphql_query_for('group', { full_path: group.full_path }, iteration_nodes)
end
describe 'group-owned iteration' do
it_behaves_like 'scoped path' do
let(:queried_iteration) { iteration }
let(:expected_scope_path) { nil }
let(:expected_scope_url) { nil }
end
end
end
context 'root context' do
subject { graphql_data['iteration'] }
let(:query) do
graphql_query_for('iteration', { id: iteration.to_global_id.to_s }, [:scoped_path, :scoped_url])
end
describe 'group-owned iteration' do
it_behaves_like 'scoped path' do
let(:queried_iteration) { iteration }
let(:expected_scope_path) { nil }
let(:expected_scope_url) { nil }
end
end
describe 'project-owned iteration' do
it_behaves_like 'scoped path' do
let(:queried_iteration) { project_iteration }
let(:expected_scope_path) { nil }
let(:expected_scope_url) { nil }
end
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