diff options
Diffstat (limited to 'app/assets/javascripts/groups_projects/components/transfer_locations.vue')
-rw-r--r-- | app/assets/javascripts/groups_projects/components/transfer_locations.vue | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/app/assets/javascripts/groups_projects/components/transfer_locations.vue b/app/assets/javascripts/groups_projects/components/transfer_locations.vue new file mode 100644 index 00000000000..e0c8ce36e3c --- /dev/null +++ b/app/assets/javascripts/groups_projects/components/transfer_locations.vue @@ -0,0 +1,282 @@ +<script> +import { + GlAlert, + GlFormGroup, + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlDropdownDivider, + GlSearchBoxByType, + GlIntersectionObserver, + GlLoadingIcon, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; +import { s__, __ } from '~/locale'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import currentUserNamespace from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql'; + +export const i18n = { + SELECT_A_NAMESPACE: __('Select a new namespace'), + GROUPS: __('Groups'), + USERS: __('Users'), + ERROR_MESSAGE: s__( + 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.', + ), + ALERT_DISMISS_LABEL: __('Dismiss'), +}; + +export default { + name: 'TransferLocations', + components: { + GlAlert, + GlFormGroup, + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlDropdownDivider, + GlSearchBoxByType, + GlIntersectionObserver, + GlLoadingIcon, + }, + inject: ['resourceId'], + props: { + value: { + type: Object, + required: false, + default: null, + }, + groupTransferLocationsApiMethod: { + type: Function, + required: true, + }, + showUserTransferLocations: { + type: Boolean, + required: false, + default: true, + }, + additionalDropdownItems: { + type: Array, + required: false, + default() { + return []; + }, + }, + label: { + type: String, + required: false, + default: i18n.SELECT_A_NAMESPACE, + }, + }, + initialTransferLocationsLoaded: false, + data() { + return { + searchTerm: '', + userTransferLocations: [], + groupTransferLocations: [], + filteredAdditionalDropdownItems: this.additionalDropdownItems, + isLoading: false, + isSearchLoading: false, + hasError: false, + page: 1, + totalPages: 1, + }; + }, + computed: { + hasUserTransferLocations() { + return this.userTransferLocations.length; + }, + hasGroupTransferLocations() { + return this.groupTransferLocations.length; + }, + selectedText() { + return this.value?.humanName || this.label; + }, + hasNextPageOfGroups() { + return this.page < this.totalPages; + }, + showAdditionalDropdownItems() { + return !this.isLoading && this.filteredAdditionalDropdownItems.length; + }, + }, + watch: { + searchTerm() { + this.page = 1; + + this.debouncedSearch(); + }, + }, + methods: { + handleSelect(item) { + this.searchTerm = ''; + this.$emit('input', item); + }, + async handleShow() { + if (this.$options.initialTransferLocationsLoaded) { + return; + } + + this.isLoading = true; + + [this.groupTransferLocations, this.userTransferLocations] = await Promise.all([ + this.getGroupTransferLocations(), + this.getUserTransferLocations(), + ]); + + this.isLoading = false; + this.$options.initialTransferLocationsLoaded = true; + }, + async getGroupTransferLocations() { + try { + const { + data: groupTransferLocations, + headers, + } = await this.groupTransferLocationsApiMethod(this.resourceId, { + page: this.page, + search: this.searchTerm, + }); + + const { totalPages } = parseIntPagination(normalizeHeaders(headers)); + this.totalPages = totalPages; + + return groupTransferLocations.map(({ id, full_name: humanName }) => ({ + id, + humanName, + })); + } catch { + this.handleError(); + + return []; + } + }, + async getUserTransferLocations() { + if (!this.showUserTransferLocations) { + return []; + } + + try { + const { + data: { + currentUser: { namespace }, + }, + } = await this.$apollo.query({ + query: currentUserNamespace, + }); + + if (!namespace) { + return []; + } + + return [ + { + id: getIdFromGraphQLId(namespace.id), + humanName: namespace.fullName, + }, + ]; + } catch { + this.handleError(); + + return []; + } + }, + async handleLoadMoreGroups() { + this.isLoading = true; + this.page += 1; + + const groupTransferLocations = await this.getGroupTransferLocations(); + this.groupTransferLocations.push(...groupTransferLocations); + + this.isLoading = false; + }, + debouncedSearch: debounce(async function debouncedSearch() { + this.isSearchLoading = true; + + this.groupTransferLocations = await this.getGroupTransferLocations(); + + this.filteredAdditionalDropdownItems = this.additionalDropdownItems.filter((dropdownItem) => + dropdownItem.humanName.toLowerCase().includes(this.searchTerm.toLowerCase()), + ); + + this.isSearchLoading = false; + }, DEBOUNCE_DELAY), + handleError() { + this.hasError = true; + }, + handleAlertDismiss() { + this.hasError = false; + }, + }, + i18n, +}; +</script> +<template> + <div> + <gl-alert + v-if="hasError" + variant="danger" + :dismiss-label="$options.i18n.ALERT_DISMISS_LABEL" + @dismiss="handleAlertDismiss" + >{{ $options.i18n.ERROR_MESSAGE }}</gl-alert + > + <gl-form-group :label="label"> + <gl-dropdown + :text="selectedText" + data-qa-selector="namespaces_list" + data-testid="transfer-locations-dropdown" + block + toggle-class="gl-mb-0" + @show="handleShow" + > + <template #header> + <gl-search-box-by-type + v-model.trim="searchTerm" + :is-loading="isSearchLoading" + data-qa-selector="namespaces_list_search" + /> + </template> + <template v-if="showAdditionalDropdownItems"> + <gl-dropdown-item + v-for="item in filteredAdditionalDropdownItems" + :key="item.id" + @click="handleSelect(item)" + >{{ item.humanName }}</gl-dropdown-item + > + <gl-dropdown-divider /> + </template> + <div + v-if="hasUserTransferLocations" + data-qa-selector="namespaces_list_users" + data-testid="user-transfer-locations" + > + <gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header> + <gl-dropdown-item + v-for="item in userTransferLocations" + :key="item.id" + data-qa-selector="namespaces_list_item" + @click="handleSelect(item)" + >{{ item.humanName }}</gl-dropdown-item + > + </div> + <div + v-if="hasGroupTransferLocations" + data-qa-selector="namespaces_list_groups" + data-testid="group-transfer-locations" + > + <gl-dropdown-section-header v-if="showUserTransferLocations">{{ + $options.i18n.GROUPS + }}</gl-dropdown-section-header> + <gl-dropdown-item + v-for="item in groupTransferLocations" + :key="item.id" + data-qa-selector="namespaces_list_item" + @click="handleSelect(item)" + >{{ item.humanName }}</gl-dropdown-item + > + </div> + <gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" /> + <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="handleLoadMoreGroups" /> + </gl-dropdown> + </gl-form-group> + </div> +</template> |