Commit 62a0a5dc authored by Coung Ngo's avatar Coung Ngo

Refactor Jira importer code

Refactor code to move state closer to where it's used
parent 6966a8fb
<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,19 @@ import {
GlLabel,
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
GlTable,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
const debounceWait = 500;
export default {
name: 'JiraImportForm',
components: {
GlAlert,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
......@@ -29,10 +34,13 @@ export default {
GlLabel,
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
GlTable,
},
currentUsername: gon.current_username,
dropdownLabel: __('The GitLab user to which the Jira user %{jiraDisplayName} will be mapped'),
previousImportsMessage: __(`You have imported from this project %{numberOfPreviousImports} times
before. Each new import will create duplicate issues.`),
tableConfig: [
{
key: 'jiraDisplayName',
......@@ -47,11 +55,10 @@ export default {
label: __('GitLab username'),
},
],
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.`),
props: {
importLabel: {
type: String,
required: true,
},
isSubmitting: {
type: Boolean,
required: true,
......@@ -60,6 +67,10 @@ export default {
type: String,
required: true,
},
jiraImports: {
type: Array,
required: true,
},
jiraProjects: {
type: Array,
required: true,
......@@ -72,16 +83,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 +97,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 +150,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 +169,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 +189,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 +214,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 { 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,7 +228,7 @@ describe('JiraImportApp', () => {
expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
});
it('shows alert message with error message on error', () => {
it('shows alert message with error message on error', async () => {
const mutate = jest.fn(() => Promise.reject());
wrapper = mountComponent({ mutate });
......@@ -271,16 +236,15 @@ describe('JiraImportApp', () => {
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.');
});
await Vue.nextTick();
await Vue.nextTick();
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,9 +255,9 @@ describe('JiraImportApp', () => {
getAlert().vm.$emit('dismiss');
return Vue.nextTick().then(() => {
expect(getAlert().exists()).toBe(false);
});
await Vue.nextTick();
expect(getAlert().exists()).toBe(false);
});
});
......@@ -319,11 +283,15 @@ describe('JiraImportApp', () => {
expect(mutateSpy).not.toHaveBeenCalled();
});
it('shows error message when there is an error with the GraphQL mutation call', () => {
it('shows error message when there is an error with the GraphQL mutation call', async () => {
const mutate = jest.fn(() => Promise.reject());
wrapper = mountComponent({ mutate });
// One tick doesn't update the dom to the desired state so we have two ticks here
await Vue.nextTick();
await Vue.nextTick();
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