Commit 00e6ea07 authored by Jacques Erasmus's avatar Jacques Erasmus

Merge branch '26732-fix-dropdown-issues' into 'master'

Fix issues with New Project page Vue dropdown

See merge request gitlab-org/gitlab!72318
parents bb05dc52 933d91ba
import $ from 'jquery';
import eventHub from '~/projects/new/event_hub';
function setVisibilityOptions(namespaceSelector) {
if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) {
return;
}
const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset;
// Values are from lib/gitlab/visibility_level.rb
const visibilityLevel = {
private: 0,
internal: 10,
public: 20,
};
function setVisibilityOptions({ name, visibility, showPath, editPath }) {
document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => {
// Don't change anything if the option is restricted by admin
if (option.classList.contains('restricted')) {
return;
}
const optionInput = option.querySelector('input[type=radio]');
const optionValue = optionInput ? optionInput.value : 0;
const optionTitle = option.querySelector('.option-title');
const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
const optionValue = optionInput ? parseInt(optionInput.value, 10) : 0;
// don't change anything if the option is restricted by admin
if (!option.classList.contains('restricted')) {
if (visibilityLevel < optionValue) {
option.classList.add('disabled');
optionInput.disabled = true;
const reason = option.querySelector('.option-disabled-reason');
if (reason) {
reason.innerHTML = `This project cannot be ${optionName} because the visibility of
if (visibilityLevel[visibility] < optionValue) {
option.classList.add('disabled');
optionInput.disabled = true;
const reason = option.querySelector('.option-disabled-reason');
if (reason) {
const optionTitle = option.querySelector('.option-title');
const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
reason.innerHTML = `This project cannot be ${optionName} because the visibility of
<a href="${showPath}">${name}</a> is ${visibility}. To make this project
${optionName}, you must first <a href="${editPath}">change the visibility</a>
of the parent group.`;
}
} else {
option.classList.remove('disabled');
optionInput.disabled = false;
}
} else {
option.classList.remove('disabled');
optionInput.disabled = false;
}
});
}
function handleSelect2DropdownChange(namespaceSelector) {
if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) {
return;
}
const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
setVisibilityOptions(selectedNamespace.dataset);
}
export default function initProjectVisibilitySelector() {
eventHub.$on('update-visibility', setVisibilityOptions);
const namespaceSelector = document.querySelector('select.js-select-namespace');
if (namespaceSelector) {
$('.select2.js-select-namespace').on('change', () => setVisibilityOptions(namespaceSelector));
setVisibilityOptions(namespaceSelector);
$('.select2.js-select-namespace').on('change', () =>
handleSelect2DropdownChange(namespaceSelector),
);
handleSelect2DropdownChange(namespaceSelector);
}
}
......@@ -6,9 +6,9 @@ import {
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
GlLoadingIcon,
GlSearchBoxByType,
} from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
......@@ -24,7 +24,6 @@ export default {
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
GlLoadingIcon,
GlSearchBoxByType,
},
mixins: [Tracking.mixin()],
......@@ -103,6 +102,15 @@ export default {
focusInput() {
this.$refs.search.focusInput();
},
handleDropdownItemClick(namespace) {
eventHub.$emit('update-visibility', {
name: namespace.name,
visibility: namespace.visibility,
showPath: namespace.webUrl,
editPath: joinPaths(namespace.webUrl, '-', 'edit'),
});
this.setNamespace(namespace);
},
handleSelectTemplate(groupId) {
this.groupToFilterBy = this.userGroups.find(
(group) => getIdFromGraphQLId(group.id) === groupId,
......@@ -134,23 +142,23 @@ export default {
<gl-search-box-by-type
ref="search"
v-model.trim="search"
:is-loading="$apollo.queries.currentUser.loading"
data-qa-selector="select_namespace_dropdown_search_field"
/>
<gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
<template v-else>
<template v-if="!$apollo.queries.currentUser.loading">
<template v-if="hasGroupMatches">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="group of filteredGroups"
:key="group.id"
@click="setNamespace(group)"
@click="handleDropdownItemClick(group)"
>
{{ group.fullPath }}
</gl-dropdown-item>
</template>
<template v-if="hasNamespaceMatches">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="setNamespace(userNamespace)">
<gl-dropdown-item @click="handleDropdownItemClick(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
</template>
......@@ -158,6 +166,11 @@ export default {
</template>
</gl-dropdown>
<input type="hidden" name="project[namespace_id]" :value="selectedNamespace.id" />
<input
id="project_namespace_id"
type="hidden"
name="project[namespace_id]"
:value="selectedNamespace.id"
/>
</gl-button-group>
</template>
......@@ -4,6 +4,9 @@ query searchNamespacesWhereUserCanCreateProjects($search: String) {
nodes {
id
fullPath
name
visibility
webUrl
}
}
namespace {
......
......@@ -10,6 +10,7 @@
= visibility_level_label(level)
.option-description
= visibility_level_description(level, form_model)
.option-disabled-reason
.text-muted
- if all_visibility_levels_restricted?
......
......@@ -24,14 +24,23 @@ describe('NewProjectUrlSelect component', () => {
{
id: 'gid://gitlab/Group/26',
fullPath: 'flightjs',
name: 'Flight JS',
visibility: 'public',
webUrl: 'http://127.0.0.1:3000/flightjs',
},
{
id: 'gid://gitlab/Group/28',
fullPath: 'h5bp',
name: 'H5BP',
visibility: 'public',
webUrl: 'http://127.0.0.1:3000/h5bp',
},
{
id: 'gid://gitlab/Group/30',
fullPath: 'h5bp/subgroup',
name: 'H5BP Subgroup',
visibility: 'private',
webUrl: 'http://127.0.0.1:3000/h5bp/subgroup',
},
],
},
......@@ -79,6 +88,10 @@ describe('NewProjectUrlSelect component', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const findHiddenInput = () => wrapper.find('input');
const clickDropdownItem = async () => {
wrapper.findComponent(GlDropdownItem).vm.$emit('click');
await wrapper.vm.$nextTick();
};
afterEach(() => {
wrapper.destroy();
......@@ -127,7 +140,6 @@ describe('NewProjectUrlSelect component', () => {
it('focuses on the input when the dropdown is opened', async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
......@@ -140,7 +152,6 @@ describe('NewProjectUrlSelect component', () => {
it('renders expected dropdown items', async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
......@@ -160,7 +171,6 @@ describe('NewProjectUrlSelect component', () => {
beforeEach(async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
......@@ -195,23 +205,38 @@ describe('NewProjectUrlSelect component', () => {
};
wrapper = mountComponent({ search: 'no matches', queryResponse, mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(wrapper.find('li').text()).toBe('No matches found');
});
it('updates hidden input with selected namespace', async () => {
it('emits `update-visibility` event to update the visibility radio options', async () => {
wrapper = mountComponent();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
wrapper.findComponent(GlDropdownItem).vm.$emit('click');
const spy = jest.spyOn(eventHub, '$emit');
await clickDropdownItem();
const namespace = data.currentUser.groups.nodes[0];
expect(spy).toHaveBeenCalledWith('update-visibility', {
name: namespace.name,
visibility: namespace.visibility,
showPath: namespace.webUrl,
editPath: `${namespace.webUrl}/-/edit`,
});
});
it('updates hidden input with selected namespace', async () => {
wrapper = mountComponent();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
await clickDropdownItem();
expect(findHiddenInput().attributes()).toMatchObject({
name: 'project[namespace_id]',
value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment