Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
afbe8fe679
commit
dbdfb3e36d
|
|
@ -1,83 +0,0 @@
|
||||||
<script>
|
|
||||||
import { s__, __, sprintf } from '~/locale';
|
|
||||||
import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
|
|
||||||
import ActionButtonGroup from './action_button_group.vue';
|
|
||||||
import LeaveButton from './leave_button.vue';
|
|
||||||
import RemoveMemberButton from './remove_member_button.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'UserActionButtons',
|
|
||||||
i18n: {
|
|
||||||
title: __('Remove member'),
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
ActionButtonGroup,
|
|
||||||
RemoveMemberButton,
|
|
||||||
LeaveButton,
|
|
||||||
LdapOverrideButton: () =>
|
|
||||||
import('ee_component/members/components/ldap/ldap_override_button.vue'),
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
member: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isCurrentUser: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
message() {
|
|
||||||
const { user, source } = this.member;
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
return sprintf(
|
|
||||||
s__('Members|Are you sure you want to remove %{usersName} from "%{source}"?'),
|
|
||||||
{
|
|
||||||
usersName: user.name,
|
|
||||||
source: source.fullName,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf(
|
|
||||||
s__('Members|Are you sure you want to remove this orphaned member from "%{source}"?'),
|
|
||||||
{
|
|
||||||
source: source.fullName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
userDeletionObstaclesUserData() {
|
|
||||||
return {
|
|
||||||
name: this.member.user?.name,
|
|
||||||
obstacles: parseUserDeletionObstacles(this.member.user),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<action-button-group>
|
|
||||||
<div v-if="permissions.canRemove" class="gl-px-1">
|
|
||||||
<leave-button v-if="isCurrentUser" :member="member" />
|
|
||||||
<remove-member-button
|
|
||||||
v-else
|
|
||||||
:member-id="member.id"
|
|
||||||
:member-type="member.type"
|
|
||||||
:user-deletion-obstacles="userDeletionObstaclesUserData"
|
|
||||||
:message="message"
|
|
||||||
:title="$options.i18n.title"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="permissions.canOverride && !member.isOverridden" class="gl-px-1">
|
|
||||||
<ldap-override-button :member="member" />
|
|
||||||
</div>
|
|
||||||
</action-button-group>
|
|
||||||
</template>
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { __, s__ } from '~/locale';
|
||||||
|
|
||||||
|
export const I18N = {
|
||||||
|
actions: __('More actions'),
|
||||||
|
editPermissions: s__('Members|Edit permissions'),
|
||||||
|
leaveGroup: __('Leave group'),
|
||||||
|
removeMember: __('Remove member'),
|
||||||
|
confirmNormalUserRemoval: s__(
|
||||||
|
'Members|Are you sure you want to remove %{userName} from "%{group}"?',
|
||||||
|
),
|
||||||
|
confirmOrphanedUserRemoval: s__(
|
||||||
|
'Members|Are you sure you want to remove this orphaned member from "%{group}"?',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
|
import { GlDropdownItem, GlModalDirective } from '@gitlab/ui';
|
||||||
import { __ } from '~/locale';
|
|
||||||
import { LEAVE_MODAL_ID } from '../../constants';
|
import { LEAVE_MODAL_ID } from '../../constants';
|
||||||
import LeaveModal from '../modals/leave_modal.vue';
|
import LeaveModal from '../modals/leave_modal.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'LeaveButton',
|
name: 'LeaveGroupDropdownItem',
|
||||||
title: __('Leave'),
|
|
||||||
modalId: LEAVE_MODAL_ID,
|
modalId: LEAVE_MODAL_ID,
|
||||||
components: {
|
components: {
|
||||||
GlButton,
|
GlDropdownItem,
|
||||||
LeaveModal,
|
LeaveModal,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlModal: GlModalDirective,
|
GlModal: GlModalDirective,
|
||||||
GlTooltip: GlTooltipDirective,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
member: {
|
member: {
|
||||||
|
|
@ -26,14 +23,10 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<gl-dropdown-item v-gl-modal="$options.modalId">
|
||||||
<gl-button
|
<span class="gl-text-red-500">
|
||||||
v-gl-tooltip.hover
|
<slot></slot>
|
||||||
v-gl-modal="$options.modalId"
|
</span>
|
||||||
:title="$options.title"
|
|
||||||
:aria-label="$options.title"
|
|
||||||
icon="leave"
|
|
||||||
/>
|
|
||||||
<leave-modal :member="member" />
|
<leave-modal :member="member" />
|
||||||
</div>
|
</gl-dropdown-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script>
|
||||||
|
import { GlDropdownItem } from '@gitlab/ui';
|
||||||
|
import { mapActions, mapState } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RemoveMemberDropdownItem',
|
||||||
|
components: { GlDropdownItem },
|
||||||
|
inject: ['namespace'],
|
||||||
|
props: {
|
||||||
|
memberId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
memberType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
modalMessage: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isAccessRequest: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isInvite: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
userDeletionObstacles: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
memberPath(state) {
|
||||||
|
return state[this.namespace].memberPath;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
modalData() {
|
||||||
|
return {
|
||||||
|
isAccessRequest: this.isAccessRequest,
|
||||||
|
isInvite: this.isInvite,
|
||||||
|
memberPath: this.memberPath.replace(':id', this.memberId),
|
||||||
|
memberType: this.memberType,
|
||||||
|
message: this.modalMessage,
|
||||||
|
userDeletionObstacles: this.userDeletionObstacles,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions({
|
||||||
|
showRemoveMemberModal(dispatch, payload) {
|
||||||
|
return dispatch(`${this.namespace}/showRemoveMemberModal`, payload);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<gl-dropdown-item
|
||||||
|
data-qa-selector="delete_member_dropdown_item"
|
||||||
|
@click="showRemoveMemberModal(modalData)"
|
||||||
|
>
|
||||||
|
<span class="gl-text-red-500">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</gl-dropdown-item>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script>
|
||||||
|
import { GlDropdown, GlTooltipDirective } from '@gitlab/ui';
|
||||||
|
import { sprintf } from '~/locale';
|
||||||
|
import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
|
||||||
|
import { I18N } from './constants';
|
||||||
|
import LeaveGroupDropdownItem from './leave_group_dropdown_item.vue';
|
||||||
|
import RemoveMemberDropdownItem from './remove_member_dropdown_item.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UserActionDropdown',
|
||||||
|
i18n: I18N,
|
||||||
|
components: {
|
||||||
|
GlDropdown,
|
||||||
|
LdapOverrideDropdownItem: () =>
|
||||||
|
import('ee_component/members/components/ldap/ldap_override_dropdown_item.vue'),
|
||||||
|
LeaveGroupDropdownItem,
|
||||||
|
RemoveMemberDropdownItem,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
GlTooltip: GlTooltipDirective,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
member: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isCurrentUser: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
modalMessage() {
|
||||||
|
const { user, source } = this.member;
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return sprintf(
|
||||||
|
this.$options.i18n.confirmNormalUserRemoval,
|
||||||
|
{ userName: user.name, group: source.fullName },
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf(this.$options.i18n.confirmOrphanedUserRemoval, { group: source.fullName });
|
||||||
|
},
|
||||||
|
userDeletionObstaclesUserData() {
|
||||||
|
return {
|
||||||
|
name: this.member.user?.name,
|
||||||
|
obstacles: parseUserDeletionObstacles(this.member.user),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
showDropdown() {
|
||||||
|
return this.permissions.canRemove || this.showLdapOverride;
|
||||||
|
},
|
||||||
|
showLdapOverride() {
|
||||||
|
return this.permissions.canOverride && !this.member.isOverridden;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<gl-dropdown
|
||||||
|
v-if="showDropdown"
|
||||||
|
v-gl-tooltip="$options.i18n.actions"
|
||||||
|
:text="$options.i18n.actions"
|
||||||
|
text-sr-only
|
||||||
|
icon="ellipsis_v"
|
||||||
|
category="tertiary"
|
||||||
|
no-caret
|
||||||
|
right
|
||||||
|
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">{{
|
||||||
|
$options.i18n.leaveGroup
|
||||||
|
}}</leave-group-dropdown-item>
|
||||||
|
<remove-member-dropdown-item
|
||||||
|
v-else
|
||||||
|
:member-id="member.id"
|
||||||
|
:member-type="member.type"
|
||||||
|
:user-deletion-obstacles="userDeletionObstaclesUserData"
|
||||||
|
:modal-message="modalMessage"
|
||||||
|
>{{ $options.i18n.removeMember }}</remove-member-dropdown-item
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<ldap-override-dropdown-item v-else-if="showLdapOverride" :member="member">{{
|
||||||
|
$options.i18n.editPermissions
|
||||||
|
}}</ldap-override-dropdown-item>
|
||||||
|
</gl-dropdown>
|
||||||
|
</template>
|
||||||
|
|
@ -3,12 +3,12 @@ import { MEMBER_TYPES, EE_ACTION_BUTTONS } from 'ee_else_ce/members/constants';
|
||||||
import AccessRequestActionButtons from '../action_buttons/access_request_action_buttons.vue';
|
import AccessRequestActionButtons from '../action_buttons/access_request_action_buttons.vue';
|
||||||
import GroupActionButtons from '../action_buttons/group_action_buttons.vue';
|
import GroupActionButtons from '../action_buttons/group_action_buttons.vue';
|
||||||
import InviteActionButtons from '../action_buttons/invite_action_buttons.vue';
|
import InviteActionButtons from '../action_buttons/invite_action_buttons.vue';
|
||||||
import UserActionButtons from '../action_buttons/user_action_buttons.vue';
|
import UserActionDropdown from '../action_dropdowns/user_action_dropdown.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MemberActionButtons',
|
name: 'MemberActionButtons',
|
||||||
components: {
|
components: {
|
||||||
UserActionButtons,
|
UserActionDropdown,
|
||||||
GroupActionButtons,
|
GroupActionButtons,
|
||||||
InviteActionButtons,
|
InviteActionButtons,
|
||||||
AccessRequestActionButtons,
|
AccessRequestActionButtons,
|
||||||
|
|
@ -36,7 +36,7 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
actionButtonComponent() {
|
actionButtonComponent() {
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
[MEMBER_TYPES.user]: 'user-action-buttons',
|
[MEMBER_TYPES.user]: 'user-action-dropdown',
|
||||||
[MEMBER_TYPES.group]: 'group-action-buttons',
|
[MEMBER_TYPES.group]: 'group-action-buttons',
|
||||||
[MEMBER_TYPES.invite]: 'invite-action-buttons',
|
[MEMBER_TYPES.invite]: 'invite-action-buttons',
|
||||||
[MEMBER_TYPES.accessRequest]: 'access-request-action-buttons',
|
[MEMBER_TYPES.accessRequest]: 'access-request-action-buttons',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ module ContainerRegistry
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
attr_accessor :uri
|
attr_accessor :uri
|
||||||
|
attr_reader :options, :base_uri
|
||||||
|
|
||||||
REGISTRY_VERSION_HEADER = 'gitlab-container-registry-version'
|
REGISTRY_VERSION_HEADER = 'gitlab-container-registry-version'
|
||||||
REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features'
|
REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features'
|
||||||
|
|
|
||||||
|
|
@ -25824,10 +25824,10 @@ msgstr ""
|
||||||
msgid "Members|Are you sure you want to remove \"%{groupName}\"?"
|
msgid "Members|Are you sure you want to remove \"%{groupName}\"?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Members|Are you sure you want to remove %{usersName} from \"%{source}\"?"
|
msgid "Members|Are you sure you want to remove %{userName} from \"%{group}\"?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Members|Are you sure you want to remove this orphaned member from \"%{source}\"?"
|
msgid "Members|Are you sure you want to remove this orphaned member from \"%{group}\"?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join \"%{source}\""
|
msgid "Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join \"%{source}\""
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,12 @@ module QA
|
||||||
element :access_level_link
|
element :access_level_link
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/assets/javascripts/members/components/action_buttons/remove_member_button.vue' do
|
view 'app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue' do
|
||||||
element :delete_member_button
|
element :user_action_dropdown
|
||||||
|
end
|
||||||
|
|
||||||
|
view 'app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue' do
|
||||||
|
element :delete_member_dropdown_item
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/assets/javascripts/members/components/members_tabs.vue' do
|
view 'app/assets/javascripts/members/components/members_tabs.vue' do
|
||||||
|
|
@ -41,7 +45,8 @@ module QA
|
||||||
|
|
||||||
def remove_member(username)
|
def remove_member(username)
|
||||||
within_element(:member_row, text: username) do
|
within_element(:member_row, text: username) do
|
||||||
click_element :delete_member_button
|
click_element :user_action_dropdown
|
||||||
|
click_element :delete_member_dropdown_item
|
||||||
end
|
end
|
||||||
|
|
||||||
within_element(:remove_member_modal) do
|
within_element(:remove_member_modal) do
|
||||||
|
|
|
||||||
|
|
@ -269,9 +269,12 @@ RSpec.describe 'Admin Groups', feature_category: :subgroups do
|
||||||
expect(page).to have_content('Developer')
|
expect(page).to have_content('Developer')
|
||||||
end
|
end
|
||||||
|
|
||||||
find_member_row(current_user).click_button(title: 'Leave')
|
show_actions_for_username(current_user)
|
||||||
|
click_button _('Leave group')
|
||||||
|
|
||||||
accept_gl_confirm(button_text: 'Leave')
|
within_modal do
|
||||||
|
click_button _('Leave')
|
||||||
|
end
|
||||||
|
|
||||||
wait_for_all_requests
|
wait_for_all_requests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,12 +151,11 @@ RSpec.describe "Admin::Projects", feature_category: :projects do
|
||||||
|
|
||||||
expect(find_member_row(current_user)).to have_content('Developer')
|
expect(find_member_row(current_user)).to have_content('Developer')
|
||||||
|
|
||||||
page.within find_member_row(current_user) do
|
show_actions_for_username(current_user)
|
||||||
click_button 'Leave'
|
click_button _('Leave group')
|
||||||
end
|
|
||||||
|
|
||||||
within_modal do
|
within_modal do
|
||||||
click_button('Leave')
|
click_button _('Leave')
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page).to have_current_path(dashboard_projects_path, ignore_query: true, url: false)
|
expect(page).to have_current_path(dashboard_projects_path, ignore_query: true, url: false)
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,13 @@ RSpec.describe 'Groups > Members > Manage members', feature_category: :subgroups
|
||||||
|
|
||||||
# Open modal
|
# Open modal
|
||||||
page.within(second_row) do
|
page.within(second_row) do
|
||||||
click_button 'Remove member'
|
show_actions
|
||||||
|
click_button _('Remove member')
|
||||||
end
|
end
|
||||||
|
|
||||||
within_modal do
|
within_modal do
|
||||||
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
|
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
|
||||||
click_button('Remove member')
|
click_button _('Remove member')
|
||||||
end
|
end
|
||||||
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
|
||||||
|
|
@ -139,17 +139,15 @@ RSpec.describe 'Projects > Members > Manage members', :js, feature_category: :on
|
||||||
|
|
||||||
it 'can only remove non-Owner members' do
|
it 'can only remove non-Owner members' do
|
||||||
page.within find_member_row(project_owner) do
|
page.within find_member_row(project_owner) do
|
||||||
expect(page).not_to have_button('Remove member')
|
expect(page).not_to have_selector user_action_dropdown
|
||||||
end
|
end
|
||||||
|
|
||||||
# Open modal
|
show_actions_for_username(project_developer)
|
||||||
page.within find_member_row(project_developer) do
|
click_button _('Remove member')
|
||||||
click_button 'Remove member'
|
|
||||||
end
|
|
||||||
|
|
||||||
within_modal do
|
within_modal do
|
||||||
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
|
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
|
||||||
click_button('Remove member')
|
click_button _('Remove member')
|
||||||
end
|
end
|
||||||
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
@ -163,18 +161,12 @@ RSpec.describe 'Projects > Members > Manage members', :js, feature_category: :on
|
||||||
let(:current_user) { group_owner }
|
let(:current_user) { group_owner }
|
||||||
|
|
||||||
it 'can remove any direct member' do
|
it 'can remove any direct member' do
|
||||||
page.within find_member_row(project_owner) do
|
show_actions_for_username(project_owner)
|
||||||
expect(page).to have_button('Remove member')
|
click_button _('Remove member')
|
||||||
end
|
|
||||||
|
|
||||||
# Open modal
|
|
||||||
page.within find_member_row(project_owner) do
|
|
||||||
click_button 'Remove member'
|
|
||||||
end
|
|
||||||
|
|
||||||
within_modal do
|
within_modal do
|
||||||
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
|
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
|
||||||
click_button('Remove member')
|
click_button _('Remove member')
|
||||||
end
|
end
|
||||||
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,12 @@ RSpec.describe 'Projects > Settings > User manages project members', feature_cat
|
||||||
it 'cancels a team member', :js do
|
it 'cancels a team member', :js do
|
||||||
visit(project_project_members_path(project))
|
visit(project_project_members_path(project))
|
||||||
|
|
||||||
page.within find_member_row(user_dmitriy) do
|
show_actions_for_username(user_dmitriy)
|
||||||
click_button 'Remove member'
|
click_button _('Remove member')
|
||||||
end
|
|
||||||
|
|
||||||
within_modal do
|
within_modal do
|
||||||
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
|
expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
|
||||||
click_button('Remove member')
|
click_button _('Remove member')
|
||||||
end
|
end
|
||||||
|
|
||||||
visit(project_project_members_path(project))
|
visit(project_project_members_path(project))
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import { GlButton } from '@gitlab/ui';
|
|
||||||
import { shallowMount } from '@vue/test-utils';
|
|
||||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
|
||||||
import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
|
|
||||||
import LeaveModal from '~/members/components/modals/leave_modal.vue';
|
|
||||||
import { LEAVE_MODAL_ID } from '~/members/constants';
|
|
||||||
import { member } from '../../mock_data';
|
|
||||||
|
|
||||||
describe('LeaveButton', () => {
|
|
||||||
let wrapper;
|
|
||||||
|
|
||||||
const createComponent = (propsData = {}) => {
|
|
||||||
wrapper = shallowMount(LeaveButton, {
|
|
||||||
propsData: {
|
|
||||||
member,
|
|
||||||
...propsData,
|
|
||||||
},
|
|
||||||
directives: {
|
|
||||||
GlTooltip: createMockDirective(),
|
|
||||||
GlModal: createMockDirective(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const findButton = () => wrapper.findComponent(GlButton);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
createComponent();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
wrapper.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays a tooltip', () => {
|
|
||||||
const button = findButton();
|
|
||||||
|
|
||||||
expect(getBinding(button.element, 'gl-tooltip')).not.toBeUndefined();
|
|
||||||
expect(button.attributes('title')).toBe('Leave');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets `aria-label` attribute', () => {
|
|
||||||
expect(findButton().attributes('aria-label')).toBe('Leave');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders leave modal', () => {
|
|
||||||
const leaveModal = wrapper.findComponent(LeaveModal);
|
|
||||||
|
|
||||||
expect(leaveModal.exists()).toBe(true);
|
|
||||||
expect(leaveModal.props('member')).toEqual(member);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('triggers leave modal', () => {
|
|
||||||
const binding = getBinding(findButton().element, 'gl-modal');
|
|
||||||
|
|
||||||
expect(binding).not.toBeUndefined();
|
|
||||||
expect(binding.value).toBe(LEAVE_MODAL_ID);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { GlDropdownItem } from '@gitlab/ui';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||||
|
import LeaveGroupDropdownItem from '~/members/components/action_dropdowns/leave_group_dropdown_item.vue';
|
||||||
|
import LeaveModal from '~/members/components/modals/leave_modal.vue';
|
||||||
|
import { LEAVE_MODAL_ID } from '~/members/constants';
|
||||||
|
import { member } from '../../mock_data';
|
||||||
|
|
||||||
|
describe('LeaveGroupDropdownItem', () => {
|
||||||
|
let wrapper;
|
||||||
|
const text = 'dummy';
|
||||||
|
|
||||||
|
const createComponent = (propsData = {}) => {
|
||||||
|
wrapper = shallowMount(LeaveGroupDropdownItem, {
|
||||||
|
propsData: {
|
||||||
|
member,
|
||||||
|
...propsData,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
GlModal: createMockDirective(),
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: text,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a slot with red text', () => {
|
||||||
|
expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains LeaveModal component', () => {
|
||||||
|
const leaveModal = wrapper.findComponent(LeaveModal);
|
||||||
|
|
||||||
|
expect(leaveModal.props('member')).toEqual(member);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('binds to the LeaveModal component', () => {
|
||||||
|
const binding = getBinding(findDropdownItem().element, 'gl-modal');
|
||||||
|
|
||||||
|
expect(binding.value).toBe(LEAVE_MODAL_ID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { GlDropdownItem } from '@gitlab/ui';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import { modalData } from 'jest/members/mock_data';
|
||||||
|
import RemoveMemberDropdownItem from '~/members/components/action_dropdowns/remove_member_dropdown_item.vue';
|
||||||
|
import { MEMBER_TYPES } from '~/members/constants';
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
describe('RemoveMemberDropdownItem', () => {
|
||||||
|
let wrapper;
|
||||||
|
const text = 'dummy';
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
showRemoveMemberModal: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createStore = (state = {}) => {
|
||||||
|
return new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
[MEMBER_TYPES.user]: {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
memberPath: '/groups/foo-bar/-/group_members/:id',
|
||||||
|
...state,
|
||||||
|
},
|
||||||
|
actions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createComponent = (propsData = {}, state) => {
|
||||||
|
wrapper = shallowMount(RemoveMemberDropdownItem, {
|
||||||
|
store: createStore(state),
|
||||||
|
provide: {
|
||||||
|
namespace: MEMBER_TYPES.user,
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
memberId: 1,
|
||||||
|
memberType: 'GroupMember',
|
||||||
|
modalMessage: 'Are you sure you want to remove John Smith?',
|
||||||
|
isAccessRequest: true,
|
||||||
|
isInvite: true,
|
||||||
|
userDeletionObstacles: { name: 'user', obstacles: [] },
|
||||||
|
...propsData,
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: text,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a slot with red text', () => {
|
||||||
|
expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls Vuex action to show `remove member` modal when clicked', () => {
|
||||||
|
findDropdownItem().vm.$emit('click');
|
||||||
|
|
||||||
|
expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), modalData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,25 +1,31 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
|
import { sprintf } from '~/locale';
|
||||||
import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
|
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||||
import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
|
import LeaveGroupDropdownItem from '~/members/components/action_dropdowns/leave_group_dropdown_item.vue';
|
||||||
|
import RemoveMemberDropdownItem from '~/members/components/action_dropdowns/remove_member_dropdown_item.vue';
|
||||||
|
import UserActionDropdown from '~/members/components/action_dropdowns/user_action_dropdown.vue';
|
||||||
|
import { I18N } from '~/members/components/action_dropdowns/constants';
|
||||||
import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
|
import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
|
||||||
import { member, orphanedMember } from '../../mock_data';
|
import { member, orphanedMember } from '../../mock_data';
|
||||||
|
|
||||||
describe('UserActionButtons', () => {
|
describe('UserActionDropdown', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const createComponent = (propsData = {}) => {
|
const createComponent = (propsData = {}) => {
|
||||||
wrapper = shallowMount(UserActionButtons, {
|
wrapper = shallowMount(UserActionDropdown, {
|
||||||
propsData: {
|
propsData: {
|
||||||
member,
|
member,
|
||||||
isCurrentUser: false,
|
isCurrentUser: false,
|
||||||
isInvitedUser: false,
|
isInvitedUser: false,
|
||||||
...propsData,
|
...propsData,
|
||||||
},
|
},
|
||||||
|
directives: {
|
||||||
|
GlTooltip: createMockDirective(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
|
const findRemoveMemberDropdownItem = () => wrapper.findComponent(RemoveMemberDropdownItem);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
|
|
@ -34,16 +40,30 @@ describe('UserActionButtons', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders remove member button', () => {
|
it('renders remove member dropdown with correct text', () => {
|
||||||
expect(findRemoveMemberButton().exists()).toBe(true);
|
const removeMemberDropdownItem = findRemoveMemberDropdownItem();
|
||||||
|
expect(removeMemberDropdownItem.exists()).toBe(true);
|
||||||
|
expect(removeMemberDropdownItem.html()).toContain(I18N.removeMember);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a tooltip', () => {
|
||||||
|
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
|
||||||
|
expect(tooltip).not.toBeUndefined();
|
||||||
|
expect(tooltip.value).toBe(I18N.actions);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets props correctly', () => {
|
it('sets props correctly', () => {
|
||||||
expect(findRemoveMemberButton().props()).toEqual({
|
expect(findRemoveMemberDropdownItem().props()).toEqual({
|
||||||
memberId: member.id,
|
memberId: member.id,
|
||||||
memberType: 'GroupMember',
|
memberType: 'GroupMember',
|
||||||
message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"?`,
|
modalMessage: sprintf(
|
||||||
title: UserActionButtons.i18n.title,
|
I18N.confirmNormalUserRemoval,
|
||||||
|
{
|
||||||
|
userName: member.user.name,
|
||||||
|
group: member.source.fullName,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
),
|
||||||
isAccessRequest: false,
|
isAccessRequest: false,
|
||||||
isInvite: false,
|
isInvite: false,
|
||||||
userDeletionObstacles: {
|
userDeletionObstacles: {
|
||||||
|
|
@ -62,14 +82,14 @@ describe('UserActionButtons', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findRemoveMemberButton().props('message')).toBe(
|
expect(findRemoveMemberDropdownItem().props('modalMessage')).toBe(
|
||||||
`Are you sure you want to remove this orphaned member from "${orphanedMember.source.fullName}"?`,
|
sprintf(I18N.confirmOrphanedUserRemoval, { group: orphanedMember.source.fullName }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when member is the current user', () => {
|
describe('when member is the current user', () => {
|
||||||
it('renders leave button', () => {
|
it('renders leave dropdown with correct text', () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
isCurrentUser: true,
|
isCurrentUser: true,
|
||||||
permissions: {
|
permissions: {
|
||||||
|
|
@ -77,20 +97,22 @@ describe('UserActionButtons', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.findComponent(LeaveButton).exists()).toBe(true);
|
const leaveGroupDropdownItem = wrapper.findComponent(LeaveGroupDropdownItem);
|
||||||
|
expect(leaveGroupDropdownItem.exists()).toBe(true);
|
||||||
|
expect(leaveGroupDropdownItem.html()).toContain(I18N.leaveGroup);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when user does not have `canRemove` permissions', () => {
|
describe('when user does not have `canRemove` permissions', () => {
|
||||||
it('does not render remove member button', () => {
|
it('does not render remove member dropdown', () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
permissions: {
|
permissions: {
|
||||||
canRemove: false,
|
canRemove: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findRemoveMemberButton().exists()).toBe(false);
|
expect(findRemoveMemberDropdownItem().exists()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -108,7 +130,7 @@ describe('UserActionButtons', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets member type correctly', () => {
|
it('sets member type correctly', () => {
|
||||||
expect(findRemoveMemberButton().props().memberType).toBe('GroupMember');
|
expect(findRemoveMemberDropdownItem().props().memberType).toBe('GroupMember');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -126,7 +148,7 @@ describe('UserActionButtons', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets member type correctly', () => {
|
it('sets member type correctly', () => {
|
||||||
expect(findRemoveMemberButton().props().memberType).toBe('ProjectMember');
|
expect(findRemoveMemberDropdownItem().props().memberType).toBe('ProjectMember');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
|
||||||
import AccessRequestActionButtons from '~/members/components/action_buttons/access_request_action_buttons.vue';
|
import AccessRequestActionButtons from '~/members/components/action_buttons/access_request_action_buttons.vue';
|
||||||
import GroupActionButtons from '~/members/components/action_buttons/group_action_buttons.vue';
|
import GroupActionButtons from '~/members/components/action_buttons/group_action_buttons.vue';
|
||||||
import InviteActionButtons from '~/members/components/action_buttons/invite_action_buttons.vue';
|
import InviteActionButtons from '~/members/components/action_buttons/invite_action_buttons.vue';
|
||||||
import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
|
import UserActionDropdown from '~/members/components/action_dropdowns/user_action_dropdown.vue';
|
||||||
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
|
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
|
||||||
import { MEMBER_TYPES } from '~/members/constants';
|
import { MEMBER_TYPES } from '~/members/constants';
|
||||||
import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
|
import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
|
||||||
|
|
@ -29,7 +29,7 @@ describe('MemberActionButtons', () => {
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
memberType | member | expectedComponent | expectedComponentName
|
memberType | member | expectedComponent | expectedComponentName
|
||||||
${MEMBER_TYPES.user} | ${memberMock} | ${UserActionButtons} | ${'UserActionButtons'}
|
${MEMBER_TYPES.user} | ${memberMock} | ${UserActionDropdown} | ${'UserActionDropdown'}
|
||||||
${MEMBER_TYPES.group} | ${group} | ${GroupActionButtons} | ${'GroupActionButtons'}
|
${MEMBER_TYPES.group} | ${group} | ${GroupActionButtons} | ${'GroupActionButtons'}
|
||||||
${MEMBER_TYPES.invite} | ${invite} | ${InviteActionButtons} | ${'InviteActionButtons'}
|
${MEMBER_TYPES.invite} | ${invite} | ${InviteActionButtons} | ${'InviteActionButtons'}
|
||||||
${MEMBER_TYPES.accessRequest} | ${accessRequest} | ${AccessRequestActionButtons} | ${'AccessRequestActionButtons'}
|
${MEMBER_TYPES.accessRequest} | ${accessRequest} | ${AccessRequestActionButtons} | ${'AccessRequestActionButtons'}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,22 @@ module Spec
|
||||||
click_button 'Search'
|
click_button 'Search'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user_action_dropdown
|
||||||
|
'[data-testid="user-action-dropdown"]'
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_actions
|
||||||
|
within user_action_dropdown do
|
||||||
|
find('button').click
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_actions_for_username(user)
|
||||||
|
within find_username_row(user) do
|
||||||
|
show_actions
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue