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>
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { last } from 'lodash';
import { __ } from '~/locale';
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 JiraImportForm from './jira_import_form.vue';
import JiraImportProgress from './jira_import_progress.vue';
......@@ -52,9 +48,7 @@ export default {
},
data() {
return {
isSubmitting: false,
jiraImportDetails: {},
userMappings: [],
errorMessage: '',
showAlert: false,
};
......@@ -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: {
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) {
this.errorMessage = message;
this.showAlert = true;
......@@ -175,14 +106,12 @@ export default {
/>
<jira-import-form
v-else
:is-submitting="isSubmitting"
:issues-path="issuesPath"
:jira-imports="jiraImportDetails.imports"
:jira-projects="jiraImportDetails.projects"
:project-id="projectId"
:user-mappings="userMappings"
@initiateJiraImport="initiateJiraImport"
@updateMapping="updateMapping"
:project-path="projectPath"
@error="setAlertMessage"
/>
</div>
</template>
......@@ -16,6 +16,10 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
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 {
debounceWait,
dropdownLabel,
......@@ -47,10 +51,6 @@ export default {
tableConfig,
userMappingMessage,
props: {
isSubmitting: {
type: Boolean,
required: true,
},
issuesPath: {
type: String,
required: true,
......@@ -67,17 +67,19 @@ export default {
type: String,
required: true,
},
userMappings: {
type: Array,
projectPath: {
type: String,
required: true,
},
},
data() {
return {
isFetching: false,
isSubmitting: false,
searchTerm: '',
selectedProject: undefined,
selectState: null,
userMappings: [],
users: [],
};
},
......@@ -106,6 +108,24 @@ export default {
}, debounceWait),
},
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()
.then(data => {
this.initialUsers = data;
......@@ -138,13 +158,54 @@ export default {
},
initiateJiraImport(event) {
event.preventDefault();
if (this.selectedProject) {
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 {
this.showValidationError();
}
},
updateMapping(jiraAccountId, gitlabId, gitlabUsername) {
this.userMappings = this.userMappings.map(userMapping =>
userMapping.jiraAccountId === jiraAccountId
? {
...userMapping,
gitlabId,
gitlabUsername,
}
: userMapping,
);
},
hideValidationError() {
this.selectState = null;
},
......@@ -227,7 +288,7 @@ export default {
v-for="user in users"
v-else
: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 }})
</gl-new-dropdown-item>
......
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import JiraImportApp from '~/jira_import/components/jira_import_app.vue';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue';
import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue';
import initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql';
import getJiraUserMappingMutation from '~/jira_import/queries/get_jira_user_mapping.mutation.graphql';
import { imports, issuesPath, jiraIntegrationPath, jiraProjects, userMappings } from '../mock_data';
import {
imports,
issuesPath,
jiraIntegrationPath,
jiraProjects,
projectId,
projectPath,
} from '../mock_data';
describe('JiraImportApp', () => {
let axiosMock;
let mutateSpy;
let wrapper;
const inProgressIllustration = 'in-progress-illustration.svg';
const setupIllustration = 'setup-illustration.svg';
const getFormComponent = () => wrapper.find(JiraImportForm);
const getProgressComponent = () => wrapper.find(JiraImportProgress);
......@@ -32,22 +37,19 @@ describe('JiraImportApp', () => {
showAlert = false,
isInProgress = false,
loading = false,
mutate = mutateSpy,
} = {}) =>
shallowMount(JiraImportApp, {
propsData: {
inProgressIllustration: 'in-progress-illustration.svg',
inProgressIllustration,
isJiraConfigured,
issuesPath,
jiraIntegrationPath,
projectId: '5',
projectPath: 'gitlab-org/gitlab-test',
setupIllustration: 'setup-illustration.svg',
projectId,
projectPath,
setupIllustration,
},
data() {
return {
isSubmitting: false,
userMappings,
errorMessage,
showAlert,
jiraImportDetails: {
......@@ -61,26 +63,11 @@ describe('JiraImportApp', () => {
mocks: {
$apollo: {
loading,
mutate,
},
},
});
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
mutateSpy = jest.fn(() =>
Promise.resolve({
data: {
jiraImportStart: { errors: [] },
jiraImportUsers: { jiraUsers: [], errors: [] },
},
}),
);
});
afterEach(() => {
axiosMock.restore();
mutateSpy.mockRestore();
wrapper.destroy();
wrapper = null;
});
......@@ -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(() => {
wrapper = mountComponent({ isInProgress: true });
});
it('shows the illustration', () => {
expect(getProgressComponent().props('illustration')).toBe('in-progress-illustration.svg');
it('receives the illustration', () => {
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');
});
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');
});
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');
});
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');
});
});
describe('initiating a Jira import', () => {
it('calls the mutation with the expected arguments', () => {
describe('import form component', () => {
beforeEach(() => {
wrapper = mountComponent();
});
const mutationArguments = {
mutation: initiateJiraImportMutation,
variables: {
input: {
jiraProjectKey: 'MTG',
projectPath: 'gitlab-org/gitlab-test',
usersMapping: [
{
jiraAccountId: 'aei23f98f-q23fj98qfj',
gitlabId: 15,
},
{
jiraAccountId: 'fu39y8t34w-rq3u289t3h4i',
gitlabId: undefined,
},
],
},
},
};
it('receives the illustration', () => {
expect(getFormComponent().props('issuesPath')).toBe(issuesPath);
});
getFormComponent().vm.$emit('initiateJiraImport', 'MTG');
it('receives the name of the most recent import initiator', () => {
expect(getFormComponent().props('jiraImports')).toEqual(imports);
});
expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
it('receives the name of the most recent imported project', () => {
expect(getFormComponent().props('jiraProjects')).toEqual(jiraProjects);
});
describe('when there is an error', () => {
beforeEach(() => {
const mutate = jest.fn(() => Promise.reject());
wrapper = mountComponent({ mutate });
it('receives the project ID', () => {
expect(getFormComponent().props('projectId')).toBe(projectId);
});
getFormComponent().vm.$emit('initiateJiraImport', 'MTG');
});
it('receives the project path', () => {
expect(getFormComponent().props('projectPath')).toBe(projectPath);
});
it('shows alert message with error message', async () => {
expect(getAlert().text()).toBe('There was an error importing the Jira project.');
});
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', () => {
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';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
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 {
imports,
issuesPath,
jiraProjects,
projectId,
projectPath,
userMappings as defaultUserMappings,
} from '../mock_data';
describe('JiraImportForm', () => {
let axiosMock;
let mutateSpy;
let wrapper;
const currentUsername = 'mrgitlab';
......@@ -35,35 +40,53 @@ describe('JiraImportForm', () => {
const mountComponent = ({
isSubmitting = false,
loading = false,
mutate = mutateSpy,
selectedProject = 'MTG',
userMappings = defaultUserMappings,
mountFunction = shallowMount,
} = {}) =>
mountFunction(JiraImportForm, {
propsData: {
isSubmitting,
issuesPath,
jiraImports: imports,
jiraProjects,
projectId: '5',
userMappings,
projectId,
projectPath,
},
data: () => ({
isFetching: false,
isSubmitting,
searchTerm: '',
selectedProject,
selectState: null,
users: [],
userMappings,
}),
mocks: {
$apollo: {
loading,
mutate,
},
},
currentUsername,
});
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
mutateSpy = jest.fn(() =>
Promise.resolve({
data: {
jiraImportStart: { errors: [] },
jiraImportUsers: { jiraUsers: [], errors: [] },
},
}),
);
});
afterEach(() => {
axiosMock.restore();
mutateSpy.mockRestore();
wrapper.destroy();
wrapper = null;
});
......@@ -238,15 +261,61 @@ describe('JiraImportForm', () => {
});
});
describe('form', () => {
it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => {
const selectedProject = 'MTG';
describe('submitting the form', () => {
it('initiates the Jira import mutation with the expected arguments', () => {
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');
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';
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 = {
query: getJiraImportDetailsQuery,
variables: {
......@@ -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 = [
{ text: 'My Jira Project (MJP)', value: 'MJP' },
{ 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