summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-01-06 09:10:31 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-01-06 09:10:31 +0000
commitb3c8b65ec2ab3af29d4d14eac27837e0c4793939 (patch)
tree8428c98fbb03f62a848ceeef79651172b04a7de4
parent92bd840b61c7963eb54e0c8de12618b8fe22b715 (diff)
downloadgitlab-ce-b3c8b65ec2ab3af29d4d14eac27837e0c4793939.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/controllers/projects/project_members_controller.rb13
-rw-r--r--app/helpers/projects/project_members_helper.rb29
-rw-r--r--app/views/groups/group_members/index.html.haml20
-rw-r--r--app/views/projects/project_members/_groups.html.haml17
-rw-r--r--app/views/projects/project_members/_team.html.haml22
-rw-r--r--app/views/projects/project_members/index.html.haml91
-rw-r--r--app/views/shared/members/tab_pane/_form_item.html.haml (renamed from app/views/groups/group_members/tab_pane/_form_item.html.haml)0
-rw-r--r--app/views/shared/members/tab_pane/_header.html.haml (renamed from app/views/groups/group_members/tab_pane/_header.html.haml)0
-rw-r--r--app/views/shared/members/tab_pane/_title.html.haml (renamed from app/views/groups/group_members/tab_pane/_title.html.haml)0
-rw-r--r--changelogs/unreleased/281824-convert-project-members-list-view-from-haml-to-vue-setup-tabs.yml5
-rw-r--r--doc/development/migration_style_guide.md6
-rw-r--r--doc/user/project/members/img/access_requests_management.pngbin10436 -> 0 bytes
-rw-r--r--doc/user/project/members/img/access_requests_management_13_8.pngbin0 -> 21685 bytes
-rw-r--r--doc/user/project/members/img/add_user_email_accept.pngbin16878 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_email_accept_13_8.pngbin0 -> 18139 bytes
-rw-r--r--doc/user/project/members/img/add_user_email_ready.pngbin28171 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_email_ready_13_8.pngbin0 -> 28850 bytes
-rw-r--r--doc/user/project/members/img/add_user_email_search.pngbin29628 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_email_search_13_8.pngbin0 -> 29293 bytes
-rw-r--r--doc/user/project/members/img/add_user_give_permissions.pngbin36619 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_give_permissions_13_8.pngbin0 -> 69132 bytes
-rw-r--r--doc/user/project/members/img/add_user_import_members_from_another_project.pngbin25333 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_import_members_from_another_project_13_8.pngbin0 -> 35191 bytes
-rw-r--r--doc/user/project/members/img/add_user_imported_members.pngbin25398 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_imported_members_13_8.pngbin0 -> 47167 bytes
-rw-r--r--doc/user/project/members/img/add_user_list_members.pngbin16916 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_list_members_13_8.pngbin0 -> 39827 bytes
-rw-r--r--doc/user/project/members/img/add_user_search_people.pngbin25368 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_search_people_13_8.pngbin0 -> 28335 bytes
-rw-r--r--doc/user/project/members/img/other_group_sees_shared_project_v13_6.pngbin291848 -> 0 bytes
-rw-r--r--doc/user/project/members/img/other_group_sees_shared_project_v13_8.pngbin0 -> 52192 bytes
-rw-r--r--doc/user/project/members/img/project_groups_tab_13_8.pngbin0 -> 65200 bytes
-rw-r--r--doc/user/project/members/img/project_members.pngbin36955 -> 0 bytes
-rw-r--r--doc/user/project/members/img/project_members_13_8.pngbin0 -> 34744 bytes
-rw-r--r--doc/user/project/members/img/share_project_with_groups_tab_v13_6.pngbin378257 -> 0 bytes
-rw-r--r--doc/user/project/members/img/share_project_with_groups_tab_v13_8.pngbin0 -> 62368 bytes
-rw-r--r--doc/user/project/members/img/share_project_with_groups_v13_6.pngbin395958 -> 0 bytes
-rw-r--r--doc/user/project/members/index.md20
-rw-r--r--doc/user/project/members/share_project_with_groups.md11
-rw-r--r--lib/gitlab/error_tracking.rb43
-rw-r--r--locale/gitlab.pot12
-rw-r--r--package.json4
-rw-r--r--qa/qa/page/project/members.rb2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb133
-rw-r--r--spec/factories/project_group_links.rb2
-rw-r--r--spec/factories/project_members.rb4
-rw-r--r--spec/features/groups/members/master_manages_access_requests_spec.rb1
-rw-r--r--spec/features/projects/members/group_members_spec.rb54
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb17
-rw-r--r--spec/features/projects/members/invite_group_spec.rb10
-rw-r--r--spec/features/projects/members/list_spec.rb4
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb1
-rw-r--r--spec/features/projects/members/tabs_spec.rb73
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb6
-rw-r--r--spec/frontend/static_site_editor/components/submit_changes_error_spec.js2
-rw-r--r--spec/helpers/projects/project_members_helper_spec.rb145
-rw-r--r--spec/lib/gitlab/error_tracking_spec.rb21
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb10
-rw-r--r--yarn.lock16
59 files changed, 604 insertions, 190 deletions
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 631f627838b..5972b29a298 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -17,17 +17,18 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@skip_groups += @project.group.self_and_ancestors_ids if @project.group
@group_links = @project.project_group_links
- @group_links = @group_links.search(params[:search]) if params[:search].present?
+ @group_links = @group_links.search(params[:search_groups]) if params[:search_groups].present?
- @project_members = MembersFinder
+ project_members = MembersFinder
.new(@project, current_user, params: filter_params)
.execute(include_relations: requested_relations)
- @project_members = present_members(@project_members.page(params[:page]))
+ if helpers.can_manage_project_members?(@project)
+ @invited_members = present_members(project_members.invite)
+ @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
+ end
- @requesters = present_members(
- AccessRequestsFinder.new(@project).execute(current_user)
- )
+ @project_members = present_members(project_members.non_invite.page(params[:page]))
@project_member = @project.project_members.new
end
diff --git a/app/helpers/projects/project_members_helper.rb b/app/helpers/projects/project_members_helper.rb
new file mode 100644
index 00000000000..168526d2abb
--- /dev/null
+++ b/app/helpers/projects/project_members_helper.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Projects::ProjectMembersHelper
+ def can_manage_project_members?(project)
+ can?(current_user, :admin_project_member, project)
+ end
+
+ def show_groups?(group_links)
+ group_links.exists? || groups_tab_active?
+ end
+
+ def show_invited_members?(project, invited_members)
+ can_manage_project_members?(project) && invited_members.exists?
+ end
+
+ def show_access_requests?(project, requesters)
+ can_manage_project_members?(project) && requesters.exists?
+ end
+
+ def groups_tab_active?
+ params[:search_groups].present?
+ end
+
+ def current_user_is_group_owner?(project)
+ return false if project.group.nil?
+
+ project.group.has_owner?(current_user)
+ end
+end
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index f9939f19c1a..f13c1f29041 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -53,18 +53,18 @@
#tab-members.tab-pane{ class: ('active' unless invited_active) }
.card.card-without-border
- unless filtered_search_enabled
- = render 'groups/group_members/tab_pane/header' do
- = render 'groups/group_members/tab_pane/title' do
+ = render 'shared/members/tab_pane/header' do
+ = render 'shared/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
+ = render 'shared/members/tab_pane/form_item' do
= label_tag '2fa', _('2FA'), class: form_item_label_css_class
= render 'shared/members/filter_2fa_dropdown'
- = render 'groups/group_members/tab_pane/form_item' do
+ = render 'shared/members/tab_pane/form_item' do
= label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
= render 'shared/members/sort_dropdown'
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
@@ -75,8 +75,8 @@
#tab-groups.tab-pane
.card.card-without-border
- unless filtered_search_enabled
- = render 'groups/group_members/tab_pane/header' do
- = render 'groups/group_members/tab_pane/title' do
+ = render 'shared/members/tab_pane/header' do
+ = render 'shared/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 }
.js-group-linked-list{ data: linked_groups_list_data_attributes(@group) }
.loading
@@ -85,8 +85,8 @@
#tab-invited-members.tab-pane{ class: ('active' if invited_active) }
.card.card-without-border
- unless filtered_search_enabled
- = render 'groups/group_members/tab_pane/header' do
- = render 'groups/group_members/tab_pane/title' do
+ = render 'shared/members/tab_pane/header' do
+ = render 'shared/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'
@@ -98,8 +98,8 @@
#tab-access-requests.tab-pane
.card.card-without-border
- unless filtered_search_enabled
- = render 'groups/group_members/tab_pane/header' do
- = render 'groups/group_members/tab_pane/title' do
+ = render 'shared/members/tab_pane/header' do
+ = render 'shared/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 }
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
.loading
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
index 39ef1e52a0d..fe8a50ebb42 100644
--- a/app/views/projects/project_members/_groups.html.haml
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -1,8 +1,11 @@
-.card.project-members-groups
- .card-header
- = html_escape(_("Groups with access to %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(@project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
- %span.badge.badge-pill= group_links.size
- %ul.content-list.members-list
- - can_admin_member = can?(current_user, :admin_project_member, @project)
+.card.card-without-border
+ = render 'shared/members/tab_pane/header' do
+ = render 'shared/members/tab_pane/title' do
+ = html_escape(_("Groups with access to %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(@project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
+ = form_tag project_project_members_path(@project), method: :get, class: 'user-search-form gl-mx-n3 gl-my-n3', data: { testid: 'group-link-search-form' } do
+ .gl-px-3.gl-py-2
+ .search-control-wrap.gl-relative
+ = render 'shared/members/search_field', name: 'search_groups'
+ %ul.content-list.members-list{ data: { testid: 'project-member-groups' } }
- @group_links.each do |group_link|
- = render 'shared/members/group', group_link: group_link, can_admin_member: can_admin_member, group_link_path: project_group_link_path(@project, group_link)
+ = render 'shared/members/group', group_link: group_link, can_admin_member: can_manage_project_members?(@project), group_link_path: project_group_link_path(@project, group_link)
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 171212b6a96..24ca7ebded9 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,20 +1,18 @@
- project = local_assigns.fetch(:project)
- members = local_assigns.fetch(:members)
- group = local_assigns.fetch(:group)
-- current_user_is_group_owner = group && group.has_owner?(current_user)
+- current_user_is_group_owner = local_assigns.fetch(:current_user_is_group_owner)
-.card
- .card-header.flex-project-members-panel
- %span.flex-project-title
+.card.card-without-border
+ = render 'shared/members/tab_pane/header' do
+ = render 'shared/members/tab_pane/title' do
= html_escape(_("Members of %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
- %span.badge.badge-pill= members.total_count
- = form_tag project_project_members_path(project), method: :get, class: 'form-inline user-search-form flex-users-form' do
- .form-group
- .position-relative
- = search_field_tag :search, params[:search], { placeholder: _('Find existing members by name'), class: 'form-control', spellcheck: false }
- %button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") }
- = sprite_icon('search', css_class: 'gl-vertical-align-middle!')
- = label_tag :sort_by, _('Sort by'), class: 'col-form-label label-bold px-2'
+ = form_tag project_project_members_path(project), 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'
+ = render 'shared/members/tab_pane/form_item' do
+ = label_tag :sort_by, _('Sort by'), class: 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center'
= render 'shared/members/sort_dropdown'
%ul.content-list.members-list{ data: { qa_selector: 'members_list', testid: 'members-table' } }
= render partial: 'shared/members/member',
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index cad76d7aeac..0f5f169f548 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,5 +1,4 @@
- page_title _("Members")
-- can_admin_project_members = can?(current_user, :admin_project_member, @project)
- group = @project.group
.js-remove-member-modal
@@ -8,37 +7,73 @@
- if project_can_be_shared?
%h4
= _("Project members")
- - if can_admin_project_members
+ - if can_manage_project_members?(@project)
%p= share_project_description(@project)
- else
%p
= html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
- .light
- - if can_admin_project_members && project_can_be_shared?
- - if !membership_locked? && @project.allowed_to_share_with_group?
- %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
- %li.nav-tab{ role: 'presentation' }
- %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member")
- %li.nav-tab{ role: 'presentation', class: ('active' if membership_locked?) }
- %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group")
+ - if can_manage_project_members?(@project) && project_can_be_shared?
+ - if !membership_locked? && @project.allowed_to_share_with_group?
+ %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
+ %li.nav-tab{ role: 'presentation' }
+ %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member")
+ %li.nav-tab{ role: 'presentation', class: ('active' if membership_locked?) }
+ %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group")
- .tab-content.gitlab-tab-content
- .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
- = render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project)
- .tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) }
- = render 'shared/members/invite_group', submit_url: project_group_links_path(@project), access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, group_link_field: 'link_group_id', group_access_field: 'link_group_access'
- - elsif !membership_locked?
- .invite-member= render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project)
- - elsif @project.allowed_to_share_with_group?
- .invite-group= render 'shared/members/invite_group', access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, submit_url: project_group_links_path(@project), group_link_field: 'link_group_id', group_access_field: 'link_group_access'
-
- = render 'shared/members/requests', membership_source: @project, group: group, requesters: @requesters
- .clearfix
- %h5.member.existing-title
- = _("Existing members and groups")
- - if @group_links.any?
+ .tab-content.gitlab-tab-content
+ .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
+ = render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project)
+ .tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) }
+ = render 'shared/members/invite_group', submit_url: project_group_links_path(@project), access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, group_link_field: 'link_group_id', group_access_field: 'link_group_access'
+ - elsif !membership_locked?
+ .invite-member= render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project)
+ - elsif @project.allowed_to_share_with_group?
+ .invite-group= render 'shared/members/invite_group', access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, submit_url: project_group_links_path(@project), group_link_field: 'link_group_id', group_access_field: 'link_group_access'
+ %ul.nav-links.mobile-separator.nav.nav-tabs
+ %li.nav-item
+ = link_to '#tab-members', class: ['nav-link', ('active' unless groups_tab_active?)], data: { toggle: 'tab' } do
+ %span
+ = _('Members')
+ %span.badge.badge-pill= @project_members.total_count
+ - if show_groups?(@group_links)
+ %li.nav-item
+ = link_to '#tab-groups', class: ['nav-link', ('active' if groups_tab_active?)] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do
+ %span
+ = _('Groups')
+ %span.badge.badge-pill= @group_links.count
+ - if show_invited_members?(@project, @invited_members)
+ %li.nav-item
+ = link_to '#tab-invited-members', class: 'nav-link', data: { toggle: 'tab' } do
+ %span
+ = _('Invited')
+ %span.badge.badge-pill= @invited_members.count
+ - if show_access_requests?(@project, @requesters)
+ %li.nav-item
+ = link_to '#tab-access-requests', class: 'nav-link', data: { toggle: 'tab' } do
+ %span
+ = _('Access requests')
+ %span.badge.badge-pill= @requesters.count
+ .tab-content
+ #tab-members.tab-pane{ class: ('active' unless groups_tab_active?) }
+ = render 'projects/project_members/team', project: @project, group: group, members: @project_members, current_user_is_group_owner: current_user_is_group_owner?(@project)
+ = paginate @project_members, theme: "gitlab", params: { search_groups: nil }
+ - if show_groups?(@group_links)
+ #tab-groups.tab-pane{ class: ('active' if groups_tab_active?) }
= render 'projects/project_members/groups', group_links: @group_links
-
- = render 'projects/project_members/team', project: @project, group: group, members: @project_members
- = paginate @project_members, theme: "gitlab"
+ - if show_invited_members?(@project, @invited_members)
+ #tab-invited-members.tab-pane
+ .card.card-without-border
+ = render 'shared/members/tab_pane/header' do
+ = render 'shared/members/tab_pane/title' do
+ = html_escape(_('Members invited to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ %ul.content-list.members-list
+ = render partial: 'shared/members/member', collection: @invited_members, as: :member, locals: { membership_source: @project, group: group, current_user_is_group_owner: current_user_is_group_owner?(@project) }
+ - if show_access_requests?(@project, @requesters)
+ #tab-access-requests.tab-pane
+ .card.card-without-border
+ = render 'shared/members/tab_pane/header' do
+ = render 'shared/members/tab_pane/title' do
+ = html_escape(_('Users requesting access to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ %ul.content-list.members-list
+ = render partial: 'shared/members/member', collection: @requesters, as: :member, locals: { membership_source: @project, group: group }
diff --git a/app/views/groups/group_members/tab_pane/_form_item.html.haml b/app/views/shared/members/tab_pane/_form_item.html.haml
index 9e57d3329d7..9e57d3329d7 100644
--- a/app/views/groups/group_members/tab_pane/_form_item.html.haml
+++ b/app/views/shared/members/tab_pane/_form_item.html.haml
diff --git a/app/views/groups/group_members/tab_pane/_header.html.haml b/app/views/shared/members/tab_pane/_header.html.haml
index a02bf90eddf..a02bf90eddf 100644
--- a/app/views/groups/group_members/tab_pane/_header.html.haml
+++ b/app/views/shared/members/tab_pane/_header.html.haml
diff --git a/app/views/groups/group_members/tab_pane/_title.html.haml b/app/views/shared/members/tab_pane/_title.html.haml
index c1418a5f7c8..c1418a5f7c8 100644
--- a/app/views/groups/group_members/tab_pane/_title.html.haml
+++ b/app/views/shared/members/tab_pane/_title.html.haml
diff --git a/changelogs/unreleased/281824-convert-project-members-list-view-from-haml-to-vue-setup-tabs.yml b/changelogs/unreleased/281824-convert-project-members-list-view-from-haml-to-vue-setup-tabs.yml
new file mode 100644
index 00000000000..1c1e4c48bad
--- /dev/null
+++ b/changelogs/unreleased/281824-convert-project-members-list-view-from-haml-to-vue-setup-tabs.yml
@@ -0,0 +1,5 @@
+---
+title: Reorganize project member management into tabs
+merge_request: 49764
+author:
+type: changed
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 8cdfbd558ca..e1205346585 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -516,12 +516,14 @@ class MyMigration < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
+ INDEX_NAME = 'index_name'
+
def up
- add_concurrent_index :table, :column
+ add_concurrent_index :table, :column, name: INDEX_NAME
end
def down
- remove_concurrent_index :table, :column, name: index_name
+ remove_concurrent_index :table, :column, name: INDEX_NAME
end
end
```
diff --git a/doc/user/project/members/img/access_requests_management.png b/doc/user/project/members/img/access_requests_management.png
deleted file mode 100644
index 9a1c9621e41..00000000000
--- a/doc/user/project/members/img/access_requests_management.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/access_requests_management_13_8.png b/doc/user/project/members/img/access_requests_management_13_8.png
new file mode 100644
index 00000000000..950ef4dec01
--- /dev/null
+++ b/doc/user/project/members/img/access_requests_management_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/add_user_email_accept.png b/doc/user/project/members/img/add_user_email_accept.png
deleted file mode 100644
index cbee9e08c70..00000000000
--- a/doc/user/project/members/img/add_user_email_accept.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_email_accept_13_8.png b/doc/user/project/members/img/add_user_email_accept_13_8.png
new file mode 100644
index 00000000000..ed980036af5
--- /dev/null
+++ b/doc/user/project/members/img/add_user_email_accept_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/add_user_email_ready.png b/doc/user/project/members/img/add_user_email_ready.png
deleted file mode 100644
index 0066eb3427b..00000000000
--- a/doc/user/project/members/img/add_user_email_ready.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_email_ready_13_8.png b/doc/user/project/members/img/add_user_email_ready_13_8.png
new file mode 100644
index 00000000000..a610b46a176
--- /dev/null
+++ b/doc/user/project/members/img/add_user_email_ready_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/add_user_email_search.png b/doc/user/project/members/img/add_user_email_search.png
deleted file mode 100644
index 66bcd6aad80..00000000000
--- a/doc/user/project/members/img/add_user_email_search.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_email_search_13_8.png b/doc/user/project/members/img/add_user_email_search_13_8.png
new file mode 100644
index 00000000000..934cf19bd3d
--- /dev/null
+++ b/doc/user/project/members/img/add_user_email_search_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/add_user_give_permissions.png b/doc/user/project/members/img/add_user_give_permissions.png
deleted file mode 100644
index 376a3eefccc..00000000000
--- a/doc/user/project/members/img/add_user_give_permissions.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_give_permissions_13_8.png b/doc/user/project/members/img/add_user_give_permissions_13_8.png
new file mode 100644
index 00000000000..1916d056a52
--- /dev/null
+++ b/doc/user/project/members/img/add_user_give_permissions_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/add_user_import_members_from_another_project.png b/doc/user/project/members/img/add_user_import_members_from_another_project.png
deleted file mode 100644
index cb3b70bd4b5..00000000000
--- a/doc/user/project/members/img/add_user_import_members_from_another_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_import_members_from_another_project_13_8.png b/doc/user/project/members/img/add_user_import_members_from_another_project_13_8.png
new file mode 100644
index 00000000000..a6dddec3fb7
--- /dev/null
+++ b/doc/user/project/members/img/add_user_import_members_from_another_project_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/add_user_imported_members.png b/doc/user/project/members/img/add_user_imported_members.png
deleted file mode 100644
index 51fd7688890..00000000000
--- a/doc/user/project/members/img/add_user_imported_members.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_imported_members_13_8.png b/doc/user/project/members/img/add_user_imported_members_13_8.png
new file mode 100644
index 00000000000..725e447604f
--- /dev/null
+++ b/doc/user/project/members/img/add_user_imported_members_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/add_user_list_members.png b/doc/user/project/members/img/add_user_list_members.png
deleted file mode 100644
index e0fa404288d..00000000000
--- a/doc/user/project/members/img/add_user_list_members.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_list_members_13_8.png b/doc/user/project/members/img/add_user_list_members_13_8.png
new file mode 100644
index 00000000000..b8c0160c6d8
--- /dev/null
+++ b/doc/user/project/members/img/add_user_list_members_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/add_user_search_people.png b/doc/user/project/members/img/add_user_search_people.png
deleted file mode 100644
index 41767a9167c..00000000000
--- a/doc/user/project/members/img/add_user_search_people.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_search_people_13_8.png b/doc/user/project/members/img/add_user_search_people_13_8.png
new file mode 100644
index 00000000000..e9aa58512ab
--- /dev/null
+++ b/doc/user/project/members/img/add_user_search_people_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/other_group_sees_shared_project_v13_6.png b/doc/user/project/members/img/other_group_sees_shared_project_v13_6.png
deleted file mode 100644
index e6e3f8f043b..00000000000
--- a/doc/user/project/members/img/other_group_sees_shared_project_v13_6.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/other_group_sees_shared_project_v13_8.png b/doc/user/project/members/img/other_group_sees_shared_project_v13_8.png
new file mode 100644
index 00000000000..aa2aaf071e1
--- /dev/null
+++ b/doc/user/project/members/img/other_group_sees_shared_project_v13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/project_groups_tab_13_8.png b/doc/user/project/members/img/project_groups_tab_13_8.png
new file mode 100644
index 00000000000..5d7948f0761
--- /dev/null
+++ b/doc/user/project/members/img/project_groups_tab_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/project_members.png b/doc/user/project/members/img/project_members.png
deleted file mode 100644
index 218f5a24d2e..00000000000
--- a/doc/user/project/members/img/project_members.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/project_members_13_8.png b/doc/user/project/members/img/project_members_13_8.png
new file mode 100644
index 00000000000..9120d471b3b
--- /dev/null
+++ b/doc/user/project/members/img/project_members_13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/share_project_with_groups_tab_v13_6.png b/doc/user/project/members/img/share_project_with_groups_tab_v13_6.png
deleted file mode 100644
index 7d83659ef7a..00000000000
--- a/doc/user/project/members/img/share_project_with_groups_tab_v13_6.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/share_project_with_groups_tab_v13_8.png b/doc/user/project/members/img/share_project_with_groups_tab_v13_8.png
new file mode 100644
index 00000000000..6cbbb386396
--- /dev/null
+++ b/doc/user/project/members/img/share_project_with_groups_tab_v13_8.png
Binary files differ
diff --git a/doc/user/project/members/img/share_project_with_groups_v13_6.png b/doc/user/project/members/img/share_project_with_groups_v13_6.png
deleted file mode 100644
index 121e77671a3..00000000000
--- a/doc/user/project/members/img/share_project_with_groups_v13_6.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index 85cb139c45b..cccb998fc31 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -21,7 +21,7 @@ project's **Members**.
When your project belongs to the group, group members inherit the membership and permission
level for the project from the group.
-![Project members page](img/project_members.png)
+![Project members page](img/project_members_13_8.png)
From the image above, we can deduce the following things:
@@ -46,17 +46,17 @@ using the dropdown on the right side:
Right next to **People**, start typing the name or username of the user you
want to add.
-![Search for people](img/add_user_search_people.png)
+![Search for people](img/add_user_search_people_13_8.png)
Select the user and the [permission level](../../permissions.md)
that you'd like to give the user. Note that you can select more than one user.
-![Give user permissions](img/add_user_give_permissions.png)
+![Give user permissions](img/add_user_give_permissions_13_8.png)
Once done, select **Add users to project** and they are immediately added to
your project with the permissions you gave them above.
-![List members](img/add_user_list_members.png)
+![List members](img/add_user_list_members_13_8.png)
From there on, you can either remove an existing user or change their access
level to the project.
@@ -68,14 +68,14 @@ You can import another project's users in your own project by hitting the
In the dropdown menu, you can see only the projects you are Maintainer on.
-![Import members from another project](img/add_user_import_members_from_another_project.png)
+![Import members from another project](img/add_user_import_members_from_another_project_13_8.png)
Select the one you want and hit **Import project members**. A flash message
displays, notifying you that the import was successful, and the new members
are now in the project's members list. Notice that the permissions that they
had on the project you imported from are retained.
-![Members list of new members](img/add_user_imported_members.png)
+![Members list of new members](img/add_user_imported_members_13_8.png)
## Invite people using their e-mail address
@@ -83,18 +83,18 @@ If a user you want to give access to doesn't have an account on your GitLab
instance, you can invite them just by typing their e-mail address in the
user search field.
-![Invite user by mail](img/add_user_email_search.png)
+![Invite user by mail](img/add_user_email_search_13_8.png)
As you can imagine, you can mix inviting multiple people and adding existing
GitLab users to the project.
-![Invite user by mail ready to submit](img/add_user_email_ready.png)
+![Invite user by mail ready to submit](img/add_user_email_ready_13_8.png)
Once done, hit **Add users to project** and watch that there is a new member
with the e-mail address we used above. From there on, you can resend the
invitation, change their access level, or even delete them.
-![Invite user members list](img/add_user_email_accept.png)
+![Invite user members list](img/add_user_email_accept_13_8.png)
While unaccepted, the system automatically sends reminder emails on the second, fifth,
and tenth day after the invitation was initially sent.
@@ -130,7 +130,7 @@ NOTE:
If a project does not have any maintainers, the notification is sent to the
most recently active owners of the project's group.
-![Manage access requests](img/access_requests_management.png)
+![Manage access requests](img/access_requests_management_13_8.png)
If you change your mind before your request is approved, just click the
**Withdraw Access Request** button.
diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md
index edfe8ae3b5b..d17717fb29c 100644
--- a/doc/user/project/members/share_project_with_groups.md
+++ b/doc/user/project/members/share_project_with_groups.md
@@ -26,19 +26,20 @@ To share 'Project Acme' with the 'Engineering' group:
1. For 'Project Acme' use the left navigation menu to go to **Members**.
- ![share project with groups](img/share_project_with_groups_tab_v13_6.png)
+ ![share project with groups](img/share_project_with_groups_tab_v13_8.png)
1. Select the **Invite group** tab.
1. Add the 'Engineering' group with the maximum access level of your choice.
1. Optionally, select an expiring date.
1. Click **Invite**.
+1. After sharing 'Project Acme' with 'Engineering':
+ - The group is listed in the **Groups** tab.
- ![share project with groups tab](img/share_project_with_groups_tab_v13_6.png)
+ !['Engineering' group is listed in Groups tab](img/project_groups_tab_13_8.png)
-1. After sharing 'Project Acme' with 'Engineering', the project is listed
- on the group dashboard
+ - The project is listed on the group dashboard.
- !['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project_v13_6.png)
+ !['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project_v13_8.png)
Note that you can only share a project with:
diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb
index a5ace2be773..e6877d73cf8 100644
--- a/lib/gitlab/error_tracking.rb
+++ b/lib/gitlab/error_tracking.rb
@@ -111,8 +111,8 @@ module Gitlab
private
def before_send(event, hint)
- event = add_context_from_exception_type(event, hint)
- event = custom_fingerprinting(event, hint)
+ inject_context_for_exception(event, hint[:exception])
+ custom_fingerprinting(event, hint[:exception])
event
end
@@ -123,7 +123,6 @@ module Gitlab
end
extra = sanitize_request_parameters(extra)
- inject_sql_query_into_extra(exception, extra)
if sentry && Raven.configuration.server
Raven.capture_exception(exception, tags: default_tags, extra: extra)
@@ -150,12 +149,6 @@ module Gitlab
filter.filter(parameters)
end
- def inject_sql_query_into_extra(exception, extra)
- return unless exception.is_a?(ActiveRecord::StatementInvalid)
-
- extra[:sql] = PgQuery.normalize(exception.sql.to_s)
- end
-
def sentry_dsn
return unless Rails.env.production? || Rails.env.development?
return unless Gitlab.config.sentry.enabled
@@ -183,9 +176,17 @@ module Gitlab
{}
end
- # Debugging for https://gitlab.com/gitlab-org/gitlab-foss/issues/57727
- def add_context_from_exception_type(event, hint)
- if ActiveModel::MissingAttributeError === hint[:exception]
+ # Group common, mostly non-actionable exceptions by type and message,
+ # rather than cause
+ def custom_fingerprinting(event, ex)
+ return event unless CUSTOM_FINGERPRINTING.include?(ex.class.name)
+
+ event.fingerprint = [ex.class.name, ex.message]
+ end
+
+ def inject_context_for_exception(event, ex)
+ case ex
+ when ActiveModel::MissingAttributeError # Debugging for https://gitlab.com/gitlab-org/gitlab/-/issues/26751
columns_hash = ActiveRecord::Base
.connection
.schema_cache
@@ -193,21 +194,11 @@ module Gitlab
.transform_values { |v| v.map(&:first) }
event.extra.merge!(columns_hash)
+ when ActiveRecord::StatementInvalid
+ event.extra[:sql] = PgQuery.normalize(ex.sql.to_s)
+ else
+ inject_context_for_exception(event, ex.cause) if ex.cause.present?
end
-
- event
- end
-
- # Group common, mostly non-actionable exceptions by type and message,
- # rather than cause
- def custom_fingerprinting(event, hint)
- ex = hint[:exception]
-
- return event unless CUSTOM_FINGERPRINTING.include?(ex.class.name)
-
- event.fingerprint = [ex.class.name, ex.message]
-
- event
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c96260b83b3..c34387c23b9 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11559,9 +11559,6 @@ msgstr ""
msgid "Existing branch name, tag, or commit SHA"
msgstr ""
-msgid "Existing members and groups"
-msgstr ""
-
msgid "Existing projects may be moved into a group"
msgstr ""
@@ -12428,9 +12425,6 @@ msgstr ""
msgid "Find by path"
msgstr ""
-msgid "Find existing members by name"
-msgstr ""
-
msgid "Find file"
msgstr ""
@@ -17272,6 +17266,9 @@ msgstr ""
msgid "Members invited to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
+msgid "Members invited to %{strong_start}%{project_name}%{strong_end}"
+msgstr ""
+
msgid "Members listed as CODEOWNERS of affected files."
msgstr ""
@@ -30733,6 +30730,9 @@ msgstr ""
msgid "Users requesting access to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
+msgid "Users requesting access to %{strong_start}%{project_name}%{strong_end}"
+msgstr ""
+
msgid "Users were successfully added."
msgstr ""
diff --git a/package.json b/package.json
index 072c9efadfe..2c95b597e4f 100644
--- a/package.json
+++ b/package.json
@@ -42,9 +42,9 @@
"@babel/plugin-syntax-import-meta": "^7.10.1",
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
- "@gitlab/svgs": "1.177.0",
+ "@gitlab/svgs": "1.178.0",
"@gitlab/tributejs": "1.0.0",
- "@gitlab/ui": "25.2.1",
+ "@gitlab/ui": "25.3.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4",
diff --git a/qa/qa/page/project/members.rb b/qa/qa/page/project/members.rb
index 88b05ceb1d1..447049ce22a 100644
--- a/qa/qa/page/project/members.rb
+++ b/qa/qa/page/project/members.rb
@@ -17,6 +17,7 @@ module QA
view 'app/views/projects/project_members/index.html.haml' do
element :invite_group_tab
+ element :groups_list_tab
end
view 'app/views/shared/members/_invite_group.html.haml' do
@@ -48,6 +49,7 @@ module QA
def remove_group(group_name)
click_element :invite_group_tab
+ click_element :groups_list_tab
page.accept_alert do
within_element(:group_row, text: group_name) do
click_element :delete_group_access_link
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 74311fa89f3..971eb782fa4 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -14,32 +14,137 @@ RSpec.describe Projects::ProjectMembersController do
expect(response).to have_gitlab_http_status(:ok)
end
- context 'when project belongs to group' do
- let(:user_in_group) { create(:user) }
- let(:project_in_group) { create(:project, :public, group: group) }
+ context 'project members' do
+ context 'when project belongs to group' do
+ let(:user_in_group) { create(:user) }
+ let(:project_in_group) { create(:project, :public, group: group) }
+
+ before do
+ group.add_owner(user_in_group)
+ project_in_group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'lists inherited project members by default' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group }
+
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id)
+ end
+
+ it 'lists direct project members only' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' }
+
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id)
+ end
+
+ it 'lists inherited project members only' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' }
+
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id)
+ end
+ end
+
+ context 'when invited members are present' do
+ let!(:invited_member) { create(:project_member, :invited, project: project) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'excludes the invited members from project members list' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(assigns(:project_members).map(&:invite_email)).not_to contain_exactly(invited_member.invite_email)
+ end
+ end
+ end
+
+ context 'group links' do
+ let!(:project_group_link) { create(:project_group_link, project: project, group: group) }
+
+ it 'lists group links' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(assigns(:group_links).map(&:id)).to contain_exactly(project_group_link.id)
+ end
+
+ context 'when `search_groups` param is present' do
+ let(:group_2) { create(:group, :public, name: 'group_2') }
+ let!(:project_group_link_2) { create(:project_group_link, project: project, group: group_2) }
+
+ it 'lists group links that match search' do
+ get :index, params: { namespace_id: project.namespace, project_id: project, search_groups: 'group_2' }
+
+ expect(assigns(:group_links).map(&:id)).to contain_exactly(project_group_link_2.id)
+ end
+ end
+ end
+
+ context 'invited members' do
+ let!(:invited_member) { create(:project_member, :invited, project: project) }
before do
- group.add_owner(user_in_group)
- project_in_group.add_maintainer(user)
+ project.add_maintainer(user)
sign_in(user)
end
- it 'lists inherited project members by default' do
- get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group }
+ context 'when user has `admin_project_member` permissions' do
+ before do
+ allow(controller.helpers).to receive(:can_manage_project_members?).with(project).and_return(true)
+ end
+
+ it 'lists invited members' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(assigns(:invited_members).map(&:invite_email)).to contain_exactly(invited_member.invite_email)
+ end
+ end
+
+ context 'when user does not have `admin_project_member` permissions' do
+ before do
+ allow(controller.helpers).to receive(:can_manage_project_members?).with(project).and_return(false)
+ end
+
+ it 'does not list invited members' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(assigns(:invited_members)).to be_nil
+ end
+ end
+ end
+
+ context 'access requests' do
+ let(:access_requester_user) { create(:user) }
- expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id)
+ before do
+ project.request_access(access_requester_user)
+ project.add_maintainer(user)
+ sign_in(user)
end
- it 'lists direct project members only' do
- get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' }
+ context 'when user has `admin_project_member` permissions' do
+ before do
+ allow(controller.helpers).to receive(:can_manage_project_members?).with(project).and_return(true)
+ end
+
+ it 'lists access requests' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
- expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id)
+ expect(assigns(:requesters).map(&:user_id)).to contain_exactly(access_requester_user.id)
+ end
end
- it 'lists inherited project members only' do
- get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' }
+ context 'when user does not have `admin_project_member` permissions' do
+ before do
+ allow(controller.helpers).to receive(:can_manage_project_members?).with(project).and_return(false)
+ end
+
+ it 'does not list access requests' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
- expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id)
+ expect(assigns(:requesters)).to be_nil
+ end
end
end
end
diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb
index 5e3e83f18c1..b1b0f04d84c 100644
--- a/spec/factories/project_group_links.rb
+++ b/spec/factories/project_group_links.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :project_group_link do
project
- group
+ group { association(:group) }
expires_at { nil }
group_access { Gitlab::Access::DEVELOPER }
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index 0c2ffac4112..3e83ab7118c 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -15,7 +15,9 @@ FactoryBot.define do
trait(:invited) do
user_id { nil }
invite_token { 'xxx' }
- invite_email { 'email@email.com' }
+ sequence :invite_email do |n|
+ "email#{n}@email.com"
+ end
end
trait :blocked do
diff --git a/spec/features/groups/members/master_manages_access_requests_spec.rb b/spec/features/groups/members/master_manages_access_requests_spec.rb
index 71c9b280ebe..2a17e7d2a5c 100644
--- a/spec/features/groups/members/master_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/master_manages_access_requests_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe 'Groups > Members > Maintainer manages access requests' do
it_behaves_like 'Maintainer manages access requests' do
- let(:has_tabs) { true }
let(:entity) { create(:group, :public) }
let(:members_page_path) { group_group_members_path(entity) }
end
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index 3060d2c6a43..9c740fd3834 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects members' do
+RSpec.describe 'Projects members', :js do
let(:user) { create(:user) }
let(:developer) { create(:user) }
let(:group) { create(:group, :public) }
@@ -66,62 +66,60 @@ RSpec.describe 'Projects members' do
end
end
- context 'with a group and a project invitee' do
+ context 'with a group, a project invitee, and a project requester' do
before do
+ group.request_access(group_requester)
+ project.request_access(project_requester)
group_invitee
project_invitee
visit project_project_members_path(project)
end
- it 'shows the project invitee, the project developer, and the group owner' do
+ it 'shows the group owner' do
page.within first('.content-list') do
- expect(page).to have_content('test1@abc.com')
- expect(page).not_to have_content('test2@abc.com')
-
- # Project developer
- expect(page).to have_content(developer.name)
-
# Group owner
expect(page).to have_content(user.name)
expect(page).to have_content(group.name)
end
end
- end
- context 'with a group requester' do
- before do
- group.request_access(group_requester)
- visit project_project_members_path(project)
+ it 'shows the project developer' do
+ page.within first('.content-list') do
+ # Project developer
+ expect(page).to have_content(developer.name)
+ end
end
- it 'does not appear in the project members page' do
+ it 'shows the project invitee' do
+ click_link 'Invited'
+
page.within first('.content-list') do
+ expect(page).to have_content('test1@abc.com')
+ expect(page).not_to have_content('test2@abc.com')
+ end
+ end
+
+ it 'shows the project requester' do
+ click_link 'Access requests'
+
+ page.within first('.content-list') do
+ expect(page).to have_content(project_requester.name)
expect(page).not_to have_content(group_requester.name)
end
end
end
- context 'with a group and a project requesters' do
+ context 'with a group requester' do
before do
group.request_access(group_requester)
- project.request_access(project_requester)
visit project_project_members_path(project)
end
- it 'shows the project requester, the project developer, and the group owner' do
+ it 'does not appear in the project members page' do
+ expect(page).not_to have_link('Access requests')
page.within first('.content-list') do
- expect(page).to have_content(project_requester.name)
expect(page).not_to have_content(group_requester.name)
end
-
- page.within all('.content-list').last do
- # Project developer
- expect(page).to have_content(developer.name)
-
- # Group owner
- expect(page).to have_content(user.name)
- expect(page).to have_content(group.name)
- end
end
end
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index d59f8eb4b1d..686d86b1783 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
project.add_maintainer(user)
sign_in(user)
visit project_project_members_path(project)
+ click_groups_tab
end
it 'updates group access level' do
@@ -29,6 +30,8 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
visit project_project_members_path(project)
+ click_groups_tab
+
expect(first('.group_member')).to have_content('Guest')
end
@@ -71,23 +74,31 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
expect(page).not_to have_selector('.group_member')
end
- context 'search in existing members (yes, this filters the groups list as well)' do
+ context 'search in existing members' do
it 'finds no results' do
page.within '.user-search-form' do
- fill_in 'search', with: 'testing 123'
+ fill_in 'search_groups', with: 'testing 123'
find('.user-search-btn').click
end
+ click_groups_tab
+
expect(page).not_to have_selector('.group_member')
end
it 'finds results' do
page.within '.user-search-form' do
- fill_in 'search', with: group.name
+ fill_in 'search_groups', with: group.name
find('.user-search-btn').click
end
+ click_groups_tab
+
expect(page).to have_selector('.group_member', count: 1)
end
end
+
+ def click_groups_tab
+ click_link 'Groups'
+ end
end
diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb
index 30e32ad1366..bb56ae348fb 100644
--- a/spec/features/projects/members/invite_group_spec.rb
+++ b/spec/features/projects/members/invite_group_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'Project > Members > Invite group', :js do
it 'the project can be shared with another group' do
visit project_project_members_path(project)
- expect(page).not_to have_css('.project-members-groups')
+ expect(page).not_to have_link 'Groups'
click_on 'invite-group-tab'
@@ -47,7 +47,9 @@ RSpec.describe 'Project > Members > Invite group', :js do
page.find('body').click
find('.btn-success').click
- page.within('.project-members-groups') do
+ click_link 'Groups'
+
+ page.within('[data-testid="project-member-groups"]') do
expect(page).to have_content(group_to_share_with.name)
end
end
@@ -132,7 +134,9 @@ RSpec.describe 'Project > Members > Invite group', :js do
end
it 'the group link shows the expiration time with a warning class' do
- page.within('.project-members-groups') do
+ click_link 'Groups'
+
+ page.within('[data-testid="project-member-groups"]') do
# Using distance_of_time_in_words_to_now because it is not the same as
# subtraction, and this way avoids time zone issues as well
expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index 36ff461aac2..eba0867dc8c 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -82,7 +82,9 @@ RSpec.describe 'Project members list' do
add_user('test@example.com', 'Reporter')
- page.within(second_row) do
+ click_link 'Invited'
+
+ page.within(first_row) do
expect(page).to have_content('test@example.com')
expect(page).to have_content('Invited')
expect(page).to have_button('Reporter')
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index 2fdc75dca91..4c3eaa93352 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe 'Projects > Members > Maintainer manages access requests' do
it_behaves_like 'Maintainer manages access requests' do
- let(:has_tabs) { false }
let(:entity) { create(:project, :public) }
let(:members_page_path) { project_project_members_path(entity) }
end
diff --git a/spec/features/projects/members/tabs_spec.rb b/spec/features/projects/members/tabs_spec.rb
new file mode 100644
index 00000000000..bdcf02c82a4
--- /dev/null
+++ b/spec/features/projects/members/tabs_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Projects > Members > Tabs' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, creator: user, namespace: user.namespace) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project_members) { create_list(:project_member, 2, project: project) }
+ let_it_be(:access_requests) { create_list(:project_member, 2, :access_request, project: project) }
+ let_it_be(:invites) { create_list(:project_member, 2, :invited, project: project) }
+ let_it_be(:project_group_links) { create_list(:project_group_link, 2, project: project) }
+
+ shared_examples 'active "Members" tab' do
+ it 'displays "Members" tab' do
+ expect(page).to have_selector('.nav-link.active', text: 'Members')
+ end
+ end
+
+ before do
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+
+ sign_in(user)
+ visit project_project_members_path(project)
+ end
+
+ where(:tab, :count) do
+ 'Members' | 3
+ 'Invited' | 2
+ 'Groups' | 2
+ 'Access requests' | 2
+ end
+
+ with_them do
+ it "renders #{params[:tab]} tab" do
+ expect(page).to have_selector('.nav-link', text: "#{tab} #{count}")
+ end
+ end
+
+ context 'displays "Members" tab by default' do
+ it_behaves_like 'active "Members" tab'
+ end
+
+ context 'when searching "Groups"', :js do
+ before do
+ click_link 'Groups'
+
+ page.within '[data-testid="group-link-search-form"]' do
+ fill_in 'search_groups', with: 'group'
+ find('button[type="submit"]').click
+ end
+ end
+
+ it 'displays "Groups" tab' do
+ expect(page).to have_selector('.nav-link.active', text: 'Groups')
+ end
+
+ context 'and then searching "Members"' do
+ before do
+ click_link 'Members 3'
+
+ page.within '[data-testid="user-search-form"]' do
+ fill_in 'search', with: 'user'
+ find('button[type="submit"]').click
+ end
+ end
+
+ it_behaves_like 'active "Members" tab'
+ end
+ end
+end
diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb
index 3836b95a28a..726b8fb6840 100644
--- a/spec/features/projects/settings/user_manages_project_members_spec.rb
+++ b/spec/features/projects/settings/user_manages_project_members_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe 'Projects > Settings > User manages project members' do
end
end
- it 'shows all members of project shared group' do
+ it 'shows all members of project shared group', :js do
group.add_owner(user)
group.add_developer(user_dmitriy)
@@ -67,7 +67,9 @@ RSpec.describe 'Projects > Settings > User manages project members' do
visit(project_project_members_path(project))
- page.within('.project-members-groups') do
+ click_link 'Groups'
+
+ page.within('[data-testid="project-member-groups"]') do
expect(page).to have_content('OpenSource')
expect(first('.group_member')).to have_content('Maintainer')
end
diff --git a/spec/frontend/static_site_editor/components/submit_changes_error_spec.js b/spec/frontend/static_site_editor/components/submit_changes_error_spec.js
index 1218710a186..7af3014b338 100644
--- a/spec/frontend/static_site_editor/components/submit_changes_error_spec.js
+++ b/spec/frontend/static_site_editor/components/submit_changes_error_spec.js
@@ -19,7 +19,7 @@ describe('Submit Changes Error', () => {
});
};
- const findRetryButton = () => wrapper.findAll(GlButton).at(1);
+ const findRetryButton = () => wrapper.find(GlButton);
const findAlert = () => wrapper.find(GlAlert);
beforeEach(() => {
diff --git a/spec/helpers/projects/project_members_helper_spec.rb b/spec/helpers/projects/project_members_helper_spec.rb
new file mode 100644
index 00000000000..cc290367e34
--- /dev/null
+++ b/spec/helpers/projects/project_members_helper_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ProjectMembersHelper do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ let(:allow_admin_project) { nil }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ allow(helper).to receive(:can?).with(current_user, :admin_project_member, project).and_return(allow_admin_project)
+ end
+
+ shared_examples 'when `current_user` does not have `admin_project_member` permissions' do
+ let(:allow_admin_project) { false }
+
+ it { is_expected.to be(false) }
+ end
+
+ describe '#can_manage_project_members?' do
+ subject { helper.can_manage_project_members?(project) }
+
+ context 'when `current_user` has `admin_project_member` permissions' do
+ let(:allow_admin_project) { true }
+
+ it { is_expected.to be(true) }
+ end
+
+ include_examples 'when `current_user` does not have `admin_project_member` permissions'
+ end
+
+ describe '#show_groups?' do
+ subject { helper.show_groups?(project.project_group_links) }
+
+ context 'when group links exist' do
+ let!(:project_group_link) { create(:project_group_link, project: project) }
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when `search_groups` param is set' do
+ before do
+ allow(helper).to receive(:params).and_return({ search_groups: 'foo' })
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when `search_groups` param is not set and group links do not exist' do
+ it { is_expected.to be(false) }
+ end
+ end
+
+ describe '#show_invited_members?' do
+ subject { helper.show_invited_members?(project, project.project_members.invite) }
+
+ context 'when `current_user` has `admin_project_member` permissions' do
+ let(:allow_admin_project) { true }
+
+ context 'when invited members exist' do
+ let!(:invite) { create(:project_member, :invited, project: project) }
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when invited members do not exist' do
+ it { is_expected.to be(false) }
+ end
+ end
+
+ include_examples 'when `current_user` does not have `admin_project_member` permissions'
+ end
+
+ describe '#show_access_requests?' do
+ subject { helper.show_access_requests?(project, project.requesters) }
+
+ context 'when `current_user` has `admin_project_member` permissions' do
+ let(:allow_admin_project) { true }
+
+ context 'when access requests exist' do
+ let!(:access_request) { create(:project_member, :access_request, project: project) }
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when access requests do not exist' do
+ it { is_expected.to be(false) }
+ end
+ end
+
+ include_examples 'when `current_user` does not have `admin_project_member` permissions'
+ end
+
+ describe '#groups_tab_active?' do
+ subject { helper.groups_tab_active? }
+
+ context 'when `search_groups` param is set' do
+ before do
+ allow(helper).to receive(:params).and_return({ search_groups: 'foo' })
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when `search_groups` param is not set' do
+ it { is_expected.to be(false) }
+ end
+ end
+
+ describe '#current_user_is_group_owner?' do
+ let(:group) { create(:group) }
+
+ subject { helper.current_user_is_group_owner?(project2) }
+
+ describe "when current user is the owner of the project's parent group" do
+ let(:project2) { create(:project, namespace: group) }
+
+ before do
+ group.add_owner(current_user)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ describe "when current user is not the owner of the project's parent group" do
+ let_it_be(:user) { create(:user) }
+ let(:project2) { create(:project, namespace: group) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it { is_expected.to be(false) }
+ end
+
+ describe "when project does not have a parent group" do
+ let(:user) { create(:user) }
+ let(:project2) { create(:project, namespace: user.namespace) }
+
+ it { is_expected.to be(false) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb
index 68a46b11487..764478ad1d7 100644
--- a/spec/lib/gitlab/error_tracking_spec.rb
+++ b/spec/lib/gitlab/error_tracking_spec.rb
@@ -236,7 +236,7 @@ RSpec.describe Gitlab::ErrorTracking do
context 'the exception implements :sentry_extra_data' do
let(:extra_info) { { event: 'explosion', size: :massive } }
- let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info, backtrace: caller) }
+ let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info, backtrace: caller, cause: nil) }
it 'includes the extra data from the exception in the tracking information' do
track_exception
@@ -247,7 +247,7 @@ RSpec.describe Gitlab::ErrorTracking do
end
context 'the exception implements :sentry_extra_data, which returns nil' do
- let(:exception) { double(message: 'bang!', sentry_extra_data: nil, backtrace: caller) }
+ let(:exception) { double(message: 'bang!', sentry_extra_data: nil, backtrace: caller, cause: nil) }
let(:extra) { { issue_url: issue_url } }
it 'just includes the other extra info' do
@@ -287,10 +287,23 @@ RSpec.describe Gitlab::ErrorTracking do
let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1') }
it 'injects the normalized sql query into extra' do
+ allow(Raven.client.transport).to receive(:send_event) do |event|
+ expect(event.extra).to include(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
+ end
+
track_exception
+ end
+ end
- expect(Raven).to have_received(:capture_exception)
- .with(exception, a_hash_including(extra: a_hash_including(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')))
+ context 'when the `ActiveRecord::StatementInvalid` is wrapped in another exception' do
+ let(:exception) { RuntimeError.new(cause: ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1')) }
+
+ it 'injects the normalized sql query into extra' do
+ allow(Raven.client.transport).to receive(:send_event) do |event|
+ expect(event.extra).to include(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
+ end
+
+ track_exception
end
end
end
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 1dbaace1c89..c2dc87b0fb0 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
@@ -12,9 +12,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do
sign_in(maintainer)
visit members_page_path
- if has_tabs
- click_on 'Access requests'
- end
+ click_on 'Access requests'
end
it 'maintainer can see access requests', :js do
@@ -48,11 +46,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do
end
def expect_visible_access_request(entity, user)
- if has_tabs
- expect(page).to have_content "Access requests 1"
- else
- expect(page).to have_content "Users requesting access to #{entity.name} 1"
- end
+ expect(page).to have_content "Access requests 1"
expect(page).to have_content user.name
end
diff --git a/yarn.lock b/yarn.lock
index edce7ab6dec..71a4a088a5a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -861,20 +861,20 @@
eslint-plugin-vue "^6.2.1"
vue-eslint-parser "^7.0.0"
-"@gitlab/svgs@1.177.0":
- version "1.177.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.177.0.tgz#e481ed327a11d3834c8b1668d7485b9eefef97f5"
- integrity sha512-L7DggusgkbubNFCRIYtCuYiLx+t5Hp8y/XIxJ3RM5mqAfxkTR1KxALNLDP9CT7xWieHDhNvgcXAdamGoi0ofDQ==
+"@gitlab/svgs@1.178.0":
+ version "1.178.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.178.0.tgz#069edb8abb4c7137d48f527592476655f066538b"
+ integrity sha512-m1xe5SPgpi9lSFCHHTkkGeScxkqhi7aD8qApL5F4MqCGeKF9IhELIVoMD1R6vkfjzFJh0BwFREPkuwjnAOMKfA==
"@gitlab/tributejs@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
-"@gitlab/ui@25.2.1":
- version "25.2.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.2.1.tgz#2c332134bbc82a6c40ff5fdb73aacccf730629d8"
- integrity sha512-bOkL2sfkovCV6MO/N70Xfe+vTdyi2Vp2efgDvOx4tHzqJllM6Y379wculi0VmdGw3X4TpmPI+zLWAAZ9vkhDAQ==
+"@gitlab/ui@25.3.1":
+ version "25.3.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.3.1.tgz#7557c810397f8c4b81c5360e4642afc3f8274dfc"
+ integrity sha512-vCl74UZgQ5m1caJk8O067KKYa+DP40ES2XDnM/wAc9mZAMynP0GPpePc3cmTLY8vpfzxx2A2iJr04SLgI2pxjA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"