diff options
Diffstat (limited to 'app/assets/javascripts/admin/users')
15 files changed, 662 insertions, 89 deletions
diff --git a/app/assets/javascripts/admin/users/components/actions/activate.vue b/app/assets/javascripts/admin/users/components/actions/activate.vue index 99c260bf11e..74e9c60a57b 100644 --- a/app/assets/javascripts/admin/users/components/actions/activate.vue +++ b/app/assets/javascripts/admin/users/components/actions/activate.vue @@ -1,6 +1,16 @@ <script> import { GlDropdownItem } from '@gitlab/ui'; -import { sprintf, s__ } from '~/locale'; +import { sprintf, s__, __ } from '~/locale'; +import { I18N_USER_ACTIONS } from '../../constants'; + +// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 +const messageHtml = ` + <p>${s__('AdminUsers|Reactivating a user will:')}</p> + <ul> + <li>${s__('AdminUsers|Restore user access to the account, including web, Git and API.')}</li> + </ul> + <p>${s__('AdminUsers|You can always deactivate their account again if needed.')}</p> +`; export default { components: { @@ -25,9 +35,14 @@ export default { title: sprintf(s__('AdminUsers|Activate user %{username}?'), { username: this.username, }), - message: s__('AdminUsers|You can always deactivate their account again if needed.'), - okVariant: 'confirm', - okTitle: s__('AdminUsers|Activate'), + messageHtml, + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.activate, + attributes: [{ variant: 'confirm' }], + }, }), }; }, @@ -36,9 +51,7 @@ export default { </script> <template> - <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> - <gl-dropdown-item> - <slot></slot> - </gl-dropdown-item> - </div> + <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> + <slot></slot> + </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/admin/users/components/actions/approve.vue b/app/assets/javascripts/admin/users/components/actions/approve.vue index 6fc43c246ea..77a9be8eec2 100644 --- a/app/assets/javascripts/admin/users/components/actions/approve.vue +++ b/app/assets/javascripts/admin/users/components/actions/approve.vue @@ -1,21 +1,60 @@ <script> import { GlDropdownItem } from '@gitlab/ui'; +import { sprintf, s__, __ } from '~/locale'; +import { I18N_USER_ACTIONS } from '../../constants'; + +// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 +const messageHtml = ` + <p>${s__('AdminUsers|Approved users can:')}</p> + <ul> + <li>${s__('AdminUsers|Log in')}</li> + <li>${s__('AdminUsers|Access Git repositories')}</li> + <li>${s__('AdminUsers|Access the API')}</li> + <li>${s__('AdminUsers|Be added to groups and projects')}</li> + </ul> +`; export default { components: { GlDropdownItem, }, props: { + username: { + type: String, + required: true, + }, path: { type: String, required: true, }, }, + computed: { + attributes() { + return { + 'data-path': this.path, + 'data-method': 'put', + 'data-modal-attributes': JSON.stringify({ + title: sprintf(s__('AdminUsers|Approve user %{username}?'), { + username: this.username, + }), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.approve, + attributes: [{ variant: 'confirm', 'data-qa-selector': 'approve_user_confirm_button' }], + }, + messageHtml, + }), + 'data-qa-selector': 'approve_user_button', + }; + }, + }, }; </script> <template> - <gl-dropdown-item :href="path" data-method="put"> + <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...attributes }"> <slot></slot> </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/admin/users/components/actions/ban.vue b/app/assets/javascripts/admin/users/components/actions/ban.vue new file mode 100644 index 00000000000..4e9cefbfdd7 --- /dev/null +++ b/app/assets/javascripts/admin/users/components/actions/ban.vue @@ -0,0 +1,69 @@ +<script> +import { GlDropdownItem } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { sprintf, s__, __ } from '~/locale'; +import { I18N_USER_ACTIONS } from '../../constants'; + +// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 +const messageHtml = ` + <p>${s__('AdminUsers|When banned, users:')}</p> + <ul> + <li>${s__("AdminUsers|Can't log in.")}</li> + <li>${s__("AdminUsers|Can't access Git repositories.")}</li> + </ul> + <p>${s__('AdminUsers|You can unban their account in the future. Their data remains intact.')}</p> + <p>${sprintf( + s__('AdminUsers|Learn more about %{link_start}banned users.%{link_end}'), + { + link_start: `<a href="${helpPagePath('user/admin_area/moderate_users', { + anchor: 'ban-a-user', + })}" target="_blank">`, + link_end: '</a>', + }, + false, + )}</p> +`; + +export default { + components: { + GlDropdownItem, + }, + props: { + username: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + }, + computed: { + modalAttributes() { + return { + 'data-path': this.path, + 'data-method': 'put', + 'data-modal-attributes': JSON.stringify({ + title: sprintf(s__('AdminUsers|Ban user %{username}?'), { + username: this.username, + }), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.ban, + attributes: [{ variant: 'confirm' }], + }, + messageHtml, + }), + }; + }, + }, +}; +</script> + +<template> + <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> + <slot></slot> + </gl-dropdown-item> +</template> diff --git a/app/assets/javascripts/admin/users/components/actions/block.vue b/app/assets/javascripts/admin/users/components/actions/block.vue index 68dfefe14c2..03557008a89 100644 --- a/app/assets/javascripts/admin/users/components/actions/block.vue +++ b/app/assets/javascripts/admin/users/components/actions/block.vue @@ -1,6 +1,7 @@ <script> import { GlDropdownItem } from '@gitlab/ui'; -import { sprintf, s__ } from '~/locale'; +import { sprintf, s__, __ } from '~/locale'; +import { I18N_USER_ACTIONS } from '../../constants'; // TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 const messageHtml = ` @@ -11,6 +12,7 @@ const messageHtml = ` <li>${s__('AdminUsers|Personal projects will be left')}</li> <li>${s__('AdminUsers|Owned groups will be left')}</li> </ul> + <p>${s__('AdminUsers|You can always unblock their account, their data will remain intact.')}</p> `; export default { @@ -34,8 +36,13 @@ export default { 'data-method': 'put', 'data-modal-attributes': JSON.stringify({ title: sprintf(s__('AdminUsers|Block user %{username}?'), { username: this.username }), - okVariant: 'confirm', - okTitle: s__('AdminUsers|Block'), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.block, + attributes: [{ variant: 'confirm' }], + }, messageHtml, }), }; @@ -45,9 +52,7 @@ export default { </script> <template> - <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> - <gl-dropdown-item> - <slot></slot> - </gl-dropdown-item> - </div> + <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> + <slot></slot> + </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/admin/users/components/actions/deactivate.vue b/app/assets/javascripts/admin/users/components/actions/deactivate.vue index 7e0c17ba296..640c8fefc20 100644 --- a/app/assets/javascripts/admin/users/components/actions/deactivate.vue +++ b/app/assets/javascripts/admin/users/components/actions/deactivate.vue @@ -1,6 +1,7 @@ <script> import { GlDropdownItem } from '@gitlab/ui'; -import { sprintf, s__ } from '~/locale'; +import { sprintf, s__, __ } from '~/locale'; +import { I18N_USER_ACTIONS } from '../../constants'; // TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 const messageHtml = ` @@ -16,6 +17,9 @@ const messageHtml = ` )}</li> <li>${s__('AdminUsers|Personal projects, group and user history will be left intact')}</li> </ul> + <p>${s__( + 'AdminUsers|You can always re-activate their account, their data will remain intact.', + )}</p> `; export default { @@ -41,8 +45,13 @@ export default { title: sprintf(s__('AdminUsers|Deactivate user %{username}?'), { username: this.username, }), - okVariant: 'confirm', - okTitle: s__('AdminUsers|Deactivate'), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.deactivate, + attributes: [{ variant: 'confirm' }], + }, messageHtml, }), }; @@ -52,9 +61,7 @@ export default { </script> <template> - <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> - <gl-dropdown-item> - <slot></slot> - </gl-dropdown-item> - </div> + <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> + <slot></slot> + </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/admin/users/components/actions/index.js b/app/assets/javascripts/admin/users/components/actions/index.js index e34b01346b9..4e63a85df89 100644 --- a/app/assets/javascripts/admin/users/components/actions/index.js +++ b/app/assets/javascripts/admin/users/components/actions/index.js @@ -1,20 +1,24 @@ import Activate from './activate.vue'; import Approve from './approve.vue'; +import Ban from './ban.vue'; import Block from './block.vue'; import Deactivate from './deactivate.vue'; import Delete from './delete.vue'; import DeleteWithContributions from './delete_with_contributions.vue'; import Reject from './reject.vue'; +import Unban from './unban.vue'; import Unblock from './unblock.vue'; import Unlock from './unlock.vue'; export default { Activate, Approve, + Ban, Block, Deactivate, Delete, DeleteWithContributions, + Unban, Unblock, Unlock, Reject, diff --git a/app/assets/javascripts/admin/users/components/actions/reject.vue b/app/assets/javascripts/admin/users/components/actions/reject.vue index a80c1ff5458..901306455fa 100644 --- a/app/assets/javascripts/admin/users/components/actions/reject.vue +++ b/app/assets/javascripts/admin/users/components/actions/reject.vue @@ -1,21 +1,70 @@ <script> import { GlDropdownItem } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { sprintf, s__, __ } from '~/locale'; +import { I18N_USER_ACTIONS } from '../../constants'; + +// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 +const messageHtml = ` + <p>${s__('AdminUsers|Rejected users:')}</p> + <ul> + <li>${s__('AdminUsers|Cannot sign in or access instance information')}</li> + <li>${s__('AdminUsers|Will be deleted')}</li> + </ul> + <p>${sprintf( + s__( + 'AdminUsers|For more information, please refer to the %{link_start}user account deletion documentation.%{link_end}', + ), + { + link_start: `<a href="${helpPagePath('user/profile/account/delete_account', { + anchor: 'associated-records', + })}" target="_blank">`, + link_end: '</a>', + }, + false, + )}</p> +`; export default { components: { GlDropdownItem, }, props: { + username: { + type: String, + required: true, + }, path: { type: String, required: true, }, }, + computed: { + modalAttributes() { + return { + 'data-path': this.path, + 'data-method': 'delete', + 'data-modal-attributes': JSON.stringify({ + title: sprintf(s__('AdminUsers|Reject user %{username}?'), { + username: this.username, + }), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.reject, + attributes: [{ variant: 'danger' }], + }, + messageHtml, + }), + }; + }, + }, }; </script> <template> - <gl-dropdown-item :href="path" data-method="delete"> + <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> <slot></slot> </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/admin/users/components/actions/unban.vue b/app/assets/javascripts/admin/users/components/actions/unban.vue new file mode 100644 index 00000000000..8083e26177e --- /dev/null +++ b/app/assets/javascripts/admin/users/components/actions/unban.vue @@ -0,0 +1,53 @@ +<script> +import { GlDropdownItem } from '@gitlab/ui'; +import { sprintf, s__, __ } from '~/locale'; +import { I18N_USER_ACTIONS } from '../../constants'; + +// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 +const messageHtml = `<p>${s__( + 'AdminUsers|You can ban their account in the future if necessary.', +)}</p>`; + +export default { + components: { + GlDropdownItem, + }, + props: { + username: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + }, + computed: { + modalAttributes() { + return { + 'data-path': this.path, + 'data-method': 'put', + 'data-modal-attributes': JSON.stringify({ + title: sprintf(s__('AdminUsers|Unban user %{username}?'), { + username: this.username, + }), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.unban, + attributes: [{ variant: 'confirm' }], + }, + messageHtml, + }), + }; + }, + }, +}; +</script> + +<template> + <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> + <slot></slot> + </gl-dropdown-item> +</template> diff --git a/app/assets/javascripts/admin/users/components/actions/unblock.vue b/app/assets/javascripts/admin/users/components/actions/unblock.vue index d4c0f900c94..7de6653e0cd 100644 --- a/app/assets/javascripts/admin/users/components/actions/unblock.vue +++ b/app/assets/javascripts/admin/users/components/actions/unblock.vue @@ -1,6 +1,7 @@ <script> import { GlDropdownItem } from '@gitlab/ui'; -import { sprintf, s__ } from '~/locale'; +import { sprintf, s__, __ } from '~/locale'; +import { I18N_USER_ACTIONS } from '../../constants'; export default { components: { @@ -24,8 +25,13 @@ export default { 'data-modal-attributes': JSON.stringify({ title: sprintf(s__('AdminUsers|Unblock user %{username}?'), { username: this.username }), message: s__('AdminUsers|You can always block their account again if needed.'), - okVariant: 'confirm', - okTitle: s__('AdminUsers|Unblock'), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.unblock, + attributes: [{ variant: 'confirm' }], + }, }), }; }, @@ -34,9 +40,7 @@ export default { </script> <template> - <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> - <gl-dropdown-item> - <slot></slot> - </gl-dropdown-item> - </div> + <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> + <slot></slot> + </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/admin/users/components/actions/unlock.vue b/app/assets/javascripts/admin/users/components/actions/unlock.vue index 294aaade7c1..10d4fb06d61 100644 --- a/app/assets/javascripts/admin/users/components/actions/unlock.vue +++ b/app/assets/javascripts/admin/users/components/actions/unlock.vue @@ -1,6 +1,7 @@ <script> import { GlDropdownItem } from '@gitlab/ui'; import { sprintf, s__, __ } from '~/locale'; +import { I18N_USER_ACTIONS } from '../../constants'; export default { components: { @@ -24,8 +25,13 @@ export default { 'data-modal-attributes': JSON.stringify({ title: sprintf(s__('AdminUsers|Unlock user %{username}?'), { username: this.username }), message: __('Are you sure?'), - okVariant: 'confirm', - okTitle: s__('AdminUsers|Unlock'), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.unlock, + attributes: [{ variant: 'confirm' }], + }, }), }; }, @@ -34,9 +40,7 @@ export default { </script> <template> - <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> - <gl-dropdown-item> - <slot></slot> - </gl-dropdown-item> - </div> + <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }"> + <slot></slot> + </gl-dropdown-item> </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 new file mode 100644 index 00000000000..413163c8536 --- /dev/null +++ b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue @@ -0,0 +1,151 @@ +<script> +import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { s__, sprintf } from '~/locale'; +import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; + +export default { + components: { + GlModal, + GlButton, + GlFormInput, + GlSprintf, + OncallSchedulesList, + }, + 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, + }, + oncallSchedules: { + type: String, + required: false, + default: '[]', + }, + }, + data() { + return { + enteredUsername: '', + }; + }, + computed: { + modalTitle() { + return sprintf(this.title, { username: this.username }, false); + }, + secondaryButtonLabel() { + return s__('AdminUsers|Block user'); + }, + canSubmit() { + return this.enteredUsername === this.username; + }, + schedules() { + try { + return JSON.parse(this.oncallSchedules); + } catch (e) { + Sentry.captureException(e); + } + return []; + }, + }, + methods: { + show() { + this.$refs.modal.show(); + }, + onCancel() { + this.enteredUsername = ''; + this.$refs.modal.hide(); + }, + onSecondaryAction() { + const { form } = this.$refs; + + form.action = this.blockUserUrl; + 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"> + <template #username> + <strong>{{ username }}</strong> + </template> + <template #strong="props"> + <strong>{{ props.content }}</strong> + </template> + </gl-sprintf> + </p> + + <oncall-schedules-list v-if="schedules.length" :schedules="schedules" :user-name="username" /> + + <p> + <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')"> + <template #username> + <code>{{ username }}</code> + </template> + </gl-sprintf> + </p> + + <form ref="form" :action="deleteUserUrl" method="post" @submit.prevent> + <input ref="method" type="hidden" name="_method" value="delete" /> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> + <gl-form-input + v-model="enteredUsername" + autofocus + type="text" + name="username" + autocomplete="off" + /> + </form> + <template #modal-footer> + <gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button> + <gl-button + :disabled="!canSubmit" + category="secondary" + variant="danger" + @click="onSecondaryAction" + > + {{ secondaryAction }} + </gl-button> + <gl-button :disabled="!canSubmit" category="primary" variant="danger" @click="onSubmit">{{ + action + }}</gl-button> + </template> + </gl-modal> +</template> 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 new file mode 100644 index 00000000000..1dfea3f1e7b --- /dev/null +++ b/app/assets/javascripts/admin/users/components/modals/user_modal_manager.vue @@ -0,0 +1,77 @@ +<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/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue index b782526e6be..c076e0bedf0 100644 --- a/app/assets/javascripts/admin/users/components/user_actions.vue +++ b/app/assets/javascripts/admin/users/components/user_actions.vue @@ -5,6 +5,7 @@ import { GlDropdownItem, GlDropdownSectionHeader, GlDropdownDivider, + GlTooltipDirective, } from '@gitlab/ui'; import { convertArrayToCamelCase } from '~/lib/utils/common_utils'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; @@ -21,6 +22,9 @@ export default { GlDropdownDivider, ...Actions, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { user: { type: Object, @@ -30,6 +34,11 @@ export default { type: Object, required: true, }, + showButtonLabels: { + type: Boolean, + required: false, + default: false, + }, }, computed: { userActions() { @@ -56,6 +65,13 @@ export default { userPaths() { return generateUserPaths(this.paths, this.user.username); }, + editButtonAttrs() { + return { + 'data-testid': 'edit', + icon: 'pencil-square', + href: this.userPaths.edit, + }; + }, }, methods: { isLdapAction(action) { @@ -70,51 +86,68 @@ export default { </script> <template> - <div class="gl-display-flex gl-justify-content-end" :data-testid="`user-actions-${user.id}`"> - <gl-button v-if="hasEditAction" data-testid="edit" :href="userPaths.edit">{{ - $options.i18n.edit - }}</gl-button> + <div + class="gl-display-flex gl-justify-content-end gl-my-n2 gl-mx-n2" + :data-testid="`user-actions-${user.id}`" + > + <div v-if="hasEditAction" class="gl-p-2"> + <gl-button v-if="showButtonLabels" v-bind="editButtonAttrs">{{ + $options.i18n.edit + }}</gl-button> + <gl-button + v-else + v-gl-tooltip="$options.i18n.edit" + v-bind="editButtonAttrs" + :aria-label="$options.i18n.edit" + /> + </div> - <gl-dropdown - v-if="hasDropdownActions" - data-testid="dropdown-toggle" - right - class="gl-ml-2" - icon="settings" - > - <gl-dropdown-section-header>{{ $options.i18n.settings }}</gl-dropdown-section-header> + <div v-if="hasDropdownActions" class="gl-p-2"> + <gl-dropdown + data-testid="dropdown-toggle" + right + :text="$options.i18n.userAdministration" + :text-sr-only="!showButtonLabels" + icon="settings" + data-qa-selector="user_actions_dropdown_toggle" + :data-qa-username="user.username" + > + <gl-dropdown-section-header>{{ + $options.i18n.userAdministration + }}</gl-dropdown-section-header> - <template v-for="action in dropdownSafeActions"> - <component - :is="getActionComponent(action)" - v-if="getActionComponent(action)" - :key="action" - :path="userPaths[action]" - :username="user.name" - :data-testid="action" - > - {{ $options.i18n[action] }} - </component> - <gl-dropdown-item v-else-if="isLdapAction(action)" :key="action" :data-testid="action"> - {{ $options.i18n[action] }} - </gl-dropdown-item> - </template> + <template v-for="action in dropdownSafeActions"> + <component + :is="getActionComponent(action)" + v-if="getActionComponent(action)" + :key="action" + :path="userPaths[action]" + :username="user.name" + :data-testid="action" + > + {{ $options.i18n[action] }} + </component> + <gl-dropdown-item v-else-if="isLdapAction(action)" :key="action" :data-testid="action"> + {{ $options.i18n[action] }} + </gl-dropdown-item> + </template> - <gl-dropdown-divider v-if="hasDeleteActions" /> + <gl-dropdown-divider v-if="hasDeleteActions" /> - <template v-for="action in dropdownDeleteActions"> - <component - :is="getActionComponent(action)" - v-if="getActionComponent(action)" - :key="action" - :paths="userPaths" - :username="user.name" - :oncall-schedules="user.oncallSchedules" - :data-testid="`delete-${action}`" - > - {{ $options.i18n[action] }} - </component> - </template> - </gl-dropdown> + <template v-for="action in dropdownDeleteActions"> + <component + :is="getActionComponent(action)" + v-if="getActionComponent(action)" + :key="action" + :paths="userPaths" + :username="user.name" + :oncall-schedules="user.oncallSchedules" + :data-testid="`delete-${action}`" + > + {{ $options.i18n[action] }} + </component> + </template> + </gl-dropdown> + </div> </div> </template> diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js index c55edefe607..4636c8705a5 100644 --- a/app/assets/javascripts/admin/users/constants.js +++ b/app/assets/javascripts/admin/users/constants.js @@ -6,7 +6,7 @@ export const LENGTH_OF_USER_NOTE_TOOLTIP = 100; export const I18N_USER_ACTIONS = { edit: __('Edit'), - settings: __('Settings'), + userAdministration: s__('AdminUsers|User administration'), unlock: __('Unlock'), block: s__('AdminUsers|Block'), unblock: s__('AdminUsers|Unblock'), @@ -17,4 +17,12 @@ export const I18N_USER_ACTIONS = { ldapBlocked: s__('AdminUsers|Cannot unblock LDAP blocked users'), delete: s__('AdminUsers|Delete user'), deleteWithContributions: s__('AdminUsers|Delete user and contributions'), + 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 54c8edc080b..852b253d25a 100644 --- a/app/assets/javascripts/admin/users/index.js +++ b/app/assets/javascripts/admin/users/index.js @@ -2,7 +2,15 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; 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 UserActions from './components/user_actions.vue'; +import { + CONFIRM_DELETE_BUTTON_SELECTOR, + MODAL_TEXTS_CONTAINER_SELECTOR, + MODAL_MANAGER_SELECTOR, +} from './constants'; Vue.use(VueApollo); @@ -10,22 +18,71 @@ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }), }); -export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => { +const initApp = (el, component, userPropKey, props = {}) => { if (!el) { return false; } - const { users, paths } = el.dataset; + const { [userPropKey]: user, paths } = el.dataset; return new Vue({ el, apolloProvider, render: (createElement) => - createElement(AdminUsersApp, { + createElement(component, { props: { - users: convertObjectPropsToCamelCase(JSON.parse(users), { deep: true }), + [userPropKey]: convertObjectPropsToCamelCase(JSON.parse(user), { deep: true }), paths: convertObjectPropsToCamelCase(JSON.parse(paths)), + ...props, }, }), }); }; + +export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => + initApp(el, AdminUsersApp, 'users'); + +export const initAdminUserActions = (el = document.querySelector('#js-admin-user-actions')) => + 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, + functional: true, + methods: { + show(...args) { + this.$refs.manager.show(...args); + }, + }, + render(h) { + return h(ModalManager, { + ref: 'manager', + props: { + selector: CONFIRM_DELETE_BUTTON_SELECTOR, + modalConfiguration, + csrfToken: csrf.token, + }, + }); + }, + }); +}; |