Commit 43bcc1bf authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge branch '7328-reorder-epics-api' into 'master'

Add API for reordering child epics

Closes #7328

See merge request gitlab-org/gitlab-ee!9781
parents fadfbe18 c81c9025
...@@ -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