diff options
Diffstat (limited to 'app/assets/javascripts/pages/projects/forks')
4 files changed, 190 insertions, 78 deletions
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue index f92a40e057f..b415e36bf09 100644 --- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue +++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue @@ -3,15 +3,12 @@ import { GlIcon, GlLink, GlForm, - GlFormInputGroup, - GlInputGroupText, GlFormInput, GlFormGroup, GlFormTextarea, GlButton, GlFormRadio, GlFormRadioGroup, - GlFormSelect, } from '@gitlab/ui'; import { kebabCase } from 'lodash'; import { buildApiUrl } from '~/api/api_utils'; @@ -21,16 +18,13 @@ import csrf from '~/lib/utils/csrf'; import { redirectTo } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import validation from '~/vue_shared/directives/validation'; - -const PRIVATE_VISIBILITY = 'private'; -const INTERNAL_VISIBILITY = 'internal'; -const PUBLIC_VISIBILITY = 'public'; - -const VISIBILITY_LEVEL = { - [PRIVATE_VISIBILITY]: 0, - [INTERNAL_VISIBILITY]: 10, - [PUBLIC_VISIBILITY]: 20, -}; +import { + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, + VISIBILITY_LEVELS_STRING_TO_INTEGER, +} from '~/visibility_level/constants'; +import ProjectNamespace from './project_namespace.vue'; const initFormField = ({ value, required = true, skipValidation = false }) => ({ value, @@ -39,28 +33,18 @@ const initFormField = ({ value, required = true, skipValidation = false }) => ({ feedback: null, }); -function sortNamespaces(namespaces) { - if (!namespaces || !namespaces?.length) { - return namespaces; - } - - return namespaces.sort((a, b) => a.full_name.localeCompare(b.full_name)); -} - export default { components: { GlForm, GlIcon, GlLink, GlButton, - GlFormInputGroup, - GlInputGroupText, GlFormInput, GlFormTextarea, GlFormGroup, GlFormRadio, GlFormRadioGroup, - GlFormSelect, + ProjectNamespace, }, directives: { validation: validation(), @@ -72,9 +56,6 @@ export default { visibilityHelpPath: { default: '', }, - endpoint: { - default: '', - }, projectFullPath: { default: '', }, @@ -96,6 +77,9 @@ export default { restrictedVisibilityLevels: { default: [], }, + namespaceId: { + default: '', + }, }, data() { const form = { @@ -117,20 +101,17 @@ export default { }; return { isSaving: false, - namespaces: [], form, }; }, computed: { - projectUrl() { - return `${gon.gitlab_url}/`; - }, projectVisibilityLevel() { - return VISIBILITY_LEVEL[this.projectVisibility]; + return VISIBILITY_LEVELS_STRING_TO_INTEGER[this.projectVisibility]; }, namespaceVisibilityLevel() { - const visibility = this.form.fields.namespace.value?.visibility || PUBLIC_VISIBILITY; - return VISIBILITY_LEVEL[visibility]; + const visibility = + this.form.fields.namespace.value?.visibility || VISIBILITY_LEVEL_PUBLIC_STRING; + return VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility]; }, visibilityLevelCap() { return Math.min(this.projectVisibilityLevel, this.namespaceVisibilityLevel); @@ -139,7 +120,7 @@ export default { return new Set(this.restrictedVisibilityLevels); }, allowedVisibilityLevels() { - const allowedLevels = Object.entries(VISIBILITY_LEVEL).reduce( + const allowedLevels = Object.entries(VISIBILITY_LEVELS_STRING_TO_INTEGER).reduce( (levels, [levelName, levelValue]) => { if ( !this.restrictedVisibilityLevelsSet.has(levelValue) && @@ -153,7 +134,7 @@ export default { ); if (!allowedLevels.length) { - return [PRIVATE_VISIBILITY]; + return [VISIBILITY_LEVEL_PRIVATE_STRING]; } return allowedLevels; @@ -162,58 +143,56 @@ export default { return [ { text: s__('ForkProject|Private'), - value: PRIVATE_VISIBILITY, + value: VISIBILITY_LEVEL_PRIVATE_STRING, icon: 'lock', help: s__( 'ForkProject|Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.', ), - disabled: this.isVisibilityLevelDisabled(PRIVATE_VISIBILITY), + disabled: this.isVisibilityLevelDisabled(VISIBILITY_LEVEL_PRIVATE_STRING), }, { text: s__('ForkProject|Internal'), - value: INTERNAL_VISIBILITY, + value: VISIBILITY_LEVEL_INTERNAL_STRING, icon: 'shield', help: s__('ForkProject|The project can be accessed by any logged in user.'), - disabled: this.isVisibilityLevelDisabled(INTERNAL_VISIBILITY), + disabled: this.isVisibilityLevelDisabled(VISIBILITY_LEVEL_INTERNAL_STRING), }, { text: s__('ForkProject|Public'), - value: PUBLIC_VISIBILITY, + value: VISIBILITY_LEVEL_PUBLIC_STRING, icon: 'earth', help: s__('ForkProject|The project can be accessed without any authentication.'), - disabled: this.isVisibilityLevelDisabled(PUBLIC_VISIBILITY), + disabled: this.isVisibilityLevelDisabled(VISIBILITY_LEVEL_PUBLIC_STRING), }, ]; }, }, watch: { // eslint-disable-next-line func-names - 'form.fields.namespace.value': function () { - this.form.fields.visibility.value = - this.restrictedVisibilityLevels.length !== 0 ? null : PRIVATE_VISIBILITY; - }, - // eslint-disable-next-line func-names 'form.fields.name.value': function (newVal) { this.form.fields.slug.value = kebabCase(newVal); }, }, - mounted() { - this.fetchNamespaces(); - }, methods: { - async fetchNamespaces() { - const { data } = await axios.get(this.endpoint); - this.namespaces = sortNamespaces(data.namespaces); - }, isVisibilityLevelDisabled(visibility) { return !this.allowedVisibilityLevels.includes(visibility); }, getInitialVisibilityValue() { return this.restrictedVisibilityLevels.length !== 0 ? null : this.projectVisibility; }, + setNamespace(namespace) { + this.form.fields.visibility.value = + this.restrictedVisibilityLevels.length !== 0 ? null : VISIBILITY_LEVEL_PRIVATE_STRING; + this.form.fields.namespace.value = namespace; + this.form.fields.namespace.state = true; + }, async onSubmit() { this.form.showValidation = true; + if (!this.form.fields.namespace.value) { + this.form.fields.namespace.state = false; + } + if (!this.form.state) { return; } @@ -282,30 +261,7 @@ export default { :state="form.fields.namespace.state" :invalid-feedback="s__('ForkProject|Please select a namespace')" > - <gl-form-input-group> - <template #prepend> - <gl-input-group-text> - {{ projectUrl }} - </gl-input-group-text> - </template> - <gl-form-select - id="fork-url" - v-model="form.fields.namespace.value" - v-validation:[form.showValidation] - name="namespace" - data-testid="fork-url-input" - data-qa-selector="fork_namespace_dropdown" - :state="form.fields.namespace.state" - required - > - <template #first> - <option :value="null" disabled>{{ s__('ForkProject|Select a namespace') }}</option> - </template> - <option v-for="namespace in namespaces" :key="namespace.id" :value="namespace"> - {{ namespace.full_name }} - </option> - </gl-form-select> - </gl-form-input-group> + <project-namespace @select="setNamespace" /> </gl-form-group> </div> <div class="gl-flex-basis-half"> diff --git a/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue b/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue new file mode 100644 index 00000000000..2b3055ecd66 --- /dev/null +++ b/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue @@ -0,0 +1,136 @@ +<script> +import { + GlButton, + GlButtonGroup, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlDropdownSectionHeader, + GlSearchBoxByType, + GlTruncate, +} from '@gitlab/ui'; +import createFlash from '~/flash'; +import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants'; +import { s__ } from '~/locale'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; +import searchForkableNamespaces from '../queries/search_forkable_namespaces.query.graphql'; + +export default { + components: { + GlButton, + GlButtonGroup, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlDropdownSectionHeader, + GlSearchBoxByType, + GlTruncate, + }, + apollo: { + project: { + query: searchForkableNamespaces, + variables() { + return { + projectPath: this.projectFullPath, + search: this.search, + }; + }, + skip() { + const { length } = this.search; + return length > 0 && length < MINIMUM_SEARCH_LENGTH; + }, + error(error) { + createFlash({ + message: s__( + 'ForkProject|Something went wrong while loading data. Please refresh the page to try again.', + ), + captureError: true, + error, + }); + }, + debounce: DEBOUNCE_DELAY, + }, + }, + inject: ['projectFullPath'], + data() { + return { + project: {}, + search: '', + selectedNamespace: null, + }; + }, + computed: { + rootUrl() { + return `${gon.gitlab_url}/`; + }, + namespaces() { + return this.project.forkTargets?.nodes || []; + }, + hasMatches() { + return this.namespaces.length; + }, + dropdownText() { + return this.selectedNamespace?.fullPath || s__('ForkProject|Select a namespace'); + }, + }, + methods: { + handleDropdownShown() { + this.$refs.search.focusInput(); + }, + setNamespace(namespace) { + const id = getIdFromGraphQLId(namespace.id); + + this.$emit('select', { + id, + name: namespace.name, + visibility: namespace.visibility, + }); + + this.selectedNamespace = { id, fullPath: namespace.fullPath }; + }, + }, +}; +</script> + +<template> + <gl-button-group class="gl-w-full"> + <gl-button class="gl-text-truncate gl-flex-grow-0! gl-max-w-34" label :title="rootUrl">{{ + rootUrl + }}</gl-button> + + <gl-dropdown + class="gl-flex-grow-1" + toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20" + data-qa-selector="select_namespace_dropdown" + data-testid="select_namespace_dropdown" + no-flip + @shown="handleDropdownShown" + > + <template #button-text> + <gl-truncate :text="dropdownText" position="start" with-tooltip /> + </template> + <gl-search-box-by-type + ref="search" + v-model.trim="search" + :is-loading="$apollo.queries.project.loading" + data-qa-selector="select_namespace_dropdown_search_field" + data-testid="select_namespace_dropdown_search_field" + /> + <template v-if="!$apollo.queries.project.loading"> + <template v-if="hasMatches"> + <gl-dropdown-section-header>{{ __('Namespaces') }}</gl-dropdown-section-header> + <gl-dropdown-item + v-for="namespace of namespaces" + :key="namespace.id" + data-qa-selector="select_namespace_dropdown_item" + @click="setNamespace(namespace)" + > + {{ namespace.fullPath }} + </gl-dropdown-item> + </template> + <gl-dropdown-text v-else>{{ __('No matches found') }}</gl-dropdown-text> + </template> + </gl-dropdown> + </gl-button-group> +</template> diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js index cbf74f755e7..d3a5ce5390f 100644 --- a/app/assets/javascripts/pages/projects/forks/new/index.js +++ b/app/assets/javascripts/pages/projects/forks/new/index.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; import App from './components/app.vue'; const mountElement = document.getElementById('fork-groups-mount-element'); @@ -17,9 +19,14 @@ const { restrictedVisibilityLevels, } = mountElement.dataset; +Vue.use(VueApollo); + // eslint-disable-next-line no-new new Vue({ el: mountElement, + apolloProvider: new VueApollo({ + defaultClient: createDefaultClient(), + }), provide: { newGroupPath, visibilityHelpPath, diff --git a/app/assets/javascripts/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql b/app/assets/javascripts/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql new file mode 100644 index 00000000000..089b57815bd --- /dev/null +++ b/app/assets/javascripts/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql @@ -0,0 +1,13 @@ +query searchForkableNamespaces($projectPath: ID!, $search: String) { + project(fullPath: $projectPath) { + id + forkTargets(search: $search) { + nodes { + id + fullPath + name + visibility + } + } + } +} |