diff options
Diffstat (limited to 'app/assets/javascripts/invite_members')
6 files changed, 196 insertions, 127 deletions
diff --git a/app/assets/javascripts/invite_members/components/invite_group_trigger.vue b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue index c08a4d75c59..424a9d3fabd 100644 --- a/app/assets/javascripts/invite_members/components/invite_group_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue @@ -28,7 +28,12 @@ export default { </script> <template> - <gl-button :class="classes" data-qa-selector="invite_a_group_button" @click="openModal"> + <gl-button + :class="classes" + data-qa-selector="invite_a_group_button" + data-test-id="invite-group-button" + @click="openModal" + > {{ displayText }} </gl-button> </template> diff --git a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue index 6598000c464..f266d978ffa 100644 --- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue @@ -4,6 +4,7 @@ import Api from '~/api'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import { GROUP_FILTERS, GROUP_MODAL_LABELS } from '../constants'; import eventHub from '../event_hub'; +import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message'; import GroupSelect from './group_select.vue'; import InviteModalBase from './invite_modal_base.vue'; @@ -55,6 +56,8 @@ export default { }, data() { return { + invalidFeedbackMessage: '', + isLoading: false, modalId: uniqueId('invite-groups-modal-'), groupToBeSharedWith: {}, }; @@ -83,13 +86,19 @@ export default { }); }, methods: { + showInvalidFeedbackMessage(response) { + this.invalidFeedbackMessage = getInvalidFeedbackMessage(response); + }, openModal() { this.$root.$emit(BV_SHOW_MODAL, this.modalId); }, closeModal() { this.$root.$emit(BV_HIDE_MODAL, this.modalId); }, - sendInvite({ onError, onSuccess, data: { accessLevel, expiresAt } }) { + sendInvite({ accessLevel, expiresAt }) { + this.invalidFeedbackMessage = ''; + this.isLoading = true; + const apiShareWithGroup = this.isProject ? Api.projectShareWithGroup.bind(Api) : Api.groupShareWithGroup.bind(Api); @@ -101,18 +110,27 @@ export default { expires_at: expiresAt, }) .then(() => { - onSuccess(); this.showSuccessMessage(); }) - .catch(onError); + .catch((e) => { + this.showInvalidFeedbackMessage(e); + }) + .finally(() => { + this.isLoading = false; + }); }, resetFields() { + this.invalidFeedbackMessage = ''; + this.isLoading = false; this.groupToBeSharedWith = {}; }, showSuccessMessage() { this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions); this.closeModal(); }, + clearValidation() { + this.invalidFeedbackMessage = ''; + }, }, labels: GROUP_MODAL_LABELS, }; @@ -129,10 +147,12 @@ export default { :label-intro-text="labelIntroText" :label-search-field="$options.labels.searchField" :submit-disabled="inviteDisabled" + :invalid-feedback-message="invalidFeedbackMessage" + :is-loading="isLoading" @reset="resetFields" @submit="sendInvite" > - <template #select="{ clearValidation }"> + <template #select> <group-select v-model="groupToBeSharedWith" :access-levels="accessLevels" diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index 6c0fc5caf26..be48a58d838 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -21,6 +21,7 @@ import { } from '../constants'; import eventHub from '../event_hub'; import { responseMessageFromSuccess } from '../utils/response_message_parser'; +import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message'; import ModalConfetti from './confetti.vue'; import MembersTokenSelect from './members_token_select.vue'; @@ -84,6 +85,8 @@ export default { }, data() { return { + invalidFeedbackMessage: '', + isLoading: false, modalId: uniqueId('invite-members-modal-'), newUsersToInvite: [], selectedTasksToBeDone: [], @@ -152,6 +155,9 @@ export default { } }, methods: { + showInvalidFeedbackMessage(response) { + this.invalidFeedbackMessage = getInvalidFeedbackMessage(response); + }, partitionNewUsersToInvite() { const [usersToInviteByEmail, usersToAddById] = partition( this.newUsersToInvite, @@ -176,7 +182,10 @@ export default { const tracking = new ExperimentTracking(experimentName); tracking.event(eventName); }, - sendInvite({ onError, onSuccess, data: { accessLevel, expiresAt } }) { + sendInvite({ accessLevel, expiresAt }) { + this.isLoading = true; + this.invalidFeedbackMessage = ''; + const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite(); const promises = []; const baseData = { @@ -220,19 +229,17 @@ export default { const message = responseMessageFromSuccess(responses); if (message) { - onError({ - response: { - data: { - message, - }, - }, + this.showInvalidFeedbackMessage({ + response: { data: { message } }, }); } else { - onSuccess(); this.showSuccessMessage(); } }) - .catch(onError); + .catch((e) => this.showInvalidFeedbackMessage(e)) + .finally(() => { + this.isLoading = false; + }); }, trackinviteMembersForTask() { const label = 'selected_tasks_to_be_done'; @@ -241,6 +248,8 @@ export default { tracking.event(INVITE_MEMBERS_FOR_TASK.submit); }, resetFields() { + this.isLoading = false; + this.invalidFeedbackMessage = ''; this.newUsersToInvite = []; this.selectedTasksToBeDone = []; [this.selectedTaskProject] = this.projects; @@ -260,6 +269,9 @@ export default { onAccessLevelUpdate(val) { this.selectedAccessLevel = val; }, + clearValidation() { + this.invalidFeedbackMessage = ''; + }, }, labels: MEMBER_MODAL_LABELS, }; @@ -276,6 +288,8 @@ export default { :label-search-field="$options.labels.searchField" :form-group-description="$options.labels.placeHolder" :submit-disabled="inviteDisabled" + :invalid-feedback-message="invalidFeedbackMessage" + :is-loading="isLoading" @reset="resetFields" @submit="sendInvite" @access-level="onAccessLevelUpdate" @@ -288,7 +302,7 @@ export default { <span v-if="isCelebration">{{ $options.labels.modal.celebrate.intro }} </span> <modal-confetti v-if="isCelebration" /> </template> - <template #select="{ clearValidation, validationState, labelId }"> + <template #select="{ validationState, labelId }"> <members-token-select v-model="newUsersToInvite" class="gl-mb-2" diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue index fc00f5b9343..bafbe94b8bd 100644 --- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue +++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue @@ -10,19 +10,27 @@ import { GlButton, GlFormInput, } from '@gitlab/ui'; -import { unescape } from 'lodash'; -import { sanitize } from '~/lib/dompurify'; import { sprintf } from '~/locale'; +import ContentTransition from '~/vue_shared/components/content_transition.vue'; import { ACCESS_LEVEL, ACCESS_EXPIRE_DATE, - INVALID_FEEDBACK_MESSAGE_DEFAULT, READ_MORE_TEXT, INVITE_BUTTON_TEXT, CANCEL_BUTTON_TEXT, HEADER_CLOSE_LABEL, } from '../constants'; -import { responseMessageFromError } from '../utils/response_message_parser'; + +const DEFAULT_SLOT = 'default'; +const DEFAULT_SLOTS = [ + { + key: DEFAULT_SLOT, + attributes: { + class: 'invite-modal-content', + 'data-testid': 'invite-modal-initial-content', + }, + }, +]; export default { components: { @@ -35,6 +43,7 @@ export default { GlSprintf, GlButton, GlFormInput, + ContentTransition, }, inheritAttrs: false, props: { @@ -80,14 +89,37 @@ export default { required: false, default: false, }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + invalidFeedbackMessage: { + type: String, + required: false, + default: '', + }, + submitButtonText: { + type: String, + required: false, + default: INVITE_BUTTON_TEXT, + }, + currentSlot: { + type: String, + required: false, + default: DEFAULT_SLOT, + }, + extraSlots: { + type: Array, + required: false, + default: () => [], + }, }, data() { // Be sure to check out reset! return { - invalidFeedbackMessage: '', selectedAccessLevel: this.defaultAccessLevel, selectedDate: undefined, - isLoading: false, minDate: new Date(), }; }, @@ -106,6 +138,9 @@ export default { (key) => this.accessLevels[key] === Number(this.selectedAccessLevel), ); }, + contentSlots() { + return [...DEFAULT_SLOTS, ...(this.extraSlots || [])]; + }, }, watch: { selectedAccessLevel: { @@ -116,16 +151,9 @@ export default { }, }, methods: { - showInvalidFeedbackMessage(response) { - const message = this.unescapeMsg(responseMessageFromError(response)); - - this.invalidFeedbackMessage = message || INVALID_FEEDBACK_MESSAGE_DEFAULT; - }, reset() { // This component isn't necessarily disposed, // so we might need to reset it's state. - this.isLoading = false; - this.invalidFeedbackMessage = ''; this.selectedAccessLevel = this.defaultAccessLevel; this.selectedDate = undefined; @@ -135,33 +163,15 @@ export default { this.reset(); this.$refs.modal.hide(); }, - clearValidation() { - this.invalidFeedbackMessage = ''; - }, changeSelectedItem(item) { this.selectedAccessLevel = item; }, submit() { - this.isLoading = true; - this.invalidFeedbackMessage = ''; - this.$emit('submit', { - onSuccess: () => { - this.isLoading = false; - }, - onError: (...args) => { - this.isLoading = false; - this.showInvalidFeedbackMessage(...args); - }, - data: { - accessLevel: this.selectedAccessLevel, - expiresAt: this.selectedDate, - }, + accessLevel: this.selectedAccessLevel, + expiresAt: this.selectedDate, }); }, - unescapeMsg(message) { - return unescape(sanitize(message, { ALLOWED_TAGS: [] })); - }, }, HEADER_CLOSE_LABEL, ACCESS_EXPIRE_DATE, @@ -169,6 +179,7 @@ export default { READ_MORE_TEXT, INVITE_BUTTON_TEXT, CANCEL_BUTTON_TEXT, + DEFAULT_SLOT, }; </script> @@ -185,91 +196,105 @@ export default { @close="reset" @hide="reset" > - <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" + <content-transition + class="gl-display-grid" + transition-name="invite-modal-transition" + :slots="contentSlots" + :current-slot="currentSlot" > - <label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label> - <slot - name="select" - v-bind="{ clearValidation, validationState, labelId: selectLabelId }" - ></slot> - </gl-form-group> + <template #[$options.DEFAULT_SLOT]> + <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> - <label class="gl-font-weight-bold">{{ $options.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)" - > - <div>{{ item }}</div> - </gl-dropdown-item> - </template> - </gl-dropdown> - </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="{ validationState, labelId: selectLabelId }"></slot> + </gl-form-group> - <div class="gl-mt-2 gl-w-half gl-xs-w-full"> - <gl-sprintf :message="$options.READ_MORE_TEXT"> - <template #link="{ content }"> - <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> - </div> + <label class="gl-font-weight-bold">{{ $options.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)" + > + <div>{{ item }}</div> + </gl-dropdown-item> + </template> + </gl-dropdown> + </div> - <label class="gl-mt-5 gl-display-block" for="expires_at">{{ - $options.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 class="gl-mt-2 gl-w-half gl-xs-w-full"> + <gl-sprintf :message="$options.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.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> + </template> + <template v-for="{ key } in extraSlots" #[key]> + <slot :name="key"></slot> + </template> + </content-transition> <template #modal-footer> - <gl-button data-testid="cancel-button" @click="closeModal"> - {{ $options.CANCEL_BUTTON_TEXT }} - </gl-button> + <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="success" + variant="confirm" data-qa-selector="invite_button" data-testid="invite-button" @click="submit" > - {{ $options.INVITE_BUTTON_TEXT }} + {{ submitButtonText }} </gl-button> </template> </gl-modal> diff --git a/app/assets/javascripts/invite_members/init_invite_members_form.js b/app/assets/javascripts/invite_members/init_invite_members_form.js deleted file mode 100644 index 5f8688755ba..00000000000 --- a/app/assets/javascripts/invite_members/init_invite_members_form.js +++ /dev/null @@ -1,7 +0,0 @@ -import { disableButtonIfEmptyField } from '~/lib/utils/common_utils'; - -// This is only used when `invite_members_group_modal` feature flag is disabled. -// This file can be removed when `invite_members_group_modal` feature flag is removed -export default () => { - disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); -}; diff --git a/app/assets/javascripts/invite_members/utils/get_invalid_feedback_message.js b/app/assets/javascripts/invite_members/utils/get_invalid_feedback_message.js new file mode 100644 index 00000000000..62f66d009dc --- /dev/null +++ b/app/assets/javascripts/invite_members/utils/get_invalid_feedback_message.js @@ -0,0 +1,12 @@ +import { unescape } from 'lodash'; +import { sanitize } from '~/lib/dompurify'; +import { INVALID_FEEDBACK_MESSAGE_DEFAULT } from '../constants'; +import { responseMessageFromError } from './response_message_parser'; + +const unescapeMsg = (message) => unescape(sanitize(message, { ALLOWED_TAGS: [] })); + +export const getInvalidFeedbackMessage = (response) => { + const message = unescapeMsg(responseMessageFromError(response)); + + return message || INVALID_FEEDBACK_MESSAGE_DEFAULT; +}; |