Resolve "New design for user deletion confirmation in admin area"

This commit is contained in:
Shah El-Rahman 2018-02-08 05:17:12 +00:00 committed by Clement Ho
parent 6d7df4f8b1
commit 3cb19dd42c
9 changed files with 288 additions and 17 deletions

View file

@ -114,6 +114,16 @@ var Dispatcher;
.then(callDefault) .then(callDefault)
.catch(fail); .catch(fail);
break; 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:index':
case 'dashboard:projects:starred': case 'dashboard:projects:starred':
import('./pages/dashboard/projects') import('./pages/dashboard/projects')

View file

@ -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>

View 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;
}
});
};

View file

@ -46,6 +46,11 @@
required: false, required: false,
default: '', default: '',
}, },
secondaryButtonLabel: {
type: String,
required: false,
default: '',
},
submitDisabled: { submitDisabled: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -129,6 +134,21 @@
> >
{{ closeButtonLabel }} {{ closeButtonLabel }}
</button> </button>
<slot
v-if="secondaryButtonLabel"
name="secondary-button"
>
<button
v-if="secondaryButtonLabel"
type="button"
class="btn"
data-dismiss="modal"
>
{{ secondaryButtonLabel }}
</button>
</slot>
<button <button
v-if="primaryButtonLabel" v-if="primaryButtonLabel"
type="button" type="button"

View file

@ -38,12 +38,19 @@
%li.divider %li.divider
- if user.can_be_removed? - if user.can_be_removed?
%li %li
= link_to 'Remove user', admin_user_path(user), %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, target: '#delete-user-modal',
class: 'text-danger', delete_user_url: admin_user_path(user),
method: :delete block_user_url: block_admin_user_path(user),
username: user.name,
delete_contributions: 'false' }, type: 'button' }
= s_('AdminUsers|Delete user')
%li %li
= link_to 'Remove user and contributions', admin_user_path(user, hard_delete: true), %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
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?" }, target: '#delete-user-modal',
class: 'text-danger', delete_user_url: admin_user_path(user, hard_delete: true),
method: :delete block_user_url: block_admin_user_path(user),
username: user.name,
delete_contributions: 'true' }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')

View file

@ -76,3 +76,6 @@
= render partial: 'admin/users/user', collection: @users = render partial: 'admin/users/user', collection: @users
= paginate @users, theme: "gitlab" = paginate @users, theme: "gitlab"
#delete-user-modal

View file

@ -172,13 +172,19 @@
.panel.panel-danger .panel.panel-danger
.panel-heading .panel-heading
Remove user = s_('AdminUsers|Delete user')
.panel-body .panel-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects: %p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user = render 'users/deletion_guidance', user: @user
%br %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 - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
%p %p
@ -192,7 +198,7 @@
.panel.panel-danger .panel.panel-danger
.panel-heading .panel-heading
Remove user and contributions = s_('AdminUsers|Delete user and contributions')
.panel-body .panel-body
- if can?(current_user, :destroy_user, @user) - if can?(current_user, :destroy_user, @user)
%p %p
@ -204,7 +210,15 @@
the user, and projects in them, will also be removed. Commits the user, and projects in them, will also be removed. Commits
to other projects are unaffected. to other projects are unaffected.
%br %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 - else
%p %p
You don't have access to delete this user. You don't have access to delete this user.
#delete-user-modal

View file

@ -1,7 +1,7 @@
# Deleting a User Account # Deleting a User Account
- As a user, you can delete your own account by navigating to **Settings** > **Account** and selecting **Delete 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 ## Associated Records

View file

@ -26,8 +26,8 @@ describe "Admin::Users" do
expect(page).to have_content(user.email) expect(page).to have_content(user.email)
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(page).to have_link('Block', href: block_admin_user_path(user)) 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_button('Delete 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 and contributions')
end end
describe 'Two-factor Authentication filters' do 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.email)
expect(page).to have_content(user.name) 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('Block user', href: block_admin_user_path(user))
expect(page).to have_link('Remove user', href: admin_user_path(user)) expect(page).to have_button('Delete 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 and contributions')
end end
describe 'Impersonation' do describe 'Impersonation' do