Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-03 06:09:47 +00:00
parent e1e9056d03
commit 9214e550c0
22 changed files with 549 additions and 76 deletions

View file

@ -1,4 +1,6 @@
export default {
issues: 'issue-recent-searches',
merge_requests: 'merge-request-recent-searches',
group_members: 'group-members-recent-searches',
group_invited_members: 'group-invited-members-recent-searches',
};

View file

@ -2,12 +2,15 @@
import { mapState, mapMutations } from 'vuex';
import { GlAlert } from '@gitlab/ui';
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 { HIDE_ERROR } from '~/members/store/mutation_types';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'GroupMembersApp',
components: { MembersTable, GlAlert },
components: { MembersTable, FilterSortContainer, GlAlert },
mixins: [glFeatureFlagsMixin()],
computed: {
...mapState(['showError', 'errorMessage']),
},
@ -33,6 +36,7 @@ export default {
<gl-alert v-if="showError" ref="errorAlert" variant="danger" @dismiss="hideError">{{
errorMessage
}}</gl-alert>
<filter-sort-container v-if="glFeatures.groupMembersFilteredSearch" />
<members-table />
</div>
</template>

View file

@ -1,11 +1,19 @@
<script>
import { GlLink } from '@gitlab/ui';
import { GlLink, GlAlert } from '@gitlab/ui';
import { __, s__ } from '~/locale';
/**
* Renders Unmet Prerequisites block for job's view.
*/
export default {
i18n: {
failMessage: s__(
'Job|This job failed because the necessary resources were not successfully created.',
),
moreInformation: __('More information'),
},
components: {
GlLink,
GlAlert,
},
props: {
helpPath: {
@ -16,15 +24,10 @@ export default {
};
</script>
<template>
<div class="bs-callout bs-callout-danger">
<p class="js-failed-unmet-prerequisites gl-mb-0">
{{
s__(`Job|This job failed because the necessary resources were not successfully created.`)
}}
<gl-link :href="helpPath" class="js-help-path">
<strong> {{ __('More information') }} </strong>
</gl-link>
</p>
</div>
<gl-alert variant="danger" class="gl-mt-3" :dismissible="false">
{{ $options.i18n.failMessage }}
<gl-link :href="helpPath">
{{ $options.i18n.moreInformation }}
</gl-link>
</gl-alert>
</template>

View file

@ -0,0 +1,18 @@
<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>

View file

@ -0,0 +1,132 @@
<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>

View file

@ -69,3 +69,7 @@ export const DAYS_TO_EXPIRE_SOON = 7;
export const LEAVE_MODAL_ID = 'member-leave-modal';
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';

View file

@ -6,7 +6,7 @@ import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import { initGroupMembersApp } from '~/groups/members';
import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils';
import { __ } from '~/locale';
import { s__ } from '~/locale';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
@ -33,7 +33,7 @@ initGroupMembersApp(document.querySelector('.js-group-members-list'), {
show: true,
tokens: ['two_factor', 'with_inherited_permissions'],
searchParam: 'search',
placeholder: __('Members|Filter members'),
placeholder: s__('Members|Filter members'),
recentSearchesStorageKey: 'group_members',
},
});
@ -52,7 +52,7 @@ initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), {
show: true,
tokens: [],
searchParam: 'search_invited',
placeholder: __('Members|Search invited'),
placeholder: s__('Members|Search invited'),
recentSearchesStorageKey: 'group_invited_members',
},
});

View file

@ -14,6 +14,10 @@ class Groups::GroupMembersController < Groups::ApplicationController
# Authorize
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_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,

View file

