Commit 89356357 authored by Diana Zubova's avatar Diana Zubova Committed by Miguel Rincon

Add transition between invite members modal

Smooth UX transition

EE: true
parent d94c5986
...@@ -131,10 +131,13 @@ export default { ...@@ -131,10 +131,13 @@ export default {
return this.glFeatures.overageMembersModal; return this.glFeatures.overageMembersModal;
}, },
modalInfo() { modalInfo() {
const infoText = this.$options.i18n.infoText(this.subscriptionSeats); if (this.totalUserCount) {
const infoWarning = this.$options.i18n.infoWarning(this.totalUserCount, this.name); const infoText = this.$options.i18n.infoText(this.subscriptionSeats);
const infoWarning = this.$options.i18n.infoWarning(this.totalUserCount, this.name);
return `${infoText} ${infoWarning}`; return `${infoText} ${infoWarning}`;
}
return '';
}, },
modalTitleLabel() { modalTitleLabel() {
return this.showOverageModal ? this.$options.i18n.OVERAGE_MODAL_TITLE : this.modalTitle; return this.showOverageModal ? this.$options.i18n.OVERAGE_MODAL_TITLE : this.modalTitle;
...@@ -243,88 +246,102 @@ export default { ...@@ -243,88 +246,102 @@ export default {
@close="reset" @close="reset"
@hide="reset" @hide="reset"
> >
<div v-show="!showOverageModal"> <div class="gl-display-grid">
<div class="gl-display-flex" data-testid="modal-base-intro-text"> <transition name="invite-modal-transition">
<slot name="intro-text-before"></slot> <div
<p> v-show="!showOverageModal"
<gl-sprintf :message="introText"> class="invite-modal-content"
<template #strong="{ content }"> data-testid="invite-modal-initial-content"
<strong>{{ content }}</strong> >
</template> <div class="gl-display-flex" data-testid="modal-base-intro-text">
</gl-sprintf> <slot name="intro-text-before"></slot>
</p> <p>
<slot name="intro-text-after"></slot> <gl-sprintf :message="introText">
</div> <template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</p>
<slot name="intro-text-after"></slot>
</div>
<gl-form-group <gl-form-group
:invalid-feedback="invalidFeedbackMessage" :invalid-feedback="invalidFeedbackMessage"
:state="validationState" :state="validationState"
:description="formGroupDescription" :description="formGroupDescription"
data-testid="members-form-group" data-testid="members-form-group"
> >
<label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label> <label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label>
<slot <slot
name="select" name="select"
v-bind="{ clearValidation, validationState, labelId: selectLabelId }" v-bind="{ clearValidation, validationState, labelId: selectLabelId }"
></slot> ></slot>
</gl-form-group> </gl-form-group>
<label class="gl-font-weight-bold">{{ $options.i18n.ACCESS_LEVEL }}</label> <label class="gl-font-weight-bold">{{ $options.i18n.ACCESS_LEVEL }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full"> <div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown <gl-dropdown
class="gl-shadow-none gl-w-full" class="gl-shadow-none gl-w-full"
data-qa-selector="access_level_dropdown" data-qa-selector="access_level_dropdown"
v-bind="$attrs" v-bind="$attrs"
:text="selectedRoleName" :text="selectedRoleName"
>
<template v-for="(key, item) in accessLevels">
<gl-dropdown-item
:key="key"
active-class="is-active"
is-check-item
:is-checked="key === selectedAccessLevel"
@click="changeSelectedItem(key)"
> >
<div>{{ item }}</div> <template v-for="(key, item) in accessLevels">
</gl-dropdown-item> <gl-dropdown-item
</template> :key="key"
</gl-dropdown> active-class="is-active"
</div> is-check-item
:is-checked="key === selectedAccessLevel"
@click="changeSelectedItem(key)"
>
<div>{{ item }}</div>
</gl-dropdown-item>
</template>
</gl-dropdown>
</div>
<div class="gl-mt-2 gl-w-half gl-xs-w-full"> <div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-sprintf :message="$options.i18n.READ_MORE_TEXT"> <gl-sprintf :message="$options.i18n.READ_MORE_TEXT">
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="helpLink" target="_blank">{{ content }}</gl-link> <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</div> </div>
<label class="gl-mt-5 gl-display-block" for="expires_at">{{ <label class="gl-mt-5 gl-display-block" for="expires_at">{{
$options.i18n.ACCESS_EXPIRE_DATE $options.i18n.ACCESS_EXPIRE_DATE
}}</label> }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block"> <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
<gl-datepicker <gl-datepicker
v-model="selectedDate" v-model="selectedDate"
class="gl-display-inline!" class="gl-display-inline!"
:min-date="minDate" :min-date="minDate"
:target="null" :target="null"
>
<template #default="{ formattedDate }">
<gl-form-input
class="gl-w-full"
:value="formattedDate"
:placeholder="__(`YYYY-MM-DD`)"
/>
</template>
</gl-datepicker>
</div>
<slot name="form-after"></slot>
</div>
</transition>
<transition name="invite-modal-transition">
<div
v-show="showOverageModal"
class="invite-modal-content"
data-testid="invite-modal-overage-content"
> >
<template #default="{ formattedDate }"> {{ modalInfo }}
<gl-form-input <gl-link :href="$options.i18n.OVERAGE_MODAL_LINK" target="_blank">{{
class="gl-w-full" $options.i18n.OVERAGE_MODAL_LINK_TEXT
:value="formattedDate" }}</gl-link>
:placeholder="__(`YYYY-MM-DD`)" </div>
/> </transition>
</template>
</gl-datepicker>
</div>
<slot name="form-after"></slot>
</div>
<div v-if="showOverageModal">
{{ modalInfo }}
<gl-link :href="$options.i18n.OVERAGE_MODAL_LINK" target="_blank">{{
$options.i18n.OVERAGE_MODAL_LINK_TEXT
}}</gl-link>
</div> </div>
<template #modal-footer> <template #modal-footer>
<template v-if="!showOverageModal"> <template v-if="!showOverageModal">
......
...@@ -42,3 +42,34 @@ ...@@ -42,3 +42,34 @@
svg g { fill: $gray-600; } svg g { fill: $gray-600; }
} }
} }
.invite-modal-content {
grid-row: 1;
grid-column: 1;
}
$max-invite-modal-height: 600px;
// Custom styles for invite-modal-transition
// Used by Vue Transition API
.invite-modal-transition-enter-active,
.invite-modal-transition-leave-active {
transition-property: max-height, opacity;
transition-timing-function: ease-in-out;
@include gl-transition-slow;
overflow: hidden;
}
.invite-modal-transition-enter,
.invite-modal-transition-leave-to {
max-height: 0;
opacity: 0;
}
.invite-modal-transition-enter-to,
.invite-modal-transition-leave {
max-height: px-to-rem($max-invite-modal-height);
}
...@@ -16,7 +16,6 @@ import { ...@@ -16,7 +16,6 @@ import {
OVERAGE_MODAL_CONTINUE_BUTTON, OVERAGE_MODAL_CONTINUE_BUTTON,
OVERAGE_MODAL_BACK_BUTTON, OVERAGE_MODAL_BACK_BUTTON,
} from 'ee/invite_members/constants'; } from 'ee/invite_members/constants';
import waitForPromises from 'helpers/wait_for_promises';
import { propsData } from 'jest/invite_members/mock_data/modal_base'; import { propsData } from 'jest/invite_members/mock_data/modal_base';
describe('InviteModalBase', () => { describe('InviteModalBase', () => {
...@@ -63,6 +62,8 @@ describe('InviteModalBase', () => { ...@@ -63,6 +62,8 @@ describe('InviteModalBase', () => {
const findInviteButton = () => wrapper.findByTestId('invite-button'); const findInviteButton = () => wrapper.findByTestId('invite-button');
const findBackButton = () => wrapper.findByTestId('overage-back-button'); const findBackButton = () => wrapper.findByTestId('overage-back-button');
const findOverageInviteButton = () => wrapper.findByTestId('invite-with-overage-button'); const findOverageInviteButton = () => wrapper.findByTestId('invite-with-overage-button');
const findInitialModalContent = () => wrapper.findByTestId('invite-modal-initial-content');
const findOverageModalContent = () => wrapper.findByTestId('invite-modal-overage-content');
const clickInviteButton = () => findInviteButton().vm.$emit('click'); const clickInviteButton = () => findInviteButton().vm.$emit('click');
const clickBackButton = () => findBackButton().vm.$emit('click'); const clickBackButton = () => findBackButton().vm.$emit('click');
...@@ -102,25 +103,23 @@ describe('InviteModalBase', () => { ...@@ -102,25 +103,23 @@ describe('InviteModalBase', () => {
}); });
}); });
describe('rendering the help link', () => { it('renders the correct link', () => {
it('renders the correct link', () => { expect(findLink().attributes('href')).toBe(propsData.helpLink);
expect(findLink().attributes('href')).toBe(propsData.helpLink);
});
}); });
describe('rendering the access expiration date field', () => { it('renders the datepicker', () => {
it('renders the datepicker', () => { expect(findDatepicker().exists()).toBe(true);
expect(findDatepicker().exists()).toBe(true); });
});
it("doesn't show the overage content", () => {
expect(findOverageModalContent().isVisible()).toBe(false);
}); });
}); });
describe('displays overage modal', () => { describe('displays overage modal', () => {
beforeEach(async () => { beforeEach(() => {
createComponent({}, {}, { glFeatures: { overageMembersModal: true } }); createComponent({}, {}, { glFeatures: { overageMembersModal: true } });
clickInviteButton(); clickInviteButton();
await waitForPromises();
}); });
it('renders the modal with the correct title', () => { it('renders the modal with the correct title', () => {
...@@ -141,11 +140,21 @@ describe('InviteModalBase', () => { ...@@ -141,11 +140,21 @@ describe('InviteModalBase', () => {
); );
}); });
it('switches back to the intial modal', async () => { it('doesn\t show the initial modal content', () => {
clickBackButton(); expect(findInitialModalContent().isVisible()).toBe(false);
await waitForPromises(); });
describe('when switches back to the initial modal', () => {
beforeEach(() => clickBackButton());
expect(wrapper.findComponent(GlModal).props('title')).toBe('_modal_title_'); it('shows the initial modal', () => {
expect(wrapper.findComponent(GlModal).props('title')).toBe('_modal_title_');
expect(findInitialModalContent().isVisible()).toBe(true);
});
it("doesn't show the overage content", () => {
expect(findOverageModalContent().isVisible()).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