Commit c3851700 authored by Tom Quirk's avatar Tom Quirk

Add source_branch_dropdown component for Jira

Adds a new component for selecting the source branch
when creating a new branch via Jira Connect.
parent 2754e4db
......@@ -47,7 +47,7 @@ export default {
this.initialProjectsLoading = false;
},
error() {
this.onError({ message: __('Failed to load projects.') });
this.onError({ message: __('Failed to load projects') });
},
},
},
......
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { BRANCHES_PER_PAGE } from '../constants';
import getProjectQuery from '../graphql/queries/get_project.query.graphql';
export default {
BRANCHES_PER_PAGE,
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
},
props: {
selectedProject: {
type: Object,
required: false,
default: null,
},
selectedBranchName: {
type: String,
required: false,
default: null,
},
},
data() {
return {
sourceBranchSearchQuery: '',
initialSourceBranchNamesLoading: false,
sourceBranchNamesLoading: false,
sourceBranchNames: [],
};
},
computed: {
hasSelectedProject() {
return Boolean(this.selectedProject);
},
hasSelectedSourceBranch() {
return Boolean(this.selectedBranchName);
},
branchDropdownText() {
return this.selectedBranchName || __('Select a branch');
},
},
watch: {
selectedProject: {
immediate: true,
async handler(selectedProject) {
if (!selectedProject) return;
this.initialSourceBranchNamesLoading = true;
await this.fetchSourceBranchNames({ projectPath: selectedProject.fullPath });
this.initialSourceBranchNamesLoading = false;
},
},
},
methods: {
onSourceBranchSelect(branchName) {
this.$emit('change', branchName);
},
onSourceBranchSearchQuery(branchSearchQuery) {
this.branchSearchQuery = branchSearchQuery;
this.fetchSourceBranchNames({
projectPath: this.selectedProject.fullPath,
searchPattern: this.branchSearchQuery,
});
},
onError({ message } = {}) {
this.$emit('error', { message });
},
async fetchSourceBranchNames({ projectPath, searchPattern } = {}) {
this.sourceBranchNamesLoading = true;
try {
const { data } = await this.$apollo.query({
query: getProjectQuery,
variables: {
projectPath,
branchNamesLimit: this.$options.BRANCHES_PER_PAGE,
branchNamesOffset: 0,
branchNamesSearchPattern: searchPattern ? `*${searchPattern}*` : '*',
},
});
const { branchNames, rootRef } = data?.project.repository || {};
this.sourceBranchNames = branchNames || [];
// Use root ref as the default selection
if (rootRef && !this.hasSelectedSourceBranch) {
this.onSourceBranchSelect(rootRef);
}
} catch (err) {
this.onError({
message: __('Something went wrong while fetching source branches.'),
});
} finally {
this.sourceBranchNamesLoading = false;
}
},
},
};
</script>
<template>
<gl-dropdown
:text="branchDropdownText"
:loading="initialSourceBranchNamesLoading"
:disabled="!hasSelectedProject"
:class="{ 'gl-font-monospace': hasSelectedSourceBranch }"
>
<template #header>
<gl-search-box-by-type
:debounce="250"
:value="sourceBranchSearchQuery"
@input="onSourceBranchSearchQuery"
/>
</template>
<gl-loading-icon v-show="sourceBranchNamesLoading" />
<template v-if="!sourceBranchNamesLoading">
<gl-dropdown-item
v-for="branchName in sourceBranchNames"
v-show="!sourceBranchNamesLoading"
:key="branchName"
:is-checked="branchName === selectedBranchName"
is-check-item
class="gl-font-monospace"
@click="onSourceBranchSelect(branchName)"
>
{{ branchName }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>
query getProject(
$projectPath: ID!
$branchNamesLimit: Int!
$branchNamesOffset: Int!
$branchNamesSearchPattern: String!
) {
project(fullPath: $projectPath) {
repository {
branchNames(
limit: $branchNamesLimit
offset: $branchNamesOffset
searchPattern: $branchNamesSearchPattern
)
rootRef
}
}
}
......@@ -30600,6 +30600,9 @@ msgstr ""
msgid "Something went wrong while fetching requirements list."
msgstr ""
msgid "Something went wrong while fetching source branches."
msgstr ""
msgid "Something went wrong while fetching the environments for this merge request. Please try again."
msgstr ""
......
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 SourceBranchDropdown from '~/jira_connect/branches/components/source_branch_dropdown.vue';
import { BRANCHES_PER_PAGE } from '~/jira_connect/branches/constants';
import getProjectQuery from '~/jira_connect/branches/graphql/queries/get_project.query.graphql';
const localVue = createLocalVue();
const mockProject = {
id: 'test',
fullPath: 'test-path',
repository: {
branchNames: ['main', 'f-test', 'release'],
rootRef: 'main',
},
};
const mockProjectQueryResponse = {
data: {
project: mockProject,
},
};
const mockGetProjectQuery = jest.fn().mockResolvedValue(mockProjectQueryResponse);
const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
describe('SourceBranchDropdown', () => {
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);
const assertDropdownItems = () => {
const dropdownItems = findAllDropdownItems();
expect(dropdownItems.wrappers).toHaveLength(mockProject.repository.branchNames.length);
expect(dropdownItems.wrappers.map((item) => item.text())).toEqual(
mockProject.repository.branchNames,
);
};
function createMockApolloProvider({ getProjectQueryLoading = false } = {}) {
localVue.use(VueApollo);
const mockApollo = createMockApollo([
[getProjectQuery, getProjectQueryLoading ? mockQueryLoading : mockGetProjectQuery],
]);
return mockApollo;
}
function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) {
wrapper = mountFn(SourceBranchDropdown, {
localVue,
apolloProvider: mockApollo || createMockApolloProvider(),
propsData: props,
});
}
afterEach(() => {
wrapper.destroy();
});
describe('when `selectedProject` prop is not specified', () => {
beforeEach(() => {
createComponent();
});
it('sets dropdown `disabled` prop to `true`', () => {
expect(findDropdown().props('disabled')).toBe(true);
});
describe('when `selectedProject` becomes specified', () => {
beforeEach(async () => {
wrapper.setProps({
selectedProject: mockProject,
});
await waitForPromises();
});
it('sets dropdown props correctly', () => {
expect(findDropdown().props()).toMatchObject({
loading: false,
disabled: false,
text: 'Select a branch',
});
});
it('renders available source branches as dropdown items', () => {
assertDropdownItems();
});
});
});
describe('when `selectedProject` prop is specified', () => {
describe('when branches are loading', () => {
it('renders loading icon in dropdown', () => {
createComponent({
mockApollo: createMockApolloProvider({ getProjectQueryLoading: true }),
props: { selectedProject: mockProject },
});
expect(findLoadingIcon().isVisible()).toBe(true);
});
});
describe('when branches have loaded', () => {
describe('when searching branches', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount, props: { selectedProject: mockProject } });
await waitForPromises();
jest.clearAllMocks();
const mockSearchTerm = 'mai';
await findSearchBox().vm.$emit('input', mockSearchTerm);
expect(mockGetProjectQuery).toHaveBeenCalledWith({
branchNamesLimit: BRANCHES_PER_PAGE,
branchNamesOffset: 0,
branchNamesSearchPattern: `*${mockSearchTerm}*`,
projectPath: 'test-path',
});
});
});
describe('template', () => {
beforeEach(async () => {
createComponent({ props: { selectedProject: mockProject } });
await waitForPromises();
});
it('sets dropdown props correctly', () => {
expect(findDropdown().props()).toMatchObject({
loading: false,
disabled: false,
text: 'Select a branch',
});
});
it('omits monospace styling from dropdown', () => {
expect(findDropdown().classes()).not.toContain('gl-font-monospace');
});
it('renders available source branches as dropdown items', () => {
assertDropdownItems();
});
it("emits `change` event with the repository's `rootRef` by default", () => {
expect(wrapper.emitted('change')[0]).toEqual([mockProject.repository.rootRef]);
});
describe('when selecting a dropdown item', () => {
it('emits `change` event with the selected branch name', async () => {
const mockBranchName = mockProject.repository.branchNames[1];
const itemToSelect = findDropdownItemByText(mockBranchName);
await itemToSelect.vm.$emit('click');
expect(wrapper.emitted('change')[1]).toEqual([mockBranchName]);
});
});
describe('when `selectedBranchName` prop is specified', () => {
const mockBranchName = mockProject.repository.branchNames[2];
beforeEach(async () => {
wrapper.setProps({
selectedBranchName: mockBranchName,
});
});
it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
expect(findDropdownItemByText(mockBranchName).props('isChecked')).toBe(true);
});
it('sets dropdown text to `selectedBranchName` value', () => {
expect(findDropdown().props('text')).toBe(mockBranchName);
});
it('adds monospace styling to dropdown', () => {
expect(findDropdown().classes()).toContain('gl-font-monospace');
});
});
});
});
});
});
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