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>2023-08-18 13:50:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-18 13:50:51 +0300
commitdb384e6b19af03b4c3c82a5760d83a3fd79f7982 (patch)
tree34beaef37df5f47ccbcf5729d7583aae093cffa0 /app/assets/javascripts/invite_members
parent54fd7b1bad233e3944434da91d257fa7f63c3996 (diff)
Add latest changes from gitlab-org/gitlab@16-3-stable-eev16.3.0-rc42
Diffstat (limited to 'app/assets/javascripts/invite_members')
-rw-r--r--app/assets/javascripts/invite_members/components/confetti.vue1
-rw-r--r--app/assets/javascripts/invite_members/components/group_select.vue38
-rw-r--r--app/assets/javascripts/invite_members/components/import_project_members_modal.vue144
-rw-r--r--app/assets/javascripts/invite_members/components/user_limit_notification.vue19
-rw-r--r--app/assets/javascripts/invite_members/constants.js8
5 files changed, 157 insertions, 53 deletions
diff --git a/app/assets/javascripts/invite_members/components/confetti.vue b/app/assets/javascripts/invite_members/components/confetti.vue
index 2e5744afcd4..562f935e2b3 100644
--- a/app/assets/javascripts/invite_members/components/confetti.vue
+++ b/app/assets/javascripts/invite_members/components/confetti.vue
@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/multi-word-component-names -->
<script>
import confetti from 'canvas-confetti';
diff --git a/app/assets/javascripts/invite_members/components/group_select.vue b/app/assets/javascripts/invite_members/components/group_select.vue
index 1369deae3f9..42257127bbc 100644
--- a/app/assets/javascripts/invite_members/components/group_select.vue
+++ b/app/assets/javascripts/invite_members/components/group_select.vue
@@ -1,5 +1,6 @@
<script>
import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
+import axios from 'axios';
import { debounce } from 'lodash';
import { s__ } from '~/locale';
import { getGroups, getDescendentGroups } from '~/rest_api';
@@ -42,6 +43,7 @@ export default {
searchTerm: '',
pagination: {},
infiniteScrollLoading: false,
+ activeApiRequestAbortController: null,
};
},
computed: {
@@ -61,15 +63,13 @@ export default {
methods: {
retrieveGroups: debounce(async function debouncedRetrieveGroups() {
this.isFetching = true;
-
try {
const response = await this.fetchGroups();
this.pagination = this.processPagination(response);
this.groups = this.processGroups(response);
- } catch {
- this.onApiError();
- } finally {
this.isFetching = false;
+ } catch (e) {
+ this.onApiError(e);
}
}, SEARCH_DELAY),
processGroups({ data }) {
@@ -98,16 +98,32 @@ export default {
this.retrieveGroups();
},
fetchGroups(options = {}) {
+ if (this.activeApiRequestAbortController !== null) {
+ this.activeApiRequestAbortController.abort();
+ }
+
+ this.activeApiRequestAbortController = new AbortController();
+
const combinedOptions = {
...this.$options.defaultFetchOptions,
...options,
};
+ const axiosConfig = {
+ signal: this.activeApiRequestAbortController.signal,
+ };
+
switch (this.groupsFilter) {
case GROUP_FILTERS.DESCENDANT_GROUPS:
- return getDescendentGroups(this.parentGroupId, this.searchTerm, combinedOptions);
+ return getDescendentGroups(
+ this.parentGroupId,
+ this.searchTerm,
+ combinedOptions,
+ undefined,
+ axiosConfig,
+ );
default:
- return getGroups(this.searchTerm, combinedOptions);
+ return getGroups(this.searchTerm, combinedOptions, undefined, axiosConfig);
}
},
async onBottomReached() {
@@ -117,13 +133,15 @@ export default {
const response = await this.fetchGroups({ page: this.pagination.page + 1 });
this.pagination = this.processPagination(response);
this.groups.push(...this.processGroups(response));
- } catch {
- this.onApiError();
- } finally {
this.infiniteScrollLoading = false;
+ } catch (e) {
+ this.onApiError(e);
}
},
- onApiError() {
+ onApiError(error) {
+ if (axios.isCancel(error)) return;
+ this.isFetching = false;
+ this.infiniteScrollLoading = false;
this.$emit('error', this.$options.i18n.errorMessage);
},
},
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 66d4a9ccc07..5599ad276f0 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
@@ -1,5 +1,5 @@
<script>
-import { GlFormGroup, GlModal, GlSprintf } from '@gitlab/ui';
+import { GlFormGroup, GlModal, GlSprintf, GlAlert, GlCollapse, GlIcon, GlButton } from '@gitlab/ui';
import { uniqueId, isEmpty } from 'lodash';
import { importProjectMembers } from '~/api/projects_api';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
@@ -16,8 +16,10 @@ import {
PROJECT_SELECT_LABEL_ID,
IMPORT_PROJECT_MEMBERS_MODAL_TRACKING_CATEGORY,
IMPORT_PROJECT_MEMBERS_MODAL_TRACKING_LABEL,
+ MEMBER_MODAL_LABELS,
} from '../constants';
+import { responseFromSuccess } from '../utils/response_message_parser';
import UserLimitNotification from './user_limit_notification.vue';
import ProjectSelect from './project_select.vue';
@@ -27,6 +29,10 @@ export default {
GlFormGroup,
GlModal,
GlSprintf,
+ GlAlert,
+ GlCollapse,
+ GlIcon,
+ GlButton,
UserLimitNotification,
ProjectSelect,
},
@@ -60,6 +66,9 @@ export default {
return {
projectToBeImported: {},
invalidFeedbackMessage: '',
+ totalMembersCount: 0,
+ invalidMembers: {},
+ isErrorsSectionExpanded: false,
isLoading: false,
};
},
@@ -94,6 +103,40 @@ export default {
actionCancel() {
return { text: this.$options.i18n.modalCancelButton };
},
+ hasInvalidMembers() {
+ return !isEmpty(this.invalidMembers);
+ },
+ memberErrorTitle() {
+ return sprintf(
+ s__(
+ 'InviteMembersModal|The following %{errorMembersLength} out of %{totalMembersCount} members could not be added',
+ ),
+ { errorMembersLength: this.errorList.length, totalMembersCount: this.totalMembersCount },
+ );
+ },
+ errorList() {
+ return Object.entries(this.invalidMembers).map(([member, error]) => {
+ return { member, displayedMemberName: `@${member}`, message: error };
+ });
+ },
+ errorsLimited() {
+ return this.errorList.slice(0, this.$options.errorsLimit);
+ },
+ errorsExpanded() {
+ return this.errorList.slice(this.$options.errorsLimit);
+ },
+ shouldErrorsSectionExpand() {
+ return Boolean(this.errorsExpanded.length);
+ },
+ errorCollapseText() {
+ if (this.isErrorsSectionExpanded) {
+ return this.$options.labels.expandedErrors;
+ }
+
+ return sprintf(this.$options.labels.collapsedErrors, {
+ count: this.errorsExpanded.length,
+ });
+ },
},
mounted() {
if (this.reloadPageOnSubmit) {
@@ -113,21 +156,37 @@ export default {
this.$root.$emit(BV_HIDE_MODAL, this.$options.modalId);
},
resetFields() {
+ this.clearValidation();
this.invalidFeedbackMessage = '';
this.projectToBeImported = {};
},
- submitImport(e) {
+ async submitImport(event) {
// We never want to hide when submitting
- e.preventDefault();
+ event.preventDefault();
this.isLoading = true;
- return importProjectMembers(this.projectId, this.projectToBeImported.id)
- .then(this.onInviteSuccess)
- .catch(this.showErrorAlert)
- .finally(() => {
- this.isLoading = false;
- this.projectToBeImported = {};
- });
+
+ try {
+ const response = await importProjectMembers(this.projectId, this.projectToBeImported.id);
+
+ const { error, message } = responseFromSuccess(response);
+
+ if (error) {
+ this.totalMembersCount = response.data.total_members_count;
+ this.showMemberErrors(message);
+ } else {
+ this.onInviteSuccess();
+ }
+ } catch {
+ this.showErrorAlert();
+ } finally {
+ this.isLoading = false;
+ this.projectToBeImported = {};
+ }
+ },
+ showMemberErrors(message) {
+ this.invalidMembers = message;
+ this.$refs.alerts.focus();
},
onInviteSuccess() {
this.track('invite_successful');
@@ -151,6 +210,13 @@ export default {
onClose() {
this.track('click_x');
},
+ clearValidation() {
+ this.invalidFeedbackMessage = '';
+ this.invalidMembers = {};
+ },
+ toggleErrorExpansion() {
+ this.isErrorsSectionExpanded = !this.isErrorsSectionExpanded;
+ },
},
toastOptions() {
return {
@@ -173,8 +239,10 @@ export default {
defaultError: s__('ImportAProjectModal|Unable to import project members'),
successMessage: s__('ImportAProjectModal|Successfully imported'),
},
+ errorsLimit: 2,
projectSelectLabelId: PROJECT_SELECT_LABEL_ID,
modalId: uniqueId('import-a-project-modal-'),
+ labels: MEMBER_MODAL_LABELS,
};
</script>
@@ -186,18 +254,62 @@ export default {
:title="$options.i18n.modalTitle"
:action-primary="actionPrimary"
:action-cancel="actionCancel"
+ data-testid="import-project-members-modal"
no-focus-on-show
@primary="submitImport"
@hidden="resetFields"
@cancel="onCancel"
@close="onClose"
>
- <user-limit-notification
- v-if="showUserLimitNotification"
- class="gl-mb-5"
- :limit-variant="limitVariant"
- :users-limit-dataset="usersLimitDataset"
- />
+ <div ref="alerts" tabindex="-1">
+ <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"
+ class="gl-mb-5"
+ :limit-variant="limitVariant"
+ :users-limit-dataset="usersLimitDataset"
+ />
+ </div>
<p ref="modalIntro">
<gl-sprintf :message="modalIntro">
<template #strong="{ content }">
diff --git a/app/assets/javascripts/invite_members/components/user_limit_notification.vue b/app/assets/javascripts/invite_members/components/user_limit_notification.vue
index 1d061a4b81e..5d8f2ddfe15 100644
--- a/app/assets/javascripts/invite_members/components/user_limit_notification.vue
+++ b/app/assets/javascripts/invite_members/components/user_limit_notification.vue
@@ -4,15 +4,12 @@ import { n__, sprintf } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
- INFO_ALERT_TITLE,
WARNING_ALERT_TITLE,
DANGER_ALERT_TITLE,
REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
REACHED_LIMIT_VARIANT,
CLOSE_TO_LIMIT_MESSAGE,
CLOSE_TO_LIMIT_VARIANT,
- NOTIFICATION_LIMIT_MESSAGE,
- NOTIFICATION_LIMIT_VARIANT,
} from '../constants';
export default {
@@ -32,15 +29,6 @@ export default {
computed: {
limitAttributes() {
return {
- [NOTIFICATION_LIMIT_VARIANT]: {
- variant: 'info',
- title: this.notificationTitle(
- INFO_ALERT_TITLE,
- this.name,
- this.usersLimitDataset.freeUsersLimit,
- ),
- message: this.message(NOTIFICATION_LIMIT_MESSAGE, this.usersLimitDataset.freeUsersLimit),
- },
[CLOSE_TO_LIMIT_VARIANT]: {
variant: 'warning',
title: this.title(WARNING_ALERT_TITLE, this.usersLimitDataset.remainingSeats),
@@ -55,13 +43,6 @@ export default {
},
},
methods: {
- notificationTitle(titleTemplate, namespaceName, dashboardLimit) {
- return sprintf(titleTemplate, {
- namespaceName,
- dashboardLimit,
- });
- },
-
title(titleTemplate, count) {
return sprintf(titleTemplate, {
count,
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index a4fe1a413aa..1cee0c32008 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -157,9 +157,6 @@ export const GROUP_MODAL_LABELS = {
export const ON_SHOW_TRACK_LABEL = 'over_limit_modal_viewed';
export const ON_CELEBRATION_TRACK_LABEL = 'invite_celebration_modal';
-export const INFO_ALERT_TITLE = s__(
- 'InviteMembersModal|Your top-level group %{namespaceName} is over the %{dashboardLimit} user limit.',
-);
export const WARNING_ALERT_TITLE = s__(
'InviteMembersModal|You only have space for %{count} more %{members} in %{name}',
);
@@ -169,7 +166,6 @@ export const DANGER_ALERT_TITLE = s__(
export const REACHED_LIMIT_VARIANT = 'reached';
export const CLOSE_TO_LIMIT_VARIANT = 'close';
-export const NOTIFICATION_LIMIT_VARIANT = 'notification';
export const REACHED_LIMIT_MESSAGE = s__(
'InviteMembersModal|To invite new users to this top-level group, you must remove existing users. You can still add existing users from the top-level group, including any subgroups and projects.',
@@ -184,7 +180,3 @@ export const REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE = REACHED_LIMIT_MESSAGE.co
export const CLOSE_TO_LIMIT_MESSAGE = s__(
'InviteMembersModal|To get more members an owner of the group can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
);
-
-export const NOTIFICATION_LIMIT_MESSAGE = s__(
- 'InviteMembersModal|GitLab will enforce this limit in the future. If you are over %{dashboardLimit} users when enforcement begins, your top-level group will be placed in a %{freeUserLimitLinkStart}read-only state%{freeUserLimitLinkEnd}. To avoid being placed in a read-only state, reduce your top-level group to %{dashboardLimit} users or less, or purchase a paid tier.',
-);