Commit 2f7981ad authored by Coung Ngo's avatar Coung Ngo Committed by Kushal Pandya

Add Jira Importer user mapping form

Added user mapping form so users can choose which GitLab users
their Jira users should be mapped to when importing a Jira project.
parent bfadedb5
...@@ -3,6 +3,7 @@ import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; ...@@ -3,6 +3,7 @@ import { GlAlert, GlLoadingIcon, GlSprintf } 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';
import getJiraUserMappingMutation from '../queries/get_jira_user_mapping.mutation.graphql';
import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql'; import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql';
import { addInProgressImportToStore } from '../utils/cache_update'; import { addInProgressImportToStore } from '../utils/cache_update';
import { isInProgress, extractJiraProjectsOptions } from '../utils/jira_import_utils'; import { isInProgress, extractJiraProjectsOptions } from '../utils/jira_import_utils';
...@@ -37,6 +38,10 @@ export default { ...@@ -37,6 +38,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: String,
required: true,
},
projectPath: { projectPath: {
type: String, type: String,
required: true, required: true,
...@@ -48,10 +53,12 @@ export default { ...@@ -48,10 +53,12 @@ export default {
}, },
data() { data() {
return { return {
isSubmitting: false,
jiraImportDetails: {}, jiraImportDetails: {},
selectedProject: undefined,
userMappings: [],
errorMessage: '', errorMessage: '',
showAlert: false, showAlert: false,
selectedProject: undefined,
}; };
}, },
apollo: { apollo: {
...@@ -89,16 +96,43 @@ export default { ...@@ -89,16 +96,43 @@ export default {
: 'jira-import::KEY-1'; : 'jira-import::KEY-1';
}, },
}, },
mounted() {
if (this.isJiraConfigured) {
this.$apollo
.mutate({
mutation: getJiraUserMappingMutation,
variables: {
input: {
projectPath: this.projectPath,
startAt: 1,
},
},
})
.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) { initiateJiraImport(project) {
this.isSubmitting = true;
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: initiateJiraImportMutation, mutation: initiateJiraImportMutation,
variables: { variables: {
input: { input: {
projectPath: this.projectPath,
jiraProjectKey: project, jiraProjectKey: project,
usersMapping: [], projectPath: this.projectPath,
usersMapping: this.userMappings.map(({ gitlabId, jiraAccountId }) => ({
gitlabId,
jiraAccountId,
})),
}, },
}, },
update: (store, { data }) => update: (store, { data }) =>
...@@ -111,7 +145,21 @@ export default { ...@@ -111,7 +145,21 @@ export default {
this.selectedProject = undefined; this.selectedProject = undefined;
} }
}) })
.catch(() => this.setAlertMessage(__('There was an error importing the Jira project.'))); .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;
...@@ -156,9 +204,13 @@ export default { ...@@ -156,9 +204,13 @@ export default {
v-else v-else
v-model="selectedProject" v-model="selectedProject"
:import-label="importLabel" :import-label="importLabel"
:is-submitting="isSubmitting"
:issues-path="issuesPath" :issues-path="issuesPath"
:jira-projects="jiraImportDetails.projects" :jira-projects="jiraImportDetails.projects"
:project-id="projectId"
:user-mappings="userMappings"
@initiateJiraImport="initiateJiraImport" @initiateJiraImport="initiateJiraImport"
@updateMapping="updateMapping"
/> />
</div> </div>
</template> </template>
<script> <script>
import { GlAvatar, GlButton, GlFormGroup, GlFormSelect, GlLabel } from '@gitlab/ui'; import {
GlButton,
GlNewDropdown,
GlNewDropdownItem,
GlNewDropdownText,
GlFormGroup,
GlFormSelect,
GlIcon,
GlLabel,
GlLoadingIcon,
GlSearchBoxByType,
GlTable,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
export default { export default {
name: 'JiraImportForm', name: 'JiraImportForm',
components: { components: {
GlAvatar,
GlButton, GlButton,
GlNewDropdown,
GlNewDropdownItem,
GlNewDropdownText,
GlFormGroup, GlFormGroup,
GlFormSelect, GlFormSelect,
GlIcon,
GlLabel, GlLabel,
GlLoadingIcon,
GlSearchBoxByType,
GlTable,
}, },
currentUserAvatarUrl: gon.current_user_avatar_url,
currentUsername: gon.current_username, 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'),
},
],
props: { props: {
importLabel: { importLabel: {
type: String, type: String,
required: true, required: true,
}, },
isSubmitting: {
type: Boolean,
required: true,
},
issuesPath: { issuesPath: {
type: String, type: String,
required: true, required: true,
...@@ -25,6 +64,14 @@ export default { ...@@ -25,6 +64,14 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
projectId: {
type: String,
required: true,
},
userMappings: {
type: Array,
required: true,
},
value: { value: {
type: String, type: String,
required: false, required: false,
...@@ -33,10 +80,53 @@ export default { ...@@ -33,10 +80,53 @@ export default {
}, },
data() { data() {
return { return {
isFetching: false,
searchTerm: '',
selectState: null, selectState: null,
users: [],
}; };
}, },
computed: {
shouldShowNoMatchesFoundText() {
return !this.isFetching && this.users.length === 0;
},
},
watch: {
searchTerm: debounce(function debouncedUserSearch() {
this.searchUsers();
}, 500),
},
mounted() {
this.searchUsers()
.then(data => {
this.initialUsers = data;
})
.catch(() => {});
},
methods: { methods: {
searchUsers() {
const params = {
active: true,
project_id: this.projectId,
search: this.searchTerm,
};
this.isFetching = true;
return axios
.get('/-/autocomplete/users.json', { params })
.then(({ data }) => {
this.users = data;
return data;
})
.finally(() => {
this.isFetching = false;
});
},
resetDropdown() {
this.searchTerm = '';
this.users = this.initialUsers;
},
initiateJiraImport(event) { initiateJiraImport(event) {
event.preventDefault(); event.preventDefault();
if (this.value) { if (this.value) {
...@@ -80,7 +170,7 @@ export default { ...@@ -80,7 +170,7 @@ export default {
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
class="row align-items-center" class="row gl-align-items-center gl-mb-6"
:label="__('Issue label')" :label="__('Issue label')"
label-cols-sm="2" label-cols-sm="2"
label-for="jira-project-label" label-for="jira-project-label"
...@@ -94,46 +184,54 @@ export default { ...@@ -94,46 +184,54 @@ export default {
/> />
</gl-form-group> </gl-form-group>
<hr /> <h4 class="gl-mb-4">{{ __('Jira-GitLab user mapping template') }}</h4>
<p class="offset-md-1"> <p>
{{ {{
__( __(
"For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:", `Jira users have been matched with similar GitLab users.
This can be overwritten by selecting a GitLab user from the dropdown in the "GitLab
username" column.
If it wasn't possible to match a Jira user with a GitLab user, the dropdown defaults to
the user conducting the import.`,
) )
}} }}
</p> </p>
<gl-form-group <gl-table :fields="$options.tableConfig" :items="userMappings" fixed>
class="row align-items-center mb-1" <template #cell(arrow)>
:label="__('Title')" <gl-icon name="arrow-right" :aria-label="__('Will be mapped to')" />
label-cols-sm="2" </template>
label-for="jira-project-title" <template #cell(gitlabUsername)="data">
> <gl-new-dropdown
<p id="jira-project-title" class="mb-2">{{ __('jira.issue.summary') }}</p> :text="data.value || $options.currentUsername"
</gl-form-group> class="w-100"
<gl-form-group :aria-label="
class="row align-items-center mb-1" sprintf($options.dropdownLabel, { jiraDisplayName: data.item.jiraDisplayName })
:label="__('Reporter')" "
label-cols-sm="2" @hide="resetDropdown"
label-for="jira-project-reporter" >
> <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" />
<gl-avatar
id="jira-project-reporter" <div v-if="isFetching" class="gl-text-center">
class="mb-2" <gl-loading-icon />
:src="$options.currentUserAvatarUrl" </div>
:size="24"
:aria-label="$options.currentUsername" <gl-new-dropdown-item
/> v-for="user in users"
</gl-form-group> v-else
<gl-form-group :key="user.id"
class="row align-items-center mb-1" @click="$emit('updateMapping', data.item.jiraAccountId, user.id, user.username)"
:label="__('Description')" >
label-cols-sm="2" {{ user.username }} ({{ user.name }})
label-for="jira-project-description" </gl-new-dropdown-item>
>
<p id="jira-project-description" class="mb-2">{{ __('jira.issue.description.content') }}</p> <gl-new-dropdown-text v-show="shouldShowNoMatchesFoundText" class="text-secondary">
</gl-form-group> {{ __('No matches found') }}
</gl-new-dropdown-text>
</gl-new-dropdown>
</template>
</gl-table>
<div class="footer-block row-content-block d-flex justify-content-between"> <div class="footer-block row-content-block d-flex justify-content-between">
<gl-button <gl-button
...@@ -141,9 +239,10 @@ export default { ...@@ -141,9 +239,10 @@ export default {
category="primary" category="primary"
variant="success" variant="success"
class="js-no-auto-disable" class="js-no-auto-disable"
:loading="isSubmitting"
data-qa-selector="jira_issues_import_button" data-qa-selector="jira_issues_import_button"
> >
{{ __('Next') }} {{ __('Continue') }}
</gl-button> </gl-button>
<gl-button :href="issuesPath">{{ __('Cancel') }}</gl-button> <gl-button :href="issuesPath">{{ __('Cancel') }}</gl-button>
</div> </div>
......
...@@ -28,6 +28,7 @@ export default function mountJiraImportApp() { ...@@ -28,6 +28,7 @@ export default function mountJiraImportApp() {
isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured), isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
issuesPath: el.dataset.issuesPath, issuesPath: el.dataset.issuesPath,
jiraIntegrationPath: el.dataset.jiraIntegrationPath, jiraIntegrationPath: el.dataset.jiraIntegrationPath,
projectId: el.dataset.projectId,
projectPath: el.dataset.projectPath, projectPath: el.dataset.projectPath,
setupIllustration: el.dataset.setupIllustration, setupIllustration: el.dataset.setupIllustration,
}, },
......
mutation($input: JiraImportUsersInput!) {
jiraImportUsers(input: $input) {
jiraUsers {
jiraAccountId
jiraDisplayName
jiraEmail
gitlabId
}
errors
}
}
...@@ -3,4 +3,5 @@ ...@@ -3,4 +3,5 @@
jira_integration_path: edit_project_service_path(@project, :jira), jira_integration_path: edit_project_service_path(@project, :jira),
is_jira_configured: @project.jira_service&.active? && @project.jira_service&.valid_connection?.to_s, is_jira_configured: @project.jira_service&.active? && @project.jira_service&.valid_connection?.to_s,
in_progress_illustration: image_path('illustrations/export-import.svg'), in_progress_illustration: image_path('illustrations/export-import.svg'),
project_id: @project.id,
setup_illustration: image_path('illustrations/manual_action.svg') } } setup_illustration: image_path('illustrations/manual_action.svg') } }
---
title: Add Jira Importer user mapping form
merge_request: 33320
author:
type: added
...@@ -40,6 +40,8 @@ Make sure you have the integration set up before trying to import Jira issues. ...@@ -40,6 +40,8 @@ Make sure you have the integration set up before trying to import Jira issues.
## Import Jira issues to GitLab ## Import Jira issues to GitLab
> New import form [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216145) in GitLab 13.2.
To import Jira issues to a GitLab project, follow the steps below. To import Jira issues to a GitLab project, follow the steps below.
NOTE: **Note:** NOTE: **Note:**
...@@ -47,27 +49,34 @@ Importing Jira issues is done as an asynchronous background job, which ...@@ -47,27 +49,34 @@ Importing Jira issues is done as an asynchronous background job, which
may result in delays based on import queues load, system load, or other factors. may result in delays based on import queues load, system load, or other factors.
Importing large projects may take several minutes depending on the size of the import. Importing large projects may take several minutes depending on the size of the import.
1. On the **{issues}** **Issues** page, click the **Import Issues** (**{import}**) button. 1. On the **{issues}** **Issues** page, click **Import Issues** (**{import}**) **> Import from Jira**.
1. Select **Import from Jira**.
This option is only visible if you have the [correct permissions](#permissions).
![Import issues from Jira button](img/jira/import_issues_from_jira_button_v12_10.png) ![Import issues from Jira button](img/jira/import_issues_from_jira_button_v12_10.png)
The **Import from Jira** option is only visible if you have the [correct permissions](#permissions).
The following form appears. The following form appears.
If you've previously set up the [Jira integration](../integrations/jira.md), you can now see
the Jira projects that you have access to in the dropdown.
![Import issues from Jira form](img/jira/import_issues_from_jira_form_v13_2.png)
![Import issues from Jira form](img/jira/import_issues_from_jira_form_v12_10.png) 1. Click the **Import from** dropdown and select the Jira project that you wish to import issues from.
If you've previously set up the [Jira integration](../integrations/jira.md), you now see the Jira In the **Jira-GitLab user mapping template** section, the table shows to which GitLab users your Jira
projects that you have access to in the dropdown. users will be mapped.
If it wasn't possible to match a Jira user with a GitLab user, the dropdown defaults to the user
conducting the import.
1. Select the Jira project that you wish to import issues from. 1. To change any of the suggested mappings, click the dropdown in the **GitLab username** column and
select the user you want to map to each Jira user.
![Import issues from Jira form](img/jira/import_issues_from_jira_projects_v12_10.png) The dropdown may not show all the users, so use the search bar to find a specific
user in this GitLab project.
1. Click **Continue**. You're presented with a confirmation that import has started.
1. Click **Import Issues**. You're presented with a confirmation that import has started.
While the import is running in the background, you can navigate away from the import status page While the import is running in the background, you can navigate away from the import status page
to the issues page, and you'll see the new issues appearing in the issues list. to the issues page, and you'll see the new issues appearing in the issues list.
1. To check the status of your import, go back to the Jira import page. 1. To check the status of your import, go to the Jira import page again.
![Import issues from Jira button](img/jira/import_issues_from_jira_button_v12_10.png)
...@@ -10410,9 +10410,6 @@ msgstr "" ...@@ -10410,9 +10410,6 @@ msgstr ""
msgid "Footer message" msgid "Footer message"
msgstr "" msgstr ""
msgid "For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:"
msgstr ""
msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)" msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr "" msgstr ""
...@@ -11082,6 +11079,9 @@ msgstr "" ...@@ -11082,6 +11079,9 @@ msgstr ""
msgid "GitLab single sign-on URL" msgid "GitLab single sign-on URL"
msgstr "" msgstr ""
msgid "GitLab username"
msgstr ""
msgid "GitLab uses %{jaeger_link} to monitor distributed systems." msgid "GitLab uses %{jaeger_link} to monitor distributed systems."
msgstr "" msgstr ""
...@@ -13065,6 +13065,9 @@ msgstr "" ...@@ -13065,6 +13065,9 @@ msgstr ""
msgid "Jira Issues" msgid "Jira Issues"
msgstr "" msgstr ""
msgid "Jira display name"
msgstr ""
msgid "Jira import is already running." msgid "Jira import is already running."
msgstr "" msgstr ""
...@@ -13080,6 +13083,12 @@ msgstr "" ...@@ -13080,6 +13083,12 @@ msgstr ""
msgid "Jira service not configured." msgid "Jira service not configured."
msgstr "" msgstr ""
msgid "Jira users have been matched with similar GitLab users. This can be overwritten by selecting a GitLab user from the dropdown in the \"GitLab username\" column. If it wasn't possible to match a Jira user with a GitLab user, the dropdown defaults to the user conducting the import."
msgstr ""
msgid "Jira-GitLab user mapping template"
msgstr ""
msgid "JiraService| on branch %{branch_link}" msgid "JiraService| on branch %{branch_link}"
msgstr "" msgstr ""
...@@ -19630,9 +19639,6 @@ msgstr "" ...@@ -19630,9 +19639,6 @@ msgstr ""
msgid "Reported %{timeAgo} by %{reportedBy}" msgid "Reported %{timeAgo} by %{reportedBy}"
msgstr "" msgstr ""
msgid "Reporter"
msgstr ""
msgid "Reporting" msgid "Reporting"
msgstr "" msgstr ""
...@@ -23138,6 +23144,9 @@ msgstr "" ...@@ -23138,6 +23144,9 @@ msgstr ""
msgid "The Git LFS objects will <strong>not</strong> be synced." msgid "The Git LFS objects will <strong>not</strong> be synced."
msgstr "" msgstr ""
msgid "The GitLab user to which the Jira user %{jiraDisplayName} will be mapped"
msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project" msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr "" msgstr ""
...@@ -23671,6 +23680,9 @@ msgstr "" ...@@ -23671,6 +23680,9 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes." msgid "There was an error resetting user pipeline minutes."
msgstr "" msgstr ""
msgid "There was an error retrieving the Jira users."
msgstr ""
msgid "There was an error saving this Geo Node." msgid "There was an error saving this Geo Node."
msgstr "" msgstr ""
...@@ -26594,6 +26606,9 @@ msgstr "" ...@@ -26594,6 +26606,9 @@ msgstr ""
msgid "Will be created" msgid "Will be created"
msgstr "" msgstr ""
msgid "Will be mapped to"
msgstr ""
msgid "Will deploy to" msgid "Will deploy to"
msgstr "" msgstr ""
...@@ -27959,12 +27974,6 @@ msgstr "" ...@@ -27959,12 +27974,6 @@ msgstr ""
msgid "jigsaw is not defined" msgid "jigsaw is not defined"
msgstr "" msgstr ""
msgid "jira.issue.description.content"
msgstr ""
msgid "jira.issue.summary"
msgstr ""
msgid "latest" msgid "latest"
msgstr "" msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JiraImportForm table body shows correct information in each cell 1`] = `
<table
aria-busy="false"
aria-colcount="3"
class="table b-table gl-table b-table-fixed"
role="table"
>
<!---->
<!---->
<thead
class=""
role="rowgroup"
>
<!---->
<tr
class=""
role="row"
>
<th
aria-colindex="1"
class=""
role="columnheader"
scope="col"
>
Jira display name
</th>
<th
aria-colindex="2"
aria-label="Arrow"
class=""
role="columnheader"
scope="col"
/>
<th
aria-colindex="3"
class=""
role="columnheader"
scope="col"
>
GitLab username
</th>
</tr>
</thead>
<tbody
role="rowgroup"
>
<!---->
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
Jane Doe
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
<svg
aria-label="Will be mapped to"
class="gl-icon s16"
>
<use
href="#arrow-right"
/>
</svg>
</td>
<td
aria-colindex="3"
class=""
role="cell"
>
<div
aria-label="The GitLab user to which the Jira user Jane Doe will be mapped"
class="dropdown b-dropdown gl-new-dropdown w-100 btn-group"
>
<!---->
<button
aria-expanded="false"
aria-haspopup="true"
class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
type="button"
>
<!---->
<span
class="gl-new-dropdown-button-text"
>
janedoe
</span>
<svg
class="dropdown-chevron gl-icon s16"
>
<use
href="#chevron-down"
/>
</svg>
</button>
<ul
class="dropdown-menu"
role="menu"
tabindex="-1"
>
<!---->
<div
class="gl-search-box-by-type m-2"
>
<svg
class="gl-search-box-by-type-search-icon gl-icon s16"
>
<use
href="#search"
/>
</svg>
<input
aria-label="Search"
class="gl-form-input gl-search-box-by-type-input form-control"
placeholder="Search"
type="text"
/>
<div
class="gl-search-box-by-type-right-icons"
>
<!---->
<button
aria-hidden="true"
class="gl-clear-icon-button gl-search-box-by-type-clear gl-clear-icon-button"
name="clear"
style="display: none;"
title="Clear"
>
<svg
class="gl-icon s16"
>
<use
href="#clear"
/>
</svg>
</button>
</div>
</div>
<li
class="gl-new-dropdown-text text-secondary"
role="presentation"
>
<p
class="b-dropdown-text"
>
No matches found
</p>
</li>
</ul>
</div>
</td>
</tr>
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
Fred Chopin
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
<svg
aria-label="Will be mapped to"
class="gl-icon s16"
>
<use
href="#arrow-right"
/>
</svg>
</td>
<td
aria-colindex="3"
class=""
role="cell"
>
<div
aria-label="The GitLab user to which the Jira user Fred Chopin will be mapped"
class="dropdown b-dropdown gl-new-dropdown w-100 btn-group"
>
<!---->
<button
aria-expanded="false"
aria-haspopup="true"
class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
type="button"
>
<!---->
<span
class="gl-new-dropdown-button-text"
>
mrgitlab
</span>
<svg
class="dropdown-chevron gl-icon s16"
>
<use
href="#chevron-down"
/>
</svg>
</button>
<ul
class="dropdown-menu"
role="menu"
tabindex="-1"
>
<!---->
<div
class="gl-search-box-by-type m-2"
>
<svg
class="gl-search-box-by-type-search-icon gl-icon s16"
>
<use
href="#search"
/>
</svg>
<input
aria-label="Search"
class="gl-form-input gl-search-box-by-type-input form-control"
placeholder="Search"
type="text"
/>
<div
class="gl-search-box-by-type-right-icons"
>
<!---->
<button
aria-hidden="true"
class="gl-clear-icon-button gl-search-box-by-type-clear gl-clear-icon-button"
name="clear"
style="display: none;"
title="Clear"
>
<svg
class="gl-icon s16"
>
<use
href="#clear"
/>
</svg>
</button>
</div>
</div>
<li
class="gl-new-dropdown-text text-secondary"
role="presentation"
>
<p
class="b-dropdown-text"
>
No matches found
</p>
</li>
</ul>
</div>
</td>
</tr>
<!---->
<!---->
</tbody>
<!---->
</table>
`;
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, 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 initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql';
import getJiraUserMappingMutation from '~/jira_import/queries/get_jira_user_mapping.mutation.graphql';
const mountComponent = ({ import { imports, issuesPath, jiraIntegrationPath, jiraProjects, userMappings } from '../mock_data';
isJiraConfigured = true,
errorMessage = '',
selectedProject = 'MTG',
showAlert = false,
isInProgress = false,
loading = false,
mutate = jest.fn(() => Promise.resolve()),
mountType,
} = {}) => {
const mountFunction = mountType === 'mount' ? mount : shallowMount;
return mountFunction(JiraImportApp, {
propsData: {
inProgressIllustration: 'in-progress-illustration.svg',
isJiraConfigured,
issuesPath: 'gitlab-org/gitlab-test/-/issues',
jiraIntegrationPath: 'gitlab-org/gitlab-test/-/services/jira/edit',
projectPath: 'gitlab-org/gitlab-test',
setupIllustration: 'setup-illustration.svg',
},
data() {
return {
errorMessage,
showAlert,
selectedProject,
jiraImportDetails: {
isInProgress,
imports: [
{
jiraProjectKey: 'MTG',
scheduledAt: '2020-04-08T10:11:12+00:00',
scheduledBy: {
name: 'John Doe',
},
},
{
jiraProjectKey: 'MSJP',
scheduledAt: '2020-04-09T13:14:15+00:00',
scheduledBy: {
name: 'Jimmy Doe',
},
},
{
jiraProjectKey: 'MTG',
scheduledAt: '2020-04-09T16:17:18+00:00',
scheduledBy: {
name: 'Jane Doe',
},
},
],
mostRecentImport: {
jiraProjectKey: 'MTG',
scheduledAt: '2020-04-09T16:17:18+00:00',
scheduledBy: {
name: 'Jane Doe',
},
},
projects: [
{ text: 'My Jira Project (MJP)', value: 'MJP' },
{ text: 'My Second Jira Project (MSJP)', value: 'MSJP' },
{ text: 'Migrate to GitLab (MTG)', value: 'MTG' },
],
},
};
},
mocks: {
$apollo: {
loading,
mutate,
},
},
});
};
describe('JiraImportApp', () => { describe('JiraImportApp', () => {
let axiosMock;
let mutateSpy;
let wrapper; let wrapper;
const getFormComponent = () => wrapper.find(JiraImportForm); const getFormComponent = () => wrapper.find(JiraImportForm);
...@@ -95,7 +26,64 @@ describe('JiraImportApp', () => { ...@@ -95,7 +26,64 @@ describe('JiraImportApp', () => {
const getLoadingIcon = () => wrapper.find(GlLoadingIcon); const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
const mountComponent = ({
isJiraConfigured = true,
errorMessage = '',
selectedProject = 'MTG',
showAlert = false,
isInProgress = false,
loading = false,
mutate = mutateSpy,
mountFunction = shallowMount,
} = {}) =>
mountFunction(JiraImportApp, {
propsData: {
inProgressIllustration: 'in-progress-illustration.svg',
isJiraConfigured,
issuesPath,
jiraIntegrationPath,
projectId: '5',
projectPath: 'gitlab-org/gitlab-test',
setupIllustration: 'setup-illustration.svg',
},
data() {
return {
isSubmitting: false,
selectedProject,
userMappings,
errorMessage,
showAlert,
jiraImportDetails: {
isInProgress,
imports,
mostRecentImport: imports[imports.length - 1],
projects: jiraProjects,
},
};
},
mocks: {
$apollo: {
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;
}); });
...@@ -223,7 +211,7 @@ describe('JiraImportApp', () => { ...@@ -223,7 +211,7 @@ describe('JiraImportApp', () => {
}); });
it('shows warning alert to explain project MTG has been imported 2 times before', () => { it('shows warning alert to explain project MTG has been imported 2 times before', () => {
wrapper = mountComponent({ mountType: 'mount' }); wrapper = mountComponent({ mountFunction: mount });
expect(getAlert().text()).toBe( expect(getAlert().text()).toBe(
'You have imported from this project 2 times before. Each new import will create duplicate issues.', 'You have imported from this project 2 times before. Each new import will create duplicate issues.',
...@@ -248,9 +236,7 @@ describe('JiraImportApp', () => { ...@@ -248,9 +236,7 @@ describe('JiraImportApp', () => {
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', () => {
const mutate = jest.fn(() => Promise.resolve()); wrapper = mountComponent();
wrapper = mountComponent({ mutate });
const mutationArguments = { const mutationArguments = {
mutation: initiateJiraImportMutation, mutation: initiateJiraImportMutation,
...@@ -258,14 +244,23 @@ describe('JiraImportApp', () => { ...@@ -258,14 +244,23 @@ describe('JiraImportApp', () => {
input: { input: {
jiraProjectKey: 'MTG', jiraProjectKey: 'MTG',
projectPath: 'gitlab-org/gitlab-test', projectPath: 'gitlab-org/gitlab-test',
usersMapping: [], usersMapping: [
{
jiraAccountId: 'aei23f98f-q23fj98qfj',
gitlabId: 15,
},
{
jiraAccountId: 'fu39y8t34w-rq3u289t3h4i',
gitlabId: undefined,
},
],
}, },
}, },
}; };
getFormComponent().vm.$emit('initiateJiraImport', 'MTG'); getFormComponent().vm.$emit('initiateJiraImport', 'MTG');
expect(mutate).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', () => {
...@@ -284,19 +279,53 @@ describe('JiraImportApp', () => { ...@@ -284,19 +279,53 @@ describe('JiraImportApp', () => {
}); });
}); });
it('can dismiss alert message', () => { describe('alert', () => {
wrapper = mountComponent({ it('can be dismissed', () => {
errorMessage: 'There was an error importing the Jira project.', wrapper = mountComponent({
showAlert: true, errorMessage: 'There was an error importing the Jira project.',
selectedProject: null, showAlert: true,
selectedProject: null,
});
expect(getAlert().exists()).toBe(true);
getAlert().vm.$emit('dismiss');
return Vue.nextTick().then(() => {
expect(getAlert().exists()).toBe(false);
});
}); });
});
expect(getAlert().exists()).toBe(true); describe('on mount', () => {
it('makes a GraphQL mutation call to get user mappings', () => {
wrapper = mountComponent();
getAlert().vm.$emit('dismiss'); const mutationArguments = {
mutation: getJiraUserMappingMutation,
variables: {
input: {
projectPath: 'gitlab-org/gitlab-test',
startAt: 1,
},
},
};
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 });
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 });
return Vue.nextTick().then(() => { expect(getAlert().exists()).toBe(true);
expect(getAlert().exists()).toBe(false);
}); });
}); });
}); });
import { GlAvatar, GlButton, GlFormSelect, GlLabel } from '@gitlab/ui'; import { GlButton, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
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 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 } from '../mock_data';
const importLabel = 'jira-import::MTG-1';
const value = 'MTG';
const mountComponent = ({ mountType } = {}) => {
const mountFunction = mountType === 'mount' ? mount : shallowMount;
return mountFunction(JiraImportForm, {
propsData: {
importLabel,
issuesPath: 'gitlab-org/gitlab-test/-/issues',
jiraProjects: [
{
text: 'My Jira Project',
value: 'MJP',
},
{
text: 'My Second Jira Project',
value: 'MSJP',
},
{
text: 'Migrate to GitLab',
value: 'MTG',
},
],
value,
},
});
};
describe('JiraImportForm', () => { describe('JiraImportForm', () => {
let axiosMock;
let wrapper; let wrapper;
const currentUsername = 'mrgitlab';
const importLabel = 'jira-import::MTG-1';
const value = 'MTG';
const getSelectDropdown = () => wrapper.find(GlFormSelect); const getSelectDropdown = () => wrapper.find(GlFormSelect);
const getCancelButton = () => wrapper.findAll(GlButton).at(1); const getCancelButton = () => wrapper.findAll(GlButton).at(1);
const getHeader = name => getByRole(wrapper.element, 'columnheader', { name });
const mountComponent = ({ isSubmitting = false, mountFunction = shallowMount } = {}) =>
mountFunction(JiraImportForm, {
propsData: {
importLabel,
isSubmitting,
issuesPath,
jiraProjects,
projectId: '5',
userMappings,
value,
},
data: () => ({
isFetching: false,
searchTerm: '',
selectState: null,
users: [],
}),
currentUsername,
});
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => { afterEach(() => {
axiosMock.restore();
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
...@@ -51,16 +58,22 @@ describe('JiraImportForm', () => { ...@@ -51,16 +58,22 @@ describe('JiraImportForm', () => {
}); });
it('contains a list of Jira projects to select from', () => { it('contains a list of Jira projects to select from', () => {
wrapper = mountComponent({ mountType: 'mount' }); wrapper = mountComponent({ mountFunction: mount });
const optionItems = ['My Jira Project', 'My Second Jira Project', 'Migrate to GitLab'];
getSelectDropdown() getSelectDropdown()
.findAll('option') .findAll('option')
.wrappers.forEach((optionEl, index) => { .wrappers.forEach((optionEl, index) => {
expect(optionEl.text()).toBe(optionItems[index]); expect(optionEl.text()).toBe(jiraProjects[index].text);
}); });
}); });
it('emits an "input" event when the input select value changes', () => {
wrapper = mountComponent();
getSelectDropdown().vm.$emit('change', value);
expect(wrapper.emitted('input')[0]).toEqual([value]);
});
}); });
describe('form information', () => { describe('form information', () => {
...@@ -72,64 +85,90 @@ describe('JiraImportForm', () => { ...@@ -72,64 +85,90 @@ describe('JiraImportForm', () => {
expect(wrapper.find(GlLabel).props('title')).toBe(importLabel); 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' }),
).toBeTruthy();
});
it('shows information to the user', () => { it('shows information to the user', () => {
expect(wrapper.find('p').text()).toBe( expect(wrapper.find('p').text()).toBe(
"For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:", 'Jira users have been matched with similar GitLab users. This can be overwritten by selecting a GitLab user from the dropdown in the "GitLab username" column. If it wasn\'t possible to match a Jira user with a GitLab user, the dropdown defaults to the user conducting the import.',
); );
}); });
});
it('shows jira.issue.summary for the Title', () => { describe('table', () => {
expect(wrapper.find('[id="jira-project-title"]').text()).toBe('jira.issue.summary'); describe('headers', () => {
}); beforeEach(() => {
wrapper = mountComponent({ mountFunction: mount });
});
it('shows an avatar for the Reporter', () => { it('has a "Jira display name" column', () => {
expect(wrapper.contains(GlAvatar)).toBe(true); expect(getHeader('Jira display name')).toBeTruthy();
}); });
it('shows jira.issue.description.content for the Description', () => { it('has an "arrow" column', () => {
expect(wrapper.find('[id="jira-project-description"]').text()).toBe( expect(getHeader('Arrow')).toBeTruthy();
'jira.issue.description.content', });
);
});
});
describe('Next button', () => { it('has a "GitLab username" column', () => {
beforeEach(() => { expect(getHeader('GitLab username')).toBeTruthy();
wrapper = mountComponent(); });
}); });
it('is shown', () => { describe('body', () => {
expect(wrapper.find(GlButton).text()).toBe('Next'); it('shows all user mappings', () => {
wrapper = mountComponent({ mountFunction: mount });
expect(wrapper.find(GlTable).findAll('tbody tr').length).toBe(userMappings.length);
});
it('shows correct information in each cell', () => {
wrapper = mountComponent({ mountFunction: mount });
expect(wrapper.find(GlTable).element).toMatchSnapshot();
});
}); });
}); });
describe('Cancel button', () => { describe('buttons', () => {
beforeEach(() => { describe('"Continue" button', () => {
wrapper = mountComponent(); it('is shown', () => {
}); wrapper = mountComponent();
it('is shown', () => { expect(wrapper.find(GlButton).text()).toBe('Continue');
expect(getCancelButton().text()).toBe('Cancel'); });
});
it('is in loading state when the form is submitting', async () => {
wrapper = mountComponent({ isSubmitting: true });
it('links to the Issues page', () => { expect(wrapper.find(GlButton).props('loading')).toBe(true);
expect(getCancelButton().attributes('href')).toBe('gitlab-org/gitlab-test/-/issues'); });
}); });
});
it('emits an "input" event when the input select value changes', () => { describe('"Cancel" button', () => {
wrapper = mountComponent({ mountType: 'mount' }); beforeEach(() => {
wrapper = mountComponent();
});
getSelectDropdown().vm.$emit('change', value); it('is shown', () => {
expect(getCancelButton().text()).toBe('Cancel');
});
expect(wrapper.emitted('input')[0]).toEqual([value]); it('links to the Issues page', () => {
expect(getCancelButton().attributes('href')).toBe(issuesPath);
});
});
}); });
it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => { describe('form', () => {
wrapper = mountComponent(); it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => {
wrapper = mountComponent();
wrapper.find('form').trigger('submit'); wrapper.find('form').trigger('submit');
expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([value]); expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([value]);
});
}); });
}); });
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue'; import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue';
import { illustration, issuesPath } from '../mock_data';
const illustration = 'illustration.svg';
const importProject = 'JIRAPROJECT';
const issuesPath = 'gitlab-org/gitlab-test/-/issues';
describe('JiraImportProgress', () => { describe('JiraImportProgress', () => {
let wrapper; let wrapper;
const importProject = 'JIRAPROJECT';
const getGlEmptyStateProp = attribute => wrapper.find(GlEmptyState).props(attribute); const getGlEmptyStateProp = attribute => wrapper.find(GlEmptyState).props(attribute);
const getParagraphText = () => wrapper.find('p').text(); const getParagraphText = () => wrapper.find('p').text();
......
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue'; import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue';
import { illustration, jiraIntegrationPath } from '../mock_data';
const illustration = 'illustration.svg';
const jiraIntegrationPath = 'gitlab-org/gitlab-test/-/services/jira/edit';
describe('JiraImportSetup', () => { describe('JiraImportSetup', () => {
let wrapper; let wrapper;
......
...@@ -70,3 +70,56 @@ export const jiraImportMutationResponse = { ...@@ -70,3 +70,56 @@ export const jiraImportMutationResponse = {
__typename: 'JiraImportStartPayload', __typename: 'JiraImportStartPayload',
}, },
}; };
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' },
{ text: 'Migrate to GitLab (MTG)', value: 'MTG' },
];
export const imports = [
{
jiraProjectKey: 'MTG',
scheduledAt: '2020-04-08T10:11:12+00:00',
scheduledBy: {
name: 'John Doe',
},
},
{
jiraProjectKey: 'MSJP',
scheduledAt: '2020-04-09T13:14:15+00:00',
scheduledBy: {
name: 'Jimmy Doe',
},
},
{
jiraProjectKey: 'MTG',
scheduledAt: '2020-04-09T16:17:18+00:00',
scheduledBy: {
name: 'Jane Doe',
},
},
];
export const userMappings = [
{
jiraAccountId: 'aei23f98f-q23fj98qfj',
jiraDisplayName: 'Jane Doe',
jiraEmail: 'janedoe@example.com',
gitlabId: 15,
gitlabUsername: 'janedoe',
},
{
jiraAccountId: 'fu39y8t34w-rq3u289t3h4i',
jiraDisplayName: 'Fred Chopin',
jiraEmail: 'fredchopin@example.com',
gitlabId: undefined,
gitlabUsername: undefined,
},
];
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