Commit 9a0212d1 authored by Dylan Griffith's avatar Dylan Griffith

Merge branch 'peterhegman/remove-vue_project_members_list-feature-flag' into 'master'

Remove `vue_project_members_list` feature flag [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!55902
parents 7e6c8784 e842c800
import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
// This is only used when `invite_members_group_modal` feature flag is disabled.
// This file can be removed when `invite_members_group_modal` feature flag is removed
export default () => {
disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
};
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
import { Rails } from '~/lib/utils/rails_ujs';
import { __, sprintf } from '~/locale';
export default class Members {
constructor() {
this.addListeners();
this.initGLDropdown();
}
addListeners() {
// eslint-disable-next-line @gitlab/no-global-event-off
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
// eslint-disable-next-line @gitlab/no-global-event-off
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
dropdownClicked(options) {
options.e.preventDefault();
this.formSubmit(null, options.$el);
}
// eslint-disable-next-line class-methods-use-this
dropdownToggleLabel(selected, $el) {
return $el.text();
}
// eslint-disable-next-line class-methods-use-this
dropdownIsSelectable(selected, $el) {
return !$el.hasClass('is-active');
}
initGLDropdown() {
$('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn);
initDeprecatedJQueryDropdown($btn, {
selectable: true,
isSelectable: (selected, $el) => this.dropdownIsSelectable(selected, $el),
fieldName: $btn.data('fieldName'),
id(selected, $el) {
return $el.data('id');
},
toggleLabel: (selected, $el) => this.dropdownToggleLabel(selected, $el, $btn),
clicked: (options) => this.dropdownClicked(options),
});
});
}
formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el;
const { $toggle, $dateInput } = this.getMemberListItems($this);
const formEl = $this.closest('form').get(0);
Rails.fire(formEl, 'submit');
$toggle.disable();
$dateInput.disable();
}
formSuccess(e) {
const { $toggle, $dateInput, $expiresIn, $expiresInText } = this.getMemberListItems(
$(e.currentTarget).closest('.js-member'),
);
const [data] = e.detail;
const expiresIn = data?.expires_in;
if (expiresIn) {
$expiresIn.removeClass('gl-display-none');
$expiresInText.text(sprintf(__('Expires in %{expires_at}'), { expires_at: expiresIn }));
const { expires_soon: expiresSoon, expires_at_formatted: expiresAtFormatted } = data;
if (expiresSoon) {
$expiresInText.addClass('text-warning');
} else {
$expiresInText.removeClass('text-warning');
}
// Update tooltip
if (expiresAtFormatted) {
$expiresInText.attr('title', expiresAtFormatted);
$expiresInText.attr('data-original-title', expiresAtFormatted);
}
} else {
$expiresIn.addClass('gl-display-none');
}
$toggle.enable();
$dateInput.enable();
}
// eslint-disable-next-line class-methods-use-this
getMemberListItems($el) {
const $memberListItem = $el.is('.js-member') ? $el : $(`#${$el.data('elId')}`);
return {
$memberListItem,
$expiresIn: $memberListItem.find('.js-expires-in'),
$expiresInText: $memberListItem.find('.js-expires-in-text'),
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
$dateInput: $memberListItem.find('.js-access-expiration-date'),
};
}
}
......@@ -2,11 +2,12 @@ import Vue from 'vue';
import { groupMemberRequestFormatter } from '~/groups/members/utils';
import groupsSelect from '~/groups_select';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
import initInviteMembersForm from '~/invite_members/init_invite_members_form';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { s__ } from '~/locale';
import memberExpirationDate from '~/member_expiration_date';
import { initMembersApp } from '~/members/index';
import { initMembersApp } from '~/members';
import { groupLinkRequestFormatter } from '~/members/utils';
import UsersSelect from '~/users_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
......@@ -73,4 +74,8 @@ initInviteMembersModal();
initInviteMembersTrigger();
initInviteGroupTrigger();
// This is only used when `invite_members_group_modal` feature flag is disabled.
// This can be removed when `invite_members_group_modal` feature flag is removed.
initInviteMembersForm();
new UsersSelect(); // eslint-disable-line no-new
import Vue from 'vue';
import { deprecatedCreateFlash as flash } from '~/flash';
import groupsSelect from '~/groups_select';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
import initInviteMembersForm from '~/invite_members/init_invite_members_form';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { __ } from '~/locale';
import { s__ } from '~/locale';
import memberExpirationDate from '~/member_expiration_date';
import Members from '~/members';
import { initMembersApp } from '~/members';
import { groupLinkRequestFormatter } from '~/members/utils';
import { projectMemberRequestFormatter } from '~/projects/members/utils';
import UsersSelect from '~/users_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
......@@ -32,67 +34,49 @@ initInviteMembersModal();
initInviteMembersTrigger();
initInviteGroupTrigger();
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
// This is only used when `invite_members_group_modal` feature flag is disabled.
// This can be removed when `invite_members_group_modal` feature flag is removed.
initInviteMembersForm();
if (window.gon.features.vueProjectMembersList) {
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
new UsersSelect(); // eslint-disable-line no-new
Promise.all([
import('~/members/index'),
import('~/members/utils'),
import('~/projects/members/utils'),
import('~/locale'),
])
.then(
([
{ initMembersApp },
{ groupLinkRequestFormatter },
{ projectMemberRequestFormatter },
{ s__ },
]) => {
initMembersApp(document.querySelector('.js-project-members-list'), {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
requestFormatter: projectMemberRequestFormatter,
filteredSearchBar: {
show: true,
tokens: ['with_inherited_permissions'],
searchParam: 'search',
placeholder: s__('Members|Filter members'),
recentSearchesStorageKey: 'project_members',
},
});
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-project-members-list'), {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
requestFormatter: projectMemberRequestFormatter,
filteredSearchBar: {
show: true,
tokens: ['with_inherited_permissions'],
searchParam: 'search',
placeholder: s__('Members|Filter members'),
recentSearchesStorageKey: 'project_members',
},
});
initMembersApp(document.querySelector('.js-project-group-links-list'), {
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
tr: { 'data-qa-selector': 'group_row' },
},
requestFormatter: groupLinkRequestFormatter,
filteredSearchBar: {
show: true,
tokens: [],
searchParam: 'search_groups',
placeholder: s__('Members|Search groups'),
recentSearchesStorageKey: 'project_group_links',
},
});
initMembersApp(document.querySelector('.js-project-group-links-list'), {
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
tr: { 'data-qa-selector': 'group_row' },
},
requestFormatter: groupLinkRequestFormatter,
filteredSearchBar: {
show: true,
tokens: [],
searchParam: 'search_groups',
placeholder: s__('Members|Search groups'),
recentSearchesStorageKey: 'project_group_links',
},
});
initMembersApp(document.querySelector('.js-project-invited-members-list'), {
tableFields: SHARED_FIELDS.concat('invited'),
requestFormatter: projectMemberRequestFormatter,
});
initMembersApp(document.querySelector('.js-project-invited-members-list'), {
tableFields: SHARED_FIELDS.concat('invited'),
requestFormatter: projectMemberRequestFormatter,
});
initMembersApp(document.querySelector('.js-project-access-requests-list'), {
tableFields: SHARED_FIELDS.concat('requested'),
requestFormatter: projectMemberRequestFormatter,
});
},
)
.catch(() => {
flash(__('An error occurred while loading the members, please try again.'));
});
}
initMembersApp(document.querySelector('.js-project-access-requests-list'), {
tableFields: SHARED_FIELDS.concat('requested'),
requestFormatter: projectMemberRequestFormatter,
});
......@@ -8,10 +8,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
before_action do
push_frontend_feature_flag(:vue_project_members_list, @project, default_enabled: :yaml)
end
feature_category :authentication_and_authorization
def index
......
.card.card-without-border
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_("Groups with access to %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(@project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
= form_tag project_project_members_path(@project), method: :get, class: 'user-search-form gl-mx-n3 gl-my-n3', data: { testid: 'group-link-search-form' } do
.gl-px-3.gl-py-2
.search-control-wrap.gl-relative
= render 'shared/members/search_field', name: 'search_groups'
%ul.content-list.members-list{ data: { testid: 'project-member-groups' } }
- @group_links.each do |group_link|
= render 'shared/members/group', group_link: group_link, can_admin_member: can_manage_project_members?(@project), group_link_path: project_group_link_path(@project, group_link)
- project = local_assigns.fetch(:project)
- members = local_assigns.fetch(:members)
- group = local_assigns.fetch(:group)
- current_user_is_group_owner = local_assigns.fetch(:current_user_is_group_owner)
.card.card-without-border
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_("Members of %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
= form_tag project_project_members_path(project), method: :get, class: 'user-search-form gl-display-flex gl-md-align-items-center gl-flex-wrap gl-flex-direction-column gl-md-flex-direction-row gl-mx-n3 gl-my-n3', data: { testid: 'user-search-form' } do
.gl-px-3.gl-py-2
.search-control-wrap.gl-relative
= render 'shared/members/search_field'
= render 'shared/members/tab_pane/form_item' do
= label_tag :sort_by, _('Sort by'), class: 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center'
= render 'shared/members/sort_dropdown'
%ul.content-list.members-list{ data: { qa_selector: 'members_list', testid: 'members-table' } }
= render partial: 'shared/members/member',
collection: members, as: :member,
locals: { membership_source: project,
group: group,
current_user_is_group_owner: current_user_is_group_owner }
- page_title _("Members")
- group = @project.group
- vue_project_members_list_enabled = Feature.enabled?(:vue_project_members_list, @project, default_enabled: :yaml)
.js-remove-member-modal
.row.gl-mt-3
......@@ -76,44 +74,22 @@
%span.badge.badge-pill= @requesters.count
.tab-content
#tab-members.tab-pane{ class: ('active' unless groups_tab_active?) }
- if vue_project_members_list_enabled
.js-project-members-list{ data: project_members_list_data_attributes(@project, @project_members) }
.loading
.spinner.spinner-md
- else
= render 'projects/project_members/team', project: @project, group: group, members: @project_members, current_user_is_group_owner: current_user_is_group_owner?(@project)
.js-project-members-list{ data: project_members_list_data_attributes(@project, @project_members) }
.loading
.spinner.spinner-md
= paginate @project_members, theme: "gitlab", params: { search_groups: nil }
- if show_groups?(@group_links)
#tab-groups.tab-pane{ class: ('active' if groups_tab_active?) }
- if vue_project_members_list_enabled
.js-project-group-links-list{ data: project_group_links_list_data_attributes(@project, @group_links) }
.loading
.spinner.spinner-md
- else
= render 'projects/project_members/groups', group_links: @group_links
.js-project-group-links-list{ data: project_group_links_list_data_attributes(@project, @group_links) }
.loading
.spinner.spinner-md
- if show_invited_members?(@project, @invited_members)
#tab-invited-members.tab-pane
- if vue_project_members_list_enabled
.js-project-invited-members-list{ data: project_members_list_data_attributes(@project, @invited_members) }
.loading
.spinner.spinner-md
- else
.card.card-without-border
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_('Members invited to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @invited_members, as: :member, locals: { membership_source: @project, group: group, current_user_is_group_owner: current_user_is_group_owner?(@project) }
.js-project-invited-members-list{ data: project_members_list_data_attributes(@project, @invited_members) }
.loading
.spinner.spinner-md
- if show_access_requests?(@project, @requesters)
#tab-access-requests.tab-pane
- if vue_project_members_list_enabled
.js-project-access-requests-list{ data: project_members_list_data_attributes(@project, @requesters) }
.loading
.spinner.spinner-md
- else
.card.card-without-border
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_('Users requesting access to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @requesters, as: :member, locals: { membership_source: @project, group: group }
.js-project-access-requests-list{ data: project_members_list_data_attributes(@project, @requesters) }
.loading
.spinner.spinner-md
---
title: Remove `vue_project_members_list` feature flag
merge_request: 55902
author:
type: changed
---
name: vue_project_members_list
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52148
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299954
milestone: '13.9'
type: development
group: group::access
default_enabled: true
......@@ -37,10 +37,7 @@ From the image above, we can deduce the following things:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21727) in GitLab 12.6.
> - [Improved](https://gitlab.com/groups/gitlab-org/-/epics/4901) in GitLab 13.9.
> - Improvements are [deployed behind a feature flag](../../feature_flags.md), enabled by default.
> - Improvements are enabled on GitLab.com.
> - Improvements are recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable improvements](#enable-or-disable-improvements-to-project-member-management). **(FREE SELF)**
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/299954) in GitLab 13.10.
The following sections illustrate how you can filter and sort members in a project. To view these options,
navigate to your desired project, go to **Members**, and include the noted search terms.
......@@ -199,27 +196,3 @@ To remove a member from a project:
A **Remove member** modal appears.
1. (Optional) Select the **Also unassign this user from related issues and merge requests** checkbox.
1. Click **Remove member**.
## Enable or disable improvements to project member management **(FREE SELF)**
Project member management improvements are deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can opt to disable the improvements.
To disable them:
```ruby
# For the instance
Feature.disable(:vue_project_members_list)
# For a single project
Feature.disable(:vue_project_members_list, Project.find(<project id>))
```
To enable them:
```ruby
# For the instance
Feature.enable(:vue_project_members_list)
# For a single project
Feature.enable(:vue_project_members_list, Project.find(<project id>))
```
......@@ -18,7 +18,6 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
let!(:regular_member) { create(:group_member, :guest, group: group, user: maryjane, ldap: false) }
before do
stub_feature_flags(vue_project_members_list: false)
# We need to actually activate the LDAP config otherwise `Group#ldap_synced?` will always be false!
allow(Gitlab.config.ldap).to receive_messages(enabled: true)
......
......@@ -113,53 +113,23 @@ RSpec.describe 'Projects > Audit Events', :js do
project.add_developer(pete)
end
context 'when `vue_project_members_list` feature flag is enabled' do
it "appears in the project's audit events" do
visit project_project_members_path(project)
page.within find_member_row(pete) do
click_button 'Developer'
click_button 'Maintainer'
end
page.within('.qa-project-sidebar') do
find(:link, text: 'Security & Compliance').click
click_link 'Audit Events'
end
page.within('.audit-log-table') do
expect(page).to have_content 'Changed access level from Developer to Maintainer'
expect(page).to have_content(project.owner.name)
expect(page).to have_content('Pete')
end
end
end
it "appears in the project's audit events" do
visit project_project_members_path(project)
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
page.within find_member_row(pete) do
click_button 'Developer'
click_button 'Maintainer'
end
it "appears in the project's audit events" do
visit project_project_members_path(project)
project_member = project.project_member(pete)
page.within "#project_member_#{project_member.id}" do
click_button 'Developer'
click_link 'Maintainer'
end
page.within('.qa-project-sidebar') do
find(:link, text: 'Security & Compliance').click
click_link 'Audit Events'
end
page.within('.qa-project-sidebar') do
find(:link, text: 'Security & Compliance').click
click_link 'Audit Events'
end
page.within('.audit-log-table') do
expect(page).to have_content 'Changed access level from Developer to Maintainer'
expect(page).to have_content(project.owner.name)
expect(page).to have_content('Pete')
end
page.within('.audit-log-table') do
expect(page).to have_content 'Changed access level from Developer to Maintainer'
expect(page).to have_content(project.owner.name)
expect(page).to have_content('Pete')
end
end
end
......
......@@ -71,65 +71,41 @@ RSpec.describe 'Project > Members > Invite group and members', :js do
context 'when the group has "Share with group lock" and "Member lock" disabled' do
it_behaves_like 'the project can be shared with groups and members'
context 'when `vue_project_members_list` feature flag is enabled' do
it 'allows the project to be shared with another group using the invite form' do
stub_feature_flags(invite_members_group_modal: false)
it 'allows the project to be shared with another group using the invite form' do
stub_feature_flags(invite_members_group_modal: false)
visit project_project_members_path(project)
visit project_project_members_path(project)
click_on 'invite-group-tab'
click_on 'invite-group-tab'
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
find('.btn-confirm').click
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
find('.btn-confirm').click
click_link 'Groups'
click_link 'Groups'
page.within(members_table) do
expect(page).to have_content(group_to_share_with.name)
end
end
it 'allows the project to be shared with another group using the invite modal' do
stub_feature_flags(invite_members_group_modal: true)
visit project_project_members_path(project)
click_on 'Invite a group'
click_on 'Select a group'
wait_for_requests
click_button group_to_share_with.name
click_button 'Invite'
visit project_project_members_path(project)
click_link 'Groups'
page.within(members_table) do
expect(page).to have_content(group_to_share_with.name)
end
page.within(members_table) do
expect(page).to have_content(group_to_share_with.name)
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
end
it 'allows the project to be shared with another group using the invite modal' do
stub_feature_flags(invite_members_group_modal: true)
it 'allows the project to be shared with another group' do
visit project_project_members_path(project)
visit project_project_members_path(project)
click_on 'invite-group-tab'
click_on 'Invite a group'
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
find('.btn-confirm').click
click_on 'Select a group'
wait_for_requests
click_button group_to_share_with.name
click_button 'Invite'
click_link 'Groups'
visit project_project_members_path(project)
click_link 'Groups'
page.within('[data-testid="project-member-groups"]') do
expect(page).to have_content(group_to_share_with.name)
end
page.within(members_table) do
expect(page).to have_content(group_to_share_with.name)
end
end
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Member is removed from project' do
RSpec.describe 'Projects > Members > Member is removed from project', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:other_user) { create(:user) }
......@@ -10,71 +10,37 @@ RSpec.describe 'Projects > Members > Member is removed from project' do
before do
project.add_maintainer(user)
project.add_maintainer(other_user)
end
context 'when `vue_project_members_list` feature flag is enabled', :js do
before do
sign_in(user)
visit project_project_members_path(project)
end
it 'user is removed from project' do
click_button 'Leave'
sign_in(user)
visit project_project_members_path(project)
end
page.within('[role="dialog"]') do
click_button('Leave')
end
it 'user is removed from project' do
click_button 'Leave'
expect(project.users.exists?(user.id)).to be_falsey
page.within('[role="dialog"]') do
click_button('Leave')
end
context 'when the user has been specifically allowed to access a protected branch' do
let!(:matching_protected_branch) { create(:protected_branch, authorize_user_to_push: user, authorize_user_to_merge: user, project: project) }
let!(:non_matching_protected_branch) { create(:protected_branch, authorize_user_to_push: other_user, authorize_user_to_merge: other_user, project: project) }
it 'user leaves project' do
click_button 'Leave'
page.within('[role="dialog"]') do
click_button('Leave')
end
expect(project.users.exists?(user.id)).to be_falsey
expect(matching_protected_branch.push_access_levels.where(user: user)).not_to exist
expect(matching_protected_branch.merge_access_levels.where(user: user)).not_to exist
expect(non_matching_protected_branch.push_access_levels.where(user: other_user)).to exist
expect(non_matching_protected_branch.merge_access_levels.where(user: other_user)).to exist
end
end
expect(project.users.exists?(user.id)).to be_falsey
end
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
context 'when the user has been specifically allowed to access a protected branch' do
let!(:matching_protected_branch) { create(:protected_branch, authorize_user_to_push: user, authorize_user_to_merge: user, project: project) }
let!(:non_matching_protected_branch) { create(:protected_branch, authorize_user_to_push: other_user, authorize_user_to_merge: other_user, project: project) }
sign_in(user)
visit project_project_members_path(project)
end
it 'user leaves project' do
click_button 'Leave'
it 'user is removed from project' do
click_link 'Leave'
page.within('[role="dialog"]') do
click_button('Leave')
end
expect(project.users.exists?(user.id)).to be_falsey
end
context 'when the user has been specifically allowed to access a protected branch' do
let!(:matching_protected_branch) { create(:protected_branch, authorize_user_to_push: user, authorize_user_to_merge: user, project: project) }
let!(:non_matching_protected_branch) { create(:protected_branch, authorize_user_to_push: other_user, authorize_user_to_merge: other_user, project: project) }
it 'user leaves project' do
click_link 'Leave'
expect(project.users.exists?(user.id)).to be_falsey
expect(matching_protected_branch.push_access_levels.where(user: user)).not_to exist
expect(matching_protected_branch.merge_access_levels.where(user: user)).not_to exist
expect(non_matching_protected_branch.push_access_levels.where(user: other_user)).to exist
expect(non_matching_protected_branch.merge_access_levels.where(user: other_user)).to exist
end
expect(matching_protected_branch.push_access_levels.where(user: user)).not_to exist
expect(matching_protected_branch.merge_access_levels.where(user: user)).not_to exist
expect(non_matching_protected_branch.push_access_levels.where(user: other_user)).to exist
expect(non_matching_protected_branch.merge_access_levels.where(user: other_user)).to exist
end
end
end
......@@ -3451,9 +3451,6 @@ msgstr ""
msgid "An error occurred while loading the file. Please try again later."
msgstr ""
msgid "An error occurred while loading the members, please try again."
msgstr ""
msgid "An error occurred while loading the merge request changes."
msgstr ""
......@@ -14925,9 +14922,6 @@ msgstr ""
msgid "Groups to synchronize"
msgstr ""
msgid "Groups with access to %{strong_open}%{project_name}%{strong_close}"
msgstr ""
msgid "GroupsDropdown|Frequently visited"
msgstr ""
......@@ -18766,9 +18760,6 @@ msgstr ""
msgid "Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}"
msgstr ""
msgid "Members invited to %{strong_start}%{project_name}%{strong_end}"
msgstr ""
msgid "Members listed as CODEOWNERS of affected files."
msgstr ""
......@@ -18778,9 +18769,6 @@ msgstr ""
msgid "Members of %{group} can also push to this branch: %{branch}"
msgstr ""
msgid "Members of %{strong_open}%{project_name}%{strong_close}"
msgstr ""
msgid "Members of a group may only view projects they have permission to access"
msgstr ""
......@@ -32946,9 +32934,6 @@ msgstr ""
msgid "Users requesting access to"
msgstr ""
msgid "Users requesting access to %{strong_start}%{project_name}%{strong_end}"
msgstr ""
msgid "Users were successfully added."
msgstr ""
......
......@@ -4,12 +4,8 @@ module QA
RSpec.describe 'Manage', :requires_admin do
describe 'Add project member' do
before do
Runtime::Feature.enable('vue_project_members_list')
Runtime::Feature.enable(:invite_members_group_modal)
end
after do
Runtime::Feature.disable('vue_project_members_list')
end
it 'user adds project member', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/482' do
Flow::Login.sign_in
......
......@@ -16,15 +16,10 @@ module QA
end
before do
Runtime::Feature.enable('vue_project_members_list', project: project)
Runtime::Feature.enable(:invite_members_group_modal)
Flow::Login.sign_in
end
after do
Runtime::Feature.disable('vue_project_members_list', project: project)
end
it 'is received by a user for project invitation', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/676' do
project.visit!
......
......@@ -103,7 +103,6 @@ module QA
context 'Add and remove project access', :requires_admin, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/735' do
before do
Runtime::Feature.enable('vue_project_members_list', project: project)
Runtime::Feature.enable(:invite_members_group_modal)
sign_in
project.visit!
......@@ -121,10 +120,6 @@ module QA
group.visit!
end
after do
Runtime::Feature.disable('vue_project_members_list', project: project)
end
it_behaves_like 'audit event', ['Added project access', 'Removed project access']
end
end
......
......@@ -92,97 +92,46 @@ RSpec.describe "Admin::Projects" do
end
end
context 'when `vue_project_members_list` feature flag is enabled', :js do
describe 'admin adds themselves to the project' do
before do
project.add_maintainer(user)
stub_feature_flags(invite_members_group_modal: false)
end
it 'adds admin to the project as developer', :js do
visit project_project_members_path(project)
page.within '.invite-users-form' do
select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level'
end
click_button 'Invite'
expect(find_member_row(current_user)).to have_content('Developer')
end
describe 'admin adds themselves to the project', :js do
before do
project.add_maintainer(user)
stub_feature_flags(invite_members_group_modal: false)
end
describe 'admin removes themselves from the project' do
before do
project.add_maintainer(user)
project.add_developer(current_user)
end
it 'removes admin from the project' do
visit project_project_members_path(project)
expect(find_member_row(current_user)).to have_content('Developer')
it 'adds admin to the project as developer' do
visit project_project_members_path(project)
page.within find_member_row(current_user) do
click_button 'Leave'
end
page.within '.invite-users-form' do
select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level'
end
page.within('[role="dialog"]') do
click_button('Leave')
end
click_button 'Invite'
expect(current_path).to match dashboard_projects_path
end
expect(find_member_row(current_user)).to have_content('Developer')
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
describe 'admin removes themselves from the project', :js do
before do
stub_feature_flags(vue_project_members_list: false)
project.add_maintainer(user)
project.add_developer(current_user)
end
describe 'admin adds themselves to the project' do
before do
project.add_maintainer(user)
stub_feature_flags(invite_members_group_modal: false)
end
it 'adds admin to the project as developer', :js do
visit project_project_members_path(project)
page.within '.invite-users-form' do
select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level'
end
it 'removes admin from the project' do
visit project_project_members_path(project)
click_button 'Invite'
expect(find_member_row(current_user)).to have_content('Developer')
page.within '.content-list' do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
page.within find_member_row(current_user) do
click_button 'Leave'
end
end
describe 'admin removes themselves from the project' do
before do
project.add_maintainer(user)
project.add_developer(current_user)
page.within('[role="dialog"]') do
click_button('Leave')
end
it 'removes admin from the project' do
visit project_project_members_path(project)
page.within '.content-list' do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
find(:css, '.content-list li', text: current_user.name).find(:css, 'a.btn-danger').click
expect(page).not_to have_selector(:css, '.content-list')
end
expect(current_path).to match dashboard_projects_path
end
end
end
......@@ -14,25 +14,9 @@ RSpec.describe 'Projects > Members > Anonymous user sees members' do
create(:project_group_link, project: project, group: group)
end
context 'when `vue_project_members_list` feature flag is enabled', :js do
it "anonymous user visits the project's members page and sees the list of members" do
visit project_project_members_path(project)
it "anonymous user visits the project's members page and sees the list of members", :js do
visit project_project_members_path(project)
expect(find_member_row(user)).to have_content(user.name)
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
end
it "anonymous user visits the project's members page and sees the list of members" do
visit project_project_members_path(project)
expect(current_path).to eq(
project_project_members_path(project))
expect(page).to have_content(user.name)
end
expect(find_member_row(user)).to have_content(user.name)
end
end
......@@ -20,218 +20,96 @@ RSpec.describe 'Projects members', :js do
sign_in(user)
end
context 'when `vue_project_members_list` feature flag is enabled' do
context 'with a group invitee' do
before do
group_invitee
visit project_project_members_path(project)
end
it 'does not appear in the project members page' do
expect(members_table).not_to have_content('test2@abc.com')
end
context 'with a group invitee' do
before do
group_invitee
visit project_project_members_path(project)
end
context 'with a group' do
it 'shows group and project members by default' do
visit project_project_members_path(project)
expect(members_table).to have_content(developer.name)
expect(members_table).to have_content(user.name)
expect(members_table).to have_content(group.name)
end
it 'shows project members only if requested' do
visit project_project_members_path(project, with_inherited_permissions: 'exclude')
expect(members_table).to have_content(developer.name)
expect(members_table).not_to have_content(user.name)
expect(members_table).not_to have_content(group.name)
end
it 'does not appear in the project members page' do
expect(members_table).not_to have_content('test2@abc.com')
end
end
it 'shows group members only if requested' do
visit project_project_members_path(project, with_inherited_permissions: 'only')
context 'with a group' do
it 'shows group and project members by default' do
visit project_project_members_path(project)
expect(members_table).not_to have_content(developer.name)
expect(members_table).to have_content(user.name)
expect(members_table).to have_content(group.name)
end
expect(members_table).to have_content(developer.name)
expect(members_table).to have_content(user.name)
expect(members_table).to have_content(group.name)
end
context 'with a group, a project invitee, and a project requester' do
before do
group.request_access(group_requester)
project.request_access(project_requester)
group_invitee
project_invitee
visit project_project_members_path(project)
end
it 'shows the group owner' do
expect(members_table).to have_content(user.name)
expect(members_table).to have_content(group.name)
end
it 'shows the project developer' do
expect(members_table).to have_content(developer.name)
end
it 'shows the project invitee' do
click_link 'Invited'
expect(members_table).to have_content('test1@abc.com')
expect(members_table).not_to have_content('test2@abc.com')
end
it 'shows the project requester' do
click_link 'Access requests'
expect(members_table).to have_content(project_requester.name)
expect(members_table).not_to have_content(group_requester.name)
end
end
it 'shows project members only if requested' do
visit project_project_members_path(project, with_inherited_permissions: 'exclude')
context 'with a group requester' do
before do
stub_feature_flags(invite_members_group_modal: false)
group.request_access(group_requester)
visit project_project_members_path(project)
end
it 'does not appear in the project members page' do
expect(page).not_to have_link('Access requests')
expect(members_table).not_to have_content(group_requester.name)
end
expect(members_table).to have_content(developer.name)
expect(members_table).not_to have_content(user.name)
expect(members_table).not_to have_content(group.name)
end
context 'showing status of members' do
it 'shows the status' do
create(:user_status, user: user, emoji: 'smirk', message: 'Authoring this object')
it 'shows group members only if requested' do
visit project_project_members_path(project, with_inherited_permissions: 'only')
visit project_project_members_path(project)
expect(first_row).to have_selector('gl-emoji[data-name="smirk"]')
end
expect(members_table).not_to have_content(developer.name)
expect(members_table).to have_content(user.name)
expect(members_table).to have_content(group.name)
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
context 'with a group, a project invitee, and a project requester' do
before do
stub_feature_flags(vue_project_members_list: false)
group.request_access(group_requester)
project.request_access(project_requester)
group_invitee
project_invitee
visit project_project_members_path(project)
end
context 'with a group invitee' do
before do
group_invitee
visit project_project_members_path(project)
end
it 'does not appear in the project members page' do
page.within first('.content-list') do
expect(page).not_to have_content('test2@abc.com')
end
end
it 'shows the group owner' do
expect(members_table).to have_content(user.name)
expect(members_table).to have_content(group.name)
end
context 'with a group' do
it 'shows group and project members by default' do
visit project_project_members_path(project)
page.within first('.content-list') do
expect(page).to have_content(developer.name)
expect(page).to have_content(user.name)
expect(page).to have_content(group.name)
end
end
it 'shows project members only if requested' do
visit project_project_members_path(project, with_inherited_permissions: 'exclude')
page.within first('.content-list') do
expect(page).to have_content(developer.name)
it 'shows the project developer' do
expect(members_table).to have_content(developer.name)
end
expect(page).not_to have_content(user.name)
expect(page).not_to have_content(group.name)
end
end
it 'shows the project invitee' do
click_link 'Invited'
it 'shows group members only if requested' do
visit project_project_members_path(project, with_inherited_permissions: 'only')
expect(members_table).to have_content('test1@abc.com')
expect(members_table).not_to have_content('test2@abc.com')
end
page.within first('.content-list') do
expect(page).not_to have_content(developer.name)
it 'shows the project requester' do
click_link 'Access requests'
expect(page).to have_content(user.name)
expect(page).to have_content(group.name)
end
end
expect(members_table).to have_content(project_requester.name)
expect(members_table).not_to have_content(group_requester.name)
end
end
context 'with a group, a project invitee, and a project requester' do
before do
group.request_access(group_requester)
project.request_access(project_requester)
group_invitee
project_invitee
visit project_project_members_path(project)
end
it 'shows the group owner' do
page.within first('.content-list') do
# Group owner
expect(page).to have_content(user.name)
expect(page).to have_content(group.name)
end
end
it 'shows the project developer' do
page.within first('.content-list') do
# Project developer
expect(page).to have_content(developer.name)
end
end
it 'shows the project invitee' do
click_link 'Invited'
page.within first('.content-list') do
expect(page).to have_content('test1@abc.com')
expect(page).not_to have_content('test2@abc.com')
end
end
it 'shows the project requester' do
click_link 'Access requests'
page.within first('.content-list') do
expect(page).to have_content(project_requester.name)
expect(page).not_to have_content(group_requester.name)
end
end
context 'with a group requester' do
before do
stub_feature_flags(invite_members_group_modal: false)
group.request_access(group_requester)
visit project_project_members_path(project)
end
context 'with a group requester' do
before do
stub_feature_flags(invite_members_group_modal: false)
group.request_access(group_requester)
visit project_project_members_path(project)
end
it 'does not appear in the project members page' do
expect(page).not_to have_link('Access requests')
page.within first('.content-list') do
expect(page).not_to have_content(group_requester.name)
end
end
it 'does not appear in the project members page' do
expect(page).not_to have_link('Access requests')
expect(members_table).not_to have_content(group_requester.name)
end
end
context 'showing status of members' do
it 'shows the status' do
create(:user_status, user: user, emoji: 'smirk', message: 'Authoring this object')
context 'showing status of members' do
it_behaves_like 'showing user status' do
let(:user_with_status) { developer }
visit project_project_members_path(project)
subject { visit project_project_members_path(project) }
end
expect(first_row).to have_selector('gl-emoji[data-name="smirk"]')
end
end
end
......@@ -17,172 +17,80 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
project.add_maintainer(user)
sign_in(user)
end
context 'when `vue_project_members_list` feature flag is enabled' do
before do
visit project_project_members_path(project)
click_groups_tab
end
it 'updates group access level' do
click_button group_link.human_access
click_button 'Guest'
wait_for_requests
visit project_project_members_path(project)
click_groups_tab
expect(find_group_row(group)).to have_content('Guest')
end
visit project_project_members_path(project)
click_groups_tab
end
it 'updates expiry date' do
page.within find_group_row(group) do
fill_in 'Expiration date', with: 5.days.from_now.to_date
find_field('Expiration date').native.send_keys :enter
it 'updates group access level' do
click_button group_link.human_access
click_button 'Guest'
wait_for_requests
wait_for_requests
expect(page).to have_content(/in \d days/)
end
end
visit project_project_members_path(project)
context 'when link has expiry date set' do
let(:additional_link_attrs) { { expires_at: 5.days.from_now.to_date } }
click_groups_tab
it 'clears expiry date' do
page.within find_group_row(group) do
expect(page).to have_content(/in \d days/)
expect(find_group_row(group)).to have_content('Guest')
end
find('[data-testid="clear-button"]').click
it 'updates expiry date' do
page.within find_group_row(group) do
fill_in 'Expiration date', with: 5.days.from_now.to_date
find_field('Expiration date').native.send_keys :enter
wait_for_requests
wait_for_requests
expect(page).to have_content('No expiration set')
end
end
expect(page).to have_content(/in \d days/)
end
end
it 'deletes group link' do
expect(page).to have_content(group.full_name)
context 'when link has expiry date set' do
let(:additional_link_attrs) { { expires_at: 5.days.from_now.to_date } }
it 'clears expiry date' do
page.within find_group_row(group) do
click_button 'Remove group'
end
page.within('[role="dialog"]') do
click_button('Remove group')
end
expect(page).not_to have_content(group.full_name)
end
context 'search in existing members' do
it 'finds no results' do
fill_in_filtered_search 'Search groups', with: 'testing 123'
click_groups_tab
expect(page).not_to have_content(group.full_name)
end
expect(page).to have_content(/in \d days/)
it 'finds results' do
fill_in_filtered_search 'Search groups', with: group.full_name
find('[data-testid="clear-button"]').click
click_groups_tab
wait_for_requests
expect(members_table).to have_content(group.full_name)
expect(page).to have_content('No expiration set')
end
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
visit project_project_members_path(project)
click_groups_tab
end
it 'updates group access level' do
click_button group_link.human_access
page.within '.dropdown-menu' do
click_link 'Guest'
end
wait_for_requests
visit project_project_members_path(project)
click_groups_tab
it 'deletes group link' do
expect(page).to have_content(group.full_name)
expect(first('.group_member')).to have_content('Guest')
page.within find_group_row(group) do
click_button 'Remove group'
end
it 'updates expiry date' do
expires_at_field = "member_expires_at_#{group.id}"
fill_in expires_at_field, with: 3.days.from_now.to_date
find_field(expires_at_field).native.send_keys :enter
wait_for_requests
page.within(find('li.group_member')) do
expect(page).to have_content('Expires in 3 days')
end
page.within('[role="dialog"]') do
click_button('Remove group')
end
context 'when link has expiry date set' do
let(:additional_link_attrs) { { expires_at: 3.days.from_now.to_date } }
it 'clears expiry date' do
page.within(find('li.group_member')) do
expect(page).to have_content('Expires in 3 days')
page.within(find('.js-edit-member-form')) do
find('.js-clear-input').click
end
wait_for_requests
expect(page).not_to have_content(group.full_name)
end
expect(page).not_to have_content('Expires in')
end
end
end
context 'search in existing members' do
it 'finds no results' do
fill_in_filtered_search 'Search groups', with: 'testing 123'
it 'deletes group link' do
page.within(first('.group_member')) do
accept_confirm { find('.btn-danger').click }
end
wait_for_requests
click_groups_tab
expect(page).not_to have_selector('.group_member')
expect(page).not_to have_content(group.full_name)
end
context 'search in existing members' do
it 'finds no results' do
page.within '.user-search-form' do
fill_in 'search_groups', with: 'testing 123'
find('.user-search-btn').click
end
click_groups_tab
expect(page).not_to have_selector('.group_member')
end
it 'finds results' do
page.within '.user-search-form' do
fill_in 'search_groups', with: group.name
find('.user-search-btn').click
end
it 'finds results' do
fill_in_filtered_search 'Search groups', with: group.full_name
click_groups_tab
click_groups_tab
expect(page).to have_selector('.group_member', count: 1)
end
expect(members_table).to have_content(group.full_name)
end
end
......
......@@ -41,46 +41,20 @@ RSpec.describe 'Project > Members > Invite group', :js do
context 'when the group has "Share with group lock" disabled' do
it_behaves_like 'the project can be shared with groups'
context 'when `vue_project_members_list` feature flag is enabled' do
it 'the project can be shared with another group' do
visit project_project_members_path(project)
it 'the project can be shared with another group' do
visit project_project_members_path(project)
expect(page).not_to have_link 'Groups'
expect(page).not_to have_link 'Groups'
click_on 'invite-group-tab'
click_on 'invite-group-tab'
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
find('.btn-confirm').click
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
find('.btn-confirm').click
click_link 'Groups'
click_link 'Groups'
expect(members_table).to have_content(group_to_share_with.name)
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
end
it 'the project can be shared with another group' do
visit project_project_members_path(project)
expect(page).not_to have_link 'Groups'
click_on 'invite-group-tab'
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
find('.btn-confirm').click
click_link 'Groups'
page.within('[data-testid="project-member-groups"]') do
expect(page).to have_content(group_to_share_with.name)
end
end
expect(members_table).to have_content(group_to_share_with.name)
end
end
......@@ -162,33 +136,12 @@ RSpec.describe 'Project > Members > Invite group', :js do
find('.btn-confirm').click
end
context 'when `vue_project_members_list` feature flag is enabled' do
it 'the group link shows the expiration time with a warning class' do
setup
click_link 'Groups'
expect(find_group_row(group)).to have_content(/in \d days/)
expect(find_group_row(group)).to have_selector('.gl-text-orange-500')
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
end
it 'the group link shows the expiration time with a warning class' do
setup
click_link 'Groups'
it 'the group link shows the expiration time with a warning class' do
setup
click_link 'Groups'
page.within('[data-testid="project-member-groups"]') do
# Using distance_of_time_in_words_to_now because it is not the same as
# subtraction, and this way avoids time zone issues as well
expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
expect(page).to have_content(expires_in_text)
expect(page).to have_selector('.text-warning')
end
end
expect(find_group_row(group)).to have_content(/in \d days/)
expect(find_group_row(group)).to have_selector('.gl-text-orange-500')
end
end
......
......@@ -2,8 +2,9 @@
require 'spec_helper'
RSpec.describe 'Project members list' do
RSpec.describe 'Project members list', :js do
include Select2Helper
include Spec::Support::Helpers::Features::MembersHelpers
let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') }
......@@ -17,264 +18,154 @@ RSpec.describe 'Project members list' do
group.add_owner(user1)
end
context 'when `vue_project_members_list` feature flag is enabled', :js do
include Spec::Support::Helpers::Features::MembersHelpers
it 'show members from project and group' do
project.add_developer(user2)
it 'pushes `vue_project_members_list` feature flag to the frontend' do
visit_members_page
expect(page).to have_pushed_frontend_feature_flags(vueProjectMembersList: true)
end
it 'show members from project and group' do
project.add_developer(user2)
visit_members_page
visit_members_page
expect(first_row).to have_content(user1.name)
expect(second_row).to have_content(user2.name)
end
expect(first_row).to have_content(user1.name)
expect(second_row).to have_content(user2.name)
end
it 'show user once if member of both group and project' do
project.add_developer(user1)
it 'show user once if member of both group and project' do
project.add_developer(user1)
visit_members_page
visit_members_page
expect(first_row).to have_content(user1.name)
expect(second_row).to be_blank
end
expect(first_row).to have_content(user1.name)
expect(second_row).to be_blank
end
it 'update user access level', :js do
project.add_developer(user2)
it 'update user access level' do
project.add_developer(user2)
visit_members_page
visit_members_page
page.within find_member_row(user2) do
click_button('Developer')
click_button('Reporter')
page.within find_member_row(user2) do
click_button('Developer')
click_button('Reporter')
expect(page).to have_button('Reporter')
end
expect(page).to have_button('Reporter')
end
end
it 'add user to project', :js do
visit_members_page
it 'add user to project' do
visit_members_page
add_user(user2.name, 'Reporter')
add_user(user2.name, 'Reporter')
page.within find_member_row(user2) do
expect(page).to have_button('Reporter')
end
page.within find_member_row(user2) do
expect(page).to have_button('Reporter')
end
end
it 'uses ProjectMember access_level_roles for the invite members modal access option', :js do
visit_members_page
click_on 'Invite members'
click_on 'Guest'
wait_for_requests
page.within '.dropdown-menu' do
expect(page).to have_button('Guest')
expect(page).to have_button('Reporter')
expect(page).to have_button('Developer')
expect(page).to have_button('Maintainer')
expect(page).not_to have_button('Owner')
end
end
it 'remove user from project', :js do
other_user = create(:user)
project.add_developer(other_user)
visit_members_page
it 'uses ProjectMember access_level_roles for the invite members modal access option' do
visit_members_page
# Open modal
page.within find_member_row(other_user) do
click_button 'Remove member'
end
click_on 'Invite members'
page.within('[role="dialog"]') do
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
click_button('Remove member')
end
click_on 'Guest'
wait_for_requests
wait_for_requests
expect(members_table).not_to have_content(other_user.name)
page.within '.dropdown-menu' do
expect(page).to have_button('Guest')
expect(page).to have_button('Reporter')
expect(page).to have_button('Developer')
expect(page).to have_button('Maintainer')
expect(page).not_to have_button('Owner')
end
end
it 'invite user to project', :js do
visit_members_page
add_user('test@example.com', 'Reporter')
it 'remove user from project' do
other_user = create(:user)
project.add_developer(other_user)
click_link 'Invited'
visit_members_page
page.within find_invited_member_row('test@example.com') do
expect(page).to have_button('Reporter')
end
# Open modal
page.within find_member_row(other_user) do
click_button 'Remove member'
end
context 'project bots' do
let(:project_bot) { create(:user, :project_bot, name: 'project_bot') }
before do
project.add_maintainer(project_bot)
end
it 'does not show form used to change roles and "Expiration date" or the remove user button' do
visit_members_page
page.within find_member_row(project_bot) do
expect(page).not_to have_button('Maintainer')
expect(page).to have_field('Expiration date', disabled: true)
expect(page).not_to have_button('Remove member')
end
end
page.within('[role="dialog"]') do
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
click_button('Remove member')
end
describe 'when user has 2FA enabled' do
let_it_be(:admin) { create(:admin) }
let_it_be(:user_with_2fa) { create(:user, :two_factor_via_otp) }
wait_for_requests
before do
project.add_guest(user_with_2fa)
end
it 'shows 2FA badge to user with "Maintainer" access level' do
project.add_maintainer(user1)
visit_members_page
expect(find_member_row(user_with_2fa)).to have_content('2FA')
end
it 'shows 2FA badge to admins' do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
visit_members_page
expect(find_member_row(user_with_2fa)).to have_content('2FA')
end
it 'does not show 2FA badge to users with access level below "Maintainer"' do
group.add_developer(user1)
expect(members_table).not_to have_content(other_user.name)
end
visit_members_page
it 'invite user to project' do
visit_members_page
expect(find_member_row(user_with_2fa)).not_to have_content('2FA')
end
add_user('test@example.com', 'Reporter')
it 'shows 2FA badge to themselves' do
sign_in(user_with_2fa)
click_link 'Invited'
visit_members_page
expect(find_member_row(user_with_2fa)).to have_content('2FA')
end
page.within find_invited_member_row('test@example.com') do
expect(page).to have_button('Reporter')
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
include Spec::Support::Helpers::Features::ListRowsHelpers
context 'project bots' do
let(:project_bot) { create(:user, :project_bot, name: 'project_bot') }
before do
stub_feature_flags(vue_project_members_list: false)
project.add_maintainer(project_bot)
end
it 'show members from project and group' do
project.add_developer(user2)
it 'does not show form used to change roles and "Expiration date" or the remove user button' do
visit_members_page
expect(first_row.text).to include(user1.name)
expect(second_row.text).to include(user2.name)
page.within find_member_row(project_bot) do
expect(page).not_to have_button('Maintainer')
expect(page).to have_field('Expiration date', disabled: true)
expect(page).not_to have_button('Remove member')
end
end
end
it 'show user once if member of both group and project' do
project.add_developer(user1)
visit_members_page
describe 'when user has 2FA enabled' do
let_it_be(:admin) { create(:admin) }
let_it_be(:user_with_2fa) { create(:user, :two_factor_via_otp) }
expect(first_row.text).to include(user1.name)
expect(second_row).to be_blank
before do
project.add_guest(user_with_2fa)
end
it 'update user access level', :js do
project.add_developer(user2)
visit_members_page
page.within(second_row) do
click_button('Developer')
click_link('Reporter')
expect(page).to have_button('Reporter')
end
end
it 'shows 2FA badge to user with "Maintainer" access level' do
project.add_maintainer(user1)
it 'add user to project', :js do
visit_members_page
add_user(user2.name, 'Reporter')
page.within(second_row) do
expect(page).to have_content(user2.name)
expect(page).to have_button('Reporter')
end
expect(find_member_row(user_with_2fa)).to have_content('2FA')
end
it 'remove user from project', :js do
other_user = create(:user)
project.add_developer(other_user)
it 'shows 2FA badge to admins' do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
visit_members_page
# Open modal
find(:css, 'li.project_member', text: other_user.name).find(:css, 'button.btn-danger').click
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
click_on('Remove member')
wait_for_requests
expect(page).not_to have_content(other_user.name)
expect(project.users).not_to include(other_user)
expect(find_member_row(user_with_2fa)).to have_content('2FA')
end
it 'invite user to project', :js do
visit_members_page
add_user('test@example.com', 'Reporter')
it 'does not show 2FA badge to users with access level below "Maintainer"' do
group.add_developer(user1)
click_link 'Invited'
visit_members_page
page.within(first_row) do
expect(page).to have_content('test@example.com')
expect(page).to have_content('Invited')
expect(page).to have_button('Reporter')
end
expect(find_member_row(user_with_2fa)).not_to have_content('2FA')
end
context 'project bots' do
let(:project_bot) { create(:user, :project_bot, name: 'project_bot') }
it 'shows 2FA badge to themselves' do
sign_in(user_with_2fa)
before do
project.add_maintainer(project_bot)
end
it 'does not show form used to change roles and "Expiration date" or the remove user button' do
project_member = project.project_members.find_by(user_id: project_bot.id)
visit_members_page
visit_members_page
expect(page).not_to have_selector("#edit_project_member_#{project_member.id}")
expect(page).to have_no_selector("#project_member_#{project_member.id} .btn-danger")
end
expect(find_member_row(user_with_2fa)).to have_content('2FA')
end
end
......
......@@ -18,107 +18,51 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date
sign_in(maintainer)
end
context 'when `vue_project_members_list` feature flag is enabled' do
it 'expiration date is displayed in the members list' do
stub_feature_flags(invite_members_group_modal: false)
it 'expiration date is displayed in the members list' do
stub_feature_flags(invite_members_group_modal: false)
visit project_project_members_path(project)
visit project_project_members_path(project)
page.within '.invite-users-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
page.within '.invite-users-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: 5.days.from_now.to_date
find_field('expires_at').native.send_keys :enter
fill_in 'expires_at', with: 5.days.from_now.to_date
find_field('expires_at').native.send_keys :enter
click_on 'Invite'
end
page.within find_member_row(new_member) do
expect(page).to have_content(/in \d days/)
end
end
it 'changes expiration date' do
project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date)
visit project_project_members_path(project)
page.within find_member_row(new_member) do
fill_in 'Expiration date', with: 5.days.from_now.to_date
find_field('Expiration date').native.send_keys :enter
wait_for_requests
expect(page).to have_content(/in \d days/)
end
click_on 'Invite'
end
it 'clears expiration date' do
project.team.add_users([new_member.id], :developer, expires_at: 5.days.from_now.to_date)
visit project_project_members_path(project)
page.within find_member_row(new_member) do
expect(page).to have_content(/in \d days/)
find('[data-testid="clear-button"]').click
wait_for_requests
expect(page).to have_content('No expiration set')
end
page.within find_member_row(new_member) do
expect(page).to have_content(/in \d days/)
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
end
it 'expiration date is displayed in the members list' do
stub_feature_flags(invite_members_group_modal: false)
visit project_project_members_path(project)
page.within '.invite-users-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: 3.days.from_now.to_date
find_field('expires_at').native.send_keys :enter
it 'changes expiration date' do
project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date)
visit project_project_members_path(project)
click_on 'Invite'
end
page.within find_member_row(new_member) do
fill_in 'Expiration date', with: 5.days.from_now.to_date
find_field('Expiration date').native.send_keys :enter
page.within "#project_member_#{project_member_id}" do
expect(page).to have_content('Expires in 3 days')
end
end
it 'changes expiration date' do
project.team.add_users([new_member.id], :developer, expires_at: 1.day.from_now.to_date)
visit project_project_members_path(project)
page.within "#project_member_#{project_member_id}" do
fill_in 'Expiration date', with: 3.days.from_now.to_date
find_field('Expiration date').native.send_keys :enter
wait_for_requests
wait_for_requests
expect(page).to have_content('Expires in 3 days')
end
expect(page).to have_content(/in \d days/)
end
end
it 'clears expiration date' do
project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date)
visit project_project_members_path(project)
it 'clears expiration date' do
project.team.add_users([new_member.id], :developer, expires_at: 5.days.from_now.to_date)
visit project_project_members_path(project)
page.within "#project_member_#{project_member_id}" do
expect(page).to have_content('Expires in 3 days')
page.within find_member_row(new_member) do
expect(page).to have_content(/in \d days/)
find('.js-clear-input').click
find('[data-testid="clear-button"]').click
wait_for_requests
wait_for_requests
expect(page).not_to have_content('Expires in')
end
expect(page).to have_content('No expiration set')
end
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Sorting' do
RSpec.describe 'Projects > Members > Sorting', :js do
include Spec::Support::Helpers::Features::MembersHelpers
let(:maintainer) { create(:user, name: 'John Doe') }
......@@ -15,165 +15,85 @@ RSpec.describe 'Projects > Members > Sorting' do
sign_in(maintainer)
end
context 'when `vue_project_members_list` feature flag is enabled', :js do
it 'sorts by account by default' do
visit_members_list(sort: nil)
it 'sorts by account by default' do
visit_members_list(sort: nil)
expect(first_row).to have_content(maintainer.name)
expect(second_row).to have_content(developer.name)
expect(first_row).to have_content(maintainer.name)
expect(second_row).to have_content(developer.name)
expect_sort_by('Account', :asc)
end
it 'sorts by max role ascending' do
visit_members_list(sort: :access_level_asc)
expect(first_row).to have_content(developer.name)
expect(second_row).to have_content(maintainer.name)
expect_sort_by('Max role', :asc)
end
it 'sorts by max role descending' do
visit_members_list(sort: :access_level_desc)
expect(first_row).to have_content(maintainer.name)
expect(second_row).to have_content(developer.name)
expect_sort_by('Max role', :desc)
end
it 'sorts by access granted ascending' do
visit_members_list(sort: :last_joined)
expect(first_row).to have_content(maintainer.name)
expect(second_row).to have_content(developer.name)
expect_sort_by('Access granted', :asc)
end
it 'sorts by access granted descending' do
visit_members_list(sort: :oldest_joined)
expect(first_row).to have_content(developer.name)
expect(second_row).to have_content(maintainer.name)
expect_sort_by('Access granted', :desc)
end
it 'sorts by account ascending' do
visit_members_list(sort: :name_asc)
expect(first_row).to have_content(maintainer.name)
expect(second_row).to have_content(developer.name)
expect_sort_by('Account', :asc)
end
it 'sorts by account descending' do
visit_members_list(sort: :name_desc)
expect(first_row).to have_content(developer.name)
expect(second_row).to have_content(maintainer.name)
expect_sort_by('Account', :desc)
end
expect_sort_by('Account', :asc)
end
it 'sorts by last sign-in ascending', :clean_gitlab_redis_shared_state do
visit_members_list(sort: :recent_sign_in)
it 'sorts by max role ascending' do
visit_members_list(sort: :access_level_asc)
expect(first_row).to have_content(maintainer.name)
expect(second_row).to have_content(developer.name)
expect(first_row).to have_content(developer.name)
expect(second_row).to have_content(maintainer.name)
expect_sort_by('Last sign-in', :asc)
end
expect_sort_by('Max role', :asc)
end
it 'sorts by last sign-in descending', :clean_gitlab_redis_shared_state do
visit_members_list(sort: :oldest_sign_in)
it 'sorts by max role descending' do
visit_members_list(sort: :access_level_desc)
expect(first_row).to have_content(developer.name)
expect(second_row).to have_content(maintainer.name)
expect(first_row).to have_content(maintainer.name)
expect(second_row).to have_content(developer.name)
expect_sort_by('Last sign-in', :desc)
end
expect_sort_by('Max role', :desc)
end
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
end
it 'sorts alphabetically by default' do
visit_members_list(sort: nil)
it 'sorts by access granted ascending' do
visit_members_list(sort: :last_joined)
expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
expect(first_row).to have_content(maintainer.name)
expect(second_row).to have_content(developer.name)
it 'sorts by access level ascending' do
visit_members_list(sort: :access_level_asc)
expect_sort_by('Access granted', :asc)
end
expect(first_member).to include(developer.name)
expect(second_member).to include(maintainer.name)
expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
end
it 'sorts by access granted descending' do
visit_members_list(sort: :oldest_joined)
it 'sorts by access level descending' do
visit_members_list(sort: :access_level_desc)
expect(first_row).to have_content(developer.name)
expect(second_row).to have_content(maintainer.name)
expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
end
expect_sort_by('Access granted', :desc)
end
it 'sorts by last joined' do
visit_members_list(sort: :last_joined)
it 'sorts by account ascending' do
visit_members_list(sort: :name_asc)
expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
end
expect(first_row).to have_content(maintainer.name)
expect(second_row).to have_content(developer.name)
it 'sorts by oldest joined' do
visit_members_list(sort: :oldest_joined)
expect_sort_by('Account', :asc)
end
expect(first_member).to include(developer.name)
expect(second_member).to include(maintainer.name)
expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
end
it 'sorts by account descending' do
visit_members_list(sort: :name_desc)
it 'sorts by name ascending' do
visit_members_list(sort: :name_asc)
expect(first_row).to have_content(developer.name)
expect(second_row).to have_content(maintainer.name)
expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
expect_sort_by('Account', :desc)
end
it 'sorts by name descending' do
visit_members_list(sort: :name_desc)
it 'sorts by last sign-in ascending', :clean_gitlab_redis_shared_state do
visit_members_list(sort: :recent_sign_in)
expect(first_member).to include(developer.name)
expect(second_member).to include(maintainer.name)
expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
end
expect(first_row).to have_content(maintainer.name)
expect(second_row).to have_content(developer.name)
it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
visit_members_list(sort: :recent_sign_in)
expect_sort_by('Last sign-in', :asc)
end
expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
end
it 'sorts by last sign-in descending', :clean_gitlab_redis_shared_state do
visit_members_list(sort: :oldest_sign_in)
it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
visit_members_list(sort: :oldest_sign_in)
expect(first_row).to have_content(developer.name)
expect(second_row).to have_content(maintainer.name)
expect(first_member).to include(developer.name)
expect(second_member).to include(maintainer.name)
expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
end
expect_sort_by('Last sign-in', :desc)
end
private
......
......@@ -14,6 +14,11 @@ RSpec.describe 'Projects > Members > Tabs' do
let_it_be(:invites) { create_list(:project_member, 2, :invited, project: project) }
let_it_be(:project_group_links) { create_list(:project_group_link, 2, project: project) }
before do
sign_in(user)
visit project_project_members_path(project)
end
shared_examples 'active "Members" tab' do
it 'displays "Members" tab' do
expect(page).to have_selector('.nav-link.active', text: 'Members')
......@@ -21,11 +26,6 @@ RSpec.describe 'Projects > Members > Tabs' do
end
context 'tabs' do
before do
sign_in(user)
visit project_project_members_path(project)
end
where(:tab, :count) do
'Members' | 3
'Invited' | 2
......@@ -44,69 +44,25 @@ RSpec.describe 'Projects > Members > Tabs' do
end
end
context 'when `vue_project_members_list` feature flag is enabled' do
context 'when searching "Groups"', :js do
before do
sign_in(user)
visit project_project_members_path(project)
end
context 'when searching "Groups"', :js do
before do
click_link 'Groups'
fill_in_filtered_search 'Search groups', with: 'group'
end
it 'displays "Groups" tab' do
expect(page).to have_selector('.nav-link.active', text: 'Groups')
end
click_link 'Groups'
context 'and then searching "Members"' do
before do
click_link 'Members 3'
fill_in_filtered_search 'Filter members', with: 'user'
end
it_behaves_like 'active "Members" tab'
end
fill_in_filtered_search 'Search groups', with: 'group'
end
end
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
sign_in(user)
visit project_project_members_path(project)
it 'displays "Groups" tab' do
expect(page).to have_selector('.nav-link.active', text: 'Groups')
end
context 'when searching "Groups"', :js do
context 'and then searching "Members"' do
before do
click_link 'Groups'
click_link 'Members 3'
page.within '[data-testid="group-link-search-form"]' do
fill_in 'search_groups', with: 'group'
find('button[type="submit"]').click
end
fill_in_filtered_search 'Filter members', with: 'user'
end
it 'displays "Groups" tab' do
expect(page).to have_selector('.nav-link.active', text: 'Groups')
end
context 'and then searching "Members"' do
before do
click_link 'Members 3'
page.within '[data-testid="user-search-form"]' do
fill_in 'search', with: 'user'
find('button[type="submit"]').click
end
end
it_behaves_like 'active "Members" tab'
end
it_behaves_like 'active "Members" tab'
end
end
end
......@@ -19,123 +19,54 @@ RSpec.describe 'Projects > Settings > User manages project members' do
sign_in(user)
end
context 'when `vue_project_members_list` feature flag is enabled' do
it 'cancels a team member', :js do
visit(project_project_members_path(project))
it 'cancels a team member', :js do
visit(project_project_members_path(project))
page.within find_member_row(user_dmitriy) do
click_button 'Remove member'
end
page.within('[role="dialog"]') do
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
click_button('Remove member')
end
visit(project_project_members_path(project))
expect(members_table).not_to have_content(user_dmitriy.name)
expect(members_table).not_to have_content(user_dmitriy.username)
page.within find_member_row(user_dmitriy) do
click_button 'Remove member'
end
it 'imports a team from another project', :js do
stub_feature_flags(invite_members_group_modal: false)
project2.add_maintainer(user)
project2.add_reporter(user_mike)
visit(project_project_members_path(project))
page.within('.invite-users-form') do
click_link('Import')
end
select2(project2.id, from: '#source_project_id')
click_button('Import project members')
expect(find_member_row(user_mike)).to have_content('Reporter')
page.within('[role="dialog"]') do
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
click_button('Remove member')
end
it 'shows all members of project shared group', :js do
group.add_owner(user)
group.add_developer(user_dmitriy)
share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)
share_link.group_id = group.id
share_link.save!
visit(project_project_members_path(project))
visit(project_project_members_path(project))
click_link 'Groups'
expect(find_group_row(group)).to have_content('Maintainer')
end
expect(members_table).not_to have_content(user_dmitriy.name)
expect(members_table).not_to have_content(user_dmitriy.username)
end
context 'when `vue_project_members_list` feature flag is disabled' do
before do
stub_feature_flags(vue_project_members_list: false)
end
it 'imports a team from another project', :js do
stub_feature_flags(invite_members_group_modal: false)
it 'cancels a team member', :js do
visit(project_project_members_path(project))
project2.add_maintainer(user)
project2.add_reporter(user_mike)
project_member = project.project_members.find_by(user_id: user_dmitriy.id)
visit(project_project_members_path(project))
page.within("#project_member_#{project_member.id}") do
# Open modal
click_on('Remove user from project')
end
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
click_on('Remove member')
visit(project_project_members_path(project))
expect(page).not_to have_content(user_dmitriy.name)
expect(page).not_to have_content(user_dmitriy.username)
page.within('.invite-users-form') do
click_link('Import')
end
it 'imports a team from another project' do
stub_feature_flags(invite_members_group_modal: false)
project2.add_maintainer(user)
project2.add_reporter(user_mike)
visit(project_project_members_path(project))
select2(project2.id, from: '#source_project_id')
click_button('Import project members')
page.within('.invite-users-form') do
click_link('Import')
end
select(project2.full_name, from: 'source_project_id')
click_button('Import')
project_member = project.project_members.find_by(user_id: user_mike.id)
page.within("#project_member_#{project_member.id}") do
expect(page).to have_content('Mike')
expect(page).to have_content('Reporter')
end
end
expect(find_member_row(user_mike)).to have_content('Reporter')
end
it 'shows all members of project shared group', :js do
group.add_owner(user)
group.add_developer(user_dmitriy)
it 'shows all members of project shared group', :js do
group.add_owner(user)
group.add_developer(user_dmitriy)
share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)
share_link.group_id = group.id
share_link.save!
share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)
share_link.group_id = group.id
share_link.save!
visit(project_project_members_path(project))
visit(project_project_members_path(project))
click_link 'Groups'
click_link 'Groups'
page.within('[data-testid="project-member-groups"]') do
expect(page).to have_content('OpenSource')
expect(first('.group_member')).to have_content('Maintainer')
end
end
expect(find_group_row(group)).to have_content('Maintainer')
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