Commit dd9c3eed authored by Scott Hampton's avatar Scott Hampton

Fetch more group projects

The GraphQL API only returns a max of
100 projects at a time. This sets up an
observer to watch for when the user
scrolls to the bottom of the dropdown,
and fetches the next "page" of projects.
parent 11cb76ff
...@@ -4,12 +4,16 @@ import { ...@@ -4,12 +4,16 @@ import {
GlDropdown, GlDropdown,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlDropdownItem, GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlModal, GlModal,
GlModalDirective, GlModalDirective,
GlSearchBoxByType, GlSearchBoxByType,
} from '@gitlab/ui'; } from '@gitlab/ui';
import produce from 'immer';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { pikadayToString } from '~/lib/utils/datetime_utility'; import { pikadayToString } from '~/lib/utils/datetime_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import getGroupProjects from '../graphql/queries/get_group_projects.query.graphql'; import getGroupProjects from '../graphql/queries/get_group_projects.query.graphql';
export default { export default {
...@@ -19,6 +23,8 @@ export default { ...@@ -19,6 +23,8 @@ export default {
GlDropdown, GlDropdown,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlDropdownItem, GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlModal, GlModal,
GlSearchBoxByType, GlSearchBoxByType,
}, },
...@@ -46,15 +52,19 @@ export default { ...@@ -46,15 +52,19 @@ export default {
update(data) { update(data) {
return data.group.projects.nodes.map(project => ({ return data.group.projects.nodes.map(project => ({
...project, ...project,
id: project.id.split('Project/')[1], id: getIdFromGraphQLId(project.id),
isSelected: false, isSelected: false,
})); }));
}, },
result({ data }) {
this.projectsPageInfo = data?.group?.projects?.pageInfo;
},
}, },
}, },
data() { data() {
return { return {
groupProjects: [], groupProjects: [],
projectsPageInfo: {},
projectSearchTerm: '', projectSearchTerm: '',
selectAllProjects: true, selectAllProjects: true,
selectedDateRange: this.$options.dateRangeOptions[2], selectedDateRange: this.$options.dateRangeOptions[2],
...@@ -125,6 +135,26 @@ export default { ...@@ -125,6 +135,26 @@ export default {
clickDateRange(dateRange) { clickDateRange(dateRange) {
this.selectedDateRange = dateRange; this.selectedDateRange = dateRange;
}, },
loadMoreProjects() {
if (this.projectsPageInfo.hasNextPage) {
this.$apollo.queries.groupProjects.fetchMore({
variables: {
groupFullPath: this.groupFullPath,
after: this.projectsPageInfo?.endCursor,
},
updateQuery(previousResult, { fetchMoreResult }) {
const results = produce(fetchMoreResult, draftData => {
// eslint-disable-next-line no-param-reassign
draftData.group.projects.nodes = [
...previousResult.group.projects.nodes,
...draftData.group.projects.nodes,
];
});
return results;
},
});
}
},
}, },
text: { text: {
codeCoverageHeader: s__('RepositoriesAnalytics|Test Code Coverage'), codeCoverageHeader: s__('RepositoriesAnalytics|Test Code Coverage'),
...@@ -168,17 +198,17 @@ export default { ...@@ -168,17 +198,17 @@ export default {
> >
<div>{{ $options.text.downloadCSVModalDescription }}</div> <div>{{ $options.text.downloadCSVModalDescription }}</div>
<div class="gl-my-4"> <div class="gl-my-4">
<label class="gl-display-block col-form-label-sm col-form-label">{{ <label class="gl-display-block col-form-label-sm col-form-label">
$options.text.projectDropdownHeader {{ $options.text.projectDropdownHeader }}
}}</label> </label>
<gl-dropdown <gl-dropdown
:text="$options.text.projectDropdown" :text="$options.text.projectDropdown"
class="gl-w-half" class="gl-w-half"
data-testid="group-code-coverage-project-dropdown" data-testid="group-code-coverage-project-dropdown"
> >
<gl-dropdown-section-header>{{ <gl-dropdown-section-header>
$options.text.projectDropdownHeader {{ $options.text.projectDropdownHeader }}
}}</gl-dropdown-section-header> </gl-dropdown-section-header>
<gl-search-box-by-type v-model.trim="projectSearchTerm" class="gl-my-2 gl-mx-3" /> <gl-search-box-by-type v-model.trim="projectSearchTerm" class="gl-my-2 gl-mx-3" />
<gl-dropdown-item <gl-dropdown-item
:is-check-item="true" :is-check-item="true"
...@@ -196,6 +226,9 @@ export default { ...@@ -196,6 +226,9 @@ export default {
@click.native.capture.stop="clickDropdownProject(project.id)" @click.native.capture.stop="clickDropdownProject(project.id)"
>{{ project.name }}</gl-dropdown-item >{{ project.name }}</gl-dropdown-item
> >
<gl-intersection-observer v-if="projectsPageInfo.hasNextPage" @appear="loadMoreProjects">
<gl-loading-icon v-if="$apollo.queries.groupProjects.loading" size="md" />
</gl-intersection-observer>
</gl-dropdown> </gl-dropdown>
<gl-button <gl-button
...@@ -208,13 +241,13 @@ export default { ...@@ -208,13 +241,13 @@ export default {
</div> </div>
<div class="gl-my-4"> <div class="gl-my-4">
<label class="gl-display-block col-form-label-sm col-form-label">{{ <label class="gl-display-block col-form-label-sm col-form-label">
$options.text.dateRangeHeader {{ $options.text.dateRangeHeader }}
}}</label> </label>
<gl-dropdown :text="selectedDateRange.text" class="gl-w-half"> <gl-dropdown :text="selectedDateRange.text" class="gl-w-half">
<gl-dropdown-section-header>{{ <gl-dropdown-section-header>
$options.text.dateRangeHeader {{ $options.text.dateRangeHeader }}
}}</gl-dropdown-section-header> </gl-dropdown-section-header>
<gl-dropdown-item <gl-dropdown-item
v-for="dateRange in $options.dateRangeOptions" v-for="dateRange in $options.dateRangeOptions"
:key="dateRange.value" :key="dateRange.value"
......
query getGroupProjects($groupFullPath: ID!) { #import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getGroupProjects($groupFullPath: ID!, $after: String) {
group(fullPath: $groupFullPath) { group(fullPath: $groupFullPath) {
projects { projects(after: $after, first: 100) {
nodes { nodes {
name name
id id
} }
pageInfo {
...PageInfo
}
} }
} }
} }
---
title: Fetch more group projects for the test coverage dropdown
merge_request: 43044
author:
type: added
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlModal } from '@gitlab/ui'; import {
GlDropdown,
GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlModal,
} from '@gitlab/ui';
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import GroupRepositoryAnalytics from 'ee/analytics/repository_analytics/components/group_repository_analytics.vue'; import GroupRepositoryAnalytics from 'ee/analytics/repository_analytics/components/group_repository_analytics.vue';
...@@ -24,6 +30,8 @@ describe('Group repository analytics app', () => { ...@@ -24,6 +30,8 @@ describe('Group repository analytics app', () => {
wrapper wrapper
.find(`[data-testid="group-code-coverage-download-select-project-${id}"]`) .find(`[data-testid="group-code-coverage-download-select-project-${id}"]`)
.trigger('click'); .trigger('click');
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const injectedProperties = { const injectedProperties = {
groupAnalyticsCoverageReportsPath: '/coverage.csv?ref_path=refs/heads/master', groupAnalyticsCoverageReportsPath: '/coverage.csv?ref_path=refs/heads/master',
...@@ -31,24 +39,39 @@ describe('Group repository analytics app', () => { ...@@ -31,24 +39,39 @@ describe('Group repository analytics app', () => {
}; };
const groupProjectsData = [{ id: 1, name: '1' }, { id: 2, name: '2' }]; const groupProjectsData = [{ id: 1, name: '1' }, { id: 2, name: '2' }];
const createComponent = () => { const createComponent = ({ data = {}, apolloGroupProjects = {} }) => {
wrapper = shallowMount(GroupRepositoryAnalytics, { wrapper = shallowMount(GroupRepositoryAnalytics, {
localVue, localVue,
data() { data() {
return { return {
// Ensure that isSelected is set to false for each project so that every test is reset properly // Ensure that isSelected is set to false for each project so that every test is reset properly
groupProjects: groupProjectsData.map(project => ({ ...project, isSelected: false })), groupProjects: groupProjectsData.map(project => ({ ...project, isSelected: false })),
projectsPageInfo: {
hasNextPage: false,
endCursor: null,
},
...data,
}; };
}, },
provide: { provide: {
...injectedProperties, ...injectedProperties,
}, },
mocks: {
$apollo: {
queries: {
groupProjects: {
fetchMore: jest.fn().mockResolvedValue(),
...apolloGroupProjects,
},
},
},
},
stubs: { GlDropdown, GlDropdownItem, GlModal }, stubs: { GlDropdown, GlDropdownItem, GlModal },
}); });
}; };
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({});
}); });
afterEach(() => { afterEach(() => {
...@@ -121,6 +144,52 @@ describe('Group repository analytics app', () => { ...@@ -121,6 +144,52 @@ describe('Group repository analytics app', () => {
expect(findCodeCoverageDownloadButton().attributes('disabled')).toBe('true'); expect(findCodeCoverageDownloadButton().attributes('disabled')).toBe('true');
}); });
}); });
describe('when there is only one page of projects', () => {
it('should not render the intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(false);
});
});
describe('when there is more than a page of projects', () => {
beforeEach(() => {
createComponent({ data: { projectsPageInfo: { hasNextPage: true } } });
});
it('should render the intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
describe('when the intersection observer component appears in view', () => {
beforeEach(() => {
jest
.spyOn(wrapper.vm.$apollo.queries.groupProjects, 'fetchMore')
.mockImplementation(jest.fn());
findIntersectionObserver().vm.$emit('appear');
return wrapper.vm.$nextTick();
});
it('makes a query to fetch more projects', () => {
expect(wrapper.vm.$apollo.queries.groupProjects.fetchMore).toHaveBeenCalled();
});
});
describe('when a query is loading a new page of projects', () => {
beforeEach(() => {
createComponent({
data: { projectsPageInfo: { hasNextPage: true } },
apolloGroupProjects: {
loading: true,
},
});
});
it('should render the loading spinner', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
});
}); });
describe('when selecting a date range', () => { describe('when selecting a date range', () => {
......
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