Commit c81c9025 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Add API for reordering child epics

Also makes the listing API return an ordered list of child epics
parent 69676ec1
...@@ -21,7 +21,7 @@ GET /groups/:id/epics/:epic_iid/epics ...@@ -21,7 +21,7 @@ GET /groups/:id/epics/:epic_iid/epics
| Attribute | Type | Required | Description | | 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 | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `epic_iid` | integer/string | yes | The internal ID of the epic. | | `epic_iid` | integer | yes | The internal ID of the epic. |
```bash ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/epics/ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/epics/
...@@ -73,8 +73,8 @@ POST /groups/:id/epics/:epic_iid/epics ...@@ -73,8 +73,8 @@ POST /groups/:id/epics/:epic_iid/epics
| Attribute | Type | Required | Description | | 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 | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `epic_iid` | integer/string | yes | The internal ID of the epic. | | `epic_iid` | integer | yes | The internal ID of the epic. |
| `child_epic_id` | integer/string | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. | | `child_epic_id` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. |
```bash ```bash
curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/epics/6 curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/epics/6
...@@ -113,9 +113,62 @@ Example response: ...@@ -113,9 +113,62 @@ Example response:
} }
``` ```
## Delete an epic parent ## Re-order a child epic
Removes an epic - epic association. ```
PUT /groups/:id/epics/:epic_iid/epics/:child_epic_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. |
| `epic_iid` | integer | yes | The internal ID of the epic. |
| `child_epic_id` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. |
| `move_before_id` | integer | no | The global ID of a sibling epic that should be placed before the child epic. |
| `move_after_id` | integer | no | The global ID of a sibling epic that should be placed after the child epic. |
```bash
curl --header PUT "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/4/epics/5
```
Example response:
```json
[
{
"id": 29,
"iid": 6,
"group_id": 1,
"parent_id": 5,
"title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.",
"description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
"author": {
"id": 10,
"name": "Lu Mayer",
"username": "kam",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
"web_url": "http://localhost:3001/kam"
},
"start_date": null,
"start_date_is_fixed": false,
"start_date_fixed": null,
"start_date_from_milestones": null,
"end_date": "2018-07-31",
"due_date": "2018-07-31",
"due_date_is_fixed": false,
"due_date_fixed": null,
"due_date_from_milestones": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"labels": []
}
]
```
## Unassign a child epic
Unassigns a child epic from a parent epic.
``` ```
DELETE /groups/:id/epics/:epic_iid/epics/:child_epic_id DELETE /groups/:id/epics/:epic_iid/epics/:child_epic_id
...@@ -124,8 +177,8 @@ DELETE /groups/:id/epics/:epic_iid/epics/:child_epic_id ...@@ -124,8 +177,8 @@ DELETE /groups/:id/epics/:epic_iid/epics/:child_epic_id
| Attribute | Type | Required | Description | | 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. | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `epic_iid` | integer/string | yes | The internal ID of the epic. | | `epic_iid` | integer | yes | The internal ID of the epic. |
| `child_epic_id` | integer/string | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. | | `child_epic_id` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. |
```bash ```bash
curl --header DELETE "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/4/epics/5 curl --header DELETE "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/4/epics/5
......
---
title: Add API for reordering child epics
merge_request: 9781
author:
type: added
...@@ -19,6 +19,14 @@ module API ...@@ -19,6 +19,14 @@ module API
end end
end end
def child_epics
EpicsFinder.new(current_user, {
parent_id: epic.id,
group_id: user_group.id,
sort: 'relative_position'
}).execute
end
params :child_epic_id do params :child_epic_id do
# Unique ID should be used because epics from other groups can be assigned as child. # Unique ID should be used because epics from other groups can be assigned as child.
requires :child_epic_id, type: Integer, desc: 'The global ID of the epic that will be assigned as child' requires :child_epic_id, type: Integer, desc: 'The global ID of the epic that will be assigned as child'
...@@ -29,6 +37,7 @@ module API ...@@ -29,6 +37,7 @@ module API
requires :id, type: String, desc: 'The ID of a group' requires :id, type: String, desc: 'The ID of a group'
requires :epic_iid, type: Integer, desc: 'The internal ID of an epic' requires :epic_iid, type: Integer, desc: 'The internal ID of an epic'
end end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get related epics' do desc 'Get related epics' do
success EE::API::Entities::Epic success EE::API::Entities::Epic
...@@ -36,8 +45,6 @@ module API ...@@ -36,8 +45,6 @@ module API
get ':id/(-/)epics/:epic_iid/epics' do get ':id/(-/)epics/:epic_iid/epics' do
authorize_can_read! authorize_can_read!
child_epics = EpicsFinder.new(current_user, parent_id: epic.id, group_id: user_group.id).execute
present child_epics, with: EE::API::Entities::Epic present child_epics, with: EE::API::Entities::Epic
end end
...@@ -72,6 +79,26 @@ module API ...@@ -72,6 +79,26 @@ module API
present updated_epic, with: EE::API::Entities::Epic present updated_epic, with: EE::API::Entities::Epic
end end
desc 'Reorder child epics'
params do
use :child_epic_id
optional :move_before_id, type: Integer, desc: 'The id of the epic that should be positioned before the child epic'
optional :move_after_id, type: Integer, desc: 'The id of the epic that should be positioned after the child epic'
end
put ':id/(-/)epics/:epic_iid/epics/:child_epic_id' do
authorize_can_admin!
update_params = params.slice(:move_before_id, :move_after_id)
result = ::EpicLinks::UpdateService.new(child_epic, current_user, update_params).execute
if result[:status] == :success
present child_epics, with: EE::API::Entities::Epic
else
render_api_error!(result[:message], result[:http_status])
end
end
end end
end end
end end
...@@ -11,21 +11,25 @@ describe API::EpicLinks do ...@@ -11,21 +11,25 @@ describe API::EpicLinks do
it 'returns 403 when epics feature is disabled' do it 'returns 403 when epics feature is disabled' do
group.add_developer(user) group.add_developer(user)
get api(url, user) subject
expect(response).to have_gitlab_http_status(403) expect(response).to have_gitlab_http_status(403)
end end
it 'returns 401 unauthorized error for non authenticated user' do context 'unauthenticated user' do
get api(url) let(:user) { nil }
expect(response).to have_gitlab_http_status(401) it 'returns 401 unauthorized error' do
subject
expect(response).to have_gitlab_http_status(401)
end
end end
it 'returns 404 not found error for a user without permissions to see the group' do it 'returns 404 not found error for a user without permissions to see the group' do
group.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE) group.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
get api(url, user) subject
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
...@@ -34,6 +38,8 @@ describe API::EpicLinks do ...@@ -34,6 +38,8 @@ describe API::EpicLinks do
describe 'GET /groups/:id/epics/:epic_iid/epics' do describe 'GET /groups/:id/epics/:epic_iid/epics' do
let(:url) { "/groups/#{group.path}/epics/#{epic.iid}/epics" } let(:url) { "/groups/#{group.path}/epics/#{epic.iid}/epics" }
subject { get api(url, user) }
it_behaves_like 'user does not have access' it_behaves_like 'user does not have access'
context 'when epics feature is enabled' do context 'when epics feature is enabled' do
...@@ -41,24 +47,26 @@ describe API::EpicLinks do ...@@ -41,24 +47,26 @@ describe API::EpicLinks do
stub_licensed_features(epics: true) stub_licensed_features(epics: true)
end end
let!(:child_epic1) { create(:epic, group: group, parent: epic) } let!(:child_epic1) { create(:epic, group: group, parent: epic, relative_position: 200) }
let!(:child_epic2) { create(:epic, group: group, parent: epic) } let!(:child_epic2) { create(:epic, group: group, parent: epic, relative_position: 100) }
it 'returns 200 status' do it 'returns 200 status' do
get api(url, user) subject
epics = JSON.parse(response.body) epics = JSON.parse(response.body)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/epics', dir: 'ee') expect(response).to match_response_schema('public_api/v4/epics', dir: 'ee')
expect(epics.map { |epic| epic["id"] }).to match_array([child_epic1.id, child_epic2.id]) expect(epics.map { |epic| epic["id"] }).to eq([child_epic2.id, child_epic1.id])
end end
end end
end end
describe 'POST /groups/:id/epics/:epic_iid/epics' do describe 'POST /groups/:id/epics/:epic_iid/epics' do
let(:child_epic) { create(:epic, group: group) } let(:child_epic) { create(:epic, group: group) }
let(:url) { "/groups/#{group.path}/epics/#{epic.iid}/epics" } let(:url) { "/groups/#{group.path}/epics/#{epic.iid}/epics/#{child_epic.id}" }
subject { post api(url, user) }
it_behaves_like 'user does not have access' it_behaves_like 'user does not have access'
...@@ -71,7 +79,7 @@ describe API::EpicLinks do ...@@ -71,7 +79,7 @@ describe API::EpicLinks do
it 'returns 403' do it 'returns 403' do
group.add_guest(user) group.add_guest(user)
post api("#{url}/#{child_epic.id}", user) subject
expect(response).to have_gitlab_http_status(403) expect(response).to have_gitlab_http_status(403)
end end
...@@ -81,7 +89,7 @@ describe API::EpicLinks do ...@@ -81,7 +89,7 @@ describe API::EpicLinks do
it 'returns 201 status' do it 'returns 201 status' do
group.add_developer(user) group.add_developer(user)
post api("#{url}/#{child_epic.id}", user) subject
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
expect(response).to match_response_schema('public_api/v4/epic', dir: 'ee') expect(response).to match_response_schema('public_api/v4/epic', dir: 'ee')
...@@ -96,7 +104,7 @@ describe API::EpicLinks do ...@@ -96,7 +104,7 @@ describe API::EpicLinks do
it 'returns 404 status' do it 'returns 404 status' do
group.add_developer(user) group.add_developer(user)
post api(url, user), params: { child_epic_id: child_epic.id } subject
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
...@@ -104,9 +112,53 @@ describe API::EpicLinks do ...@@ -104,9 +112,53 @@ describe API::EpicLinks do
end end
end end
describe 'PUT /groups/:id/epics/:epic_iid/epics/:child_epic_id' do
let!(:child_epic) { create(:epic, group: group, parent: epic, relative_position: 100) }
let!(:sibling_1) { create(:epic, group: group, parent: epic, relative_position: 200) }
let!(:sibling_2) { create(:epic, group: group, parent: epic, relative_position: 300) }
let(:url) { "/groups/#{group.path}/epics/#{epic.iid}/epics/#{child_epic.id}" }
subject { put api(url, user), params: { move_before_id: sibling_1.id, move_after_id: sibling_2.id } }
it_behaves_like 'user does not have access'
context 'when epics are enabled' do
before do
stub_licensed_features(epics: true)
end
context 'when user has permissions to reorder epics' do
before do
group.add_developer(user)
end
it 'returns status 200' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/epics', dir: 'ee')
expect(json_response.map { |epic| epic['id'] }).to eq([sibling_1.id, child_epic.id, sibling_2.id])
end
end
context 'when user does not have permissions to reorder epics' do
it 'returns status 403' do
group.add_guest(user)
subject
expect(response).to have_gitlab_http_status(403)
end
end
end
end
describe 'DELETE /groups/:id/epics/:epic_iid/epics' do describe 'DELETE /groups/:id/epics/:epic_iid/epics' do
let!(:child_epic) { create(:epic, group: group, parent: epic)} let!(:child_epic) { create(:epic, group: group, parent: epic)}
let(:url) { "/groups/#{group.path}/epics/#{epic.iid}/epics" } let(:url) { "/groups/#{group.path}/epics/#{epic.iid}/epics/#{child_epic.id}" }
subject { delete api(url, user) }
it_behaves_like 'user does not have access' it_behaves_like 'user does not have access'
...@@ -119,7 +171,7 @@ describe API::EpicLinks do ...@@ -119,7 +171,7 @@ describe API::EpicLinks do
it 'returns 403' do it 'returns 403' do
group.add_guest(user) group.add_guest(user)
delete api("#{url}/#{child_epic.id}", user) subject
expect(response).to have_gitlab_http_status(403) expect(response).to have_gitlab_http_status(403)
end end
...@@ -129,7 +181,7 @@ describe API::EpicLinks do ...@@ -129,7 +181,7 @@ describe API::EpicLinks do
it 'returns 200 status' do it 'returns 200 status' do
group.add_developer(user) group.add_developer(user)
delete api("#{url}/#{child_epic.id}", user) subject
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/epic', dir: 'ee') expect(response).to match_response_schema('public_api/v4/epic', dir: 'ee')
......
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