Commit 714706cf authored by Coung Ngo's avatar Coung Ngo Committed by Phil Hughes

Delay query fetching for New Project page dropdown

parent 18a34cdb
export const DASH_SCOPE = '-'; export const DASH_SCOPE = '-';
const PATH_SEPARATOR = '/'; export const PATH_SEPARATOR = '/';
const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`); const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`); const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
const SHA_REGEX = /[\da-f]{40}/gi; const SHA_REGEX = /[\da-f]{40}/gi;
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlSearchBoxByType, GlSearchBoxByType,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility'; import { joinPaths, PATH_SEPARATOR } 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';
...@@ -36,7 +36,9 @@ export default { ...@@ -36,7 +36,9 @@ export default {
}; };
}, },
skip() { skip() {
return this.search.length > 0 && this.search.length < MINIMUM_SEARCH_LENGTH; const hasNotEnoughSearchCharacters =
this.search.length > 0 && this.search.length < MINIMUM_SEARCH_LENGTH;
return this.shouldSkipQuery || hasNotEnoughSearchCharacters;
}, },
debounce: DEBOUNCE_DELAY, debounce: DEBOUNCE_DELAY,
}, },
...@@ -52,7 +54,7 @@ export default { ...@@ -52,7 +54,7 @@ export default {
data() { data() {
return { return {
currentUser: {}, currentUser: {},
groupToFilterBy: undefined, groupPathToFilterBy: undefined,
search: '', search: '',
selectedNamespace: this.namespaceId selectedNamespace: this.namespaceId
? { ? {
...@@ -63,6 +65,7 @@ export default { ...@@ -63,6 +65,7 @@ export default {
id: this.userNamespaceId, id: this.userNamespaceId,
fullPath: this.userNamespaceFullPath, fullPath: this.userNamespaceFullPath,
}, },
shouldSkipQuery: true,
}; };
}, },
computed: { computed: {
...@@ -73,10 +76,8 @@ export default { ...@@ -73,10 +76,8 @@ export default {
return this.currentUser.namespace || {}; return this.currentUser.namespace || {};
}, },
filteredGroups() { filteredGroups() {
return this.groupToFilterBy return this.groupPathToFilterBy
? this.userGroups.filter((group) => ? this.userGroups.filter((group) => group.fullPath.startsWith(this.groupPathToFilterBy))
group.fullPath.startsWith(this.groupToFilterBy.fullPath),
)
: this.userGroups; : this.userGroups;
}, },
hasGroupMatches() { hasGroupMatches() {
...@@ -85,7 +86,7 @@ export default { ...@@ -85,7 +86,7 @@ export default {
hasNamespaceMatches() { hasNamespaceMatches() {
return ( return (
this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase()) && this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase()) &&
!this.groupToFilterBy !this.groupPathToFilterBy
); );
}, },
hasNoMatches() { hasNoMatches() {
...@@ -99,7 +100,10 @@ export default { ...@@ -99,7 +100,10 @@ export default {
eventHub.$off('select-template', this.handleSelectTemplate); eventHub.$off('select-template', this.handleSelectTemplate);
}, },
methods: { methods: {
focusInput() { handleDropdownShown() {
if (this.shouldSkipQuery) {
this.shouldSkipQuery = false;
}
this.$refs.search.focusInput(); this.$refs.search.focusInput();
}, },
handleDropdownItemClick(namespace) { handleDropdownItemClick(namespace) {
...@@ -111,13 +115,9 @@ export default { ...@@ -111,13 +115,9 @@ export default {
}); });
this.setNamespace(namespace); this.setNamespace(namespace);
}, },
handleSelectTemplate(groupId) { handleSelectTemplate(id, fullPath) {
this.groupToFilterBy = this.userGroups.find( this.groupPathToFilterBy = fullPath.split(PATH_SEPARATOR).shift();
(group) => getIdFromGraphQLId(group.id) === groupId, this.setNamespace({ id, fullPath });
);
if (this.groupToFilterBy) {
this.setNamespace(this.groupToFilterBy);
}
}, },
setNamespace({ id, fullPath }) { setNamespace({ id, fullPath }) {
this.selectedNamespace = { this.selectedNamespace = {
...@@ -137,7 +137,7 @@ export default { ...@@ -137,7 +137,7 @@ export default {
toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20" toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20"
data-qa-selector="select_namespace_dropdown" data-qa-selector="select_namespace_dropdown"
@show="track('activate_form_input', { label: trackLabel, property: 'project_path' })" @show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
@shown="focusInput" @shown="handleDropdownShown"
> >
<gl-search-box-by-type <gl-search-box-by-type
ref="search" ref="search"
......
...@@ -57,7 +57,8 @@ const bindEvents = () => { ...@@ -57,7 +57,8 @@ const bindEvents = () => {
const templateName = $(this).data('template-name'); const templateName = $(this).data('template-name');
if (subgroupId) { if (subgroupId) {
eventHub.$emit('select-template', groupId); const subgroupFullPath = $(this).data('subgroup-full-path');
eventHub.$emit('select-template', subgroupId, subgroupFullPath);
$subgroupWithTemplatesIdInput.val(subgroupId); $subgroupWithTemplatesIdInput.val(subgroupId);
$namespaceSelect.val(groupId).trigger('change'); $namespaceSelect.val(groupId).trigger('change');
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
%a.btn.gl-button.btn-default.gl-mr-3{ href: project_path(project), rel: 'noopener noreferrer', target: '_blank' } %a.btn.gl-button.btn-default.gl-mr-3{ href: project_path(project), rel: 'noopener noreferrer', target: '_blank' }
= _('Preview') = _('Preview')
%label.btn.gl-button.btn-success.custom-template-button.choose-template.gl-mb-0{ for: project.name } %label.btn.gl-button.btn-success.custom-template-button.choose-template.gl-mb-0{ for: project.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_project_id]", id: project.name, value: project.id, data: { subgroup_id: project.namespace_id, template_name: project.name, parent_group_id: namespace_id || group.parent_id } } %input{ type: "radio", autocomplete: "off", name: "project[template_project_id]", id: project.name, value: project.id, data: { subgroup_full_path: project.namespace.full_path, subgroup_id: project.namespace_id, template_name: project.name, parent_group_id: namespace_id || group.parent_id } }
%span.qa-use-template-button %span.qa-use-template-button
= _('Use template') = _('Use template')
......
...@@ -5,7 +5,8 @@ import { ...@@ -5,7 +5,8 @@ import {
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlSearchBoxByType, GlSearchBoxByType,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
...@@ -52,8 +53,7 @@ describe('NewProjectUrlSelect component', () => { ...@@ -52,8 +53,7 @@ describe('NewProjectUrlSelect component', () => {
}, },
}; };
const localVue = createLocalVue(); Vue.use(VueApollo);
localVue.use(VueApollo);
const defaultProvide = { const defaultProvide = {
namespaceFullPath: 'h5bp', namespaceFullPath: 'h5bp',
...@@ -64,17 +64,19 @@ describe('NewProjectUrlSelect component', () => { ...@@ -64,17 +64,19 @@ describe('NewProjectUrlSelect component', () => {
userNamespaceId: '1', userNamespaceId: '1',
}; };
let mockQueryResponse;
const mountComponent = ({ const mountComponent = ({
search = '', search = '',
queryResponse = data, queryResponse = data,
provide = defaultProvide, provide = defaultProvide,
mountFn = shallowMount, mountFn = shallowMount,
} = {}) => { } = {}) => {
const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data: queryResponse })]]; mockQueryResponse = jest.fn().mockResolvedValue({ data: queryResponse });
const requestHandlers = [[searchQuery, mockQueryResponse]];
const apolloProvider = createMockApollo(requestHandlers); const apolloProvider = createMockApollo(requestHandlers);
return mountFn(NewProjectUrlSelect, { return mountFn(NewProjectUrlSelect, {
localVue,
apolloProvider, apolloProvider,
provide, provide,
data() { data() {
...@@ -88,12 +90,19 @@ describe('NewProjectUrlSelect component', () => { ...@@ -88,12 +90,19 @@ describe('NewProjectUrlSelect component', () => {
const findButtonLabel = () => wrapper.findComponent(GlButton); const findButtonLabel = () => wrapper.findComponent(GlButton);
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('[name="project[namespace_id]"]');
const clickDropdownItem = async () => { const clickDropdownItem = async () => {
wrapper.findComponent(GlDropdownItem).vm.$emit('click'); wrapper.findComponent(GlDropdownItem).vm.$emit('click');
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}; };
const showDropdown = async () => {
findDropdown().vm.$emit('shown');
await wrapper.vm.$apollo.queries.currentUser.refetch();
jest.runOnlyPendingTimers();
};
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -141,20 +150,18 @@ describe('NewProjectUrlSelect component', () => { ...@@ -141,20 +150,18 @@ 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();
await wrapper.vm.$nextTick();
const spy = jest.spyOn(findInput().vm, 'focusInput'); const spy = jest.spyOn(findInput().vm, 'focusInput');
findDropdown().vm.$emit('shown'); await showDropdown();
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
}); });
it('renders expected dropdown items', async () => { it('renders expected dropdown items', async () => {
wrapper = mountComponent({ mountFn: mount }); wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick(); await showDropdown();
const listItems = wrapper.findAll('li'); const listItems = wrapper.findAll('li');
...@@ -167,15 +174,36 @@ describe('NewProjectUrlSelect component', () => { ...@@ -167,15 +174,36 @@ describe('NewProjectUrlSelect component', () => {
expect(listItems.at(5).text()).toBe(data.currentUser.namespace.fullPath); expect(listItems.at(5).text()).toBe(data.currentUser.namespace.fullPath);
}); });
describe('query fetching', () => {
describe('on component mount', () => {
it('does not fetch query', () => {
wrapper = mountComponent({ mountFn: mount });
expect(mockQueryResponse).not.toHaveBeenCalled();
});
});
describe('on dropdown shown', () => {
it('fetches query', async () => {
wrapper = mountComponent({ mountFn: mount });
await showDropdown();
expect(mockQueryResponse).toHaveBeenCalled();
});
});
});
describe('when selecting from a group template', () => { describe('when selecting from a group template', () => {
const groupId = getIdFromGraphQLId(data.currentUser.groups.nodes[1].id); const { fullPath, id } = data.currentUser.groups.nodes[1];
beforeEach(async () => { beforeEach(async () => {
wrapper = mountComponent({ mountFn: mount }); wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
eventHub.$emit('select-template', groupId); // Show dropdown to fetch projects
await showDropdown();
eventHub.$emit('select-template', getIdFromGraphQLId(id), fullPath);
}); });
it('filters the dropdown items to the selected group and children', async () => { it('filters the dropdown items to the selected group and children', async () => {
...@@ -188,7 +216,7 @@ describe('NewProjectUrlSelect component', () => { ...@@ -188,7 +216,7 @@ describe('NewProjectUrlSelect component', () => {
}); });
it('sets the selection to the group', async () => { it('sets the selection to the group', async () => {
expect(findDropdown().props('text')).toBe(data.currentUser.groups.nodes[1].fullPath); expect(findDropdown().props('text')).toBe(fullPath);
}); });
}); });
...@@ -214,12 +242,13 @@ describe('NewProjectUrlSelect component', () => { ...@@ -214,12 +242,13 @@ describe('NewProjectUrlSelect component', () => {
}); });
it('emits `update-visibility` event to update the visibility radio options', async () => { it('emits `update-visibility` event to update the visibility radio options', async () => {
wrapper = mountComponent(); wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
const spy = jest.spyOn(eventHub, '$emit'); const spy = jest.spyOn(eventHub, '$emit');
// Show dropdown to fetch projects
await showDropdown();
await clickDropdownItem(); await clickDropdownItem();
const namespace = data.currentUser.groups.nodes[0]; const namespace = data.currentUser.groups.nodes[0];
...@@ -233,16 +262,16 @@ describe('NewProjectUrlSelect component', () => { ...@@ -233,16 +262,16 @@ describe('NewProjectUrlSelect component', () => {
}); });
it('updates hidden input with selected namespace', async () => { it('updates hidden input with selected namespace', async () => {
wrapper = mountComponent(); wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick(); // Show dropdown to fetch projects
await showDropdown();
await clickDropdownItem(); await clickDropdownItem();
expect(findHiddenInput().attributes()).toMatchObject({ expect(findHiddenInput().attributes('value')).toBe(
name: 'project[namespace_id]', getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(), );
});
}); });
it('tracks clicking on the dropdown', () => { it('tracks clicking on the dropdown', () => {
......
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