summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/invite_members
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/invite_members')
-rw-r--r--app/assets/javascripts/invite_members/components/import_project_members_modal.vue23
-rw-r--r--app/assets/javascripts/invite_members/components/invite_group_notification.vue37
-rw-r--r--app/assets/javascripts/invite_members/components/invite_groups_modal.vue37
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue161
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue107
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue17
-rw-r--r--app/assets/javascripts/invite_members/constants.js11
-rw-r--r--app/assets/javascripts/invite_members/init_import_project_members_modal.js4
-rw-r--r--app/assets/javascripts/invite_members/init_invite_groups_modal.js5
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js1
-rw-r--r--app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js23
11 files changed, 295 insertions, 131 deletions
diff --git a/app/assets/javascripts/invite_members/components/import_project_members_modal.vue b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
index 31b7fd4cc42..b4e9a3a1559 100644
--- a/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
@@ -5,6 +5,10 @@ import { importProjectMembers } from '~/api/projects_api';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '../utils/trigger_successful_invite_alert';
import ProjectSelect from './project_select.vue';
export default {
@@ -24,6 +28,11 @@ export default {
type: String,
required: true,
},
+ reloadPageOnSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -59,6 +68,10 @@ export default {
},
},
mounted() {
+ if (this.reloadPageOnSubmit) {
+ displaySuccessfulInvitationAlert();
+ }
+
eventHub.$on('openProjectMembersModal', () => {
this.openModal();
});
@@ -74,16 +87,22 @@ export default {
submitImport() {
this.isLoading = true;
return importProjectMembers(this.projectId, this.projectToBeImported.id)
- .then(this.showToastMessage)
+ .then(this.onInviteSuccess)
.catch(this.showErrorAlert)
.finally(() => {
this.isLoading = false;
this.projectToBeImported = {};
});
},
+ onInviteSuccess() {
+ if (this.reloadPageOnSubmit) {
+ reloadOnInvitationSuccess();
+ } else {
+ this.showToastMessage();
+ }
+ },
showToastMessage() {
this.$toast.show(this.$options.i18n.successMessage, this.$options.toastOptions);
-
this.closeModal();
},
showErrorAlert() {
diff --git a/app/assets/javascripts/invite_members/components/invite_group_notification.vue b/app/assets/javascripts/invite_members/components/invite_group_notification.vue
new file mode 100644
index 00000000000..767675cc64c
--- /dev/null
+++ b/app/assets/javascripts/invite_members/components/invite_group_notification.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { GROUP_MODAL_ALERT_BODY } from '../constants';
+
+const SHARE_GROUP_LINK =
+ 'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group';
+
+export default {
+ SHARE_GROUP_LINK,
+ name: 'InviteGroupNotification',
+ components: { GlAlert, GlSprintf, GlLink },
+ inject: ['freeUsersLimit'],
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ body: GROUP_MODAL_ALERT_BODY,
+ },
+};
+</script>
+
+<template>
+ <gl-alert variant="warning" :dismissible="false">
+ <gl-sprintf :message="$options.i18n.body">
+ <template #link="{ content }">
+ <gl-link :href="$options.SHARE_GROUP_LINK" target="_blank" class="gl-label-link">{{
+ content
+ }}</gl-link>
+ </template>
+
+ <template #count>{{ freeUsersLimit }}</template>
+ </gl-sprintf>
+ </gl-alert>
+</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 2ad4bb1a11a..3be3b9df747 100644
--- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
@@ -6,13 +6,19 @@ import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_b
import { GROUP_FILTERS, GROUP_MODAL_LABELS } from '../constants';
import eventHub from '../event_hub';
import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '../utils/trigger_successful_invite_alert';
import GroupSelect from './group_select.vue';
+import InviteGroupNotification from './invite_group_notification.vue';
export default {
name: 'InviteMembersModal',
components: {
GroupSelect,
InviteModalBase,
+ InviteGroupNotification,
},
props: {
id: {
@@ -31,6 +37,10 @@ export default {
type: String,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
accessLevels: {
type: Object,
required: true,
@@ -57,6 +67,15 @@ export default {
type: Array,
required: true,
},
+ freeUserCapEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ reloadPageOnSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -85,6 +104,10 @@ export default {
},
},
mounted() {
+ if (this.reloadPageOnSubmit) {
+ displaySuccessfulInvitationAlert();
+ }
+
eventHub.$on('openGroupModal', () => {
this.openModal();
});
@@ -114,7 +137,7 @@ export default {
expires_at: expiresAt,
})
.then(() => {
- this.showSuccessMessage();
+ this.onInviteSuccess();
})
.catch((e) => {
this.showInvalidFeedbackMessage(e);
@@ -128,6 +151,13 @@ export default {
this.isLoading = false;
this.groupToBeSharedWith = {};
},
+ onInviteSuccess() {
+ if (this.reloadPageOnSubmit) {
+ reloadOnInvitationSuccess();
+ } else {
+ this.showSuccessMessage();
+ }
+ },
showSuccessMessage() {
this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
this.closeModal();
@@ -155,9 +185,14 @@ export default {
:root-group-id="rootId"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
+ :full-path="fullPath"
@reset="resetFields"
@submit="sendInvite"
>
+ <template #alert>
+ <invite-group-notification v-if="freeUserCapEnabled" :name="name" />
+ </template>
+
<template #select>
<group-select
v-model="groupToBeSharedWith"
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 f61e822bf7e..fbb547c28ff 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -29,6 +29,10 @@ import eventHub from '../event_hub';
import { responseFromSuccess } from '../utils/response_message_parser';
import { memberName } from '../utils/member_utils';
import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '../utils/trigger_successful_invite_alert';
import ModalConfetti from './confetti.vue';
import MembersTokenSelect from './members_token_select.vue';
import UserLimitNotification from './user_limit_notification.vue';
@@ -98,11 +102,20 @@ export default {
type: Array,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
usersLimitDataset: {
type: Object,
required: false,
default: () => ({}),
},
+ reloadPageOnSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -119,7 +132,7 @@ export default {
selectedAccessLevel: undefined,
errorsLimit: 2,
isErrorsSectionExpanded: false,
- emptyInvitesError: false,
+ shouldShowEmptyInvitesAlert: false,
};
},
computed: {
@@ -204,12 +217,15 @@ export default {
count: this.errorsExpanded.length,
});
},
+ formGroupDescription() {
+ return this.invalidFeedbackMessage ? null : this.$options.labels.placeHolder;
+ },
},
watch: {
isEmptyInvites: {
handler(updatedValue) {
// nothing to do if the invites are **still** empty and the emptyInvites were never set from submit
- if (!updatedValue && !this.emptyInvitesError) {
+ if (!updatedValue && !this.shouldShowEmptyInvitesAlert) {
return;
}
@@ -218,6 +234,10 @@ export default {
},
},
mounted() {
+ if (this.reloadPageOnSubmit) {
+ displaySuccessfulInvitationAlert();
+ }
+
eventHub.$on('openModal', (options) => {
this.openModal(options);
if (this.isOnLearnGitlab) {
@@ -258,16 +278,17 @@ export default {
const tracking = new ExperimentTracking(experimentName);
tracking.event(eventName);
},
- showEmptyInvitesError() {
- this.invalidFeedbackMessage = this.$options.labels.emptyInvitesErrorText;
- this.emptyInvitesError = true;
+ showEmptyInvitesAlert() {
+ this.invalidFeedbackMessage = this.$options.labels.placeHolder;
+ this.shouldShowEmptyInvitesAlert = true;
+ this.$refs.alerts.focus();
},
sendInvite({ accessLevel, expiresAt }) {
this.isLoading = true;
this.clearValidation();
if (!this.isEmptyInvites) {
- this.showEmptyInvitesError();
+ this.showEmptyInvitesAlert();
return;
}
@@ -298,7 +319,7 @@ export default {
if (error) {
this.showMemberErrors(message);
} else {
- this.showSuccessMessage();
+ this.onInviteSuccess();
}
})
.catch((e) => this.showInvalidFeedbackMessage(e))
@@ -308,6 +329,7 @@ export default {
},
showMemberErrors(message) {
this.invalidMembers = message;
+ this.$refs.alerts.focus();
},
tokenName(username) {
// initial token creation hits this and nothing is found... so safe navigation
@@ -322,6 +344,7 @@ export default {
resetFields() {
this.clearValidation();
this.isLoading = false;
+ this.shouldShowEmptyInvitesAlert = false;
this.newUsersToInvite = [];
this.selectedTasksToBeDone = [];
[this.selectedTaskProject] = this.projects;
@@ -329,6 +352,13 @@ export default {
changeSelectedTaskProject(project) {
this.selectedTaskProject = project;
},
+ onInviteSuccess() {
+ if (this.reloadPageOnSubmit) {
+ reloadOnInvitationSuccess();
+ } else {
+ this.showSuccessMessage();
+ }
+ },
showSuccessMessage() {
if (this.isOnLearnGitlab) {
eventHub.$emit('showSuccessfulInvitationsAlert');
@@ -347,7 +377,7 @@ export default {
},
clearEmptyInviteError() {
this.invalidFeedbackMessage = '';
- this.emptyInvitesError = false;
+ this.shouldShowEmptyInvitesAlert = false;
},
removeToken(token) {
delete this.invalidMembers[memberName(token)];
@@ -370,12 +400,13 @@ export default {
:help-link="helpLink"
:label-intro-text="labelIntroText"
:label-search-field="$options.labels.searchField"
- :form-group-description="$options.labels.placeHolder"
+ :form-group-description="formGroupDescription"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
:new-users-to-invite="newUsersToInvite"
:root-group-id="rootId"
:users-limit-dataset="usersLimitDataset"
+ :full-path="fullPath"
@reset="resetFields"
@submit="sendInvite"
@access-level="onAccessLevelUpdate"
@@ -390,59 +421,77 @@ export default {
</template>
<template #alert>
- <gl-alert
- v-if="hasInvalidMembers"
- variant="danger"
- :dismissible="false"
- :title="memberErrorTitle"
- data-testid="alert-member-error"
- >
- {{ $options.labels.memberErrorListText }}
- <ul class="gl-pl-5 gl-mb-0">
- <li v-for="error in errorsLimited" :key="error.member" data-testid="errors-limited-item">
- <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
- </li>
- </ul>
- <template v-if="shouldErrorsSectionExpand">
- <gl-collapse v-model="isErrorsSectionExpanded">
- <ul class="gl-pl-5 gl-mb-0">
- <li
- v-for="error in errorsExpanded"
- :key="error.member"
- data-testid="errors-expanded-item"
- >
- <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
- </li>
- </ul>
- </gl-collapse>
- <gl-button
- class="gl-text-decoration-none! gl-shadow-none! gl-mt-3"
- data-testid="accordion-button"
- variant="link"
- @click="toggleErrorExpansion"
- >
- {{ errorCollapseText }}
- <gl-icon
- name="chevron-down"
- class="gl-transition-medium"
- :class="{ 'gl-rotate-180': isErrorsSectionExpanded }"
- />
- </gl-button>
- </template>
- </gl-alert>
- <user-limit-notification
- v-else-if="showUserLimitNotification"
- :limit-variant="limitVariant"
- :users-limit-dataset="usersLimitDataset"
- />
+ <div ref="alerts" tabindex="-1">
+ <gl-alert
+ v-if="shouldShowEmptyInvitesAlert"
+ id="empty-invites-alert"
+ class="gl-mb-4"
+ variant="danger"
+ :dismissible="false"
+ data-testid="empty-invites-alert"
+ >
+ {{ $options.labels.emptyInvitesAlertText }}
+ </gl-alert>
+ <gl-alert
+ v-if="hasInvalidMembers"
+ class="gl-mb-4"
+ variant="danger"
+ :dismissible="false"
+ :title="memberErrorTitle"
+ data-testid="alert-member-error"
+ >
+ {{ $options.labels.memberErrorListText }}
+ <ul class="gl-pl-5 gl-mb-0">
+ <li
+ v-for="error in errorsLimited"
+ :key="error.member"
+ data-testid="errors-limited-item"
+ >
+ <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
+ </li>
+ </ul>
+ <template v-if="shouldErrorsSectionExpand">
+ <gl-collapse v-model="isErrorsSectionExpanded">
+ <ul class="gl-pl-5 gl-mb-0">
+ <li
+ v-for="error in errorsExpanded"
+ :key="error.member"
+ data-testid="errors-expanded-item"
+ >
+ <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
+ </li>
+ </ul>
+ </gl-collapse>
+ <gl-button
+ class="gl-text-decoration-none! gl-shadow-none! gl-mt-3"
+ data-testid="accordion-button"
+ variant="link"
+ @click="toggleErrorExpansion"
+ >
+ {{ errorCollapseText }}
+ <gl-icon
+ name="chevron-down"
+ class="gl-transition-medium"
+ :class="{ 'gl-rotate-180': isErrorsSectionExpanded }"
+ />
+ </gl-button>
+ </template>
+ </gl-alert>
+ <user-limit-notification
+ v-else-if="showUserLimitNotification"
+ :limit-variant="limitVariant"
+ :users-limit-dataset="usersLimitDataset"
+ />
+ </div>
</template>
- <template #select="{ exceptionState, labelId }">
+ <template #select="{ exceptionState, inputId }">
<members-token-select
v-model="newUsersToInvite"
class="gl-mb-2"
+ aria-labelledby="empty-invites-alert"
+ :input-id="inputId"
:exception-state="exceptionState"
- :aria-labelledby="labelId"
:users-filter="usersFilter"
:filter-id="filterId"
:invalid-members="invalidMembers"
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 e3511a49fc5..2cbd681c67d 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -1,14 +1,5 @@
<script>
-import {
- GlFormGroup,
- GlModal,
- GlDropdown,
- GlDropdownItem,
- GlDatepicker,
- GlLink,
- GlSprintf,
- GlFormInput,
-} from '@gitlab/ui';
+import { GlFormGroup, GlFormSelect, GlModal, GlDatepicker, GlLink, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking';
import { sprintf } from '~/locale';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -37,13 +28,11 @@ const DEFAULT_SLOTS = [
export default {
components: {
GlFormGroup,
+ GlFormSelect,
GlDatepicker,
GlLink,
GlModal,
- GlDropdown,
- GlDropdownItem,
GlSprintf,
- GlFormInput,
ContentTransition,
},
mixins: [Tracking.mixin()],
@@ -141,14 +130,23 @@ export default {
};
},
computed: {
+ accessLevelsOptions() {
+ return Object.entries(this.accessLevels).map(([text, value]) => ({ text, value }));
+ },
introText() {
return sprintf(this.labelIntroText, { name: this.name });
},
exceptionState() {
return this.invalidFeedbackMessage ? false : null;
},
- selectLabelId() {
- return `${this.modalId}_select`;
+ selectId() {
+ return `${this.modalId}_search`;
+ },
+ dropdownId() {
+ return `${this.modalId}_dropdown`;
+ },
+ datepickerId() {
+ return `${this.modalId}_expires_at`;
},
selectedRoleName() {
return Object.keys(this.accessLevels).find(
@@ -218,9 +216,6 @@ export default {
this.$emit('cancel');
},
- changeSelectedItem(item) {
- this.selectedAccessLevel = item;
- },
onSubmit(e) {
// We never want to hide when submitting
e.preventDefault();
@@ -279,64 +274,50 @@ export default {
<slot name="alert"></slot>
<gl-form-group
+ :label="labelSearchField"
+ :label-for="selectId"
:invalid-feedback="invalidFeedbackMessage"
:state="exceptionState"
:description="formGroupDescription"
data-testid="members-form-group"
>
- <label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label>
- <slot name="select" v-bind="{ exceptionState, labelId: selectLabelId }"></slot>
+ <slot name="select" v-bind="{ exceptionState, inputId: selectId }"></slot>
</gl-form-group>
- <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"
+ <gl-form-group
+ class="gl-w-half gl-xs-w-full"
+ :label="$options.ACCESS_LEVEL"
+ :label-for="dropdownId"
+ >
+ <template #description>
+ <gl-sprintf :message="$options.READ_MORE_TEXT">
+ <template #link="{ content }">
+ <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ <gl-form-select
+ :id="dropdownId"
+ v-model="selectedAccessLevel"
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.READ_MORE_TEXT">
- <template #link="{ content }">
- <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </div>
+ :options="accessLevelsOptions"
+ />
+ </gl-form-group>
- <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-form-group
+ class="gl-w-half gl-xs-w-full"
+ :label="$options.ACCESS_EXPIRE_DATE"
+ :label-for="datepickerId"
+ >
<gl-datepicker
v-model="selectedDate"
- class="gl-display-inline!"
+ :input-id="datepickerId"
+ class="gl-display-block!"
: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>
+ />
+ </gl-form-group>
+
<slot name="form-after"></slot>
</template>
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index 2ddb04e1eeb..68602068699 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -49,6 +49,11 @@ export default {
type: Object,
required: true,
},
+ inputId: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -84,6 +89,13 @@ export default {
hasInvalidMembers() {
return !isEmpty(this.invalidMembers);
},
+ textInputAttrs() {
+ return {
+ 'data-testid': 'members-token-select-input',
+ 'data-qa-selector': 'members_token_select_input',
+ id: this.inputId,
+ };
+ },
},
watch: {
// We might not really want this to be *reactive* since we want the "class" state to be
@@ -183,10 +195,7 @@ export default {
:hide-dropdown-with-no-items="hideDropdownWithNoItems"
:placeholder="placeholderText"
:aria-labelledby="ariaLabelledby"
- :text-input-attrs="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
- 'data-testid': 'members-token-select-input',
- 'data-qa-selector': 'members_token_select_input',
- } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :text-input-attrs="textInputAttrs"
@blur="handleBlur"
@text-input="handleTextInput"
@input="handleInput"
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index de7b1019782..a894eb24d38 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -9,6 +9,7 @@ export const INVITE_MEMBERS_FOR_TASK = {
view: 'modal_opened_from_email',
submit: 'submit',
};
+export const TOAST_MESSAGE_LOCALSTORAGE_KEY = 'members_invited_successfully';
export const GROUP_FILTERS = {
ALL: 'all',
@@ -57,6 +58,10 @@ export const GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
);
+export const GROUP_MODAL_ALERT_BODY = s__(
+ 'InviteMembersModal| Inviting a group %{linkStart}adds its members to your group%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit.',
+);
+
export const GROUP_SEARCH_FIELD = s__('InviteMembersModal|Select a group to invite');
export const GROUP_PLACEHOLDER = s__('InviteMembersModal|Search for a group to invite');
@@ -77,9 +82,7 @@ export const MEMBER_ERROR_LIST_TEXT = s__(
);
export const COLLAPSED_ERRORS = s__('InviteMembersModal|Show more (%{count})');
export const EXPANDED_ERRORS = s__('InviteMembersModal|Show less');
-export const EMPTY_INVITES_ERROR_TEXT = s__(
- 'InviteMembersModal|Please select members or type email addresses to invite',
-);
+export const EMPTY_INVITES_ALERT_TEXT = s__('InviteMembersModal|Please add members to invite');
export const MEMBER_MODAL_LABELS = {
modal: {
@@ -117,7 +120,7 @@ export const MEMBER_MODAL_LABELS = {
memberErrorListText: MEMBER_ERROR_LIST_TEXT,
collapsedErrors: COLLAPSED_ERRORS,
expandedErrors: EXPANDED_ERRORS,
- emptyInvitesErrorText: EMPTY_INVITES_ERROR_TEXT,
+ emptyInvitesAlertText: EMPTY_INVITES_ALERT_TEXT,
};
export const GROUP_MODAL_LABELS = {
diff --git a/app/assets/javascripts/invite_members/init_import_project_members_modal.js b/app/assets/javascripts/invite_members/init_import_project_members_modal.js
index daaa1315884..227d8395250 100644
--- a/app/assets/javascripts/invite_members/init_import_project_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_import_project_members_modal.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import ImportProjectMembersModal from '~/invite_members/components/import_project_members_modal.vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
export default function initImportProjectMembersModal() {
const el = document.querySelector('.js-import-project-members-modal');
@@ -8,7 +9,7 @@ export default function initImportProjectMembersModal() {
return false;
}
- const { projectId, projectName } = el.dataset;
+ const { projectId, projectName, reloadPageOnSubmit } = el.dataset;
return new Vue({
el,
@@ -17,6 +18,7 @@ export default function initImportProjectMembersModal() {
props: {
projectId,
projectName,
+ reloadPageOnSubmit: parseBoolean(reloadPageOnSubmit),
},
}),
});
diff --git a/app/assets/javascripts/invite_members/init_invite_groups_modal.js b/app/assets/javascripts/invite_members/init_invite_groups_modal.js
index be1576ad0b0..53b756b610f 100644
--- a/app/assets/javascripts/invite_members/init_invite_groups_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_groups_modal.js
@@ -28,6 +28,9 @@ export default function initInviteGroupsModal() {
return new Vue({
el,
+ provide: {
+ freeUsersLimit: parseInt(el.dataset.freeUsersLimit, 10),
+ },
render: (createElement) =>
createElement(InviteGroupsModal, {
props: {
@@ -38,6 +41,8 @@ export default function initInviteGroupsModal() {
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
invalidGroups: JSON.parse(el.dataset.invalidGroups || '[]'),
+ freeUserCapEnabled: parseBoolean(el.dataset.freeUserCapEnabled),
+ reloadPageOnSubmit: parseBoolean(el.dataset.reloadPageOnSubmit),
},
}),
});
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index a4be3f205a3..842ab07f368 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -41,6 +41,7 @@ export default (function initInviteMembersModal() {
usersLimitDataset: convertObjectPropsToCamelCase(
JSON.parse(el.dataset.usersLimitDataset || '{}'),
),
+ reloadPageOnSubmit: parseBoolean(el.dataset.reloadPageOnSubmit),
},
}),
});
diff --git a/app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js b/app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js
new file mode 100644
index 00000000000..4d3a7951265
--- /dev/null
+++ b/app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js
@@ -0,0 +1,23 @@
+import { createAlert } from '~/flash';
+import AccessorUtilities from '~/lib/utils/accessor';
+
+import { TOAST_MESSAGE_LOCALSTORAGE_KEY, TOAST_MESSAGE_SUCCESSFUL } from '../constants';
+
+export function displaySuccessfulInvitationAlert() {
+ if (!AccessorUtilities.canUseLocalStorage()) {
+ return;
+ }
+
+ const showAlert = Boolean(localStorage.getItem(TOAST_MESSAGE_LOCALSTORAGE_KEY));
+ if (showAlert) {
+ localStorage.removeItem(TOAST_MESSAGE_LOCALSTORAGE_KEY);
+ createAlert({ message: TOAST_MESSAGE_SUCCESSFUL, variant: 'info' });
+ }
+}
+
+export function reloadOnInvitationSuccess() {
+ if (AccessorUtilities.canUseLocalStorage()) {
+ localStorage.setItem(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true');
+ }
+ window.location.reload();
+}