Commit 8b67d761 authored by Peter Hegman's avatar Peter Hegman Committed by Brandon Labuschagne

Add member leave button

Displayed when member is the current user. Part of a larger initiative
to convert the group members view from HAML to Vue
parent d762147b
<script>
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import LeaveModal from '../modals/leave_modal.vue';
import { LEAVE_MODAL_ID } from '../constants';
export default {
name: 'LeaveButton',
title: __('Leave'),
modalId: LEAVE_MODAL_ID,
components: {
GlButton,
LeaveModal,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
props: {
member: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div>
<gl-button
v-gl-tooltip.hover
v-gl-modal="$options.modalId"
:title="$options.title"
:aria-label="$options.title"
icon="leave"
variant="danger"
/>
<leave-modal :member="member" />
</div>
</template>
<script>
import ActionButtonGroup from './action_button_group.vue';
import RemoveMemberButton from './remove_member_button.vue';
import LeaveButton from './leave_button.vue';
import { s__, sprintf } from '~/locale';
export default {
name: 'UserActionButtons',
components: { ActionButtonGroup, RemoveMemberButton },
components: { ActionButtonGroup, RemoveMemberButton, LeaveButton },
props: {
member: {
type: Object,
......@@ -48,9 +49,7 @@ export default {
<template>
<action-button-group>
<div v-if="permissions.canRemove" class="gl-px-1">
<template v-if="isCurrentUser">
<!-- Leave button will go here -->
</template>
<leave-button v-if="isCurrentUser" :member="member" />
<remove-member-button
v-else
:member-id="member.id"
......
......@@ -64,3 +64,5 @@ export const MEMBER_TYPES = {
};
export const DAYS_TO_EXPIRE_SOON = 7;
export const LEAVE_MODAL_ID = 'member-leave-modal';
<script>
import { mapState } from 'vuex';
import { GlModal, GlForm, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
import { LEAVE_MODAL_ID } 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}"?'),
components: { GlModal, GlForm, GlSprintf },
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
member: {
type: Object,
required: true,
},
},
computed: {
...mapState(['memberPath']),
leavePath() {
return this.memberPath.replace(/:id$/, 'leave');
},
modalTitle() {
return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.name });
},
},
methods: {
handlePrimary() {
this.$refs.form.$el.submit();
},
},
};
</script>
<template>
<gl-modal
v-bind="$attrs"
:modal-id="$options.modalId"
:title="modalTitle"
:action-primary="$options.actionPrimary"
:action-cancel="$options.actionCancel"
size="sm"
@primary="handlePrimary"
>
<gl-form ref="form" :action="leavePath" method="post">
<p>
<gl-sprintf :message="$options.modalContent">
<template #source>{{ member.source.name }}</template>
</gl-sprintf>
</p>
<input type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
</gl-form>
</gl-modal>
</template>
......@@ -15951,6 +15951,9 @@ msgstr ""
msgid "Members|Are you sure you want to deny %{usersName}'s request to join \"%{source}\""
msgstr ""
msgid "Members|Are you sure you want to leave \"%{source}\"?"
msgstr ""
msgid "Members|Are you sure you want to remove %{usersName} from \"%{source}\""
msgstr ""
......@@ -15966,6 +15969,9 @@ msgstr ""
msgid "Members|Expired"
msgstr ""
msgid "Members|Leave \"%{source}\""
msgstr ""
msgid "Members|No expiration set"
msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import LeaveButton from '~/vue_shared/components/members/action_buttons/leave_button.vue';
import LeaveModal from '~/vue_shared/components/members/modals/leave_modal.vue';
import { LEAVE_MODAL_ID } from '~/vue_shared/components/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.find(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.find(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);
});
});
import { shallowMount } from '@vue/test-utils';
import UserActionButtons from '~/vue_shared/components/members/action_buttons/user_action_buttons.vue';
import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
import LeaveButton from '~/vue_shared/components/members/action_buttons/leave_button.vue';
import { member, orphanedMember } from '../mock_data';
describe('UserActionButtons', () => {
......@@ -59,6 +60,19 @@ describe('UserActionButtons', () => {
);
});
});
describe('when member is the current user', () => {
it('renders leave button', () => {
createComponent({
isCurrentUser: true,
permissions: {
canRemove: true,
},
});
expect(wrapper.find(LeaveButton).exists()).toBe(true);
});
});
});
describe('when user does not have `canRemove` permissions', () => {
......
import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import { GlModal, GlForm } from '@gitlab/ui';
import { nextTick } from 'vue';
import { within } from '@testing-library/dom';
import Vuex from 'vuex';
import LeaveModal from '~/vue_shared/components/members/modals/leave_modal.vue';
import { LEAVE_MODAL_ID } from '~/vue_shared/components/members/constants';
import { member } from '../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
const localVue = createLocalVue();
localVue.use(Vuex);
describe('LeaveModal', () => {
let wrapper;
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
memberPath: '/groups/foo-bar/-/group_members/:id',
...state,
},
});
};
const createComponent = (propsData = {}, state) => {
wrapper = mount(LeaveModal, {
localVue,
store: createStore(state),
propsData: {
member,
...propsData,
},
attrs: {
static: true,
visible: true,
},
});
};
const findModal = () => wrapper.find(GlModal);
const findForm = () => findModal().find(GlForm);
const getByText = (text, options) =>
createWrapper(within(findModal().element).getByText(text, options));
beforeEach(async () => {
createComponent();
await nextTick();
});
afterEach(() => {
wrapper.destroy();
});
it('sets modal ID', () => {
expect(findModal().props('modalId')).toBe(LEAVE_MODAL_ID);
});
it('displays modal title', () => {
expect(getByText(`Leave "${member.source.name}"`).exists()).toBe(true);
});
it('displays modal body', () => {
expect(getByText(`Are you sure you want to leave "${member.source.name}"?`).exists()).toBe(
true,
);
});
it('displays form with correct action and inputs', () => {
const form = findForm();
expect(form.attributes('action')).toBe('/groups/foo-bar/-/group_members/leave');
expect(form.find('input[name="_method"]').attributes('value')).toBe('delete');
expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(
'mock-csrf-token',
);
});
it('submits the form when "Leave" button is clicked', () => {
const submitSpy = jest.spyOn(findForm().element, 'submit');
getByText('Leave').trigger('click');
expect(submitSpy).toHaveBeenCalled();
submitSpy.mockRestore();
});
});
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