@ -4,6 +4,7 @@
- show_access_requests = can_manage_members && @requesters.exists?
- 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)
- filtered_search_enabled = Feature.enabled?(:group_members_filtered_search, @group)
- 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'
@ -54,20 +55,21 @@
.tab-content
#tab-members.tab-pane{ class: ('active' unless invited_active) }
.card.card-without-border
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' 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 }
= 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
.gl-px-3.gl-py-2
.search-control-wrap.gl-relative
= render 'shared/members/search_field'
- if can_manage_members
- unless filtered_search_enabled
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' 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 }
= 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
.gl-px-3.gl-py-2
.search-control-wrap.gl-relative
= 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
= 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
= label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
= render 'shared/members/sort_dropdown'
= label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
= render 'shared/members/sort_dropdown'
- if vue_members_list_enabled
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
.loading
@ -83,9 +85,10 @@
- if @group.shared_with_group_links.any?
#tab-groups.tab-pane
.card.card-without-border
= render 'groups/group_members/tab_pane/header' do
= 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 }
- unless filtered_search_enabled
= render 'groups/group_members/tab_pane/header' do
= 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
.js-group-linked-list{ data: linked_groups_list_data_attributes(@group) }
.loading
@ -97,11 +100,12 @@
- if show_invited_members
#tab-invited-members.tab-pane{ class: ('active' if invited_active) }
.card.card-without-border
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' 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 }
= 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'
- unless filtered_search_enabled
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' 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 }
= 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
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) }
.loading
@ -117,9 +121,10 @@
- if show_access_requests
#tab-access-requests.tab-pane
.card.card-without-border
= render 'groups/group_members/tab_pane/header' do
= 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 }
- unless filtered_search_enabled
= render 'groups/group_members/tab_pane/header' do
= 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
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
.loading

View file

@ -0,0 +1,5 @@
---
title: Migrate bs-callout to GlAlert in …/unmet_prerequisites_block.vue
merge_request: 48398
author:
type: other

View file

@ -0,0 +1,8 @@
---
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

View file

@ -68,7 +68,9 @@ If you are writing your own endpoint (web server) to receive
GitLab webhooks, keep in mind the following things:
- Your endpoint should send its HTTP response as fast as possible. If
you wait too long, GitLab may decide the hook failed and retry it.
you wait too long (by default, a timeout of 10 seconds), GitLab may decide
the hook failed and retry it. You can configure this timeout with
`gitlab_rails['webhook_timeout']`.
- Your endpoint should ALWAYS return a valid HTTP response. If you do
not do this then GitLab thinks the hook failed and retries it.
Most HTTP libraries take care of this for you automatically but if

View file

@ -16864,6 +16864,9 @@ msgstr ""
msgid "Members|%{userName} is currently an LDAP user. Editing their permissions will override the settings from the LDAP group sync."
msgstr ""
msgid "Members|2FA"
msgstr ""
msgid "Members|An error occurred while trying to enable LDAP override, please try again."
msgstr ""
@ -16897,9 +16900,18 @@ msgstr ""
msgid "Members|Are you sure you want to withdraw your access request for \"%{source}\""
msgstr ""
msgid "Members|Direct"
msgstr ""
msgid "Members|Disabled"
msgstr ""
msgid "Members|Edit permissions"
msgstr ""
msgid "Members|Enabled"
msgstr ""
msgid "Members|Expiration date removed successfully."
msgstr ""
@ -16912,12 +16924,18 @@ msgstr ""
msgid "Members|Filter members"
msgstr ""
msgid "Members|Inherited"
msgstr ""
msgid "Members|LDAP override enabled."
msgstr ""
msgid "Members|Leave \"%{source}\""
msgstr ""
msgid "Members|Membership"
msgstr ""
msgid "Members|No expiration set"
msgstr ""

View file

@ -11,8 +11,7 @@ RSpec.describe 'Groups > Members > Filter members', :js do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
two_factor_auth_dropdown_toggle_selector = '[data-testid="member-filter-2fa-dropdown"] [data-testid="dropdown-toggle"]'
active_inherited_members_filter_selector = '[data-testid="filter-members-with-inherited-permissions"] a.is-active'
filtered_search_bar_selector = '[data-testid="members-filtered-search-bar"]'
before do
group.add_owner(user)
@ -27,7 +26,6 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user.name)
expect(member(1)).to include(user_with_2fa.name)
expect(page).to have_css(two_factor_auth_dropdown_toggle_selector, text: 'Everyone')
end
it 'shows only 2FA members' do
@ -35,7 +33,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user_with_2fa.name)
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
it 'shows only non 2FA members' do
@ -43,7 +44,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user.name)
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
it 'shows inherited members by default' do
@ -53,15 +57,16 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(1)).to include(user_with_2fa.name)
expect(member(2)).to include(nested_group_user.name)
expect(all_rows.size).to eq(3)
expect(page).to have_css(active_inherited_members_filter_selector, text: 'Show all members', visible: false)
end
it 'shows only group members' do
visit_members_list(nested_group, with_inherited_permissions: 'exclude')
expect(member(0)).to include(nested_group_user.name)
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
it 'shows only inherited members' do
@ -69,7 +74,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user.name)
expect(member(1)).to include(user_with_2fa.name)
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
def visit_members_list(group, options = {})

View file

