Commit 41d70ea3 authored by Andre Guedes's avatar Andre Guedes

Added Issue Board API support

  - Includes documentation and tests
parent 8ddb082f
......@@ -18,6 +18,7 @@ v 8.13.0 (unreleased)
- Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API
- Added soft wrap button to repository file/blob editor
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
......
# Boards
Every API call to boards must be authenticated.
If a user is not a member of a project and the project is private, a `GET`
request on that project will result to a `404` status code.
## Project Board
Lists Issue Boards in the given project.
```
GET /projects/:id/boards
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/boards
```
Example response:
```json
[
{
"id" : 1,
"lists" : [
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
},
{
"id" : 2,
"label" : {
"name" : "Ready",
"color" : "#FF0000",
"description" : null
},
"position" : 2
},
{
"id" : 3,
"label" : {
"name" : "Production",
"color" : "#FF5F00",
"description" : null
},
"position" : 3
}
]
}
]
```
## List board lists
Get a list of the board's lists.
Does not include `backlog` and `done` lists
```
GET /projects/:id/boards/:board_id/lists
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
| `board_id` | integer | yes | The ID of a board |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists
```
Example response:
```json
[
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
},
{
"id" : 2,
"label" : {
"name" : "Ready",
"color" : "#FF0000",
"description" : null
},
"position" : 2
},
{
"id" : 3,
"label" : {
"name" : "Production",
"color" : "#FF5F00",
"description" : null
},
"position" : 3
}
]
```
## Single board list
Get a single board list.
```
GET /projects/:id/boards/:board_id/lists/:list_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
| `board_id` | integer | yes | The ID of a board |
| `list_id`| integer | yes | The ID of a board's list |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
```
Example response:
```json
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
}
```
## New board list
Creates a new Issue Board list.
If the operation is successful, a status code of `200` and the newly-created
list is returned. If an error occurs, an error number and a message explaining
the reason is returned.
```
POST /projects/:id/boards/:board_id/lists
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
| `board_id` | integer | yes | The ID of a board |
| `label_id` | integer | yes | The ID of a label |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists?label_id=5
```
Example response:
```json
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
}
```
## Edit board list
Updates an existing Issue Board list. This call is used to change list position.
If the operation is successful, a code of `200` and the updated board list is
returned. If an error occurs, an error number and a message explaining the
reason is returned.
```
PUT /projects/:id/boards/:board_id/lists/:list_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
| `position` | integer | yes | The position of the list |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1?position=2
```
Example response:
```json
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
}
```
## Delete a board list
Only for admins and project owners. Soft deletes the board list in question.
If the operation is successful, a status code `200` is returned. In case you cannot
destroy this board list, or it is not present, code `404` is given.
```
DELETE /projects/:id/boards/:board_id/lists/:list_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
```
Example response:
```json
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
}
```
......@@ -43,6 +43,7 @@ module API
mount ::API::Groups
mount ::API::Internal
mount ::API::Issues
mount ::API::Boards
mount ::API::Keys
mount ::API::Labels
mount ::API::LicenseTemplates
......
module API
# Boards API
class Boards < Grape::API
before { authenticate! }
resource :projects do
# Get the project board
get ':id/boards' do
authorize!(:read_board, user_project)
present [user_project.board], with: Entities::Board
end
segment ':id/boards/:board_id' do
helpers do
def project_board
board = user_project.board
if params[:board_id].to_i == board.id
board
else
not_found!('Board')
end
end
def board_lists
project_board.lists.destroyable
end
end
# Get the lists of a project board
# Does not include `backlog` and `done` lists
get '/lists' do
authorize!(:read_board, user_project)
present board_lists, with: Entities::List
end
# Get a list of a project board
get '/lists/:list_id' do
authorize!(:read_board, user_project)
present board_lists.find(params[:list_id]), with: Entities::List
end
# Create a new board list
#
# Parameters:
# id (required) - The ID of a project
# label_id (required) - The ID of an existing label
# Example Request:
# POST /projects/:id/boards/:board_id/lists
post '/lists' do
required_attributes! [:label_id]
unless user_project.labels.exists?(params[:label_id])
render_api_error!({ error: "Label not found!" }, 400)
end
authorize!(:admin_list, user_project)
list = ::Boards::Lists::CreateService.new(user_project, current_user,
{ label_id: params[:label_id] }).execute
if list.valid?
present list, with: Entities::List
else
render_validation_error!(list)
end
end
# Moves a board list to a new position
#
# Parameters:
# id (required) - The ID of a project
# board_id (required) - The ID of a board
# position (required) - The position of the list
# Example Request:
# PUT /projects/:id/boards/:board_id/lists/:list_id
put '/lists/:list_id' do
list = project_board.lists.movable.find(params[:list_id])
authorize!(:admin_list, user_project)
moved = ::Boards::Lists::MoveService.new(user_project, current_user,
{ position: params[:position].to_i }).execute(list)
if moved
present list, with: Entities::List
else
render_api_error!({ error: "List could not be moved!" }, 400)
end
end
# Delete a board list
#
# Parameters:
# id (required) - The ID of a project
# board_id (required) - The ID of a board
# list_id (required) - The ID of a board list
# Example Request:
# DELETE /projects/:id/boards/:board_id/lists/:list_id
delete "/lists/:list_id" do
list = board_lists.find_by(id: params[:list_id])
authorize!(:admin_list, user_project)
if list
destroyed_list = ::Boards::Lists::DestroyService.new(
user_project, current_user).execute(list)
present destroyed_list, with: Entities::List
else
not_found!('List')
end
end
end
end
end
end
......@@ -432,8 +432,11 @@ module API
end
end
class Label < Grape::Entity
class LabelBasic < Grape::Entity
expose :name, :color, :description
end
class Label < LabelBasic
expose :open_issues_count, :closed_issues_count, :open_merge_requests_count
expose :subscribed do |label, options|
......@@ -441,6 +444,19 @@ module API
end
end
class List < Grape::Entity
expose :id
expose :label, using: Entities::LabelBasic
expose :position
end
class Board < Grape::Entity
expose :id
expose :lists, using: Entities::List do |board|
board.lists.destroyable
end
end
class Compare < Grape::Entity
expose :commit, using: Entities::RepoCommit do |compare, options|
Commit.decorate(compare.commits, nil).last
......
require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
let(:guest) { create(:user) }
let(:admin) { create(:user, :admin) }
let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
let!(:dev_label) do
create(:label, title: 'Development', color: '#FFAABB', project: project)
end
let!(:test_label) do
create(:label, title: 'Testing', color: '#FFAACC', project: project)
end
let!(:ux_label) do
create(:label, title: 'UX', color: '#FF0000', project: project)
end
let!(:dev_list) do
create(:list, label: dev_label, position: 1)
end
let!(:test_list) do
create(:list, label: test_label, position: 2)
end
let!(:board) do
create(:board, project: project, lists: [dev_list, test_list])
end
before do
project.team << [user, :reporter]
project.team << [guest, :guest]
end
describe "GET /projects/:id/boards" do
let(:base_url) { "/projects/#{project.id}/boards" }
context "when unauthenticated" do
it "returns authentication error" do
get api(base_url)
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
it "returns the project issue board" do
get api(base_url, user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(board.id)
expect(json_response.first['lists']).to be_an Array
expect(json_response.first['lists'].length).to eq(2)
expect(json_response.first['lists'].last).to have_key('position')
end
end
end
describe "GET /projects/:id/boards/:board_id/lists" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it 'returns issue board lists' do
get api(base_url, user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['label']['name']).to eq(dev_label.title)
end
it 'returns 404 if board not found' do
get api("/projects/#{project.id}/boards/22343/lists", user)
expect(response).to have_http_status(404)
end
end
describe "GET /projects/:id/boards/:board_id/lists/:list_id" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it 'returns a list' do
get api("#{base_url}/#{dev_list.id}", user)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(dev_list.id)
expect(json_response['label']['name']).to eq(dev_label.title)
expect(json_response['position']).to eq(1)
end
it 'returns 404 if list not found' do
get api("#{base_url}/5324", user)
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/board/lists" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it 'creates a new issue board list' do
post api(base_url, user),
label_id: ux_label.id
expect(response).to have_http_status(201)
expect(json_response['label']['name']).to eq(ux_label.title)
expect(json_response['position']).to eq(3)
end
it 'returns 400 when creating a new list if label_id is invalid' do
post api(base_url, user),
label_id: 23423
expect(response).to have_http_status(400)
end
it "returns 403 for project members with guest role" do
put api("#{base_url}/#{test_list.id}", guest),
position: 1
expect(response).to have_http_status(403)
end
end
describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it "updates a list" do
put api("#{base_url}/#{test_list.id}", user),
position: 1
expect(response).to have_http_status(200)
expect(json_response['position']).to eq(1)
end
it "returns 404 error if list id not found" do
put api("#{base_url}/44444", user),
position: 1
expect(response).to have_http_status(404)
end
it "returns 403 for project members with guest role" do
put api("#{base_url}/#{test_list.id}", guest),
position: 1
expect(response).to have_http_status(403)
end
end
describe "DELETE /projects/:id/board/lists/:list_id" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it "rejects a non member from deleting a list" do
delete api("#{base_url}/#{dev_list.id}", non_member)
expect(response).to have_http_status(403)
end
it "rejects a user with guest role from deleting a list" do
delete api("#{base_url}/#{dev_list.id}", guest)
expect(response).to have_http_status(403)
end
it "returns 404 error if list id not found" do
delete api("#{base_url}/44444", user)
expect(response).to have_http_status(404)
end
context "when the user is project owner" do
let(:owner) { create(:user) }
let(:project) { create(:project, namespace: owner.namespace) }
it "deletes the list if an admin requests it" do
delete api("#{base_url}/#{dev_list.id}", owner)
expect(response).to have_http_status(200)
expect(json_response['position']).to eq(1)
end
end
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