diff options
Diffstat (limited to 'app/assets/javascripts/admin')
10 files changed, 124 insertions, 269 deletions
diff --git a/app/assets/javascripts/admin/topics/components/remove_avatar.vue b/app/assets/javascripts/admin/topics/components/remove_avatar.vue index 5e94d6185e0..a54c30a8336 100644 --- a/app/assets/javascripts/admin/topics/components/remove_avatar.vue +++ b/app/assets/javascripts/admin/topics/components/remove_avatar.vue @@ -1,6 +1,6 @@ <script> import { uniqueId } from 'lodash'; -import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; +import { GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui'; import { __ } from '~/locale'; import csrf from '~/lib/utils/csrf'; @@ -8,11 +8,12 @@ export default { components: { GlButton, GlModal, + GlSprintf, }, directives: { GlModal: GlModalDirective, }, - inject: ['path'], + inject: ['path', 'name'], data() { return { modalId: uniqueId('remove-topic-avatar-'), @@ -25,8 +26,8 @@ export default { }, i18n: { remove: __('Remove avatar'), - title: __('Confirm remove avatar'), - body: __('Avatar will be removed. Are you sure?'), + title: __('Remove topic avatar'), + body: __('Topic avatar for %{name} will be removed. This cannot be undone.'), }, modal: { actionPrimary: { @@ -57,7 +58,9 @@ export default { :modal-id="modalId" size="sm" @primary="deleteApplication" - >{{ $options.i18n.body }} + ><gl-sprintf :message="$options.i18n.body" + ><template #name>{{ name }}</template></gl-sprintf + > <form ref="deleteForm" method="post" :action="path"> <input type="hidden" name="_method" value="delete" /> <input type="hidden" name="authenticity_token" :value="$options.csrf.token" /> diff --git a/app/assets/javascripts/admin/topics/index.js b/app/assets/javascripts/admin/topics/index.js index 8fbcadf3369..09e9b20f220 100644 --- a/app/assets/javascripts/admin/topics/index.js +++ b/app/assets/javascripts/admin/topics/index.js @@ -8,12 +8,13 @@ export default () => { return false; } - const { path } = el.dataset; + const { path, name } = el.dataset; return new Vue({ el, provide: { path, + name, }, render(h) { return h(RemoveAvatar); diff --git a/app/assets/javascripts/admin/users/components/actions/delete.vue b/app/assets/javascripts/admin/users/components/actions/delete.vue index e6dde5898e7..ae0c6731271 100644 --- a/app/assets/javascripts/admin/users/components/actions/delete.vue +++ b/app/assets/javascripts/admin/users/components/actions/delete.vue @@ -1,9 +1,11 @@ <script> -import SharedDeleteAction from './shared/shared_delete_action.vue'; +import { GlDropdownItem } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import eventHub, { EVENT_OPEN_DELETE_USER_MODAL } from '../modals/delete_user_modal_event_hub'; export default { components: { - SharedDeleteAction, + GlDropdownItem, }, props: { username: { @@ -20,17 +22,32 @@ export default { default: () => [], }, }, + methods: { + onClick() { + const { username, paths, userDeletionObstacles } = this; + eventHub.$emit(EVENT_OPEN_DELETE_USER_MODAL, { + username, + blockPath: paths.block, + deletePath: paths.delete, + userDeletionObstacles, + i18n: { + title: s__('AdminUsers|Delete User %{username}?'), + primaryButtonLabel: s__('AdminUsers|Delete user'), + messageBody: s__(`AdminUsers|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 %{strongStart}block user%{strongEnd} feature instead. Once you %{strongStart}Delete user%{strongEnd}, + it cannot be undone or recovered.`), + }, + }); + }, + }, }; </script> <template> - <shared-delete-action - modal-type="delete" - :username="username" - :paths="paths" - :delete-path="paths.delete" - :user-deletion-obstacles="userDeletionObstacles" - > - <slot></slot> - </shared-delete-action> + <gl-dropdown-item @click="onClick"> + <span class="gl-text-red-500"> + <slot></slot> + </span> + </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue index bd920a91516..a39df1cbfb6 100644 --- a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue +++ b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue @@ -1,9 +1,11 @@ <script> -import SharedDeleteAction from './shared/shared_delete_action.vue'; +import { GlDropdownItem } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import eventHub, { EVENT_OPEN_DELETE_USER_MODAL } from '../modals/delete_user_modal_event_hub'; export default { components: { - SharedDeleteAction, + GlDropdownItem, }, props: { username: { @@ -20,17 +22,32 @@ export default { default: () => [], }, }, + methods: { + onClick() { + const { username, paths, userDeletionObstacles } = this; + eventHub.$emit(EVENT_OPEN_DELETE_USER_MODAL, { + username, + blockPath: paths.block, + deletePath: paths.deleteWithContributions, + userDeletionObstacles, + i18n: { + title: s__('AdminUsers|Delete User %{username} and contributions?'), + primaryButtonLabel: s__('AdminUsers|Delete user and contributions'), + messageBody: s__(`AdminUsers|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 %{strongStart}block user%{strongEnd} feature instead. Once you %{strongStart}Delete user%{strongEnd}, + it cannot be undone or recovered.`), + }, + }); + }, + }, }; </script> <template> - <shared-delete-action - modal-type="delete-with-contributions" - :username="username" - :paths="paths" - :delete-path="paths.deleteWithContributions" - :user-deletion-obstacles="userDeletionObstacles" - > - <slot></slot> - </shared-delete-action> + <gl-dropdown-item @click="onClick"> + <span class="gl-text-red-500"> + <slot></slot> + </span> + </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue b/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue deleted file mode 100644 index c9f29b55dbf..00000000000 --- a/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue +++ /dev/null @@ -1,52 +0,0 @@ -<script> -import { GlDropdownItem } from '@gitlab/ui'; - -export default { - components: { - GlDropdownItem, - }, - props: { - username: { - type: String, - required: true, - }, - paths: { - type: Object, - required: true, - }, - deletePath: { - type: String, - required: true, - }, - modalType: { - type: String, - required: true, - }, - userDeletionObstacles: { - type: Array, - required: true, - }, - }, - computed: { - modalAttributes() { - return { - 'data-block-user-url': this.paths.block, - 'data-delete-user-url': this.deletePath, - 'data-gl-modal-action': this.modalType, - 'data-username': this.username, - 'data-user-deletion-obstacles': JSON.stringify(this.userDeletionObstacles), - }; - }, - }, -}; -</script> - -<template> - <div class="js-delete-user-modal-button" v-bind="{ ...modalAttributes }"> - <gl-dropdown-item> - <span class="gl-text-red-500"> - <slot></slot> - </span> - </gl-dropdown-item> - </div> -</template> diff --git a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue index d7c08096376..31fe86775ee 100644 --- a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue +++ b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue @@ -1,8 +1,8 @@ <script> import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; import { s__, sprintf } from '~/locale'; import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue'; +import eventHub, { EVENT_OPEN_DELETE_USER_MODAL } from './delete_user_modal_event_hub'; export default { components: { @@ -13,47 +13,23 @@ export default { UserDeletionObstaclesList, }, props: { - title: { - type: String, - required: true, - }, - content: { - type: String, - required: true, - }, - action: { - type: String, - required: true, - }, - secondaryAction: { - type: String, - required: true, - }, - deleteUserUrl: { - type: String, - required: true, - }, - blockUserUrl: { - type: String, - required: true, - }, - username: { - type: String, - required: true, - }, csrfToken: { type: String, required: true, }, - userDeletionObstacles: { - type: String, - required: false, - default: '[]', - }, }, data() { return { enteredUsername: '', + username: '', + blockPath: '', + deletePath: '', + userDeletionObstacles: [], + i18n: { + title: '', + primaryButtonLabel: '', + messageBody: '', + }, }; }, computed: { @@ -61,75 +37,80 @@ export default { return this.username.trim(); }, modalTitle() { - return sprintf(this.title, { username: this.trimmedUsername }, false); - }, - secondaryButtonLabel() { - return s__('AdminUsers|Block user'); + return sprintf(this.i18n.title, { username: this.trimmedUsername }, false); }, canSubmit() { - return this.enteredUsername === this.trimmedUsername; + return this.enteredUsername && this.enteredUsername === this.trimmedUsername; }, - obstacles() { - try { - return JSON.parse(this.userDeletionObstacles); - } catch (e) { - Sentry.captureException(e); - } - return []; + secondaryButtonLabel() { + return s__('AdminUsers|Block user'); }, }, + mounted() { + eventHub.$on(EVENT_OPEN_DELETE_USER_MODAL, this.onOpenEvent); + }, + destroyed() { + eventHub.$off(EVENT_OPEN_DELETE_USER_MODAL, this.onOpenEvent); + }, methods: { - show() { + onOpenEvent({ username, blockPath, deletePath, userDeletionObstacles, i18n }) { + this.username = username; + this.blockPath = blockPath; + this.deletePath = deletePath; + this.userDeletionObstacles = userDeletionObstacles; + this.i18n = i18n; + this.openModal(); + }, + openModal() { this.$refs.modal.show(); }, + onSubmit() { + this.$refs.form.submit(); + this.enteredUsername = ''; + }, onCancel() { this.enteredUsername = ''; this.$refs.modal.hide(); }, onSecondaryAction() { const { form } = this.$refs; - - form.action = this.blockUserUrl; + form.action = this.blockPath; this.$refs.method.value = 'put'; - form.submit(); }, - onSubmit() { - this.$refs.form.submit(); - this.enteredUsername = ''; - }, }, }; </script> - <template> <gl-modal ref="modal" modal-id="delete-user-modal" :title="modalTitle" kind="danger"> <p> - <gl-sprintf :message="content"> + <gl-sprintf :message="i18n.messageBody"> <template #username> - <strong>{{ trimmedUsername }}</strong> + <strong data-testid="message-username">{{ trimmedUsername }}</strong> </template> - <template #strong="props"> - <strong>{{ props.content }}</strong> + <template #strong="{ content }"> + <strong>{{ content }}</strong> </template> </gl-sprintf> </p> <user-deletion-obstacles-list - v-if="obstacles.length" - :obstacles="obstacles" + v-if="userDeletionObstacles.length" + :obstacles="userDeletionObstacles" :user-name="trimmedUsername" /> <p> <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')"> <template #username> - <code class="gl-white-space-pre-wrap">{{ trimmedUsername }}</code> + <code data-testid="confirm-username" class="gl-white-space-pre-wrap">{{ + trimmedUsername + }}</code> </template> </gl-sprintf> </p> - <form ref="form" :action="deleteUserUrl" method="post" @submit.prevent> + <form ref="form" :action="deletePath" method="post" @submit.prevent> <input ref="method" type="hidden" name="_method" value="delete" /> <input :value="csrfToken" type="hidden" name="authenticity_token" /> <gl-form-input @@ -140,6 +121,7 @@ export default { autocomplete="off" /> </form> + <template #modal-footer> <gl-button @click="onCancel">{{ __('Cancel') }}</gl-button> <gl-button @@ -148,10 +130,10 @@ export default { variant="danger" @click="onSecondaryAction" > - {{ secondaryAction }} + {{ secondaryButtonLabel }} </gl-button> <gl-button :disabled="!canSubmit" category="primary" variant="danger" @click="onSubmit">{{ - action + i18n.primaryButtonLabel }}</gl-button> </template> </gl-modal> diff --git a/app/assets/javascripts/admin/users/components/modals/delete_user_modal_event_hub.js b/app/assets/javascripts/admin/users/components/modals/delete_user_modal_event_hub.js new file mode 100644 index 00000000000..001061dcc6b --- /dev/null +++ b/app/assets/javascripts/admin/users/components/modals/delete_user_modal_event_hub.js @@ -0,0 +1,5 @@ +import createEventHub from '~/helpers/event_hub_factory'; + +export default createEventHub(); + +export const EVENT_OPEN_DELETE_USER_MODAL = Symbol('OPEN'); diff --git a/app/assets/javascripts/admin/users/components/modals/user_modal_manager.vue b/app/assets/javascripts/admin/users/components/modals/user_modal_manager.vue deleted file mode 100644 index 1dfea3f1e7b..00000000000 --- a/app/assets/javascripts/admin/users/components/modals/user_modal_manager.vue +++ /dev/null @@ -1,77 +0,0 @@ -<script> -import DeleteUserModal from './delete_user_modal.vue'; - -export default { - components: { DeleteUserModal }, - props: { - modalConfiguration: { - required: true, - type: Object, - }, - csrfToken: { - required: true, - type: String, - }, - selector: { - required: true, - type: String, - }, - }, - data() { - return { - currentModalData: null, - }; - }, - computed: { - activeModal() { - return Boolean(this.currentModalData); - }, - - modalProps() { - const { glModalAction: requestedAction } = this.currentModalData; - return { - ...this.modalConfiguration[requestedAction], - ...this.currentModalData, - csrfToken: this.csrfToken, - }; - }, - }, - - mounted() { - /* - * Here we're looking for every button that needs to launch a modal - * on click, and then attaching a click event handler to show the modal - * if it's correctly configured. - * - * TODO: Replace this with integrated modal components https://gitlab.com/gitlab-org/gitlab/-/issues/320922 - */ - document.querySelectorAll(this.selector).forEach((button) => { - button.addEventListener('click', (e) => { - if (!button.dataset.glModalAction) return; - - e.preventDefault(); - this.show(button.dataset); - }); - }); - }, - - methods: { - show(modalData) { - const { glModalAction: requestedAction } = modalData; - - if (!this.modalConfiguration[requestedAction]) { - throw new Error(`Modal action ${requestedAction} has no configuration in HTML`); - } - - this.currentModalData = modalData; - - return this.$nextTick().then(() => { - this.$refs.modal.show(); - }); - }, - }, -}; -</script> -<template> - <delete-user-modal v-if="activeModal" ref="modal" v-bind="modalProps" /> -</template> diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js index 4636c8705a5..9cd61d6b1db 100644 --- a/app/assets/javascripts/admin/users/constants.js +++ b/app/assets/javascripts/admin/users/constants.js @@ -20,9 +20,3 @@ export const I18N_USER_ACTIONS = { ban: s__('AdminUsers|Ban user'), unban: s__('AdminUsers|Unban user'), }; - -export const CONFIRM_DELETE_BUTTON_SELECTOR = '.js-delete-user-modal-button'; - -export const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts'; - -export const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal'; diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js index 0c485d2a239..2bd37d3fffe 100644 --- a/app/assets/javascripts/admin/users/index.js +++ b/app/assets/javascripts/admin/users/index.js @@ -4,13 +4,8 @@ import createDefaultClient from '~/lib/graphql'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import csrf from '~/lib/utils/csrf'; import AdminUsersApp from './components/app.vue'; -import ModalManager from './components/modals/user_modal_manager.vue'; +import DeleteUserModal from './components/modals/delete_user_modal.vue'; import UserActions from './components/user_actions.vue'; -import { - CONFIRM_DELETE_BUTTON_SELECTOR, - MODAL_TEXTS_CONTAINER_SELECTOR, - MODAL_MANAGER_SELECTOR, -} from './constants'; Vue.use(VueApollo); @@ -46,43 +41,13 @@ export const initAdminUserActions = (el = document.querySelector('#js-admin-user initApp(el, UserActions, 'user', { showButtonLabels: true }); export const initDeleteUserModals = () => { - const modalsMountElement = document.querySelector(MODAL_TEXTS_CONTAINER_SELECTOR); - - if (!modalsMountElement) { - return; - } - - const modalConfiguration = Array.from(modalsMountElement.children).reduce((accumulator, node) => { - const { modal, ...config } = node.dataset; - - return { - ...accumulator, - [modal]: { - title: node.dataset.title, - ...config, - content: node.innerHTML, - }, - }; - }, {}); - - // eslint-disable-next-line no-new - new Vue({ - el: MODAL_MANAGER_SELECTOR, + return new Vue({ functional: true, - methods: { - show(...args) { - this.$refs.manager.show(...args); - }, - }, - render(h) { - return h(ModalManager, { - ref: 'manager', + render: (createElement) => + createElement(DeleteUserModal, { props: { - selector: CONFIRM_DELETE_BUTTON_SELECTOR, - modalConfiguration, csrfToken: csrf.token, }, - }); - }, - }); + }), + }).$mount(); }; |