Commit 4615c56d authored by Nick Thomas's avatar Nick Thomas

Merge branch '14724-show-ancestor-group-epics-in-epics-select' into 'master'

Show ancestor group epics in epic select

See merge request gitlab-org/gitlab!16218
parents 7540c54c 85d2c4df
......@@ -49,6 +49,8 @@ GET /groups/:id/epics?state=opened
| `created_before` | datetime | no | Return epics created on or before the given time |
| `updated_after` | datetime | no | Return epics updated on or after the given time |
| `updated_before` | datetime | no | Return epics updated on or before the given time |
| `include_ancestor_groups` | boolean | no | Include epics from the requested group's ancestors. Default is `false` |
| `include_descendant_groups` | boolean | no | Include epics from the requested group's descendants. Default is `true` |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics
......
......@@ -7,7 +7,8 @@ export default {
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
subscriptionPath: '/api/:version/namespaces/:id/gitlab_subscription',
childEpicPath: '/api/:version/groups/:id/epics/:epic_iid/epics',
groupEpicsPath: '/api/:version/groups/:id/epics',
groupEpicsPath:
'/api/:version/groups/:id/epics?include_ancestor_groups=:includeAncestorGroups&include_descendant_groups=:includeDescendantGroups',
epicIssuePath: '/api/:version/groups/:id/epics/:epic_iid/issues/:issue_id',
userSubscription(namespaceId) {
......@@ -43,8 +44,11 @@ export default {
});
},
groupEpics({ groupId }) {
const url = Api.buildUrl(this.groupEpicsPath).replace(':id', groupId);
groupEpics({ groupId, includeAncestorGroups = false, includeDescendantGroups = true }) {
const url = Api.buildUrl(this.groupEpicsPath)
.replace(':id', groupId)
.replace(':includeAncestorGroups', includeAncestorGroups)
.replace(':includeDescendantGroups', includeDescendantGroups);
return axios.get(url);
},
......
......@@ -17,14 +17,12 @@ export const setSelectedEpic = ({ commit }, selectedEpic) =>
commit(types.SET_SELECTED_EPIC, selectedEpic);
export const requestEpics = ({ commit }) => commit(types.REQUEST_EPICS);
export const receiveEpicsSuccess = ({ state, commit }, data) => {
const epics = data
.filter(rawEpic => rawEpic.group_id === state.groupId)
.map(rawEpic =>
convertObjectPropsToCamelCase(Object.assign(rawEpic, { url: rawEpic.web_edit_url }), {
dropKeys: ['web_edit_url'],
}),
);
export const receiveEpicsSuccess = ({ commit }, data) => {
const epics = data.map(rawEpic =>
convertObjectPropsToCamelCase(Object.assign({}, rawEpic, { url: rawEpic.web_edit_url }), {
dropKeys: ['web_edit_url'],
}),
);
commit(types.RECEIVE_EPICS_SUCCESS, { epics });
};
......@@ -37,6 +35,8 @@ export const fetchEpics = ({ state, dispatch }) => {
Api.groupEpics({
groupId: state.groupId,
includeDescendantGroups: false,
includeAncestorGroups: true,
})
.then(({ data }) => {
dispatch('receiveEpicsSuccess', data);
......
# frozen_string_literal: true
# Params:
# iids: integer[]
# state: 'open' or 'closed' or 'all'
# group_id: integer
# parent_id: integer
# author_id: integer
# author_username: string
# label_name: string
# search: string
# sort: string
# start_date: datetime
# end_date: datetime
# created_after: datetime
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
# include_ancestor_groups: boolean
# include_descendant_groups: boolean
class EpicsFinder < IssuableFinder
def self.scalar_params
@scalar_params ||= %i[
......@@ -59,7 +78,7 @@ class EpicsFinder < IssuableFinder
# The `group` method takes care of checking permissions
[group]
else
groups_user_can_read_epics(group.self_and_descendants)
groups_user_can_read_epics(related_groups)
end
Epic.where(group: groups)
......@@ -68,6 +87,21 @@ class EpicsFinder < IssuableFinder
private
def related_groups
include_ancestors = params.fetch(:include_ancestor_groups, false)
include_descendants = params.fetch(:include_descendant_groups, true)
if include_ancestors && include_descendants
group.self_and_hierarchy
elsif include_ancestors
group.self_and_ancestors
elsif include_descendants
group.self_and_descendants
else
Group.id_in(group.id)
end
end
def count_key(value)
last_value = Array(value).last
......
......@@ -34,6 +34,8 @@ module API
optional :created_before, type: DateTime, desc: 'Return epics created before the specified time'
optional :updated_after, type: DateTime, desc: 'Return epics updated after the specified time'
optional :updated_before, type: DateTime, desc: 'Return epics updated before the specified time'
optional :include_ancestor_groups, type: Boolean, default: false, desc: 'Include epics from ancestor groups'
optional :include_descendant_groups, type: Boolean, default: true, desc: 'Include epics from descendant groups'
use :pagination
end
get ':id/(-/)epics' do
......
......@@ -12,7 +12,7 @@ describe EpicsFinder do
describe '#execute' do
def epics(params = {})
params[:group_id] = group.id
params[:group_id] ||= group.id
described_class.new(search_user, params).execute
end
......@@ -113,11 +113,49 @@ describe EpicsFinder do
context 'when subgroups are supported' do
let(:subgroup) { create(:group, :private, parent: group) }
let(:subgroup2) { create(:group, :private, parent: subgroup) }
let!(:subepic1) { create(:epic, group: subgroup) }
let!(:subepic2) { create(:epic, group: subgroup2) }
let!(:subgroup_epic) { create(:epic, group: subgroup) }
let!(:subgroup2_epic) { create(:epic, group: subgroup2) }
it 'returns all epics that belong to the given group and its subgroups' do
expect(epics).to contain_exactly(epic1, epic2, epic3, subepic1, subepic2)
expect(epics).to contain_exactly(epic1, epic2, epic3, subgroup_epic, subgroup2_epic)
end
describe 'hierarchy params' do
let(:finder_params) { {} }
subject { epics(finder_params.merge(group_id: subgroup.id)) }
it 'excludes ancestor groups and includes descendant groups by default' do
is_expected.to contain_exactly(subgroup_epic, subgroup2_epic)
end
context 'when include_descendant_groups is false' do
context 'and include_ancestor_groups is false' do
let(:finder_params) { { include_descendant_groups: false, include_ancestor_groups: false } }
it { is_expected.to contain_exactly(subgroup_epic) }
end
context 'and include_ancestor_groups is true' do
let(:finder_params) { { include_descendant_groups: false, include_ancestor_groups: true } }
it { is_expected.to contain_exactly(subgroup_epic, epic1, epic2, epic3) }
end
end
context 'when include_descendant_groups is true' do
context 'and include_ancestor_groups is false' do
let(:finder_params) { { include_descendant_groups: true, include_ancestor_groups: false } }
it { is_expected.to contain_exactly(subgroup_epic, subgroup2_epic) }
end
context 'and include_ancestor_groups is true' do
let(:finder_params) { { include_descendant_groups: true, include_ancestor_groups: true } }
it { is_expected.to contain_exactly(subgroup_epic, subgroup2_epic, epic1, epic2, epic3) }
end
end
end
it 'does not execute more than 14 SQL queries' do
......
......@@ -88,7 +88,7 @@ describe('Api', () => {
describe('groupEpics', () => {
it('calls `axios.get` using param `groupId`', done => {
const groupId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/epics`;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/epics?include_ancestor_groups=false&include_descendant_groups=true`;
mock.onGet(expectedUrl).reply(200, mockEpics);
......
......@@ -184,7 +184,11 @@ describe('EpicsSelect', () => {
dispatch: () => {},
});
expect(Api.groupEpics).toHaveBeenCalledWith({ groupId: state.groupId });
expect(Api.groupEpics).toHaveBeenCalledWith({
groupId: state.groupId,
includeDescendantGroups: false,
includeAncestorGroups: true,
});
});
});
......
......@@ -330,6 +330,33 @@ describe API::Epics do
end
end
context 'with hierarchy params' do
let(:subgroup) { create(:group, parent: group) }
let(:subgroup2) { create(:group, parent: subgroup) }
let!(:subgroup_epic) { create(:epic, group: subgroup) }
let!(:subgroup2_epic) { create(:epic, group: subgroup2) }
let(:url) { "/groups/#{subgroup.id}/epics" }
before do
stub_licensed_features(epics: true)
epic
end
it 'excludes descendant group epics' do
get api(url), params: { include_descendant_groups: false }
expect_paginated_array_response(subgroup_epic.id)
end
it 'includes ancestor group epics' do
get api(url), params: { include_ancestor_groups: true }
expect_paginated_array_response([epic.id, subgroup2_epic.id, subgroup_epic.id])
end
end
context 'with pagination params' do
let(:page) { 1 }
let(:per_page) { 2 }
......
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