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