diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue')
-rw-r--r-- | app/assets/javascripts/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue | 208 |
1 files changed, 208 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue new file mode 100644 index 00000000000..b079181bd10 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue @@ -0,0 +1,208 @@ +<script> +import { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlLoadingIcon, + GlSearchBoxByType, +} from '@gitlab/ui'; +import { createAlert } from '~/flash'; +import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility'; +import { __, sprintf } from '~/locale'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; +import AccessorUtilities from '~/lib/utils/accessor'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import searchUserProjectsWithIssuesEnabled from './graphql/search_user_projects_with_issues_enabled.query.graphql'; +import { RESOURCE_TYPE_ISSUE, RESOURCE_TYPES, RESOURCE_OPTIONS } from './constants'; + +export default { + i18n: { + noMatchesFound: __('No matches found'), + toggleButtonLabel: __('Toggle project select'), + }, + components: { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlLoadingIcon, + GlSearchBoxByType, + LocalStorageSync, + }, + props: { + resourceType: { + type: String, + required: false, + default: RESOURCE_TYPE_ISSUE, + validator: (value) => RESOURCE_TYPES.includes(value), + }, + query: { + type: Object, + required: false, + default: () => searchUserProjectsWithIssuesEnabled, + }, + groupId: { + type: String, + required: false, + default: '', + }, + queryVariables: { + type: Object, + required: false, + default: () => ({}), + }, + extractProjects: { + type: Function, + required: false, + default: (data) => data?.projects?.nodes, + }, + withLocalStorage: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + projects: [], + search: '', + selectedProject: {}, + shouldSkipQuery: true, + }; + }, + apollo: { + projects: { + query() { + return this.query; + }, + variables() { + return { + search: this.search, + ...this.queryVariables, + }; + }, + update(data) { + return this.extractProjects(data) || []; + }, + error(error) { + createAlert({ + message: __('An error occurred while loading projects.'), + captureError: true, + error, + }); + }, + skip() { + return this.shouldSkipQuery; + }, + debounce: DEBOUNCE_DELAY, + }, + }, + computed: { + localStorageKey() { + return `group-${this.groupId}-new-${this.resourceType}-recent-project`; + }, + resourceOptions() { + return RESOURCE_OPTIONS[this.resourceType]; + }, + defaultDropdownText() { + return sprintf(__('Select project to create %{type}'), { type: this.resourceOptions.label }); + }, + dropdownHref() { + return this.hasSelectedProject + ? joinPaths(this.selectedProject.webUrl, DASH_SCOPE, this.resourceOptions.path) + : undefined; + }, + dropdownText() { + return this.hasSelectedProject + ? sprintf(__('New %{type} in %{project}'), { + type: this.resourceOptions.label, + project: this.selectedProject.name, + }) + : this.defaultDropdownText; + }, + hasSelectedProject() { + return this.selectedProject.webUrl; + }, + showNoSearchResultsText() { + return !this.projects.length && this.search; + }, + canUseLocalStorage() { + return this.withLocalStorage && AccessorUtilities.canUseLocalStorage(); + }, + selectedProjectForLocalStorage() { + const { webUrl, name } = this.selectedProject; + + return { webUrl, name }; + }, + }, + methods: { + handleDropdownClick() { + if (!this.dropdownHref) { + this.$refs.dropdown.show(); + } + }, + handleDropdownShown() { + if (this.shouldSkipQuery) { + this.shouldSkipQuery = false; + } + this.$refs.search.focusInput(); + }, + selectProject(project) { + this.selectedProject = project; + }, + initFromLocalStorage(storedProject) { + // Historically, the selected project was saved with the URL as the `url` property, so we are + // falling back to that legacy property if `webUrl` is empty. This ensures that we support + // localStorage data that was persisted prior to this change. + let webUrl = storedProject.webUrl || storedProject.url; + + // The select2 implementation used to include the resource path in the local storage. We + // need to clean this up so that we can then re-build a fresh URL in the computed prop. + webUrl = webUrl.endsWith(this.resourceOptions.path) + ? webUrl.slice(0, webUrl.length - this.resourceOptions.path.length) + : webUrl; + const dashSuffix = `${DASH_SCOPE}/`; + webUrl = webUrl.endsWith(dashSuffix) + ? webUrl.slice(0, webUrl.length - dashSuffix.length) + : webUrl; + + this.selectedProject = { webUrl, name: storedProject.name }; + }, + }, +}; +</script> + +<template> + <local-storage-sync + :storage-key="localStorageKey" + :value="selectedProjectForLocalStorage" + @input="initFromLocalStorage" + > + <gl-dropdown + ref="dropdown" + right + split + :split-href="dropdownHref" + :text="dropdownText" + :toggle-text="$options.i18n.toggleButtonLabel" + variant="confirm" + data-testid="new-resource-dropdown" + @click="handleDropdownClick" + @shown="handleDropdownShown" + > + <gl-search-box-by-type ref="search" v-model.trim="search" /> + <gl-loading-icon v-if="$apollo.queries.projects.loading" /> + <template v-else> + <gl-dropdown-item + v-for="project of projects" + :key="project.id" + @click="selectProject(project)" + > + {{ project.nameWithNamespace || project.name }} + </gl-dropdown-item> + <gl-dropdown-text v-if="showNoSearchResultsText"> + {{ $options.i18n.noMatchesFound }} + </gl-dropdown-text> + </template> + </gl-dropdown> + </local-storage-sync> +</template> |