Commit 4d1d361a authored by Coung Ngo's avatar Coung Ngo Committed by cngo

Refactor `RemoveMemberModal` into members Vue app

Before this change, `RemoveMemberModal` was mounted on the page
separately from the Vue app. `RemoveMemberModal` listened for
clicks of the remove member button and read data from the button's
HTML dataset.

This change moves `RemoveMemberModal` into the Vue app. Clicking
the remove member button now directly opens `RemoveMemberModal`.
`RemoveMemberModal` reads data from the Vue app's Vuex store,
in keeping with the app's state architecture.
parent 510b8752
<script> <script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
export default { export default {
name: 'RemoveMemberButton', name: 'RemoveMemberButton',
...@@ -45,7 +45,7 @@ export default { ...@@ -45,7 +45,7 @@ export default {
oncallSchedules: { oncallSchedules: {
type: Object, type: Object,
required: false, required: false,
default: () => {}, default: () => ({}),
}, },
}, },
computed: { computed: {
...@@ -54,30 +54,35 @@ export default { ...@@ -54,30 +54,35 @@ export default {
return state[this.namespace].memberPath; return state[this.namespace].memberPath;
}, },
}), }),
computedMemberPath() { modalData() {
return this.memberPath.replace(':id', this.memberId); return {
}, isAccessRequest: this.isAccessRequest,
stringifiedSchedules() { isInvite: this.isInvite,
return JSON.stringify(this.oncallSchedules); memberPath: this.memberPath.replace(':id', this.memberId),
memberType: this.memberType,
message: this.message,
oncallSchedules: this.oncallSchedules,
};
}, },
}, },
methods: {
...mapActions({
showRemoveMemberModal(dispatch, payload) {
return dispatch(`${this.namespace}/showRemoveMemberModal`, payload);
},
}),
},
}; };
</script> </script>
<template> <template>
<gl-button <gl-button
v-gl-tooltip.hover v-gl-tooltip
class="js-remove-member-button"
variant="danger" variant="danger"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
:icon="icon" :icon="icon"
:data-member-path="computedMemberPath"
:data-member-type="memberType"
:data-is-access-request="isAccessRequest"
:data-is-invite="isInvite"
:data-message="message"
:data-oncall-schedules="stringifiedSchedules"
data-qa-selector="delete_member_button" data-qa-selector="delete_member_button"
@click="showRemoveMemberModal(modalData)"
/> />
</template> </template>
<script> <script>
import { GlFormCheckbox, GlModal } from '@gitlab/ui'; import { GlFormCheckbox, GlModal } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import { mapActions, mapState } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
...@@ -16,20 +15,33 @@ export default { ...@@ -16,20 +15,33 @@ export default {
GlModal, GlModal,
OncallSchedulesList, OncallSchedulesList,
}, },
data() { inject: ['namespace'],
return {
modalData: {},
};
},
computed: { computed: {
isAccessRequest() { ...mapState({
return parseBoolean(this.modalData.isAccessRequest); isAccessRequest(state) {
}, return state[this.namespace].removeMemberModalData.isAccessRequest;
isInvite() { },
return parseBoolean(this.modalData.isInvite); isInvite(state) {
}, return state[this.namespace].removeMemberModalData.isInvite;
},
memberPath(state) {
return state[this.namespace].removeMemberModalData.memberPath;
},
memberType(state) {
return state[this.namespace].removeMemberModalData.memberType;
},
message(state) {
return state[this.namespace].removeMemberModalData.message;
},
oncallSchedules(state) {
return state[this.namespace].removeMemberModalData.oncallSchedules ?? {};
},
removeMemberModalVisible(state) {
return state[this.namespace].removeMemberModalVisible;
},
}),
isGroupMember() { isGroupMember() {
return this.modalData.memberType === 'GroupMember'; return this.memberType === 'GroupMember';
}, },
actionText() { actionText() {
if (this.isAccessRequest) { if (this.isAccessRequest) {
...@@ -54,29 +66,13 @@ export default { ...@@ -54,29 +66,13 @@ export default {
isPartOfOncallSchedules() { isPartOfOncallSchedules() {
return !this.isAccessRequest && this.oncallSchedules.schedules?.length; return !this.isAccessRequest && this.oncallSchedules.schedules?.length;
}, },
oncallSchedules() {
try {
return JSON.parse(this.modalData.oncallSchedules);
} catch (e) {
Sentry.captureException(e);
}
return {};
},
},
mounted() {
document.addEventListener('click', this.handleClick);
},
beforeDestroy() {
document.removeEventListener('click', this.handleClick);
}, },
methods: { methods: {
handleClick(event) { ...mapActions({
const removeButton = event.target.closest('.js-remove-member-button'); hideRemoveMemberModal(dispatch) {
if (removeButton) { return dispatch(`${this.namespace}/hideRemoveMemberModal`);
this.modalData = removeButton.dataset; },
this.$refs.modal.show(); }),
}
},
submitForm() { submitForm() {
this.$refs.form.submit(); this.$refs.form.submit();
}, },
...@@ -91,11 +87,13 @@ export default { ...@@ -91,11 +87,13 @@ export default {
:action-cancel="$options.actionCancel" :action-cancel="$options.actionCancel"
:action-primary="actionPrimary" :action-primary="actionPrimary"
:title="actionText" :title="actionText"
:visible="removeMemberModalVisible"
data-qa-selector="remove_member_modal_content" data-qa-selector="remove_member_modal_content"
@primary="submitForm" @primary="submitForm"
@hide="hideRemoveMemberModal"
> >
<form ref="form" :action="modalData.memberPath" method="post"> <form ref="form" :action="memberPath" method="post">
<p data-testid="modal-message">{{ modalData.message }}</p> <p>{{ message }}</p>
<oncall-schedules-list <oncall-schedules-list
v-if="isPartOfOncallSchedules" v-if="isPartOfOncallSchedules"
......
...@@ -7,6 +7,7 @@ import { mergeUrlParams } from '~/lib/utils/url_utility'; ...@@ -7,6 +7,7 @@ import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers'; import initUserPopovers from '~/user_popovers';
import { FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME } from '../../constants'; import { FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME } from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue'; import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import RemoveMemberModal from '../modals/remove_member_modal.vue';
import CreatedAt from './created_at.vue'; import CreatedAt from './created_at.vue';
import ExpirationDatepicker from './expiration_datepicker.vue'; import ExpirationDatepicker from './expiration_datepicker.vue';
import ExpiresAt from './expires_at.vue'; import ExpiresAt from './expires_at.vue';
...@@ -29,6 +30,7 @@ export default { ...@@ -29,6 +30,7 @@ export default {
MemberActionButtons, MemberActionButtons,
RoleDropdown, RoleDropdown,
RemoveGroupLinkModal, RemoveGroupLinkModal,
RemoveMemberModal,
ExpirationDatepicker, ExpirationDatepicker,
LdapOverrideConfirmationModal: () => LdapOverrideConfirmationModal: () =>
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'), import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
...@@ -225,6 +227,7 @@ export default { ...@@ -225,6 +227,7 @@ export default {
align="center" align="center"
/> />
<remove-group-link-modal /> <remove-group-link-modal />
<remove-member-modal />
<ldap-override-confirmation-modal /> <ldap-override-confirmation-modal />
</div> </div>
</template> </template>
...@@ -25,6 +25,14 @@ export const hideRemoveGroupLinkModal = ({ commit }) => { ...@@ -25,6 +25,14 @@ export const hideRemoveGroupLinkModal = ({ commit }) => {
commit(types.HIDE_REMOVE_GROUP_LINK_MODAL); commit(types.HIDE_REMOVE_GROUP_LINK_MODAL);
}; };
export const showRemoveMemberModal = ({ commit }, modalData) => {
commit(types.SHOW_REMOVE_MEMBER_MODAL, modalData);
};
export const hideRemoveMemberModal = ({ commit }) => {
commit(types.HIDE_REMOVE_MEMBER_MODAL);
};
export const updateMemberExpiration = async ({ state, commit }, { memberId, expiresAt }) => { export const updateMemberExpiration = async ({ state, commit }, { memberId, expiresAt }) => {
try { try {
await axios.put( await axios.put(
......
...@@ -8,3 +8,6 @@ export const HIDE_ERROR = 'HIDE_ERROR'; ...@@ -8,3 +8,6 @@ export const HIDE_ERROR = 'HIDE_ERROR';
export const SHOW_REMOVE_GROUP_LINK_MODAL = 'SHOW_REMOVE_GROUP_LINK_MODAL'; export const SHOW_REMOVE_GROUP_LINK_MODAL = 'SHOW_REMOVE_GROUP_LINK_MODAL';
export const HIDE_REMOVE_GROUP_LINK_MODAL = 'HIDE_REMOVE_GROUP_LINK_MODAL'; export const HIDE_REMOVE_GROUP_LINK_MODAL = 'HIDE_REMOVE_GROUP_LINK_MODAL';
export const SHOW_REMOVE_MEMBER_MODAL = 'SHOW_REMOVE_MEMBER_MODAL';
export const HIDE_REMOVE_MEMBER_MODAL = 'HIDE_REMOVE_MEMBER_MODAL';
...@@ -47,4 +47,11 @@ export default { ...@@ -47,4 +47,11 @@ export default {
[types.HIDE_REMOVE_GROUP_LINK_MODAL](state) { [types.HIDE_REMOVE_GROUP_LINK_MODAL](state) {
state.removeGroupLinkModalVisible = false; state.removeGroupLinkModalVisible = false;
}, },
[types.SHOW_REMOVE_MEMBER_MODAL](state, modalData) {
state.removeMemberModalData = modalData;
state.removeMemberModalVisible = true;
},
[types.HIDE_REMOVE_MEMBER_MODAL](state) {
state.removeMemberModalVisible = false;
},
}; };
...@@ -20,4 +20,6 @@ export default ({ ...@@ -20,4 +20,6 @@ export default ({
errorMessage: '', errorMessage: '',
removeGroupLinkModalVisible: false, removeGroupLinkModalVisible: false,
groupLinkToRemove: null, groupLinkToRemove: null,
removeMemberModalData: {},
removeMemberModalVisible: false,
}); });
import Vue from 'vue';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
function mountRemoveMemberModal() { new UsersSelect(); // eslint-disable-line no-new
const el = document.querySelector('.js-remove-member-modal');
if (!el) {
return false;
}
return new Vue({
el,
render(createComponent) {
return createComponent(RemoveMemberModal);
},
});
}
document.addEventListener('DOMContentLoaded', () => {
mountRemoveMemberModal();
new UsersSelect(); // eslint-disable-line no-new
});
import Vue from 'vue';
import NamespaceSelect from '~/namespace_select'; import NamespaceSelect from '~/namespace_select';
import ProjectsList from '~/projects_list'; import ProjectsList from '~/projects_list';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
if (!el) {
return false;
}
return new Vue({
el,
render(createComponent) {
return createComponent(RemoveMemberModal);
},
});
}
mountRemoveMemberModal();
new ProjectsList(); // eslint-disable-line no-new new ProjectsList(); // eslint-disable-line no-new
......
import Vue from 'vue';
import { groupMemberRequestFormatter } from '~/groups/members/utils'; import { groupMemberRequestFormatter } from '~/groups/members/utils';
import groupsSelect from '~/groups_select'; import groupsSelect from '~/groups_select';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger'; import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
...@@ -11,21 +10,6 @@ import { initMembersApp } from '~/members'; ...@@ -11,21 +10,6 @@ import { initMembersApp } from '~/members';
import { MEMBER_TYPES } from '~/members/constants'; import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils'; import { groupLinkRequestFormatter } from '~/members/utils';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
if (!el) {
return false;
}
return new Vue({
el,
render(createComponent) {
return createComponent(RemoveMemberModal);
},
});
}
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions']; const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
...@@ -71,7 +55,6 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), { ...@@ -71,7 +55,6 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), {
groupsSelect(); groupsSelect();
memberExpirationDate(); memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups'); memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
initInviteMembersModal(); initInviteMembersModal();
initInviteMembersTrigger(); initInviteMembersTrigger();
initInviteGroupTrigger(); initInviteGroupTrigger();
......
import Vue from 'vue';
import groupsSelect from '~/groups_select'; import groupsSelect from '~/groups_select';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger'; import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
import initInviteMembersForm from '~/invite_members/init_invite_members_form'; import initInviteMembersForm from '~/invite_members/init_invite_members_form';
...@@ -11,26 +10,10 @@ import { MEMBER_TYPES } from '~/members/constants'; ...@@ -11,26 +10,10 @@ import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils'; import { groupLinkRequestFormatter } from '~/members/utils';
import { projectMemberRequestFormatter } from '~/projects/members/utils'; import { projectMemberRequestFormatter } from '~/projects/members/utils';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
if (!el) {
return false;
}
return new Vue({
el,
render(createComponent) {
return createComponent(RemoveMemberModal);
},
});
}
groupsSelect(); groupsSelect();
memberExpirationDate(); memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups'); memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
initInviteMembersModal(); initInviteMembersModal();
initInviteMembersTrigger(); initInviteMembersTrigger();
initInviteGroupTrigger(); initInviteGroupTrigger();
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
- page_title @group.name, _("Groups") - page_title @group.name, _("Groups")
- current_user_is_group_owner = @group && @group.has_owner?(current_user) - current_user_is_group_owner = @group && @group.has_owner?(current_user)
.js-remove-member-modal
%h3.page-title %h3.page-title
= _('Group: %{group_name}') % { group_name: @group.full_name } = _('Group: %{group_name}') % { group_name: @group.full_name }
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
- @content_class = "admin-projects" - @content_class = "admin-projects"
- current_user_is_group_owner = @group && @group.has_owner?(current_user) - current_user_is_group_owner = @group && @group.has_owner?(current_user)
.js-remove-member-modal
%h3.page-title %h3.page-title
= _('Project: %{name}') % { name: @project.full_name } = _('Project: %{name}') % { name: @project.full_name }
= link_to edit_project_path(@project), class: "btn btn-default gl-button float-right" do = link_to edit_project_path(@project), class: "btn btn-default gl-button float-right" do
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
- page_title _('Group members') - page_title _('Group members')
- groups_select_tag_data = group_select_data(@group).merge({ skip_groups: @skip_groups }) - groups_select_tag_data = group_select_data(@group).merge({ skip_groups: @skip_groups })
.js-remove-member-modal
.row.gl-mt-3 .row.gl-mt-3
.col-lg-12 .col-lg-12
.gl-display-flex.gl-flex-wrap .gl-display-flex.gl-flex-wrap
......
- add_page_specific_style 'page_bundles/members' - add_page_specific_style 'page_bundles/members'
- page_title _("Members") - page_title _("Members")
.js-remove-member-modal
.row.gl-mt-3 .row.gl-mt-3
.col-lg-12 .col-lg-12
- if can_invite_members_for_project?(@project) || can_invite_group_for_project?(@project) - if can_invite_members_for_project?(@project) || can_invite_group_for_project?(@project)
......
...@@ -48,6 +48,7 @@ describe('MemberList', () => { ...@@ -48,6 +48,7 @@ describe('MemberList', () => {
'member-action-buttons', 'member-action-buttons',
'role-dropdown', 'role-dropdown',
'remove-group-link-modal', 'remove-group-link-modal',
'remove-member-modal',
'expiration-datepicker', 'expiration-datepicker',
], ],
}); });
......
...@@ -7,7 +7,7 @@ module QA ...@@ -7,7 +7,7 @@ module QA
include Page::Component::InviteMembersModal include Page::Component::InviteMembersModal
include Page::Component::UsersSelect include Page::Component::UsersSelect
view 'app/assets/javascripts/vue_shared/components/remove_member_modal.vue' do view 'app/assets/javascripts/members/components/modals/remove_member_modal.vue' do
element :remove_member_modal_content element :remove_member_modal_content
end end
......
...@@ -37,7 +37,7 @@ describe('InviteActionButtons', () => { ...@@ -37,7 +37,7 @@ describe('InviteActionButtons', () => {
}); });
it('sets props correctly', () => { it('sets props correctly', () => {
expect(findRemoveMemberButton().props()).toEqual({ expect(findRemoveMemberButton().props()).toMatchObject({
memberId: member.id, memberId: member.id,
memberType: null, memberType: null,
message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.fullName}"`, message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.fullName}"`,
......
import { GlButton } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { modalData } from 'jest/members/mock_data';
import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue'; import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
import { MEMBER_TYPES } from '~/members/constants'; import { MEMBER_TYPES } from '~/members/constants';
...@@ -10,6 +12,10 @@ localVue.use(Vuex); ...@@ -10,6 +12,10 @@ localVue.use(Vuex);
describe('RemoveMemberButton', () => { describe('RemoveMemberButton', () => {
let wrapper; let wrapper;
const actions = {
showRemoveMemberModal: jest.fn(),
};
const createStore = (state = {}) => { const createStore = (state = {}) => {
return new Vuex.Store({ return new Vuex.Store({
modules: { modules: {
...@@ -19,6 +25,7 @@ describe('RemoveMemberButton', () => { ...@@ -19,6 +25,7 @@ describe('RemoveMemberButton', () => {
memberPath: '/groups/foo-bar/-/group_members/:id', memberPath: '/groups/foo-bar/-/group_members/:id',
...state, ...state,
}, },
actions,
}, },
}, },
}); });
...@@ -47,20 +54,16 @@ describe('RemoveMemberButton', () => { ...@@ -47,20 +54,16 @@ describe('RemoveMemberButton', () => {
}); });
}; };
beforeEach(() => {
createComponent();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('sets attributes on button', () => { it('sets attributes on button', () => {
createComponent();
expect(wrapper.attributes()).toMatchObject({ expect(wrapper.attributes()).toMatchObject({
'data-member-path': '/groups/foo-bar/-/group_members/1',
'data-member-type': 'GroupMember',
'data-message': 'Are you sure you want to remove John Smith?',
'data-is-access-request': 'true',
'data-is-invite': 'true',
'data-oncall-schedules': '{"name":"user","schedules":[]}',
'aria-label': 'Remove member', 'aria-label': 'Remove member',
title: 'Remove member', title: 'Remove member',
icon: 'remove', icon: 'remove',
...@@ -68,14 +71,12 @@ describe('RemoveMemberButton', () => { ...@@ -68,14 +71,12 @@ describe('RemoveMemberButton', () => {
}); });
it('displays `title` prop as a tooltip', () => { it('displays `title` prop as a tooltip', () => {
createComponent();
expect(getBinding(wrapper.element, 'gl-tooltip')).not.toBeUndefined(); expect(getBinding(wrapper.element, 'gl-tooltip')).not.toBeUndefined();
}); });
it('has CSS class used by `remove_member_modal.vue`', () => { it('calls Vuex action to show `remove member` modal when clicked', () => {
createComponent(); wrapper.findComponent(GlButton).vm.$emit('click');
expect(wrapper.classes()).toContain('js-remove-member-button'); expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), modalData);
}); });
}); });
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import RemoveMemberModal from '~/members/components/modals/remove_member_modal.vue';
import { MEMBER_TYPES } from '~/members/constants';
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
const mockSchedules = JSON.stringify({ Vue.use(Vuex);
schedules: [
{
id: 1,
name: 'Schedule 1',
},
],
name: 'User1',
});
describe('RemoveMemberModal', () => { describe('RemoveMemberModal', () => {
const memberPath = '/gitlab-org/gitlab-test/-/project_members/90'; const memberPath = '/gitlab-org/gitlab-test/-/project_members/90';
const mockSchedules = {
name: 'User1',
schedules: [{ id: 1, name: 'Schedule 1' }],
};
let wrapper; let wrapper;
const actions = {
hideRemoveMemberModal: jest.fn(),
};
const createStore = (removeMemberModalData) =>
new Vuex.Store({
modules: {
[MEMBER_TYPES.user]: {
namespaced: true,
state: {
removeMemberModalData,
},
actions,
},
},
});
const createComponent = (state) => {
wrapper = shallowMount(RemoveMemberModal, {
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.user,
},
});
};
const findForm = () => wrapper.find({ ref: 'form' }); const findForm = () => wrapper.find({ ref: 'form' });
const findGlModal = () => wrapper.findComponent(GlModal); const findGlModal = () => wrapper.findComponent(GlModal);
const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList); const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe.each` describe.each`
state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | onCallSchedules state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | onCallSchedules
${'removing a group member'} | ${'GroupMember'} | ${false} | ${'false'} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${`{}`} ${'removing a group member'} | ${'GroupMember'} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}}
${'removing a project member'} | ${'ProjectMember'} | ${false} | ${'false'} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockSchedules} ${'removing a project member'} | ${'ProjectMember'} | ${false} | ${false} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockSchedules}
${'denying an access request'} | ${'ProjectMember'} | ${true} | ${'false'} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${`{}`} ${'denying an access request'} | ${'ProjectMember'} | ${true} | ${false} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${{}}
${'revoking invite'} | ${'ProjectMember'} | ${false} | ${'true'} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockSchedules} ${'revoking invite'} | ${'ProjectMember'} | ${false} | ${true} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockSchedules}
`( `(
'when $state', 'when $state',
({ ({
...@@ -45,24 +69,17 @@ describe('RemoveMemberModal', () => { ...@@ -45,24 +69,17 @@ describe('RemoveMemberModal', () => {
onCallSchedules, onCallSchedules,
}) => { }) => {
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(RemoveMemberModal, { createComponent({
data() { isAccessRequest,
return { isInvite,
modalData: { message,
isAccessRequest, memberPath,
isInvite, memberType,
message, onCallSchedules,
memberPath,
memberType,
onCallSchedules,
},
};
},
}); });
}); });
const parsedSchedules = JSON.parse(onCallSchedules); const isPartOfOncallSchedules = Boolean(isAccessRequest && onCallSchedules.schedules?.length);
const isPartOfOncallSchedules = Boolean(isAccessRequest && parsedSchedules.schedules?.length);
it(`has the title ${actionText}`, () => { it(`has the title ${actionText}`, () => {
expect(findGlModal().attributes('title')).toBe(actionText); expect(findGlModal().attributes('title')).toBe(actionText);
...@@ -73,7 +90,7 @@ describe('RemoveMemberModal', () => { ...@@ -73,7 +90,7 @@ describe('RemoveMemberModal', () => {
}); });
it('displays a message to the user', () => { it('displays a message to the user', () => {
expect(wrapper.find('[data-testid=modal-message]').text()).toBe(message); expect(wrapper.find('p').text()).toBe(message);
}); });
it(`shows ${ it(`shows ${
...@@ -105,6 +122,12 @@ describe('RemoveMemberModal', () => { ...@@ -105,6 +122,12 @@ describe('RemoveMemberModal', () => {
spy.mockRestore(); spy.mockRestore();
}); });
it('calls Vuex action to hide the modal when `GlModal` emits `hide` event', () => {
findGlModal().vm.$emit('hide');
expect(actions.hideRemoveMemberModal).toHaveBeenCalled();
});
}, },
); );
}); });
...@@ -72,6 +72,7 @@ describe('MembersTable', () => { ...@@ -72,6 +72,7 @@ describe('MembersTable', () => {
'member-action-buttons', 'member-action-buttons',
'role-dropdown', 'role-dropdown',
'remove-group-link-modal', 'remove-group-link-modal',
'remove-member-modal',
'expiration-datepicker', 'expiration-datepicker',
], ],
}); });
......
...@@ -57,6 +57,15 @@ export const group = { ...@@ -57,6 +57,15 @@ export const group = {
validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 }, validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
}; };
export const modalData = {
isAccessRequest: true,
isInvite: true,
memberPath: '/groups/foo-bar/-/group_members/1',
memberType: 'GroupMember',
message: 'Are you sure you want to remove John Smith?',
oncallSchedules: { name: 'user', schedules: [] },
};
const { user, ...memberNoUser } = member; const { user, ...memberNoUser } = member;
export const invite = { export const invite = {
...memberNoUser, ...memberNoUser,
......
...@@ -3,12 +3,14 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -3,12 +3,14 @@ import MockAdapter from 'axios-mock-adapter';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { members, group } from 'jest/members/mock_data'; import { members, group, modalData } from 'jest/members/mock_data';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { import {
updateMemberRole, updateMemberRole,
showRemoveGroupLinkModal, showRemoveGroupLinkModal,
hideRemoveGroupLinkModal, hideRemoveGroupLinkModal,
showRemoveMemberModal,
hideRemoveMemberModal,
updateMemberExpiration, updateMemberExpiration,
} from '~/members/store/actions'; } from '~/members/store/actions';
import * as types from '~/members/store/mutation_types'; import * as types from '~/members/store/mutation_types';
...@@ -153,4 +155,32 @@ describe('Vuex members actions', () => { ...@@ -153,4 +155,32 @@ describe('Vuex members actions', () => {
}); });
}); });
}); });
describe('Remove member modal', () => {
const state = {
removeMemberModalVisible: false,
removeMemberModalData: {},
};
describe('showRemoveMemberModal', () => {
it(`commits ${types.SHOW_REMOVE_MEMBER_MODAL} mutation`, () => {
testAction(showRemoveMemberModal, modalData, state, [
{
type: types.SHOW_REMOVE_MEMBER_MODAL,
payload: modalData,
},
]);
});
});
describe('hideRemoveMemberModal', () => {
it(`commits ${types.HIDE_REMOVE_MEMBER_MODAL} mutation`, () => {
testAction(hideRemoveMemberModal, undefined, state, [
{
type: types.HIDE_REMOVE_MEMBER_MODAL,
},
]);
});
});
});
}); });
import { members, group } from 'jest/members/mock_data'; import { members, group, modalData } from 'jest/members/mock_data';
import * as types from '~/members/store/mutation_types'; import * as types from '~/members/store/mutation_types';
import mutations from '~/members/store/mutations'; import mutations from '~/members/store/mutations';
...@@ -154,4 +154,32 @@ describe('Vuex members mutations', () => { ...@@ -154,4 +154,32 @@ describe('Vuex members mutations', () => {
expect(state.removeGroupLinkModalVisible).toBe(false); expect(state.removeGroupLinkModalVisible).toBe(false);
}); });
}); });
describe(types.SHOW_REMOVE_MEMBER_MODAL, () => {
it('sets `removeMemberModalVisible` and `removeMemberModalData`', () => {
const state = {
removeMemberModalVisible: false,
removeMemberModalData: {},
};
mutations[types.SHOW_REMOVE_MEMBER_MODAL](state, modalData);
expect(state).toEqual({
removeMemberModalVisible: true,
removeMemberModalData: modalData,
});
});
});
describe(types.HIDE_REMOVE_MEMBER_MODAL, () => {
it('sets `removeMemberModalVisible` to `false`', () => {
const state = {
removeMemberModalVisible: true,
};
mutations[types.HIDE_REMOVE_MEMBER_MODAL](state);
expect(state.removeMemberModalVisible).toBe(false);
});
});
}); });
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