Commit 5c95fb3d authored by Phil Hughes's avatar Phil Hughes

Merge branch '26732-delay-query-fetching' into 'master'

Delay query fetching for New Project page dropdown

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