Commit 3f50733d authored by Coung Ngo's avatar Coung Ngo Committed by Mike Greiling

Add option to unassign user from issuables when removing from project

This allows users to be removed from issuables such as confidential
ones which they might otherwise have access to despite being removed
from the project.
parent 396e78c0
import UsersSelect from '../../../../users_select';
import Vue from 'vue';
import UsersSelect from '~/users_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
document.addEventListener('DOMContentLoaded', () => new UsersSelect());
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
if (!el) {
return false;
}
return new Vue({
el,
render(createComponent) {
return createComponent(RemoveMemberModal);
},
});
}
document.addEventListener('DOMContentLoaded', () => {
mountRemoveMemberModal();
new UsersSelect(); // eslint-disable-line no-new
});
import ProjectsList from '../../../projects_list';
import NamespaceSelect from '../../../namespace_select';
import Vue from 'vue';
import ProjectsList from '~/projects_list';
import NamespaceSelect from '~/namespace_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
if (!el) {
return false;
}
return new Vue({
el,
render(createComponent) {
return createComponent(RemoveMemberModal);
},
});
}
document.addEventListener('DOMContentLoaded', () => {
mountRemoveMemberModal();
new ProjectsList(); // eslint-disable-line no-new
document
......
/* eslint-disable no-new */
import Vue from 'vue';
import Members from 'ee_else_ce/members';
import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
if (!el) {
return false;
}
return new Vue({
el,
render(createComponent) {
return createComponent(RemoveMemberModal);
},
});
}
document.addEventListener('DOMContentLoaded', () => {
groupsSelect();
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
new Members();
groupsSelect();
new UsersSelect();
mountRemoveMemberModal();
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
});
import Vue from 'vue';
import Members from 'ee_else_ce/members';
import memberExpirationDate from '../../../member_expiration_date';
import UsersSelect from '../../../users_select';
import groupsSelect from '../../../groups_select';
import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
if (!el) {
return false;
}
return new Vue({
el,
render(createComponent) {
return createComponent(RemoveMemberModal);
},
});
}
document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate('.js-access-expiration-date-groups');
groupsSelect();
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
});
<script>
import { GlFormCheckbox, GlModal } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import csrf from '~/lib/utils/csrf';
import { __ } from '~/locale';
export default {
actionCancel: {
text: __('Cancel'),
},
csrf,
components: {
GlFormCheckbox,
GlModal,
},
data() {
return {
modalData: {},
};
},
computed: {
isAccessRequest() {
return parseBoolean(this.modalData.isAccessRequest);
},
actionText() {
return this.isAccessRequest ? __('Deny access request') : __('Remove member');
},
actionPrimary() {
return {
text: this.actionText,
attributes: {
variant: 'danger',
},
};
},
},
mounted() {
document.addEventListener('click', this.handleClick);
},
beforeDestroy() {
document.removeEventListener('click', this.handleClick);
},
methods: {
handleClick(event) {
const removeButton = event.target.closest('.js-remove-member-button');
if (removeButton) {
this.modalData = removeButton.dataset;
this.$refs.modal.show();
}
},
submitForm() {
this.$refs.form.submit();
},
},
};
</script>
<template>
<gl-modal
ref="modal"
modal-id="remove-member-modal"
:action-cancel="$options.actionCancel"
:action-primary="actionPrimary"
:title="actionText"
@primary="submitForm"
>
<form ref="form" :action="modalData.memberPath" method="post">
<p data-testid="modal-message">{{ modalData.message }}</p>
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<gl-form-checkbox v-if="!isAccessRequest" name="unassign_issuables">
{{ __('Also unassign this user from related issues and merge requests') }}
</gl-form-checkbox>
</form>
</gl-modal>
</template>
......@@ -48,11 +48,11 @@ module MembersHelper
"#{request.path}?#{options.to_param}"
end
def member_path(member, unassign_issuables: false)
def member_path(member)
if member.is_a?(GroupMember)
group_group_member_path(member.source, member, { unassign_issuables: unassign_issuables })
group_group_member_path(member.source, member)
else
project_project_member_path(member.source, member, { unassign_issuables: unassign_issuables })
project_project_member_path(member.source, member)
end
end
......
- add_to_breadcrumbs _("Groups"), admin_groups_path
- breadcrumb_title @group.name
- page_title @group.name, _("Groups")
.js-remove-member-modal
%h3.page-title
= _('Group: %{group_name}') % { group_name: @group.full_name }
......
......@@ -3,6 +3,7 @@
- page_title @project.full_name, _("Projects")
- @content_class = "admin-projects"
.js-remove-member-modal
%h3.page-title
Project: #{@project.full_name}
= link_to edit_project_path(@project), class: "btn btn-nr float-right" do
......
......@@ -4,6 +4,7 @@
- pending_active = params[:search_invited].present?
- total_count = @members.count + @group.shared_with_group_links.count
.js-remove-member-modal
.project-members-page.gl-mt-3
%h4
= _("Group members")
......
- page_title _("Members")
- can_admin_project_members = can?(current_user, :admin_project_member, @project)
.js-remove-member-modal
.row.gl-mt-3
.col-lg-12
- if project_can_be_shared?
......
......@@ -118,11 +118,9 @@
data: { confirm: leave_confirmation_message(member.source) },
class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}"
- elsif !user&.project_bot?
= link_to member_path(member.member),
method: :delete,
data: { confirm: remove_member_message(member), qa_selector: 'delete_member_button' },
class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}",
title: remove_member_title(member) do
%button{ data: { member_path: member_path(member.member), message: remove_member_message(member), is_access_request: member.request?.to_s, qa_selector: 'delete_member_button' },
class: "js-remove-member-button btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}",
title: remove_member_title(member) }
%span{ class: ('d-block d-sm-none' unless force_mobile_view) }
= _("Delete")
- unless force_mobile_view
......
---
title: Add option to unassign member from issuables when removing them from a project
merge_request: 34946
author:
type: added
......@@ -2316,6 +2316,9 @@ msgstr ""
msgid "Also called \"Relying party service URL\" or \"Reply URL\""
msgstr ""
msgid "Also unassign this user from related issues and merge requests"
msgstr ""
msgid "Alternate support URL for help page and help dropdown"
msgstr ""
......@@ -7519,6 +7522,9 @@ msgstr ""
msgid "Deny"
msgstr ""
msgid "Deny access request"
msgstr ""
msgid "Dependencies"
msgstr ""
......@@ -19186,6 +19192,9 @@ msgstr ""
msgid "Remove limit"
msgstr ""
msgid "Remove member"
msgstr ""
msgid "Remove milestone"
msgstr ""
......
......@@ -68,9 +68,12 @@ RSpec.describe 'Groups > Members > Manage members' do
visit group_group_members_path(group)
accept_confirm do
find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click
end
# Open modal
find(:css, '.project-members-page li', text: user2.name).find(:css, 'button.btn-remove').click
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
click_on('Remove member')
wait_for_requests
......
......@@ -64,9 +64,12 @@ RSpec.describe 'Project members list' do
visit_members_page
accept_confirm do
find(:css, 'li.project_member', text: other_user.name).find(:css, 'a.btn-remove').click
end
# Open modal
find(:css, 'li.project_member', text: other_user.name).find(:css, 'button.btn-remove').click
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
click_on('Remove member')
wait_for_requests
......
......@@ -16,15 +16,20 @@ RSpec.describe 'Projects > Settings > User manages project members' do
sign_in(user)
end
it 'cancels a team member' do
it 'cancels a team member', :js do
visit(project_project_members_path(project))
project_member = project.project_members.find_by(user_id: user_dmitriy.id)
page.within("#project_member_#{project_member.id}") do
click_link('Remove user from project')
# 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)
......
import { GlFormCheckbox, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
describe('RemoveMemberModal', () => {
const memberPath = '/gitlab-org/gitlab-test/-/project_members/90';
let wrapper;
const findForm = () => wrapper.find({ ref: 'form' });
const findGlModal = () => wrapper.find(GlModal);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe.each`
state | isAccessRequest | actionText | checkboxTestDescription | checkboxExpected | message
${'removing a member'} | ${'false'} | ${'Remove member'} | ${'shows a checkbox to allow removal from related issues and MRs'} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'}
${'denying an access request'} | ${'true'} | ${'Deny access request'} | ${'does not show a checkbox'} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"}
`(
'when $state',
({ actionText, isAccessRequest, message, checkboxTestDescription, checkboxExpected }) => {
beforeEach(() => {
wrapper = shallowMount(RemoveMemberModal, {
data() {
return {
modalData: {
isAccessRequest,
message,
memberPath,
},
};
},
});
});
it(`has the title ${actionText}`, () => {
expect(findGlModal().attributes('title')).toBe(actionText);
});
it('contains a form action', () => {
expect(findForm().attributes('action')).toBe(memberPath);
});
it('displays a message to the user', () => {
expect(wrapper.find('[data-testid=modal-message]').text()).toBe(message);
});
it(`${checkboxTestDescription}`, () => {
expect(wrapper.contains(GlFormCheckbox)).toBe(checkboxExpected);
});
it('submits the form when the modal is submitted', () => {
const spy = jest.spyOn(findForm().element, 'submit');
findGlModal().vm.$emit('primary');
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
},
);
});
......@@ -35,7 +35,12 @@ RSpec.shared_examples 'Maintainer manages access requests' do
expect_visible_access_request(entity, user)
accept_confirm { click_on 'Deny access' }
# Open modal
click_on 'Deny access request'
expect(page).not_to have_field "Also unassign this user from related issues and merge requests"
click_on 'Deny access request'
expect_no_visible_access_request(entity, user)
expect(page).not_to have_content user.name
......
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