Resolve "New design for user deletion confirmation in admin area"
This commit is contained in:
parent
6d7df4f8b1
commit
3cb19dd42c
9 changed files with 288 additions and 17 deletions
|
@ -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')
|
||||||
|
|
|
@ -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,
|
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"
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue