Commit 34281425 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch 'cngo-refactor-jira-importer' into 'master'

Refactor Jira importer code

See merge request gitlab-org/gitlab!39219
parents be7a6b4b e488d70c
<script>
import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { last } from 'lodash';
import { __ } from '~/locale';
import getJiraImportDetailsQuery from '../queries/get_jira_import_details.query.graphql';
......@@ -16,7 +16,6 @@ export default {
components: {
GlAlert,
GlLoadingIcon,
GlSprintf,
JiraImportForm,
JiraImportProgress,
JiraImportSetup,
......@@ -55,7 +54,6 @@ export default {
return {
isSubmitting: false,
jiraImportDetails: {},
selectedProject: undefined,
userMappings: [],
errorMessage: '',
showAlert: false,
......@@ -80,22 +78,6 @@ export default {
},
},
},
computed: {
numberOfPreviousImports() {
return this.jiraImportDetails.imports?.reduce?.(
(acc, jiraProject) => (jiraProject.jiraProjectKey === this.selectedProject ? acc + 1 : acc),
0,
);
},
hasPreviousImports() {
return this.numberOfPreviousImports > 0;
},
importLabel() {
return this.selectedProject
? `jira-import::${this.selectedProject}-${this.numberOfPreviousImports + 1}`
: 'jira-import::KEY-1';
},
},
mounted() {
if (this.isJiraConfigured) {
this.$apollo
......@@ -168,9 +150,6 @@ export default {
this.showAlert = false;
},
},
previousImportsMessage: __(
'You have imported from this project %{numberOfPreviousImports} times before. Each new import will create duplicate issues.',
),
};
</script>
......@@ -179,11 +158,6 @@ export default {
<gl-alert v-if="showAlert" variant="danger" @dismiss="dismissAlert">
{{ errorMessage }}
</gl-alert>
<gl-alert v-if="hasPreviousImports" variant="warning" :dismissible="false">
<gl-sprintf :message="$options.previousImportsMessage">
<template #numberOfPreviousImports>{{ numberOfPreviousImports }}</template>
</gl-sprintf>
</gl-alert>
<jira-import-setup
v-if="!isJiraConfigured"
......@@ -201,10 +175,9 @@ export default {
/>
<jira-import-form
v-else
v-model="selectedProject"
:import-label="importLabel"
:is-submitting="isSubmitting"
:issues-path="issuesPath"
:jira-imports="jiraImportDetails.imports"
:jira-projects="jiraImportDetails.projects"
:project-id="projectId"
:user-mappings="userMappings"
......
<script>
import {
GlAlert,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
......@@ -10,15 +11,23 @@ import {
GlLabel,
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
GlTable,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import {
debounceWait,
dropdownLabel,
previousImportsMessage,
tableConfig,
userMappingMessage,
} from '../utils/constants';
export default {
name: 'JiraImportForm',
components: {
GlAlert,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
......@@ -29,29 +38,15 @@ export default {
GlLabel,
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
GlTable,
},
currentUsername: gon.current_username,
dropdownLabel: __('The GitLab user to which the Jira user %{jiraDisplayName} will be mapped'),
tableConfig: [
{
key: 'jiraDisplayName',
label: __('Jira display name'),
},
{
key: 'arrow',
label: '',
},
{
key: 'gitlabUsername',
label: __('GitLab username'),
},
],
dropdownLabel,
previousImportsMessage,
tableConfig,
userMappingMessage,
props: {
importLabel: {
type: String,
required: true,
},
isSubmitting: {
type: Boolean,
required: true,
......@@ -60,6 +55,10 @@ export default {
type: String,
required: true,
},
jiraImports: {
type: Array,
required: true,
},
jiraProjects: {
type: Array,
required: true,
......@@ -72,16 +71,12 @@ export default {
type: Array,
required: true,
},
value: {
type: String,
required: false,
default: undefined,
},
},
data() {
return {
isFetching: false,
searchTerm: '',
selectedProject: undefined,
selectState: null,
users: [],
};
......@@ -90,11 +85,25 @@ export default {
shouldShowNoMatchesFoundText() {
return !this.isFetching && this.users.length === 0;
},
numberOfPreviousImports() {
return this.jiraImports?.reduce?.(
(acc, jiraProject) => (jiraProject.jiraProjectKey === this.selectedProject ? acc + 1 : acc),
0,
);
},
hasPreviousImports() {
return this.numberOfPreviousImports > 0;
},
importLabel() {
return this.selectedProject
? `jira-import::${this.selectedProject}-${this.numberOfPreviousImports + 1}`
: 'jira-import::KEY-1';
},
},
watch: {
searchTerm: debounce(function debouncedUserSearch() {
this.searchUsers();
}, 500),
}, debounceWait),
},
mounted() {
this.searchUsers()
......@@ -129,9 +138,9 @@ export default {
},
initiateJiraImport(event) {
event.preventDefault();
if (this.value) {
if (this.selectedProject) {
this.hideValidationError();
this.$emit('initiateJiraImport', this.value);
this.$emit('initiateJiraImport', this.selectedProject);
} else {
this.showValidationError();
}
......@@ -148,8 +157,16 @@ export default {
<template>
<div>
<gl-alert v-if="hasPreviousImports" variant="warning" :dismissible="false">
<gl-sprintf :message="$options.previousImportsMessage">
<template #numberOfPreviousImports>{{ numberOfPreviousImports }}</template>
</gl-sprintf>
</gl-alert>
<h3 class="page-title">{{ __('New Jira import') }}</h3>
<hr />
<form @submit="initiateJiraImport">
<gl-form-group
class="row align-items-center"
......@@ -160,12 +177,11 @@ export default {
>
<gl-form-select
id="jira-project-select"
v-model="selectedProject"
data-qa-selector="jira_project_dropdown"
class="mb-2"
:options="jiraProjects"
:state="selectState"
:value="value"
@change="$emit('input', $event)"
/>
</gl-form-group>
......@@ -186,16 +202,7 @@ export default {
<h4 class="gl-mb-4">{{ __('Jira-GitLab user mapping template') }}</h4>
<p>
{{
__(
`Jira users have been imported from the configured Jira instance.
They can be mapped by selecting a GitLab user from the dropdown in the "GitLab
username" column.
When the form appears, the dropdown defaults to the user conducting the import.`,
)
}}
</p>
<p>{{ $options.userMappingMessage }}</p>
<gl-table :fields="$options.tableConfig" :items="userMappings" fixed>
<template #cell(arrow)>
......
import { __ } from '~/locale';
export const debounceWait = 500;
export const dropdownLabel = __(
'The GitLab user to which the Jira user %{jiraDisplayName} will be mapped',
);
export const previousImportsMessage = __(`You have imported from this project
%{numberOfPreviousImports} times before. Each new import will create duplicate issues.`);
export const tableConfig = [
{
key: 'jiraDisplayName',
label: __('Jira display name'),
},
{
key: 'arrow',
label: '',
},
{
key: 'gitlabUsername',
label: __('GitLab username'),
},
];
export const userMappingMessage = __(`Jira users have been imported from the configured Jira
instance. They can be mapped by selecting a GitLab user from the dropdown in the "GitLab username"
column. When the form appears, the dropdown defaults to the user conducting the import.`);
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
......@@ -29,14 +29,12 @@ describe('JiraImportApp', () => {
const mountComponent = ({
isJiraConfigured = true,
errorMessage = '',
selectedProject = 'MTG',
showAlert = false,
isInProgress = false,
loading = false,
mutate = mutateSpy,
mountFunction = shallowMount,
} = {}) =>
mountFunction(JiraImportApp, {
shallowMount(JiraImportApp, {
propsData: {
inProgressIllustration: 'in-progress-illustration.svg',
isJiraConfigured,
......@@ -49,7 +47,6 @@ describe('JiraImportApp', () => {
data() {
return {
isSubmitting: false,
selectedProject,
userMappings,
errorMessage,
showAlert,
......@@ -202,38 +199,6 @@ describe('JiraImportApp', () => {
});
});
describe('jira import form screen', () => {
describe('when selected project has been imported before', () => {
it('shows jira-import::MTG-3 label since project MTG has been imported 2 time before', () => {
wrapper = mountComponent();
expect(getFormComponent().props('importLabel')).toBe('jira-import::MTG-3');
});
it('shows warning alert to explain project MTG has been imported 2 times before', () => {
wrapper = mountComponent({ mountFunction: mount });
expect(getAlert().text()).toBe(
'You have imported from this project 2 times before. Each new import will create duplicate issues.',
);
});
});
describe('when selected project has not been imported before', () => {
beforeEach(() => {
wrapper = mountComponent({ selectedProject: 'MJP' });
});
it('shows jira-import::MJP-1 label since project MJP has not been imported before', () => {
expect(getFormComponent().props('importLabel')).toBe('jira-import::MJP-1');
});
it('does not show warning alert since project MJP has not been imported before', () => {
expect(getAlert().exists()).toBe(false);
});
});
});
describe('initiating a Jira import', () => {
it('calls the mutation with the expected arguments', () => {
wrapper = mountComponent();
......@@ -263,24 +228,22 @@ describe('JiraImportApp', () => {
expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
});
it('shows alert message with error message on error', () => {
const mutate = jest.fn(() => Promise.reject());
wrapper = mountComponent({ mutate });
describe('when there is an error', () => {
beforeEach(() => {
const mutate = jest.fn(() => Promise.reject());
wrapper = mountComponent({ mutate });
getFormComponent().vm.$emit('initiateJiraImport', 'MTG');
getFormComponent().vm.$emit('initiateJiraImport', 'MTG');
});
// One tick doesn't update the dom to the desired state so we have two ticks here
return Vue.nextTick()
.then(Vue.nextTick)
.then(() => {
expect(getAlert().text()).toBe('There was an error importing the Jira project.');
});
it('shows alert message with error message', async () => {
expect(getAlert().text()).toBe('There was an error importing the Jira project.');
});
});
});
describe('alert', () => {
it('can be dismissed', () => {
it('can be dismissed', async () => {
wrapper = mountComponent({
errorMessage: 'There was an error importing the Jira project.',
showAlert: true,
......@@ -291,14 +254,14 @@ describe('JiraImportApp', () => {
getAlert().vm.$emit('dismiss');
return Vue.nextTick().then(() => {
expect(getAlert().exists()).toBe(false);
});
await Vue.nextTick();
expect(getAlert().exists()).toBe(false);
});
});
describe('on mount', () => {
it('makes a GraphQL mutation call to get user mappings', () => {
describe('on mount GraphQL user mapping mutation', () => {
it('is called with the expected arguments', () => {
wrapper = mountComponent();
const mutationArguments = {
......@@ -313,18 +276,23 @@ describe('JiraImportApp', () => {
expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
});
it('does not make a GraphQL mutation call to get user mappings when Jira is not configured', () => {
wrapper = mountComponent({ isJiraConfigured: false });
describe('when Jira is not configured', () => {
it('is not called', () => {
wrapper = mountComponent({ isJiraConfigured: false });
expect(mutateSpy).not.toHaveBeenCalled();
expect(mutateSpy).not.toHaveBeenCalled();
});
});
it('shows error message when there is an error with the GraphQL mutation call', () => {
const mutate = jest.fn(() => Promise.reject());
wrapper = mountComponent({ mutate });
describe('when there is an error when called', () => {
beforeEach(() => {
const mutate = jest.fn(() => Promise.reject());
wrapper = mountComponent({ mutate });
});
expect(getAlert().exists()).toBe(true);
it('shows error message', () => {
expect(getAlert().exists()).toBe(true);
});
});
});
});
import { GlButton, GlNewDropdown, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
import { GlAlert, GlButton, GlNewDropdown, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
import { issuesPath, jiraProjects, userMappings as defaultUserMappings } from '../mock_data';
import {
imports,
issuesPath,
jiraProjects,
userMappings as defaultUserMappings,
} from '../mock_data';
describe('JiraImportForm', () => {
let axiosMock;
let wrapper;
const currentUsername = 'mrgitlab';
const importLabel = 'jira-import::MTG-1';
const value = 'MTG';
const getAlert = () => wrapper.find(GlAlert);
const getSelectDropdown = () => wrapper.find(GlFormSelect);
......@@ -20,6 +25,8 @@ describe('JiraImportForm', () => {
const getCancelButton = () => wrapper.findAll(GlButton).at(1);
const getLabel = () => wrapper.find(GlLabel);
const getTable = () => wrapper.find(GlTable);
const getUserDropdown = () => getTable().find(GlNewDropdown);
......@@ -28,22 +35,23 @@ describe('JiraImportForm', () => {
const mountComponent = ({
isSubmitting = false,
selectedProject = 'MTG',
userMappings = defaultUserMappings,
mountFunction = shallowMount,
} = {}) =>
mountFunction(JiraImportForm, {
propsData: {
importLabel,
isSubmitting,
issuesPath,
jiraImports: imports,
jiraProjects,
projectId: '5',
userMappings,
value,
},
data: () => ({
isFetching: false,
searchTerm: '',
selectedProject,
selectState: null,
users: [],
}),
......@@ -60,7 +68,7 @@ describe('JiraImportForm', () => {
wrapper = null;
});
describe('select dropdown', () => {
describe('select dropdown project selection', () => {
it('is shown', () => {
wrapper = mountComponent();
......@@ -77,12 +85,34 @@ describe('JiraImportForm', () => {
});
});
it('emits an "input" event when the input select value changes', () => {
wrapper = mountComponent();
describe('when selected project has been imported before', () => {
it('shows jira-import::MTG-3 label since project MTG has been imported 2 time before', () => {
wrapper = mountComponent();
expect(getLabel().props('title')).toBe('jira-import::MTG-3');
});
it('shows warning alert to explain project MTG has been imported 2 times before', () => {
wrapper = mountComponent({ mountFunction: mount });
expect(getAlert().text()).toBe(
'You have imported from this project 2 times before. Each new import will create duplicate issues.',
);
});
});
getSelectDropdown().vm.$emit('change', value);
describe('when selected project has not been imported before', () => {
beforeEach(() => {
wrapper = mountComponent({ selectedProject: 'MJP' });
});
it('shows jira-import::MJP-1 label since project MJP has not been imported before', () => {
expect(getLabel().props('title')).toBe('jira-import::MJP-1');
});
expect(wrapper.emitted('input')[0]).toEqual([value]);
it('does not show warning alert since project MJP has not been imported before', () => {
expect(getAlert().exists()).toBe(false);
});
});
});
......@@ -91,10 +121,6 @@ describe('JiraImportForm', () => {
wrapper = mountComponent();
});
it('shows a label which will be applied to imported Jira projects', () => {
expect(wrapper.find(GlLabel).props('title')).toBe(importLabel);
});
it('shows a heading for the user mapping section', () => {
expect(
getByRole(wrapper.element, 'heading', { name: 'Jira-GitLab user mapping template' }),
......@@ -214,11 +240,13 @@ describe('JiraImportForm', () => {
describe('form', () => {
it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => {
wrapper = mountComponent();
const selectedProject = 'MTG';
wrapper = mountComponent({ selectedProject });
wrapper.find('form').trigger('submit');
expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([value]);
expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([selectedProject]);
});
});
});
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