From 9214e550c07793a8deb6d5cd5bb136d0d010a7ca Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 3 Dec 2020 06:09:47 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../recent_searches_storage_keys.js | 2 + .../groups/members/components/app.vue | 6 +- .../components/unmet_prerequisites_block.vue | 27 +-- .../filter_sort/filter_sort_container.vue | 18 ++ .../members_filtered_search_bar.vue | 132 +++++++++++++ app/assets/javascripts/members/constants.js | 4 + .../pages/groups/group_members/index.js | 6 +- .../groups/group_members_controller.rb | 4 + .../groups/group_members/index.html.haml | 53 +++--- ...t-in-app-assets-javascripts-jobs-compo.yml | 5 + .../group_members_filtered_search.yml | 8 + doc/user/project/integrations/webhooks.md | 4 +- locale/gitlab.pot | 18 ++ .../groups/members/filter_members_spec.rb | 26 ++- .../groups/members/search_members_spec.rb | 7 +- .../groups/members/sort_members_spec.rb | 2 + spec/features/groups/members/tabs_spec.rb | 14 +- .../groups/members/components/app_spec.js | 23 ++- .../unmet_prerequisites_block_spec.js | 34 ++-- .../filter_sort/filter_sort_container_spec.js | 55 ++++++ .../members_filtered_search_bar_spec.js | 176 ++++++++++++++++++ ..._manages_access_requests_shared_example.rb | 1 - 22 files changed, 549 insertions(+), 76 deletions(-) create mode 100644 app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue create mode 100644 app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue create mode 100644 changelogs/unreleased/242028-migrate-bs-callout-to-glalert-in-app-assets-javascripts-jobs-compo.yml create mode 100644 config/feature_flags/development/group_members_filtered_search.yml create mode 100644 spec/frontend/members/components/filter_sort/filter_sort_container_spec.js create mode 100644 spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js diff --git a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js index 7e9b809e9b2..54d49821d92 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js +++ b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js @@ -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', }; diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/groups/members/components/app.vue index 8f1bb6e8094..f6f3a955813 100644 --- a/app/assets/javascripts/groups/members/components/app.vue +++ b/app/assets/javascripts/groups/members/components/app.vue @@ -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 { {{ errorMessage }} + diff --git a/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue b/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue index 633561c879e..c9747ca9f02 100644 --- a/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue +++ b/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue @@ -1,11 +1,19 @@ diff --git a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue new file mode 100644 index 00000000000..f2acc3215cd --- /dev/null +++ b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue new file mode 100644 index 00000000000..c1df0b94234 --- /dev/null +++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue @@ -0,0 +1,132 @@ + + + diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js index 5885420a122..a23e9b942ef 100644 --- a/app/assets/javascripts/members/constants.js +++ b/app/assets/javascripts/members/constants.js @@ -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'; diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index fbb960a7ceb..d3900b84fa7 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -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', }, }); diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 5df7ff0632a..8f836010d70 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -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, diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 2a87b42ef13..a08212f151c 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -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: ''.html_safe, strong_end: ''.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: ''.html_safe, strong_end: ''.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: ''.html_safe, strong_end: ''.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: ''.html_safe, strong_end: ''.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: ''.html_safe, strong_end: ''.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: ''.html_safe, strong_end: ''.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: ''.html_safe, strong_end: ''.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: ''.html_safe, strong_end: ''.html_safe } - if vue_members_list_enabled .js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) } .loading diff --git a/changelogs/unreleased/242028-migrate-bs-callout-to-glalert-in-app-assets-javascripts-jobs-compo.yml b/changelogs/unreleased/242028-migrate-bs-callout-to-glalert-in-app-assets-javascripts-jobs-compo.yml new file mode 100644 index 00000000000..8dd080ac936 --- /dev/null +++ b/changelogs/unreleased/242028-migrate-bs-callout-to-glalert-in-app-assets-javascripts-jobs-compo.yml @@ -0,0 +1,5 @@ +--- +title: Migrate bs-callout to GlAlert in …/unmet_prerequisites_block.vue +merge_request: 48398 +author: +type: other diff --git a/config/feature_flags/development/group_members_filtered_search.yml b/config/feature_flags/development/group_members_filtered_search.yml new file mode 100644 index 00000000000..ea1a5b6a74f --- /dev/null +++ b/config/feature_flags/development/group_members_filtered_search.yml @@ -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 diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 2aca15e04b9..4fc55d5afd9 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 19d91b563c3..15bbf479050 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/features/groups/members/filter_members_spec.rb b/spec/features/groups/members/filter_members_spec.rb index b6d33b3f4aa..917b35659a6 100644 --- a/spec/features/groups/members/filter_members_spec.rb +++ b/spec/features/groups/members/filter_members_spec.rb @@ -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 = {}) diff --git a/spec/features/groups/members/search_members_spec.rb b/spec/features/groups/members/search_members_spec.rb index 0b2d2fd478d..fe5fed307d7 100644 --- a/spec/features/groups/members/search_members_spec.rb +++ b/spec/features/groups/members/search_members_spec.rb @@ -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) diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb index f03cc36df18..74c736f6e2c 100644 --- a/spec/features/groups/members/sort_members_spec.rb +++ b/spec/features/groups/members/sort_members_spec.rb @@ -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) diff --git a/spec/features/groups/members/tabs_spec.rb b/spec/features/groups/members/tabs_spec.rb index fa77d1a2ff8..2f95e9fa6d3 100644 --- a/spec/features/groups/members/tabs_spec.rb +++ b/spec/features/groups/members/tabs_spec.rb @@ -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 diff --git a/spec/frontend/groups/members/components/app_spec.js b/spec/frontend/groups/members/components/app_spec.js index 0db3fc18c34..208e2fc35b6 100644 --- a/spec/frontend/groups/members/components/app_spec.js +++ b/spec/frontend/groups/members/components/app_spec.js @@ -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); + }); + }, + ); }); diff --git a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js index 68fcb321214..9092d3f8163 100644 --- a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js +++ b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js @@ -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); }); }); diff --git a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js new file mode 100644 index 00000000000..4abf9f50959 --- /dev/null +++ b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js @@ -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); + }); + }); +}); diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js new file mode 100644 index 00000000000..ca885000c2f --- /dev/null +++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js @@ -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', + ); + }); + }); +}); diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb index 724d6db2705..1dbaace1c89 100644 --- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb +++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb @@ -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