Merge branch '41301-new-design-for-user-deletion-confirmation-in-admin-area' into 'master'
Resolve "New design for user deletion confirmation in admin area" Closes #41301 See merge request gitlab-org/gitlab-ce!16811
This commit is contained in:
commit
9b7534cc2d
9 changed files with 288 additions and 17 deletions
|
@ -114,6 +114,16 @@ var Dispatcher;
|
|||
.then(callDefault)
|
||||
.catch(fail);
|
||||
break;
|
||||
case 'admin:users:index':
|
||||
import('./pages/admin/users/shared')
|
||||
.then(callDefault)
|
||||
.catch(fail);
|
||||
break;
|
||||
case 'admin:users:show':
|
||||
import('./pages/admin/users/shared')
|
||||
.then(callDefault)
|
||||
.catch(fail);
|
||||
break;
|
||||
case 'dashboard:projects:index':
|
||||
case 'dashboard:projects:starred':
|
||||
import('./pages/dashboard/projects')
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import modal from '~/vue_shared/components/modal.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
modal,
|
||||
},
|
||||
props: {
|
||||
deleteUserUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
blockUserUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
deleteContributions: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
csrfToken: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
enteredUsername: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?');
|
||||
const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?');
|
||||
|
||||
return sprintf(
|
||||
this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, {
|
||||
username: `'${_.escape(this.username)}'`,
|
||||
}, false);
|
||||
},
|
||||
text() {
|
||||
const keepContributionsText = s__(`AdminArea|
|
||||
You are about to permanently delete the user %{username}.
|
||||
This will delete all of the issues, merge requests, and groups linked to them.
|
||||
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
|
||||
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
|
||||
|
||||
const deleteContributionsText = s__(`AdminArea|
|
||||
You are about to permanently delete the user %{username}.
|
||||
Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
|
||||
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
|
||||
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
|
||||
|
||||
return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
|
||||
{
|
||||
username: `<strong>${_.escape(this.username)}</strong>`,
|
||||
strong_start: '<strong>',
|
||||
strong_end: '</strong>',
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
confirmationTextLabel() {
|
||||
return sprintf(s__('AdminUsers|To confirm, type %{username}'),
|
||||
{
|
||||
username: `<code>${_.escape(this.username)}</code>`,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
primaryButtonLabel() {
|
||||
const keepContributionsLabel = s__('AdminUsers|Delete user');
|
||||
const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions');
|
||||
|
||||
return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel;
|
||||
},
|
||||
secondaryButtonLabel() {
|
||||
return s__('AdminUsers|Block user');
|
||||
},
|
||||
canSubmit() {
|
||||
return this.enteredUsername === this.username;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onCancel() {
|
||||
this.enteredUsername = '';
|
||||
},
|
||||
onSecondaryAction() {
|
||||
const form = this.$refs.form;
|
||||
|
||||
form.action = this.blockUserUrl;
|
||||
this.$refs.method.value = 'put';
|
||||
|
||||
form.submit();
|
||||
},
|
||||
onSubmit() {
|
||||
this.$refs.form.submit();
|
||||
this.enteredUsername = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<modal
|
||||
id="delete-user-modal"
|
||||
:title="title"
|
||||
:text="text"
|
||||
kind="danger"
|
||||
:primary-button-label="primaryButtonLabel"
|
||||
:secondary-button-label="secondaryButtonLabel"
|
||||
:submit-disabled="!canSubmit"
|
||||
@submit="onSubmit"
|
||||
@cancel="onCancel"
|
||||
>
|
||||
<template
|
||||
slot="body"
|
||||
slot-scope="props"
|
||||
>
|
||||
<p v-html="props.text"></p>
|
||||
<p v-html="confirmationTextLabel"></p>
|
||||
<form
|
||||
ref="form"
|
||||
:action="deleteUserUrl"
|
||||
method="post"
|
||||
>
|
||||
<input
|
||||
ref="method"
|
||||
type="hidden"
|
||||
name="_method"
|
||||
value="delete"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="authenticity_token"
|
||||
:value="csrfToken"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
class="form-control"
|
||||
v-model="enteredUsername"
|
||||
aria-labelledby="input-label"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
<template
|
||||
slot="secondary-button"
|
||||
slot-scope="props"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn js-secondary-button btn-warning"
|
||||
:disabled="!canSubmit"
|
||||
@click="onSecondaryAction"
|
||||
data-dismiss="modal"
|
||||
>
|
||||
{{ secondaryButtonLabel }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
43
app/assets/javascripts/pages/admin/users/shared/index.js
Normal file
43
app/assets/javascripts/pages/admin/users/shared/index.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
|
||||
import deleteUserModal from './components/delete_user_modal.vue';
|
||||
|
||||
export default () => {
|
||||
Vue.use(Translate);
|
||||
|
||||
const deleteUserModalEl = document.getElementById('delete-user-modal');
|
||||
|
||||
const deleteModal = new Vue({
|
||||
el: deleteUserModalEl,
|
||||
data: {
|
||||
deleteUserUrl: '',
|
||||
blockUserUrl: '',
|
||||
deleteContributions: '',
|
||||
username: '',
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(deleteUserModal, {
|
||||
props: {
|
||||
deleteUserUrl: this.deleteUserUrl,
|
||||
blockUserUrl: this.blockUserUrl,
|
||||
deleteContributions: this.deleteContributions,
|
||||
username: this.username,
|
||||
csrfToken: csrf.token,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
$(document).on('shown.bs.modal', (event) => {
|
||||
if (event.relatedTarget.classList.contains('delete-user-button')) {
|
||||
const buttonProps = event.relatedTarget.dataset;
|
||||
deleteModal.deleteUserUrl = buttonProps.deleteUserUrl;
|
||||
deleteModal.blockUserUrl = buttonProps.blockUserUrl;
|
||||
deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions');
|
||||
deleteModal.username = buttonProps.username;
|
||||
}
|
||||
});
|
||||
};
|
|
@ -46,6 +46,11 @@
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
secondaryButtonLabel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
submitDisabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -129,6 +134,21 @@
|
|||
>
|
||||
{{ closeButtonLabel }}
|
||||
</button>
|
||||
|
||||
<slot
|
||||
v-if="secondaryButtonLabel"
|
||||
name="secondary-button"
|
||||
>
|
||||
<button
|
||||
v-if="secondaryButtonLabel"
|
||||
type="button"
|
||||
class="btn"
|
||||
data-dismiss="modal"
|
||||
>
|
||||
{{ secondaryButtonLabel }}
|
||||
</button>
|
||||
</slot>
|
||||
|
||||
<button
|
||||
v-if="primaryButtonLabel"
|
||||
type="button"
|
||||
|
|
|
@ -38,12 +38,19 @@
|
|||
%li.divider
|
||||
- if user.can_be_removed?
|
||||
%li
|
||||
= link_to 'Remove user', admin_user_path(user),
|
||||
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" },
|
||||
class: 'text-danger',
|
||||
method: :delete
|
||||
%button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
|
||||
target: '#delete-user-modal',
|
||||
delete_user_url: admin_user_path(user),
|
||||
block_user_url: block_admin_user_path(user),
|
||||
username: user.name,
|
||||
delete_contributions: 'false' }, type: 'button' }
|
||||
= s_('AdminUsers|Delete user')
|
||||
|
||||
%li
|
||||
= link_to 'Remove user and contributions', admin_user_path(user, hard_delete: true),
|
||||
data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and comments authored by this user, and groups owned solely by them, will also be removed! Are you sure?" },
|
||||
class: 'text-danger',
|
||||
method: :delete
|
||||
%button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
|
||||
target: '#delete-user-modal',
|
||||
delete_user_url: admin_user_path(user, hard_delete: true),
|
||||
block_user_url: block_admin_user_path(user),
|
||||
username: user.name,
|
||||
delete_contributions: 'true' }, type: 'button' }
|
||||
= s_('AdminUsers|Delete user and contributions')
|
||||
|
|
|
@ -76,3 +76,6 @@
|
|||
= render partial: 'admin/users/user', collection: @users
|
||||
|
||||
= paginate @users, theme: "gitlab"
|
||||
|
||||
#delete-user-modal
|
||||
|
||||
|
|
|
@ -172,13 +172,19 @@
|
|||
|
||||
.panel.panel-danger
|
||||
.panel-heading
|
||||
Remove user
|
||||
= s_('AdminUsers|Delete user')
|
||||
.panel-body
|
||||
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
|
||||
%p Deleting a user has the following effects:
|
||||
= render 'users/deletion_guidance', user: @user
|
||||
%br
|
||||
= link_to 'Remove user', admin_user_path(@user), data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove"
|
||||
%button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
|
||||
target: '#delete-user-modal',
|
||||
delete_user_url: admin_user_path(@user),
|
||||
block_user_url: block_admin_user_path(@user),
|
||||
username: @user.name,
|
||||
delete_contributions: 'false' }, type: 'button' }
|
||||
= s_('AdminUsers|Delete user')
|
||||
- else
|
||||
- if @user.solo_owned_groups.present?
|
||||
%p
|
||||
|
@ -192,7 +198,7 @@
|
|||
|
||||
.panel.panel-danger
|
||||
.panel-heading
|
||||
Remove user and contributions
|
||||
= s_('AdminUsers|Delete user and contributions')
|
||||
.panel-body
|
||||
- if can?(current_user, :destroy_user, @user)
|
||||
%p
|
||||
|
@ -204,7 +210,15 @@
|
|||
the user, and projects in them, will also be removed. Commits
|
||||
to other projects are unaffected.
|
||||
%br
|
||||
= link_to 'Remove user and contributions', admin_user_path(@user, hard_delete: true), data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove"
|
||||
%button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
|
||||
target: '#delete-user-modal',
|
||||
delete_user_url: admin_user_path(@user, hard_delete: true),
|
||||
block_user_url: block_admin_user_path(@user),
|
||||
username: @user.name,
|
||||
delete_contributions: 'true' }, type: 'button' }
|
||||
= s_('AdminUsers|Delete user and contributions')
|
||||
- else
|
||||
%p
|
||||
You don't have access to delete this user.
|
||||
|
||||
#delete-user-modal
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Deleting a User Account
|
||||
|
||||
- As a user, you can delete your own account by navigating to **Settings** > **Account** and selecting **Delete account**
|
||||
- As an admin, you can delete a user account by navigating to the **Admin Area**, selecting the **Users** tab, selecting a user, and clicking on **Remove user**
|
||||
- As an admin, you can delete a user account by navigating to the **Admin Area**, selecting the **Users** tab, selecting a user, and clicking on **Delete user**
|
||||
|
||||
## Associated Records
|
||||
|
||||
|
|
|
@ -26,8 +26,8 @@ describe "Admin::Users" do
|
|||
expect(page).to have_content(user.email)
|
||||
expect(page).to have_content(user.name)
|
||||
expect(page).to have_link('Block', href: block_admin_user_path(user))
|
||||
expect(page).to have_link('Remove user', href: admin_user_path(user))
|
||||
expect(page).to have_link('Remove user and contributions', href: admin_user_path(user, hard_delete: true))
|
||||
expect(page).to have_button('Delete user')
|
||||
expect(page).to have_button('Delete user and contributions')
|
||||
end
|
||||
|
||||
describe 'Two-factor Authentication filters' do
|
||||
|
@ -122,8 +122,8 @@ describe "Admin::Users" do
|
|||
expect(page).to have_content(user.email)
|
||||
expect(page).to have_content(user.name)
|
||||
expect(page).to have_link('Block user', href: block_admin_user_path(user))
|
||||
expect(page).to have_link('Remove user', href: admin_user_path(user))
|
||||
expect(page).to have_link('Remove user and contributions', href: admin_user_path(user, hard_delete: true))
|
||||
expect(page).to have_button('Delete user')
|
||||
expect(page).to have_button('Delete user and contributions')
|
||||
end
|
||||
|
||||
describe 'Impersonation' do
|
||||
|
|
Loading…
Reference in a new issue