Commit a5521d78 authored by Mark Chao's avatar Mark Chao

Add API endpoint for viewing and editing board config

Allow Labels::FindOrCreateService to find ancestor group labels
parent c4bbfe3e
......@@ -26,6 +26,8 @@ ee/db/**/*
ee/app/serializers/ee/merge_request_widget_entity.rb
ee/lib/api/epics.rb
ee/lib/api/geo_nodes.rb
ee/lib/ee/api/group_boards.rb
ee/lib/ee/api/boards.rb
ee/lib/ee/gitlab/ldap/sync/admin_users.rb
ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb
ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb
......
......@@ -20,6 +20,7 @@ module Labels
@available_labels ||= LabelsFinder.new(
current_user,
"#{parent_type}_id".to_sym => parent.id,
include_ancestor_groups: include_ancestor_groups?,
only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization)
end
......@@ -30,7 +31,8 @@ module Labels
new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent)
create_params = params.except(:include_ancestor_groups)
new_label = Labels::CreateService.new(create_params).execute(parent_type.to_sym => parent)
end
new_label
......@@ -47,5 +49,9 @@ module Labels
def parent_is_group?
parent_type == "group"
end
def include_ancestor_groups?
params[:include_ancestor_groups] == true
end
end
end
......@@ -209,6 +209,86 @@ Example response:
}
```
## Update a board **[STARTER]**
Updates a board.
```
PUT /projects/:id/boards/:board_id
```
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `name` | string | no | The new name of the board |
| `assignee_id` | integer | no | The assignee the board should be scoped to |
| `milestone_id` | integer | no | The milestone the board should be scoped to |
| `labels` | string | no | Comma-separated list of label names which the board should be scoped to |
| `weight` | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1?name=new_name&milestone_id=43&assignee_id=1&labels=Doing&weight=4
```
Example response:
```json
{
"id": 1,
"project": {
"id": 5,
"name": "Diaspora Project Site",
"name_with_namespace": "Diaspora / Diaspora Project Site",
"path": "diaspora-project-site",
"path_with_namespace": "diaspora/diaspora-project-site",
"created_at": "2018-07-03T05:48:49.982Z",
"default_branch": null,
"tag_list": [],
"ssh_url_to_repo": "ssh://user@example.com/diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
"readme_url": null,
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"last_activity_at": "2018-07-03T05:48:49.982Z"
},
"lists": [],
"name": "new_name",
"group": null,
"milestone": {
"id": 43,
"iid": 1,
"project_id": 15,
"title": "Milestone 1",
"description": "Milestone 1 desc",
"state": "active",
"created_at": "2018-07-03T06:36:42.618Z",
"updated_at": "2018-07-03T06:36:42.618Z",
"due_date": null,
"start_date": null,
"web_url": "http://example.com/root/board1/milestones/1"
},
"assignee": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://example.com/root"
},
"labels": [{
"id": 10,
"name": "Doing",
"color": "#5CB85C",
"description": null
}],
"weight": 4
}
```
## Delete a board **[STARTER]**
Deletes a board.
......
......@@ -28,7 +28,36 @@ Example response:
{
"id": 1,
"name:": "group issue board",
"group_id": 5,
"group": {
"id": 5,
"name": "Documentcloud",
"path": "documentcloud",
"owner_id": null,
"created_at": "2018-05-07T06:52:45.788Z",
"updated_at": "2018-07-03T06:48:17.005Z",
"description": "Consequatur aut a aperiam ut.",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 20,
"request_access_enabled": false,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": null,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"runners_token": "rgeeL-nv4wa9YdRvuMid"
},
"milestone": {
"id": 12
"title": "10.0"
......@@ -89,7 +118,36 @@ Example response:
{
"id": 1,
"name:": "group issue board",
"group_id": 5,
"group": {
"id": 5,
"name": "Documentcloud",
"path": "documentcloud",
"owner_id": null,
"created_at": "2018-05-07T06:52:45.788Z",
"updated_at": "2018-07-03T06:48:17.005Z",
"description": "Consequatur aut a aperiam ut.",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 20,
"request_access_enabled": false,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": null,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"runners_token": "rgeeL-nv4wa9YdRvuMid"
},
"milestone": {
"id": 12
"title": "10.0"
......@@ -149,7 +207,36 @@ Example response:
{
"id": 1,
"name": "newboard",
"group_id": 5,
"group": {
"id": 5,
"name": "Documentcloud",
"path": "documentcloud",
"owner_id": null,
"created_at": "2018-05-07T06:52:45.788Z",
"updated_at": "2018-07-03T06:48:17.005Z",
"description": "Consequatur aut a aperiam ut.",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 20,
"request_access_enabled": false,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": null,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"runners_token": "rgeeL-nv4wa9YdRvuMid"
},
"milestone": {
"id": 12
"title": "10.0"
......@@ -186,6 +273,98 @@ Example response:
}
```
## Update a board
Updates a board.
```
PUT /groups/:id/boards/:board_id
```
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `name` | string | no | The new name of the board |
| `assignee_id` | integer | no | The assignee the board should be scoped to |
| `milestone_id` | integer | no | The milestone the board should be scoped to |
| `labels` | string | no | Comma-separated list of label names which the board should be scoped to |
| `weight` | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/boards/1?name=new_name&milestone_id=44&assignee_id=1&labels=GroupLabel&weight=4
```
Example response:
```json
{
"id": 1,
"project": null,
"lists": [],
"name": "new_name",
"group": {
"id": 5,
"name": "Documentcloud",
"path": "documentcloud",
"owner_id": null,
"created_at": "2018-05-07T06:52:45.788Z",
"updated_at": "2018-07-03T06:48:17.005Z",
"description": "Consequatur aut a aperiam ut.",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 20,
"request_access_enabled": false,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": null,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"runners_token": "rgeeL-nv3wa6YdRvuMid"
},
"milestone": {
"id": 44,
"iid": 1,
"group_id": 5,
"title": "Group Milestone",
"description": "Group Milestone Desc",
"state": "active",
"created_at": "2018-07-03T07:15:19.271Z",
"updated_at": "2018-07-03T07:15:19.271Z",
"due_date": null,
"start_date": null,
"web_url": "http://example.com/groups/documentcloud/-/milestones/1"
},
"assignee": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://example.com/root"
},
"labels": [{
"id": 11,
"name": "GroupLabel",
"color": "#428BCA",
"description": ""
}],
"weight": 4
}
```
## Delete a board
Deletes a board.
......
......@@ -10,6 +10,7 @@ module Boards
set_assignee
set_milestone
set_labels
board.update(params)
end
......
......@@ -27,6 +27,23 @@ module EE
params[:milestone_id] = milestone&.id
end
def set_labels
labels = params.delete(:labels)
return unless labels
params[:label_ids] = labels.split(",").map do |label_name|
label = Labels::FindOrCreateService.new(
current_user,
parent,
title: label_name.strip,
include_ancestor_groups: true
).execute
label.try(:id)
end.compact
end
end
end
end
---
title: Add API endpoint for viewing and editing board config
merge_request: 5954
author:
type: added
......@@ -31,6 +31,19 @@ module EE
create_board
end
desc 'Update a project board' do
detail 'This feature was introduced in 11.0'
success ::API::Entities::Board
end
params do
use :update_params
end
put '/:board_id' do
authorize!(:admin_board, board_parent)
update_board
end
desc 'Delete a project board' do
detail 'This feature was introduced in 10.4'
success ::API::Entities::Board
......
......@@ -14,6 +14,17 @@ module EE
present board, with: ::API::Entities::Board
end
def update_board
service = ::Boards::UpdateService.new(board_parent, current_user, declared_params)
service.execute(board)
if board.valid?
present board, with: ::API::Entities::Board
else
bad_request!("Failed to save board #{board.errors.messages}")
end
end
def delete_board
forbidden! unless board_parent.multiple_issue_boards_available?
......@@ -22,6 +33,14 @@ module EE
service.execute(board)
end
end
params :update_params do
optional :name, type: String, desc: 'The board name'
optional :assignee_id, type: Integer, desc: 'The ID of a user to associate with board'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to associate with board'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :weight, type: Integer, desc: 'The weight of the board'
end
end
end
end
......
......@@ -34,6 +34,19 @@ module EE
create_board
end
desc 'Update a group board' do
detail 'This feature was introduced in 11.0'
success ::API::Entities::Board
end
params do
use :update_params
end
put '/:board_id' do
authorize!(:admin_board, board_parent)
update_board
end
desc 'Delete a group board' do
detail 'This feature was introduced in 10.4'
success ::API::Entities::Board
......
......@@ -79,8 +79,6 @@ describe Boards::UpdateService, services: true do
end
context 'group board milestone' do
let(:group) { create(:group) }
let(:group_board) { create(:board, group: group, name: 'Backend Group') }
let!(:milestone) { create(:milestone) }
before do
......@@ -90,18 +88,18 @@ describe Boards::UpdateService, services: true do
it 'is not updated if it is not within group milestones' do
service = described_class.new(group, double, milestone_id: milestone.id)
service.execute(group_board)
service.execute(board)
expect(group_board.reload.milestone).to be_nil
expect(board.reload.milestone).to be_nil
end
it 'is updated if it is within group milestones' do
milestone.update!(project: nil, group: group)
service = described_class.new(group, double, milestone_id: milestone.id)
service.execute(group_board)
service.execute(board)
expect(group_board.reload.milestone).to eq(milestone)
expect(board.reload.milestone).to eq(milestone)
end
end
......@@ -141,5 +139,81 @@ describe Boards::UpdateService, services: true do
expect(board.reload.milestone).to eq(milestone)
end
end
context '#set_labels' do
def expect_label_assigned(user, board, input_labels, expected_labels)
service = described_class.new(board.parent, user, labels: input_labels.join(','))
service.execute(board)
expect(board.reload.labels.map(&:title)).to contain_exactly(*expected_labels)
end
let(:user) { create(:user) }
let(:role) { :guest }
context 'group board labels' do
let!(:board) { create(:board, group: group, name: 'Backend') }
let!(:group_label) { create(:group_label, title: 'group_label', group: group) }
before do
group.add_user(user, role)
stub_licensed_features(scoped_issue_board: true)
end
it 'updates using only existing label' do
expect_label_assigned(user, board, %w{group_label new_label}, %w{group_label})
end
context 'user with admin_label ability' do
let(:role) { :reporter }
it 'finds and creates labels' do
expect_label_assigned(user, board, %w{group_label new_label}, %w{group_label new_label})
end
end
context 'nested group' do
let!(:child_group) { create(:group, parent: group)}
let(:project) { create(:project, group: child_group) }
it "allows using ancestor group's label" do
expect_label_assigned(user, board, %w{group_label}, %w{group_label})
end
end
end
context 'project board labels' do
let(:project) { create(:project, group: group) }
let!(:board) { create(:board, project: project, name: 'Backend') }
let!(:group_label) { create(:group_label, title: 'group_label', group: group) }
let!(:label) { create(:label, title: 'project_label', project: project) }
before do
project.add_user(user, role)
stub_licensed_features(scoped_issue_board: true)
end
context 'user with admin_label ability' do
let(:role) { :reporter }
it 'finds and creates labels' do
expect_label_assigned(user, board, %w{group_label project_label new_label}, %w{group_label project_label new_label})
end
end
it 'updates using only existing label' do
expect_label_assigned(user, board, %w{group_label project_label new_label}, %w{group_label project_label})
end
context 'nested group' do
let!(:child_group) { create(:group, parent: group)}
let(:project) { create(:project, group: child_group) }
it "allows using ancestor group's label" do
expect_label_assigned(user, board, %w{group_label project_label new_label}, %w{group_label project_label})
end
end
end
end
end
end
......@@ -17,7 +17,25 @@ shared_examples_for 'multiple and scoped issue boards' do |route_definition|
end
end
describe "DELETE #{route_definition}" do
describe "PUT #{route_definition}/:board_id" do
let(:url) { "#{root_url}/#{board.id}" }
it 'updates a board' do
put api(url, user), name: 'new name', weight: 4, labels: 'foo, bar'
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/board', dir: "ee")
board.reload
expect(board.name).to eq('new name')
expect(board.weight).to eq(4)
expect(board.labels.map(&:title)).to contain_exactly('foo', 'bar')
end
end
describe "DELETE #{route_definition}/:board_id" do
let(:url) { "#{root_url}/#{board.id}" }
it 'deletes a board' do
......
......@@ -44,6 +44,26 @@ describe Labels::FindOrCreateService do
expect(service.execute).to eq project_label
end
end
context 'when include_ancestor_groups is true' do
let(:group) { create(:group, :nested) }
let(:params) do
{
title: 'Audit',
include_ancestor_groups: true
}
end
it 'returns the ancestor group labels' do
group_label = create(:group_label, group: group.parent, title: 'Audit')
expect(service.execute).to eq group_label
end
it 'creates new labels if labels are not found' do
expect { service.execute }.to change(project.labels, :count).by(1)
end
end
end
context 'when finding labels on group level' do
......
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