Commit 3f7be75b authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '329940-fix-remove-user-in-seat-usage' into 'master'

Make remove user buttons consistent in admin and groups dashboards

See merge request gitlab-org/gitlab!75497
parents 2d919014 d3c2516d
...@@ -112,7 +112,7 @@ export default { ...@@ -112,7 +112,7 @@ export default {
right right
:text="$options.i18n.userAdministration" :text="$options.i18n.userAdministration"
:text-sr-only="!showButtonLabels" :text-sr-only="!showButtonLabels"
icon="settings" icon="ellipsis_h"
data-qa-selector="user_actions_dropdown_toggle" data-qa-selector="user_actions_dropdown_toggle"
:data-qa-username="user.username" :data-qa-username="user.username"
> >
......
...@@ -53,6 +53,7 @@ export default { ...@@ -53,6 +53,7 @@ export default {
:title="s__('Member|Deny access')" :title="s__('Member|Deny access')"
:is-access-request="true" :is-access-request="true"
icon="close" icon="close"
button-category="primary"
/> />
</div> </div>
</action-button-group> </action-button-group>
......
...@@ -41,6 +41,8 @@ export default { ...@@ -41,6 +41,8 @@ export default {
<remove-member-button <remove-member-button
:member-id="member.id" :member-id="member.id"
:message="message" :message="message"
icon="remove"
button-category="primary"
:title="s__('Member|Revoke invite')" :title="s__('Member|Revoke invite')"
is-invite is-invite
/> />
......
...@@ -30,7 +30,17 @@ export default { ...@@ -30,7 +30,17 @@ export default {
icon: { icon: {
type: String, type: String,
required: false, required: false,
default: 'remove', default: undefined,
},
buttonText: {
type: String,
required: false,
default: '',
},
buttonCategory: {
type: String,
required: false,
default: 'secondary',
}, },
isAccessRequest: { isAccessRequest: {
type: Boolean, type: Boolean,
...@@ -79,10 +89,12 @@ export default { ...@@ -79,10 +89,12 @@ export default {
<gl-button <gl-button
v-gl-tooltip v-gl-tooltip
variant="danger" variant="danger"
:category="buttonCategory"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
:icon="icon" :icon="icon"
data-qa-selector="delete_member_button" data-qa-selector="delete_member_button"
@click="showRemoveMemberModal(modalData)" @click="showRemoveMemberModal(modalData)"
/> ><template v-if="buttonText">{{ buttonText }}</template></gl-button
>
</template> </template>
<script> <script>
import { s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils'; import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import ActionButtonGroup from './action_button_group.vue'; import ActionButtonGroup from './action_button_group.vue';
import LeaveButton from './leave_button.vue'; import LeaveButton from './leave_button.vue';
...@@ -23,6 +23,10 @@ export default { ...@@ -23,6 +23,10 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
isInvitedUser: {
type: Boolean,
required: true,
},
permissions: { permissions: {
type: Object, type: Object,
required: true, required: true,
...@@ -56,6 +60,15 @@ export default { ...@@ -56,6 +60,15 @@ export default {
obstacles: parseUserDeletionObstacles(this.member.user), obstacles: parseUserDeletionObstacles(this.member.user),
}; };
}, },
removeMemberButtonText() {
return this.isInvitedUser ? null : __('Remove user');
},
removeMemberButtonIcon() {
return this.isInvitedUser ? 'remove' : '';
},
removeMemberButtonCategory() {
return this.isInvitedUser ? 'primary' : 'secondary';
},
}, },
}; };
</script> </script>
...@@ -70,6 +83,9 @@ export default { ...@@ -70,6 +83,9 @@ export default {
:member-type="member.type" :member-type="member.type"
:user-deletion-obstacles="userDeletionObstaclesUserData" :user-deletion-obstacles="userDeletionObstaclesUserData"
:message="message" :message="message"
:icon="removeMemberButtonIcon"
:button-text="removeMemberButtonText"
:button-category="removeMemberButtonCategory"
:title="s__('Member|Remove member')" :title="s__('Member|Remove member')"
/> />
</div> </div>
......
...@@ -30,6 +30,10 @@ export default { ...@@ -30,6 +30,10 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
isInvitedUser: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
actionButtonComponent() { actionButtonComponent() {
...@@ -53,5 +57,6 @@ export default { ...@@ -53,5 +57,6 @@ export default {
:member="member" :member="member"
:permissions="permissions" :permissions="permissions"
:is-current-user="isCurrentUser" :is-current-user="isCurrentUser"
:is-invited-user="isInvitedUser"
/> />
</template> </template>
...@@ -8,6 +8,7 @@ import initUserPopovers from '~/user_popovers'; ...@@ -8,6 +8,7 @@ import initUserPopovers from '~/user_popovers';
import { import {
FIELDS, FIELDS,
ACTIVE_TAB_QUERY_PARAM_NAME, ACTIVE_TAB_QUERY_PARAM_NAME,
TAB_QUERY_PARAM_VALUES,
MEMBER_STATE_AWAITING, MEMBER_STATE_AWAITING,
USER_STATE_BLOCKED_PENDING_APPROVAL, USER_STATE_BLOCKED_PENDING_APPROVAL,
BADGE_LABELS_PENDING_OWNER_APPROVAL, BADGE_LABELS_PENDING_OWNER_APPROVAL,
...@@ -82,6 +83,9 @@ export default { ...@@ -82,6 +83,9 @@ export default {
return paramName && currentPage && perPage && totalItems; return paramName && currentPage && perPage && totalItems;
}, },
isInvitedUser() {
return this.tabQueryParamValue === TAB_QUERY_PARAM_VALUES.invite;
},
}, },
mounted() { mounted() {
initUserPopovers(this.$el.querySelectorAll('.js-user-link')); initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
...@@ -275,6 +279,7 @@ export default { ...@@ -275,6 +279,7 @@ export default {
<member-action-buttons <member-action-buttons
:member-type="memberType" :member-type="memberType"
:is-current-user="isCurrentUser" :is-current-user="isCurrentUser"
:is-invited-user="isInvitedUser"
:permissions="permissions" :permissions="permissions"
:member="member" :member="member"
/> />
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
%span.light.vertical-align-middle= group_member.human_access %span.light.vertical-align-middle= group_member.human_access
- unless group_member.owner? - unless group_member.owner?
= link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member), testid: 'remove-user' }, method: :delete, remote: true, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from group') do = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member), testid: 'remove-user' }, method: :delete, remote: true, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from group') do
= sprite_icon('close', size: 16, css_class: 'gl-icon') = sprite_icon('remove', size: 16, css_class: 'gl-icon')
.row .row
.col-md-6 .col-md-6
...@@ -47,6 +47,6 @@ ...@@ -47,6 +47,6 @@
- if member.respond_to? :project - if member.respond_to? :project
= link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from project') do = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from project') do
= sprite_icon('close', size: 16, css_class: 'gl-icon') = sprite_icon('remove', size: 16, css_class: 'gl-icon')
= render partial: 'admin/users/modals' = render partial: 'admin/users/modals'
...@@ -5,8 +5,6 @@ import { ...@@ -5,8 +5,6 @@ import {
GlAvatarLink, GlAvatarLink,
GlBadge, GlBadge,
GlButton, GlButton,
GlDropdown,
GlDropdownItem,
GlModal, GlModal,
GlModalDirective, GlModalDirective,
GlIcon, GlIcon,
...@@ -41,8 +39,6 @@ export default { ...@@ -41,8 +39,6 @@ export default {
GlAvatarLink, GlAvatarLink,
GlBadge, GlBadge,
GlButton, GlButton,
GlDropdown,
GlDropdownItem,
GlModal, GlModal,
GlIcon, GlIcon,
GlPagination, GlPagination,
...@@ -265,15 +261,15 @@ export default { ...@@ -265,15 +261,15 @@ export default {
</template> </template>
<template #cell(actions)="data"> <template #cell(actions)="data">
<gl-dropdown icon="ellipsis_h" right data-testid="user-actions"> <gl-button
<gl-dropdown-item v-gl-modal="$options.removeBillableMemberModalId"
v-gl-modal="$options.removeBillableMemberModalId" category="secondary"
data-testid="remove-user" variant="danger"
@click="displayRemoveMemberModal(data.item.user)" data-testid="remove-user"
> @click="displayRemoveMemberModal(data.item.user)"
{{ __('Remove user') }} >
</gl-dropdown-item> {{ __('Remove user') }}
</gl-dropdown> </gl-button>
</template> </template>
<template #row-details="{ item }"> <template #row-details="{ item }">
......
...@@ -69,7 +69,6 @@ RSpec.describe 'Groups > Usage Quotas > Seat Usage', :js do ...@@ -69,7 +69,6 @@ RSpec.describe 'Groups > Usage Quotas > Seat Usage', :js do
context 'with a modal to confirm removal' do context 'with a modal to confirm removal' do
before do before do
within user_to_remove_row do within user_to_remove_row do
find('[data-testid="user-actions"]').click
click_button 'Remove user' click_button 'Remove user'
end end
end end
...@@ -106,7 +105,6 @@ RSpec.describe 'Groups > Usage Quotas > Seat Usage', :js do ...@@ -106,7 +105,6 @@ RSpec.describe 'Groups > Usage Quotas > Seat Usage', :js do
context 'removing the user' do context 'removing the user' do
before do before do
within user_to_remove_row do within user_to_remove_row do
find('[data-testid="user-actions"]').click
click_button 'Remove user' click_button 'Remove user'
end end
end end
...@@ -164,7 +162,6 @@ RSpec.describe 'Groups > Usage Quotas > Seat Usage', :js do ...@@ -164,7 +162,6 @@ RSpec.describe 'Groups > Usage Quotas > Seat Usage', :js do
it 'displays an error modal' do it 'displays an error modal' do
within shared_user_row do within shared_user_row do
find('[data-testid="user-actions"]').click
click_button 'Remove user' click_button 'Remove user'
end end
......
...@@ -16,6 +16,7 @@ describe('UserActionButtons', () => { ...@@ -16,6 +16,7 @@ describe('UserActionButtons', () => {
propsData: { propsData: {
member, member,
isCurrentUser: false, isCurrentUser: false,
isInvitedUser: false,
...propsData, ...propsData,
}, },
}); });
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
exports[`Subscription Seats renders table content renders the correct data 1`] = ` exports[`Subscription Seats renders table content renders the correct data 1`] = `
Array [ Array [
Object { Object {
"dropdownExists": true,
"email": "administrator@email.com", "email": "administrator@email.com",
"removeUserButtonExists": true,
"tooltip": undefined, "tooltip": undefined,
"user": Object { "user": Object {
"avatarLabeled": Object { "avatarLabeled": Object {
...@@ -19,8 +19,8 @@ Array [ ...@@ -19,8 +19,8 @@ Array [
}, },
}, },
Object { Object {
"dropdownExists": true,
"email": "agustin_walker@email.com", "email": "agustin_walker@email.com",
"removeUserButtonExists": true,
"tooltip": undefined, "tooltip": undefined,
"user": Object { "user": Object {
"avatarLabeled": Object { "avatarLabeled": Object {
...@@ -35,8 +35,8 @@ Array [ ...@@ -35,8 +35,8 @@ Array [
}, },
}, },
Object { Object {
"dropdownExists": true,
"email": "Private", "email": "Private",
"removeUserButtonExists": true,
"tooltip": "An email address is only visible for users with public emails.", "tooltip": "An email address is only visible for users with public emails.",
"user": Object { "user": Object {
"avatarLabeled": Object { "avatarLabeled": Object {
...@@ -53,8 +53,8 @@ Array [ ...@@ -53,8 +53,8 @@ Array [
}, },
}, },
Object { Object {
"dropdownExists": true,
"email": "jdoe@email.com", "email": "jdoe@email.com",
"removeUserButtonExists": true,
"tooltip": undefined, "tooltip": undefined,
"user": Object { "user": Object {
"avatarLabeled": Object { "avatarLabeled": Object {
......
import { import {
GlAlert, GlAlert,
GlPagination, GlPagination,
GlDropdown, GlButton,
GlTable, GlTable,
GlAvatarLink, GlAvatarLink,
GlAvatarLabeled, GlAvatarLabeled,
...@@ -107,7 +107,7 @@ describe('Subscription Seats', () => { ...@@ -107,7 +107,7 @@ describe('Subscription Seats', () => {
user: serializeUser(rowWrapper), user: serializeUser(rowWrapper),
email: emailWrapper.text(), email: emailWrapper.text(),
tooltip: emailWrapper.find('span').attributes('title'), tooltip: emailWrapper.find('span').attributes('title'),
dropdownExists: rowWrapper.findComponent(GlDropdown).exists(), removeUserButtonExists: rowWrapper.findComponent(GlButton).exists(),
}; };
}; };
......
...@@ -54,6 +54,8 @@ describe('RemoveMemberButton', () => { ...@@ -54,6 +54,8 @@ describe('RemoveMemberButton', () => {
}); });
}; };
const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
}); });
...@@ -66,7 +68,6 @@ describe('RemoveMemberButton', () => { ...@@ -66,7 +68,6 @@ describe('RemoveMemberButton', () => {
expect(wrapper.attributes()).toMatchObject({ expect(wrapper.attributes()).toMatchObject({
'aria-label': 'Remove member', 'aria-label': 'Remove member',
title: 'Remove member', title: 'Remove member',
icon: 'remove',
}); });
}); });
...@@ -75,8 +76,22 @@ describe('RemoveMemberButton', () => { ...@@ -75,8 +76,22 @@ describe('RemoveMemberButton', () => {
}); });
it('calls Vuex action to show `remove member` modal when clicked', () => { it('calls Vuex action to show `remove member` modal when clicked', () => {
wrapper.findComponent(GlButton).vm.$emit('click'); findButton().vm.$emit('click');
expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), modalData); expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), modalData);
}); });
describe('button optional properties', () => {
it('has default value for category and text', () => {
createComponent();
expect(findButton().props('category')).toBe('secondary');
expect(findButton().text()).toBe('');
});
it('allow changing value of button category and text', () => {
createComponent({ buttonCategory: 'primary', buttonText: 'Decline request' });
expect(findButton().props('category')).toBe('primary');
expect(findButton().text()).toBe('Decline request');
});
});
}); });
...@@ -13,6 +13,7 @@ describe('UserActionButtons', () => { ...@@ -13,6 +13,7 @@ describe('UserActionButtons', () => {
propsData: { propsData: {
member, member,
isCurrentUser: false, isCurrentUser: false,
isInvitedUser: false,
...propsData, ...propsData,
}, },
}); });
...@@ -45,7 +46,9 @@ describe('UserActionButtons', () => { ...@@ -45,7 +46,9 @@ describe('UserActionButtons', () => {
title: 'Remove member', title: 'Remove member',
isAccessRequest: false, isAccessRequest: false,
isInvite: false, isInvite: false,
icon: 'remove', icon: '',
buttonCategory: 'secondary',
buttonText: 'Remove user',
userDeletionObstacles: { userDeletionObstacles: {
name: member.user.name, name: member.user.name,
obstacles: parseUserDeletionObstacles(member.user), obstacles: parseUserDeletionObstacles(member.user),
...@@ -129,4 +132,30 @@ describe('UserActionButtons', () => { ...@@ -129,4 +132,30 @@ describe('UserActionButtons', () => {
expect(findRemoveMemberButton().props().memberType).toBe('ProjectMember'); expect(findRemoveMemberButton().props().memberType).toBe('ProjectMember');
}); });
}); });
describe('isInvitedUser', () => {
it.each`
isInvitedUser | icon | buttonText | buttonCategory
${true} | ${'remove'} | ${null} | ${'primary'}
${false} | ${''} | ${'Remove user'} | ${'secondary'}
`(
'passes the correct props to remove-member-button when isInvitedUser is $isInvitedUser',
({ isInvitedUser, icon, buttonText, buttonCategory }) => {
createComponent({
isInvitedUser,
permissions: {
canRemove: true,
},
});
expect(findRemoveMemberButton().props()).toEqual(
expect.objectContaining({
icon,
buttonText,
buttonCategory,
}),
);
},
);
});
}); });
...@@ -14,6 +14,7 @@ describe('MemberActionButtons', () => { ...@@ -14,6 +14,7 @@ describe('MemberActionButtons', () => {
wrapper = shallowMount(MemberActionButtons, { wrapper = shallowMount(MemberActionButtons, {
propsData: { propsData: {
isCurrentUser: false, isCurrentUser: false,
isInvitedUser: false,
permissions: { permissions: {
canRemove: true, canRemove: true,
}, },
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment