Commit 9ea658a6 authored by Alexandru Croitor's avatar Alexandru Croitor Committed by Alexandru Croitor

Extend Project and Group Labels API

Add support for search in Project and Group Labels API, as well as
support to include descendant groups and project labels within
Group Labels API
parent b0961026
---
title: Add support for search and inclusion of project labels within Group Labels API
merge_request: 44415
author:
type: changed
......@@ -26,6 +26,9 @@ GET /groups/:id/labels
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `with_counts` | boolean | no | Whether or not to include issue and merge request counts. Defaults to `false`. _([Introduced in GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/31543))_ |
| `include_ancestor_groups` | boolean | no | Include ancestor groups. Defaults to `true`. |
| `include_descendant_groups` | boolean | no | Include descendant groups. Defaults to `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259024) in GitLab 13.6 |
| `only_group_labels` | boolean | no | Toggle to include only group labels or also project labels. Defaults to `true`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259024) in GitLab 13.6 |
| `search` | string | no | Keyword to filter labels by. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259024) in GitLab 13.6 |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/labels?with_counts=true"
......@@ -75,6 +78,8 @@ GET /groups/:id/labels/:label_id
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `label_id` | integer or string | yes | The ID or title of a group's label. |
| `include_ancestor_groups` | boolean | no | Include ancestor groups. Defaults to `true`. |
| `include_descendant_groups` | boolean | no | Include descendant groups. Defaults to `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259024) in GitLab 13.6 |
| `only_group_labels` | boolean | no | Toggle to include only group labels or also project labels. Defaults to `true`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259024) in GitLab 13.6 |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/labels/bug"
......
......@@ -24,6 +24,7 @@ GET /projects/:id/labels
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `with_counts` | boolean | no | Whether or not to include issue and merge request counts. Defaults to `false`. _([Introduced in GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/31543))_ |
| `include_ancestor_groups` | boolean | no | Include ancestor groups. Defaults to `true`. |
| `search` | string | no | Keyword to filter labels by. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259024) in GitLab 13.6 |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/labels?with_counts=true"
......
......@@ -20,10 +20,16 @@ module API
desc: 'Include issue and merge request counts'
optional :include_ancestor_groups, type: Boolean, default: true,
desc: 'Include ancestor groups'
optional :include_descendant_groups, type: Boolean, default: false,
desc: 'Include descendant groups. This feature was added in GitLab 13.6'
optional :only_group_labels, type: Boolean, default: true,
desc: 'Toggle to include only group labels or also project labels. This feature was added in GitLab 13.6'
optional :search, type: String,
desc: 'Keyword to filter labels by. This feature was added in GitLab 13.6'
use :pagination
end
get ':id/labels' do
get_labels(user_group, Entities::GroupLabel, include_ancestor_groups: params[:include_ancestor_groups])
get_labels(user_group, Entities::GroupLabel, declared_params)
end
desc 'Get a single label' do
......@@ -33,9 +39,13 @@ module API
params do
optional :include_ancestor_groups, type: Boolean, default: true,
desc: 'Include ancestor groups'
optional :include_descendant_groups, type: Boolean, default: false,
desc: 'Include descendant groups. This feature was added in GitLab 13.6'
optional :only_group_labels, type: Boolean, default: true,
desc: 'Toggle to include only group labels or also project labels. This feature was added in GitLab 13.6'
end
get ':id/labels/:name' do
get_label(user_group, Entities::GroupLabel, include_ancestor_groups: params[:include_ancestor_groups])
get_label(user_group, Entities::GroupLabel, declared_params)
end
desc 'Create a new label' do
......
......@@ -89,16 +89,15 @@ module API
@project ||= find_project!(params[:id])
end
def available_labels_for(label_parent, include_ancestor_groups: true)
search_params = { include_ancestor_groups: include_ancestor_groups }
def available_labels_for(label_parent, params = { include_ancestor_groups: true, only_group_labels: true })
if label_parent.is_a?(Project)
search_params[:project_id] = label_parent.id
params.delete(:only_group_labels)
params[:project_id] = label_parent.id
else
search_params.merge!(group_id: label_parent.id, only_group_labels: true)
params[:group_id] = label_parent.id
end
LabelsFinder.new(current_user, search_params).execute
LabelsFinder.new(current_user, params).execute
end
def find_user(id)
......
......@@ -28,23 +28,23 @@ module API
at_least_one_of :new_name, :color, :description
end
def find_label(parent, id_or_title, include_ancestor_groups: true)
labels = available_labels_for(parent, include_ancestor_groups: include_ancestor_groups)
def find_label(parent, id_or_title, params = { include_ancestor_groups: true })
labels = available_labels_for(parent, params)
label = labels.find_by_id(id_or_title) || labels.find_by_title(id_or_title)
label || not_found!('Label')
end
def get_labels(parent, entity, include_ancestor_groups: true)
present paginate(available_labels_for(parent, include_ancestor_groups: include_ancestor_groups)),
def get_labels(parent, entity, params = {})
present paginate(available_labels_for(parent, params)),
with: entity,
current_user: current_user,
parent: parent,
with_counts: params[:with_counts]
end
def get_label(parent, entity, include_ancestor_groups: true)
label = find_label(parent, params_id_or_title, include_ancestor_groups: include_ancestor_groups)
def get_label(parent, entity, params = {})
label = find_label(parent, params_id_or_title, params)
present label, with: entity, current_user: current_user, parent: parent
end
......
......@@ -19,10 +19,12 @@ module API
desc: 'Include issue and merge request counts'
optional :include_ancestor_groups, type: Boolean, default: true,
desc: 'Include ancestor groups'
optional :search, type: String,
desc: 'Keyword to filter labels by. This feature was added in GitLab 13.6'
use :pagination
end
get ':id/labels' do
get_labels(user_project, Entities::ProjectLabel, include_ancestor_groups: params[:include_ancestor_groups])
get_labels(user_project, Entities::ProjectLabel, declared_params)
end
desc 'Get a single label' do
......@@ -34,7 +36,7 @@ module API
desc: 'Include ancestor groups'
end
get ':id/labels/:name' do
get_label(user_project, Entities::ProjectLabel, include_ancestor_groups: params[:include_ancestor_groups])
get_label(user_project, Entities::ProjectLabel, declared_params)
end
desc 'Create a new label' do
......
......@@ -7,60 +7,97 @@ RSpec.describe API::GroupLabels do
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
let!(:group_label1) { create(:group_label, title: 'feature', group: group) }
let!(:group_label1) { create(:group_label, title: 'feature-label', group: group) }
let!(:group_label2) { create(:group_label, title: 'bug', group: group) }
let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
let!(:subgroup_label) { create(:group_label, title: 'support-label', group: subgroup) }
describe 'GET :id/labels' do
it 'returns all available labels for the group' do
get api("/groups/#{group.id}/labels", user)
context 'get current group labels' do
let(:request) { get api("/groups/#{group.id}/labels", user) }
let(:expected_labels) { [group_label1.name, group_label2.name] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(2)
expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug')
end
context 'when the with_counts parameter is set' do
it 'includes counts in the response' do
get api("/groups/#{group.id}/labels", user), params: { with_counts: true }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label_with_counts'))
expect(json_response.size).to eq(2)
expect(json_response.map { |r| r['open_issues_count'] }).to contain_exactly(0, 0)
it_behaves_like 'fetches labels'
context 'when search param is provided' do
let(:request) { get api("/groups/#{group.id}/labels?search=lab", user) }
let(:expected_labels) { [group_label1.name] }
it_behaves_like 'fetches labels'
end
end
end
describe 'GET :subgroup_id/labels' do
context 'when the include_ancestor_groups parameter is not set' do
it 'returns all available labels for the group and ancestor groups' do
get api("/groups/#{subgroup.id}/labels", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(3)
expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug', 'support')
context 'when the with_counts parameter is set' do
it 'includes counts in the response' do
get api("/groups/#{group.id}/labels", user), params: { with_counts: true }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label_with_counts'))
expect(json_response.size).to eq(2)
expect(json_response.map { |r| r['open_issues_count'] }).to contain_exactly(0, 0)
end
end
context 'when include_descendant_groups param is provided' do
let!(:project) { create(:project, group: group) }
let!(:project_label1) { create(:label, title: 'project-label1', project: project, priority: 3) }
let!(:project_label2) { create(:label, title: 'project-bug', project: project) }
let(:request) { get api("/groups/#{group.id}/labels", user), params: { include_descendant_groups: true } }
let(:expected_labels) { [group_label1.name, group_label2.name, subgroup_label.name] }
it_behaves_like 'fetches labels'
context 'when search param is provided' do
let(:request) { get api("/groups/#{group.id}/labels", user), params: { search: 'lab', include_descendant_groups: true } }
let(:expected_labels) { [group_label1.name, subgroup_label.name] }
it_behaves_like 'fetches labels'
end
context 'when only_group_labels param is false' do
let(:request) { get api("/groups/#{group.id}/labels", user), params: { include_descendant_groups: true, only_group_labels: false } }
let(:expected_labels) { [group_label1.name, group_label2.name, subgroup_label.name, project_label1.name, project_label2.name] }
it_behaves_like 'fetches labels'
context 'when search param is provided' do
let(:request) { get api("/groups/#{group.id}/labels", user), params: { search: 'lab', include_descendant_groups: true, only_group_labels: false } }
let(:expected_labels) { [group_label1.name, subgroup_label.name, project_label1.name] }
it_behaves_like 'fetches labels'
end
end
end
end
context 'when the include_ancestor_groups parameter is set to false' do
it 'returns all available labels for the group but not for ancestor groups' do
get api("/groups/#{subgroup.id}/labels", user), params: { include_ancestor_groups: false }
describe 'with subgroup labels' do
context 'when the include_ancestor_groups parameter is not set' do
let(:request) { get api("/groups/#{subgroup.id}/labels", user) }
let(:expected_labels) { [group_label1.name, group_label2.name, subgroup_label.name] }
it_behaves_like 'fetches labels'
context 'when search param is provided' do
let(:request) { get api("/groups/#{subgroup.id}/labels?search=lab", user) }
let(:expected_labels) { [group_label1.name, subgroup_label.name] }
it_behaves_like 'fetches labels'
end
end
context 'when the include_ancestor_groups parameter is set to false' do
let(:request) { get api("/groups/#{subgroup.id}/labels", user), params: { include_ancestor_groups: false } }
let(:expected_labels) { [subgroup_label.name] }
it_behaves_like 'fetches labels'
context 'when search param is provided' do
let(:request) { get api("/groups/#{subgroup.id}/labels?search=lab", user), params: { include_ancestor_groups: false } }
let(:expected_labels) { [subgroup_label.name] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(1)
expect(json_response.map {|r| r['name'] }).to contain_exactly('support')
it_behaves_like 'fetches labels'
end
end
end
end
......@@ -223,7 +260,7 @@ RSpec.describe API::GroupLabels do
expect(response).to have_gitlab_http_status(:ok)
expect(subgroup.labels[0].name).to eq('New Label')
expect(group_label1.name).to eq('feature')
expect(group_label1.name).to eq(group_label1.title)
end
it 'returns 404 if label does not exist' do
......@@ -278,7 +315,7 @@ RSpec.describe API::GroupLabels do
expect(response).to have_gitlab_http_status(:ok)
expect(subgroup.labels[0].name).to eq('New Label')
expect(group_label1.name).to eq('feature')
expect(group_label1.name).to eq(group_label1.title)
end
it 'returns 404 if label does not exist' do
......
......@@ -178,8 +178,8 @@ RSpec.describe API::Labels do
end
describe 'GET /projects/:id/labels' do
let(:group) { create(:group) }
let!(:group_label) { create(:group_label, title: 'feature', group: group) }
let_it_be(:group) { create(:group) }
let_it_be(:group_label) { create(:group_label, title: 'feature label', group: group) }
before do
project.update!(group: group)
......@@ -250,49 +250,41 @@ RSpec.describe API::Labels do
end
end
context 'when the include_ancestor_groups parameter is not set' do
let(:group) { create(:group) }
let!(:group_label) { create(:group_label, title: 'feature', group: group) }
let(:subgroup) { create(:group, parent: group) }
let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
context 'with subgroups' do
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:subgroup_label) { create(:group_label, title: 'support label', group: subgroup) }
before do
subgroup.add_owner(user)
project.update!(group: subgroup)
end
it 'returns all available labels for the project, parent group and ancestor groups' do
get api("/projects/#{project.id}/labels", user)
context 'when the include_ancestor_groups parameter is not set' do
let(:request) { get api("/projects/#{project.id}/labels", user) }
let(:expected_labels) { [priority_label.name, group_label.name, subgroup_label.name, label1.name] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(4)
expect(json_response.map {|r| r['name'] }).to contain_exactly(group_label.name, subgroup_label.name, priority_label.name, label1.name)
end
end
it_behaves_like 'fetches labels'
context 'when the include_ancestor_groups parameter is set to false' do
let(:group) { create(:group) }
let!(:group_label) { create(:group_label, title: 'feature', group: group) }
let(:subgroup) { create(:group, parent: group) }
let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) }
context 'when search param is provided' do
let(:request) { get api("/projects/#{project.id}/labels?search=lab", user) }
let(:expected_labels) { [group_label.name, subgroup_label.name, label1.name] }
before do
subgroup.add_owner(user)
project.update!(group: subgroup)
it_behaves_like 'fetches labels'
end
end
it 'returns all available labels for the project and the parent group only' do
get api("/projects/#{project.id}/labels", user), params: { include_ancestor_groups: false }
context 'when the include_ancestor_groups parameter is set to false' do
let(:request) { get api("/projects/#{project.id}/labels", user), params: { include_ancestor_groups: false } }
let(:expected_labels) { [subgroup_label.name, priority_label.name, label1.name] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(3)
expect(json_response.map {|r| r['name'] }).to contain_exactly(subgroup_label.name, priority_label.name, label1.name)
it_behaves_like 'fetches labels'
context 'when search param is provided' do
let(:request) { get api("/projects/#{project.id}/labels?search=lab", user), params: { include_ancestor_groups: false } }
let(:expected_labels) { [subgroup_label.name, label1.name] }
it_behaves_like 'fetches labels'
end
end
end
end
......@@ -513,7 +505,7 @@ RSpec.describe API::Labels do
end
describe 'PUT /projects/:id/labels/promote' do
let(:group) { create(:group) }
let_it_be(:group) { create(:group) }
before do
group.add_owner(user)
......
# frozen_string_literal: true
RSpec.shared_examples 'fetches labels' do
it 'returns correct labels' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(expected_labels.size)
expect(json_response.map {|r| r['name'] }).to match_array(expected_labels)
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