Commit 82c623ed authored by Jason Goodman's avatar Jason Goodman Committed by Vitaly Slobodin

Filter Search Results in Group Modal Invite

parent 8c28ec09
...@@ -3,9 +3,9 @@ import { buildApiUrl } from './api_utils'; ...@@ -3,9 +3,9 @@ import { buildApiUrl } from './api_utils';
import { DEFAULT_PER_PAGE } from './constants'; import { DEFAULT_PER_PAGE } from './constants';
const GROUPS_PATH = '/api/:version/groups.json'; const GROUPS_PATH = '/api/:version/groups.json';
const DESCENDANT_GROUPS_PATH = '/api/:version/groups/:id/descendant_groups';
export function getGroups(query, options, callback = () => {}) { const axiosGet = (url, query, options, callback) => {
const url = buildApiUrl(GROUPS_PATH);
return axios return axios
.get(url, { .get(url, {
params: { params: {
...@@ -19,4 +19,14 @@ export function getGroups(query, options, callback = () => {}) { ...@@ -19,4 +19,14 @@ export function getGroups(query, options, callback = () => {}) {
return data; return data;
}); });
};
export function getGroups(query, options, callback = () => {}) {
const url = buildApiUrl(GROUPS_PATH);
return axiosGet(url, query, options, callback);
}
export function getDescendentGroups(parentGroupId, query, options, callback = () => {}) {
const url = buildApiUrl(DESCENDANT_GROUPS_PATH.replace(':id', parentGroupId));
return axiosGet(url, query, options, callback);
} }
...@@ -7,9 +7,9 @@ import { ...@@ -7,9 +7,9 @@ import {
GlSearchBoxByType, GlSearchBoxByType,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import Api from '~/api';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { SEARCH_DELAY } from '../constants'; import { getGroups, getDescendentGroups } from '~/rest_api';
import { SEARCH_DELAY, GROUP_FILTERS } from '../constants';
export default { export default {
name: 'GroupSelect', name: 'GroupSelect',
...@@ -23,6 +23,18 @@ export default { ...@@ -23,6 +23,18 @@ export default {
model: { model: {
prop: 'selectedGroup', prop: 'selectedGroup',
}, },
props: {
groupsFilter: {
type: String,
required: false,
default: GROUP_FILTERS.ALL,
},
parentGroupId: {
type: Number,
required: false,
default: null,
},
},
data() { data() {
return { return {
isFetching: false, isFetching: false,
...@@ -50,7 +62,7 @@ export default { ...@@ -50,7 +62,7 @@ export default {
methods: { methods: {
retrieveGroups: debounce(function debouncedRetrieveGroups() { retrieveGroups: debounce(function debouncedRetrieveGroups() {
this.isFetching = true; this.isFetching = true;
return Api.groups(this.searchTerm, this.$options.defaultFetchOptions) return this.fetchGroups()
.then((response) => { .then((response) => {
this.groups = response.map((group) => ({ this.groups = response.map((group) => ({
id: group.id, id: group.id,
...@@ -69,6 +81,18 @@ export default { ...@@ -69,6 +81,18 @@ export default {
this.$emit('input', this.selectedGroup); this.$emit('input', this.selectedGroup);
}, },
fetchGroups() {
switch (this.groupsFilter) {
case GROUP_FILTERS.DESCENDANT_GROUPS:
return getDescendentGroups(
this.parentGroupId,
this.searchTerm,
this.$options.defaultFetchOptions,
);
default:
return getGroups(this.searchTerm, this.$options.defaultFetchOptions);
}
},
}, },
i18n: { i18n: {
dropdownText: s__('GroupSelect|Select a group'), dropdownText: s__('GroupSelect|Select a group'),
......
...@@ -16,7 +16,7 @@ import GroupSelect from '~/invite_members/components/group_select.vue'; ...@@ -16,7 +16,7 @@ import GroupSelect from '~/invite_members/components/group_select.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { INVITE_MEMBERS_IN_COMMENT } from '../constants'; import { INVITE_MEMBERS_IN_COMMENT, GROUP_FILTERS } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
...@@ -54,6 +54,16 @@ export default { ...@@ -54,6 +54,16 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
groupSelectFilter: {
type: String,
required: false,
default: GROUP_FILTERS.ALL,
},
groupSelectParentId: {
type: Number,
required: false,
default: null,
},
helpLink: { helpLink: {
type: String, type: String,
required: true, required: true,
...@@ -293,7 +303,12 @@ export default { ...@@ -293,7 +303,12 @@ export default {
:aria-labelledby="$options.membersTokenSelectLabelId" :aria-labelledby="$options.membersTokenSelectLabelId"
:placeholder="$options.labels[inviteeType].placeHolder" :placeholder="$options.labels[inviteeType].placeHolder"
/> />
<group-select v-if="isInviteGroup" v-model="groupToBeSharedWith" /> <group-select
v-if="isInviteGroup"
v-model="groupToBeSharedWith"
:groups-filter="groupSelectFilter"
:parent-group-id="groupSelectParentId"
/>
</div> </div>
<label class="gl-font-weight-bold gl-mt-3">{{ $options.labels.accessLevel }}</label> <label class="gl-font-weight-bold gl-mt-3">{{ $options.labels.accessLevel }}</label>
......
export const SEARCH_DELAY = 200; export const SEARCH_DELAY = 200;
export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment'; export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment';
export const GROUP_FILTERS = {
ALL: 'all',
DESCENDANT_GROUPS: 'descendant_groups',
};
...@@ -21,6 +21,8 @@ export default function initInviteMembersModal() { ...@@ -21,6 +21,8 @@ export default function initInviteMembersModal() {
isProject: parseBoolean(el.dataset.isProject), isProject: parseBoolean(el.dataset.isProject),
accessLevels: JSON.parse(el.dataset.accessLevels), accessLevels: JSON.parse(el.dataset.accessLevels),
defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10), defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10),
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
}, },
}), }),
}); });
......
...@@ -31,4 +31,12 @@ module InviteMembersHelper ...@@ -31,4 +31,12 @@ module InviteMembersHelper
{ member_human_access: member.human_access, name: member.source.name } { member_human_access: member.human_access, name: member.source.name }
end end
end end
def group_select_data(group)
if group.root_ancestor.namespace_settings.prevent_sharing_groups_outside_hierarchy
{ groups_filter: 'descendant_groups', parent_id: group.root_ancestor.id }
else
{}
end
end
end end
...@@ -4,4 +4,4 @@ ...@@ -4,4 +4,4 @@
is_project: 'false', is_project: 'false',
access_levels: GroupMember.access_level_roles.to_json, access_levels: GroupMember.access_level_roles.to_json,
default_access_level: Gitlab::Access::GUEST, default_access_level: Gitlab::Access::GUEST,
help_link: help_page_url('user/permissions') } } help_link: help_page_url('user/permissions') }.merge(group_select_data(group)) }
- add_page_specific_style 'page_bundles/members' - add_page_specific_style 'page_bundles/members'
- page_title _('Group members') - page_title _('Group members')
- groups_select_tag_data = @group.root_ancestor.namespace_settings.prevent_sharing_groups_outside_hierarchy ? { groups_filter: 'descendant_groups', parent_id: @group.root_ancestor.id, skip_groups: @skip_groups } : { skip_groups: @skip_groups } - groups_select_tag_data = group_select_data(@group).merge({ skip_groups: @skip_groups })
.js-remove-member-modal .js-remove-member-modal
.row.gl-mt-3 .row.gl-mt-3
......
...@@ -170,6 +170,18 @@ RSpec.describe 'Groups > Members > Manage groups', :js do ...@@ -170,6 +170,18 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
expect(page).to have_text group_outside_hierarchy.name expect(page).to have_text group_outside_hierarchy.name
end end
end end
context 'when the invite members group modal is enabled' do
it 'shows groups within and outside the hierarchy in search results' do
visit group_group_members_path(group)
click_on 'Invite a group'
click_on 'Select a group'
expect(page).to have_text group_within_hierarchy.name
expect(page).to have_text group_outside_hierarchy.name
end
end
end end
context 'when sharing with groups outside the hierarchy is disabled' do context 'when sharing with groups outside the hierarchy is disabled' do
...@@ -192,6 +204,18 @@ RSpec.describe 'Groups > Members > Manage groups', :js do ...@@ -192,6 +204,18 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
expect(page).not_to have_text group_outside_hierarchy.name expect(page).not_to have_text group_outside_hierarchy.name
end end
end end
context 'when the invite members group modal is enabled' do
it 'shows only groups within the hierarchy in search results' do
visit group_group_members_path(group)
click_on 'Invite a group'
click_on 'Select a group'
expect(page).to have_text group_within_hierarchy.name
expect(page).not_to have_text group_outside_hierarchy.name
end
end
end end
end end
......
import { GlAvatarLabeled, GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; import { GlAvatarLabeled, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api'; import * as groupsApi from '~/api/groups_api';
import GroupSelect from '~/invite_members/components/group_select.vue'; import GroupSelect from '~/invite_members/components/group_select.vue';
const createComponent = () => { const createComponent = () => {
...@@ -16,7 +16,7 @@ describe('GroupSelect', () => { ...@@ -16,7 +16,7 @@ describe('GroupSelect', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
jest.spyOn(Api, 'groups').mockResolvedValue(allGroups); jest.spyOn(groupsApi, 'getGroups').mockResolvedValue(allGroups);
wrapper = createComponent(); wrapper = createComponent();
}); });
...@@ -45,7 +45,7 @@ describe('GroupSelect', () => { ...@@ -45,7 +45,7 @@ describe('GroupSelect', () => {
let resolveApiRequest; let resolveApiRequest;
beforeEach(() => { beforeEach(() => {
jest.spyOn(Api, 'groups').mockImplementation( jest.spyOn(groupsApi, 'getGroups').mockImplementation(
() => () =>
new Promise((resolve) => { new Promise((resolve) => {
resolveApiRequest = resolve; resolveApiRequest = resolve;
...@@ -58,7 +58,7 @@ describe('GroupSelect', () => { ...@@ -58,7 +58,7 @@ describe('GroupSelect', () => {
it('calls the API', () => { it('calls the API', () => {
resolveApiRequest({ data: allGroups }); resolveApiRequest({ data: allGroups });
expect(Api.groups).toHaveBeenCalledWith(group1.name, { expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
active: true, active: true,
exclude_internal: true, exclude_internal: true,
}); });
......
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