Commit 8eab7b08 authored by Jarka Kadlecova's avatar Jarka Kadlecova

Prepare backend for assigning issues to epics

parent 50298bcc
......@@ -228,6 +228,7 @@ module IssuablesHelper
if parent.is_a?(Group)
data[:groupPath] = parent.path
data[:issueLinksEndpoint] = group_epic_issues_path(parent, issuable)
else
data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path)
end
......
......@@ -38,6 +38,9 @@ class Issue < ActiveRecord::Base
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
has_one :epic_issue
has_one :epic, through: :epic_issue
validates :project, presence: true
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
......
......@@ -78,6 +78,8 @@ constraints(GroupUrlConstrainer.new) do
member do
get :realtime_changes
end
resources :epic_issues, only: [:index, :create, :destroy], as: 'issues', path: 'issues'
end
legacy_ee_group_boards_redirect = redirect do |params, request|
......
class CreateEpicIssuesTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :epic_issues do |t|
t.references :epic, null: false, index: true, foreign_key: true
t.references :issue, null: false, index: { unique: true }, foreign_key: true
t.timestamps_with_timezone
end
end
def down
drop_table :epic_issues
end
end
......@@ -732,6 +732,16 @@ ActiveRecord::Schema.define(version: 20171107144726) do
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree
add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
create_table "epic_issues", force: :cascade do |t|
t.integer "epic_id", null: false
t.integer "issue_id", null: false
t.datetime_with_timezone "created_at"
t.datetime_with_timezone "updated_at"
end
add_index "epic_issues", ["epic_id"], name: "index_epic_issues_on_epic_id", using: :btree
add_index "epic_issues", ["issue_id"], name: "index_epic_issues_on_issue_id", unique: true, using: :btree
create_table "epic_metrics", force: :cascade do |t|
t.integer "epic_id", null: false
t.datetime_with_timezone "created_at", null: false
......@@ -2393,6 +2403,8 @@ ActiveRecord::Schema.define(version: 20171107144726) do
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
add_foreign_key "epic_issues", "epics"
add_foreign_key "epic_issues", "issues"
add_foreign_key "epic_metrics", "epics", on_delete: :cascade
add_foreign_key "epics", "milestones", on_delete: :nullify
add_foreign_key "epics", "namespaces", column: "group_id", name: "fk_f081aa4489", on_delete: :cascade
......
module IssuableLinks
def index
render json: issues
end
def create
result = create_service.execute
render json: { message: result[:message], issues: issues }, status: result[:http_status]
end
def destroy
result = destroy_service.execute
render json: { issues: issues }, status: result[:http_status]
end
private
def create_params
params.slice(:issue_references)
end
def create_service
raise NotImplementedError
end
def destroy_service
raise NotImplementedError
end
end
class Groups::EpicIssuesController < Groups::EpicsController
include IssuableLinks
skip_before_action :authorize_destroy_issuable!
before_action :authorize_admin_epic!, only: [:create, :destroy]
private
def create_service
EpicIssues::CreateService.new(epic, current_user, create_params)
end
def destroy_service
epic_issue = EpicIssue.find(params[:id])
EpicIssues::DestroyService.new(epic_issue, current_user)
end
def issues
EpicIssues::ListService.new(epic, current_user).execute
end
def authorize_admin_epic!
render_403 unless can?(current_user, :admin_epic, epic)
end
end
......@@ -9,7 +9,7 @@ class Groups::EpicsController < Groups::ApplicationController
private
def epic
@issuable = @epic ||= @group.epics.find_by(iid: params[:id])
@issuable = @epic ||= @group.epics.find_by(iid: params[:epic_id] || params[:id])
return render_404 unless can?(current_user, :read_epic, @epic)
......
module Projects
class IssueLinksController < Projects::ApplicationController
include IssuableLinks
before_action :authorize_admin_issue_link!, only: [:create, :destroy]
def index
render json: issues
end
def create
create_params = params.slice(:issue_references)
result = IssueLinks::CreateService.new(issue, current_user, create_params).execute
render json: { message: result[:message], issues: issues }, status: result[:http_status]
end
def destroy
issue_link = IssueLink.find(params[:id])
result = IssueLinks::DestroyService.new(issue_link, current_user).execute
render json: { issues: issues }, status: result[:http_status]
end
private
def issues
......@@ -36,5 +19,14 @@ module Projects
.execute
.find_by!(iid: params[:issue_id])
end
def create_service
IssueLinks::CreateService.new(issue, current_user, create_params)
end
def destroy_service
issue_link = IssueLink.find(params[:id])
IssueLinks::DestroyService.new(issue_link, current_user)
end
end
end
......@@ -10,6 +10,8 @@ module EE
belongs_to :assignee, class_name: "User"
belongs_to :group
has_many :epic_issues
validates :group, presence: true
end
......@@ -24,5 +26,13 @@ module EE
def supports_weight?
false
end
def issues(current_user)
related_issues = ::Issue.select('issues.*, epic_issues.id as epic_issue_id')
.joins(:epic_issue)
.where("epic_issues.epic_id = #{id}")
Ability.issues_readable_by_user(related_issues, current_user)
end
end
end
class EpicIssue < ActiveRecord::Base
validates :epic, :issue, presence: true
validates :issue, uniqueness: true
belongs_to :epic
belongs_to :issue
end
......@@ -55,6 +55,7 @@ module EE
enable :admin_board
enable :read_deploy_board
enable :admin_issue_link
enable :admin_epic_issue
end
rule { can?(:developer_access) }.enable :admin_board
......
module EpicIssues
class CreateService < IssuableLinks::CreateService
private
def relate_issues(referenced_issue)
link = EpicIssue.find_or_initialize_by(issue: referenced_issue)
link.epic = issuable
link.save
end
def create_notes?
false
end
def extractor_context
{ group: issuable.group }
end
def linkable_issues(issues)
issues.select { |issue| can?(current_user, :admin_epic_issue, issue) && issue.project.group == issuable.group }
end
end
end
module EpicIssues
class DestroyService < IssuableLinks::DestroyService
private
def create_notes?
false
end
def source
@source ||= link.epic
end
def target
@target ||= link.issue
end
def permission_to_remove_relation?
can?(current_user, :admin_epic_issue, target) && can?(current_user, :admin_epic, source)
end
end
end
module EpicIssues
class ListService < IssuableLinks::ListService
private
def issues
issuable.issues(current_user)
end
def destroy_relation_path(issue)
if can_destroy_issue_link?(issue)
group_epic_issue_path(issuable.group, issuable.iid, issue.epic_issue_id)
end
end
def can_destroy_issue_link?(issue)
Ability.allowed?(current_user, :admin_issue_link, issue) && Ability.allowed?(current_user, :admin_epic, issuable)
end
def reference(issue)
issue.to_reference(full: true)
end
end
end
module IssueLinks
module IssuableLinks
class CreateService < BaseService
def initialize(issue, user, params)
@issue, @current_user, @params = issue, user, params.dup
attr_reader :issuable, :current_user, :params
def initialize(issuable, user, params)
@issuable, @current_user, @params = issuable, user, params.dup
end
def execute
......@@ -17,20 +19,10 @@ module IssueLinks
def create_issue_links
referenced_issues.each do |referenced_issue|
create_notes(referenced_issue) if relate_issues(referenced_issue)
create_notes(referenced_issue) if relate_issues(referenced_issue) && create_notes?
end
end
# Returns a Boolean indicating if the Issue was related.
def relate_issues(referenced_issue)
IssueLink.new(source: @issue, target: referenced_issue).save
end
def create_notes(referenced_issue)
SystemNoteService.relate_issue(@issue, referenced_issue, current_user)
SystemNoteService.relate_issue(referenced_issue, @issue, current_user)
end
def referenced_issues
@referenced_issues ||= begin
target_issue = params[:target_issue]
......@@ -43,7 +35,7 @@ module IssueLinks
[]
end
issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
linkable_issues(issues)
end
end
......@@ -51,10 +43,31 @@ module IssueLinks
issue_references = params[:issue_references]
text = issue_references.join(' ')
extractor = Gitlab::ReferenceExtractor.new(@issue.project, @current_user)
extractor.analyze(text)
extractor = Gitlab::ReferenceExtractor.new(issuable.project, @current_user)
extractor.analyze(text, extractor_context)
extractor.issues
end
def create_notes(referenced_issue)
SystemNoteService.relate_issue(issuable, referenced_issue, current_user)
SystemNoteService.relate_issue(referenced_issue, issuable, current_user)
end
def extractor_context
{}
end
def create_notes?
true
end
def linkable_issues(issues)
raise NotImplementedError
end
def relate_issues(referenced_issue)
raise NotImplementedError
end
end
end
module IssueLinks
module IssuableLinks
class DestroyService < BaseService
def initialize(issue_link, user)
@issue_link = issue_link
attr_reader :link, :current_user
def initialize(link, user)
@link = link
@current_user = user
@issue = issue_link.source
@referenced_issue = issue_link.target
end
def execute
return error('No Issue Link found', 404) unless permission_to_remove_relation?
remove_relation
create_notes
create_notes if create_notes?
success(message: 'Relation was removed')
end
private
def remove_relation
@issue_link.destroy!
def create_notes
SystemNoteService.unrelate_issue(source, target, current_user)
SystemNoteService.unrelate_issue(target, source, current_user)
end
def create_notes
SystemNoteService.unrelate_issue(@issue, @referenced_issue, current_user)
SystemNoteService.unrelate_issue(@referenced_issue, @issue, current_user)
def remove_relation
link.destroy!
end
def permission_to_remove_relation?
can?(current_user, :admin_issue_link, @issue) &&
can?(current_user, :admin_issue_link, @referenced_issue)
def create_notes?
true
end
end
end
module IssueLinks
module IssuableLinks
class ListService
include Gitlab::Routing
def initialize(issue, user)
@issue, @current_user, @project = issue, user, issue.project
attr_reader :issuable, :current_user
def initialize(issuable, user)
@issuable, @current_user = issuable, user
end
def execute
......@@ -12,7 +14,7 @@ module IssueLinks
id: referenced_issue.id,
title: referenced_issue.title,
state: referenced_issue.state,
reference: referenced_issue.to_reference(@project),
reference: reference(referenced_issue),
path: project_issue_path(referenced_issue.project, referenced_issue.iid),
destroy_relation_path: destroy_relation_path(referenced_issue)
}
......@@ -21,26 +23,12 @@ module IssueLinks
private
def issues
@issue.related_issues(@current_user, preload: { project: :namespace })
end
def destroy_relation_path(issue)
# Make sure the user can admin both the current issue AND the
# referenced issue projects in order to return the removal link.
if can_destroy_issue_link_on_current_project? && can_destroy_issue_link?(issue.project)
project_issue_link_path(@project, @issue.iid, issue.issue_link_id)
end
end
def can_destroy_issue_link_on_current_project?
return @can_destroy_on_current_project if defined?(@can_destroy_on_current_project)
@can_destroy_on_current_project = can_destroy_issue_link?(@project)
raise NotImplementedError
end
def can_destroy_issue_link?(project)
Ability.allowed?(@current_user, :admin_issue_link, project)
def reference(issue)
issue.to_reference(issuable.project)
end
end
end
module IssueLinks
class CreateService < IssuableLinks::CreateService
def relate_issues(referenced_issue)
IssueLink.new(source: issuable, target: referenced_issue).save
end
def linkable_issues(issues)
issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
end
end
end
module IssueLinks
class DestroyService < IssuableLinks::DestroyService
private
def source
@source ||= link.source
end
def target
@target ||= link.target
end
def permission_to_remove_relation?
can?(current_user, :admin_issue_link, source) && can?(current_user, :admin_issue_link, target)
end
end
end
module IssueLinks
class ListService < IssuableLinks::ListService
private
def issues
issuable.related_issues(current_user, preload: { project: :namespace })
end
def destroy_relation_path(issue)
current_project = issuable.project
# Make sure the user can admin both the current issue AND the
# referenced issue projects in order to return the removal link.
if can_destroy_issue_link_on_current_project?(current_project) && can_destroy_issue_link?(issue.project)
project_issue_link_path(current_project, issuable.iid, issue.issue_link_id)
end
end
def can_destroy_issue_link_on_current_project?(current_project)
return @can_destroy_on_current_project if defined?(@can_destroy_on_current_project)
@can_destroy_on_current_project = can_destroy_issue_link?(current_project)
end
def can_destroy_issue_link?(project)
Ability.allowed?(current_user, :admin_issue_link, project)
end
end
end
......@@ -6,8 +6,7 @@ module Banzai
def nodes_visible_to_user(user, nodes)
issues = issues_for_nodes(nodes)
readable_issues = Ability
.issues_readable_by_user(issues.values, user).to_set
readable_issues = Ability.issues_readable_by_user(issues.values, user).to_set
nodes.select do |node|
readable_issues.include?(issues[node])
......
require 'spec_helper'
describe Groups::EpicIssuesController do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: group) }
let(:epic) { create(:epic, group: group) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
before do
sign_in(user)
end
describe 'GET #index' do
let!(:epic_issues) { create(:epic_issue, epic: epic, issue: issue) }
before do
group.add_developer(user)
get :index, group_id: group, epic_id: epic.to_param
end
it 'returns status 200' do
expect(response.status).to eq(200)
end
it 'returns the correct json' do
expected_result = [
{
'id' => issue.id,
'title' => issue.title,
'state' => issue.state,
'reference' => "#{project.full_path}##{issue.iid}",
'path' => "/#{project.full_path}/issues/#{issue.iid}",
'destroy_relation_path' => "/groups/#{group.full_path}/-/epics/#{epic.iid}/issues/#{epic_issues.id}"
}
]
expect(JSON.parse(response.body)).to eq(expected_result)
end
end
describe 'POST #create' do
subject do
reference = [issue.to_reference(full: true)]
post :create, group_id: group, epic_id: epic.to_param, issue_references: reference
end
context 'when user has permissions to create requested associtaion' do
before do
group.add_developer(user)
end
it 'returns correct response for the correct issue reference' do
subject
list_service_response = EpicIssues::ListService.new(epic, user).execute
expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq('message' => nil, 'issues' => list_service_response.as_json)
end
it 'creates a new EpicIssue record' do
expect { subject }.to change { EpicIssue.count }.from(0).to(1)
end
end
context 'when user does not have permissions to create requested associtaion' do
it 'returns correct response for the correct issue reference' do
subject
expect(response).to have_gitlab_http_status(403)
end
it 'does not create a new EpicIssue record' do
expect { subject }.not_to change { EpicIssue.count }.from(0)
end
end
end
describe 'DELETE #destroy' do
let!(:epic_issue) { create(:epic_issue, epic: epic, issue: issue) }
subject do
delete :destroy, group_id: group, epic_id: epic.to_param, id: epic_issue.id
end
context 'when user has permissions to detele the link' do
before do
group.add_developer(user)
end
it 'returns status 200' do
subject
expect(response.status).to eq(200)
end
it 'destroys the link' do
expect { subject }.to change { EpicIssue.count }.from(1).to(0)
end
end
context 'when user does not have permissions to delete the link' do
it 'returns status 404' do
subject
expect(response.status).to eq(403)
end
it 'does not destroy the link' do
expect { subject }.not_to change { EpicIssue.count }.from(1)
end
end
context 'when the epic_issue record does not exixst' do
it 'returns status 404' do
delete :destroy, group_id: group, epic_id: epic.to_param, id: 9999
expect(response.status).to eq(403)
end
end
end
end
......@@ -5,6 +5,7 @@ describe Epic do
subject { build(:epic) }
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to belong_to(:assignee).class_name('User') }
it { is_expected.to belong_to(:group) }
end
......@@ -21,4 +22,40 @@ describe Epic do
it { is_expected.to include_module(InternalId) }
end
describe '#issues' do
let(:user) { create(:user) }
let(:group) { create(:group, :private) }
let(:project) { create(:project, group: group) }
let(:project2) { create(:project, group: group) }
let!(:epic) { create(:epic, group: group) }
let!(:issue) { create(:issue, project: project)}
let!(:lone_issue) { create(:issue, project: project)}
let!(:other_issue) { create(:issue, project: project2)}
let!(:epic_issues) do
[
create(:epic_issue, epic: epic, issue: issue),
create(:epic_issue, epic: epic, issue: other_issue)
]
end
subject { epic.issues(user) }
it 'returns all issues if a user has access to them' do
group.add_developer(user)
expect(subject.count).to eq(2)
expect(subject.map(&:id)).to match_array([issue.id, other_issue.id])
expect(subject.map(&:epic_issue_id)).to match_array(epic_issues.map(&:id))
end
it 'does not return issues user can not see' do
project.add_developer(user)
expect(subject.count).to eq(1)
expect(subject.map(&:id)).to match_array([issue.id])
expect(subject.map(&:epic_issue_id)).to match_array([epic_issues.first.id])
end
end
end
require 'spec_helper'
describe EpicIssues::CreateService do
describe '#execute' do
let(:group) { create :group }
let(:epic) { create :epic, group: group }
let(:project) { create(:project, group: group) }
let(:issue) { create :issue, project: project }
let(:user) { create :user }
let(:reference) { issue.to_reference(full: true) }
let(:params) do
{}
end
subject { described_class.new(epic, user, params).execute }
context 'when user has permissions to link the issue' do
before do
group.add_developer(user)
end
context 'when the reference list is empty' do
let(:params) do
{ issue_references: [] }
end
it 'returns error' do
is_expected.to eq(message: 'No Issue found for given params', status: :error, http_status: 404)
end
end
context 'when there is an issue to relate' do
context 'when shortcut for Issue is given' do
let(:params) do
{ issue_references: [issue.to_reference] }
end
it 'returns error' do
is_expected.to eq(message: 'No Issue found for given params', status: :error, http_status: 404)
end
it 'no relationship is created' do
expect { subject }.not_to change { EpicIssue.count }
end
end
context 'when a full reference is given' do
let(:params) do
{ issue_references: [reference] }
end
it 'creates relationships' do
expect { subject }.to change(EpicIssue, :count).from(0).to(1)
expect(EpicIssue.find_by!(issue_id: issue.id)).to have_attributes(epic: epic)
end
it 'returns success status' do
is_expected.to eq(status: :success)
end
end
context 'when an issue links is given' do
let(:params) do
{ issue_references: [IssuesHelper.url_for_issue(issue.iid, issue.project)] }
end
it 'creates relationships' do
expect { subject }.to change(EpicIssue, :count).from(0).to(1)
expect(EpicIssue.find_by!(issue_id: issue.id)).to have_attributes(epic: epic)
end
it 'returns success status' do
is_expected.to eq(status: :success)
end
end
end
end
context 'when user does not have permissions to link the issue' do
let(:params) do
{ issue_references: [reference] }
end
it 'returns error' do
is_expected.to eq(message: 'No Issue found for given params', status: :error, http_status: 404)
end
it 'no relationship is created' do
expect { subject }.not_to change { EpicIssue.count }
end
end
context 'when an issue is already assigned to another epic' do
let(:params) do
{ issue_references: [reference] }
end
before do
group.add_developer(user)
create(:epic_issue, epic: epic, issue: issue)
end
let(:another_epic) { create(:epic, group: group) }
subject { described_class.new(another_epic, user, params).execute }
it 'does not create a new association' do
expect { subject }.not_to change(EpicIssue, :count).from(1)
end
it 'updates the existing association' do
expect { subject }.to change { EpicIssue.find_by!(issue_id: issue.id).epic }.from(epic).to(another_epic)
end
it 'returns success status' do
is_expected.to eq(status: :success)
end
end
context 'when issue from non group project is given' do
let(:another_issue) { create :issue }
let(:params) do
{ issue_references: [another_issue.to_reference(full: true)] }
end
before do
group.add_developer(user)
another_issue.project.add_developer(user)
end
it 'returns error' do
is_expected.to eq(message: 'No Issue found for given params', status: :error, http_status: 404)
end
it 'no relationship is created' do
expect { subject }.not_to change { EpicIssue.count }
end
end
end
end
require 'spec_helper'
describe EpicIssues::DestroyService do
describe '#execute' do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, group: group) }
let(:epic) { create(:epic, group: group) }
let(:issue) { create(:issue, project: project) }
let!(:epic_issue) { create(:epic_issue, epic: epic, issue: issue) }
subject { described_class.new(epic_issue, user).execute }
context 'when user has permissions to remove associations' do
before do
group.add_reporter(user)
end
it 'removes related issue' do
expect { subject }.to change { EpicIssue.count }.from(1).to(0)
end
it 'returns success message' do
is_expected.to eq(message: 'Relation was removed', status: :success)
end
end
context 'user does not have permissions to remove associations' do
it 'does not remove relation' do
expect { subject }.not_to change { EpicIssue.count }.from(1)
end
it 'returns error message' do
is_expected.to eq(message: 'No Issue Link found', status: :error, http_status: 404)
end
end
end
end
require 'spec_helper'
describe EpicIssues::ListService do
let(:user) { create :user }
let(:group) { create(:group, :private) }
let(:project) { create(:project_empty_repo, group: group) }
let(:other_project) { create(:project_empty_repo, group: group) }
let(:epic) { create(:epic, group: group) }
let(:issue1) { create :issue, project: project }
let(:issue2) { create :issue, project: project }
let(:issue3) { create :issue, project: other_project }
let!(:epic_issue1) { create(:epic_issue, issue: issue1, epic: epic) }
let!(:epic_issue2) { create(:epic_issue, issue: issue2, epic: epic) }
let!(:epic_issue3) { create(:epic_issue, issue: issue3, epic: epic) }
describe '#execute' do
subject { described_class.new(epic, user).execute }
context 'user can see all issues and destroy their associations' do
before do
group.add_developer(user)
end
it 'returns related issues JSON' do
expected_result = [
{
id: issue1.id,
title: issue1.title,
state: issue1.state,
reference: issue1.to_reference(full: true),
path: "/#{project.full_path}/issues/#{issue1.iid}",
destroy_relation_path: "/groups/#{group.full_path}/-/epics/#{epic.iid}/issues/#{epic_issue1.id}"
},
{
id: issue2.id,
title: issue2.title,
state: issue2.state,
reference: issue2.to_reference(full: true),
path: "/#{project.full_path}/issues/#{issue2.iid}",
destroy_relation_path: "/groups/#{group.full_path}/-/epics/#{epic.iid}/issues/#{epic_issue2.id}"
},
{
id: issue3.id,
title: issue3.title,
state: issue3.state,
reference: issue3.to_reference(full: true),
path: "/#{other_project.full_path}/issues/#{issue3.iid}",
destroy_relation_path: "/groups/#{group.full_path}/-/epics/#{epic.iid}/issues/#{epic_issue3.id}"
}
]
expect(subject).to match_array(expected_result)
end
end
context 'user can see only some issues' do
before do
project.add_developer(user)
end
it 'returns related issues JSON' do
expected_result = [
{
id: issue1.id,
title: issue1.title,
state: issue1.state,
reference: issue1.to_reference(full: true),
path: "/#{project.full_path}/issues/#{issue1.iid}",
destroy_relation_path: nil
},
{
id: issue2.id,
title: issue2.title,
state: issue2.state,
reference: issue2.to_reference(full: true),
path: "/#{project.full_path}/issues/#{issue2.iid}",
destroy_relation_path: nil
}
]
expect(subject).to match_array(expected_result)
end
end
end
end
FactoryGirl.define do
factory :epic_issue do
epic
issue
end
end
......@@ -197,6 +197,7 @@ describe IssuablesHelper do
expected_data = {
'endpoint' => "/groups/#{@group.full_path}/-/epics/#{epic.iid}",
'issueLinksEndpoint' => "/groups/#{@group.full_path}/-/epics/#{epic.iid}/links",
'canUpdate' => true,
'canDestroy' => true,
'issuableRef' => nil,
......
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