Commit a3e82755 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'issue_40915' into 'master'

Allow assigning and filtering issuables by ancestor group labels

Closes #40915

See merge request gitlab-org/gitlab-ce!17873
parents aff9bf11 ad7148d9
...@@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager { ...@@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager {
this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page; this.page = page;
this.groupsOnly = isGroup; this.groupsOnly = isGroup;
this.groupAncestor = isGroupAncestor; this.includeAncestorGroups = isGroupAncestor;
this.isGroupDecendent = isGroupDecendent; this.includeDescendantGroups = isGroupDecendent;
this.setupMapping(); this.setupMapping();
...@@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager { ...@@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager {
} }
getLabelsEndpoint() { getLabelsEndpoint() {
const endpoint = `${this.baseEndpoint}/labels.json`; let endpoint = `${this.baseEndpoint}/labels.json?`;
if (this.groupsOnly) {
endpoint = `${endpoint}only_group_labels=true&`;
}
if (this.includeAncestorGroups) {
endpoint = `${endpoint}include_ancestor_groups=true&`;
}
if (this.includeDescendantGroups) {
endpoint = `${endpoint}include_descendant_groups=true`;
}
return endpoint; return endpoint;
} }
......
...@@ -21,7 +21,7 @@ export default class FilteredSearchManager { ...@@ -21,7 +21,7 @@ export default class FilteredSearchManager {
constructor({ constructor({
page, page,
isGroup = false, isGroup = false,
isGroupAncestor = false, isGroupAncestor = true,
isGroupDecendent = false, isGroupDecendent = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys, filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters', stateFiltersSelector = '.issues-state-filters',
...@@ -86,6 +86,7 @@ export default class FilteredSearchManager { ...@@ -86,6 +86,7 @@ export default class FilteredSearchManager {
page: this.page, page: this.page,
isGroup: this.isGroup, isGroup: this.isGroup,
isGroupAncestor: this.isGroupAncestor, isGroupAncestor: this.isGroupAncestor,
isGroupDecendent: this.isGroupDecendent,
filteredSearchTokenKeys: this.filteredSearchTokenKeys, filteredSearchTokenKeys: this.filteredSearchTokenKeys,
}); });
......
...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants'; ...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({ initFilteredSearch({
page: FILTERED_SEARCH.ISSUES, page: FILTERED_SEARCH.ISSUES,
isGroupDecendent: true,
}); });
projectSelect(); projectSelect();
}); });
...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants'; ...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({ initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS, page: FILTERED_SEARCH.MERGE_REQUESTS,
isGroupDecendent: true,
}); });
projectSelect(); projectSelect();
}); });
...@@ -150,7 +150,8 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -150,7 +150,8 @@ class Projects::LabelsController < Projects::ApplicationController
end end
def find_labels def find_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute @available_labels ||=
LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups]).execute
end end
def authorize_admin_labels! def authorize_admin_labels!
......
...@@ -28,9 +28,10 @@ class LabelsFinder < UnionFinder ...@@ -28,9 +28,10 @@ class LabelsFinder < UnionFinder
if project if project
if project.group.present? if project.group.present?
labels_table = Label.arel_table labels_table = Label.arel_table
group_ids = group_ids_for(project.group)
label_ids << Label.where( label_ids << Label.where(
labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or( labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].in(group_ids)).or(
labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id)) labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
) )
) )
...@@ -38,11 +39,14 @@ class LabelsFinder < UnionFinder ...@@ -38,11 +39,14 @@ class LabelsFinder < UnionFinder
label_ids << project.labels label_ids << project.labels
end end
end end
elsif only_group_labels?
label_ids << Label.where(group_id: group_ids)
else else
if group?
group = Group.find(params[:group_id])
label_ids << Label.where(group_id: group_ids_for(group))
end
label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id)) label_ids << Label.where(project_id: projects.select(:id)) unless only_group_labels?
end end
label_ids label_ids
...@@ -59,22 +63,33 @@ class LabelsFinder < UnionFinder ...@@ -59,22 +63,33 @@ class LabelsFinder < UnionFinder
items.where(title: title) items.where(title: title)
end end
def group_ids # Gets redacted array of group ids
# which can include the ancestors and descendants of the requested group.
def group_ids_for(group)
strong_memoize(:group_ids) do strong_memoize(:group_ids) do
groups_user_can_read_labels(groups_to_include).map(&:id) groups = groups_to_include(group)
groups_user_can_read_labels(groups).map(&:id)
end end
end end
def groups_to_include def groups_to_include(group)
group = Group.find(params[:group_id])
groups = [group] groups = [group]
groups += group.ancestors if params[:include_ancestor_groups].present? groups += group.ancestors if include_ancestor_groups?
groups += group.descendants if params[:include_descendant_groups].present? groups += group.descendants if include_descendant_groups?
groups groups
end end
def include_ancestor_groups?
params[:include_ancestor_groups]
end
def include_descendant_groups?
params[:include_descendant_groups]
end
def group? def group?
params[:group_id].present? params[:group_id].present?
end end
......
...@@ -53,10 +53,12 @@ module BoardsHelper ...@@ -53,10 +53,12 @@ module BoardsHelper
end end
def board_list_data def board_list_data
include_descendant_groups = @group&.present?
{ {
toggle: "dropdown", toggle: "dropdown",
list_labels_path: labels_filter_path(true), list_labels_path: labels_filter_path(true, include_ancestor_groups: true),
labels: labels_filter_path(true), labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups),
labels_endpoint: @labels_endpoint, labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path, namespace_path: @namespace_path,
project_path: @project&.path, project_path: @project&.path,
......
...@@ -129,13 +129,17 @@ module LabelsHelper ...@@ -129,13 +129,17 @@ module LabelsHelper
end end
end end
def labels_filter_path(only_group_labels = false) def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false)
project = @target_project || @project project = @target_project || @project
options = {}
options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups
options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups
if project if project
project_labels_path(project, :json) project_labels_path(project, :json, options)
elsif @group elsif @group
options = { only_group_labels: only_group_labels } if only_group_labels options[:only_group_labels] = only_group_labels if only_group_labels
group_labels_path(@group, :json, options) group_labels_path(@group, :json, options)
else else
dashboard_labels_path(:json) dashboard_labels_path(:json)
......
...@@ -12,11 +12,15 @@ module Boards ...@@ -12,11 +12,15 @@ module Boards
private private
def available_labels_for(board) def available_labels_for(board)
options = { include_ancestor_groups: true }
if board.group_board? if board.group_board?
parent.labels options.merge!(group_id: parent.id, only_group_labels: true)
else else
LabelsFinder.new(current_user, project_id: parent.id).execute options[:project_id] = parent.id
end end
LabelsFinder.new(current_user, options).execute
end end
def next_position(board) def next_position(board)
......
...@@ -106,7 +106,7 @@ class IssuableBaseService < BaseService ...@@ -106,7 +106,7 @@ class IssuableBaseService < BaseService
end end
def available_labels def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: true).execute
end end
def handle_quick_actions_on_create(issuable) def handle_quick_actions_on_create(issuable)
......
...@@ -21,7 +21,8 @@ module Projects ...@@ -21,7 +21,8 @@ module Projects
end end
def labels(target = nil) def labels(target = nil)
labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title]) labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true)
.execute.select([:color, :title])
return labels unless target&.respond_to?(:labels) return labels unless target&.respond_to?(:labels)
......
...@@ -200,7 +200,7 @@ module QuickActions ...@@ -200,7 +200,7 @@ module QuickActions
end end
params '~label1 ~"label 2"' params '~label1 ~"label 2"'
condition do condition do
available_labels = LabelsFinder.new(current_user, project_id: project.id).execute available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute
current_user.can?(:"admin_#{issuable.to_ability_name}", project) && current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any? available_labels.any?
...@@ -562,7 +562,7 @@ module QuickActions ...@@ -562,7 +562,7 @@ module QuickActions
def find_labels(labels_param) def find_labels(labels_param)
extract_references(labels_param, :label) | extract_references(labels_param, :label) |
LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split, include_ancestor_groups: true).execute
end end
def find_label_references(labels_param) def find_label_references(labels_param)
...@@ -593,6 +593,7 @@ module QuickActions ...@@ -593,6 +593,7 @@ module QuickActions
def extract_references(arg, type) def extract_references(arg, type)
ext = Gitlab::ReferenceExtractor.new(project, current_user) ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(arg, author: current_user) ext.analyze(arg, author: current_user)
ext.references(type) ext.references(type)
......
...@@ -107,7 +107,7 @@ ...@@ -107,7 +107,7 @@
- selected_labels.each do |label| - selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (project_labels_path(@project, :json) if @project) } } %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project) } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels") = multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true') = icon('chevron-down', 'aria-hidden': 'true')
......
---
title: Allow assigning and filtering issuables by ancestor group labels
merge_request:
author:
type: added
...@@ -9,7 +9,7 @@ Labels allow you to categorize issues or merge requests using descriptive titles ...@@ -9,7 +9,7 @@ Labels allow you to categorize issues or merge requests using descriptive titles
In GitLab, you can create project and group labels: In GitLab, you can create project and group labels:
- **Project labels** can be assigned to issues or merge requests in that project only. - **Project labels** can be assigned to issues or merge requests in that project only.
- **Group labels** can be assigned to any issue or merge request of any project in that group. - **Group labels** can be assigned to any issue or merge request of any project in that group or subgroup.
- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/40915), you will be able to assign group labels to issues and merge reqeusts of projects in [subgroups](../group/subgroups/index.md). - In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/40915), you will be able to assign group labels to issues and merge reqeusts of projects in [subgroups](../group/subgroups/index.md).
## Creating labels ## Creating labels
...@@ -74,9 +74,9 @@ Every issue and merge request can be assigned any number of labels. The labels a ...@@ -74,9 +74,9 @@ Every issue and merge request can be assigned any number of labels. The labels a
### Filtering in list pages ### Filtering in list pages
From the project issue list page and the project merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group labels and project labels. From the project issue list page and the project merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group (including subgroup ancestors) labels and project labels.
From the group issue list page and the group merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group labels and project labels. From the group issue list page and the group merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group labels (including subgroup ancestors and subgroup descendants) and project labels.
![Labels group issues](img/labels_group_issues.png) ![Labels group issues](img/labels_group_issues.png)
......
...@@ -83,11 +83,12 @@ module API ...@@ -83,11 +83,12 @@ module API
end end
def available_labels_for(label_parent) def available_labels_for(label_parent)
search_params = search_params = { include_ancestor_groups: true }
if label_parent.is_a?(Project) if label_parent.is_a?(Project)
{ project_id: label_parent.id } search_params[:project_id] = label_parent.id
else else
{ group_id: label_parent.id, only_group_labels: true } search_params.merge!(group_id: label_parent.id, only_group_labels: true)
end end
LabelsFinder.new(current_user, search_params).execute LabelsFinder.new(current_user, search_params).execute
......
...@@ -41,7 +41,7 @@ module Banzai ...@@ -41,7 +41,7 @@ module Banzai
end end
def find_labels(project) def find_labels(project)
LabelsFinder.new(nil, project_id: project.id).execute(skip_authorization: true) LabelsFinder.new(nil, project_id: project.id, include_ancestor_groups: true).execute(skip_authorization: true)
end end
# Parameters to pass to `Label.find_by` based on the given arguments # Parameters to pass to `Label.find_by` based on the given arguments
......
...@@ -22,15 +22,6 @@ describe 'Filter issues', :js do ...@@ -22,15 +22,6 @@ describe 'Filter issues', :js do
end end
end end
def expect_issues_list_count(open_count, closed_count = 0)
all_count = open_count + closed_count
expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: open_count)
end
end
before do before do
project.add_master(user) project.add_master(user)
......
require 'spec_helper'
feature 'Labels Hierarchy', :js, :nested_groups do
include FilteredSearchHelpers
let!(:user) { create(:user) }
let!(:grandparent) { create(:group) }
let!(:parent) { create(:group, parent: grandparent) }
let!(:child) { create(:group, parent: parent) }
let!(:project_1) { create(:project, namespace: parent) }
let!(:grandparent_group_label) { create(:group_label, group: grandparent, title: 'Label_1') }
let!(:parent_group_label) { create(:group_label, group: parent, title: 'Label_2') }
let!(:child_group_label) { create(:group_label, group: child, title: 'Label_3') }
let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') }
before do
grandparent.add_owner(user)
sign_in(user)
end
shared_examples 'assigning labels from sidebar' do
it 'can assign all ancestors labels' do
[grandparent_group_label, parent_group_label, project_label_1].each do |label|
page.within('.block.labels') do
find('.edit-link').click
end
wait_for_requests
find('a.label-item', text: label.title).click
find('.dropdown-menu-close-icon').click
wait_for_requests
expect(page).to have_selector('span.label', text: label.title)
end
end
it 'does not find child group labels on dropdown' do
page.within('.block.labels') do
find('.edit-link').click
end
wait_for_requests
expect(page).not_to have_selector('span.label', text: child_group_label.title)
end
end
shared_examples 'filtering by ancestor labels for projects' do |board = false|
it 'filters by ancestor labels' do
[grandparent_group_label, parent_group_label, project_label_1].each do |label|
select_label_on_dropdown(label.title)
wait_for_requests
if board
expect(page).to have_selector('.card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue.title)
end
else
expect_issues_list_count(1)
expect(page).to have_selector('span.issue-title-text', text: labeled_issue.title)
end
end
end
it 'does not filter by descendant group labels' do
filtered_search.set("label:")
wait_for_requests
expect(page).not_to have_selector('.btn-link', text: child_group_label.title)
end
end
shared_examples 'filtering by ancestor labels for groups' do |board = false|
let(:project_2) { create(:project, namespace: parent) }
let!(:project_label_2) { create(:label, project: project_2, title: 'Label_4') }
let(:project_3) { create(:project, namespace: child) }
let!(:group_label_3) { create(:group_label, group: child, title: 'Label_5') }
let!(:project_label_3) { create(:label, project: project_3, title: 'Label_6') }
let!(:labeled_issue_2) { create(:labeled_issue, project: project_2, labels: [grandparent_group_label, parent_group_label, project_label_2]) }
let!(:labeled_issue_3) { create(:labeled_issue, project: project_3, labels: [grandparent_group_label, parent_group_label, group_label_3]) }
let!(:issue_2) { create(:issue, project: project_2) }
it 'filters by ancestors and current group labels' do
[grandparent_group_label, parent_group_label].each do |label|
select_label_on_dropdown(label.title)
wait_for_requests
if board
expect(page).to have_selector('.card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue.title)
end
expect(page).to have_selector('.card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue_2.title)
end
else
expect_issues_list_count(3)
expect(page).to have_selector('span.issue-title-text', text: labeled_issue.title)
expect(page).to have_selector('span.issue-title-text', text: labeled_issue_2.title)
expect(page).to have_selector('span.issue-title-text', text: labeled_issue_3.title)
end
end
end
it 'filters by descendant group labels' do
wait_for_requests
if board
pending("Waiting for https://gitlab.com/gitlab-org/gitlab-ce/issues/44270")
select_label_on_dropdown(group_label_3.title)
expect(page).to have_selector('.card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue_3.title)
end
else
select_label_on_dropdown(group_label_3.title)
expect_issues_list_count(1)
expect(page).to have_selector('span.issue-title-text', text: labeled_issue_3.title)
end
end
it 'does not filter by descendant group project labels' do
filtered_search.set("label:")
wait_for_requests
expect(page).not_to have_selector('.btn-link', text: project_label_3.title)
end
end
context 'when creating new issuable' do
before do
visit new_project_issue_path(project_1)
end
it 'should be able to assign ancestor group labels' do
fill_in 'issue_title', with: 'new created issue'
fill_in 'issue_description', with: 'new issue description'
find(".js-label-select").click
wait_for_requests
find('a.label-item', text: grandparent_group_label.title).click
find('a.label-item', text: parent_group_label.title).click
find('a.label-item', text: project_label_1.title).click
find('.btn-create').click
expect(page.find('.issue-details h2.title')).to have_content('new created issue')
expect(page).to have_selector('span.label', text: grandparent_group_label.title)
expect(page).to have_selector('span.label', text: parent_group_label.title)
expect(page).to have_selector('span.label', text: project_label_1.title)
end
end
context 'issuable sidebar' do
let!(:issue) { create(:issue, project: project_1) }
context 'on issue sidebar' do
before do
visit project_issue_path(project_1, issue)
end
it_behaves_like 'assigning labels from sidebar'
end
context 'on project board issue sidebar' do
let(:board) { create(:board, project: project_1) }
before do
visit project_board_path(project_1, board)
wait_for_requests
find('.card').click
end
it_behaves_like 'assigning labels from sidebar'
end
context 'on group board issue sidebar' do
let(:board) { create(:board, group: parent) }
before do
visit group_board_path(parent, board)
wait_for_requests
find('.card').click
end
it_behaves_like 'assigning labels from sidebar'
end
end
context 'issuable filtering' do
let!(:labeled_issue) { create(:labeled_issue, project: project_1, labels: [grandparent_group_label, parent_group_label, project_label_1]) }
let!(:issue) { create(:issue, project: project_1) }
context 'on project issuable list' do
before do
visit project_issues_path(project_1)
end
it_behaves_like 'filtering by ancestor labels for projects'
it 'does not filter by descendant group labels' do
filtered_search.set("label:")
wait_for_requests
expect(page).not_to have_selector('.btn-link', text: child_group_label.title)
end
end
context 'on group issuable list' do
before do
visit issues_group_path(parent)
end
it_behaves_like 'filtering by ancestor labels for groups'
end
context 'on project boards filter' do
let(:board) { create(:board, project: project_1) }
before do
visit project_board_path(project_1, board)
end
it_behaves_like 'filtering by ancestor labels for projects', true
end
context 'on group boards filter' do
let(:board) { create(:board, group: parent) }
before do
visit group_board_path(parent, board)
end
it_behaves_like 'filtering by ancestor labels for groups', true
end
end
context 'creating boards lists' do
context 'on project boards' do
let(:board) { create(:board, project: project_1) }
before do
visit project_board_path(project_1, board)
find('.js-new-board-list').click
wait_for_requests
end
it 'creates lists from all ancestor labels' do
[grandparent_group_label, parent_group_label, project_label_1].each do |label|
find('a', text: label.title).click
end
wait_for_requests
expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title)
expect(page).to have_selector('.board-title-text', text: parent_group_label.title)
expect(page).to have_selector('.board-title-text', text: project_label_1.title)
end
end
context 'on group boards' do
let(:board) { create(:board, group: parent) }
before do
visit group_board_path(parent, board)
find('.js-new-board-list').click
wait_for_requests
end
it 'creates lists from all ancestor group labels' do
[grandparent_group_label, parent_group_label].each do |label|
find('a', text: label.title).click
end
wait_for_requests
expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title)
expect(page).to have_selector('.board-title-text', text: parent_group_label.title)
end
it 'does not create lists from descendant groups' do
expect(page).not_to have_selector('a', text: child_group_label.title)
end
end
end
end
...@@ -71,6 +71,24 @@ describe LabelsFinder do ...@@ -71,6 +71,24 @@ describe LabelsFinder do
end end
end end
context 'when group has no projects' do
let(:empty_group) { create(:group) }
let!(:empty_group_label_1) { create(:group_label, group: empty_group, title: 'Label 1 (empty group)') }
let!(:empty_group_label_2) { create(:group_label, group: empty_group, title: 'Label 2 (empty group)') }
before do
empty_group.add_developer(user)
end
context 'when only group labels is false' do
it 'returns group labels' do
finder = described_class.new(user, group_id: empty_group.id)
expect(finder.execute).to eq [empty_group_label_1, empty_group_label_2]
end
end
end
context 'when including labels from group ancestors', :nested_groups do context 'when including labels from group ancestors', :nested_groups do
it 'returns labels from group and its ancestors' do it 'returns labels from group and its ancestors' do
private_group_1.add_developer(user) private_group_1.add_developer(user)
...@@ -110,7 +128,21 @@ describe LabelsFinder do ...@@ -110,7 +128,21 @@ describe LabelsFinder do
end end
end end
context 'filtering by project_id' do context 'filtering by project_id', :nested_groups do
context 'when include_ancestor_groups is true' do
let!(:sub_project) { create(:project, namespace: private_subgroup_1 ) }
let!(:project_label) { create(:label, project: sub_project, title: 'Label 5') }
let(:finder) { described_class.new(user, project_id: sub_project.id, include_ancestor_groups: true) }
before do
private_group_1.add_developer(user)
end
it 'returns all ancestor labels' do
expect(finder.execute).to match_array([private_subgroup_label_1, private_group_label_1, project_label])
end
end
it 'returns labels available for the project' do it 'returns labels available for the project' do
finder = described_class.new(user, project_id: project_1.id) finder = described_class.new(user, project_id: project_1.id)
......
...@@ -48,5 +48,36 @@ describe API::Boards do ...@@ -48,5 +48,36 @@ describe API::Boards do
expect(json_response['label']['name']).to eq(group_label.title) expect(json_response['label']['name']).to eq(group_label.title)
expect(json_response['position']).to eq(3) expect(json_response['position']).to eq(3)
end end
it 'creates a new board list for ancestor group labels' do
group = create(:group)
sub_group = create(:group, parent: group)
group_label = create(:group_label, group: group)
board_parent.update(group: sub_group)
group.add_developer(user)
sub_group.add_developer(user)
post api(url, user), label_id: group_label.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['label']['name']).to eq(group_label.title)
end
end
describe "POST /groups/:id/boards/lists", :nested_groups do
set(:group) { create(:group) }
set(:board_parent) { create(:group, parent: group ) }
let(:url) { "/groups/#{board_parent.id}/boards/#{board.id}/lists" }
set(:board) { create(:board, group: board_parent) }
it 'creates a new board list for ancestor group labels' do
group.add_developer(user)
group_label = create(:group_label, group: group)
post api(url, user), label_id: group_label.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['label']['name']).to eq(group_label.title)
end
end end
end end
...@@ -21,6 +21,29 @@ module FilteredSearchHelpers ...@@ -21,6 +21,29 @@ module FilteredSearchHelpers
end end
end end
# Select a label clicking in the search dropdown instead
# of entering label names on the input.
def select_label_on_dropdown(label_title)
input_filtered_search("label:", submit: false)
within('#js-dropdown-label') do
wait_for_requests
find('li', text: label_title).click
end
filtered_search.send_keys(:enter)
end
def expect_issues_list_count(open_count, closed_count = 0)
all_count = open_count + closed_count
expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: open_count)
end
end
# Enables input to be added character by character # Enables input to be added character by character
def input_filtered_search_keys(search_term) def input_filtered_search_keys(search_term)
# Add an extra space to engage visual tokens # Add an extra space to engage visual tokens
......
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