Commit 4c7f2e23 authored by Peter Hegman's avatar Peter Hegman Committed by Ash McKenzie

Reorganize project members into tabs

Move "Members", "Groups", "Invites" and "Access requests" into their
own tabs
parent d7f25636
......@@ -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
......
# 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
......@@ -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
......
.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)
- 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',
......
- 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 }
---
title: Reorganize project member management into tabs
merge_request: 49764
author:
type: changed
......@@ -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.
......
......@@ -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:
......
......@@ -75,7 +75,9 @@ RSpec.describe 'Project > Members > Invite group and members', :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
......
......@@ -11517,9 +11517,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 ""
......@@ -12386,9 +12383,6 @@ msgstr ""
msgid "Find by path"
msgstr ""
msgid "Find existing members by name"
msgstr ""
msgid "Find file"
msgstr ""
......@@ -17218,6 +17212,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 ""
......@@ -30652,6 +30649,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 ""
......
......@@ -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
......
......@@ -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
......
......@@ -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 }
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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)
......
......@@ -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')
......
......@@ -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
......
# 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
......@@ -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
......
# 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
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment