Commit ec557442 authored by Coung Ngo's avatar Coung Ngo Committed by Natalia Tepluhina

Add alert on issues page to show Jira import has finished

Show Jira import has finished to give the user indication
of the state of the import
parent 578c7df7
<script> <script>
import { GlAlert } from '@gitlab/ui'; import { GlAlert, GlLabel } from '@gitlab/ui';
import getJiraImportDetailsQuery from '~/jira_import/queries/get_jira_import_details.query.graphql'; import getIssuesListDetailsQuery from '../queries/get_issues_list_details.query.graphql';
import { isInProgress } from '~/jira_import/utils'; import { calculateJiraImportLabel, isFinished, isInProgress } from '~/jira_import/utils';
export default { export default {
name: 'IssuableListRoot', name: 'IssuableListRoot',
components: { components: {
GlAlert, GlAlert,
GlLabel,
}, },
props: { props: {
canEdit: { canEdit: {
...@@ -17,6 +18,10 @@ export default { ...@@ -17,6 +18,10 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
issuesPath: {
type: String,
required: true,
},
projectPath: { projectPath: {
type: String, type: String,
required: true, required: true,
...@@ -24,12 +29,14 @@ export default { ...@@ -24,12 +29,14 @@ export default {
}, },
data() { data() {
return { return {
isAlertShowing: true, isFinishedAlertShowing: true,
isInProgressAlertShowing: true,
jiraImport: {},
}; };
}, },
apollo: { apollo: {
jiraImport: { jiraImport: {
query: getJiraImportDetailsQuery, query: getIssuesListDetailsQuery,
variables() { variables() {
return { return {
fullPath: this.projectPath, fullPath: this.projectPath,
...@@ -37,6 +44,11 @@ export default { ...@@ -37,6 +44,11 @@ export default {
}, },
update: ({ project }) => ({ update: ({ project }) => ({
isInProgress: isInProgress(project.jiraImportStatus), isInProgress: isInProgress(project.jiraImportStatus),
isFinished: isFinished(project.jiraImportStatus),
label: calculateJiraImportLabel(
project.jiraImports.nodes,
project.issues.nodes.flatMap(({ labels }) => labels.nodes),
),
}), }),
skip() { skip() {
return !this.isJiraConfigured || !this.canEdit; return !this.isJiraConfigured || !this.canEdit;
...@@ -44,20 +56,41 @@ export default { ...@@ -44,20 +56,41 @@ export default {
}, },
}, },
computed: { computed: {
shouldShowAlert() { labelTarget() {
return this.isAlertShowing && this.jiraImport?.isInProgress; return `${this.issuesPath}?label_name[]=${encodeURIComponent(this.jiraImport.label.title)}`;
},
shouldShowFinishedAlert() {
return this.isFinishedAlertShowing && this.jiraImport.isFinished;
},
shouldShowInProgressAlert() {
return this.isInProgressAlertShowing && this.jiraImport.isInProgress;
}, },
}, },
methods: { methods: {
hideAlert() { hideFinishedAlert() {
this.isAlertShowing = false; this.isFinishedAlertShowing = false;
},
hideInProgressAlert() {
this.isInProgressAlertShowing = false;
}, },
}, },
}; };
</script> </script>
<template> <template>
<gl-alert v-if="shouldShowAlert" @dismiss="hideAlert"> <div class="issuable-list-root">
{{ __('Import in progress. Refresh page to see newly added issues.') }} <gl-alert v-if="shouldShowInProgressAlert" @dismiss="hideInProgressAlert">
</gl-alert> {{ __('Import in progress. Refresh page to see newly added issues.') }}
</gl-alert>
<gl-alert v-if="shouldShowFinishedAlert" variant="success" @dismiss="hideFinishedAlert">
{{ __('Issues successfully imported with the label') }}
<gl-label
:background-color="jiraImport.label.color"
scoped
size="sm"
:target="labelTarget"
:title="jiraImport.label.title"
/>
</gl-alert>
</div>
</template> </template>
...@@ -27,6 +27,7 @@ function mountIssuableListRootApp() { ...@@ -27,6 +27,7 @@ function mountIssuableListRootApp() {
props: { props: {
canEdit: parseBoolean(el.dataset.canEdit), canEdit: parseBoolean(el.dataset.canEdit),
isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured), isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
issuesPath: el.dataset.issuesPath,
projectPath: el.dataset.projectPath, projectPath: el.dataset.projectPath,
}, },
}); });
......
#import "~/jira_import/queries/jira_import.fragment.graphql"
query($fullPath: ID!) {
project(fullPath: $fullPath) {
issues {
nodes {
labels {
nodes {
title
color
}
}
}
}
jiraImportStatus
jiraImports {
nodes {
...JiraImport
}
}
}
}
<script> <script>
import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import last from 'lodash/last'; 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 initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql'; import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql';
......
import { last } from 'lodash';
export const IMPORT_STATE = { export const IMPORT_STATE = {
FAILED: 'failed', FAILED: 'failed',
FINISHED: 'finished', FINISHED: 'finished',
...@@ -8,3 +10,50 @@ export const IMPORT_STATE = { ...@@ -8,3 +10,50 @@ export const IMPORT_STATE = {
export const isInProgress = state => export const isInProgress = state =>
state === IMPORT_STATE.SCHEDULED || state === IMPORT_STATE.STARTED; state === IMPORT_STATE.SCHEDULED || state === IMPORT_STATE.STARTED;
export const isFinished = state => state === IMPORT_STATE.FINISHED;
/**
* Calculates the label title for the most recent Jira import.
*
* @param {Object[]} jiraImports - List of Jira imports
* @param {string} jiraImports[].jiraProjectKey - Jira project key
* @returns {string} - A label title
*/
const calculateJiraImportLabelTitle = jiraImports => {
const mostRecentJiraProjectKey = last(jiraImports)?.jiraProjectKey;
const jiraProjectImportCount = jiraImports.filter(
jiraImport => jiraImport.jiraProjectKey === mostRecentJiraProjectKey,
).length;
return `jira-import::${mostRecentJiraProjectKey}-${jiraProjectImportCount}`;
};
/**
* Finds the label color from a list of labels.
*
* @param {string} labelTitle - Label title
* @param {Object[]} labels - List of labels
* @param {string} labels[].title - Label title
* @param {string} labels[].color - Label color
* @returns {string} - The label color associated with the given labelTitle
*/
const calculateJiraImportLabelColor = (labelTitle, labels) =>
labels.find(label => label.title === labelTitle)?.color;
/**
* Calculates the label for the most recent Jira import.
*
* @param {Object[]} jiraImports - List of Jira imports
* @param {string} jiraImports[].jiraProjectKey - Jira project key
* @param {Object[]} labels - List of labels
* @param {string} labels[].title - Label title
* @param {string} labels[].color - Label color
* @returns {{color: string, title: string}} - A label object containing a label color and title
*/
export const calculateJiraImportLabel = (jiraImports, labels) => {
const title = calculateJiraImportLabelTitle(jiraImports);
return {
color: calculateJiraImportLabelColor(title, labels),
title,
};
};
...@@ -303,3 +303,13 @@ ul.related-merge-requests > li { ...@@ -303,3 +303,13 @@ ul.related-merge-requests > li {
} }
} }
} }
.issuable-list-root {
.gl-label-link {
text-decoration: none;
&:hover {
color: inherit;
}
}
}
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
- if @project.jira_issues_import_feature_flag_enabled? - if @project.jira_issues_import_feature_flag_enabled?
.js-projects-issues-root{ data: { can_edit: can?(current_user, :admin_project, @project).to_s, .js-projects-issues-root{ data: { can_edit: can?(current_user, :admin_project, @project).to_s,
is_jira_configured: @project.jira_service.present?.to_s, is_jira_configured: @project.jira_service.present?.to_s,
issues_path: project_issues_path(@project),
project_path: @project.full_path } } project_path: @project.full_path } }
- if project_issues(@project).exists? - if project_issues(@project).exists?
......
---
title: Add alert on project issues page to show Jira import has finished
merge_request: 31375
author:
type: added
...@@ -12026,6 +12026,9 @@ msgstr "" ...@@ -12026,6 +12026,9 @@ msgstr ""
msgid "Issues referenced by merge requests and commits within the default branch will be closed automatically" msgid "Issues referenced by merge requests and commits within the default branch will be closed automatically"
msgstr "" msgstr ""
msgid "Issues successfully imported with the label"
msgstr ""
msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities" msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities"
msgstr "" msgstr ""
......
import { GlAlert } from '@gitlab/ui'; import { GlAlert, GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import IssuableListRootApp from '~/issuables_list/components/issuable_list_root_app.vue'; import IssuableListRootApp from '~/issuables_list/components/issuable_list_root_app.vue';
const mountComponent = ({
canEdit = true,
isAlertShowing = true,
isInProgress = false,
isJiraConfigured = true,
} = {}) =>
shallowMount(IssuableListRootApp, {
propsData: {
canEdit,
isJiraConfigured,
projectPath: 'gitlab-org/gitlab-test',
},
data() {
return {
isAlertShowing,
jiraImport: {
isInProgress,
},
};
},
});
describe('IssuableListRootApp', () => { describe('IssuableListRootApp', () => {
const issuesPath = 'gitlab-org/gitlab-test/-/issues';
const label = {
color: '#333',
title: 'jira-import::MTG-3',
};
let wrapper; let wrapper;
const findAlert = () => wrapper.find(GlAlert);
const findAlertLabel = () => wrapper.find(GlAlert).find(GlLabel);
const mountComponent = ({
isFinishedAlertShowing = false,
isInProgressAlertShowing = false,
isInProgress = false,
isFinished = false,
} = {}) =>
shallowMount(IssuableListRootApp, {
propsData: {
canEdit: true,
isJiraConfigured: true,
issuesPath,
projectPath: 'gitlab-org/gitlab-test',
},
data() {
return {
isFinishedAlertShowing,
isInProgressAlertShowing,
jiraImport: {
isInProgress,
isFinished,
label,
},
};
},
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
describe('when Jira import is not in progress', () => {
it('does not show an alert', () => {
wrapper = mountComponent();
expect(wrapper.contains(GlAlert)).toBe(false);
});
});
describe('when Jira import is in progress', () => { describe('when Jira import is in progress', () => {
it('shows an alert that tells the user a Jira import is in progress', () => { it('shows an alert that tells the user a Jira import is in progress', () => {
wrapper = mountComponent({ wrapper = mountComponent({
isInProgressAlertShowing: true,
isInProgress: true, isInProgress: true,
}); });
expect(wrapper.find(GlAlert).text()).toBe( expect(findAlert().text()).toBe(
'Import in progress. Refresh page to see newly added issues.', 'Import in progress. Refresh page to see newly added issues.',
); );
}); });
}); });
describe('when Jira import is not in progress', () => { describe('when Jira import has finished', () => {
it('does not show an alert', () => { beforeEach(() => {
wrapper = mountComponent(); wrapper = mountComponent({
isFinishedAlertShowing: true,
isFinished: true,
});
});
expect(wrapper.contains(GlAlert)).toBe(false); describe('shows an alert', () => {
it('tells the user the Jira import has finished', () => {
expect(findAlert().text()).toBe('Issues successfully imported with the label');
});
it('contains the label title associated with the Jira import', () => {
const alertLabelTitle = findAlertLabel().props('title');
expect(alertLabelTitle).toBe(label.title);
});
it('contains the correct label color', () => {
const alertLabelTitle = findAlertLabel().props('backgroundColor');
expect(alertLabelTitle).toBe(label.color);
});
it('contains a link within the label', () => {
const alertLabelTarget = findAlertLabel().props('target');
expect(alertLabelTarget).toBe(
`${issuesPath}?label_name[]=${encodeURIComponent(label.title)}`,
);
});
}); });
}); });
describe('alert message', () => { describe('alert message', () => {
it('is hidden when dismissed', () => { it('is hidden when dismissed', () => {
wrapper = mountComponent({ wrapper = mountComponent({
isInProgressAlertShowing: true,
isInProgress: true, isInProgress: true,
}); });
expect(wrapper.contains(GlAlert)).toBe(true); expect(wrapper.contains(GlAlert)).toBe(true);
wrapper.find(GlAlert).vm.$emit('dismiss'); findAlert().vm.$emit('dismiss');
return Vue.nextTick(() => { return Vue.nextTick(() => {
expect(wrapper.contains(GlAlert)).toBe(false); expect(wrapper.contains(GlAlert)).toBe(false);
......
import { IMPORT_STATE, isInProgress } from '~/jira_import/utils'; import {
calculateJiraImportLabel,
IMPORT_STATE,
isFinished,
isInProgress,
} from '~/jira_import/utils';
describe('isInProgress', () => { describe('isInProgress', () => {
it('returns true when state is IMPORT_STATE.SCHEDULED', () => { it.each`
expect(isInProgress(IMPORT_STATE.SCHEDULED)).toBe(true); state | result
${IMPORT_STATE.SCHEDULED} | ${true}
${IMPORT_STATE.STARTED} | ${true}
${IMPORT_STATE.FAILED} | ${false}
${IMPORT_STATE.FINISHED} | ${false}
${IMPORT_STATE.NONE} | ${false}
${undefined} | ${false}
`('returns $result when state is $state', ({ state, result }) => {
expect(isInProgress(state)).toBe(result);
}); });
});
it('returns true when state is IMPORT_STATE.STARTED', () => { describe('isFinished', () => {
expect(isInProgress(IMPORT_STATE.STARTED)).toBe(true); it.each`
state | result
${IMPORT_STATE.SCHEDULED} | ${false}
${IMPORT_STATE.STARTED} | ${false}
${IMPORT_STATE.FAILED} | ${false}
${IMPORT_STATE.FINISHED} | ${true}
${IMPORT_STATE.NONE} | ${false}
${undefined} | ${false}
`('returns $result when state is $state', ({ state, result }) => {
expect(isFinished(state)).toBe(result);
}); });
});
it('returns false when state is IMPORT_STATE.FAILED', () => { describe('calculateJiraImportLabel', () => {
expect(isInProgress(IMPORT_STATE.FAILED)).toBe(false); const jiraImports = [
}); { jiraProjectKey: 'MTG' },
{ jiraProjectKey: 'MJP' },
{ jiraProjectKey: 'MTG' },
{ jiraProjectKey: 'MSJP' },
{ jiraProjectKey: 'MTG' },
];
it('returns false when state is IMPORT_STATE.FINISHED', () => { const labels = [
expect(isInProgress(IMPORT_STATE.FINISHED)).toBe(false); { color: '#111', title: 'jira-import::MTG-1' },
}); { color: '#222', title: 'jira-import::MTG-2' },
{ color: '#333', title: 'jira-import::MTG-3' },
];
it('returns a label with the Jira project key and correct import count in the title', () => {
const label = calculateJiraImportLabel(jiraImports, labels);
it('returns false when state is IMPORT_STATE.NONE', () => { expect(label.title).toBe('jira-import::MTG-3');
expect(isInProgress(IMPORT_STATE.NONE)).toBe(false);
}); });
it('returns false when state is undefined', () => { it('returns a label with the correct color', () => {
expect(isInProgress()).toBe(false); const label = calculateJiraImportLabel(jiraImports, labels);
expect(label.color).toBe('#333');
}); });
}); });
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