diff --git a/app/assets/javascripts/groups/settings/api/access_dropdown_api.js b/app/assets/javascripts/groups/settings/api/access_dropdown_api.js index 37f95a7ab30acf281a00481b39def4d7fcb48549..e232b1977f0a2a1ce66eb6a9d13c68d4f5f42a62 100644 --- a/app/assets/javascripts/groups/settings/api/access_dropdown_api.js +++ b/app/assets/javascripts/groups/settings/api/access_dropdown_api.js @@ -7,15 +7,16 @@ const buildUrl = (urlRoot, url) => { return joinPaths(urlRoot, url); }; -const defaultOptions = { includeParentDescendants: false }; +const defaultOptions = { includeParentDescendants: false, includeParentSharedGroups: false }; export const getSubGroups = (options = defaultOptions) => { - const { includeParentDescendants } = options; + const { includeParentDescendants, includeParentSharedGroups } = options; return axios.get(buildUrl(gon.relative_url_root || '', GROUP_SUBGROUPS_PATH), { params: { group_id: gon.current_group_id, include_parent_descendants: includeParentDescendants, + include_parent_shared_groups: includeParentSharedGroups, }, }); }; diff --git a/app/assets/javascripts/groups/settings/components/access_dropdown.vue b/app/assets/javascripts/groups/settings/components/access_dropdown.vue index fee1383c8e25139e7635a06df215a7e2eaa1456a..327f73b88c20bb4fd9222d5ebb0db8565756ebad 100644 --- a/app/assets/javascripts/groups/settings/components/access_dropdown.vue +++ b/app/assets/javascripts/groups/settings/components/access_dropdown.vue @@ -96,7 +96,7 @@ export default { Promise.all([ this.groups.length ? Promise.resolve({ data: this.groups }) - : getSubGroups({ includeParentDescendants: true }), + : getSubGroups({ includeParentDescendants: true, includeParentSharedGroups: true }), ]) .then(([groupsResponse]) => { this.consolidateData(groupsResponse.data); diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index ba691bcdf0f2968a1be82275acb95bdaccaaafbe..c89722327ecf00715f420346944e486b51539b92 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -16,6 +16,9 @@ # filter_group_ids: array of integers - only include groups from the specified list of ids # include_parent_descendants: boolean (defaults to false) - includes descendant groups when # filtering by parent. The parent param must be present. +# include_parent_shared_groups: boolean (defaults to false) - includes shared groups of a parent group +# when filtering by parent. +# Both parent and include_parent_descendants params must be present. # include_ancestors: boolean (defaults to true) # organization: Scope the groups to the Organizations::Organization # @@ -118,17 +121,27 @@ def by_organization(groups) groups.in_organization(organization) end - # rubocop: disable CodeReuse/ActiveRecord def by_parent(groups) return groups unless params[:parent] - if params.fetch(:include_parent_descendants, false) - groups.id_in(params[:parent].descendants) + if include_parent_descendants? + by_parent_descendants(groups, params[:parent]) else - groups.where(parent: params[:parent]) + by_parent_children(groups, params[:parent]) + end + end + + def by_parent_descendants(groups, parent) + if include_parent_shared_groups? + groups.descendants_with_shared_with_groups(parent) + else + groups.id_in(parent.descendants) end end - # rubocop: enable CodeReuse/ActiveRecord + + def by_parent_children(groups, parent) + groups.by_parent(parent) + end def filter_group_ids(groups) return groups unless params[:filter_group_ids] @@ -154,6 +167,14 @@ def sort(groups) groups.sort_by_attribute(params[:sort]) end + def include_parent_shared_groups? + params.fetch(:include_parent_shared_groups, false) + end + + def include_parent_descendants? + params.fetch(:include_parent_descendants, false) + end + def min_access_level? current_user && params[:min_access_level].present? end diff --git a/app/models/group.rb b/app/models/group.rb index d8fc468e7344f0051617c57668b8c7ede927aa48..80fcd4470a04486d00741d4e4294575cd9f6bf95 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -239,6 +239,27 @@ def of_ancestors_and_self .where(group_group_links: { shared_group_id: group.self_and_ancestors }) end + # Returns all groups that are shared with the given group (see :shared_with_group) + # and all descendents of the given group + # returns none if the given group is nil + scope :descendants_with_shared_with_groups, -> (group) do + return none if group.nil? + + descendants_query = group.descendants.select(:id) + # since we're only interested in ids, we query GroupGroupLink directly instead of using :shared_with_group + # to avoid an extra JOIN in the resulting query + shared_groups_query = GroupGroupLink + .where(shared_group_id: group.id) + .select('shared_with_group_id AS id') + + combined_query = Group + .from_union(descendants_query, shared_groups_query, alias_as: :combined) + .unscope(where: :type) + .select(:id) + + id_in(combined_query) + end + # WARNING: This method should never be used on its own # please do make sure the number of rows you are filtering is small # enough for this query diff --git a/ee/app/finders/autocomplete/group_subgroups_finder.rb b/ee/app/finders/autocomplete/group_subgroups_finder.rb index d150de44d05286a6489d30b059b69d4f12382703..36bbd0524800113b0f226dfe072476f89af2edbc 100644 --- a/ee/app/finders/autocomplete/group_subgroups_finder.rb +++ b/ee/app/finders/autocomplete/group_subgroups_finder.rb @@ -15,6 +15,10 @@ def include_parent_descendants? Gitlab::Utils.to_boolean(params[:include_parent_descendants]) end + def include_parent_shared_groups? + Gitlab::Utils.to_boolean(params[:include_parent_shared_groups]) + end + def group_id params[:group_id] end @@ -23,7 +27,8 @@ def group_id def execute group = ::Autocomplete::GroupFinder.new(current_user, nil, group_id: group_id).execute GroupsFinder.new(current_user, parent: group, - include_parent_descendants: include_parent_descendants?).execute.limit(LIMIT) + include_parent_descendants: include_parent_descendants?, + include_parent_shared_groups: include_parent_shared_groups?).execute.limit(LIMIT) end # rubocop: enable CodeReuse/Finder end diff --git a/ee/spec/finders/autocomplete/group_subgroups_finder_spec.rb b/ee/spec/finders/autocomplete/group_subgroups_finder_spec.rb index bf2a3974c4c8ba2b952fb09507012c9f7a9201c2..ac416291199a88e54080323cf3e643226550dad3 100644 --- a/ee/spec/finders/autocomplete/group_subgroups_finder_spec.rb +++ b/ee/spec/finders/autocomplete/group_subgroups_finder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Autocomplete::GroupSubgroupsFinder do +RSpec.describe Autocomplete::GroupSubgroupsFinder, feature_category: :groups_and_projects do describe '#execute' do let_it_be(:group) { create(:group, :private) } let_it_be(:subgroup_1) { create(:group, :private, parent: group) } @@ -10,10 +10,17 @@ let_it_be(:grandchild_1) { create(:group, :private, parent: subgroup_1) } let_it_be(:member_in_group) { create(:user).tap { |u| group.add_reporter(u) } } let_it_be(:member_in_subgroup) { create(:user).tap { |u| subgroup_1.add_reporter(u) } } + let_it_be(:invited_to_group) { create(:group, :public) } + let_it_be(:invited_to_subgroup) { create(:group, :public) } let(:params) { { group_id: group.id } } let(:current_user) { member_in_group } + before do + group.shared_with_groups << invited_to_group + grandchild_1.shared_with_groups << invited_to_subgroup + end + subject { described_class.new(current_user, params).execute } it 'returns subgroups', :aggregate_failures do @@ -21,6 +28,22 @@ expect(subject).to contain_exactly(subgroup_1, subgroup_2) end + context 'when include_parent_shared_groups parameter is true' do + before do + params[:include_parent_shared_groups] = true + params[:include_parent_descendants] = true + end + + it 'returns subgroups and shared groups' do + expect(subject.count).to eq(4) + expect(subject).to contain_exactly( + subgroup_1, + subgroup_2, + grandchild_1, + invited_to_group) + end + end + context 'when include_parent_descendants parameter is true' do before do params[:include_parent_descendants] = true diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb index b9add5b7db2de018e6133763426e804e93a6ae40..c1ca19e29ec44bcfd7aca03e915ed4161afaa7a5 100644 --- a/spec/finders/groups_finder_spec.rb +++ b/spec/finders/groups_finder_spec.rb @@ -180,17 +180,29 @@ let_it_be(:internal_sub_subgroup) { create(:group, :internal, parent: public_subgroup) } let_it_be(:private_sub_subgroup) { create(:group, :private, parent: public_subgroup) } let_it_be(:public_sub_subgroup) { create(:group, :public, parent: public_subgroup) } + let_it_be(:invited_to_group) { create(:group, :public) } + let_it_be(:invited_to_subgroup) { create(:group, :public) } let(:params) { { include_parent_descendants: true, parent: parent_group } } + before do + parent_group.shared_with_groups << invited_to_group + public_subgroup.shared_with_groups << invited_to_subgroup + end + context 'with nil parent' do - it 'returns all accessible groups' do + before do params[:parent] = nil + end + + it 'returns all accessible groups' do expect(described_class.new(user, params).execute).to contain_exactly( parent_group, public_subgroup, internal_sub_subgroup, - public_sub_subgroup + public_sub_subgroup, + invited_to_group, + invited_to_subgroup ) end end @@ -227,6 +239,22 @@ private_sub_subgroup ) end + + context 'when include shared groups is set' do + before do + params[:include_parent_shared_groups] = true + end + + it 'returns all group descendants with shared groups' do + expect(described_class.new(user, params).execute).to contain_exactly( + public_subgroup, + public_sub_subgroup, + internal_sub_subgroup, + private_sub_subgroup, + invited_to_group + ) + end + end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index f9f18a8b23e8acd0b73590a3f8b022f87bfb52f8..95c4af9c41c4b818c8edcd8f4efeb7e4e24c6bba 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1306,6 +1306,37 @@ it { is_expected.to match_array(groups) } end + + describe 'descendants_with_shared_with_groups' do + subject { described_class.descendants_with_shared_with_groups(parent_group) } + + let_it_be(:grand_parent_group) { create(:group, :public) } + let_it_be(:parent_group) { create(:group, :public, parent: grand_parent_group) } + let_it_be(:subgroup) { create(:group, :public, parent: parent_group) } + let_it_be(:subsubgroup) { create(:group, :public, parent: subgroup) } + + let_it_be(:shared_to_group) { create(:group, :public) } + let_it_be(:shared_to_sub_group) { create(:group, :public) } + + context 'when parent group is nil' do + let(:parent_group) { nil } + + it { is_expected.to match_array([]) } + end + + context 'when parent group is present and there are shared groups' do + before do + parent_group.shared_with_groups << shared_to_group + subgroup.shared_with_groups << shared_to_sub_group + end + + it { is_expected.to match_array([subgroup, subsubgroup, shared_to_group]) } + end + + context 'when parent group is present and there are no shared groups' do + it { is_expected.to match_array([subgroup, subsubgroup]) } + end + end end describe '#to_reference' do