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