Commit 1545a53b authored by Simon Knox's avatar Simon Knox

Merge branch 'dz/355065-regactor-invite-base-modal' into 'master'

Refactor invite_modal_base component

See merge request gitlab-org/gitlab!83201
parents 6abd49e7 0cce1aa5
......@@ -7,7 +7,6 @@ import {
GlDatepicker,
GlLink,
GlSprintf,
GlButton,
GlFormInput,
} from '@gitlab/ui';
import { sprintf } from '~/locale';
......@@ -41,7 +40,6 @@ export default {
GlDropdown,
GlDropdownItem,
GlSprintf,
GlButton,
GlFormInput,
ContentTransition,
},
......@@ -104,6 +102,11 @@ export default {
required: false,
default: INVITE_BUTTON_TEXT,
},
cancelButtonText: {
type: String,
required: false,
default: CANCEL_BUTTON_TEXT,
},
currentSlot: {
type: String,
required: false,
......@@ -114,6 +117,11 @@ export default {
required: false,
default: () => [],
},
preventCancelDefault: {
type: Boolean,
required: false,
default: false,
},
},
data() {
// Be sure to check out reset!
......@@ -141,6 +149,22 @@ export default {
contentSlots() {
return [...DEFAULT_SLOTS, ...(this.extraSlots || [])];
},
actionPrimary() {
return {
text: this.submitButtonText,
attributes: {
variant: 'confirm',
disabled: this.submitDisabled,
loading: this.isLoading,
'data-qa-selector': 'invite_button',
},
};
},
actionCancel() {
return {
text: this.cancelButtonText,
};
},
},
watch: {
selectedAccessLevel: {
......@@ -151,7 +175,7 @@ export default {
},
},
methods: {
reset() {
onReset() {
// This component isn't necessarily disposed,
// so we might need to reset it's state.
this.selectedAccessLevel = this.defaultAccessLevel;
......@@ -159,14 +183,23 @@ export default {
this.$emit('reset');
},
closeModal() {
this.reset();
onCloseModal(e) {
if (this.preventCancelDefault) {
e.preventDefault();
} else {
this.onReset();
this.$refs.modal.hide();
}
this.$emit('cancel');
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
},
submit() {
onSubmit(e) {
// We never want to hide when submitting
e.preventDefault();
this.$emit('submit', {
accessLevel: this.selectedAccessLevel,
expiresAt: this.selectedDate,
......@@ -192,9 +225,11 @@ export default {
size="sm"
:title="modalTitle"
:header-close-label="$options.HEADER_CLOSE_LABEL"
@hidden="reset"
@close="reset"
@hide="reset"
:action-primary="actionPrimary"
:action-cancel="actionCancel"
@primary="onSubmit"
@cancel="onCloseModal"
@hidden="onReset"
>
<content-transition
class="gl-display-grid"
......@@ -282,22 +317,5 @@ export default {
<slot :name="key"></slot>
</template>
</content-transition>
<template #modal-footer>
<slot name="cancel-button">
<gl-button data-testid="cancel-button" @click="closeModal">
{{ $options.CANCEL_BUTTON_TEXT }}
</gl-button>
</slot>
<gl-button
:disabled="submitDisabled"
:loading="isLoading"
variant="confirm"
data-qa-selector="invite_button"
data-testid="invite-button"
@click="submit"
>
{{ submitButtonText }}
</gl-button>
</template>
</gl-modal>
</template>
<script>
import { GlLink, GlButton } from '@gitlab/ui';
import { GlLink } from '@gitlab/ui';
import { partition, isString } from 'lodash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
......@@ -30,7 +30,6 @@ const EXTRA_SLOTS = [
export default {
components: {
GlLink,
GlButton,
InviteModalBase,
},
mixins: [glFeatureFlagsMixin()],
......@@ -59,6 +58,11 @@ export default {
required: false,
default: null,
},
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -80,6 +84,14 @@ export default {
showOverageModal() {
return this.hasOverage && this.enabledOverageCheck;
},
submitDisabledEE() {
if (this.showOverageModal) {
return false;
}
// Use CE default
return this.submitDisabled;
},
enabledOverageCheck() {
return this.glFeatures.overageMembersModal;
},
......@@ -95,13 +107,16 @@ export default {
modalTitleOverride() {
return this.showOverageModal ? OVERAGE_MODAL_TITLE : this.modalTitle;
},
submitButtonText() {
overageModalButtons() {
if (this.showOverageModal) {
return OVERAGE_MODAL_CONTINUE_BUTTON;
return {
submit: OVERAGE_MODAL_CONTINUE_BUTTON,
cancel: OVERAGE_MODAL_BACK_BUTTON,
};
}
// Use CE default
return undefined;
return {};
},
},
methods: {
......@@ -153,9 +168,6 @@ export default {
this.$emit('submit', { accessLevel: args.accessLevel, expiresAt: args.expiresAt });
}
},
handleBack() {
this.hasOverage = false;
},
passthroughSlotNames() {
return Object.keys(this.$scopedSlots || {});
},
......@@ -167,6 +179,11 @@ export default {
return [usersToInviteByEmail.map(({ name }) => name), usersToAddById.map(({ id }) => id)];
},
onCancel() {
if (this.showOverageModal) {
this.hasOverage = false;
}
},
},
i18n: {
OVERAGE_MODAL_TITLE,
......@@ -184,12 +201,16 @@ export default {
<invite-modal-base
v-bind="$attrs"
:name="name"
:submit-button-text="submitButtonText"
:submit-button-text="overageModalButtons.submit"
:cancel-button-text="overageModalButtons.cancel"
:modal-title="modalTitleOverride"
:current-slot="currentSlot"
:extra-slots="$options.EXTRA_SLOTS"
:submit-disabled="submitDisabledEE"
:prevent-cancel-default="showOverageModal"
@reset="onReset"
@submit="onSubmit"
@cancel="onCancel"
v-on="getPassthroughListeners()"
>
<template #[$options.OVERAGE_CONTENT_SLOT]>
......@@ -198,11 +219,6 @@ export default {
$options.i18n.OVERAGE_MODAL_LINK_TEXT
}}</gl-link>
</template>
<template v-if="enabledOverageCheck && hasOverage" #cancel-button>
<gl-button data-testid="overage-back-button" @click="handleBack">
{{ $options.i18n.OVERAGE_MODAL_BACK_BUTTON }}
</gl-button>
</template>
<template v-for="(_, slot) of $scopedSlots" #[slot]="scope">
<slot :name="slot" v-bind="scope"></slot>
</template>
......
......@@ -66,14 +66,15 @@ describe('EEInviteModalBase', () => {
});
const findCEBase = () => wrapper.findComponent(CEInviteModalBase);
const findInviteButton = () => wrapper.findByTestId('invite-button');
const findBackButton = () => wrapper.findByTestId('overage-back-button');
const findModal = () => wrapper.findComponent(GlModal);
const findInitialModalContent = () => wrapper.findByTestId('invite-modal-initial-content');
const findOverageModalContent = () => wrapper.findByTestId('invite-modal-overage-content');
const findModalTitle = () => wrapper.findComponent(GlModal).props('title');
const findModalTitle = () => findModal().props('title');
const clickInviteButton = () => findInviteButton().vm.$emit('click');
const clickBackButton = () => findBackButton().vm.$emit('click');
const emitEventFromModal = (eventName) => () =>
findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
const clickInviteButton = emitEventFromModal('primary');
const clickBackButton = emitEventFromModal('cancel');
describe('default', () => {
beforeEach(() => {
......@@ -110,10 +111,6 @@ describe('EEInviteModalBase', () => {
expect(findModalTitle()).toBe(propsData.modalTitle);
});
it('does not show back button', () => {
expect(findBackButton().exists()).toBe(false);
});
it('shows initial modal content', () => {
expect(findInitialModalContent().isVisible()).toBe(true);
});
......@@ -153,16 +150,26 @@ describe('EEInviteModalBase', () => {
});
it('renders the Back button text correctly', () => {
expect(findBackButton().text()).toBe(OVERAGE_MODAL_BACK_BUTTON);
expect(findModal().props('actionPrimary')).toMatchObject({
text: OVERAGE_MODAL_CONTINUE_BUTTON,
attributes: {
variant: 'confirm',
disabled: false,
loading: false,
'data-qa-selector': 'invite_button',
},
});
});
it('renders the Continue button text correctly', () => {
expect(findInviteButton().text()).toBe(OVERAGE_MODAL_CONTINUE_BUTTON);
expect(findModal().props('actionCancel')).toMatchObject({
text: OVERAGE_MODAL_BACK_BUTTON,
});
});
it('shows the info text', () => {
expect(wrapper.findComponent(GlModal).text()).toContain(
'Your subscription includes 1 seat. If you continue, the _name_ group will have 2 seats in use and will be billed for the overage.',
expect(findModal().text()).toContain(
'If you continue, the _name_ group will have 2 seats in use and will be billed for the overage.',
);
});
......@@ -174,7 +181,7 @@ describe('EEInviteModalBase', () => {
beforeEach(() => clickBackButton());
it('shows the initial modal', () => {
expect(wrapper.findComponent(GlModal).props('title')).toBe(propsData.modalTitle);
expect(findModal().props('title')).toBe(propsData.modalTitle);
expect(findInitialModalContent().isVisible()).toBe(true);
});
......
......@@ -42,18 +42,19 @@ describe('InviteGroupsModal', () => {
wrapper = null;
});
const findModal = () => wrapper.findComponent(GlModal);
const findGroupSelect = () => wrapper.findComponent(GroupSelect);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findInviteButton = () => wrapper.findByTestId('invite-button');
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
const clickInviteButton = () => findInviteButton().vm.$emit('click');
const clickCancelButton = () => findCancelButton().vm.$emit('click');
const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val);
const findBase = () => wrapper.findComponent(InviteModalBase);
const hideModal = () => wrapper.findComponent(GlModal).vm.$emit('hide');
const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val);
const emitEventFromModal = (eventName) => () =>
findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
const hideModal = emitEventFromModal('hidden');
const clickInviteButton = emitEventFromModal('primary');
const clickCancelButton = emitEventFromModal('cancel');
describe('displaying the correct introText and form group description', () => {
describe('when inviting to a project', () => {
......
......@@ -85,12 +85,13 @@ describe('InviteMembersModal', () => {
mock.restore();
});
const findModal = () => wrapper.findComponent(GlModal);
const findBase = () => wrapper.findComponent(InviteModalBase);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findInviteButton = () => wrapper.findByTestId('invite-button');
const clickInviteButton = () => findInviteButton().vm.$emit('click');
const clickCancelButton = () => findCancelButton().vm.$emit('click');
const emitEventFromModal = (eventName) => () =>
findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
const clickInviteButton = emitEventFromModal('primary');
const clickCancelButton = emitEventFromModal('cancel');
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
......@@ -276,7 +277,7 @@ describe('InviteMembersModal', () => {
});
it('renders the modal with the correct title', () => {
expect(wrapper.findComponent(GlModal).props('title')).toBe(MEMBERS_MODAL_CELEBRATE_TITLE);
expect(findModal().props('title')).toBe(MEMBERS_MODAL_CELEBRATE_TITLE);
});
it('includes the correct celebration text and emoji', () => {
......@@ -337,7 +338,7 @@ describe('InviteMembersModal', () => {
});
it('sets isLoading on the Invite button when it is clicked', () => {
expect(findInviteButton().props('loading')).toBe(true);
expect(findModal().props('actionPrimary').attributes.loading).toBe(true);
});
it('calls Api addGroupMembersByUserId with the correct params', () => {
......@@ -380,7 +381,7 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
expect(findMembersSelect().props('validationState')).toBe(false);
expect(findInviteButton().props('loading')).toBe(false);
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
});
describe('clearing the invalid state and message', () => {
......@@ -414,7 +415,7 @@ describe('InviteMembersModal', () => {
});
it('clears the error when the modal is hidden', async () => {
wrapper.findComponent(GlModal).vm.$emit('hide');
findModal().vm.$emit('hidden');
await nextTick();
......@@ -432,7 +433,7 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
expect(findMembersSelect().props('validationState')).toBe(false);
expect(findInviteButton().props('loading')).toBe(false);
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
findMembersSelect().vm.$emit('clear');
......@@ -440,7 +441,7 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe('');
expect(findMembersSelect().props('validationState')).toBe(null);
expect(findInviteButton().props('loading')).toBe(false);
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
});
it('displays the generic error for http server error', async () => {
......@@ -542,7 +543,7 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
expect(findMembersSelect().props('validationState')).toBe(false);
expect(findInviteButton().props('loading')).toBe(false);
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
});
it('displays the restricted email error when restricted email is invited', async () => {
......@@ -554,7 +555,7 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toContain(expectedEmailRestrictedError);
expect(findMembersSelect().props('validationState')).toBe(false);
expect(findInviteButton().props('loading')).toBe(false);
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
});
it('displays the successful toast message when email has already been invited', async () => {
......
......@@ -49,8 +49,6 @@ describe('InviteModalBase', () => {
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
const findLink = () => wrapper.findComponent(GlLink);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findInviteButton = () => wrapper.findByTestId('invite-button');
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
describe('rendering the modal', () => {
......@@ -67,15 +65,21 @@ describe('InviteModalBase', () => {
});
it('renders the Cancel button text correctly', () => {
expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
expect(wrapper.findComponent(GlModal).props('actionCancel')).toMatchObject({
text: CANCEL_BUTTON_TEXT,
});
it('renders the Invite button text correctly', () => {
expect(findInviteButton().text()).toBe(INVITE_BUTTON_TEXT);
});
it('renders the Invite button modal without isLoading', () => {
expect(findInviteButton().props('loading')).toBe(false);
it('renders the Invite button correctly', () => {
expect(wrapper.findComponent(GlModal).props('actionPrimary')).toMatchObject({
text: INVITE_BUTTON_TEXT,
attributes: {
variant: 'confirm',
disabled: false,
loading: false,
'data-qa-selector': 'invite_button',
},
});
});
describe('rendering the access levels dropdown', () => {
......@@ -114,7 +118,7 @@ describe('InviteModalBase', () => {
isLoading: true,
});
expect(findInviteButton().props('loading')).toBe(true);
expect(wrapper.findComponent(GlModal).props('actionPrimary').attributes.loading).toBe(true);
});
it('with invalidFeedbackMessage, set members form group validation state', () => {
......
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