Commit 66855f62 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'members-ui' into 'master'

Project members UI

## What does this MR do?

New UI for project members that includes groups.

## Screenshots (if relevant)


### Project members

![Screen_Shot_2016-09-02_at_15.13.27](/uploads/b9d4a634d44b7b7bbb6eddb10aee86bd/Screen_Shot_2016-09-02_at_15.13.27.png)

### Group members

![Screen_Shot_2016-09-02_at_15.13.36](/uploads/c15c173e68b2c0b49bcd06ca560269d3/Screen_Shot_2016-09-02_at_15.13.36.png)

## What are the relevant issue numbers?

Part of #19868 

Closes #21320 

See merge request !6148
parents fd2b79b6 9ec7aeac
...@@ -140,12 +140,12 @@ ...@@ -140,12 +140,12 @@
break; break;
case 'groups:group_members:index': case 'groups:group_members:index':
new gl.MemberExpirationDate(); new gl.MemberExpirationDate();
new GroupMembers(); new gl.Members();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:project_members:index': case 'projects:project_members:index':
new gl.MemberExpirationDate(); new gl.MemberExpirationDate();
new ProjectMembers(); new gl.Members();
new UsersSelect(); new UsersSelect();
break; break;
case 'groups:new': case 'groups:new':
......
(function() {
this.GroupMembers = (function() {
function GroupMembers() {
$('li.group_member').bind('ajax:success', function() {
return $(this).fadeOut();
});
}
return GroupMembers;
})();
}).call(this);
...@@ -14,14 +14,18 @@ ...@@ -14,14 +14,18 @@
inputs.datepicker({ inputs.datepicker({
dateFormat: 'yy-mm-dd', dateFormat: 'yy-mm-dd',
minDate: 1, minDate: 1,
onSelect: toggleClearInput onSelect: function () {
$(this).trigger('change');
toggleClearInput.call(this);
}
}); });
inputs.next('.js-clear-input').on('click', function(event) { inputs.next('.js-clear-input').on('click', function(event) {
event.preventDefault(); event.preventDefault();
var input = $(this).closest('.clearable-input').find('.js-access-expiration-date'); var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
input.datepicker('setDate', null); input.datepicker('setDate', null)
.trigger('change');
toggleClearInput.call(input); toggleClearInput.call(input);
}); });
......
((w) => {
w.gl = w.gl || {};
class Members {
constructor() {
this.addListeners();
}
addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit);
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess);
}
removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
.fadeOut(function () {
$(this).remove();
});
}
}
formSubmit() {
$(this).closest('form').trigger("submit.rails").end().disable();
}
formSuccess() {
$(this).find('.js-member-update-control').enable();
}
}
gl.Members = Members;
})(window);
(function() {
this.ProjectMembers = (function() {
function ProjectMembers() {
$('li.project_member').bind('ajax:success', function() {
return $(this).fadeOut();
});
}
return ProjectMembers;
})();
}).call(this);
...@@ -125,7 +125,3 @@ label { ...@@ -125,7 +125,3 @@ label {
border-right: 0; border-right: 0;
} }
} }
.help-block {
margin-bottom: 0;
}
...@@ -128,6 +128,10 @@ ul.content-list { ...@@ -128,6 +128,10 @@ ul.content-list {
color: $gl-dark-link-color; color: $gl-dark-link-color;
} }
.member-group-link {
color: $blue-normal;
}
.description { .description {
p { p {
@include str-truncated; @include str-truncated;
...@@ -168,6 +172,14 @@ ul.content-list { ...@@ -168,6 +172,14 @@ ul.content-list {
} }
} }
.member-controls {
float: none;
@media (min-width: $screen-sm-min) {
float: right;
}
}
// When dragging a list item // When dragging a list item
&.ui-sortable-helper { &.ui-sortable-helper {
border-bottom: none; border-bottom: none;
......
...@@ -13,6 +13,11 @@ ...@@ -13,6 +13,11 @@
.dropdown-menu-toggle { .dropdown-menu-toggle {
line-height: 20px; line-height: 20px;
} }
.badge {
margin-top: -2px;
margin-left: 5px;
}
} }
.panel-body { .panel-body {
......
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
background: none; background: none;
.select2-search-field input { .select2-search-field input {
padding: $gl-padding / 2; padding: 5px $gl-padding / 2;
font-size: 13px; font-size: 13px;
height: auto; height: auto;
font-family: inherit; font-family: inherit;
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
} }
.select2-search-choice { .select2-search-choice {
margin: 8px 0 0 8px; margin: 5px 0 0 8px;
box-shadow: none; box-shadow: none;
border-color: $input-border; border-color: $input-border;
color: $gl-text-color; color: $gl-text-color;
......
.member-search-form {
float: left;
input[type='search'] {
width: 225px;
vertical-align: bottom;
@media (max-width: $screen-xs-max) {
width: 100px;
vertical-align: bottom;
}
}
}
.milestone-row { .milestone-row {
@include str-truncated(90%); @include str-truncated(90%);
} }
......
.project-members-title {
padding-bottom: 10px;
border-bottom: 1px solid $border-color;
}
.member {
.list-item-name {
@media (min-width: $screen-sm-min) {
float: left;
width: 50%;
}
strong {
font-weight: 600;
}
}
.controls {
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: flex;
width: 400px;
max-width: 50%;
}
}
.form-horizontal {
margin-top: 5px;
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: flex;
width: 100%;
margin-top: 3px;
}
}
.btn-remove {
width: 100%;
@media (min-width: $screen-sm-min) {
width: auto;
}
}
}
.member-form-control {
@media (max-width: $screen-xs-max) {
padding: 5px 0;
margin-left: 0;
margin-right: 0;
}
@media (min-width: $screen-sm-min) {
width: 50%;
}
}
.member-access-text {
margin-left: auto;
line-height: 43px;
}
.member.existing-title {
@media (min-width: $screen-sm-min) {
float: left;
}
}
.member-search-form {
position: relative;
@media (min-width: $screen-sm-min) {
float: right;
}
.form-control {
width: 100%;
padding-right: 35px;
@media (min-width: $screen-sm-min) {
width: 350px;
}
}
}
.member-search-btn {
position: absolute;
right: 0;
top: 0;
height: 35px;
padding-left: 10px;
padding-right: 10px;
color: $gray-darkest;
background: transparent;
border: 0;
outline: 0;
}
class Projects::GroupLinksController < Projects::ApplicationController class Projects::GroupLinksController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :authorize_admin_project_member!, only: [:update]
def index def index
@group_links = project.project_group_links.all @group_links = project.project_group_links.all
...@@ -27,9 +28,26 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -27,9 +28,26 @@ class Projects::GroupLinksController < Projects::ApplicationController
redirect_to namespace_project_group_links_path(project.namespace, project) redirect_to namespace_project_group_links_path(project.namespace, project)
end end
def update
@group_link = @project.project_group_links.find(params[:id])
@group_link.update_attributes(group_link_params)
end
def destroy def destroy
project.project_group_links.find(params[:id]).destroy project.project_group_links.find(params[:id]).destroy
respond_to do |format|
format.html do
redirect_to namespace_project_group_links_path(project.namespace, project) redirect_to namespace_project_group_links_path(project.namespace, project)
end end
format.js { head :ok }
end
end
protected
def group_link_params
params.require(:group_link).permit(:group_access, :expires_at)
end
end end
...@@ -5,34 +5,23 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -5,34 +5,23 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index def index
@group_links = @project.project_group_links
@project_members = @project.project_members @project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
if params[:search].present? if params[:search].present?
users = @project.users.search(params[:search]).to_a users = @project.users.search(params[:search]).to_a
@project_members = @project_members.where(user_id: users) @project_members = @project_members.where(user_id: users)
end
@project_members = @project_members.order('access_level DESC')
@group = @project.group
if @group @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
@group_members = @group.group_members
@group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@group_members = @group_members.where(user_id: users)
end end
@group_members = @group_members.order('access_level DESC') @project_members = @project_members.order(access_level: :desc).page(params[:page])
end
@requesters = AccessRequestsFinder.new(@project).execute(current_user) @requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new @project_member = @project.project_members.new
@project_group_links = @project.project_group_links
end end
def create def create
...@@ -43,6 +32,21 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -43,6 +32,21 @@ class Projects::ProjectMembersController < Projects::ApplicationController
current_user: current_user current_user: current_user
) )
if params[:group_ids].present?
group_ids = params[:group_ids].split(',')
groups = Group.where(id: group_ids)
groups.each do |group|
next unless can?(current_user, :read_group, group)
project.project_group_links.create(
group: group,
group_access: params[:access_level],
expires_at: params[:expires_at]
)
end
end
redirect_to namespace_project_project_members_path(@project.namespace, @project) redirect_to namespace_project_project_members_path(@project.namespace, @project)
end end
......
= form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| = form_for @group_member, url: group_group_members_path(@group), html: { class: 'users-project-form users-group-form' } do |f|
.form-group .row
= f.label :user_ids, "People", class: 'control-label' .col-md-4.col-lg-6
.col-sm-10 = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true)
= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true) .help-block.append-bottom-10
.help-block
Search for users by name, username, or email, or invite new ones using their email address. Search for users by name, username, or email, or invite new ones using their email address.
.form-group .col-md-3.col-lg-2
= f.label :access_level, "Group Access", class: 'control-label' = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
.col-sm-10 .help-block.append-bottom-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "project-access-select select2" = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
.help-block about role permissions
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
.form-group .col-md-3.col-lg-2
= f.label :expires_at, 'Access expiration date', class: 'control-label'
.col-sm-10
.clearable-input .clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access 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
.help-block .help-block.append-bottom-10
On this date, the user(s) will automatically lose access to this group and all of its projects. On this date, the user(s) will automatically lose access to this group and all of its projects.
.form-actions .col-md-2
= f.submit 'Add users to group', class: "btn btn-create" = f.submit 'Add to group', class: "btn btn-create btn-block"
- page_title "Members" - page_title "Members"
.group-members-page.prepend-top-default .project-members-page.prepend-top-default
%h4
Members
%hr
- if can?(current_user, :admin_group_member, @group) - if can?(current_user, :admin_group_member, @group)
.panel.panel-default .project-members-new.append-bottom-default
.panel-heading %p.clearfix
Add new user to group Add new user to
.panel-body %strong= @group.name
%p.light
Members of group have access to all group projects.
.new-group-member-holder
= render "new_group_member" = render "new_group_member"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters = render 'shared/members/requests', membership_source: @group, requesters: @requesters
.append-bottom-default.clearfix
%h5.member.existing-title
Existing users
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Users with access to
%strong #{@group.name} %strong #{@group.name}
group members
%span.badge= @members.total_count %span.badge= @members.total_count
.controls
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list %ul.content-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'
:javascript
$('form.member-search-form').on('submit', function(event) {
event.preventDefault();
Turbolinks.visit(this.action + '?' + $(this).serialize());
});
:plain :plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}'); var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
new gl.MemberExpirationDate(); $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
:plain
var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}');
$("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name'));
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Group members with access to
%strong #{@group.name} %strong #{@group.name}
group members
%span.badge= members.size %span.badge= members.size
- if can?(current_user, :admin_group_member, @group) - if can?(current_user, :admin_group_member, @group)
.controls .controls
......
.panel.panel-default.project-members-groups
.panel-heading
Groups with access to
%strong #{@project.name}
%span.badge= group_links.size
%ul.content-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link
= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f| = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
.form-group .row
= f.label :user_ids, "People", class: 'control-label' .col-md-4.col-lg-6
.col-sm-10 = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true)
= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true) .help-block.append-bottom-10
.help-block
Search for users by name, username, or email, or invite new ones using their email address. Search for users by name, username, or email, or invite new ones using their email address.
.form-group .col-md-3.col-lg-2
= f.label :access_level, "Project Access", class: 'control-label' = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
.col-sm-10 .help-block.append-bottom-10
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2" = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
.help-block about role permissions
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
.form-group .col-md-3.col-lg-2
= f.label :expires_at, 'Access expiration date', class: 'control-label'
.col-sm-10
.clearable-input .clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access 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
.help-block .help-block.append-bottom-10
On this date, the user(s) will automatically lose access to this project. On this date, the user(s) will automatically lose access to this project.
.form-actions .col-md-2
= f.submit 'Add users to project', class: "btn btn-create" = f.submit "Add to project", class: "btn btn-create btn-block"
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Users with access to
%strong #{@project.name} %strong #{@project.name}
project members %span.badge= @project_members.total_count
%span.badge= members.size
.controls
= form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list %ul.content-list
= render partial: 'shared/members/member', collection: members, as: :member = render partial: 'shared/members/member', collection: members, as: :member
:javascript
$('form.member-search-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '?' + $(this).serialize());
});
- page_title "Members" - page_title "Members"
.project-members-page.js-project-members-page.prepend-top-default .project-members-page.prepend-top-default
%h4.project-members-title.clearfix
Members
= link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project"
- if can?(current_user, :admin_project_member, @project) - if can?(current_user, :admin_project_member, @project)
.panel.panel-default .project-members-new.append-bottom-default
.panel-heading %p.clearfix
Add new user to project Add new user to
.controls %strong= @project.name
= link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
Import members
.panel-body
%p.light
Users with access to this project are listed below.
= render "new_project_member" = render "new_project_member"
= render 'shared/members/requests', membership_source: @project, requesters: @requesters = render 'shared/members/requests', membership_source: @project, requesters: @requesters
= render 'team', members: @project_members .append-bottom-default.clearfix
%h5.member.existing-title
- if @group Existing users and groups
= render "group_members", members: @group_members = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
- if @group_links.any?
= render 'groups', group_links: @group_links
- if @project_group_links.any? && @project.allowed_to_share_with_group? = render 'team', members: @project_members
= render "shared_group_members" = paginate @project_members, theme: "gitlab"
:plain :plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}'); var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
new gl.MemberExpirationDate(); $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
- group_link = local_assigns[:group_link]
- group = group_link.group
- can_admin_member = can?(current_user, :admin_project_member, @project)
%li.member.group_member{ id: "group_member_#{group_link.id}" }
%span{ class: "list-item-name" }
= image_tag group_icon(group), class: "avatar s40", alt: ''
%strong
= link_to group.name, group_path(group)
.cgray
Joined #{time_ago_with_tooltip(group.created_at)}
- if group_link.expires?
·
%span{ class: ('text-warning' if group_link.expires_soon?) }
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls
= form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
= select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member
.prepend-left-5.clearable-input.member-form-control
= 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
- if can_admin_member
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link),
remote: true,
method: :delete,
data: { confirm: "Are you sure you want to remove #{group.name}?" },
class: 'btn btn-remove prepend-left-10' do
%span.visible-xs-block
Delete
= icon('trash', class: 'hidden-xs')
- show_roles = local_assigns.fetch(:show_roles, true) - show_roles = local_assigns.fetch(:show_roles, true)
- show_controls = local_assigns.fetch(:show_controls, true) - show_controls = local_assigns.fetch(:show_controls, true)
- user = member.user - user = local_assigns.fetch(:user, member.user)
- source = member.source
- can_admin_member = can?(current_user, action_member_permission(:update, member), member)
%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) } %li.member{ class: dom_class(member), id: dom_id(member) }
- if show_roles %span.list-item-name
.controls
%strong.control-text= member.human_access
- if show_controls
- if !user && can?(current_user, action_member_permission(:admin, member), member.source)
= link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
method: :post,
class: 'btn'
- if can?(current_user, action_member_permission(:update, member), member)
= button_tag icon('pencil'),
type: 'button',
class: 'btn inline js-toggle-button',
title: 'Edit'
- if member.request?
= link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
method: :post,
class: 'btn btn-success',
title: 'Grant access'
- if can?(current_user, action_member_permission(:destroy, member), member)
- if current_user == user
= link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(member.source) },
class: 'btn btn-remove'
- else
= link_to icon('trash'), member,
remote: true,
method: :delete,
data: { confirm: remove_member_message(member) },
class: 'btn btn-remove',
title: remove_member_title(member)
%span{ class: ("list-item-name" if show_controls) }
- if user - if user
= image_tag avatar_icon(user, 40), class: "avatar s40", alt: '' = image_tag avatar_icon(user, 40), class: "avatar s40", alt: ''
%strong %strong
= link_to user.name, user_path(user) = link_to user.name, user_path(user)
%span.cgray= user.username %span.cgray= user.to_reference
- if user == current_user - if user == current_user
%span.label.label-success It's you %span.label.label-success.prepend-left-5 It's you
- if user.blocked? - if user.blocked?
%label.label.label-danger %label.label.label-danger
%strong Blocked %strong Blocked
.cgray - if source.instance_of?(Group) && !@group
= link_to source, class: "member-group-link prepend-left-5" do
= #{source.name}"
.hidden-xs.cgray
- if member.request? - if member.request?
Requested Requested
= time_ago_with_tooltip(member.requested_at) = time_ago_with_tooltip(member.requested_at)
...@@ -73,20 +43,44 @@ ...@@ -73,20 +43,44 @@
by by
= link_to member.created_by.name, user_path(member.created_by) = link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at) = time_ago_with_tooltip(member.created_at)
- if show_roles - if show_roles
.edit-member.hide.js-toggle-content .controls.member-controls
%br - if show_controls
= form_for member, remote: true, html: { class: 'form-horizontal' } do |f| - if user != current_user
.form-group = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label' = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member
.col-sm-10 .prepend-left-5.clearable-input.member-form-control
= f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}" = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member
.form-group
= label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label'
.col-sm-10
.clearable-input
= f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}"
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
.prepend-top-10 - else
= f.submit 'Save', class: 'btn btn-save btn-sm' %span.member-access-text= member.human_access
- if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source)
= link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
method: :post,
class: 'btn btn-default prepend-left-10'
- elsif member.request? && can_admin_member
= link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
method: :post,
class: 'btn btn-success prepend-left-10',
title: 'Grant access'
- if can?(current_user, action_member_permission(:destroy, member), member)
- if current_user == user
= link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(member.source) },
class: 'btn btn-remove prepend-left-10'
- else
= link_to member,
remote: true,
method: :delete,
data: { confirm: remove_member_message(member) },
class: 'btn btn-remove prepend-left-10',
title: remove_member_title(member) do
%span.visible-xs-block
Delete
= icon('trash', class: 'hidden-xs')
- else
%span.member-access-text= member.human_access
- if requesters.any? - if requesters.any?
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Users requesting access to
%strong= membership_source.name %strong= membership_source.name
access requests
%span.badge= requesters.size %span.badge= requesters.size
%ul.content-list %ul.content-list
= render partial: 'shared/members/member', collection: requesters, as: :member = render partial: 'shared/members/member', collection: requesters, as: :member
...@@ -408,7 +408,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: ...@@ -408,7 +408,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end end
end end
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ } resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do member do
......
...@@ -105,7 +105,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps ...@@ -105,7 +105,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
select "Developer", from: "access_level" select "Developer", from: "access_level"
end end
click_button "Add users to group" click_button "Add to group"
end end
step 'I should see current user as "Developer"' do step 'I should see current user as "Developer"' do
......
...@@ -70,7 +70,7 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps ...@@ -70,7 +70,7 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps
select "Developer", from: "access_level" select "Developer", from: "access_level"
end end
click_button "Add users to project" click_button "Add to project"
end end
step 'I should see current user as "Developer"' do step 'I should see current user as "Developer"' do
......
class Spinach::Features::GroupMembers < Spinach::FeatureSteps class Spinach::Features::GroupMembers < Spinach::FeatureSteps
include WaitForAjax
include SharedAuthentication include SharedAuthentication
include SharedPaths include SharedPaths
include SharedGroup include SharedGroup
...@@ -13,7 +14,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps ...@@ -13,7 +14,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
select "Reporter", from: "access_level" select "Reporter", from: "access_level"
end end
click_button "Add users to group" click_button "Add to group"
end end
step 'I select "Mike" as "Master"' do step 'I select "Mike" as "Master"' do
...@@ -24,7 +25,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps ...@@ -24,7 +25,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
select "Master", from: "access_level" select "Master", from: "access_level"
end end
click_button "Add users to group" click_button "Add to group"
end end
step 'I should see "Mike" in team list as "Reporter"' do step 'I should see "Mike" in team list as "Reporter"' do
...@@ -47,7 +48,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps ...@@ -47,7 +48,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
select "Reporter", from: "access_level" select "Reporter", from: "access_level"
end end
click_button "Add users to group" click_button "Add to group"
end end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
...@@ -66,7 +67,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps ...@@ -66,7 +67,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
select "Reporter", from: "access_level" select "Reporter", from: "access_level"
end end
click_button "Add users to group" click_button "Add to group"
end end
step 'I should see user "John Doe" in team list' do step 'I should see user "John Doe" in team list' do
...@@ -108,7 +109,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps ...@@ -108,7 +109,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
step 'I search for \'Mary\' member' do step 'I search for \'Mary\' member' do
page.within '.member-search-form' do page.within '.member-search-form' do
fill_in 'search', with: 'Mary' fill_in 'search', with: 'Mary'
click_button 'Search' find('.member-search-btn').click
end end
end end
...@@ -116,9 +117,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps ...@@ -116,9 +117,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member member = mary_jane_member
page.within "#group_member_#{member.id}" do page.within "#group_member_#{member.id}" do
click_button 'Edit'
select 'Developer', from: "member_access_level_#{member.id}" select 'Developer', from: "member_access_level_#{member.id}"
click_on 'Save' wait_for_ajax
end end
end end
......
...@@ -22,7 +22,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -22,7 +22,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
select2(user.id, from: "#user_ids", multiple: true) select2(user.id, from: "#user_ids", multiple: true)
select "Reporter", from: "access_level" select "Reporter", from: "access_level"
end end
click_button "Add users to project" click_button "Add to project"
end end
step 'I should see "Mike" in team list as "Reporter"' do step 'I should see "Mike" in team list as "Reporter"' do
...@@ -36,10 +36,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -36,10 +36,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
step 'I select "sjobs@apple.com" as "Reporter"' do step 'I select "sjobs@apple.com" as "Reporter"' do
page.within ".users-project-form" do page.within ".users-project-form" do
select2("sjobs@apple.com", from: "#user_ids", multiple: true) find('#user_ids', visible: false).set('sjobs@apple.com')
select "Reporter", from: "access_level" select "Reporter", from: "access_level"
end end
click_button "Add users to project" click_button "Add to project"
end end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
...@@ -65,9 +65,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -65,9 +65,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy') user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id) project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do page.within "#project_member_#{project_member.id}" do
click_button 'Edit'
select "Reporter", from: "member_access_level_#{project_member.id}" select "Reporter", from: "member_access_level_#{project_member.id}"
click_button "Save"
end end
end end
...@@ -112,7 +110,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -112,7 +110,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end end
step 'I click link "Import team from another project"' do step 'I click link "Import team from another project"' do
click_link "Import members from another project" click_link "Import"
end end
When 'I submit "Website" project for import team' do When 'I submit "Website" project for import team' do
...@@ -144,8 +142,9 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -144,8 +142,9 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end end
step 'I should see "Opensource" group user listing' do step 'I should see "Opensource" group user listing' do
expect(page).to have_content("Shared with OpenSource group, members with Master role (2)") page.within '.project-members-groups' do
expect(page).to have_content(@os_user1.name) expect(page).to have_content('OpenSource')
expect(page).to have_content(@os_user2.name) expect(find('select').value).to eq('40')
end
end end
end end
...@@ -41,7 +41,7 @@ feature 'Groups > Members > Owner manages access requests', feature: true do ...@@ -41,7 +41,7 @@ feature 'Groups > Members > Owner manages access requests', feature: true do
def expect_visible_access_request(group, user) def expect_visible_access_request(group, user)
expect(group.requesters.exists?(user_id: user)).to be_truthy expect(group.requesters.exists?(user_id: user)).to be_truthy
expect(page).to have_content "#{group.name} access requests 1" expect(page).to have_content "Users requesting access to #{group.name} 1"
expect(page).to have_content user.name expect(page).to have_content user.name
end end
end end
require 'spec_helper'
feature 'Projects > Members > Anonymous user sees members', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:empty_project, :public) }
background do
project.team << [user, :master]
@group_link = create(:project_group_link, project: project, group: group)
login_as(user)
visit namespace_project_project_members_path(project.namespace, project)
end
it 'updates group access level' do
select 'Guest', from: "member_access_level_#{group.id}"
wait_for_ajax
visit namespace_project_project_members_path(project.namespace, project)
expect(page).to have_select("member_access_level_#{group.id}", selected: 'Guest')
end
it 'updates expiry date' do
tomorrow = Date.today + 3
fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
wait_for_ajax
page.within(find('li.group_member')) do
expect(page).to have_content('Expires in')
end
end
it 'deletes group link' do
page.within(first('.group_member')) do
find('.btn-remove').click
end
wait_for_ajax
expect(page).not_to have_selector('.group_member')
end
context 'search' do
it 'finds no results' do
page.within '.member-search-form' do
fill_in 'search', with: 'testing 123'
find('.member-search-btn').click
end
expect(page).not_to have_selector('.group_member')
end
it 'finds results' do
page.within '.member-search-form' do
fill_in 'search', with: group.name
find('.member-search-btn').click
end
expect(page).to have_selector('.group_member', count: 1)
end
end
end
require 'spec_helper' require 'spec_helper'
feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
include WaitForAjax
include Select2Helper include Select2Helper
include ActiveSupport::Testing::TimeHelpers include ActiveSupport::Testing::TimeHelpers
...@@ -20,7 +21,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: ...@@ -20,7 +21,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
page.within '.users-project-form' do page.within '.users-project-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: '2016-08-10' fill_in 'expires_at', with: '2016-08-10'
click_on 'Add users to project' click_on 'Add to project'
end end
page.within '.project_member:first-child' do page.within '.project_member:first-child' do
...@@ -35,9 +36,8 @@ feature 'Projects > Members > Master adds member with expiration date', feature: ...@@ -35,9 +36,8 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
visit namespace_project_project_members_path(project.namespace, project) visit namespace_project_project_members_path(project.namespace, project)
page.within '.project_member:first-child' do page.within '.project_member:first-child' do
click_on 'Edit' find('.js-access-expiration-date').set '2016-08-09'
fill_in 'Access expiration date', with: '2016-08-09' wait_for_ajax
click_on 'Save'
expect(page).to have_content('Expires in 3 days') expect(page).to have_content('Expires in 3 days')
end end
end end
......
...@@ -41,7 +41,7 @@ feature 'Projects > Members > Master manages access requests', feature: true do ...@@ -41,7 +41,7 @@ feature 'Projects > Members > Master manages access requests', feature: true do
def expect_visible_access_request(project, user) def expect_visible_access_request(project, user)
expect(project.requesters.exists?(user_id: user)).to be_truthy expect(project.requesters.exists?(user_id: user)).to be_truthy
expect(page).to have_content "#{project.name} access requests 1" expect(page).to have_content "Users requesting access to #{project.name} 1"
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