summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/groups/components/group_name_and_path.vue
diff options
context:
space:
mode:
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.vue215
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"