summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/invite_members/components/invite_members_modal.vue
diff options
context:
space:
mode:
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.vue419
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>