Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
commitb595cb0c1dec83de5bdee18284abe86614bed33b (patch)
tree8c3d4540f193c5ff98019352f554e921b3a41a72 /app/assets/javascripts/invite_members
parent2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff)
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/invite_members')
-rw-r--r--app/assets/javascripts/invite_members/components/group_select.vue1
-rw-r--r--app/assets/javascripts/invite_members/components/import_project_members_modal.vue (renamed from app/assets/javascripts/invite_members/components/import_a_project_modal.vue)122
-rw-r--r--app/assets/javascripts/invite_members/components/import_project_members_trigger.vue34
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue64
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue8
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue31
-rw-r--r--app/assets/javascripts/invite_members/constants.js4
-rw-r--r--app/assets/javascripts/invite_members/init_import_a_project_modal.js23
-rw-r--r--app/assets/javascripts/invite_members/init_import_project_members_modal.js23
-rw-r--r--app/assets/javascripts/invite_members/init_import_project_members_trigger.js20
-rw-r--r--app/assets/javascripts/invite_members/utils/member_utils.js4
-rw-r--r--app/assets/javascripts/invite_members/utils/response_message_parser.js31
12 files changed, 239 insertions, 126 deletions
diff --git a/app/assets/javascripts/invite_members/components/group_select.vue b/app/assets/javascripts/invite_members/components/group_select.vue
index fc14b2eba6a..e7f5211dc25 100644
--- a/app/assets/javascripts/invite_members/components/group_select.vue
+++ b/app/assets/javascripts/invite_members/components/group_select.vue
@@ -136,6 +136,7 @@ export default {
v-for="group in groups"
:key="group.id"
:name="group.name"
+ data-qa-selector="group_select_dropdown_item"
@click="selectGroup(group)"
>
<gl-avatar-labeled
diff --git a/app/assets/javascripts/invite_members/components/import_a_project_modal.vue b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
index fb6c376cfe6..31b7fd4cc42 100644
--- a/app/assets/javascripts/invite_members/components/import_a_project_modal.vue
+++ b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
@@ -1,21 +1,20 @@
<script>
-import { GlButton, GlFormGroup, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { GlFormGroup, GlModal, GlSprintf } from '@gitlab/ui';
import { uniqueId } from 'lodash';
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 ProjectSelect from './project_select.vue';
export default {
+ name: 'ImportProjectMembersModal',
components: {
- GlButton,
GlFormGroup,
GlModal,
GlSprintf,
ProjectSelect,
},
- directives: {
- GlModal: GlModalDirective,
- },
props: {
projectId: {
type: String,
@@ -45,8 +44,33 @@ export default {
validationState() {
return this.invalidFeedbackMessage === '' ? null : false;
},
+ actionPrimary() {
+ return {
+ text: this.$options.i18n.modalPrimaryButton,
+ attributes: {
+ variant: 'confirm',
+ disabled: this.importDisabled,
+ loading: this.isLoading,
+ },
+ };
+ },
+ actionCancel() {
+ return { text: this.$options.i18n.modalCancelButton };
+ },
+ },
+ mounted() {
+ eventHub.$on('openProjectMembersModal', () => {
+ this.openModal();
+ });
},
methods: {
+ openModal() {
+ this.$root.$emit(BV_SHOW_MODAL, this.$options.modalId);
+ },
+ resetFields() {
+ this.invalidFeedbackMessage = '';
+ this.projectToBeImported = {};
+ },
submitImport() {
this.isLoading = true;
return importProjectMembers(this.projectId, this.projectToBeImported.id)
@@ -57,11 +81,6 @@ export default {
this.projectToBeImported = {};
});
},
- closeModal() {
- this.invalidFeedbackMessage = '';
-
- this.$refs.modal.hide();
- },
showToastMessage() {
this.$toast.show(this.$options.i18n.successMessage, this.$options.toastOptions);
@@ -79,7 +98,6 @@ export default {
};
},
i18n: {
- buttonText: s__('ImportAProjectModal|Import from a project'),
projectLabel: __('Project'),
modalTitle: s__('ImportAProjectModal|Import members from another project'),
modalIntro: s__(
@@ -95,63 +113,37 @@ export default {
},
projectSelectLabelId: 'project-select',
modalId: uniqueId('import-a-project-modal-'),
- formClasses: 'gl-mt-3 gl-sm-w-auto gl-w-full',
- buttonClasses: 'gl-w-full',
};
</script>
<template>
- <form :class="$options.formClasses">
- <gl-button v-gl-modal="$options.modalId" :class="$options.buttonClasses" variant="default">{{
- $options.i18n.buttonText
- }}</gl-button>
-
- <gl-modal
- ref="modal"
- :modal-id="$options.modalId"
- size="sm"
- :title="$options.i18n.modalTitle"
- ok-variant="danger"
- footer-class="gl-bg-gray-10 gl-p-5"
+ <gl-modal
+ ref="modal"
+ :modal-id="$options.modalId"
+ size="sm"
+ :title="$options.i18n.modalTitle"
+ :action-primary="actionPrimary"
+ :action-cancel="actionCancel"
+ @primary="submitImport"
+ @hidden="resetFields"
+ >
+ <p ref="modalIntro">
+ <gl-sprintf :message="modalIntro">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-form-group
+ :invalid-feedback="invalidFeedbackMessage"
+ :state="validationState"
+ data-testid="form-group"
>
- <div>
- <p ref="modalIntro">
- <gl-sprintf :message="modalIntro">
- <template #strong="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- </p>
- <gl-form-group
- :invalid-feedback="invalidFeedbackMessage"
- :state="validationState"
- data-testid="form-group"
- >
- <label :id="$options.projectSelectLabelId" class="col-form-label">{{
- $options.i18n.projectLabel
- }}</label>
- <project-select v-model="projectToBeImported" />
- </gl-form-group>
- <p>{{ $options.i18n.modalHelpText }}</p>
- </div>
- <template #modal-footer>
- <div
- class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0"
- >
- <gl-button data-testid="cancel-button" @click="closeModal">
- {{ $options.i18n.modalCancelButton }}
- </gl-button>
- <div class="gl-mr-3"></div>
- <gl-button
- :disabled="importDisabled"
- :loading="isLoading"
- variant="confirm"
- data-testid="import-button"
- @click="submitImport"
- >{{ $options.i18n.modalPrimaryButton }}</gl-button
- >
- </div>
- </template>
- </gl-modal>
- </form>
+ <label :id="$options.projectSelectLabelId" class="col-form-label">{{
+ $options.i18n.projectLabel
+ }}</label>
+ <project-select v-model="projectToBeImported" />
+ </gl-form-group>
+ <p>{{ $options.i18n.modalHelpText }}</p>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/invite_members/components/import_project_members_trigger.vue b/app/assets/javascripts/invite_members/components/import_project_members_trigger.vue
new file mode 100644
index 00000000000..5781abb41b7
--- /dev/null
+++ b/app/assets/javascripts/invite_members/components/import_project_members_trigger.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import eventHub from '../event_hub';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ displayText: {
+ type: String,
+ required: false,
+ default: s__('ImportAProjectModal|Import from a project'),
+ },
+ classes: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ methods: {
+ openModal() {
+ eventHub.$emit('openProjectMembersModal');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button :class="classes" @click="openModal">
+ {{ displayText }}
+ </gl-button>
+</template>
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 d597c7e53bb..b71cfbb6112 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -7,12 +7,13 @@ import {
GlSprintf,
GlFormCheckboxGroup,
} from '@gitlab/ui';
-import { partition, isString, uniqueId } from 'lodash';
+import { partition, isString, uniqueId, isEmpty } 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 { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { getParameterValues } from '~/lib/utils/url_utility';
+import { n__ } from '~/locale';
import {
CLOSE_TO_LIMIT_COUNT,
USERS_FILTER_ALL,
@@ -21,7 +22,8 @@ import {
LEARN_GITLAB,
} from '../constants';
import eventHub from '../event_hub';
-import { responseMessageFromSuccess } from '../utils/response_message_parser';
+import { responseFromSuccess } from '../utils/response_message_parser';
+import { memberName } from '../utils/member_utils';
import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message';
import ModalConfetti from './confetti.vue';
import MembersTokenSelect from './members_token_select.vue';
@@ -101,6 +103,7 @@ export default {
isLoading: false,
modalId: uniqueId('invite-members-modal-'),
newUsersToInvite: [],
+ invalidMembers: {},
selectedTasksToBeDone: [],
selectedTaskProject: this.projects[0],
source: 'unknown',
@@ -125,6 +128,16 @@ export default {
inviteDisabled() {
return this.newUsersToInvite.length === 0;
},
+ hasInvalidMembers() {
+ return !isEmpty(this.invalidMembers);
+ },
+ memberErrorTitle() {
+ return n__(
+ "InviteMembersModal|The following member couldn't be invited",
+ "InviteMembersModal|The following %d members couldn't be invited",
+ Object.keys(this.invalidMembers).length,
+ );
+ },
tasksToBeDoneEnabled() {
return (
(getParameterValues('open_modal')[0] === 'invite_members_for_task' ||
@@ -218,7 +231,7 @@ export default {
},
sendInvite({ accessLevel, expiresAt }) {
this.isLoading = true;
- this.invalidFeedbackMessage = '';
+ this.clearValidation();
const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite();
@@ -242,12 +255,10 @@ export default {
...userId,
})
.then((response) => {
- const message = responseMessageFromSuccess(response);
+ const { error, message } = responseFromSuccess(response);
- if (message) {
- this.showInvalidFeedbackMessage({
- response: { data: { message } },
- });
+ if (error) {
+ this.showMemberErrors(message);
} else {
this.showSuccessMessage();
}
@@ -257,6 +268,13 @@ export default {
this.isLoading = false;
});
},
+ showMemberErrors(message) {
+ this.invalidMembers = message;
+ },
+ tokenName(username) {
+ // initial token creation hits this and nothing is found... so safe navigation
+ return this.newUsersToInvite.find((member) => memberName(member) === username)?.name;
+ },
trackinviteMembersForTask() {
const label = 'selected_tasks_to_be_done';
const property = this.selectedTasksToBeDone.join(',');
@@ -264,8 +282,8 @@ export default {
tracking.event(INVITE_MEMBERS_FOR_TASK.submit);
},
resetFields() {
+ this.clearValidation();
this.isLoading = false;
- this.invalidFeedbackMessage = '';
this.newUsersToInvite = [];
this.selectedTasksToBeDone = [];
[this.selectedTaskProject] = this.projects;
@@ -287,6 +305,11 @@ export default {
},
clearValidation() {
this.invalidFeedbackMessage = '';
+ this.invalidMembers = {};
+ },
+ removeToken(token) {
+ delete this.invalidMembers[memberName(token)];
+ this.invalidMembers = { ...this.invalidMembers };
},
},
labels: MEMBER_MODAL_LABELS,
@@ -324,23 +347,40 @@ export default {
<modal-confetti v-if="isCelebration" />
</template>
- <template #user-limit-notification>
+ <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">
+ <li v-for="(error, member) in invalidMembers" :key="member">
+ <strong>{{ tokenName(member) }}:</strong> {{ error }}
+ </li>
+ </ul>
+ </gl-alert>
<user-limit-notification
+ v-else
:close-to-limit="closeToLimit"
:reached-limit="reachedLimit"
:users-limit-dataset="usersLimitDataset"
/>
</template>
- <template #select="{ validationState, labelId }">
+ <template #select="{ exceptionState, labelId }">
<members-token-select
v-model="newUsersToInvite"
class="gl-mb-2"
- :validation-state="validationState"
+ :exception-state="exceptionState"
:aria-labelledby="labelId"
:users-filter="usersFilter"
:filter-id="filterId"
+ :invalid-members="invalidMembers"
@clear="clearValidation"
+ @token-remove="removeToken"
/>
</template>
<template #form-after>
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 90d266c3155..f917ebc35c2 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -159,7 +159,7 @@ export default {
introText() {
return sprintf(this.labelIntroText, { name: this.name });
},
- validationState() {
+ exceptionState() {
return this.invalidFeedbackMessage ? false : null;
},
selectLabelId() {
@@ -306,11 +306,11 @@ export default {
<slot name="intro-text-after"></slot>
</div>
- <slot name="user-limit-notification"></slot>
+ <slot name="alert"></slot>
<gl-form-group
:invalid-feedback="invalidFeedbackMessage"
- :state="validationState"
+ :state="exceptionState"
data-testid="members-form-group"
>
<template #description>
@@ -320,7 +320,7 @@ export default {
<label :id="selectLabelId" :class="selectLabelClass">{{ labelSearchField }}</label>
<gl-form-input v-if="reachedLimit" data-testid="disabled-input" disabled />
- <slot v-else name="select" v-bind="{ validationState, labelId: selectLabelId }"></slot>
+ <slot v-else name="select" v-bind="{ exceptionState, labelId: selectLabelId }"></slot>
</gl-form-group>
<template v-if="!reachedLimit">
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 30c9294344e..b2bcb9a5906 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -3,6 +3,7 @@ import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlIcon, GlSprintf } from '@
import { debounce } from 'lodash';
import { __ } from '~/locale';
import { getUsers } from '~/rest_api';
+import { memberName } from '../utils/member_utils';
import { SEARCH_DELAY, USERS_FILTER_ALL, USERS_FILTER_SAML_PROVIDER_ID } from '../constants';
export default {
@@ -23,7 +24,7 @@ export default {
type: String,
required: true,
},
- validationState: {
+ exceptionState: {
type: Boolean,
required: false,
default: false,
@@ -38,6 +39,10 @@ export default {
required: false,
default: null,
},
+ invalidMembers: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -109,13 +114,18 @@ export default {
this.hasBeenFocused = true;
},
- handleTokenRemove() {
+ handleTokenRemove(value) {
if (this.selectedTokens.length) {
+ this.$emit('token-remove', value);
+
return;
}
this.$emit('clear');
},
+ hasError(token) {
+ return Object.keys(this.invalidMembers).includes(memberName(token));
+ },
},
defaultQueryOptions: { without_project_bots: true, active: true },
i18n: {
@@ -127,7 +137,7 @@ export default {
<template>
<gl-token-selector
v-model="selectedTokens"
- :state="validationState"
+ :state="exceptionState"
:dropdown-items="users"
:loading="loading"
:allow-user-defined-tokens="emailIsValid"
@@ -145,8 +155,19 @@ export default {
@token-remove="handleTokenRemove"
>
<template #token-content="{ token }">
- <gl-icon v-if="validationState === false" name="error" :size="16" class="gl-mr-2" />
- <gl-avatar v-else-if="token.avatar_url" :src="token.avatar_url" :size="16" />
+ <gl-icon
+ v-if="hasError(token)"
+ name="error"
+ :size="16"
+ class="gl-mr-2"
+ :data-testid="`error-icon-${token.id}`"
+ />
+ <gl-avatar
+ v-else-if="token.avatar_url"
+ :src="token.avatar_url"
+ :size="16"
+ data-testid="token-avatar"
+ />
{{ token.name }}
</template>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index beb8f5b5aab..6141e5e9e0b 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -74,6 +74,9 @@ export const INVITE_BUTTON_TEXT_DISABLED = s__('InviteMembersModal|Manage member
export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel');
export const CANCEL_BUTTON_TEXT_DISABLED = s__('InviteMembersModal|Explore paid plans');
export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members');
+export const MEMBER_ERROR_LIST_TEXT = s__(
+ 'InviteMembersModal|Review the invite errors and try again:',
+);
export const MEMBER_MODAL_LABELS = {
modal: {
@@ -109,6 +112,7 @@ export const MEMBER_MODAL_LABELS = {
title: MEMBERS_TASKS_PROJECTS_TITLE,
},
toastMessageSuccessful: TOAST_MESSAGE_SUCCESSFUL,
+ memberErrorListText: MEMBER_ERROR_LIST_TEXT,
};
export const GROUP_MODAL_LABELS = {
diff --git a/app/assets/javascripts/invite_members/init_import_a_project_modal.js b/app/assets/javascripts/invite_members/init_import_a_project_modal.js
deleted file mode 100644
index 954347467de..00000000000
--- a/app/assets/javascripts/invite_members/init_import_a_project_modal.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import Vue from 'vue';
-import ImportAProjectModal from '~/invite_members/components/import_a_project_modal.vue';
-
-export default function initImportAProjectModal() {
- const el = document.querySelector('.js-import-a-project-modal');
-
- if (!el) {
- return false;
- }
-
- const { projectId, projectName } = el.dataset;
-
- return new Vue({
- el,
- render: (createElement) =>
- createElement(ImportAProjectModal, {
- props: {
- projectId,
- projectName,
- },
- }),
- });
-}
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
new file mode 100644
index 00000000000..daaa1315884
--- /dev/null
+++ b/app/assets/javascripts/invite_members/init_import_project_members_modal.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import ImportProjectMembersModal from '~/invite_members/components/import_project_members_modal.vue';
+
+export default function initImportProjectMembersModal() {
+ const el = document.querySelector('.js-import-project-members-modal');
+
+ if (!el) {
+ return false;
+ }
+
+ const { projectId, projectName } = el.dataset;
+
+ return new Vue({
+ el,
+ render: (createElement) =>
+ createElement(ImportProjectMembersModal, {
+ props: {
+ projectId,
+ projectName,
+ },
+ }),
+ });
+}
diff --git a/app/assets/javascripts/invite_members/init_import_project_members_trigger.js b/app/assets/javascripts/invite_members/init_import_project_members_trigger.js
new file mode 100644
index 00000000000..66a9bf118d2
--- /dev/null
+++ b/app/assets/javascripts/invite_members/init_import_project_members_trigger.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import ImportProjectMembersTrigger from '~/invite_members/components/import_project_members_trigger.vue';
+
+export default function initImportProjectMembersTrigger() {
+ const el = document.querySelector('.js-import-project-members-trigger');
+
+ if (!el) {
+ return false;
+ }
+
+ return new Vue({
+ el,
+ render: (createElement) =>
+ createElement(ImportProjectMembersTrigger, {
+ props: {
+ ...el.dataset,
+ },
+ }),
+ });
+}
diff --git a/app/assets/javascripts/invite_members/utils/member_utils.js b/app/assets/javascripts/invite_members/utils/member_utils.js
new file mode 100644
index 00000000000..d85162626f1
--- /dev/null
+++ b/app/assets/javascripts/invite_members/utils/member_utils.js
@@ -0,0 +1,4 @@
+export function memberName(member) {
+ // user defined tokens(invites by email) will have email in `name` and will not contain `username`
+ return member.username || member.name;
+}
diff --git a/app/assets/javascripts/invite_members/utils/response_message_parser.js b/app/assets/javascripts/invite_members/utils/response_message_parser.js
index db8ac303dc4..6e6431b89d9 100644
--- a/app/assets/javascripts/invite_members/utils/response_message_parser.js
+++ b/app/assets/javascripts/invite_members/utils/response_message_parser.js
@@ -1,15 +1,4 @@
-import { isString } from 'lodash';
-
-function responseKeyedMessageParsed(keyedMessage) {
- try {
- const keys = Object.keys(keyedMessage);
- const msg = keyedMessage[keys[0]];
-
- return msg;
- } catch {
- return '';
- }
-}
+import { isString, isArray } from 'lodash';
export function responseMessageFromError(response) {
if (!response?.response?.data) {
@@ -23,9 +12,9 @@ export function responseMessageFromError(response) {
return data.error || data.message?.error || data.message || '';
}
-export function responseMessageFromSuccess(response) {
+export function responseFromSuccess(response) {
if (!response?.data) {
- return '';
+ return { error: false };
}
const { data } = response;
@@ -34,11 +23,19 @@ export function responseMessageFromSuccess(response) {
const { message } = data;
if (isString(message)) {
- return message;
+ return { message, error: true };
+ }
+
+ if (isArray(message)) {
+ return { message: message[0], error: true };
}
+ // we assume object now with our keyed format
+ return { message: { ...message }, error: true };
+ }
- return responseKeyedMessageParsed(message);
+ if (data.error) {
+ return { message: data.error, error: true };
}
- return data.error || '';
+ return { error: false };
}