Commit 9cdfcbb5 authored by Alfredo Sumaran's avatar Alfredo Sumaran

Merge branch '25985-combine-members-and-groups-settings-pages' into 'master'

Moved the members and groups to single option called members

Closes #25985

See merge request !8281
parents f77f736b 23e6d8c3
......@@ -215,7 +215,9 @@
new gl.Members();
new UsersSelect();
break;
case 'projects:project_members:index':
case 'projects:members:show':
new gl.MemberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect();
new gl.MemberExpirationDate();
new gl.Members();
new UsersSelect();
......@@ -261,10 +263,6 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
case 'projects:group_links:index':
new gl.MemberExpirationDate();
new GroupsSelect();
break;
case 'search:show':
new Search();
break;
......
/* eslint-disable func-names, space-before-function-paren, vars-on-top, no-var, object-shorthand, comma-dangle, max-len */
(function() {
(() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
// `js-clear-input` element, then show that element when there is a value in the
// datepicker, and make clicking on that element clear the field.
//
gl.MemberExpirationDate = function() {
window.gl = window.gl || {};
gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
}
var inputs = $('.js-access-expiration-date');
const inputs = $(selector);
inputs.datepicker({
dateFormat: 'yy-mm-dd',
minDate: 1,
onSelect: function () {
onSelect: function onSelect() {
$(this).trigger('change');
toggleClearInput.call(this);
}
},
});
inputs.next('.js-clear-input').on('click', function(event) {
inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault();
var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
const input = $(this).closest('.clearable-input').find(selector);
input.datepicker('setDate', null)
.trigger('change');
toggleClearInput.call(input);
......
......@@ -163,6 +163,10 @@ ul.content-list {
&:last-child {
margin-right: 0;
@media(max-width: $screen-xs-max) {
margin: 0 auto;
}
}
}
......
......@@ -25,7 +25,7 @@
}
.form-horizontal {
margin-top: 5px;
margin-top: 20px;
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
......@@ -98,6 +98,10 @@
padding-right: 35px;
@media (min-width: $screen-sm-min) {
width: 250px;
}
@media (min-width: $screen-md-min) {
width: 350px;
}
......
......@@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
before_action :authorize_admin_project_member!, only: [:update]
def index
@group_links = project.project_group_links.all
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << project.namespace_id unless project.personal?
redirect_to namespace_project_settings_members_path
end
def create
......@@ -25,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
flash[:alert] = 'Please select a group.'
end
redirect_to namespace_project_group_links_path(project.namespace, project)
redirect_to namespace_project_settings_members_path(project.namespace, project)
end
def update
......@@ -39,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format|
format.html do
redirect_to namespace_project_group_links_path(project.namespace, project)
redirect_to namespace_project_settings_members_path(project.namespace, project)
end
format.js { head :ok }
end
......
......@@ -6,54 +6,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
group = @project.group
if group
# We need `.where.not(user_id: nil)` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
# FIXME: This whole logic should be moved to a finder!
non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
group_members = group.group_members.where.not(user_id: non_null_user_ids)
group_members = group_members.non_invite unless can?(current_user, :admin_group, @group)
end
if params[:search].present?
user_ids = @project.users.search(params[:search]).select(:id)
@project_members = @project_members.where(user_id: user_ids)
if group_members
user_ids = group.users.search(params[:search]).select(:id)
group_members = group_members.where(user_id: user_ids)
end
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
@project_members = Member.
where(wheres.join(' OR ')).
sort(@sort).
page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
sort = params[:sort].presence || sort_value_name
redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
end
def create
status = Members::CreateService.new(@project, current_user, params).execute
redirect_url = namespace_project_project_members_path(@project.namespace, @project)
redirect_url = namespace_project_settings_members_path(@project.namespace, @project)
if status
redirect_to redirect_url, notice: 'Users were successfully added.'
......@@ -76,14 +36,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
respond_to do |format|
format.html do
redirect_to namespace_project_project_members_path(@project.namespace, @project)
redirect_to namespace_project_settings_members_path(@project.namespace, @project)
end
format.js { head :ok }
end
end
def resend_invite
redirect_path = namespace_project_project_members_path(@project.namespace, @project)
redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
@project_member = @project.project_members.find(params[:id])
......@@ -106,7 +66,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
return render_404
end
redirect_to(namespace_project_project_members_path(project.namespace, project),
redirect_to(namespace_project_settings_members_path(project.namespace, project),
notice: notice)
end
......
module Projects
module Settings
class MembersController < Projects::ApplicationController
include SortingHelper
def show
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
group = @project.group
# group links
@group_links = @project.project_group_links.all
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << @project.namespace_id unless @project.personal?
if group
# We need `.where.not(user_id: nil)` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
group_members = MembersFinder.new(@project_members, group).execute(current_user)
end
if params[:search].present?
user_ids = @project.users.search(params[:search]).select(:id)
@project_members = @project_members.where(user_id: user_ids)
if group_members
user_ids = group.users.search(params[:search]).select(:id)
group_members = group_members.where(user_id: user_ids)
end
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
@project_members = Member.
where(wheres.join(' OR ')).
sort(@sort).
page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
end
end
end
end
class MembersFinder < Projects::ApplicationController
def initialize(project_members, project_group)
@project_members = project_members
@project_group = project_group
end
def execute(current_user)
non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
group_members = @project_group.group_members.where.not(user_id: non_null_user_ids)
group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group)
group_members
end
end
......@@ -206,4 +206,9 @@ module GitlabRoutingHelper
file_namespace_project_build_artifacts_path(*args)
end
end
# Settings
def project_settings_members_path(project, *args)
namespace_project_settings_members_path(project.namespace, project, *args)
end
end
......@@ -75,7 +75,7 @@ module SearchHelper
{ category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
{ category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
{ category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
{ category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) },
{ category: "Current Project", label: "Members", url: namespace_project_settings_members_path(@project.namespace, @project) },
{ category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
]
else
......
- if project_nav_tab? :team
= nav_link(controller: [:project_members, :teams]) do
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
= nav_link(controller: [:members, :teams]) do
= link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
Members
- if can_edit
- if @project.allowed_to_share_with_group?
= nav_link(controller: :group_links) do
= link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
%span
Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span
......
......@@ -20,10 +20,10 @@
.form-group
= label_tag :expires_at, 'Access expiration date', class: 'label-light'
.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-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups'
%i.clear-icon.js-clear-input
.help-block
On this date, all users in the group will automatically lose access to this project.
On this date, all members in the group will automatically lose access to this project.
= submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3
%hr
......
.row.prepend-top-default
.col-lg-3.settings-sidebar
%h4.prepend-top-0
Members
- if can?(current_user, :admin_project_member, @project)
%p
Add a new member to
%strong= @project.name
.col-lg-9
.light.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
= render "projects/project_members/new_project_member"
= render 'shared/members/requests', membership_source: @project, requesters: @requesters
.append-bottom-default.clearfix
%h5.member.existing-title
Existing members and groups
- if @group_links.any?
= render 'projects/project_members/groups', group_links: @group_links
= render 'projects/project_members/team', members: @project_members
= paginate @project_members, theme: "gitlab"
= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
.row
.col-md-4.col-lg-6
= users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true)
.form-group
= users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
.help-block.append-bottom-10
Search for users by name, username, or email, or invite new ones using their email address.
.col-md-3.col-lg-2
Search for members by name, username, or email, or invite new ones using their email address.
.form-group
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
.help-block.append-bottom-10
= link_to "Read more", help_page_path("user/permissions"), class: "vlink"
about role permissions
.col-md-3.col-lg-2
.form-group
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
.help-block.append-bottom-10
On this date, the user(s) will automatically lose access to this project.
.col-md-2
= f.submit "Add to project", class: "btn btn-create btn-block"
On this date, the member(s) will automatically lose access to this project.
= f.submit "Add to project", class: "btn btn-create"
= link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project"
.panel.panel-default
.panel-heading
Users with access to
Members with access to
%strong #{@project.name}
%span.badge= @project_members.total_count
= form_tag namespace_project_settings_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")
= render 'shared/members/sort_dropdown'
%ul.content-list
= render partial: 'shared/members/member', collection: members, as: :member
......@@ -12,5 +12,4 @@
.form-actions
= button_tag 'Import project members', class: "btn btn-create"
= link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel"
= link_to "Cancel", namespace_project_settings_members_path(@project.namespace, @project), class: "btn btn-cancel"
- page_title "Members"
.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)
.project-members-new.append-bottom-default
%p.clearfix
Add new user to
%strong= @project.name
= render "new_project_member"
= render 'shared/members/requests', membership_source: @project, requesters: @requesters
.append-bottom-default.clearfix
%h5.member.existing-title
Existing users and groups
= 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")
= render 'shared/members/sort_dropdown'
- if @group_links.any?
= render 'groups', group_links: @group_links
= render 'team', members: @project_members
= paginate @project_members, theme: "gitlab"
- page_title "Members"
= render "projects/project_members/index"
- if can?(current_user, :admin_project, @project)
- if @project.allowed_to_share_with_group?
= render "projects/group_links/index"
......@@ -37,7 +37,6 @@
%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
......
---
title: Combined the settings options project members and groups into a single one
called members
merge_request:
author:
......@@ -307,6 +307,10 @@ constraints(ProjectUrlConstrainer.new) do
end
end
namespace :settings do
resource :members, only: [:show]
end
# Since both wiki and repository routing contains wildcard characters
# its preferable to keep it below all other project routes
draw :wiki
......
......@@ -114,8 +114,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I click link "Import team from another project"' do
page.within '.users-project-form' do
click_link "Import"
end
end
When 'I submit "Website" project for import team' do
project = Project.find_by(name: "Website")
......
......@@ -31,7 +31,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
namespace_project_group_links_path(project.namespace, project)
namespace_project_settings_members_path(project.namespace, project)
)
end
end
......@@ -62,7 +62,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
namespace_project_group_links_path(project.namespace, project)
namespace_project_settings_members_path(project.namespace, project)
)
end
end
......@@ -76,7 +76,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
namespace_project_group_links_path(project.namespace, project)
namespace_project_settings_members_path(project.namespace, project)
)
expect(flash[:alert]).to eq('Please select a group.')
end
......
......@@ -5,11 +5,11 @@ describe Projects::ProjectMembersController do
let(:project) { create(:empty_project, :public, :access_requestable) }
describe 'GET index' do
it 'renders index with 200 status code' do
it 'should have the settings/members address with a 302 status code' do
get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(200)
expect(response).to render_template(:index)
expect(response).to have_http_status(302)
expect(response.location).to include namespace_project_settings_members_path(project.namespace, project)
end
end
......@@ -44,7 +44,7 @@ describe Projects::ProjectMembersController do
access_level: Gitlab::Access::GUEST
expect(response).to set_flash.to 'Users were successfully added.'
expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
end
it 'adds no user to members' do
......@@ -56,7 +56,7 @@ describe Projects::ProjectMembersController do
access_level: Gitlab::Access::GUEST
expect(response).to set_flash.to 'No users or groups specified.'
expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
end
end
end
......@@ -99,7 +99,7 @@ describe Projects::ProjectMembersController do
id: member
expect(response).to redirect_to(
namespace_project_project_members_path(project.namespace, project)
namespace_project_settings_members_path(project.namespace, project)
)
expect(project.members).not_to include member
end
......@@ -259,7 +259,7 @@ describe Projects::ProjectMembersController do
expect(project.team_members).to include member
expect(response).to set_flash.to 'Successfully imported'
expect(response).to redirect_to(
namespace_project_project_members_path(project.namespace, project)
namespace_project_settings_members_path(project.namespace, project)
)
end
end
......
require('spec_helper')
describe Projects::Settings::MembersController do
let(:project) { create(:empty_project, :public, :access_requestable) }
describe 'GET show' do
it 'renders show with 200 status code' do
get :show, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(200)
expect(response).to render_template(:show)
end
end
end
......@@ -14,10 +14,10 @@ feature 'Project group links', feature: true, js: true do
context 'setting an expiration date for a group link' do
before do
visit namespace_project_group_links_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
select2 group.id, from: '#link_group_id'
fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
click_on 'Share'
end
......
......@@ -11,10 +11,10 @@ feature 'Projects > Members > Anonymous user sees members', feature: true do
end
scenario "anonymous user visits the project's members page and sees the list of members" do
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
expect(current_path).to eq(
namespace_project_project_members_path(project.namespace, project))
namespace_project_settings_members_path(project.namespace, project))
expect(page).to have_content(user.name)
end
end
......@@ -12,7 +12,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
@group_link = create(:project_group_link, project: project, group: group)
login_as(user)
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
end
it 'updates group access level' do
......@@ -24,7 +24,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
wait_for_ajax
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
expect(first('.group_member')).to have_content('Guest')
end
......
......@@ -19,7 +19,7 @@ feature 'Projects members', feature: true do
context 'with a group invitee' do
before do
group_invitee
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'does not appear in the project members page' do
......@@ -33,7 +33,7 @@ feature 'Projects members', feature: true do
before do
group_invitee
project_invitee
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'shows the project invitee, the project developer, and the group owner' do
......@@ -54,7 +54,7 @@ feature 'Projects members', feature: true do
context 'with a group requester' do
before do
group.request_access(group_requester)
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'does not appear in the project members page' do
......@@ -68,7 +68,7 @@ feature 'Projects members', feature: true do
before do
group.request_access(group_requester)
project.request_access(project_requester)
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'shows the project requester, the project developer, and the group owner' do
......
......@@ -14,15 +14,15 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
login_as(master)
end
scenario 'expiration date is displayed in the members list' do
scenario 'expiration date is displayed in the members list', js: true do
travel_to Time.zone.parse('2016-08-06 08:00') do
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: '2016-08-10'
click_on 'Add to project'
end
find('.users-project-form').click
click_on 'Add to project'
page.within "#project_member_#{new_member.project_members.first.id}" do
expect(page).to have_content('Expires in 4 days')
......
......@@ -39,7 +39,7 @@ feature 'Projects > Members > User requests access', feature: true do
open_project_settings_menu
click_link 'Members'
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
page.within('.content') do
expect(page).not_to have_content(user.name)
end
......
......@@ -82,8 +82,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
describe "GET /:project_path/settings/members" do
subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
......
......@@ -82,8 +82,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
describe "GET /:project_path/settings/members" do
subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
......
......@@ -82,8 +82,8 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
describe "GET /:project_path/settings/members" do
subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
......
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