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 {
return this.glFeatures.overageMembersModal;
},
modalInfo() {
const infoText = this.$options.i18n.infoText(this.subscriptionSeats);
const infoWarning = this.$options.i18n.infoWarning(this.totalUserCount, this.name);
if (this.totalUserCount) {
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() {
return this.showOverageModal ? this.$options.i18n.OVERAGE_MODAL_TITLE : this.modalTitle;
......@@ -243,88 +246,102 @@ export default {
@close="reset"
@hide="reset"
>
<div v-show="!showOverageModal">
<div class="gl-display-flex" data-testid="modal-base-intro-text">
<slot name="intro-text-before"></slot>
<p>
<gl-sprintf :message="introText">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</p>
<slot name="intro-text-after"></slot>
</div>
<div class="gl-display-grid">
<transition name="invite-modal-transition">
<div
v-show="!showOverageModal"
class="invite-modal-content"
data-testid="invite-modal-initial-content"
>
<div class="gl-display-flex" data-testid="modal-base-intro-text">
<slot name="intro-text-before"></slot>
<p>
<gl-sprintf :message="introText">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</p>
<slot name="intro-text-after"></slot>
</div>
<gl-form-group
:invalid-feedback="invalidFeedbackMessage"
:state="validationState"
:description="formGroupDescription"
data-testid="members-form-group"
>
<label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label>
<slot
name="select"
v-bind="{ clearValidation, validationState, labelId: selectLabelId }"
></slot>
</gl-form-group>
<gl-form-group
:invalid-feedback="invalidFeedbackMessage"
:state="validationState"
:description="formGroupDescription"
data-testid="members-form-group"
>
<label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label>
<slot
name="select"
v-bind="{ clearValidation, validationState, labelId: selectLabelId }"
></slot>
</gl-form-group>
<label class="gl-font-weight-bold">{{ $options.i18n.ACCESS_LEVEL }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown
class="gl-shadow-none gl-w-full"
data-qa-selector="access_level_dropdown"
v-bind="$attrs"
: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)"
<label class="gl-font-weight-bold">{{ $options.i18n.ACCESS_LEVEL }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown
class="gl-shadow-none gl-w-full"
data-qa-selector="access_level_dropdown"
v-bind="$attrs"
:text="selectedRoleName"
>
<div>{{ item }}</div>
</gl-dropdown-item>
</template>
</gl-dropdown>
</div>
<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>
</gl-dropdown-item>
</template>
</gl-dropdown>
</div>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-sprintf :message="$options.i18n.READ_MORE_TEXT">
<template #link="{ content }">
<gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-sprintf :message="$options.i18n.READ_MORE_TEXT">
<template #link="{ content }">
<gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
<label class="gl-mt-5 gl-display-block" for="expires_at">{{
$options.i18n.ACCESS_EXPIRE_DATE
}}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
<gl-datepicker
v-model="selectedDate"
class="gl-display-inline!"
:min-date="minDate"
:target="null"
<label class="gl-mt-5 gl-display-block" for="expires_at">{{
$options.i18n.ACCESS_EXPIRE_DATE
}}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
<gl-datepicker
v-model="selectedDate"
class="gl-display-inline!"
:min-date="minDate"
: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 }">
<gl-form-input
class="gl-w-full"
:value="formattedDate"
:placeholder="__(`YYYY-MM-DD`)"
/>
</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>
{{ modalInfo }}
<gl-link :href="$options.i18n.OVERAGE_MODAL_LINK" target="_blank">{{
$options.i18n.OVERAGE_MODAL_LINK_TEXT
}}</gl-link>
</div>
</transition>
</div>
<template #modal-footer>
<template v-if="!showOverageModal">
......
......@@ -42,3 +42,34 @@
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 {
OVERAGE_MODAL_CONTINUE_BUTTON,
OVERAGE_MODAL_BACK_BUTTON,
} from 'ee/invite_members/constants';
import waitForPromises from 'helpers/wait_for_promises';
import { propsData } from 'jest/invite_members/mock_data/modal_base';
describe('InviteModalBase', () => {
......@@ -63,6 +62,8 @@ describe('InviteModalBase', () => {
const findInviteButton = () => wrapper.findByTestId('invite-button');
const findBackButton = () => wrapper.findByTestId('overage-back-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 clickBackButton = () => findBackButton().vm.$emit('click');
......@@ -102,25 +103,23 @@ describe('InviteModalBase', () => {
});
});
describe('rendering the help link', () => {
it('renders the correct link', () => {
expect(findLink().attributes('href')).toBe(propsData.helpLink);
});
it('renders the correct link', () => {
expect(findLink().attributes('href')).toBe(propsData.helpLink);
});
describe('rendering the access expiration date field', () => {
it('renders the datepicker', () => {
expect(findDatepicker().exists()).toBe(true);
});
it('renders the datepicker', () => {
expect(findDatepicker().exists()).toBe(true);
});
it("doesn't show the overage content", () => {
expect(findOverageModalContent().isVisible()).toBe(false);
});
});
describe('displays overage modal', () => {
beforeEach(async () => {
beforeEach(() => {
createComponent({}, {}, { glFeatures: { overageMembersModal: true } });
clickInviteButton();
await waitForPromises();
});
it('renders the modal with the correct title', () => {
......@@ -141,11 +140,21 @@ describe('InviteModalBase', () => {
);
});
it('switches back to the intial modal', async () => {
clickBackButton();
await waitForPromises();
it('doesn\t show the initial modal content', () => {
expect(findInitialModalContent().isVisible()).toBe(false);
});
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