Commit 0df4a9db authored by Peter Hegman's avatar Peter Hegman Committed by Kushal Pandya

Add filter search bar to group members view

Improves filtering capability and moves toward Pajamas design system
parent f0afb27e
export default { export default {
issues: 'issue-recent-searches', issues: 'issue-recent-searches',
merge_requests: 'merge-request-recent-searches', merge_requests: 'merge-request-recent-searches',
group_members: 'group-members-recent-searches',
group_invited_members: 'group-invited-members-recent-searches',
}; };
...@@ -2,12 +2,15 @@ ...@@ -2,12 +2,15 @@
import { mapState, mapMutations } from 'vuex'; import { mapState, mapMutations } from 'vuex';
import { GlAlert } from '@gitlab/ui'; import { GlAlert } from '@gitlab/ui';
import MembersTable from '~/members/components/table/members_table.vue'; import MembersTable from '~/members/components/table/members_table.vue';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import { scrollToElement } from '~/lib/utils/common_utils'; import { scrollToElement } from '~/lib/utils/common_utils';
import { HIDE_ERROR } from '~/members/store/mutation_types'; import { HIDE_ERROR } from '~/members/store/mutation_types';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
name: 'GroupMembersApp', name: 'GroupMembersApp',
components: { MembersTable, GlAlert }, components: { MembersTable, FilterSortContainer, GlAlert },
mixins: [glFeatureFlagsMixin()],
computed: { computed: {
...mapState(['showError', 'errorMessage']), ...mapState(['showError', 'errorMessage']),
}, },
...@@ -33,6 +36,7 @@ export default { ...@@ -33,6 +36,7 @@ export default {
<gl-alert v-if="showError" ref="errorAlert" variant="danger" @dismiss="hideError">{{ <gl-alert v-if="showError" ref="errorAlert" variant="danger" @dismiss="hideError">{{
errorMessage errorMessage
}}</gl-alert> }}</gl-alert>
<filter-sort-container v-if="glFeatures.groupMembersFilteredSearch" />
<members-table /> <members-table />
</div> </div>
</template> </template>
<script>
import { mapState } from 'vuex';
import MembersFilteredSearchBar from './members_filtered_search_bar.vue';
export default {
name: 'FilterSortContainer',
components: { MembersFilteredSearchBar },
computed: {
...mapState(['filteredSearchBar']),
},
};
</script>
<template>
<div v-if="filteredSearchBar.show" class="gl-bg-gray-10 gl-p-5">
<members-filtered-search-bar />
</div>
</template>
<script>
import { mapState } from 'vuex';
import { GlFilteredSearchToken } from '@gitlab/ui';
import { setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { getParameterByName } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { SEARCH_TOKEN_TYPE, SORT_PARAM } from '~/members/constants';
export default {
name: 'MembersFilteredSearchBar',
components: { FilteredSearchBar },
availableTokens: [
{
type: 'two_factor',
icon: 'lock',
title: s__('Members|2FA'),
token: GlFilteredSearchToken,
unique: true,
operators: [{ value: '=', description: 'is' }],
options: [
{ value: 'enabled', title: s__('Members|Enabled') },
{ value: 'disabled', title: s__('Members|Disabled') },
],
requiredPermissions: 'canManageMembers',
},
{
type: 'with_inherited_permissions',
icon: 'group',
title: s__('Members|Membership'),
token: GlFilteredSearchToken,
unique: true,
operators: [{ value: '=', description: 'is' }],
options: [
{ value: 'exclude', title: s__('Members|Direct') },
{ value: 'only', title: s__('Members|Inherited') },
],
},
],
data() {
return {
initialFilterValue: [],
};
},
computed: {
...mapState(['sourceId', 'filteredSearchBar', 'canManageMembers']),
tokens() {
return this.$options.availableTokens.filter(token => {
if (
Object.prototype.hasOwnProperty.call(token, 'requiredPermissions') &&
!this[token.requiredPermissions]
) {
return false;
}
return this.filteredSearchBar.tokens?.includes(token.type);
});
},
},
created() {
const query = queryToObject(window.location.search);
const tokens = this.tokens
.filter(token => query[token.type])
.map(token => ({
type: token.type,
value: {
data: query[token.type],
operator: '=',
},
}));
if (query[this.filteredSearchBar.searchParam]) {
tokens.push({
type: SEARCH_TOKEN_TYPE,
value: {
data: query[this.filteredSearchBar.searchParam],
},
});
}
this.initialFilterValue = tokens;
},
methods: {
handleFilter(tokens) {
const params = tokens.reduce((accumulator, token) => {
const { type, value } = token;
if (!type || !value) {
return accumulator;
}
if (type === SEARCH_TOKEN_TYPE) {
if (value.data !== '') {
return {
...accumulator,
[this.filteredSearchBar.searchParam]: value.data,
};
}
} else {
return {
...accumulator,
[type]: value.data,
};
}
return accumulator;
}, {});
const sortParam = getParameterByName(SORT_PARAM);
window.location.href = setUrlParams(
{ ...params, ...(sortParam && { sort: sortParam }) },
window.location.href,
true,
);
},
},
};
</script>
<template>
<filtered-search-bar
:namespace="sourceId.toString()"
:tokens="tokens"
:recent-searches-storage-key="filteredSearchBar.recentSearchesStorageKey"
:search-input-placeholder="filteredSearchBar.placeholder"
:initial-filter-value="initialFilterValue"
data-testid="members-filtered-search-bar"
@onFilter="handleFilter"
/>
</template>
...@@ -69,3 +69,7 @@ export const DAYS_TO_EXPIRE_SOON = 7; ...@@ -69,3 +69,7 @@ export const DAYS_TO_EXPIRE_SOON = 7;
export const LEAVE_MODAL_ID = 'member-leave-modal'; export const LEAVE_MODAL_ID = 'member-leave-modal';
export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id'; export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id';
export const SEARCH_TOKEN_TYPE = 'filtered-search-term';
export const SORT_PARAM = 'sort';
...@@ -6,7 +6,7 @@ import groupsSelect from '~/groups_select'; ...@@ -6,7 +6,7 @@ import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import { initGroupMembersApp } from '~/groups/members'; import { initGroupMembersApp } from '~/groups/members';
import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils'; import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils';
import { __ } from '~/locale'; import { s__ } from '~/locale';
function mountRemoveMemberModal() { function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal'); const el = document.querySelector('.js-remove-member-modal');
...@@ -33,7 +33,7 @@ initGroupMembersApp(document.querySelector('.js-group-members-list'), { ...@@ -33,7 +33,7 @@ initGroupMembersApp(document.querySelector('.js-group-members-list'), {
show: true, show: true,
tokens: ['two_factor', 'with_inherited_permissions'], tokens: ['two_factor', 'with_inherited_permissions'],
searchParam: 'search', searchParam: 'search',
placeholder: __('Members|Filter members'), placeholder: s__('Members|Filter members'),
recentSearchesStorageKey: 'group_members', recentSearchesStorageKey: 'group_members',
}, },
}); });
...@@ -52,7 +52,7 @@ initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), { ...@@ -52,7 +52,7 @@ initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), {
show: true, show: true,
tokens: [], tokens: [],
searchParam: 'search_invited', searchParam: 'search_invited',
placeholder: __('Members|Search invited'), placeholder: s__('Members|Search invited'),
recentSearchesStorageKey: 'group_invited_members', recentSearchesStorageKey: 'group_invited_members',
}, },
}); });
......
...@@ -14,6 +14,10 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -14,6 +14,10 @@ class Groups::GroupMembersController < Groups::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_group_member!, except: admin_not_required_endpoints before_action :authorize_admin_group_member!, except: admin_not_required_endpoints
before_action do
push_frontend_feature_flag(:group_members_filtered_search, @group)
end
skip_before_action :check_two_factor_requirement, only: :leave skip_before_action :check_two_factor_requirement, only: :leave
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access, skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite, :approve_access_request, :leave, :resend_invite,
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
- show_access_requests = can_manage_members && @requesters.exists? - show_access_requests = can_manage_members && @requesters.exists?
- invited_active = params[:search_invited].present? || params[:invited_members_page].present? - invited_active = params[:search_invited].present? || params[:invited_members_page].present?
- vue_members_list_enabled = Feature.enabled?(:vue_group_members_list, @group, default_enabled: true) - vue_members_list_enabled = Feature.enabled?(:vue_group_members_list, @group, default_enabled: true)
- filtered_search_enabled = Feature.enabled?(:group_members_filtered_search, @group)
- current_user_is_group_owner = @group && @group.has_owner?(current_user) - current_user_is_group_owner = @group && @group.has_owner?(current_user)
- form_item_label_css_class = 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center' - form_item_label_css_class = 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center'
...@@ -54,20 +55,21 @@ ...@@ -54,20 +55,21 @@
.tab-content .tab-content
#tab-members.tab-pane{ class: ('active' unless invited_active) } #tab-members.tab-pane{ class: ('active' unless invited_active) }
.card.card-without-border .card.card-without-border
= render 'groups/group_members/tab_pane/header' do - unless filtered_search_enabled
= render 'groups/group_members/tab_pane/title' do = render 'groups/group_members/tab_pane/header' do
= html_escape(_('Members with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } = render 'groups/group_members/tab_pane/title' do
= form_tag group_group_members_path(@group), method: :get, class: 'user-search-form gl-display-flex gl-md-align-items-center gl-flex-wrap gl-flex-direction-column gl-md-flex-direction-row gl-mx-n3 gl-my-n3', data: { testid: 'user-search-form' } do = html_escape(_('Members with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
.gl-px-3.gl-py-2 = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form gl-display-flex gl-md-align-items-center gl-flex-wrap gl-flex-direction-column gl-md-flex-direction-row gl-mx-n3 gl-my-n3', data: { testid: 'user-search-form' } do
.search-control-wrap.gl-relative .gl-px-3.gl-py-2
= render 'shared/members/search_field' .search-control-wrap.gl-relative
- if can_manage_members = render 'shared/members/search_field'
- if can_manage_members
= render 'groups/group_members/tab_pane/form_item' do
= label_tag '2fa', _('2FA'), class: form_item_label_css_class
= render 'shared/members/filter_2fa_dropdown'
= render 'groups/group_members/tab_pane/form_item' do = render 'groups/group_members/tab_pane/form_item' do
= label_tag '2fa', _('2FA'), class: form_item_label_css_class = label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
= render 'shared/members/filter_2fa_dropdown' = render 'shared/members/sort_dropdown'
= render 'groups/group_members/tab_pane/form_item' do
= label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
= render 'shared/members/sort_dropdown'
- if vue_members_list_enabled - if vue_members_list_enabled
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) } .js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
.loading .loading
...@@ -83,9 +85,10 @@ ...@@ -83,9 +85,10 @@
- if @group.shared_with_group_links.any? - if @group.shared_with_group_links.any?
#tab-groups.tab-pane #tab-groups.tab-pane
.card.card-without-border .card.card-without-border
= render 'groups/group_members/tab_pane/header' do - unless filtered_search_enabled
= render 'groups/group_members/tab_pane/title' do = render 'groups/group_members/tab_pane/header' do
= html_escape(_('Groups with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } = render 'groups/group_members/tab_pane/title' do
= html_escape(_('Groups with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- if vue_members_list_enabled - if vue_members_list_enabled
.js-group-linked-list{ data: linked_groups_list_data_attributes(@group) } .js-group-linked-list{ data: linked_groups_list_data_attributes(@group) }
.loading .loading
...@@ -97,11 +100,12 @@ ...@@ -97,11 +100,12 @@
- if show_invited_members - if show_invited_members
#tab-invited-members.tab-pane{ class: ('active' if invited_active) } #tab-invited-members.tab-pane{ class: ('active' if invited_active) }
.card.card-without-border .card.card-without-border
= render 'groups/group_members/tab_pane/header' do - unless filtered_search_enabled
= render 'groups/group_members/tab_pane/title' do = render 'groups/group_members/tab_pane/header' do
= html_escape(_('Members invited to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } = render 'groups/group_members/tab_pane/title' do
= form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do = html_escape(_('Members invited to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
= render 'shared/members/search_field', name: 'search_invited' = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do
= render 'shared/members/search_field', name: 'search_invited'
- if vue_members_list_enabled - if vue_members_list_enabled
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) } .js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) }
.loading .loading
...@@ -117,9 +121,10 @@ ...@@ -117,9 +121,10 @@
- if show_access_requests - if show_access_requests
#tab-access-requests.tab-pane #tab-access-requests.tab-pane
.card.card-without-border .card.card-without-border
= render 'groups/group_members/tab_pane/header' do - unless filtered_search_enabled
= render 'groups/group_members/tab_pane/title' do = render 'groups/group_members/tab_pane/header' do
= html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } = render 'groups/group_members/tab_pane/title' do
= html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- if vue_members_list_enabled - if vue_members_list_enabled
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) } .js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
.loading .loading
......
---
name: group_members_filtered_search
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48272
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/289911
milestone: '13.7'
type: development
group: group::access
default_enabled: false
...@@ -16842,6 +16842,9 @@ msgstr "" ...@@ -16842,6 +16842,9 @@ msgstr ""
msgid "Members|%{userName} is currently an LDAP user. Editing their permissions will override the settings from the LDAP group sync." msgid "Members|%{userName} is currently an LDAP user. Editing their permissions will override the settings from the LDAP group sync."
msgstr "" msgstr ""
msgid "Members|2FA"
msgstr ""
msgid "Members|An error occurred while trying to enable LDAP override, please try again." msgid "Members|An error occurred while trying to enable LDAP override, please try again."
msgstr "" msgstr ""
...@@ -16875,9 +16878,18 @@ msgstr "" ...@@ -16875,9 +16878,18 @@ msgstr ""
msgid "Members|Are you sure you want to withdraw your access request for \"%{source}\"" msgid "Members|Are you sure you want to withdraw your access request for \"%{source}\""
msgstr "" msgstr ""
msgid "Members|Direct"
msgstr ""
msgid "Members|Disabled"
msgstr ""
msgid "Members|Edit permissions" msgid "Members|Edit permissions"
msgstr "" msgstr ""
msgid "Members|Enabled"
msgstr ""
msgid "Members|Expiration date removed successfully." msgid "Members|Expiration date removed successfully."
msgstr "" msgstr ""
...@@ -16890,12 +16902,18 @@ msgstr "" ...@@ -16890,12 +16902,18 @@ msgstr ""
msgid "Members|Filter members" msgid "Members|Filter members"
msgstr "" msgstr ""
msgid "Members|Inherited"
msgstr ""
msgid "Members|LDAP override enabled." msgid "Members|LDAP override enabled."
msgstr "" msgstr ""
msgid "Members|Leave \"%{source}\"" msgid "Members|Leave \"%{source}\""
msgstr "" msgstr ""
msgid "Members|Membership"
msgstr ""
msgid "Members|No expiration set" msgid "Members|No expiration set"
msgstr "" msgstr ""
......
...@@ -11,8 +11,7 @@ RSpec.describe 'Groups > Members > Filter members', :js do ...@@ -11,8 +11,7 @@ RSpec.describe 'Groups > Members > Filter members', :js do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) } let(:nested_group) { create(:group, parent: group) }
two_factor_auth_dropdown_toggle_selector = '[data-testid="member-filter-2fa-dropdown"] [data-testid="dropdown-toggle"]' filtered_search_bar_selector = '[data-testid="members-filtered-search-bar"]'
active_inherited_members_filter_selector = '[data-testid="filter-members-with-inherited-permissions"] a.is-active'
before do before do
group.add_owner(user) group.add_owner(user)
...@@ -27,7 +26,6 @@ RSpec.describe 'Groups > Members > Filter members', :js do ...@@ -27,7 +26,6 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user.name) expect(member(0)).to include(user.name)
expect(member(1)).to include(user_with_2fa.name) expect(member(1)).to include(user_with_2fa.name)
expect(page).to have_css(two_factor_auth_dropdown_toggle_selector, text: 'Everyone')
end end
it 'shows only 2FA members' do it 'shows only 2FA members' do
...@@ -35,7 +33,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do ...@@ -35,7 +33,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user_with_2fa.name) expect(member(0)).to include(user_with_2fa.name)
expect(all_rows.size).to eq(1) expect(all_rows.size).to eq(1)
expect(page).to have_css(two_factor_auth_dropdown_toggle_selector, text: 'Enabled')
within filtered_search_bar_selector do
expect(page).to have_content '2FA = Enabled'
end
end end
it 'shows only non 2FA members' do it 'shows only non 2FA members' do
...@@ -43,7 +44,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do ...@@ -43,7 +44,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user.name) expect(member(0)).to include(user.name)
expect(all_rows.size).to eq(1) expect(all_rows.size).to eq(1)
expect(page).to have_css(two_factor_auth_dropdown_toggle_selector, text: 'Disabled')
within filtered_search_bar_selector do
expect(page).to have_content '2FA = Disabled'
end
end end
it 'shows inherited members by default' do it 'shows inherited members by default' do
...@@ -53,15 +57,16 @@ RSpec.describe 'Groups > Members > Filter members', :js do ...@@ -53,15 +57,16 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(1)).to include(user_with_2fa.name) expect(member(1)).to include(user_with_2fa.name)
expect(member(2)).to include(nested_group_user.name) expect(member(2)).to include(nested_group_user.name)
expect(all_rows.size).to eq(3) expect(all_rows.size).to eq(3)
expect(page).to have_css(active_inherited_members_filter_selector, text: 'Show all members', visible: false)
end end
it 'shows only group members' do it 'shows only group members' do
visit_members_list(nested_group, with_inherited_permissions: 'exclude') visit_members_list(nested_group, with_inherited_permissions: 'exclude')
expect(member(0)).to include(nested_group_user.name) expect(member(0)).to include(nested_group_user.name)
expect(all_rows.size).to eq(1) expect(all_rows.size).to eq(1)
expect(page).to have_css(active_inherited_members_filter_selector, text: 'Show only direct members', visible: false)
within filtered_search_bar_selector do
expect(page).to have_content 'Membership = Direct'
end
end end
it 'shows only inherited members' do it 'shows only inherited members' do
...@@ -69,7 +74,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do ...@@ -69,7 +74,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user.name) expect(member(0)).to include(user.name)
expect(member(1)).to include(user_with_2fa.name) expect(member(1)).to include(user_with_2fa.name)
expect(all_rows.size).to eq(2) expect(all_rows.size).to eq(2)
expect(page).to have_css(active_inherited_members_filter_selector, text: 'Show only inherited members', visible: false)
within filtered_search_bar_selector do
expect(page).to have_content 'Membership = Inherited'
end
end end
def visit_members_list(group, options = {}) def visit_members_list(group, options = {})
......
...@@ -21,9 +21,10 @@ RSpec.describe 'Search group member', :js do ...@@ -21,9 +21,10 @@ RSpec.describe 'Search group member', :js do
end end
it 'renders member users' do it 'renders member users' do
page.within '[data-testid="user-search-form"]' do page.within '[data-testid="members-filtered-search-bar"]' do
fill_in 'search', with: member.name find_field('Filter members').click
find('[data-testid="user-search-submit"]').click find('input').native.send_keys(member.name)
click_button 'Search'
end end
expect(members_table).to have_content(member.name) expect(members_table).to have_content(member.name)
......
...@@ -12,6 +12,8 @@ RSpec.describe 'Groups > Members > Sort members', :js do ...@@ -12,6 +12,8 @@ RSpec.describe 'Groups > Members > Sort members', :js do
dropdown_toggle_selector = '[data-testid="user-sort-dropdown"] [data-testid="dropdown-toggle"]' dropdown_toggle_selector = '[data-testid="user-sort-dropdown"] [data-testid="dropdown-toggle"]'
before do before do
stub_feature_flags(group_members_filtered_search: false)
create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago) create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago)
create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago) create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago)
......
...@@ -62,9 +62,10 @@ RSpec.describe 'Groups > Members > Tabs' do ...@@ -62,9 +62,10 @@ RSpec.describe 'Groups > Members > Tabs' do
click_link 'Invited' click_link 'Invited'
page.within '[data-testid="user-search-form"]' do page.within '[data-testid="members-filtered-search-bar"]' do
fill_in 'search_invited', with: 'email' find_field('Search invited').click
find('button[type="submit"]').click find('input').native.send_keys('email')
click_button 'Search'
end end
end end
...@@ -74,9 +75,10 @@ RSpec.describe 'Groups > Members > Tabs' do ...@@ -74,9 +75,10 @@ RSpec.describe 'Groups > Members > Tabs' do
before do before do
click_link 'Members' click_link 'Members'
page.within '[data-testid="user-search-form"]' do page.within '[data-testid="members-filtered-search-bar"]' do
fill_in 'search', with: 'test' find_field('Filter members').click
find('button[type="submit"]').click find('input').native.send_keys('test')
click_button 'Search'
end end
end end
......
...@@ -3,6 +3,7 @@ import { nextTick } from 'vue'; ...@@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { GlAlert } from '@gitlab/ui'; import { GlAlert } from '@gitlab/ui';
import App from '~/groups/members/components/app.vue'; import App from '~/groups/members/components/app.vue';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import * as commonUtils from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types'; import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
import mutations from '~/members/store/mutations'; import mutations from '~/members/store/mutations';
...@@ -14,7 +15,7 @@ describe('GroupMembersApp', () => { ...@@ -14,7 +15,7 @@ describe('GroupMembersApp', () => {
let wrapper; let wrapper;
let store; let store;
const createComponent = (state = {}) => { const createComponent = (state = {}, options = {}) => {
store = new Vuex.Store({ store = new Vuex.Store({
state: { state: {
showError: true, showError: true,
...@@ -27,10 +28,12 @@ describe('GroupMembersApp', () => { ...@@ -27,10 +28,12 @@ describe('GroupMembersApp', () => {
wrapper = shallowMount(App, { wrapper = shallowMount(App, {
localVue, localVue,
store, store,
...options,
}); });
}; };
const findAlert = () => wrapper.find(GlAlert); const findAlert = () => wrapper.find(GlAlert);
const findFilterSortContainer = () => wrapper.find(FilterSortContainer);
beforeEach(() => { beforeEach(() => {
commonUtils.scrollToElement = jest.fn(); commonUtils.scrollToElement = jest.fn();
...@@ -83,4 +86,22 @@ describe('GroupMembersApp', () => { ...@@ -83,4 +86,22 @@ describe('GroupMembersApp', () => {
expect(findAlert().exists()).toBe(false); expect(findAlert().exists()).toBe(false);
}); });
}); });
describe.each`
featureFlagValue | exists
${true} | ${true}
${false} | ${false}
`(
'when `group_members_filtered_search` feature flag is $featureFlagValue',
({ featureFlagValue, exists }) => {
it(`${exists ? 'renders' : 'does not render'} FilterSortContainer`, () => {
createComponent(
{},
{ provide: { glFeatures: { groupMembersFilteredSearch: featureFlagValue } } },
);
expect(findFilterSortContainer().exists()).toBe(exists);
});
},
);
}); });
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('FilterSortContainer', () => {
let wrapper;
const createComponent = state => {
const store = new Vuex.Store({
state: {
filteredSearchBar: {
show: true,
tokens: ['two_factor'],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
},
...state,
},
});
wrapper = shallowMount(FilterSortContainer, {
localVue,
store,
});
};
describe('when `filteredSearchBar.show` is `false`', () => {
it('renders nothing', () => {
createComponent({
filteredSearchBar: {
show: false,
},
});
expect(wrapper.html()).toBe('');
});
});
describe('when `filteredSearchBar.show` is `true`', () => {
it('renders `MembersFilteredSearchBar`', () => {
createComponent({
filteredSearchBar: {
show: true,
},
});
expect(wrapper.find(MembersFilteredSearchBar).exists()).toBe(true);
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlFilteredSearchToken } from '@gitlab/ui';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('MembersFilteredSearchBar', () => {
let wrapper;
const createComponent = state => {
const store = new Vuex.Store({
state: {
sourceId: 1,
filteredSearchBar: {
show: true,
tokens: ['two_factor'],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
},
canManageMembers: true,
...state,
},
});
wrapper = shallowMount(MembersFilteredSearchBar, {
localVue,
store,
});
};
const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar);
it('passes correct props to `FilteredSearchBar` component', () => {
createComponent();
expect(findFilteredSearchBar().props()).toMatchObject({
namespace: '1',
recentSearchesStorageKey: 'group_members',
searchInputPlaceholder: 'Filter members',
});
});
describe('filtering tokens', () => {
it('includes tokens set in `filteredSearchBar.tokens`', () => {
createComponent();
expect(findFilteredSearchBar().props('tokens')).toEqual([
{
type: 'two_factor',
icon: 'lock',
title: '2FA',
token: GlFilteredSearchToken,
unique: true,
operators: [{ value: '=', description: 'is' }],
options: [
{ value: 'enabled', title: 'Enabled' },
{ value: 'disabled', title: 'Disabled' },
],
requiredPermissions: 'canManageMembers',
},
]);
});
describe('when `canManageMembers` is false', () => {
it('excludes 2FA token', () => {
createComponent({
filteredSearchBar: {
show: true,
tokens: ['two_factor', 'with_inherited_permissions'],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
},
canManageMembers: false,
});
expect(findFilteredSearchBar().props('tokens')).toEqual([
{
type: 'with_inherited_permissions',
icon: 'group',
title: 'Membership',
token: GlFilteredSearchToken,
unique: true,
operators: [{ value: '=', description: 'is' }],
options: [{ value: 'exclude', title: 'Direct' }, { value: 'only', title: 'Inherited' }],
},
]);
});
});
});
describe('when filters are set via query params', () => {
beforeEach(() => {
delete window.location;
window.location = new URL('https://localhost');
});
it('parses and passes tokens to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
window.location.search = '?two_factor=enabled&token_not_available=foobar';
createComponent();
expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([
{
type: 'two_factor',
value: {
data: 'enabled',
operator: '=',
},
},
]);
});
it('parses and passes search param to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
window.location.search = '?search=foobar';
createComponent();
expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([
{
type: 'filtered-search-term',
value: {
data: 'foobar',
},
},
]);
});
});
describe('when filter bar is submitted', () => {
beforeEach(() => {
delete window.location;
window.location = new URL('https://localhost');
});
it('adds correct filter query params', () => {
createComponent();
findFilteredSearchBar().vm.$emit('onFilter', [
{ type: 'two_factor', value: { data: 'enabled', operator: '=' } },
]);
expect(window.location.href).toBe('https://localhost/?two_factor=enabled');
});
it('adds search query param', () => {
createComponent();
findFilteredSearchBar().vm.$emit('onFilter', [
{ type: 'two_factor', value: { data: 'enabled', operator: '=' } },
{ type: 'filtered-search-term', value: { data: 'foobar' } },
]);
expect(window.location.href).toBe('https://localhost/?two_factor=enabled&search=foobar');
});
it('adds sort query param', () => {
window.location.search = '?sort=name_asc';
createComponent();
findFilteredSearchBar().vm.$emit('onFilter', [
{ type: 'two_factor', value: { data: 'enabled', operator: '=' } },
{ type: 'filtered-search-term', value: { data: 'foobar' } },
]);
expect(window.location.href).toBe(
'https://localhost/?two_factor=enabled&search=foobar&sort=name_asc',
);
});
});
});
...@@ -50,7 +50,6 @@ RSpec.shared_examples 'Maintainer manages access requests' do ...@@ -50,7 +50,6 @@ RSpec.shared_examples 'Maintainer manages access requests' do
def expect_visible_access_request(entity, user) def expect_visible_access_request(entity, user)
if has_tabs if has_tabs
expect(page).to have_content "Access requests 1" expect(page).to have_content "Access requests 1"
expect(page).to have_content "Users requesting access to #{entity.name}"
else else
expect(page).to have_content "Users requesting access to #{entity.name} 1" expect(page).to have_content "Users requesting access to #{entity.name} 1"
end end
......
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