@ -21,9 +21,10 @@ RSpec.describe 'Search group member', :js do
end
it 'renders member users' do
page.within '[data-testid="user-search-form"]' do
fill_in 'search', with: member.name
find('[data-testid="user-search-submit"]').click
page.within '[data-testid="members-filtered-search-bar"]' do
find_field('Filter members').click
find('input').native.send_keys(member.name)
click_button 'Search'
end
expect(members_table).to have_content(member.name)

View file

@ -12,6 +12,8 @@ RSpec.describe 'Groups > Members > Sort members', :js do
dropdown_toggle_selector = '[data-testid="user-sort-dropdown"] [data-testid="dropdown-toggle"]'
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, :developer, user: developer, group: group, created_at: 3.days.ago)

View file

@ -62,9 +62,10 @@ RSpec.describe 'Groups > Members > Tabs' do
click_link 'Invited'
page.within '[data-testid="user-search-form"]' do
fill_in 'search_invited', with: 'email'
find('button[type="submit"]').click
page.within '[data-testid="members-filtered-search-bar"]' do
find_field('Search invited').click
find('input').native.send_keys('email')
click_button 'Search'
end
end
@ -74,9 +75,10 @@ RSpec.describe 'Groups > Members > Tabs' do
before do
click_link 'Members'
page.within '[data-testid="user-search-form"]' do
fill_in 'search', with: 'test'
find('button[type="submit"]').click
page.within '[data-testid="members-filtered-search-bar"]' do
find_field('Filter members').click
find('input').native.send_keys('test')
click_button 'Search'
end
end

View file

@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import Vuex from 'vuex';
import { GlAlert } from '@gitlab/ui';
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 { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
import mutations from '~/members/store/mutations';
@ -14,7 +15,7 @@ describe('GroupMembersApp', () => {
let wrapper;
let store;
const createComponent = (state = {}) => {
const createComponent = (state = {}, options = {}) => {
store = new Vuex.Store({
state: {
showError: true,
@ -27,10 +28,12 @@ describe('GroupMembersApp', () => {
wrapper = shallowMount(App, {
localVue,
store,
...options,
});
};
const findAlert = () => wrapper.find(GlAlert);
const findFilterSortContainer = () => wrapper.find(FilterSortContainer);
beforeEach(() => {
commonUtils.scrollToElement = jest.fn();
@ -83,4 +86,22 @@ describe('GroupMembersApp', () => {
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);
});
},
);
});

View file

@ -1,37 +1,41 @@
import Vue from 'vue';
import component from '~/jobs/components/unmet_prerequisites_block.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlLink } from '@gitlab/ui';
import UnmetPrerequisitesBlock from '~/jobs/components/unmet_prerequisites_block.vue';
describe('Unmet Prerequisites Block Job component', () => {
const Component = Vue.extend(component);
let vm;
let wrapper;
const helpPath = '/user/project/clusters/index.html#troubleshooting-failed-deployment-jobs';
beforeEach(() => {
vm = mountComponent(Component, {
hasNoRunnersForProject: true,
helpPath,
const createComponent = () => {
wrapper = shallowMount(UnmetPrerequisitesBlock, {
propsData: {
helpPath,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
it('renders an alert with the correct message', () => {
const container = vm.$el.querySelector('.js-failed-unmet-prerequisites');
const container = wrapper.find(GlAlert);
const alertMessage =
'This job failed because the necessary resources were not successfully created.';
expect(container).not.toBeNull();
expect(container.innerHTML).toContain(alertMessage);
expect(container.text()).toContain(alertMessage);
});
it('renders link to help page', () => {
const helpLink = vm.$el.querySelector('.js-help-path');
const helpLink = wrapper.find(GlLink);
expect(helpLink).not.toBeNull();
expect(helpLink.innerHTML).toContain('More information');
expect(helpLink.getAttribute('href')).toEqual(helpPath);
expect(helpLink.text()).toContain('More information');
expect(helpLink.attributes().href).toEqual(helpPath);
});
});

View file

@ -0,0 +1,55 @@
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);
});
});
});

View file

@ -0,0 +1,176 @@
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',
);
});
});
});

View file

@ -50,7 +50,6 @@ RSpec.shared_examples 'Maintainer manages access requests' do
def expect_visible_access_request(entity, user)
if has_tabs
expect(page).to have_content "Access requests 1"
expect(page).to have_content "Users requesting access to #{entity.name}"
else
expect(page).to have_content "Users requesting access to #{entity.name} 1"
end