Commit ca330f7e authored by Sean McGivern's avatar Sean McGivern

Merge branch 'issue_44270' into 'master'

Show issues of subgroups in group-level issue board

Closes #44270

See merge request gitlab-org/gitlab-ce!18187
parents 20e9b32c 696daa7e
...@@ -60,10 +60,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -60,10 +60,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
this.issue = this.detail.issue; this.issue = this.detail.issue;
this.list = this.detail.list; this.list = this.detail.list;
this.$nextTick(() => {
this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate;
});
}, },
deep: true deep: true
}, },
...@@ -91,7 +87,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -91,7 +87,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
saveAssignees () { saveAssignees () {
this.loadingAssignees = true; this.loadingAssignees = true;
gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint) gl.issueBoards.BoardsStore.detail.issue.update()
.then(() => { .then(() => {
this.loadingAssignees = false; this.loadingAssignees = false;
}) })
......
...@@ -68,15 +68,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -68,15 +68,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return this.issue.assignees.length > this.numberOverLimit; return this.issue.assignees.length > this.numberOverLimit;
}, },
cardUrl() {
let baseUrl = this.issueLinkBase;
if (this.groupId && this.issue.project) {
baseUrl = this.issueLinkBase.replace(':project_path', this.issue.project.path);
}
return `${baseUrl}/${this.issue.iid}`;
},
issueId() { issueId() {
if (this.issue.iid) { if (this.issue.iid) {
return `#${this.issue.iid}`; return `#${this.issue.iid}`;
...@@ -153,13 +144,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -153,13 +144,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
/> />
<a <a
class="js-no-trigger" class="js-no-trigger"
:href="cardUrl" :href="issue.path"
:title="issue.title">{{ issue.title }}</a> :title="issue.title">{{ issue.title }}</a>
<span <span
class="card-number" class="card-number"
v-if="issueId" v-if="issueId"
> >
<template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }} {{ issue.referencePath }}
</span> </span>
</h4> </h4>
<div class="card-assignee"> <div class="card-assignee">
......
...@@ -17,14 +17,10 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ ...@@ -17,14 +17,10 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
type: Object, type: Object,
required: true, required: true,
}, },
issueUpdate: {
type: String,
required: true,
},
}, },
computed: { computed: {
updateUrl() { updateUrl() {
return this.issueUpdate.replace(':project_path', this.issue.project.path); return this.issue.path;
}, },
}, },
methods: { methods: {
......
...@@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { ...@@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) { constructor(store, updateUrl = false, cantEdit = []) {
super({ super({
page: 'boards', page: 'boards',
isGroupDecendent: true,
stateFiltersSelector: '.issues-state-filters', stateFiltersSelector: '.issues-state-filters',
}); });
......
...@@ -23,6 +23,8 @@ class ListIssue { ...@@ -23,6 +23,8 @@ class ListIssue {
}; };
this.isLoading = {}; this.isLoading = {};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.referencePath = obj.reference_path;
this.path = obj.real_path;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id; this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id; this.project_id = obj.project_id;
...@@ -98,7 +100,7 @@ class ListIssue { ...@@ -98,7 +100,7 @@ class ListIssue {
this.isLoading[key] = value; this.isLoading[key] = value;
} }
update (url) { update () {
const data = { const data = {
issue: { issue: {
milestone_id: this.milestone ? this.milestone.id : null, milestone_id: this.milestone ? this.milestone.id : null,
...@@ -113,7 +115,7 @@ class ListIssue { ...@@ -113,7 +115,7 @@ class ListIssue {
} }
const projectPath = this.project ? this.project.path : ''; const projectPath = this.project ? this.project.path : '';
return Vue.http.patch(url.replace(':project_path', projectPath), data); return Vue.http.patch(`${this.path}.json`, data);
} }
} }
......
...@@ -96,7 +96,8 @@ module Boards ...@@ -96,7 +96,8 @@ module Boards
resource.as_json( resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true, labels: true,
sidebar_endpoints: true, issue_endpoints: true,
include_full_project_path: board.group_board?,
include: { include: {
project: { only: [:id, :path] }, project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
......
...@@ -12,7 +12,7 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -12,7 +12,7 @@ class Groups::MilestonesController < Groups::ApplicationController
@milestones = Kaminari.paginate_array(milestones).page(params[:page]) @milestones = Kaminari.paginate_array(milestones).page(params[:page])
end end
format.json do format.json do
render json: milestones.map { |m| m.for_display.slice(:title, :name) } render json: milestones.map { |m| m.for_display.slice(:id, :title, :name) }
end end
end end
end end
......
...@@ -272,11 +272,17 @@ class Issue < ActiveRecord::Base ...@@ -272,11 +272,17 @@ class Issue < ActiveRecord::Base
def as_json(options = {}) def as_json(options = {})
super(options).tap do |json| super(options).tap do |json|
if options.key?(:sidebar_endpoints) && project if options.key?(:issue_endpoints) && project
url_helper = Gitlab::Routing.url_helpers url_helper = Gitlab::Routing.url_helpers
json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'), issue_reference = options[:include_full_project_path] ? to_reference(full: true) : to_reference
toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self))
json.merge!(
reference_path: issue_reference,
real_path: url_helper.project_issue_path(project, self),
issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self)
)
end end
if options.key?(:labels) if options.key?(:labels)
......
...@@ -34,8 +34,8 @@ class Milestone < ActiveRecord::Base ...@@ -34,8 +34,8 @@ class Milestone < ActiveRecord::Base
scope :for_projects_and_groups, -> (project_ids, group_ids) do scope :for_projects_and_groups, -> (project_ids, group_ids) do
conditions = [] conditions = []
conditions << arel_table[:project_id].in(project_ids) if project_ids.compact.any? conditions << arel_table[:project_id].in(project_ids) if project_ids&.compact&.any?
conditions << arel_table[:group_id].in(group_ids) if group_ids.compact.any? conditions << arel_table[:group_id].in(group_ids) if group_ids&.compact&.any?
where(conditions.reduce(:or)) where(conditions.reduce(:or))
end end
......
...@@ -35,6 +35,7 @@ module Boards ...@@ -35,6 +35,7 @@ module Boards
def filter_params def filter_params
set_parent set_parent
set_state set_state
set_scope
params params
end end
...@@ -51,6 +52,10 @@ module Boards ...@@ -51,6 +52,10 @@ module Boards
params[:state] = list && list.closed? ? 'closed' : 'opened' params[:state] = list && list.closed? ? 'closed' : 'opened'
end end
def set_scope
params[:include_subgroups] = board.group_board?
end
def board_label_ids def board_label_ids
@board_label_ids ||= board.lists.movable.pluck(:label_id) @board_label_ids ||= board.lists.movable.pluck(:label_id)
end end
......
...@@ -51,9 +51,10 @@ class IssuableBaseService < BaseService ...@@ -51,9 +51,10 @@ class IssuableBaseService < BaseService
return unless milestone_id return unless milestone_id
params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE
group_ids = project.group&.self_and_ancestors&.pluck(:id)
milestone = milestone =
Milestone.for_projects_and_groups([project.id], [project.group&.id]).find_by_id(milestone_id) Milestone.for_projects_and_groups([project.id], group_ids).find_by_id(milestone_id)
params[:milestone_id] = '' unless milestone params[:milestone_id] = '' unless milestone
end end
......
...@@ -90,7 +90,7 @@ module Issues ...@@ -90,7 +90,7 @@ module Issues
issue = issue =
if board_group_id if board_group_id
IssuesFinder.new(current_user, group_id: board_group_id).find_by(id: id) IssuesFinder.new(current_user, group_id: board_group_id, include_subgroups: true).find_by(id: id)
else else
project.issues.find(id) project.issues.find(id)
end end
......
...@@ -22,6 +22,6 @@ ...@@ -22,6 +22,6 @@
= render "shared/boards/components/sidebar/labels" = render "shared/boards/components/sidebar/labels"
= render "shared/boards/components/sidebar/notifications" = render "shared/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue", %remove-btn{ ":issue" => "issue",
":issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'", ":issue-update" => "issue.sidebarInfoEndpoint",
":list" => "list", ":list" => "list",
"v-if" => "canRemove" } "v-if" => "canRemove" }
...@@ -21,8 +21,7 @@ ...@@ -21,8 +21,7 @@
.dropdown .dropdown
- dropdown_options = issue_assignees_dropdown_options - dropdown_options = issue_assignees_dropdown_options
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data, %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data,
":data-issuable-id" => "issue.iid", ":data-issuable-id" => "issue.iid" }
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
= dropdown_options[:title] = dropdown_options[:title]
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
......
...@@ -22,8 +22,7 @@ ...@@ -22,8 +22,7 @@
":value" => "issue.dueDate" } ":value" => "issue.dueDate" }
.dropdown .dropdown
%button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button', %button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" }, data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" } }
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
%span.dropdown-toggle-text Due date %span.dropdown-toggle-text Due date
= icon('chevron-down') = icon('chevron-down')
.dropdown-menu.dropdown-menu-due-date .dropdown-menu.dropdown-menu-due-date
......
...@@ -26,8 +26,7 @@ ...@@ -26,8 +26,7 @@
project_id: @project&.try(:id), project_id: @project&.try(:id),
labels: labels_filter_path(false), labels: labels_filter_path(false),
namespace_path: @namespace_path, namespace_path: @namespace_path,
project_path: @project.try(:path) }, project_path: @project.try(:path) } }
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
%span.dropdown-toggle-text %span.dropdown-toggle-text
Label Label
= icon('chevron-down') = icon('chevron-down')
......
...@@ -18,8 +18,7 @@ ...@@ -18,8 +18,7 @@
.dropdown .dropdown
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" }, %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
":data-selected" => "milestoneTitle", ":data-selected" => "milestoneTitle",
":data-issuable-id" => "issue.iid", ":data-issuable-id" => "issue.iid" }
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
Milestone Milestone
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-selectable
......
---
title: Show issues of subgroups in group-level issue board
merge_request:
author:
type: changed
...@@ -240,8 +240,7 @@ Issue Board, that is create/delete lists and drag issues around. ...@@ -240,8 +240,7 @@ Issue Board, that is create/delete lists and drag issues around.
>Introduced in GitLab 10.6 >Introduced in GitLab 10.6
Group issue board is analogous to project-level issue board and it is accessible at the group Group issue board is analogous to project-level issue board and it is accessible at the group
navigation level. A group-level issue board allows you to view all issues from all projects in that group navigation level. A group-level issue board allows you to view all issues from all projects in that group or descendant subgroups. Similarly, you can only filter by group labels for these
(currently, it does not see issues from projects in subgroups). Similarly, you can only filter by group labels for these
boards. When updating milestones and labels for an issue through the sidebar update mechanism, again only boards. When updating milestones and labels for an issue through the sidebar update mechanism, again only
group-level objects are available. group-level objects are available.
......
...@@ -115,17 +115,17 @@ feature 'Labels Hierarchy', :js, :nested_groups do ...@@ -115,17 +115,17 @@ feature 'Labels Hierarchy', :js, :nested_groups do
it 'filters by descendant group labels' do it 'filters by descendant group labels' do
wait_for_requests wait_for_requests
if board select_label_on_dropdown(group_label_3.title)
pending("Waiting for https://gitlab.com/gitlab-org/gitlab-ce/issues/44270")
select_label_on_dropdown(group_label_3.title) if board
expect(page).to have_selector('.card-title') do |card|
expect(card).not_to have_selector('a', text: labeled_issue_2.title)
end
expect(page).to have_selector('.card-title') do |card| expect(page).to have_selector('.card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue_3.title) expect(card).to have_selector('a', text: labeled_issue_3.title)
end end
else else
select_label_on_dropdown(group_label_3.title)
expect_issues_list_count(1) expect_issues_list_count(1)
expect(page).to have_selector('span.issue-title-text', text: labeled_issue_3.title) expect(page).to have_selector('span.issue-title-text', text: labeled_issue_3.title)
end end
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
"relative_position": { "type": "integer" }, "relative_position": { "type": "integer" },
"issue_sidebar_endpoint": { "type": "string" }, "issue_sidebar_endpoint": { "type": "string" },
"toggle_subscription_endpoint": { "type": "string" }, "toggle_subscription_endpoint": { "type": "string" },
"reference_path": { "type": "string" },
"real_path": { "type": "string" },
"project": { "project": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"path": { "type": "string" } "path": { "type": "string" }
......
...@@ -41,6 +41,8 @@ describe('Issue card component', () => { ...@@ -41,6 +41,8 @@ describe('Issue card component', () => {
confidential: false, confidential: false,
labels: [list.label], labels: [list.label],
assignees: [], assignees: [],
reference_path: '#1',
real_path: '/test/1',
}); });
component = new Vue({ component = new Vue({
......
...@@ -48,10 +48,8 @@ describe Boards::Issues::ListService do ...@@ -48,10 +48,8 @@ describe Boards::Issues::ListService do
context 'when parent is a group' do context 'when parent is a group' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :empty_repo, namespace: group) } let(:project) { create(:project, :empty_repo, namespace: group) }
let(:project1) { create(:project, :empty_repo, namespace: group) } let(:project1) { create(:project, :empty_repo, namespace: group) }
let(:board) { create(:board, group: group) }
let(:m1) { create(:milestone, group: group) } let(:m1) { create(:milestone, group: group) }
let(:m2) { create(:milestone, group: group) } let(:m2) { create(:milestone, group: group) }
...@@ -92,13 +90,30 @@ describe Boards::Issues::ListService do ...@@ -92,13 +90,30 @@ describe Boards::Issues::ListService do
let!(:closed_issue4) { create(:labeled_issue, :closed, project: project1, labels: [p1, p1_project1]) } let!(:closed_issue4) { create(:labeled_issue, :closed, project: project1, labels: [p1, p1_project1]) }
let!(:closed_issue5) { create(:labeled_issue, :closed, project: project1, labels: [development]) } let!(:closed_issue5) { create(:labeled_issue, :closed, project: project1, labels: [development]) }
let(:parent) { group }
before do before do
group.add_developer(user) group.add_developer(user)
end end
it_behaves_like 'issues list service' context 'and group has no parent' do
let(:parent) { group }
let(:group) { create(:group) }
let(:board) { create(:board, group: group) }
it_behaves_like 'issues list service'
end
context 'and group is an ancestor', :nested_groups do
let(:parent) { create(:group) }
let(:group) { create(:group, parent: parent) }
let!(:backlog) { create(:backlog_list, board: board) }
let(:board) { create(:board, group: parent) }
before do
parent.add_developer(user)
end
it_behaves_like 'issues list service'
end
end end
end end
end end
...@@ -97,11 +97,13 @@ describe Issues::UpdateService, :mailer do ...@@ -97,11 +97,13 @@ describe Issues::UpdateService, :mailer do
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
end end
context 'when moving issue between issues from different projects' do context 'when moving issue between issues from different projects', :nested_groups do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
let(:project_1) { create(:project, namespace: group) } let(:project_1) { create(:project, namespace: group) }
let(:project_2) { create(:project, namespace: group) } let(:project_2) { create(:project, namespace: group) }
let(:project_3) { create(:project, namespace: group) } let(:project_3) { create(:project, namespace: subgroup) }
let(:issue_1) { create(:issue, project: project_1) } let(:issue_1) { create(:issue, project: project_1) }
let(:issue_2) { create(:issue, project: project_2) } let(:issue_2) { create(:issue, project: project_2) }
......
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