Commit 2754e4db authored by Tom Quirk's avatar Tom Quirk

Add project_dropdown component for Jira Connect

Adds a new component for project selection when
creating a branch via Jira connect.
parent 3ffa2478
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { PROJECTS_PER_PAGE } from '../constants';
import getProjectsQuery from '../graphql/queries/get_projects.query.graphql';
export default {
PROJECTS_PER_PAGE,
projectQueryPageInfo: {
endCursor: '',
},
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
},
props: {
selectedProject: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
initialProjectsLoading: true,
projectSearchQuery: '',
};
},
apollo: {
projects: {
query: getProjectsQuery,
variables() {
return {
search: this.projectSearchQuery,
first: this.$options.PROJECTS_PER_PAGE,
after: this.$options.projectQueryPageInfo.endCursor,
searchNamespaces: true,
sort: 'similarity',
};
},
update(data) {
return data?.projects?.nodes.filter((project) => !project.repository.empty) ?? [];
},
result() {
this.initialProjectsLoading = false;
},
error() {
this.onError({ message: __('Failed to load projects.') });
},
},
},
computed: {
projectsLoading() {
return Boolean(this.$apollo.queries.projects.loading);
},
projectDropdownText() {
return this.selectedProject?.nameWithNamespace || __('Select a project');
},
},
methods: {
async onProjectSelect(project) {
this.$emit('change', project);
},
onError({ message } = {}) {
this.$emit('error', { message });
},
isProjectSelected(project) {
return project.id === this.selectedProject?.id;
},
},
};
</script>
<template>
<gl-dropdown :text="projectDropdownText" :loading="initialProjectsLoading">
<template #header>
<gl-search-box-by-type v-model.trim="projectSearchQuery" :debounce="250" />
</template>
<gl-loading-icon v-show="projectsLoading" />
<template v-if="!projectsLoading">
<gl-dropdown-item
v-for="project in projects"
:key="project.id"
is-check-item
:is-checked="isProjectSelected(project)"
@click="onProjectSelect(project)"
>
{{ project.nameWithNamespace }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>
export const BRANCHES_PER_PAGE = 20;
export const PROJECTS_PER_PAGE = 20;
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getProjects(
$search: String!
$after: String = ""
$first: Int!
$searchNamespaces: Boolean = false
$sort: String
$membership: Boolean = true
) {
projects(
search: $search
after: $after
first: $first
membership: $membership
searchNamespaces: $searchNamespaces
sort: $sort
) {
nodes {
id
name
nameWithNamespace
fullPath
avatarUrl
path
repository {
empty
}
}
pageInfo {
...PageInfo
}
}
}
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ProjectDropdown from '~/jira_connect/branches/components/project_dropdown.vue';
import { PROJECTS_PER_PAGE } from '~/jira_connect/branches/constants';
import getProjectsQuery from '~/jira_connect/branches/graphql/queries/get_projects.query.graphql';
const localVue = createLocalVue();
const mockProjects = [
{
id: 'test',
name: 'test',
nameWithNamespace: 'test',
avatarUrl: 'https://gitlab.com',
path: 'test-path',
fullPath: 'test-path',
repository: {
empty: false,
},
},
{
id: 'gitlab',
name: 'GitLab',
nameWithNamespace: 'gitlab-org/gitlab',
avatarUrl: 'https://gitlab.com',
path: 'gitlab',
fullPath: 'gitlab-org/gitlab',
repository: {
empty: false,
},
},
];
const mockProjectsQueryResponse = {
data: {
projects: {
nodes: mockProjects,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
},
},
};
const mockGetProjectsQuerySuccess = jest.fn().mockResolvedValue(mockProjectsQueryResponse);
const mockGetProjectsQueryFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
describe('ProjectDropdown', () => {
let wrapper;
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDropdownItemByText = (text) =>
findAllDropdownItems().wrappers.find((item) => item.text() === text);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
function createMockApolloProvider({ mockGetProjectsQuery = mockGetProjectsQuerySuccess } = {}) {
localVue.use(VueApollo);
const mockApollo = createMockApollo([[getProjectsQuery, mockGetProjectsQuery]]);
return mockApollo;
}
function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) {
wrapper = mountFn(ProjectDropdown, {
localVue,
apolloProvider: mockApollo || createMockApolloProvider(),
propsData: props,
});
}
afterEach(() => {
wrapper.destroy();
});
describe('when loading projects', () => {
beforeEach(() => {
createComponent({
mockApollo: createMockApolloProvider({ mockGetProjectsQuery: mockQueryLoading }),
});
});
it('sets dropdown `loading` prop to `true`', () => {
expect(findDropdown().props('loading')).toBe(true);
});
it('renders loading icon in dropdown', () => {
expect(findLoadingIcon().isVisible()).toBe(true);
});
});
describe('when projects query succeeds', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
await wrapper.vm.$nextTick();
});
it('sets dropdown `loading` prop to `false`', () => {
expect(findDropdown().props('loading')).toBe(false);
});
it('renders dropdown items', () => {
const dropdownItems = findAllDropdownItems();
expect(dropdownItems.wrappers).toHaveLength(mockProjects.length);
expect(dropdownItems.wrappers.map((item) => item.text())).toEqual(
mockProjects.map((project) => project.nameWithNamespace),
);
});
describe('when selecting a dropdown item', () => {
it('emits `change` event with the selected project name', async () => {
const mockProject = mockProjects[0];
const itemToSelect = findDropdownItemByText(mockProject.nameWithNamespace);
await itemToSelect.vm.$emit('click');
expect(wrapper.emitted('change')[0]).toEqual([mockProject]);
});
});
describe('when `selectedProject` prop is specified', () => {
const mockProject = mockProjects[0];
beforeEach(async () => {
wrapper.setProps({
selectedProject: mockProject,
});
});
it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
expect(findDropdownItemByText(mockProject.nameWithNamespace).props('isChecked')).toBe(true);
});
it('sets dropdown text to `selectedBranchName` value', () => {
expect(findDropdown().props('text')).toBe(mockProject.nameWithNamespace);
});
});
});
describe('when projects query fails', () => {
beforeEach(async () => {
createComponent({
mockApollo: createMockApolloProvider({ mockGetProjectsQuery: mockGetProjectsQueryFailed }),
});
await waitForPromises();
});
it('emits `error` event', () => {
expect(wrapper.emitted('error')).toBeTruthy();
});
});
describe('when searching branches', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount });
await waitForPromises();
jest.clearAllMocks();
const mockSearchTerm = 'gitl';
await findSearchBox().vm.$emit('input', mockSearchTerm);
expect(mockGetProjectsQuerySuccess).toHaveBeenCalledWith({
after: '',
first: PROJECTS_PER_PAGE,
membership: true,
search: mockSearchTerm,
searchNamespaces: true,
sort: 'similarity',
});
});
});
});
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