Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e1e9056d03
commit
9214e550c0
22 changed files with 549 additions and 76 deletions
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate bs-callout to GlAlert in …/unmet_prerequisites_block.vue
|
||||
merge_request: 48398
|
||||
author:
|
||||
type: other
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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 = {})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue