Commit fa5b37ec authored by Denys Mishunov's avatar Denys Mishunov

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

Refactor Jira importer

See merge request gitlab-org/gitlab!39474
parents 6d59e61c d19f03f3
<script> <script>
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { last } from 'lodash'; import { last } from 'lodash';
import { __ } from '~/locale';
import getJiraImportDetailsQuery from '../queries/get_jira_import_details.query.graphql'; import getJiraImportDetailsQuery from '../queries/get_jira_import_details.query.graphql';
import getJiraUserMappingMutation from '../queries/get_jira_user_mapping.mutation.graphql';
import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql';
import { addInProgressImportToStore } from '../utils/cache_update';
import { isInProgress, extractJiraProjectsOptions } from '../utils/jira_import_utils'; import { isInProgress, extractJiraProjectsOptions } from '../utils/jira_import_utils';
import JiraImportForm from './jira_import_form.vue'; import JiraImportForm from './jira_import_form.vue';
import JiraImportProgress from './jira_import_progress.vue'; import JiraImportProgress from './jira_import_progress.vue';
...@@ -52,9 +48,7 @@ export default { ...@@ -52,9 +48,7 @@ export default {
}, },
data() { data() {
return { return {
isSubmitting: false,
jiraImportDetails: {}, jiraImportDetails: {},
userMappings: [],
errorMessage: '', errorMessage: '',
showAlert: false, showAlert: false,
}; };
...@@ -78,70 +72,7 @@ export default { ...@@ -78,70 +72,7 @@ export default {
}, },
}, },
}, },
mounted() {
if (this.isJiraConfigured) {
this.$apollo
.mutate({
mutation: getJiraUserMappingMutation,
variables: {
input: {
projectPath: this.projectPath,
},
},
})
.then(({ data }) => {
if (data.jiraImportUsers.errors.length) {
this.setAlertMessage(data.jiraImportUsers.errors.join('. '));
} else {
this.userMappings = data.jiraImportUsers.jiraUsers;
}
})
.catch(() => this.setAlertMessage(__('There was an error retrieving the Jira users.')));
}
},
methods: { methods: {
initiateJiraImport(project) {
this.isSubmitting = true;
this.$apollo
.mutate({
mutation: initiateJiraImportMutation,
variables: {
input: {
jiraProjectKey: project,
projectPath: this.projectPath,
usersMapping: this.userMappings.map(({ gitlabId, jiraAccountId }) => ({
gitlabId,
jiraAccountId,
})),
},
},
update: (store, { data }) =>
addInProgressImportToStore(store, data.jiraImportStart, this.projectPath),
})
.then(({ data }) => {
if (data.jiraImportStart.errors.length) {
this.setAlertMessage(data.jiraImportStart.errors.join('. '));
} else {
this.selectedProject = undefined;
}
})
.catch(() => this.setAlertMessage(__('There was an error importing the Jira project.')))
.finally(() => {
this.isSubmitting = false;
});
},
updateMapping(jiraAccountId, gitlabId, gitlabUsername) {
this.userMappings = this.userMappings.map(userMapping =>
userMapping.jiraAccountId === jiraAccountId
? {
...userMapping,
gitlabId,
gitlabUsername,
}
: userMapping,
);
},
setAlertMessage(message) { setAlertMessage(message) {
this.errorMessage = message; this.errorMessage = message;
this.showAlert = true; this.showAlert = true;
...@@ -175,14 +106,12 @@ export default { ...@@ -175,14 +106,12 @@ export default {
/> />
<jira-import-form <jira-import-form
v-else v-else
:is-submitting="isSubmitting"
:issues-path="issuesPath" :issues-path="issuesPath"
:jira-imports="jiraImportDetails.imports" :jira-imports="jiraImportDetails.imports"
:jira-projects="jiraImportDetails.projects" :jira-projects="jiraImportDetails.projects"
:project-id="projectId" :project-id="projectId"
:user-mappings="userMappings" :project-path="projectPath"
@initiateJiraImport="initiateJiraImport" @error="setAlertMessage"
@updateMapping="updateMapping"
/> />
</div> </div>
</template> </template>
...@@ -16,6 +16,10 @@ import { ...@@ -16,6 +16,10 @@ import {
} 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 getJiraUserMappingMutation from '../queries/get_jira_user_mapping.mutation.graphql';
import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql';
import { addInProgressImportToStore } from '../utils/cache_update';
import { import {
debounceWait, debounceWait,
dropdownLabel, dropdownLabel,
...@@ -47,10 +51,6 @@ export default { ...@@ -47,10 +51,6 @@ export default {
tableConfig, tableConfig,
userMappingMessage, userMappingMessage,
props: { props: {
isSubmitting: {
type: Boolean,
required: true,
},
issuesPath: { issuesPath: {
type: String, type: String,
required: true, required: true,
...@@ -67,17 +67,19 @@ export default { ...@@ -67,17 +67,19 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
userMappings: { projectPath: {
type: Array, type: String,
required: true, required: true,
}, },
}, },
data() { data() {
return { return {
isFetching: false, isFetching: false,
isSubmitting: false,
searchTerm: '', searchTerm: '',
selectedProject: undefined, selectedProject: undefined,
selectState: null, selectState: null,
userMappings: [],
users: [], users: [],
}; };
}, },
...@@ -106,6 +108,24 @@ export default { ...@@ -106,6 +108,24 @@ export default {
}, debounceWait), }, debounceWait),
}, },
mounted() { mounted() {
this.$apollo
.mutate({
mutation: getJiraUserMappingMutation,
variables: {
input: {
projectPath: this.projectPath,
},
},
})
.then(({ data }) => {
if (data.jiraImportUsers.errors.length) {
this.$emit('error', data.jiraImportUsers.errors.join('. '));
} else {
this.userMappings = data.jiraImportUsers.jiraUsers;
}
})
.catch(() => this.$emit('error', __('There was an error retrieving the Jira users.')));
this.searchUsers() this.searchUsers()
.then(data => { .then(data => {
this.initialUsers = data; this.initialUsers = data;
...@@ -138,13 +158,54 @@ export default { ...@@ -138,13 +158,54 @@ export default {
}, },
initiateJiraImport(event) { initiateJiraImport(event) {
event.preventDefault(); event.preventDefault();
if (this.selectedProject) { if (this.selectedProject) {
this.hideValidationError(); this.hideValidationError();
this.$emit('initiateJiraImport', this.selectedProject);
this.isSubmitting = true;
this.$apollo
.mutate({
mutation: initiateJiraImportMutation,
variables: {
input: {
jiraProjectKey: this.selectedProject,
projectPath: this.projectPath,
usersMapping: this.userMappings.map(({ gitlabId, jiraAccountId }) => ({
gitlabId,
jiraAccountId,
})),
},
},
update: (store, { data }) =>
addInProgressImportToStore(store, data.jiraImportStart, this.projectPath),
})
.then(({ data }) => {
if (data.jiraImportStart.errors.length) {
this.$emit('error', data.jiraImportStart.errors.join('. '));
} else {
this.selectedProject = undefined;
}
})
.catch(() => this.$emit('error', __('There was an error importing the Jira project.')))
.finally(() => {
this.isSubmitting = false;
});
} else { } else {
this.showValidationError(); this.showValidationError();
} }
}, },
updateMapping(jiraAccountId, gitlabId, gitlabUsername) {
this.userMappings = this.userMappings.map(userMapping =>
userMapping.jiraAccountId === jiraAccountId
? {
...userMapping,
gitlabId,
gitlabUsername,
}
: userMapping,
);
},
hideValidationError() { hideValidationError() {
this.selectState = null; this.selectState = null;
}, },
...@@ -227,7 +288,7 @@ export default { ...@@ -227,7 +288,7 @@ export default {
v-for="user in users" v-for="user in users"
v-else v-else
:key="user.id" :key="user.id"
@click="$emit('updateMapping', data.item.jiraAccountId, user.id, user.username)" @click="updateMapping(data.item.jiraAccountId, user.id, user.username)"
> >
{{ user.username }} ({{ user.name }}) {{ user.username }} ({{ user.name }})
</gl-new-dropdown-item> </gl-new-dropdown-item>
......
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue'; import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import JiraImportApp from '~/jira_import/components/jira_import_app.vue'; import JiraImportApp from '~/jira_import/components/jira_import_app.vue';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue'; import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue'; import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue';
import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue'; import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue';
import initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql'; import {
import getJiraUserMappingMutation from '~/jira_import/queries/get_jira_user_mapping.mutation.graphql'; imports,
import { imports, issuesPath, jiraIntegrationPath, jiraProjects, userMappings } from '../mock_data'; issuesPath,
jiraIntegrationPath,
jiraProjects,
projectId,
projectPath,
} from '../mock_data';
describe('JiraImportApp', () => { describe('JiraImportApp', () => {
let axiosMock;
let mutateSpy;
let wrapper; let wrapper;
const inProgressIllustration = 'in-progress-illustration.svg';
const setupIllustration = 'setup-illustration.svg';
const getFormComponent = () => wrapper.find(JiraImportForm); const getFormComponent = () => wrapper.find(JiraImportForm);
const getProgressComponent = () => wrapper.find(JiraImportProgress); const getProgressComponent = () => wrapper.find(JiraImportProgress);
...@@ -32,22 +37,19 @@ describe('JiraImportApp', () => { ...@@ -32,22 +37,19 @@ describe('JiraImportApp', () => {
showAlert = false, showAlert = false,
isInProgress = false, isInProgress = false,
loading = false, loading = false,
mutate = mutateSpy,
} = {}) => } = {}) =>
shallowMount(JiraImportApp, { shallowMount(JiraImportApp, {
propsData: { propsData: {
inProgressIllustration: 'in-progress-illustration.svg', inProgressIllustration,
isJiraConfigured, isJiraConfigured,
issuesPath, issuesPath,
jiraIntegrationPath, jiraIntegrationPath,
projectId: '5', projectId,
projectPath: 'gitlab-org/gitlab-test', projectPath,
setupIllustration: 'setup-illustration.svg', setupIllustration,
}, },
data() { data() {
return { return {
isSubmitting: false,
userMappings,
errorMessage, errorMessage,
showAlert, showAlert,
jiraImportDetails: { jiraImportDetails: {
...@@ -61,26 +63,11 @@ describe('JiraImportApp', () => { ...@@ -61,26 +63,11 @@ describe('JiraImportApp', () => {
mocks: { mocks: {
$apollo: { $apollo: {
loading, loading,
mutate,
}, },
}, },
}); });
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
mutateSpy = jest.fn(() =>
Promise.resolve({
data: {
jiraImportStart: { errors: [] },
jiraImportUsers: { jiraUsers: [], errors: [] },
},
}),
);
});
afterEach(() => { afterEach(() => {
axiosMock.restore();
mutateSpy.mockRestore();
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
...@@ -173,72 +160,79 @@ describe('JiraImportApp', () => { ...@@ -173,72 +160,79 @@ describe('JiraImportApp', () => {
}); });
}); });
describe('import in progress screen', () => { describe('import setup component', () => {
beforeEach(() => {
wrapper = mountComponent({ isJiraConfigured: false });
});
it('receives the illustration', () => {
expect(getSetupComponent().props('illustration')).toBe(setupIllustration);
});
it('receives the path to the Jira integration page', () => {
expect(getSetupComponent().props('jiraIntegrationPath')).toBe(jiraIntegrationPath);
});
});
describe('import in progress component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent({ isInProgress: true }); wrapper = mountComponent({ isInProgress: true });
}); });
it('shows the illustration', () => { it('receives the illustration', () => {
expect(getProgressComponent().props('illustration')).toBe('in-progress-illustration.svg'); expect(getProgressComponent().props('illustration')).toBe(inProgressIllustration);
}); });
it('shows the name of the most recent import initiator', () => { it('receives the name of the most recent import initiator', () => {
expect(getProgressComponent().props('importInitiator')).toBe('Jane Doe'); expect(getProgressComponent().props('importInitiator')).toBe('Jane Doe');
}); });
it('shows the name of the most recent imported project', () => { it('receives the name of the most recent imported project', () => {
expect(getProgressComponent().props('importProject')).toBe('MTG'); expect(getProgressComponent().props('importProject')).toBe('MTG');
}); });
it('shows the time of the most recent import', () => { it('receives the time of the most recent import', () => {
expect(getProgressComponent().props('importTime')).toBe('2020-04-09T16:17:18+00:00'); expect(getProgressComponent().props('importTime')).toBe('2020-04-09T16:17:18+00:00');
}); });
it('has the path to the issues page', () => { it('receives the path to the issues page', () => {
expect(getProgressComponent().props('issuesPath')).toBe('gitlab-org/gitlab-test/-/issues'); expect(getProgressComponent().props('issuesPath')).toBe('gitlab-org/gitlab-test/-/issues');
}); });
}); });
describe('initiating a Jira import', () => { describe('import form component', () => {
it('calls the mutation with the expected arguments', () => { beforeEach(() => {
wrapper = mountComponent(); wrapper = mountComponent();
});
const mutationArguments = { it('receives the illustration', () => {
mutation: initiateJiraImportMutation, expect(getFormComponent().props('issuesPath')).toBe(issuesPath);
variables: { });
input: {
jiraProjectKey: 'MTG',
projectPath: 'gitlab-org/gitlab-test',
usersMapping: [
{
jiraAccountId: 'aei23f98f-q23fj98qfj',
gitlabId: 15,
},
{
jiraAccountId: 'fu39y8t34w-rq3u289t3h4i',
gitlabId: undefined,
},
],
},
},
};
getFormComponent().vm.$emit('initiateJiraImport', 'MTG');
expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments)); it('receives the name of the most recent import initiator', () => {
expect(getFormComponent().props('jiraImports')).toEqual(imports);
}); });
describe('when there is an error', () => { it('receives the name of the most recent imported project', () => {
beforeEach(() => { expect(getFormComponent().props('jiraProjects')).toEqual(jiraProjects);
const mutate = jest.fn(() => Promise.reject()); });
wrapper = mountComponent({ mutate });
getFormComponent().vm.$emit('initiateJiraImport', 'MTG'); it('receives the project ID', () => {
expect(getFormComponent().props('projectId')).toBe(projectId);
}); });
it('shows alert message with error message', async () => { it('receives the project path', () => {
expect(getAlert().text()).toBe('There was an error importing the Jira project.'); expect(getFormComponent().props('projectPath')).toBe(projectPath);
}); });
it('shows an alert when it emits an error', async () => {
expect(getAlert().exists()).toBe(false);
getFormComponent().vm.$emit('error', 'There was an error');
await Vue.nextTick();
expect(getAlert().exists()).toBe(true);
}); });
}); });
...@@ -259,40 +253,4 @@ describe('JiraImportApp', () => { ...@@ -259,40 +253,4 @@ describe('JiraImportApp', () => {
expect(getAlert().exists()).toBe(false); expect(getAlert().exists()).toBe(false);
}); });
}); });
describe('on mount GraphQL user mapping mutation', () => {
it('is called with the expected arguments', () => {
wrapper = mountComponent();
const mutationArguments = {
mutation: getJiraUserMappingMutation,
variables: {
input: {
projectPath: 'gitlab-org/gitlab-test',
},
},
};
expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
});
describe('when Jira is not configured', () => {
it('is not called', () => {
wrapper = mountComponent({ isJiraConfigured: false });
expect(mutateSpy).not.toHaveBeenCalled();
});
});
describe('when there is an error when called', () => {
beforeEach(() => {
const mutate = jest.fn(() => Promise.reject());
wrapper = mountComponent({ mutate });
});
it('shows error message', () => {
expect(getAlert().exists()).toBe(true);
});
});
});
}); });
...@@ -4,15 +4,20 @@ import { mount, shallowMount } from '@vue/test-utils'; ...@@ -4,15 +4,20 @@ 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 getJiraUserMappingMutation from '~/jira_import/queries/get_jira_user_mapping.mutation.graphql';
import initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql';
import { import {
imports, imports,
issuesPath, issuesPath,
jiraProjects, jiraProjects,
projectId,
projectPath,
userMappings as defaultUserMappings, userMappings as defaultUserMappings,
} from '../mock_data'; } from '../mock_data';
describe('JiraImportForm', () => { describe('JiraImportForm', () => {
let axiosMock; let axiosMock;
let mutateSpy;
let wrapper; let wrapper;
const currentUsername = 'mrgitlab'; const currentUsername = 'mrgitlab';
...@@ -35,35 +40,53 @@ describe('JiraImportForm', () => { ...@@ -35,35 +40,53 @@ describe('JiraImportForm', () => {
const mountComponent = ({ const mountComponent = ({
isSubmitting = false, isSubmitting = false,
loading = false,
mutate = mutateSpy,
selectedProject = 'MTG', selectedProject = 'MTG',
userMappings = defaultUserMappings, userMappings = defaultUserMappings,
mountFunction = shallowMount, mountFunction = shallowMount,
} = {}) => } = {}) =>
mountFunction(JiraImportForm, { mountFunction(JiraImportForm, {
propsData: { propsData: {
isSubmitting,
issuesPath, issuesPath,
jiraImports: imports, jiraImports: imports,
jiraProjects, jiraProjects,
projectId: '5', projectId,
userMappings, projectPath,
}, },
data: () => ({ data: () => ({
isFetching: false, isFetching: false,
isSubmitting,
searchTerm: '', searchTerm: '',
selectedProject, selectedProject,
selectState: null, selectState: null,
users: [], users: [],
userMappings,
}), }),
mocks: {
$apollo: {
loading,
mutate,
},
},
currentUsername, currentUsername,
}); });
beforeEach(() => { beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios); axiosMock = new AxiosMockAdapter(axios);
mutateSpy = jest.fn(() =>
Promise.resolve({
data: {
jiraImportStart: { errors: [] },
jiraImportUsers: { jiraUsers: [], errors: [] },
},
}),
);
}); });
afterEach(() => { afterEach(() => {
axiosMock.restore(); axiosMock.restore();
mutateSpy.mockRestore();
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
...@@ -238,15 +261,61 @@ describe('JiraImportForm', () => { ...@@ -238,15 +261,61 @@ describe('JiraImportForm', () => {
}); });
}); });
describe('form', () => { describe('submitting the form', () => {
it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => { it('initiates the Jira import mutation with the expected arguments', () => {
const selectedProject = 'MTG'; wrapper = mountComponent();
wrapper = mountComponent({ selectedProject }); const mutationArguments = {
mutation: initiateJiraImportMutation,
variables: {
input: {
jiraProjectKey: 'MTG',
projectPath,
usersMapping: [
{
jiraAccountId: 'aei23f98f-q23fj98qfj',
gitlabId: 15,
},
{
jiraAccountId: 'fu39y8t34w-rq3u289t3h4i',
gitlabId: undefined,
},
],
},
},
};
wrapper.find('form').trigger('submit'); wrapper.find('form').trigger('submit');
expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([selectedProject]); expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
});
});
describe('on mount GraphQL user mapping mutation', () => {
it('is called with the expected arguments', () => {
wrapper = mountComponent();
const mutationArguments = {
mutation: getJiraUserMappingMutation,
variables: {
input: {
projectPath,
},
},
};
expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
});
describe('when there is an error when called', () => {
beforeEach(() => {
const mutate = jest.fn(() => Promise.reject());
wrapper = mountComponent({ mutate });
});
it('shows error message', () => {
expect(getAlert().exists()).toBe(true);
});
}); });
}); });
}); });
...@@ -3,6 +3,16 @@ import { IMPORT_STATE } from '~/jira_import/utils/jira_import_utils'; ...@@ -3,6 +3,16 @@ import { IMPORT_STATE } from '~/jira_import/utils/jira_import_utils';
export const fullPath = 'gitlab-org/gitlab-test'; export const fullPath = 'gitlab-org/gitlab-test';
export const issuesPath = 'gitlab-org/gitlab-test/-/issues';
export const illustration = 'illustration.svg';
export const jiraIntegrationPath = 'gitlab-org/gitlab-test/-/services/jira/edit';
export const projectId = '5';
export const projectPath = 'gitlab-org/gitlab-test';
export const queryDetails = { export const queryDetails = {
query: getJiraImportDetailsQuery, query: getJiraImportDetailsQuery,
variables: { variables: {
...@@ -71,12 +81,6 @@ export const jiraImportMutationResponse = { ...@@ -71,12 +81,6 @@ export const jiraImportMutationResponse = {
}, },
}; };
export const issuesPath = 'gitlab-org/gitlab-test/-/issues';
export const jiraIntegrationPath = 'gitlab-org/gitlab-test/-/services/jira/edit';
export const illustration = 'illustration.svg';
export const jiraProjects = [ export const jiraProjects = [
{ text: 'My Jira Project (MJP)', value: 'MJP' }, { text: 'My Jira Project (MJP)', value: 'MJP' },
{ text: 'My Second Jira Project (MSJP)', value: 'MSJP' }, { text: 'My Second Jira Project (MSJP)', value: 'MSJP' },
......
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