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