diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-09 21:10:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-09 21:10:06 +0300 |
commit | 17deb2a503bb8163514fe37618bf36f75376b9ae (patch) | |
tree | f0bc3819cb3f9f19f30301191e250d069461c8d3 /app/assets/javascripts/members | |
parent | e9de69b545c25c9cb7fd410f9bf8ba34c6bb727b (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/members')
9 files changed, 129 insertions, 32 deletions
diff --git a/app/assets/javascripts/members/components/action_dropdowns/constants.js b/app/assets/javascripts/members/components/action_dropdowns/constants.js index f6718713e2b..eb5b2182ece 100644 --- a/app/assets/javascripts/members/components/action_dropdowns/constants.js +++ b/app/assets/javascripts/members/components/action_dropdowns/constants.js @@ -11,4 +11,8 @@ export const I18N = { confirmOrphanedUserRemoval: s__( 'Members|Are you sure you want to remove this orphaned member from "%{group}"?', ), + personalProjectOwnerCannotBeRemoved: s__("Members|A personal project's owner cannot be removed."), + lastGroupOwnerCannotBeRemoved: s__( + 'Members|A group must have at least one owner. To remove the member, assign a new owner.', + ), }; diff --git a/app/assets/javascripts/members/components/action_dropdowns/leave_group_dropdown_item.vue b/app/assets/javascripts/members/components/action_dropdowns/leave_group_dropdown_item.vue index b5e0317d54f..15606ad567c 100644 --- a/app/assets/javascripts/members/components/action_dropdowns/leave_group_dropdown_item.vue +++ b/app/assets/javascripts/members/components/action_dropdowns/leave_group_dropdown_item.vue @@ -18,6 +18,10 @@ export default { type: Object, required: true, }, + permissions: { + type: Object, + required: true, + }, }, }; </script> @@ -27,6 +31,6 @@ export default { <span class="gl-text-red-500"> <slot></slot> </span> - <leave-modal :member="member" /> + <leave-modal :member="member" :permissions="permissions" /> </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue b/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue index 18f1a8739bf..f224aaa31f7 100644 --- a/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue +++ b/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue @@ -40,6 +40,11 @@ export default { required: false, default: () => ({}), }, + preventRemoval: { + type: Boolean, + required: false, + default: false, + }, }, computed: { ...mapState({ @@ -55,6 +60,7 @@ export default { memberModelType: this.memberModelType, message: this.modalMessage, userDeletionObstacles: this.userDeletionObstacles, + preventRemoval: this.preventRemoval, }; }, }, diff --git a/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue b/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue index 4e5e6b0cab4..816481d4329 100644 --- a/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue +++ b/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue @@ -2,6 +2,10 @@ import { GlDropdown, GlTooltipDirective } from '@gitlab/ui'; import { sprintf } from '~/locale'; import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils'; +import { + MEMBER_MODEL_TYPE_GROUP_MEMBER, + MEMBER_MODEL_TYPE_PROJECT_MEMBER, +} from '~/members/constants'; import { I18N } from './constants'; import LeaveGroupDropdownItem from './leave_group_dropdown_item.vue'; import RemoveMemberDropdownItem from './remove_member_dropdown_item.vue'; @@ -37,6 +41,16 @@ export default { modalMessage() { const { user, source } = this.member; + if (this.permissions.canRemoveBlockedByLastOwner) { + if (this.member.type === MEMBER_MODEL_TYPE_PROJECT_MEMBER) { + return I18N.personalProjectOwnerCannotBeRemoved; + } + + if (this.member.type === MEMBER_MODEL_TYPE_GROUP_MEMBER) { + return I18N.lastGroupOwnerCannotBeRemoved; + } + } + if (user) { return sprintf( this.$options.i18n.confirmNormalUserRemoval, @@ -54,7 +68,10 @@ export default { }; }, showDropdown() { - return this.permissions.canRemove || this.showLdapOverride; + return this.showLeaveOrRemove || this.showLdapOverride; + }, + showLeaveOrRemove() { + return this.permissions.canRemove || this.permissions.canRemoveBlockedByLastOwner; }, showLdapOverride() { return this.permissions.canOverride && !this.member.isOverridden; @@ -76,8 +93,8 @@ export default { data-testid="user-action-dropdown" data-qa-selector="user_action_dropdown" > - <template v-if="permissions.canRemove"> - <leave-group-dropdown-item v-if="isCurrentUser" :member="member">{{ + <template v-if="showLeaveOrRemove"> + <leave-group-dropdown-item v-if="isCurrentUser" :member="member" :permissions="permissions">{{ $options.i18n.leaveGroup }}</leave-group-dropdown-item> <remove-member-dropdown-item @@ -86,6 +103,7 @@ export default { :member-model-type="member.type" :user-deletion-obstacles="userDeletionObstaclesUserData" :modal-message="modalMessage" + :prevent-removal="permissions.canRemoveBlockedByLastOwner" >{{ $options.i18n.removeMember }}</remove-member-dropdown-item > </template> diff --git a/app/assets/javascripts/members/components/modals/leave_modal.vue b/app/assets/javascripts/members/components/modals/leave_modal.vue index e39669e17dd..8bc6aca9cc1 100644 --- a/app/assets/javascripts/members/components/modals/leave_modal.vue +++ b/app/assets/javascripts/members/components/modals/leave_modal.vue @@ -5,22 +5,30 @@ import csrf from '~/lib/utils/csrf'; import { __, s__, sprintf } from '~/locale'; import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue'; import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils'; -import { LEAVE_MODAL_ID } from '../../constants'; +import { + LEAVE_MODAL_ID, + MEMBER_MODEL_TYPE_GROUP_MEMBER, + MEMBER_MODEL_TYPE_PROJECT_MEMBER, +} from '../../constants'; export default { name: 'LeaveModal', actionCancel: { text: __('Cancel'), }, - actionPrimary: { - text: __('Leave'), - attributes: { - variant: 'danger', - }, - }, csrf, modalId: LEAVE_MODAL_ID, - modalContent: s__('Members|Are you sure you want to leave "%{source}"?'), + i18n: { + title: s__('Members|Leave "%{source}"'), + body: s__('Members|Are you sure you want to leave "%{source}"?'), + preventedTitle: s__('Members|Cannot leave "%{source}"'), + preventedBodyProjectMemberModelType: s__( + 'Members|You cannot remove yourself from a personal project.', + ), + preventedBodyGroupMemberModelType: s__( + 'Members|A group must have at least one owner. To leave this group, assign a new owner.', + ), + }, components: { GlModal, GlForm, GlSprintf, UserDeletionObstaclesList }, directives: { GlTooltip: GlTooltipDirective, @@ -31,6 +39,10 @@ export default { type: Object, required: true, }, + permissions: { + type: Object, + required: true, + }, }, computed: { ...mapState({ @@ -42,7 +54,35 @@ export default { return this.memberPath.replace(/:id$/, 'leave'); }, modalTitle() { - return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.fullName }); + return sprintf( + this.permissions.canRemoveBlockedByLastOwner + ? this.$options.i18n.preventedTitle + : this.$options.i18n.title, + { source: this.member.source.fullName }, + ); + }, + preventedModalBody() { + if (this.member.type === MEMBER_MODEL_TYPE_PROJECT_MEMBER) { + return this.$options.i18n.preventedBodyProjectMemberModelType; + } + + if (this.member.type === MEMBER_MODEL_TYPE_GROUP_MEMBER) { + return this.$options.i18n.preventedBodyGroupMemberModelType; + } + + return null; + }, + actionPrimary() { + if (this.permissions.canRemoveBlockedByLastOwner) { + return null; + } + + return { + text: __('Leave'), + attributes: { + variant: 'danger', + }, + }; }, obstacles() { return parseUserDeletionObstacles(this.member.user); @@ -64,13 +104,14 @@ export default { v-bind="$attrs" :modal-id="$options.modalId" :title="modalTitle" - :action-primary="$options.actionPrimary" + :action-primary="actionPrimary" :action-cancel="$options.actionCancel" @primary="handlePrimary" > <gl-form ref="form" :action="leavePath" method="post"> <p> - <gl-sprintf :message="$options.modalContent"> + <template v-if="permissions.canRemoveBlockedByLastOwner">{{ preventedModalBody }}</template> + <gl-sprintf v-else :message="$options.i18n.body"> <template #source>{{ member.source.fullName }}</template> </gl-sprintf> </p> diff --git a/app/assets/javascripts/members/components/modals/remove_member_modal.vue b/app/assets/javascripts/members/components/modals/remove_member_modal.vue index edd464574fb..337379d8b4e 100644 --- a/app/assets/javascripts/members/components/modals/remove_member_modal.vue +++ b/app/assets/javascripts/members/components/modals/remove_member_modal.vue @@ -42,6 +42,9 @@ export default { userDeletionObstacles(state) { return state[this.namespace].removeMemberModalData.userDeletionObstacles ?? {}; }, + preventRemoval(state) { + return state[this.namespace].removeMemberModalData.preventRemoval; + }, removeMemberModalVisible(state) { return state[this.namespace].removeMemberModalVisible; }, @@ -59,6 +62,10 @@ export default { return __('Remove member'); }, actionPrimary() { + if (this.preventRemoval) { + return null; + } + return { text: this.actionText, attributes: { @@ -101,21 +108,22 @@ export default { > <form ref="form" :action="memberPath" method="post"> <p>{{ message }}</p> + <template v-if="!preventRemoval"> + <user-deletion-obstacles-list + v-if="hasObstaclesToUserDeletion" + :obstacles="userDeletionObstacles.obstacles" + :user-name="userDeletionObstacles.name" + /> - <user-deletion-obstacles-list - v-if="hasObstaclesToUserDeletion" - :obstacles="userDeletionObstacles.obstacles" - :user-name="userDeletionObstacles.name" - /> - - <input ref="method" type="hidden" name="_method" value="delete" /> - <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> - <gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships"> - {{ __('Also remove direct user membership from subgroups and projects') }} - </gl-form-checkbox> - <gl-form-checkbox v-if="hasWorkspaceAccess" name="unassign_issuables"> - {{ __('Also unassign this user from related issues and merge requests') }} - </gl-form-checkbox> + <input ref="method" type="hidden" name="_method" value="delete" /> + <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> + <gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships"> + {{ __('Also remove direct user membership from subgroups and projects') }} + </gl-form-checkbox> + <gl-form-checkbox v-if="hasWorkspaceAccess" name="unassign_issuables"> + {{ __('Also unassign this user from related issues and merge requests') }} + </gl-form-checkbox> + </template> </form> </gl-modal> </template> diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue index c847f9c8311..6d242f38d0e 100644 --- a/app/assets/javascripts/members/components/table/members_table.vue +++ b/app/assets/javascripts/members/components/table/members_table.vue @@ -2,7 +2,14 @@ import { GlTable, GlBadge, GlPagination } from '@gitlab/ui'; import { mapState } from 'vuex'; import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue'; -import { canUnban, canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils'; +import { + canUnban, + canOverride, + canRemove, + canRemoveBlockedByLastOwner, + canResend, + canUpdate, +} from 'ee_else_ce/members/utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import { FIELD_KEY_ACTIONS, @@ -43,7 +50,7 @@ export default { LdapOverrideConfirmationModal: () => import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'), }, - inject: ['namespace', 'currentUserId'], + inject: ['namespace', 'currentUserId', 'canManageMembers'], props: { tabQueryParamValue: { type: String, @@ -84,6 +91,7 @@ export default { hasActionButtons(member) { return ( canRemove(member) || + canRemoveBlockedByLastOwner(member, this.canManageMembers) || canResend(member) || canUpdate(member, this.currentUserId) || canOverride(member) || diff --git a/app/assets/javascripts/members/components/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue index 51eff428d63..407cbc55dd3 100644 --- a/app/assets/javascripts/members/components/table/members_table_cell.vue +++ b/app/assets/javascripts/members/components/table/members_table_cell.vue @@ -5,13 +5,14 @@ import { isDirectMember, isCurrentUser, canRemove, + canRemoveBlockedByLastOwner, canResend, canUpdate, } from '../../utils'; export default { name: 'MembersTableCell', - inject: ['currentUserId'], + inject: ['currentUserId', 'canManageMembers'], props: { member: { type: Object, @@ -45,6 +46,9 @@ export default { isCurrentUser() { return isCurrentUser(this.member, this.currentUserId); }, + canRemoveBlockedByLastOwner() { + return canRemoveBlockedByLastOwner(this.member, this.canManageMembers); + }, canRemove() { return canRemove(this.member); }, @@ -62,6 +66,7 @@ export default { isCurrentUser: this.isCurrentUser, permissions: { canRemove: this.canRemove, + canRemoveBlockedByLastOwner: this.canRemoveBlockedByLastOwner, canResend: this.canResend, canUpdate: this.canUpdate, }, diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js index bf87ab53d36..b7af97ebac5 100644 --- a/app/assets/javascripts/members/utils.js +++ b/app/assets/javascripts/members/utils.js @@ -51,6 +51,9 @@ export const canRemove = (member) => { return isDirectMember(member) && member.canRemove; }; +export const canRemoveBlockedByLastOwner = (member, canManageMembers) => + isDirectMember(member) && canManageMembers && member.isLastOwner; + export const canResend = (member) => { return Boolean(member.invite?.canResend); }; |