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
This commit is contained in:
Sean McGivern 2018-04-06 09:20:01 +00:00
commit ca330f7ea3
24 changed files with 75 additions and 55 deletions

View File

@ -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({
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;
}) })

View File

@ -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({
/> />
<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">

View File

@ -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: {

View File

@ -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',
}); });

View File

@ -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 {
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 {
} }
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);
} }
} }

View File

@ -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] },

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
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

View File

@ -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

View File

@ -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

View File

@ -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" }

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Show issues of subgroups in group-level issue board
merge_request:
author:
type: changed

View File

@ -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.

View File

@ -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

View File

@ -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" }

View File

@ -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({

View File

@ -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
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

View File

@ -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) }