diff options
Diffstat (limited to 'app/assets/javascripts/groups/components/group_name_and_path.vue')
-rw-r--r-- | app/assets/javascripts/groups/components/group_name_and_path.vue | 215 |
1 files changed, 170 insertions, 45 deletions
diff --git a/app/assets/javascripts/groups/components/group_name_and_path.vue b/app/assets/javascripts/groups/components/group_name_and_path.vue index 983535d3e9c..9a1ea2f1812 100644 --- a/app/assets/javascripts/groups/components/group_name_and_path.vue +++ b/app/assets/javascripts/groups/components/group_name_and_path.vue @@ -6,6 +6,13 @@ import { GlInputGroupText, GlLink, GlAlert, + GlButton, + GlButtonGroup, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlTruncate, + GlSearchBoxByType, } from '@gitlab/ui'; import { debounce } from 'lodash'; @@ -15,6 +22,11 @@ import { createAlert } from '~/flash'; import { slugify } from '~/lib/utils/text_utility'; import axios from '~/lib/utils/axios_utils'; import { helpPagePath } from '~/helpers/help_page_helper'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; + +import searchGroupsWhereUserCanCreateSubgroups from '../queries/search_groups_where_user_can_create_subgroups.query.graphql'; const DEBOUNCE_DURATION = 1000; @@ -22,7 +34,6 @@ export default { i18n: { inputs: { name: { - label: s__('Groups|Group name'), placeholder: __('My awesome group'), description: s__( 'Groups|Must start with letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.', @@ -30,7 +41,6 @@ export default { invalidFeedback: s__('Groups|Enter a descriptive name for your group.'), }, path: { - label: s__('Groups|Group URL'), placeholder: __('my-awesome-group'), invalidFeedbackInvalidPattern: s__( 'GroupSettings|Choose a group path that does not start with a dash or end with a period. It can also contain alphanumeric characters and underscores.', @@ -40,9 +50,6 @@ export default { ), validFeedback: s__('Groups|Group path is available.'), }, - groupId: { - label: s__('Groups|Group ID'), - }, }, apiLoadingMessage: s__('Groups|Checking group URL availability...'), apiErrorMessage: __( @@ -51,7 +58,7 @@ export default { changingUrlWarningMessage: s__('Groups|Changing group URL can have unintended side effects.'), learnMore: s__('Groups|Learn more'), }, - nameInputSize: { md: 'lg' }, + inputSize: { md: 'lg' }, changingGroupPathHelpPagePath: helpPagePath('user/group/index', { anchor: 'change-a-groups-path', }), @@ -63,8 +70,35 @@ export default { GlInputGroupText, GlLink, GlAlert, + GlButton, + GlButtonGroup, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlTruncate, + GlSearchBoxByType, + }, + apollo: { + currentUserGroups: { + query: searchGroupsWhereUserCanCreateSubgroups, + variables() { + return { + search: this.search, + }; + }, + update(data) { + return data.currentUser?.groups?.nodes || []; + }, + skip() { + const hasNotEnoughSearchCharacters = + this.search.length > 0 && this.search.length < MINIMUM_SEARCH_LENGTH; + + return this.shouldSkipQuery || hasNotEnoughSearchCharacters; + }, + debounce: DEBOUNCE_DELAY, + }, }, - inject: ['fields', 'basePath', 'mattermostEnabled'], + inject: ['fields', 'basePath', 'newSubgroup', 'mattermostEnabled'], data() { return { name: this.fields.name.value, @@ -76,9 +110,27 @@ export default { pathFeedbackState: null, pathInvalidFeedback: null, activeApiRequestAbortController: null, + search: '', + currentUserGroups: {}, + shouldSkipQuery: true, + selectedGroup: { + id: this.fields.parentId.value, + fullPath: this.fields.parentFullPath.value, + }, }; }, computed: { + inputLabels() { + return { + name: this.newSubgroup ? s__('Groups|Subgroup name') : s__('Groups|Group name'), + path: this.newSubgroup ? s__('Groups|Subgroup slug') : s__('Groups|Group URL'), + subgroupPath: s__('Groups|Subgroup URL'), + groupId: s__('Groups|Group ID'), + }; + }, + pathInputSize() { + return this.newSubgroup ? {} : this.$options.inputSize; + }, computedPath() { return this.apiSuggestedPath || this.path; }, @@ -129,9 +181,11 @@ export default { try { const { data: { exists, suggests }, - } = await getGroupPathAvailability(this.path, this.fields.parentId?.value, { - signal: this.activeApiRequestAbortController.signal, - }); + } = await getGroupPathAvailability( + this.path, + this.selectedGroup.id || this.fields.parentId.value, + { signal: this.activeApiRequestAbortController.signal }, + ); this.apiLoading = false; @@ -198,6 +252,21 @@ export default { this.pathInvalidFeedback = this.$options.i18n.inputs.path.invalidFeedbackInvalidPattern; this.pathFeedbackState = false; }, + handleDropdownShown() { + if (this.shouldSkipQuery) { + this.shouldSkipQuery = false; + } + + this.$refs.search.focusInput(); + }, + handleDropdownItemClick({ id, fullPath }) { + this.selectedGroup = { + id: getIdFromGraphQLId(id), + fullPath, + }; + + this.debouncedValidatePath(); + }, }, }; </script> @@ -208,10 +277,10 @@ export default { :id="fields.parentId.id" type="hidden" :name="fields.parentId.name" - :value="fields.parentId.value" + :value="selectedGroup.id" /> <gl-form-group - :label="$options.i18n.inputs.name.label" + :label="inputLabels.name" :description="$options.i18n.inputs.name.description" :label-for="fields.name.id" :invalid-feedback="$options.i18n.inputs.name.invalidFeedback" @@ -220,46 +289,102 @@ export default { <gl-form-input :id="fields.name.id" v-model="name" - class="gl-field-error-ignore" + class="gl-field-error-ignore gl-h-auto!" required :name="fields.name.name" :placeholder="$options.i18n.inputs.name.placeholder" data-qa-selector="group_name_field" - :size="$options.nameInputSize" + :size="$options.inputSize" :state="nameFeedbackState" @invalid="handleInvalidName" /> </gl-form-group> - <gl-form-group - :label="$options.i18n.inputs.path.label" - :label-for="fields.path.id" - :description="pathDescription" - :state="pathFeedbackState" - :valid-feedback="$options.i18n.inputs.path.validFeedback" - :invalid-feedback="pathInvalidFeedback" - > - <gl-form-input-group> - <template #prepend> - <gl-input-group-text class="group-root-path">{{ basePath }}</gl-input-group-text> - </template> - <gl-form-input - :id="fields.path.id" - class="gl-field-error-ignore" - :name="fields.path.name" - :value="computedPath" - :placeholder="$options.i18n.inputs.path.placeholder" - :maxlength="fields.path.maxLength" - :pattern="fields.path.pattern" - :state="pathFeedbackState" - :size="$options.nameInputSize" - required - data-qa-selector="group_path_field" - :data-bind-in="mattermostEnabled ? $options.mattermostDataBindName : null" - @input="handlePathInput" - @invalid="handleInvalidPath" - /> - </gl-form-input-group> - </gl-form-group> + + <div :class="newSubgroup && 'row gl-mb-3'"> + <gl-form-group v-if="newSubgroup" class="col-sm-6 gl-pr-0" :label="inputLabels.subgroupPath"> + <div class="input-group gl-flex-nowrap"> + <gl-button-group class="gl-w-full"> + <gl-button class="js-group-namespace-button gl-text-truncate gl-flex-grow-0!" label> + {{ basePath }} + </gl-button> + + <gl-dropdown + class="js-group-namespace-dropdown gl-flex-grow-1" + toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20" + @shown="handleDropdownShown" + > + <template #button-text> + <gl-truncate + v-if="selectedGroup.fullPath" + :text="selectedGroup.fullPath" + position="start" + with-tooltip + /> + </template> + + <gl-search-box-by-type + ref="search" + v-model.trim="search" + :is-loading="$apollo.queries.currentUserGroups.loading" + /> + + <template v-if="!$apollo.queries.currentUserGroups.loading"> + <template v-if="currentUserGroups.length"> + <gl-dropdown-item + v-for="group of currentUserGroups" + :key="group.id" + data-testid="select_group_dropdown_item" + @click="handleDropdownItemClick(group)" + > + {{ group.fullPath }} + </gl-dropdown-item> + </template> + <gl-dropdown-text v-else>{{ __('No matches found') }}</gl-dropdown-text> + </template> + </gl-dropdown> + </gl-button-group> + + <div class="gl-align-self-center gl-pl-5"> + <span class="gl-display-none gl-md-display-inline">/</span> + </div> + </div> + </gl-form-group> + + <gl-form-group + :class="newSubgroup && 'col-sm-6'" + :label="inputLabels.path" + :label-for="fields.path.id" + :description="pathDescription" + :state="pathFeedbackState" + :valid-feedback="$options.i18n.inputs.path.validFeedback" + :invalid-feedback="pathInvalidFeedback" + > + <gl-form-input-group> + <template v-if="!newSubgroup" #prepend> + <gl-input-group-text class="group-root-path"> + {{ basePath.concat(fields.parentFullPath.value) }} + </gl-input-group-text> + </template> + <gl-form-input + :id="fields.path.id" + class="gl-field-error-ignore gl-h-auto!" + :name="fields.path.name" + :value="computedPath" + :placeholder="$options.i18n.inputs.path.placeholder" + :maxlength="fields.path.maxLength" + :pattern="fields.path.pattern" + :state="pathFeedbackState" + :size="pathInputSize" + required + data-qa-selector="group_path_field" + :data-bind-in="mattermostEnabled ? $options.mattermostDataBindName : null" + @input="handlePathInput" + @invalid="handleInvalidPath" + /> + </gl-form-input-group> + </gl-form-group> + </div> + <template v-if="isEditingGroup"> <gl-alert class="gl-mb-5" :dismissible="false" variant="warning"> {{ $options.i18n.changingUrlWarningMessage }} @@ -267,7 +392,7 @@ export default { >{{ $options.i18n.learnMore }} </gl-link> </gl-alert> - <gl-form-group :label="$options.i18n.inputs.groupId.label" :label-for="fields.groupId.id"> + <gl-form-group :label="inputLabels.groupId" :label-for="fields.groupId.id"> <gl-form-input :id="fields.groupId.id" :value="fields.groupId.value" |