This instance will be upgraded to Heptapod 0.26.0rc1 on 2021-10-26 at 14:30 UTC+2 (a few minutes of down time)

Commit 515205d3 authored by Rémy Coutable's avatar Rémy Coutable
Browse files

UI and copywriting improvements



+ Move 'Edit Project/Group' out of membership-related partial
+ Show the access request buttons only to logged-in users
+ Put the request access buttons out of in a more visible button
+ Improve the copy in the #remove_member_message helper
Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 57d2667f34b1
......@@ -39,3 +39,20 @@
}
}
}
.groups-cover-block {
.container-fluid {
position: relative;
}
.access-request-button {
@include btn-gray;
position: absolute;
right: 16px;
bottom: 32px;
padding: 3px 10px;
text-transform: none;
background-color: $background-color;
}
}
......@@ -229,13 +229,20 @@
right: 16px;
bottom: 0;
.btn {
padding: 3px 10px;
background-color: $background-color;
@media (max-width: $screen-lg-min) {
top: 0;
}
@media (max-width: 1304px) {
top: 0;
.access-request-button {
position: absolute;
right: 0;
bottom: 61px;
@media (max-width: $screen-lg-min) {
position: relative;
bottom: 0;
margin-right: 10px;
}
}
}
......
module AccessRequestActions
extend ActiveSupport::Concern
def request_access
access_requestable_resource.request_access(current_user)
redirect_to access_requestable_resource_path,
notice: 'Your request for access has been queued for review.'
end
def approve_access_request
@member = access_requestable_resource.public_send(member_entity_name.pluralize).request.find(params[:id])
return render_403 unless can?(current_user, :"update_#{member_entity_name}", @member)
@member.accept_request
redirect_to access_requestable_resource_members_path
end
protected
def access_requestable_resource
raise NotImplementedError
end
def access_requestable_resource_path
access_requestable_resource
end
def access_requestable_resource_members_path
[access_requestable_resource, 'members']
end
def member_entity_name
"#{access_requestable_resource.class.to_s.underscore}_member"
end
end
module MembershipActions
extend ActiveSupport::Concern
include MembersHelper
def request_access
membershipable.request_access(current_user)
redirect_to polymorphic_path(membershipable),
notice: 'Your request for access has been queued for review.'
end
def approve_access_request
@member = membershipable.members.request.find(params[:id])
return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
@member.accept_request
redirect_to polymorphic_url([membershipable, :members])
end
def leave
@member = membershipable.members.find_by(user_id: current_user)
return render_403 unless @member
source_type = @member.real_source_type.humanize(capitalize: false)
if can?(current_user, action_member_permission(:destroy, @member), @member)
notice =
if @member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{@member.source.human_name}\" #{source_type}."
end
@member.destroy
redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice
else
if cannot_leave?
alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}."
alert << " Transfer or delete the #{source_type}."
redirect_to polymorphic_url(membershipable), alert: alert
else
render_403
end
end
end
protected
def membershipable
raise NotImplementedError
end
def cannot_leave?
raise NotImplementedError
end
end
class Groups::GroupMembersController < Groups::ApplicationController
include AccessRequestActions
include MembershipActions
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
......@@ -38,7 +38,7 @@ def destroy
return render_403 unless can?(current_user, :destroy_group_member, @group_member)
@group_member.request? ? @group_member.decline_request : @group_member.destroy
@group_member.destroy
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
......@@ -60,46 +60,16 @@ def resend_invite
end
end
def leave
@group_member =
@group.group_members.find_by(user_id: current_user.id) ||
@group.group_members.find_by(created_by_id: current_user.id)
if can?(current_user, :destroy_group_member, @group_member)
notice =
if @group_member.request?
'You withdrawn your access request to the group.'
else
"You left #{@group.name} group."
end
@group_member.destroy
redirect_to dashboard_groups_path, notice: notice
else
if @group.last_owner?(current_user)
redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
else
return render_403
end
end
end
protected
def member_params
params.require(:group_member).permit(:access_level, :user_id)
end
# AccessRequestActions concern
def access_requestable_resource
@group
end
def access_requestable_resource_path
group_path(@group)
end
# MembershipActions concern
alias_method :membershipable, :group
def access_requestable_resource_members_path
group_group_members_path(@group)
def cannot_leave?
@group.last_owner?(current_user)
end
end
class Projects::ProjectMembersController < Projects::ApplicationController
include AccessRequestActions
include MembershipActions
# Authorize
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
......@@ -52,7 +52,7 @@ def destroy
return render_403 unless can?(current_user, :destroy_project_member, @project_member)
@project_member.request? ? @project_member.decline_request : @project_member.destroy
@project_member.destroy
respond_to do |format|
format.html do
......@@ -76,31 +76,6 @@ def resend_invite
end
end
def leave
@project_member =
@project.project_members.find_by(user_id: current_user.id) ||
@project.project_members.find_by(created_by_id: current_user.id)
if can?(current_user, :destroy_project_member, @project_member)
notice =
if @project_member.request?
'You withdrawn your access request to the project.'
else
'You left the project.'
end
@project_member.destroy
redirect_to dashboard_projects_path, notice: notice
else
if current_user == @project.owner
message = 'You can not leave your own project. Transfer or delete the project.'
redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
else
render_403
end
end
end
def apply_import
source_project = Project.find(params[:source_project_id])
......@@ -121,16 +96,10 @@ def member_params
params.require(:project_member).permit(:user_id, :access_level)
end
# AccessRequestActions concern
def access_requestable_resource
@project
end
def access_requestable_resource_path
namespace_project_path(@project.namespace, @project)
end
# MembershipActions concern
alias_method :membershipable, :project
def access_requestable_resource_members_path
namespace_project_project_members_path(@project.namespace, @project)
def cannot_leave?
current_user == @project.owner
end
end
......@@ -13,10 +13,23 @@
# merge_request_path(merge_request)
#
module GitlabRoutingHelper
# Project
def project_path(project, *args)
namespace_project_path(project.namespace, project, *args)
end
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def edit_project_url(project, *args)
edit_namespace_project_url(project.namespace, project, *args)
end
def project_files_path(project, *args)
namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
end
......@@ -41,10 +54,6 @@ def activity_project_path(project, *args)
activity_namespace_project_path(project.namespace, project, *args)
end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def runners_path(project, *args)
namespace_project_runners_path(project.namespace, project, *args)
end
......@@ -65,14 +74,6 @@ def milestone_path(entity, *args)
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
def edit_project_url(project, *args)
edit_namespace_project_url(project.namespace, project, *args)
end
def issue_url(entity, *args)
namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
end
......@@ -92,4 +93,56 @@ def toggle_subscription_path(entity, *args)
toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
end
end
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
end
def project_member_path(project_member, *args)
namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
def request_access_project_members_path(project, *args)
request_access_namespace_project_project_members_path(project.namespace, project)
end
def leave_project_members_path(project, *args)
leave_namespace_project_project_members_path(project.namespace, project)
end
def approve_access_request_project_member_path(project_member, *args)
approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
def resend_invite_project_member_path(project_member, *args)
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
# Groups
## Members
def group_members_url(group, *args)
group_group_members_url(group, *args)
end
def group_member_path(group_member, *args)
group_group_member_path(group_member.source, group_member)
end
def request_access_group_members_path(group, *args)
request_access_group_group_members_path(group)
end
def leave_group_members_path(group, *args)
leave_group_group_members_path(group)
end
def approve_access_request_group_member_path(group_member, *args)
approve_access_request_group_group_member_path(group_member.source, group_member)
end
def resend_invite_group_member_path(group_member, *args)
resend_invite_group_group_member_path(group_member.source, group_member)
end
end
module MembersHelper
def member_class(member)
"#{member.source.class.to_s}Member".constantize
end
def members_association(entity)
"#{entity.class.to_s.underscore}_members".to_sym
end
# Returns a `<action>_<source>_member` association, e.g.:
# - admin_project_member, update_project_member, destroy_project_member
# - admin_group_member, update_group_member, destroy_group_member
def action_member_permission(action, member)
"#{action}_#{member.source.class.to_s.underscore}_member".to_sym
"#{action}_#{member.type.underscore}".to_sym
end
def can_see_entity_roles?(user, entity)
def can_see_member_roles?(source:, user: nil)
return false unless user
user.is_admin? || entity.send(members_association(entity)).exists?(user_id: user.id)
end
def member_path(member)
case member.source
when Project
namespace_project_project_member_path(member.source.namespace, member.source, member)
when Group
group_group_member_path(member.source, member)
else
raise ArgumentError.new('Unknown object class')
end
user.is_admin? || source.members.exists?(user_id: user.id)
end
def resend_invite_member_path(member)
case member.source
when Project
resend_invite_namespace_project_project_member_path(member.source.namespace, member.source, member)
when Group
resend_invite_group_group_member_path(member.source, member)
else
raise ArgumentError.new('Unknown object class')
end
end
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
def request_access_path(entity)
case entity
when Project
request_access_namespace_project_project_members_path(entity.namespace, entity)
when Group
request_access_group_group_members_path(entity)
else
raise ArgumentError.new('Unknown object class')
end
end
def approve_request_member_path(member)
case member.source
when Project
approve_access_request_namespace_project_project_member_path(member.source.namespace, member.source, member)
when Group
approve_access_request_group_group_member_path(member.source, member)
else
raise ArgumentError.new('Unknown object class')
end
end
text = 'Are you sure you want to '
action =
if member.request?
if member.user == user
'withdraw your access request for'
else
"deny #{member.user.name}'s request to join"
end
elsif member.invite?
"revoke the invitation for #{member.invite_email} to join"
else
"remove #{member.user.name} from"
end
def leave_path(entity)
case entity
when Project
leave_namespace_project_project_members_path(entity.namespace, entity)
when Group
leave_group_group_members_path(entity)
else
raise ArgumentError.new('Unknown object class')
end
end
def withdraw_request_message(entity)
"Are you sure you want to withdraw your access request for the \"#{entity_name(entity)}\" #{entity_type(entity)}?"
end
def remove_member_message(member)
entity = member.source
entity_type = entity_type(entity)
entity_name = entity_name(entity)
if member.request?
"You are going to deny #{member.created_by.name}'s request to join the #{entity_name} #{entity_type}. Are you sure?"
elsif member.invite?
"You are going to revoke the invitation for #{member.invite_email} to join the #{entity_name} #{entity_type}. Are you sure?"
else
"You are going to remove #{member.user.name} from the #{entity_name} #{entity_type}. Are you sure?"
end
text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
end
def remove_member_title(member)
member.request? ? 'Deny access request' : 'Remove user'
end
def leave_confirmation_message(entity)
"Are you sure you want to leave \"#{entity_name(entity)}\" #{entity_type(entity)}?"
end
private
text = " from #{member.real_source_type.humanize(capitalize: false)}"
def entity_type(entity)
entity.class.to_s.underscore
text.prepend(member.request? ? 'Deny access request' : 'Remove user')
end
def entity_name(entity)
case entity
when Project
entity.name_with_namespace
when Group
entity.name
else
raise ArgumentError.new('Unknown object class')
end
def leave_confirmation_message(member_source)
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end
end
module ProjectsHelper
def max_access_level(project, user)
Gitlab::Access.options_with_owner.key(project.team.max_member_access(user.id))
end
def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
......
module Emails
module Members
extend ActiveSupport::Concern
include MembersHelper
included do
attr_reader :member_target_type
helper_method :member, :access_requester, :member_target_type, :member_target_name, :member_target_url
helper_method :member_source, :member
end
def member_access_requested_email(member_target_type, member_id)
@member_target_type = member_target_type
def member_access_requested_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
admins = User.where(id: target.public_send(members_association).admins.pluck(:user_id)).pluck(:notification_email)
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
mail(to: admins,
subject: subject("Request to join the #{member_target_name} #{member_target_type}"))
subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
def member_access_granted_email(member_target_type, member_id)
@member_target_type = member_target_type
def member_access_granted_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
mail(to: member.user.notification_email,
subject: subject("Access to the #{member_target_name} #{member_target_type} was granted"))
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
end
def member_access_denied_email(member_target_type, target_id, user_id)
@member_target_type = member_target_type
@target = target_class.find(target_id)
def member_access_denied_email(member_source_type, source_id, user_id)
@member_source_type = member_source_type
@member_source = member_source_class.find(source_id)
requester = User.find(user_id)
mail(to: User.find(user_id).notification_email,
subject: subject("Access to the #{member_target_name} #{member_target_type} was denied"))
mail(to: requester.notification_email,
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
end
def member_invited_email(member_target_type, member_id, token)
@member_target_type = member_target_type
def member_invited_email(member_source_type, member_id, token)
@member_source_type = member_source_type
@member_id = member_id
@token = token