diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-18 13:50:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-18 13:50:51 +0300 |
commit | db384e6b19af03b4c3c82a5760d83a3fd79f7982 (patch) | |
tree | 34beaef37df5f47ccbcf5729d7583aae093cffa0 /app/assets/javascripts/invite_members | |
parent | 54fd7b1bad233e3944434da91d257fa7f63c3996 (diff) |
Add latest changes from gitlab-org/gitlab@16-3-stable-eev16.3.0-rc42
Diffstat (limited to 'app/assets/javascripts/invite_members')
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.', -); |