diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-03 09:09:47 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-03 09:09:47 +0300 |
commit | 9214e550c07793a8deb6d5cd5bb136d0d010a7ca (patch) | |
tree | bf094d583e9f57e2816a6f272bcbff302e264efe /app | |
parent | e1e9056d03fec6d72771c7a4ba3fc1174b5ac009 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
9 files changed, 212 insertions, 40 deletions
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 { <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> 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 @@ <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> 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 @@ +<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> 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 @@ +<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> 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: '<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 |