Commit 2cbe7d02 authored by Imre Farkas's avatar Imre Farkas Committed by Dmitriy Zaporozhets

Add frontend support for sharing groups with groups

Implement frontend support for sharing groups with groups
Unify templates for sharing projects and groups with groups
parent 34d91755
...@@ -3,9 +3,12 @@ ...@@ -3,9 +3,12 @@
import Members from 'ee_else_ce/members'; import Members from 'ee_else_ce/members';
import memberExpirationDate from '~/member_expiration_date'; import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate(); memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
new Members(); new Members();
groupsSelect();
new UsersSelect(); new UsersSelect();
}); });
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
.users-project-form { .invite-users-form {
.btn-success { .btn-success {
margin-right: 10px; margin-right: 10px;
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
class Groups::GroupLinksController < Groups::ApplicationController class Groups::GroupLinksController < Groups::ApplicationController
before_action :check_feature_flag! before_action :check_feature_flag!
before_action :authorize_admin_group! before_action :authorize_admin_group!
before_action :group_link, only: [:update, :destroy]
def create def create
shared_with_group = Group.find(params[:shared_with_group_id]) if params[:shared_with_group_id].present? shared_with_group = Group.find(params[:shared_with_group_id]) if params[:shared_with_group_id].present?
...@@ -22,12 +23,35 @@ class Groups::GroupLinksController < Groups::ApplicationController ...@@ -22,12 +23,35 @@ class Groups::GroupLinksController < Groups::ApplicationController
redirect_to group_group_members_path(group) redirect_to group_group_members_path(group)
end end
def update
@group_link.update(group_link_params)
end
def destroy
Groups::GroupLinks::DestroyService.new(nil, nil).execute(@group_link)
respond_to do |format|
format.html do
redirect_to group_group_members_path(group), status: :found
end
format.js { head :ok }
end
end
private private
def group_link
@group_link ||= group.shared_with_group_links.find(params[:id])
end
def group_link_create_params def group_link_create_params
params.permit(:shared_group_access, :expires_at) params.permit(:shared_group_access, :expires_at)
end end
def group_link_params
params.require(:group_link).permit(:group_access, :expires_at)
end
def check_feature_flag! def check_feature_flag!
render_404 unless Feature.enabled?(:share_group_with_group) render_404 unless Feature.enabled?(:share_group_with_group)
end end
......
...@@ -20,28 +20,17 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -20,28 +20,17 @@ class Groups::GroupMembersController < Groups::ApplicationController
:override :override
def index def index
can_manage_members = can?(current_user, :admin_group_member, @group)
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = find_members @members = find_members
if can_manage_members if can_manage_members
@invited_members = @members.invite @skip_groups = @group.related_group_ids
@invited_members = @invited_members.search_invite_email(params[:search_invited]) if params[:search_invited].present? @invited_members = present_invited_members(@members)
@invited_members = present_members(@invited_members.page(params[:invited_members_page]).per(MEMBER_PER_PAGE_LIMIT))
end end
@members = @members.non_invite @members = @members.non_invite
@members = @members.search(params[:search]) if params[:search].present? @members = present_group_members(@members)
@members = @members.sort_by_attribute(@sort)
if can_manage_members && params[:two_factor].present?
@members = @members.filter_by_2fa(params[:two_factor])
end
@members = @members.page(params[:page]).per(MEMBER_PER_PAGE_LIMIT)
@members = present_members(@members)
@requesters = present_members( @requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user)) AccessRequestsFinder.new(@group).execute(current_user))
...@@ -54,8 +43,30 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -54,8 +43,30 @@ class Groups::GroupMembersController < Groups::ApplicationController
private private
def present_invited_members(members)
invited_members = members.invite
if params[:search_invited].present?
invited_members = invited_members.search_invite_email(params[:search_invited])
end
present_members(invited_members
.page(params[:invited_members_page])
.per(MEMBER_PER_PAGE_LIMIT))
end
def find_members def find_members
GroupMembersFinder.new(@group).execute(include_relations: requested_relations) filter_params = params.slice(:two_factor, :search).merge(sort: @sort)
GroupMembersFinder.new(@group, current_user).execute(include_relations: requested_relations, params: filter_params)
end
def can_manage_members
can?(current_user, :admin_group_member, @group)
end
def present_group_members(original_members)
members = original_members.page(params[:page]).per(MEMBER_PER_PAGE_LIMIT)
present_members(members)
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class GroupMembersFinder < UnionFinder class GroupMembersFinder < UnionFinder
def initialize(group) # Params can be any of the following:
# two_factor: string. 'enabled' or 'disabled' are returning different set of data, other values are not effective.
# sort: string
# search: string
def initialize(group, user = nil)
@group = group @group = group
@user = user
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def execute(include_relations: [:inherited, :direct]) def execute(include_relations: [:inherited, :direct], params: {})
group_members = @group.members group_members = group.members
relations = [] relations = []
return group_members if include_relations == [:direct] return group_members if include_relations == [:direct]
relations << group_members if include_relations.include?(:direct) relations << group_members if include_relations.include?(:direct)
if include_relations.include?(:inherited) && @group.parent if include_relations.include?(:inherited) && group.parent
parents_members = GroupMember.non_request parents_members = GroupMember.non_request
.where(source_id: @group.ancestors.select(:id)) .where(source_id: group.ancestors.select(:id))
.where.not(user_id: @group.users.select(:id)) .where.not(user_id: group.users.select(:id))
relations << parents_members relations << parents_members
end end
if include_relations.include?(:descendants) if include_relations.include?(:descendants)
descendant_members = GroupMember.non_request descendant_members = GroupMember.non_request
.where(source_id: @group.descendants.select(:id)) .where(source_id: group.descendants.select(:id))
.where.not(user_id: @group.users.select(:id)) .where.not(user_id: group.users.select(:id))
relations << descendant_members relations << descendant_members
end end
find_union(relations, GroupMember) members = find_union(relations, GroupMember)
filter_members(members, params)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
private
attr_reader :user, :group
def filter_members(members, params)
members = members.search(params[:search]) if params[:search].present?
members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
if can_manage_members && params[:two_factor].present?
members = members.filter_by_2fa(params[:two_factor])
end
members
end
def can_manage_members
Ability.allowed?(user, :admin_group_member, group)
end
end end
GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder') GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder')
...@@ -4,6 +4,10 @@ module Groups::GroupMembersHelper ...@@ -4,6 +4,10 @@ module Groups::GroupMembersHelper
def group_member_select_options def group_member_select_options
{ multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true } { multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true }
end end
def render_invite_member_for_group(group, default_access_level)
render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: GroupMember.access_level_roles, default_access_level: default_access_level
end
end end
Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper') Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper')
...@@ -85,7 +85,8 @@ module SelectsHelper ...@@ -85,7 +85,8 @@ module SelectsHelper
first_user: opts[:first_user] && current_user ? current_user.username : false, first_user: opts[:first_user] && current_user ? current_user.username : false,
current_user: opts[:current_user] || false, current_user: opts[:current_user] || false,
author_id: opts[:author_id] || '', author_id: opts[:author_id] || '',
skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil,
qa_selector: opts[:qa_selector] || ''
} }
end end
end end
......
...@@ -420,6 +420,12 @@ class Group < Namespace ...@@ -420,6 +420,12 @@ class Group < Namespace
GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last
end end
def related_group_ids
[id,
*ancestors.pluck(:id),
*shared_with_group_links.pluck(:shared_with_group_id)]
end
def hashed_storage?(_feature) def hashed_storage?(_feature)
false false
end end
......
...@@ -20,4 +20,8 @@ class GroupGroupLink < ApplicationRecord ...@@ -20,4 +20,8 @@ class GroupGroupLink < ApplicationRecord
def self.default_access def self.default_access
Gitlab::Access::DEVELOPER Gitlab::Access::DEVELOPER
end end
def human_access
Gitlab::Access.human_access(self.group_access)
end
end end
...@@ -21,6 +21,8 @@ class ProjectGroupLink < ApplicationRecord ...@@ -21,6 +21,8 @@ class ProjectGroupLink < ApplicationRecord
after_commit :refresh_group_members_authorized_projects after_commit :refresh_group_members_authorized_projects
alias_method :shared_with_group, :group
def self.access_options def self.access_options
Gitlab::Access.options Gitlab::Access.options
end end
......
= form_for @group_member, url: group_group_members_path(@group), html: { class: 'users-project-form users-group-form' } do |f|
.row
.col-md-4.col-lg-6
= users_select_tag(:user_ids, group_member_select_options)
.form-text.text-muted.append-bottom-10
Search for members by name, username, or email, or invite new ones using their email address.
.col-md-3.col-lg-2
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
.form-text.text-muted.append-bottom-10
= link_to "Read more", help_page_path("user/permissions")
about role permissions
.col-md-3.col-lg-2
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
.form-text.text-muted.append-bottom-10
On this date, the member(s) will automatically lose access to this group and all of its projects.
.col-md-2
= f.submit 'Add to group', class: "btn btn-success btn-block", data: { qa_selector: 'add_to_group_button' }
- page_title _("Members") - page_title _("Group members")
- can_manage_members = can?(current_user, :admin_group_member, @group) - can_manage_members = can?(current_user, :admin_group_member, @group)
- show_invited_members = can_manage_members && @invited_members.exists? - show_invited_members = can_manage_members && @invited_members.exists?
- pending_active = params[:search_invited].present? - pending_active = params[:search_invited].present?
- total_count = @members.count + @group.shared_with_group_links.count
.project-members-page.prepend-top-default .project-members-page.prepend-top-default
%h4 %h4
= _("Members") = _("Group members")
%hr %hr
- if can_manage_members - if can_manage_members
.project-members-new.append-bottom-default - if Feature.enabled?(:share_group_with_group)
%p.clearfix %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
= _("Add new member to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } %li.nav-tab{ role: 'presentation' }
= render "new_group_member" %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member")
%li.nav-tab{ role: 'presentation' }
%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_invite_member_for_group(@group, @group_member.access_level)
- if Feature.enabled?(:share_group_with_group)
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
= render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access'
- else
= render_invite_member_for_group(@group, @group_member.access_level)
= render 'shared/members/requests', membership_source: @group, requesters: @requesters = render 'shared/members/requests', membership_source: @group, requesters: @requesters
...@@ -19,10 +30,10 @@ ...@@ -19,10 +30,10 @@
%ul.nav-links.mobile-separator.nav.nav-tabs.clearfix %ul.nav-links.mobile-separator.nav.nav-tabs.clearfix
%li.nav-item %li.nav-item
= link_to "#existing_members", class: ["nav-link", ("active" unless pending_active)] , 'data-toggle' => 'tab' do = link_to "#existing_shares", class: ["nav-link", ("active" unless pending_active)] , 'data-toggle' => 'tab' do
%span %span
= _("Existing") = _("Existing shares")
%span.badge.badge-pill= @members.total_count %span.badge.badge-pill= total_count
- if show_invited_members - if show_invited_members
%li.nav-item %li.nav-item
= link_to "#invited_members", class: ["nav-link", ("active" if pending_active)], 'data-toggle' => 'tab' do = link_to "#invited_members", class: ["nav-link", ("active" if pending_active)], 'data-toggle' => 'tab' do
...@@ -31,7 +42,16 @@ ...@@ -31,7 +42,16 @@
%span.badge.badge-pill= @invited_members.total_count %span.badge.badge-pill= @invited_members.total_count
.tab-content .tab-content
#existing_members.tab-pane{ :class => ("active" unless pending_active) } #existing_shares.tab-pane{ :class => ("active" unless pending_active) }
- if @group.shared_with_group_links.any?
.card.card-without-border
.d-flex.flex-column.flex-md-row.row-content-block.second-block
%span.flex-grow-1.align-self-md-center.col-form-label
= _("Groups with access to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
%ul.content-list.members-list{ data: { qa_selector: "groups_list" } }
- can_admin_member = can?(current_user, :admin_group_member, @group)
- @group.shared_with_group_links.each do |group_link|
= render 'shared/members/group', group_link: group_link, can_admin_member: can_admin_member, group_link_path: group_group_link_path(@group, group_link)
.card.card-without-border .card.card-without-border
.d-flex.flex-column.flex-md-row.row-content-block.second-block .d-flex.flex-column.flex-md-row.row-content-block.second-block
%span.flex-grow-1.align-self-md-center.col-form-label %span.flex-grow-1.align-self-md-center.col-form-label
...@@ -46,7 +66,7 @@ ...@@ -46,7 +66,7 @@
= label_tag '2fa', '2FA', class: 'col-form-label label-bold pr-md-2' = label_tag '2fa', '2FA', class: 'col-form-label label-bold pr-md-2'
= render 'shared/members/filter_2fa_dropdown' = render 'shared/members/filter_2fa_dropdown'
= render 'shared/members/sort_dropdown' = render 'shared/members/sort_dropdown'
%ul.content-list.members-list %ul.content-list.members-list{ data: { qa_selector: "members_list" } }
= render partial: 'shared/members/member', collection: @members, as: :member = render partial: 'shared/members/member', collection: @members, as: :member
= paginate @members, theme: 'gitlab' = paginate @members, theme: 'gitlab'
......
...@@ -3,4 +3,6 @@ ...@@ -3,4 +3,6 @@
= _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize(@project.name, tags: []) } = _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize(@project.name, tags: []) }
%span.badge.badge-pill= group_links.size %span.badge.badge-pill= group_links.size
%ul.content-list.members-list %ul.content-list.members-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link - can_admin_member = can?(current_user, :admin_project_member, @project)
- @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)
...@@ -13,5 +13,5 @@ ...@@ -13,5 +13,5 @@
%button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") } %button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") }
= icon("search") = icon("search")
= render 'shared/members/sort_dropdown' = render 'shared/members/sort_dropdown'
%ul.content-list.members-list.qa-members-list %ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
= render partial: 'shared/members/member', collection: members, as: :member = render partial: 'shared/members/member', collection: members, as: :member
...@@ -23,13 +23,13 @@ ...@@ -23,13 +23,13 @@
.tab-content.gitlab-tab-content .tab-content.gitlab-tab-content
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
= render 'projects/project_members/new_project_member', tab_title: _('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)
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) } .tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) }
= render 'projects/project_members/new_project_group', tab_title: _('Invite group') = 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? - elsif !membership_locked?
.invite-member= render 'projects/project_members/new_project_member', tab_title: _('Invite member') .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? - elsif @project.allowed_to_share_with_group?
.invite-group= render 'projects/project_members/new_project_group', tab_title: _('Invite 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, requesters: @requesters = render 'shared/members/requests', membership_source: @project, requesters: @requesters
.clearfix .clearfix
......
- group_link = local_assigns[:group_link] - group_link = local_assigns[:group_link]
- group = group_link.group - group = group_link.shared_with_group
- can_admin_member = can?(current_user, :admin_project_member, @project) - can_admin_member = local_assigns[:can_admin_member]
- group_link_path = local_assigns[:group_link_path]
- dom_id = "group_member_#{group_link.id}" - dom_id = "group_member_#{group_link.id}"
-# Note this is just for groups. For individual members please see shared/members/_member -# Note this is just for groups. For individual members please see shared/members/_member
...@@ -17,7 +18,7 @@ ...@@ -17,7 +18,7 @@
%span{ class: ('text-warning' if group_link.expires_soon?) } %span{ class: ('text-warning' if group_link.expires_soon?) }
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(group_link.expires_at) } = _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(group_link.expires_at) }
.controls.member-controls.align-items-center .controls.member-controls.align-items-center
= form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do = form_tag group_link_path, method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do
= hidden_field_tag "group_link[group_access]", group_link.group_access = hidden_field_tag "group_link[group_access]", group_link.group_access
.member-form-control.dropdown.mr-sm-2.d-sm-inline-block .member-form-control.dropdown.mr-sm-2.d-sm-inline-block
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
...@@ -39,7 +40,7 @@ ...@@ -39,7 +40,7 @@
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: _('Expiration date'), id: "member_expires_at_#{group.id}", disabled: !can_admin_member = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: _('Expiration date'), id: "member_expires_at_#{group.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
- if can_admin_member - if can_admin_member
= link_to project_group_link_path(@project, group_link), = link_to group_link_path,
method: :delete, method: :delete,
data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name }, qa_selector: 'delete_group_access_link' }, data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name }, qa_selector: 'delete_group_access_link' },
class: 'btn btn-remove m-0 ml-sm-2 align-self-center' do class: 'btn btn-remove m-0 ml-sm-2 align-self-center' do
......
- access_levels = local_assigns[:access_levels]
- default_access_level = local_assigns[:default_access_level]
- submit_url = local_assigns[:submit_url]
- group_link_field = local_assigns[:group_link_field]
- group_access_field = local_assigns[:group_access_field]
.row .row
.col-sm-12 .col-sm-12
= form_tag project_group_links_path(@project), class: 'js-requires-input', method: :post do = form_tag submit_url, class: 'invite-group-form js-requires-input', method: :post do
.form-group .form-group
= label_tag :link_group_id, _("Select a group to invite"), class: "label-bold" = label_tag group_link_field, _("Select a group to invite"), class: "label-bold"
= groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, class: "input-clamp qa-group-select-field", required: true) = groups_select_tag(group_link_field, data: { skip_groups: @skip_groups }, class: 'input-clamp qa-group-select-field', required: true)
.form-group .form-group
= label_tag :link_group_access, _("Max access level"), class: "label-bold" = label_tag group_access_field, _("Max access level"), class: "label-bold"
.select-wrapper .select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" = select_tag group_access_field, options_for_select(access_levels, default_access_level), data: { qa_selector: 'group_access_field' }, class: "form-control select-control"
= icon('chevron-down') = icon('chevron-down')
.form-text.text-muted.append-bottom-10 .form-text.text-muted.append-bottom-10
- permissions_docs_path = help_page_path('user/permissions') - permissions_docs_path = help_page_path('user/permissions')
......
- access_levels = local_assigns[:access_levels]
- default_access_level = local_assigns[:default_access_level]
- submit_url = local_assigns[:submit_url]
- can_import_members = local_assigns[:can_import_members?]
- import_path = local_assigns[:import_path]
.row .row
.col-sm-12 .col-sm-12
= form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f| = form_tag submit_url, class: 'invite-users-form', method: :post do
.form-group .form-group
= label_tag :user_ids, _("GitLab member or Email address"), class: "label-bold" = label_tag :user_ids, _("GitLab member or Email address"), class: "label-bold"
= users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite") = users_select_tag(:user_ids, multiple: true, class: 'input-clamp qa-member-select-field', scope: :all, email_user: true, placeholder: 'Search for members to update or invite')
.form-group .form-group
= label_tag :access_level, _("Choose a role permission"), class: "label-bold" = label_tag :access_level, _("Choose a role permission"), class: "label-bold"
.select-wrapper .select-wrapper
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control" = select_tag :access_level, options_for_select(access_levels, default_access_level), class: "form-control project-access-select select-control"
= icon('chevron-down') = icon('chevron-down')
.form-text.text-muted.append-bottom-10 .form-text.text-muted.append-bottom-10
- permissions_docs_path = help_page_path('user/permissions') - permissions_docs_path = help_page_path('user/permissions')
...@@ -18,6 +23,6 @@ ...@@ -18,6 +23,6 @@
= label_tag :expires_at, _('Access expiration date'), class: 'label-bold' = label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
= f.submit _("Add to project"), class: "btn btn-success qa-add-member-button" = submit_tag _("Invite"), class: "btn btn-success", data: { qa_selector: 'invite_member_button' }
- if can_import_members? - if can_import_members
= link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project") = link_to _("Import"), import_path, class: "btn btn-default", title: _("Import members from another project")
...@@ -28,7 +28,7 @@ describe "User manages members" do ...@@ -28,7 +28,7 @@ describe "User manages members" do
visit(project_project_members_path(project)) visit(project_project_members_path(project))
end end
it { expect(page).to have_no_button("Add members").and have_no_link("Import members") } it { expect(page).to have_no_selector(".invite-users-form") }
end end
context "as project maintainer" do context "as project maintainer" do
......
...@@ -1068,9 +1068,6 @@ msgstr "" ...@@ -1068,9 +1068,6 @@ msgstr ""
msgid "Add new directory" msgid "Add new directory"
msgstr "" msgstr ""
msgid "Add new member to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
msgid "Add or subtract spent time" msgid "Add or subtract spent time"
msgstr "" msgstr ""
...@@ -1095,9 +1092,6 @@ msgstr "" ...@@ -1095,9 +1092,6 @@ msgstr ""
msgid "Add to merge train when pipeline succeeds" msgid "Add to merge train when pipeline succeeds"
msgstr "" msgstr ""
msgid "Add to project"
msgstr ""
msgid "Add to review" msgid "Add to review"
msgstr "" msgstr ""
...@@ -7458,10 +7452,10 @@ msgstr "" ...@@ -7458,10 +7452,10 @@ msgstr ""
msgid "Excluding merge commits. Limited to 6,000 commits." msgid "Excluding merge commits. Limited to 6,000 commits."
msgstr "" msgstr ""
msgid "Existing" msgid "Existing members and groups"
msgstr "" msgstr ""
msgid "Existing members and groups" msgid "Existing shares"
msgstr "" msgstr ""
msgid "Expand" msgid "Expand"
...@@ -9122,6 +9116,9 @@ msgstr "" ...@@ -9122,6 +9116,9 @@ msgstr ""
msgid "Group maintainers can register group runners in the %{link}" msgid "Group maintainers can register group runners in the %{link}"
msgstr "" msgstr ""
msgid "Group members"
msgstr ""
msgid "Group name" msgid "Group name"
msgstr "" msgstr ""
...@@ -9392,6 +9389,9 @@ msgstr "" ...@@ -9392,6 +9389,9 @@ msgstr ""
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}." msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr "" msgstr ""
msgid "Groups with access to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
msgid "Groups with access to <strong>%{project_name}</strong>" msgid "Groups with access to <strong>%{project_name}</strong>"
msgstr "" msgstr ""
......
...@@ -7,12 +7,9 @@ module QA ...@@ -7,12 +7,9 @@ module QA
class Members < Page::Base class Members < Page::Base
include Page::Component::UsersSelect include Page::Component::UsersSelect
view 'app/views/groups/group_members/_new_group_member.html.haml' do view 'app/views/shared/members/_invite_member.html.haml' do
element :add_to_group_button
end
view 'app/helpers/groups/group_members_helper.rb' do
element :member_select_field element :member_select_field
element :invite_member_button
end end
view 'app/views/shared/members/_member.html.haml' do view 'app/views/shared/members/_member.html.haml' do
...@@ -24,7 +21,7 @@ module QA ...@@ -24,7 +21,7 @@ module QA
def add_member(username) def add_member(username)
select_user :member_select_field, username select_user :member_select_field, username
click_element :add_to_group_button click_element :invite_member_button
end end
def update_access_level(username, access_level) def update_access_level(username, access_level)
......
...@@ -8,9 +8,9 @@ module QA ...@@ -8,9 +8,9 @@ module QA
include Page::Component::UsersSelect include Page::Component::UsersSelect
include QA::Page::Component::Select2 include QA::Page::Component::Select2
view 'app/views/projects/project_members/_new_project_member.html.haml' do view 'app/views/shared/members/_invite_member.html.haml' do
element :member_select_input element :member_select_field
element :add_member_button element :invite_member_button
end end
view 'app/views/projects/project_members/_team.html.haml' do view 'app/views/projects/project_members/_team.html.haml' do
...@@ -21,7 +21,7 @@ module QA ...@@ -21,7 +21,7 @@ module QA
element :invite_group_tab element :invite_group_tab
end end
view 'app/views/projects/project_members/_new_project_group.html.haml' do view 'app/views/shared/members/_invite_group.html.haml' do
element :group_select_field element :group_select_field
element :invite_group_button element :invite_group_button
end end
...@@ -43,8 +43,8 @@ module QA ...@@ -43,8 +43,8 @@ module QA
end end
def add_member(username) def add_member(username)
select_user :member_select_input, username select_user :member_select_field, username
click_element :add_member_button click_element :invite_member_button
end end
def remove_group(group_name) def remove_group(group_name)
......
...@@ -111,4 +111,100 @@ describe Groups::GroupLinksController do ...@@ -111,4 +111,100 @@ describe Groups::GroupLinksController do
end end
end end
end end
describe '#update' do
let!(:link) do
create(:group_group_link, { shared_group: shared_group,
shared_with_group: shared_with_group })
end
let(:expiry_date) { 1.month.from_now.to_date }
subject do
post(:update, params: { group_id: shared_group,
id: link.id,
group_link: { group_access: Gitlab::Access::GUEST,
expires_at: expiry_date } })
end
context 'when user has admin access to the shared group' do
before do
shared_group.add_owner(user)
end
it 'updates existing link' do
expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
expect(link.expires_at).to be_nil
subject
link.reload
expect(link.group_access).to eq(Gitlab::Access::GUEST)
expect(link.expires_at).to eq(expiry_date)
end
end
context 'when user does not have admin access to the shared group' do
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(share_group_with_group: false)
end
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
describe '#destroy' do
let!(:link) do
create(:group_group_link, { shared_group: shared_group,
shared_with_group: shared_with_group })
end
subject do
post(:destroy, params: { group_id: shared_group,
id: link.id })
end
context 'when user has admin access to the shared group' do
before do
shared_group.add_owner(user)
end
it 'deletes existing link' do
expect { subject }.to change(GroupGroupLink, :count).by(-1)
end
end
context 'when user does not have admin access to the shared group' do
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(share_group_with_group: false)
end
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
end end
...@@ -31,6 +31,12 @@ describe Groups::GroupMembersController do ...@@ -31,6 +31,12 @@ describe Groups::GroupMembersController do
expect(assigns(:invited_members).map(&:invite_email)).to match_array(invited.map(&:invite_email)) expect(assigns(:invited_members).map(&:invite_email)).to match_array(invited.map(&:invite_email))
end end
it 'assigns skip groups' do
get :index, params: { group_id: group }
expect(assigns(:skip_groups)).to match_array(group.related_group_ids)
end
it 'restricts search to one email' do it 'restricts search to one email' do
get :index, params: { group_id: group, search_invited: invited.first.invite_email } get :index, params: { group_id: group, search_invited: invited.first.invite_email }
......
...@@ -167,14 +167,14 @@ describe 'Admin Groups' do ...@@ -167,14 +167,14 @@ describe 'Admin Groups' do
it 'adds admin a to a group as developer', :js do it 'adds admin a to a group as developer', :js do
visit group_group_members_path(group) visit group_group_members_path(group)
page.within '.users-group-form' do page.within '.invite-users-form' do
select2(current_user.id, from: '#user_ids', multiple: true) select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level' select 'Developer', from: 'access_level'
end end
click_button 'Add to group' click_button 'Invite'
page.within '.content-list' do page.within '[data-qa-selector="members_list"]' do
expect(page).to have_content(current_user.name) expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer') expect(page).to have_content('Developer')
end end
...@@ -187,7 +187,7 @@ describe 'Admin Groups' do ...@@ -187,7 +187,7 @@ describe 'Admin Groups' do
visit group_group_members_path(group) visit group_group_members_path(group)
page.within '.content-list' do page.within '[data-qa-selector="members_list"]' do
expect(page).to have_content(current_user.name) expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer') expect(page).to have_content('Developer')
end end
...@@ -196,7 +196,7 @@ describe 'Admin Groups' do ...@@ -196,7 +196,7 @@ describe 'Admin Groups' do
visit group_group_members_path(group) visit group_group_members_path(group)
page.within '.content-list' do page.within '[data-qa-selector="members_list"]' do
expect(page).not_to have_content(current_user.name) expect(page).not_to have_content(current_user.name)
expect(page).not_to have_content('Developer') expect(page).not_to have_content('Developer')
end end
......
...@@ -98,12 +98,12 @@ describe "Admin::Projects" do ...@@ -98,12 +98,12 @@ describe "Admin::Projects" do
it 'adds admin a to a project as developer', :js do it 'adds admin a to a project as developer', :js do
visit project_project_members_path(project) visit project_project_members_path(project)
page.within '.users-project-form' do page.within '.invite-users-form' do
select2(current_user.id, from: '#user_ids', multiple: true) select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level' select 'Developer', from: 'access_level'
end end
click_button 'Add to project' click_button 'Invite'
page.within '.content-list' do page.within '.content-list' do
expect(page).to have_content(current_user.name) expect(page).to have_content(current_user.name)
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Groups > Members > Manage groups', :js do
include Select2Helper
include Spec::Support::Helpers::Features::ListRowsHelpers
let(:user) { create(:user) }
let(:shared_with_group) { create(:group) }
let(:shared_group) { create(:group) }
before do
shared_group.add_owner(user)
sign_in(user)
end
context 'with share groups with groups feature flag' do
before do
stub_feature_flags(shared_with_group: true)
end
it 'add group to group' do
visit group_group_members_path(shared_group)
add_group(shared_with_group.id, 'Reporter')
page.within(first_row) do
expect(page).to have_content(shared_with_group.name)
expect(page).to have_content('Reporter')
end
end
it 'remove user from group' do
create(:group_group_link, shared_group: shared_group,
shared_with_group: shared_with_group, group_access: ::Gitlab::Access::DEVELOPER)
visit group_group_members_path(shared_group)
expect(page).to have_content(shared_with_group.name)
accept_confirm do
find(:css, '#existing_shares li', text: shared_with_group.name).find(:css, 'a.btn-remove').click
end
wait_for_requests
expect(page).not_to have_content(shared_with_group.name)
end
it 'update group to owner level' do
create(:group_group_link, shared_group: shared_group,
shared_with_group: shared_with_group, group_access: ::Gitlab::Access::DEVELOPER)
visit group_group_members_path(shared_group)
page.within(first_row) do
click_button('Developer')
click_link('Maintainer')
wait_for_requests
expect(page).to have_button('Maintainer')
end
end
def add_group(id, role)
page.click_link 'Invite group'
page.within ".invite-group-form" do
select2(id, from: "#shared_with_group_id")
select(role, from: "shared_group_access")
click_button "Invite"
end
end
end
context 'without share groups with groups feature flag' do
before do
stub_feature_flags(share_group_with_group: false)
end
it 'does not render invitation form and tabs' do
visit group_group_members_path(shared_group)
expect(page).not_to have_link('Invite member')
expect(page).not_to have_link('Invite group')
end
end
end
...@@ -113,7 +113,8 @@ describe 'Groups > Members > Manage members' do ...@@ -113,7 +113,8 @@ describe 'Groups > Members > Manage members' do
visit group_group_members_path(group) visit group_group_members_path(group)
expect(page).not_to have_button 'Add to group' expect(page).not_to have_selector '.invite-users-form'
expect(page).not_to have_selector '.invite-group-form'
page.within(second_row) do page.within(second_row) do
# Can not modify user2 role # Can not modify user2 role
...@@ -125,11 +126,10 @@ describe 'Groups > Members > Manage members' do ...@@ -125,11 +126,10 @@ describe 'Groups > Members > Manage members' do
end end
def add_user(id, role) def add_user(id, role)
page.within ".users-group-form" do page.within ".invite-users-form" do
select2(id, from: "#user_ids", multiple: true) select2(id, from: "#user_ids", multiple: true)
select(role, from: "access_level") select(role, from: "access_level")
click_button "Invite"
end end
click_button "Add to group"
end end
end end
...@@ -24,7 +24,7 @@ describe 'Search group member' do ...@@ -24,7 +24,7 @@ describe 'Search group member' do
find('.user-search-btn').click find('.user-search-btn').click
end end
group_members_list = find(".card .content-list") group_members_list = find('[data-qa-selector="members_list"]')
expect(group_members_list).to have_content(member.name) expect(group_members_list).to have_content(member.name)
expect(group_members_list).not_to have_content(user.name) expect(group_members_list).not_to have_content(user.name)
end end
......
...@@ -87,12 +87,12 @@ describe 'Project members list' do ...@@ -87,12 +87,12 @@ describe 'Project members list' do
end end
def add_user(id, role) def add_user(id, role)
page.within ".users-project-form" do page.within ".invite-users-form" do
select2(id, from: "#user_ids", multiple: true) select2(id, from: "#user_ids", multiple: true)
select(role, from: "access_level") select(role, from: "access_level")
end end
click_button "Add to project" click_button "Invite"
end end
def visit_members_page def visit_members_page
......
...@@ -20,10 +20,10 @@ describe 'Projects > Members > Maintainer adds member with expiration date', :js ...@@ -20,10 +20,10 @@ describe 'Projects > Members > Maintainer adds member with expiration date', :js
date = 4.days.from_now date = 4.days.from_now
visit project_project_members_path(project) visit project_project_members_path(project)
page.within '.users-project-form' do page.within '.invite-users-form' do
select2(new_member.id, from: '#user_ids', multiple: true) select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: date.to_s(:medium) + "\n" fill_in 'expires_at', with: date.to_s(:medium) + "\n"
click_on 'Add to project' click_on 'Invite'
end end
page.within "#project_member_#{new_member.project_members.first.id}" do page.within "#project_member_#{new_member.project_members.first.id}" do
......
...@@ -37,7 +37,7 @@ describe 'Projects > Settings > User manages project members' do ...@@ -37,7 +37,7 @@ describe 'Projects > Settings > User manages project members' do
visit(project_project_members_path(project)) visit(project_project_members_path(project))
page.within('.users-project-form') do page.within('.invite-users-form') do
click_link('Import') click_link('Import')
end end
......
...@@ -10,6 +10,7 @@ describe GroupMembersFinder, '#execute' do ...@@ -10,6 +10,7 @@ describe GroupMembersFinder, '#execute' do
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) } let(:user3) { create(:user) }
let(:user4) { create(:user) } let(:user4) { create(:user) }
let(:user5) { create(:user, :two_factor_via_otp) }
it 'returns members for top-level group' do it 'returns members for top-level group' do
member1 = group.add_maintainer(user1) member1 = group.add_maintainer(user1)
...@@ -67,4 +68,43 @@ describe GroupMembersFinder, '#execute' do ...@@ -67,4 +68,43 @@ describe GroupMembersFinder, '#execute' do
expect(result.to_a).to match_array([member1, member2, member3, member4]) expect(result.to_a).to match_array([member1, member2, member3, member4])
end end
it 'returns searched members if requested' do
group.add_maintainer(user2)
nested_group.add_maintainer(user2)
nested_group.add_maintainer(user3)
nested_group.add_maintainer(user4)
member = group.add_maintainer(user1)
result = described_class.new(group).execute(include_relations: [:direct, :descendants], params: { search: user1.name })
expect(result.to_a).to match_array([member])
end
it 'returns members with two-factor auth if requested by owner' do
group.add_owner(user2)
group.add_maintainer(user1)
nested_group.add_maintainer(user2)
nested_group.add_maintainer(user3)
nested_group.add_maintainer(user4)
member = group.add_maintainer(user5)
result = described_class.new(group, user2).execute(include_relations: [:direct, :descendants], params: { two_factor: 'enabled' })
expect(result.to_a).to contain_exactly(member)
end
it 'returns members without two-factor auth if requested by owner' do
member1 = group.add_owner(user2)
member2 = group.add_maintainer(user1)
nested_group.add_maintainer(user2)
member3 = nested_group.add_maintainer(user3)
member4 = nested_group.add_maintainer(user4)
member_with_2fa = group.add_maintainer(user5)
result = described_class.new(group, user2).execute(include_relations: [:direct, :descendants], params: { two_factor: 'disabled' })
expect(result.to_a).not_to include(member_with_2fa)
expect(result.to_a).to match_array([member1, member2, member3, member4])
end
end end
...@@ -33,4 +33,12 @@ describe GroupGroupLink do ...@@ -33,4 +33,12 @@ describe GroupGroupLink do
validate_inclusion_of(:group_access).in_array(Gitlab::Access.values)) validate_inclusion_of(:group_access).in_array(Gitlab::Access.values))
end end
end end
describe '#human_access' do
it 'delegates to Gitlab::Access' do
expect(Gitlab::Access).to receive(:human_access).with(group_group_link.group_access)
group_group_link.human_access
end
end
end end
...@@ -1003,6 +1003,57 @@ describe Group do ...@@ -1003,6 +1003,57 @@ describe Group do
end end
end end
describe '#related_group_ids' do
let(:nested_group) { create(:group, parent: group) }
let(:shared_with_group) { create(:group, parent: group) }
before do
create(:group_group_link, shared_group: nested_group,
shared_with_group: shared_with_group)
end
subject(:related_group_ids) { nested_group.related_group_ids }
it 'returns id' do
expect(related_group_ids).to include(nested_group.id)
end
it 'returns ancestor id' do
expect(related_group_ids).to include(group.id)
end
it 'returns shared with group id' do
expect(related_group_ids).to include(shared_with_group.id)
end
context 'with more than one ancestor group' do
let(:ancestor_group) { create(:group) }
before do
group.update(parent: ancestor_group)
end
it 'returns all ancestor group ids' do
expect(related_group_ids).to(
include(group.id, ancestor_group.id))
end
end
context 'with more than one shared with group' do
let(:another_shared_with_group) { create(:group, parent: group) }
before do
create(:group_group_link, shared_group: nested_group,
shared_with_group: another_shared_with_group)
end
it 'returns all shared with group ids' do
expect(related_group_ids).to(
include(shared_with_group.id, another_shared_with_group.id))
end
end
end
context 'with uploads' do context 'with uploads' do
it_behaves_like 'model with uploads', true do it_behaves_like 'model with uploads', true do
let(:model_object) { create(:group, :with_avatar) } let(:model_object) { create(:group, :with_avatar) }
......
...@@ -25,7 +25,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do ...@@ -25,7 +25,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do
expect_no_visible_access_request(entity, user) expect_no_visible_access_request(entity, user)
page.within('.members-list') do page.within('[data-qa-selector="members_list"]') do
expect(page).to have_content user.name expect(page).to have_content user.name
end end
end 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