diff options
Diffstat (limited to 'app/assets/javascripts/invite_members/components/invite_members_modal.vue')
-rw-r--r-- | app/assets/javascripts/invite_members/components/invite_members_modal.vue | 419 |
1 files changed, 110 insertions, 309 deletions
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 91a139a5105..6c0fc5caf26 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -1,56 +1,40 @@ <script> import { GlAlert, - GlFormGroup, - GlModal, GlDropdown, GlDropdownItem, - GlDatepicker, GlLink, GlSprintf, - GlButton, - GlFormInput, GlFormCheckboxGroup, } from '@gitlab/ui'; -import { partition, isString, unescape, uniqueId } from 'lodash'; +import { partition, isString, uniqueId } from 'lodash'; +import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue'; import Api from '~/api'; import ExperimentTracking from '~/experimentation/experiment_tracking'; -import { sanitize } from '~/lib/dompurify'; -import { BV_SHOW_MODAL } from '~/lib/utils/constants'; +import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import { getParameterValues } from '~/lib/utils/url_utility'; -import { sprintf } from '~/locale'; import { - GROUP_FILTERS, USERS_FILTER_ALL, INVITE_MEMBERS_FOR_TASK, - MODAL_LABELS, + MEMBER_MODAL_LABELS, LEARN_GITLAB, } from '../constants'; import eventHub from '../event_hub'; -import { - responseMessageFromError, - responseMessageFromSuccess, -} from '../utils/response_message_parser'; +import { responseMessageFromSuccess } from '../utils/response_message_parser'; import ModalConfetti from './confetti.vue'; -import GroupSelect from './group_select.vue'; import MembersTokenSelect from './members_token_select.vue'; export default { name: 'InviteMembersModal', components: { GlAlert, - GlFormGroup, - GlDatepicker, GlLink, - GlModal, GlDropdown, GlDropdownItem, GlSprintf, - GlButton, - GlFormInput, GlFormCheckboxGroup, + InviteModalBase, MembersTokenSelect, - GroupSelect, ModalConfetti, }, inject: ['newProjectPath'], @@ -75,15 +59,9 @@ export default { type: Number, required: true, }, - groupSelectFilter: { + helpLink: { type: String, - required: false, - default: GROUP_FILTERS.ALL, - }, - groupSelectParentId: { - type: Number, - required: false, - default: null, + required: true, }, usersFilter: { type: String, @@ -95,10 +73,6 @@ export default { required: false, default: null, }, - helpLink: { - type: String, - required: true, - }, tasksToBeDoneOptions: { type: Array, required: true, @@ -110,73 +84,31 @@ export default { }, data() { return { - visible: true, modalId: uniqueId('invite-members-modal-'), - selectedAccessLevel: this.defaultAccessLevel, - inviteeType: 'members', newUsersToInvite: [], - selectedDate: undefined, selectedTasksToBeDone: [], selectedTaskProject: this.projects[0], - groupToBeSharedWith: {}, source: 'unknown', - invalidFeedbackMessage: '', - isLoading: false, mode: 'default', + // Kept in sync with "base" + selectedAccessLevel: undefined, }; }, computed: { isCelebration() { return this.mode === 'celebrate'; }, - validationState() { - return this.invalidFeedbackMessage === '' ? null : false; - }, - isInviteGroup() { - return this.inviteeType === 'group'; - }, modalTitle() { - return this.$options.labels[this.inviteeType].modal[this.mode].title; - }, - introText() { - return sprintf(this.$options.labels[this.inviteeType][this.inviteTo][this.mode].introText, { - name: this.name, - }); + return this.$options.labels.modal[this.mode].title; }, inviteTo() { return this.isProject ? 'toProject' : 'toGroup'; }, - toastOptions() { - return { - onComplete: () => { - this.selectedAccessLevel = this.defaultAccessLevel; - this.newUsersToInvite = []; - this.groupToBeSharedWith = {}; - }, - }; - }, - basePostData() { - return { - expires_at: this.selectedDate, - format: 'json', - }; - }, - selectedRoleName() { - return Object.keys(this.accessLevels).find( - (key) => this.accessLevels[key] === Number(this.selectedAccessLevel), - ); + labelIntroText() { + return this.$options.labels[this.inviteTo][this.mode].introText; }, inviteDisabled() { - return ( - this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0 - ); - }, - errorFieldDescription() { - if (this.inviteeType === 'group') { - return ''; - } - - return this.$options.labels[this.inviteeType].placeHolder; + return this.newUsersToInvite.length === 0; }, tasksToBeDoneEnabled() { return ( @@ -215,7 +147,7 @@ export default { }); if (this.tasksToBeDoneEnabled) { - this.openModal({ inviteeType: 'members', source: 'in_product_marketing_email' }); + this.openModal({ source: 'in_product_marketing_email' }); this.trackEvent(INVITE_MEMBERS_FOR_TASK.name, INVITE_MEMBERS_FOR_TASK.view); } }, @@ -231,72 +163,42 @@ export default { usersToAddById.map((user) => user.id).join(','), ]; }, - openModal({ mode = 'default', inviteeType, source }) { + openModal({ mode = 'default', source }) { this.mode = mode; - this.inviteeType = inviteeType; this.source = source; this.$root.$emit(BV_SHOW_MODAL, this.modalId); }, + closeModal() { + this.$root.$emit(BV_HIDE_MODAL, this.modalId); + }, trackEvent(experimentName, eventName) { const tracking = new ExperimentTracking(experimentName); tracking.event(eventName); }, - closeModal() { - this.resetFields(); - this.$refs.modal.hide(); - }, - sendInvite() { - if (this.isInviteGroup) { - this.submitShareWithGroup(); - } else { - this.submitInviteMembers(); - } - }, - trackinviteMembersForTask() { - const label = 'selected_tasks_to_be_done'; - const property = this.selectedTasksToBeDone.join(','); - const tracking = new ExperimentTracking(INVITE_MEMBERS_FOR_TASK.name, { label, property }); - tracking.event(INVITE_MEMBERS_FOR_TASK.submit); - }, - resetFields() { - this.isLoading = false; - this.selectedAccessLevel = this.defaultAccessLevel; - this.selectedDate = undefined; - this.newUsersToInvite = []; - this.groupToBeSharedWith = {}; - this.invalidFeedbackMessage = ''; - this.selectedTasksToBeDone = []; - [this.selectedTaskProject] = this.projects; - }, - changeSelectedItem(item) { - this.selectedAccessLevel = item; - }, - changeSelectedTaskProject(project) { - this.selectedTaskProject = project; - }, - submitShareWithGroup() { - const apiShareWithGroup = this.isProject - ? Api.projectShareWithGroup.bind(Api) - : Api.groupShareWithGroup.bind(Api); - - apiShareWithGroup(this.id, this.shareWithGroupPostData(this.groupToBeSharedWith.id)) - .then(this.showSuccessMessage) - .catch(this.showInvalidFeedbackMessage); - }, - submitInviteMembers() { - this.invalidFeedbackMessage = ''; - this.isLoading = true; - + sendInvite({ onError, onSuccess, data: { accessLevel, expiresAt } }) { const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite(); const promises = []; + const baseData = { + format: 'json', + expires_at: expiresAt, + access_level: accessLevel, + invite_source: this.source, + tasks_to_be_done: this.tasksToBeDoneForPost, + tasks_project_id: this.tasksProjectForPost, + }; if (usersToInviteByEmail !== '') { const apiInviteByEmail = this.isProject ? Api.inviteProjectMembersByEmail.bind(Api) : Api.inviteGroupMembersByEmail.bind(Api); - promises.push(apiInviteByEmail(this.id, this.inviteByEmailPostData(usersToInviteByEmail))); + promises.push( + apiInviteByEmail(this.id, { + ...baseData, + email: usersToInviteByEmail, + }), + ); } if (usersToAddById !== '') { @@ -304,188 +206,103 @@ export default { ? Api.addProjectMembersByUserId.bind(Api) : Api.addGroupMembersByUserId.bind(Api); - promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById))); + promises.push( + apiAddByUserId(this.id, { + ...baseData, + user_id: usersToAddById, + }), + ); } this.trackinviteMembersForTask(); Promise.all(promises) - .then(this.conditionallyShowSuccessMessage) - .catch(this.showInvalidFeedbackMessage); - }, - inviteByEmailPostData(usersToInviteByEmail) { - return { - ...this.basePostData, - email: usersToInviteByEmail, - access_level: this.selectedAccessLevel, - invite_source: this.source, - tasks_to_be_done: this.tasksToBeDoneForPost, - tasks_project_id: this.tasksProjectForPost, - }; + .then((responses) => { + const message = responseMessageFromSuccess(responses); + + if (message) { + onError({ + response: { + data: { + message, + }, + }, + }); + } else { + onSuccess(); + this.showSuccessMessage(); + } + }) + .catch(onError); }, - addByUserIdPostData(usersToAddById) { - return { - ...this.basePostData, - user_id: usersToAddById, - access_level: this.selectedAccessLevel, - invite_source: this.source, - tasks_to_be_done: this.tasksToBeDoneForPost, - tasks_project_id: this.tasksProjectForPost, - }; + trackinviteMembersForTask() { + const label = 'selected_tasks_to_be_done'; + const property = this.selectedTasksToBeDone.join(','); + const tracking = new ExperimentTracking(INVITE_MEMBERS_FOR_TASK.name, { label, property }); + tracking.event(INVITE_MEMBERS_FOR_TASK.submit); }, - shareWithGroupPostData(groupToBeSharedWith) { - return { - ...this.basePostData, - group_id: groupToBeSharedWith, - group_access: this.selectedAccessLevel, - }; + resetFields() { + this.newUsersToInvite = []; + this.selectedTasksToBeDone = []; + [this.selectedTaskProject] = this.projects; }, - conditionallyShowSuccessMessage(response) { - const message = this.unescapeMsg(responseMessageFromSuccess(response)); - - if (message === '') { - this.showSuccessMessage(); - - return; - } - - this.invalidFeedbackMessage = message; - this.isLoading = false; + changeSelectedTaskProject(project) { + this.selectedTaskProject = project; }, showSuccessMessage() { if (this.isOnLearnGitlab) { eventHub.$emit('showSuccessfulInvitationsAlert'); } else { - this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions); + this.$toast.show(this.$options.labels.toastMessageSuccessful); } - this.closeModal(); - }, - showInvalidFeedbackMessage(response) { - const message = this.unescapeMsg(responseMessageFromError(response)); - this.isLoading = false; - this.invalidFeedbackMessage = message || this.$options.labels.invalidFeedbackMessageDefault; - }, - handleMembersTokenSelectClear() { - this.invalidFeedbackMessage = ''; + this.closeModal(); }, - unescapeMsg(message) { - return unescape(sanitize(message, { ALLOWED_TAGS: [] })); + onAccessLevelUpdate(val) { + this.selectedAccessLevel = val; }, }, - labels: MODAL_LABELS, - membersTokenSelectLabelId: 'invite-members-input', + labels: MEMBER_MODAL_LABELS, }; </script> <template> - <gl-modal - ref="modal" + <invite-modal-base :modal-id="modalId" - size="sm" - data-qa-selector="invite_members_modal_content" - data-testid="invite-members-modal" - :title="modalTitle" - :header-close-label="$options.labels.headerCloseLabel" - @hidden="resetFields" - @close="resetFields" - @hide="resetFields" + :modal-title="modalTitle" + :name="name" + :access-levels="accessLevels" + :default-access-level="defaultAccessLevel" + :help-link="helpLink" + :label-intro-text="labelIntroText" + :label-search-field="$options.labels.searchField" + :form-group-description="$options.labels.placeHolder" + :submit-disabled="inviteDisabled" + @reset="resetFields" + @submit="sendInvite" + @access-level="onAccessLevelUpdate" > - <div> - <div class="gl-display-flex"> - <div v-if="isCelebration" class="gl-p-4 gl-font-size-h1"><gl-emoji data-name="tada" /></div> - <div> - <p ref="introText"> - <gl-sprintf :message="introText"> - <template #strong="{ content }"> - <strong>{{ content }}</strong> - </template> - </gl-sprintf> - <br /> - <span v-if="isCelebration">{{ $options.labels.members.modal.celebrate.intro }} </span> - <modal-confetti v-if="isCelebration" /> - </p> - </div> - </div> - - <gl-form-group - :invalid-feedback="invalidFeedbackMessage" - :state="validationState" - :description="errorFieldDescription" - data-testid="members-form-group" - > - <label :id="$options.membersTokenSelectLabelId" class="col-form-label">{{ - $options.labels[inviteeType].searchField - }}</label> - <members-token-select - v-if="!isInviteGroup" - v-model="newUsersToInvite" - class="gl-mb-2" - :validation-state="validationState" - :aria-labelledby="$options.membersTokenSelectLabelId" - :users-filter="usersFilter" - :filter-id="filterId" - @clear="handleMembersTokenSelectClear" - /> - <group-select - v-if="isInviteGroup" - v-model="groupToBeSharedWith" - :groups-filter="groupSelectFilter" - :parent-group-id="groupSelectParentId" - @input="handleMembersTokenSelectClear" - /> - </gl-form-group> - - <label class="gl-font-weight-bold">{{ $options.labels.accessLevel }}</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> - - <div class="gl-mt-2 gl-w-half gl-xs-w-full"> - <gl-sprintf :message="$options.labels.readMoreText"> - <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.labels.accessExpireDate - }}</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="new Date()" - :target="null" - > - <template #default="{ formattedDate }"> - <gl-form-input - class="gl-w-full" - :value="formattedDate" - :placeholder="__(`YYYY-MM-DD`)" - /> - </template> - </gl-datepicker> - </div> + <template #intro-text-before> + <div v-if="isCelebration" class="gl-p-4 gl-font-size-h1"><gl-emoji data-name="tada" /></div> + </template> + <template #intro-text-after> + <br /> + <span v-if="isCelebration">{{ $options.labels.modal.celebrate.intro }} </span> + <modal-confetti v-if="isCelebration" /> + </template> + <template #select="{ clearValidation, validationState, labelId }"> + <members-token-select + v-model="newUsersToInvite" + class="gl-mb-2" + :validation-state="validationState" + :aria-labelledby="labelId" + :users-filter="usersFilter" + :filter-id="filterId" + @clear="clearValidation" + /> + </template> + <template #form-after> <div v-if="showTasksToBeDone" data-testid="invite-members-modal-tasks-to-be-done"> <label class="gl-mt-5"> - {{ $options.labels.members.tasksToBeDone.title }} + {{ $options.labels.tasksToBeDone.title }} </label> <template v-if="projects.length"> <gl-form-checkbox-group @@ -495,7 +312,7 @@ export default { /> <template v-if="showTaskProjects"> <label class="gl-mt-5 gl-display-block"> - {{ $options.labels.members.tasksProject.title }} + {{ $options.labels.tasksProject.title }} </label> <gl-dropdown class="gl-w-half gl-xs-w-full" @@ -522,7 +339,7 @@ export default { :dismissible="false" data-testid="invite-members-modal-no-projects-alert" > - <gl-sprintf :message="$options.labels.members.tasksToBeDone.noProjects"> + <gl-sprintf :message="$options.labels.tasksToBeDone.noProjects"> <template #link="{ content }"> <gl-link :href="newProjectPath" target="_blank" class="gl-label-link"> {{ content }} @@ -531,22 +348,6 @@ export default { </gl-sprintf> </gl-alert> </div> - </div> - - <template #modal-footer> - <gl-button data-testid="cancel-button" @click="closeModal"> - {{ $options.labels.cancelButtonText }} - </gl-button> - <gl-button - :disabled="inviteDisabled" - :loading="isLoading" - variant="success" - data-qa-selector="invite_button" - data-testid="invite-button" - @click="sendInvite" - > - {{ $options.labels.inviteButtonText }} - </gl-button> </template> - </gl-modal> + </invite-modal-base> </template> |