Commit 2fcb5998 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch 'feature/zentao-integration-list-part' into 'master'

Feature ZenTao integration: Extracting Issue List component

See merge request gitlab-org/gitlab!69927
parents 8976bab1 572e705d
...@@ -315,7 +315,7 @@ export default { ...@@ -315,7 +315,7 @@ export default {
<span <span
v-if="isJiraIssue" v-if="isJiraIssue"
v-safe-html="jiraLogo" v-safe-html="jiraLogo"
class="svg-container jira-logo-container" class="svg-container logo-container"
data-testid="jira-logo" data-testid="jira-logo"
></span> ></span>
{{ referencePath }} {{ referencePath }}
......
<script> <script>
import { GlEmptyState, GlButton, GlIcon, GlSprintf } from '@gitlab/ui'; import { GlEmptyState, GlButton, GlIcon, GlSprintf } from '@gitlab/ui';
import { externalIssuesListEmptyStateI18n as i18n } from 'ee/external_issues_list/constants';
import { IssuableStates } from '~/issuable_list/constants'; import { IssuableStates } from '~/issuable_list/constants';
import { __, s__ } from '~/locale';
export default { export default {
FilterStateEmptyMessage: {
[IssuableStates.Opened]: __('There are no open issues'),
[IssuableStates.Closed]: __('There are no closed issues'),
},
components: { components: {
GlEmptyState, GlEmptyState,
GlButton, GlButton,
GlIcon, GlIcon,
GlSprintf, GlSprintf,
}, },
inject: ['emptyStatePath', 'issueCreateUrl'], // The text injected is sanitized.
inject: ['emptyStatePath', 'issueCreateUrl', 'emptyStateNoIssueText', 'createNewIssueText'],
props: { props: {
currentState: { currentState: {
type: String, type: String,
...@@ -35,21 +32,25 @@ export default { ...@@ -35,21 +32,25 @@ export default {
return this.issuesCount[IssuableStates.Opened] + this.issuesCount[IssuableStates.Closed] > 0; return this.issuesCount[IssuableStates.Opened] + this.issuesCount[IssuableStates.Closed] > 0;
}, },
emptyStateTitle() { emptyStateTitle() {
const { titleWhenFilters, filterStateEmptyMessage } = i18n;
if (this.hasFiltersApplied) { if (this.hasFiltersApplied) {
return __('Sorry, your filter produced no results'); return titleWhenFilters;
} else if (this.hasIssues) { } else if (this.hasIssues) {
return this.$options.FilterStateEmptyMessage[this.currentState]; return filterStateEmptyMessage[this.currentState];
} }
return s__(
'Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira.', return this.emptyStateNoIssueText;
);
}, },
emptyStateDescription() { emptyStateDescription() {
const { descriptionWhenFilters, descriptionWhenNoIssues } = i18n;
if (this.hasFiltersApplied) { if (this.hasFiltersApplied) {
return __('To widen your search, change or remove filters above'); return descriptionWhenFilters;
} else if (!this.hasIssues) { } else if (!this.hasIssues) {
return s__('Integrations|To keep this project going, create a new issue.'); return descriptionWhenNoIssues;
} }
return ''; return '';
}, },
}, },
...@@ -63,7 +64,7 @@ export default { ...@@ -63,7 +64,7 @@ export default {
</template> </template>
<template v-if="!hasIssues" #actions> <template v-if="!hasIssues" #actions>
<gl-button :href="issueCreateUrl" target="_blank" variant="confirm"> <gl-button :href="issueCreateUrl" target="_blank" variant="confirm">
{{ s__('Integrations|Create new issue in Jira') }} {{ createNewIssueText }}
<gl-icon name="external-link" /> <gl-icon name="external-link" />
</gl-button> </gl-button>
</template> </template>
......
<script> <script>
import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg';
import { GlButton, GlIcon, GlLink, GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { GlButton, GlIcon, GlLink, GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -10,6 +9,7 @@ import { ...@@ -10,6 +9,7 @@ import {
AvailableSortOptions, AvailableSortOptions,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
} from '~/issuable_list/constants'; } from '~/issuable_list/constants';
import { i18n } from '~/issues_list/constants';
import { import {
FILTERED_SEARCH_LABELS, FILTERED_SEARCH_LABELS,
FILTERED_SEARCH_TERM, FILTERED_SEARCH_TERM,
...@@ -18,12 +18,10 @@ import { ...@@ -18,12 +18,10 @@ import {
} from '~/vue_shared/components/filtered_search_bar/constants'; } from '~/vue_shared/components/filtered_search_bar/constants';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { ISSUES_LIST_FETCH_ERROR } from '../constants'; import ExternalIssuesListEmptyState from './external_issues_list_empty_state.vue';
import getJiraIssuesQuery from '../graphql/queries/get_jira_issues.query.graphql';
import JiraIssuesListEmptyState from './jira_issues_list_empty_state.vue';
export default { export default {
name: 'JiraIssuesList', name: 'ExternalIssuesList',
IssuableListTabs, IssuableListTabs,
AvailableSortOptions, AvailableSortOptions,
defaultPageSize: DEFAULT_PAGE_SIZE, defaultPageSize: DEFAULT_PAGE_SIZE,
...@@ -33,7 +31,7 @@ export default { ...@@ -33,7 +31,7 @@ export default {
GlLink, GlLink,
GlSprintf, GlSprintf,
IssuableList, IssuableList,
JiraIssuesListEmptyState, ExternalIssuesListEmptyState,
}, },
directives: { directives: {
SafeHtml, SafeHtml,
...@@ -45,6 +43,12 @@ export default { ...@@ -45,6 +43,12 @@ export default {
'issuesFetchPath', 'issuesFetchPath',
'projectFullPath', 'projectFullPath',
'issueCreateUrl', 'issueCreateUrl',
'getIssuesQuery',
'externalIssuesLogo',
'externalIssueTrackerName',
'searchInputPlaceholderText',
'recentSearchesStorageKey',
'createNewIssueText',
], ],
props: { props: {
initialFilterParams: { initialFilterParams: {
...@@ -55,7 +59,6 @@ export default { ...@@ -55,7 +59,6 @@ export default {
}, },
data() { data() {
return { return {
jiraLogo,
issues: [], issues: [],
totalIssues: 0, totalIssues: 0,
currentState: this.initialState, currentState: this.initialState,
...@@ -71,7 +74,7 @@ export default { ...@@ -71,7 +74,7 @@ export default {
}, },
computed: { computed: {
issuesListLoading() { issuesListLoading() {
return this.$apollo.queries.jiraIssues.loading; return this.$apollo.queries.externalIssues.loading;
}, },
showPaginationControls() { showPaginationControls() {
return Boolean(!this.issuesListLoading && this.issues.length && this.totalIssues > 1); return Boolean(!this.issuesListLoading && this.issues.length && this.totalIssues > 1);
...@@ -90,8 +93,10 @@ export default { ...@@ -90,8 +93,10 @@ export default {
}, },
}, },
apollo: { apollo: {
jiraIssues: { externalIssues: {
query: getJiraIssuesQuery, query() {
return this.getIssuesQuery;
},
variables() { variables() {
return { return {
issuesFetchPath: this.issuesFetchPath, issuesFetchPath: this.issuesFetchPath,
...@@ -108,9 +113,9 @@ export default { ...@@ -108,9 +113,9 @@ export default {
return; return;
} }
const { pageInfo, nodes, errors } = data?.jiraIssues ?? {}; const { pageInfo, nodes, errors } = data?.externalIssues ?? {};
if (errors?.length > 0) { if (errors?.length > 0) {
this.onJiraIssuesQueryError(new Error(errors[0])); this.onExternalIssuesQueryError(new Error(errors[0]));
return; return;
} }
...@@ -120,7 +125,7 @@ export default { ...@@ -120,7 +125,7 @@ export default {
this.issuesCount[this.currentState] = nodes.length; this.issuesCount[this.currentState] = nodes.length;
}, },
error(error) { error(error) {
this.onJiraIssuesQueryError(error, ISSUES_LIST_FETCH_ERROR); this.onExternalIssuesQueryError(error, i18n.errorFetchingIssues);
}, },
}, },
}, },
...@@ -142,6 +147,7 @@ export default { ...@@ -142,6 +147,7 @@ export default {
}, },
]; ];
}, },
getFilteredSearchValue() { getFilteredSearchValue() {
const { labels, search } = this.filterParams || {}; const { labels, search } = this.filterParams || {};
const filteredSearchValue = []; const filteredSearchValue = [];
...@@ -166,7 +172,7 @@ export default { ...@@ -166,7 +172,7 @@ export default {
return filteredSearchValue; return filteredSearchValue;
}, },
onJiraIssuesQueryError(error, message) { onExternalIssuesQueryError(error, message) {
createFlash({ createFlash({
message: message || error.message, message: message || error.message,
captureError: true, captureError: true,
...@@ -223,7 +229,7 @@ export default { ...@@ -223,7 +229,7 @@ export default {
:namespace="projectFullPath" :namespace="projectFullPath"
:tabs="$options.IssuableListTabs" :tabs="$options.IssuableListTabs"
:current-tab="currentState" :current-tab="currentState"
:search-input-placeholder="s__('Integrations|Search Jira issues')" :search-input-placeholder="searchInputPlaceholderText"
:search-tokens="getFilteredSearchTokens()" :search-tokens="getFilteredSearchTokens()"
:sort-options="$options.AvailableSortOptions" :sort-options="$options.AvailableSortOptions"
:initial-filter-value="getFilteredSearchValue()" :initial-filter-value="getFilteredSearchValue()"
...@@ -238,7 +244,7 @@ export default { ...@@ -238,7 +244,7 @@ export default {
:next-page="currentPage + 1" :next-page="currentPage + 1"
:url-params="urlParams" :url-params="urlParams"
label-filter-param="labels" label-filter-param="labels"
recent-searches-storage-key="jira_issues" :recent-searches-storage-key="recentSearchesStorageKey"
@click-tab="onIssuableListClickTab" @click-tab="onIssuableListClickTab"
@page-change="onIssuableListPageChange" @page-change="onIssuableListPageChange"
@sort="onIssuableListSort" @sort="onIssuableListSort"
...@@ -246,16 +252,18 @@ export default { ...@@ -246,16 +252,18 @@ export default {
> >
<template #nav-actions> <template #nav-actions>
<gl-button :href="issueCreateUrl" target="_blank" class="gl-my-5"> <gl-button :href="issueCreateUrl" target="_blank" class="gl-my-5">
{{ s__('Integrations|Create new issue in Jira') }} {{ createNewIssueText }}
<gl-icon name="external-link" /> <gl-icon name="external-link" />
</gl-button> </gl-button>
</template> </template>
<template #reference="{ issuable }"> <template #reference="{ issuable }">
<span v-safe-html="jiraLogo" class="svg-container jira-logo-container"></span> <span v-safe-html="externalIssuesLogo" class="svg-container logo-container"></span>
<span v-if="issuable">{{ issuable.references.relative }}</span> <span v-if="issuable">
{{ issuable.references ? issuable.references.relative : issuable.id }}
</span>
</template> </template>
<template #author="{ author }"> <template #author="{ author }">
<gl-sprintf message="%{authorName} in Jira"> <gl-sprintf :message="`%{authorName} in ${externalIssueTrackerName}`">
<template #authorName> <template #authorName>
<gl-link class="author-link js-user-link" target="_blank" :href="author.webUrl"> <gl-link class="author-link js-user-link" target="_blank" :href="author.webUrl">
{{ author.name }} {{ author.name }}
...@@ -267,7 +275,7 @@ export default { ...@@ -267,7 +275,7 @@ export default {
<template v-if="issuable"> {{ issuable.status }} </template> <template v-if="issuable"> {{ issuable.status }} </template>
</template> </template>
<template #empty-state> <template #empty-state>
<jira-issues-list-empty-state <external-issues-list-empty-state
:current-state="currentState" :current-state="currentState"
:issues-count="issuesCount" :issues-count="issuesCount"
:has-filters-applied="hasFiltersApplied" :has-filters-applied="hasFiltersApplied"
......
import { IssuableStates } from '~/issuable_list/constants';
import { __, s__ } from '~/locale';
export const externalIssuesListEmptyStateI18n = {
titleWhenFilters: __('Sorry, your filter produced no results'),
descriptionWhenFilters: __('To widen your search, change or remove filters above'),
descriptionWhenNoIssues: s__('Integrations|To keep this project going, create a new issue.'),
filterStateEmptyMessage: {
[IssuableStates.Opened]: __('There are no open issues'),
[IssuableStates.Closed]: __('There are no closed issues'),
},
};
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import jiraIssues from './resolvers/jira_issues';
Vue.use(VueApollo); Vue.use(VueApollo);
const resolvers = { export default (externalIssuesQueryResolver) => {
Query: { const resolvers = {
jiraIssues, Query: {
}, externalIssues: externalIssuesQueryResolver,
}; },
};
const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true }); const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true });
export default new VueApollo({ return new VueApollo({
defaultClient, defaultClient,
}); });
};
import Vue from 'vue';
import { IssuableStates } from '~/issuable_list/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import ExternalIssuesListApp from './components/external_issues_list_root.vue';
import getApolloProvider from './graphql';
export default function externalIssuesListFactory({
provides: {
getIssuesQuery,
externalIssuesLogo,
externalIssueTrackerName,
searchInputPlaceholderText,
recentSearchesStorageKey,
createNewIssueText,
emptyStateNoIssueText,
},
externalIssuesQueryResolver,
}) {
return function initExternalIssuesList({ mountPointSelector }) {
const mountPointEl = document.querySelector(mountPointSelector);
if (!mountPointEl) {
return null;
}
const {
page = 1,
initialState = IssuableStates.Opened,
initialSortBy = 'created_desc',
} = mountPointEl.dataset;
const initialFilterParams = Object.assign(
convertObjectPropsToCamelCase(
queryToObject(window.location.search.substring(1), { gatherArrays: true }),
{
dropKeys: ['scope', 'utf8', 'state', 'sort'], // These keys are unsupported/unnecessary
},
),
);
return new Vue({
el: mountPointEl,
provide: {
...mountPointEl.dataset,
page: parseInt(page, 10),
initialState,
initialSortBy,
getIssuesQuery,
externalIssuesLogo,
externalIssueTrackerName,
searchInputPlaceholderText,
recentSearchesStorageKey,
createNewIssueText,
emptyStateNoIssueText,
},
apolloProvider: getApolloProvider(externalIssuesQueryResolver),
render: (createElement) =>
createElement(ExternalIssuesListApp, {
props: {
initialFilterParams,
},
}),
});
};
}
import { __ } from '~/locale';
export const ISSUES_LIST_FETCH_ERROR = __('An error occurred while loading issues');
...@@ -9,7 +9,7 @@ query jiraIssues( ...@@ -9,7 +9,7 @@ query jiraIssues(
$state: String $state: String
$page: Integer $page: Integer
) { ) {
jiraIssues( externalIssues(
issuesFetchPath: $issuesFetchPath issuesFetchPath: $issuesFetchPath
search: $search search: $search
labels: $labels labels: $labels
......
import { DEFAULT_PAGE_SIZE } from '~/issuable_list/constants'; import { DEFAULT_PAGE_SIZE } from '~/issuable_list/constants';
import { i18n } from '~/issues_list/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { ISSUES_LIST_FETCH_ERROR } from '../../constants';
const transformJiraIssueAssignees = (jiraIssue) => { const transformJiraIssueAssignees = (jiraIssue) => {
return jiraIssue.assignees.map((assignee) => ({ return jiraIssue.assignees.map((assignee) => ({
...@@ -78,7 +78,7 @@ export default function jiraIssuesResolver( ...@@ -78,7 +78,7 @@ export default function jiraIssuesResolver(
.catch((error) => { .catch((error) => {
return { return {
__typename: 'JiraIssues', __typename: 'JiraIssues',
errors: error?.response?.data?.errors || [ISSUES_LIST_FETCH_ERROR], errors: error?.response?.data?.errors || [i18n.errorFetchingIssues],
pageInfo: transformJiraIssuePageInfo(), pageInfo: transformJiraIssuePageInfo(),
nodes: [], nodes: [],
}; };
......
import Vue from 'vue'; import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg';
import externalIssuesListFactory from 'ee/external_issues_list';
import { s__ } from '~/locale';
import getIssuesQuery from './graphql/queries/get_jira_issues.query.graphql';
import jiraIssuesResolver from './graphql/resolvers/jira_issues';
import { IssuableStates } from '~/issuable_list/constants'; export default externalIssuesListFactory({
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; externalIssuesQueryResolver: jiraIssuesResolver,
import { queryToObject } from '~/lib/utils/url_utility'; provides: {
getIssuesQuery,
import JiraIssuesListApp from './components/jira_issues_list_root.vue'; externalIssuesLogo: jiraLogo,
import apolloProvider from './graphql'; externalIssueTrackerName: 'Jira', // eslint-disable-line @gitlab/require-i18n-strings
searchInputPlaceholderText: s__('Integrations|Search Jira issues'),
export default function initJiraIssuesList({ mountPointSelector }) { recentSearchesStorageKey: 'jira_issues',
const mountPointEl = document.querySelector(mountPointSelector); createNewIssueText: s__('Integrations|Create new issue in Jira'),
emptyStateNoIssueText: s__(
if (!mountPointEl) { 'Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira.',
return null;
}
const {
page = 1,
initialState = IssuableStates.Opened,
initialSortBy = 'created_desc',
} = mountPointEl.dataset;
const initialFilterParams = Object.assign(
convertObjectPropsToCamelCase(
queryToObject(window.location.search.substring(1), { gatherArrays: true }),
{
dropKeys: ['scope', 'utf8', 'state', 'sort'], // These keys are unsupported/unnecessary
},
), ),
); },
});
return new Vue({
el: mountPointEl,
provide: {
...mountPointEl.dataset,
page: parseInt(page, 10),
initialState,
initialSortBy,
},
apolloProvider,
render: (createElement) =>
createElement(JiraIssuesListApp, {
props: {
initialFilterParams,
},
}),
});
}
@import '../../../../../app/assets/stylesheets/page_bundles/issues_list'; @import '../../../../../app/assets/stylesheets/page_bundles/issues_list';
.svg-container.jira-logo-container { .svg-container.logo-container {
svg { svg {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JiraIssuesListRoot when request succeeds renders issuable-list component with correct props 1`] = ` exports[`ExternalIssuesListRoot when request succeeds renders issuable-list component with correct props 1`] = `
Object { Object {
"currentPage": 1, "currentPage": 1,
"currentTab": "opened", "currentTab": "opened",
......
import { GlEmptyState, GlSprintf, GlButton } from '@gitlab/ui'; import { GlEmptyState, GlSprintf, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import JiraIssuesListEmptyState from 'ee/integrations/jira/issues_list/components/jira_issues_list_empty_state.vue'; import ExternalIssuesListEmptyState from 'ee/external_issues_list/components/external_issues_list_empty_state.vue';
import { externalIssuesListEmptyStateI18n } from 'ee/external_issues_list/constants';
import { IssuableStates } from '~/issuable_list/constants'; import { IssuableStates } from '~/issuable_list/constants';
import { mockProvide } from '../mock_data'; import { mockProvide } from '../mock_data';
const createComponent = (props = {}) => const createComponent = (props = {}) =>
shallowMount(JiraIssuesListEmptyState, { shallowMount(ExternalIssuesListEmptyState, {
provide: mockProvide, provide: mockProvide,
propsData: { propsData: {
currentState: 'opened', currentState: 'opened',
...@@ -22,16 +23,11 @@ const createComponent = (props = {}) => ...@@ -22,16 +23,11 @@ const createComponent = (props = {}) =>
stubs: { GlEmptyState }, stubs: { GlEmptyState },
}); });
describe('JiraIssuesListEmptyState', () => { describe('ExternalIssuesListEmptyState', () => {
const titleDefault =
'Issues created in Jira are shown here once you have created the issues in project setup in Jira.';
const titleWhenFilters = 'Sorry, your filter produced no results';
const titleWhenIssues = 'There are no open issues';
const descriptionWhenFilters = 'To widen your search, change or remove filters above';
const descriptionWhenNoIssues = 'To keep this project going, create a new issue.';
let wrapper; let wrapper;
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
}); });
...@@ -61,17 +57,19 @@ describe('JiraIssuesListEmptyState', () => { ...@@ -61,17 +57,19 @@ describe('JiraIssuesListEmptyState', () => {
}); });
describe('emptyStateTitle', () => { describe('emptyStateTitle', () => {
it(`returns string "${titleWhenFilters}" when hasFiltersApplied prop is true`, async () => { it(`returns correct string when hasFiltersApplied prop is true`, async () => {
wrapper.setProps({ wrapper.setProps({
hasFiltersApplied: true, hasFiltersApplied: true,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.vm.emptyStateTitle).toBe(titleWhenFilters); expect(findEmptyState().props('title')).toBe(
externalIssuesListEmptyStateI18n.titleWhenFilters,
);
}); });
it(`returns string "${titleWhenIssues}" when hasFiltersApplied prop is false and hasIssues is true`, async () => { it(`returns correct string when hasFiltersApplied prop is false and hasIssues is true`, async () => {
wrapper.setProps({ wrapper.setProps({
hasFiltersApplied: false, hasFiltersApplied: false,
issuesCount: { issuesCount: {
...@@ -82,7 +80,9 @@ describe('JiraIssuesListEmptyState', () => { ...@@ -82,7 +80,9 @@ describe('JiraIssuesListEmptyState', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.vm.emptyStateTitle).toBe(titleWhenIssues); expect(findEmptyState().props('title')).toBe(
externalIssuesListEmptyStateI18n.filterStateEmptyMessage[IssuableStates.Opened],
);
}); });
it('returns default title string when both hasFiltersApplied and hasIssues props are false', async () => { it('returns default title string when both hasFiltersApplied and hasIssues props are false', async () => {
...@@ -92,29 +92,33 @@ describe('JiraIssuesListEmptyState', () => { ...@@ -92,29 +92,33 @@ describe('JiraIssuesListEmptyState', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.vm.emptyStateTitle).toBe(titleDefault); expect(findEmptyState().props('title')).toBe(mockProvide.emptyStateNoIssueText);
}); });
}); });
describe('emptyStateDescription', () => { describe('emptyStateDescription', () => {
it(`returns string "${descriptionWhenFilters}" when hasFiltersApplied prop is true`, async () => { it(`returns correct when hasFiltersApplied prop is true`, async () => {
wrapper.setProps({ wrapper.setProps({
hasFiltersApplied: true, hasFiltersApplied: true,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.vm.emptyStateDescription).toBe(descriptionWhenFilters); expect(wrapper.vm.emptyStateDescription).toBe(
externalIssuesListEmptyStateI18n.descriptionWhenFilters,
);
}); });
it(`returns string "${descriptionWhenNoIssues}" when both hasFiltersApplied and hasIssues props are false`, async () => { it(`returns correct string when both hasFiltersApplied and hasIssues props are false`, async () => {
wrapper.setProps({ wrapper.setProps({
hasFiltersApplied: false, hasFiltersApplied: false,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.vm.emptyStateDescription).toBe(descriptionWhenNoIssues); expect(wrapper.vm.emptyStateDescription).toBe(
externalIssuesListEmptyStateI18n.descriptionWhenNoIssues,
);
}); });
it(`returns empty string when hasFiltersApplied is false and hasIssues is true`, async () => { it(`returns empty string when hasFiltersApplied is false and hasIssues is true`, async () => {
...@@ -143,8 +147,7 @@ describe('JiraIssuesListEmptyState', () => { ...@@ -143,8 +147,7 @@ describe('JiraIssuesListEmptyState', () => {
expect(emptyStateEl.props()).toMatchObject({ expect(emptyStateEl.props()).toMatchObject({
svgPath: mockProvide.emptyStatePath, svgPath: mockProvide.emptyStatePath,
title: title: mockProvide.emptyStateNoIssueText,
'Issues created in Jira are shown here once you have created the issues in project setup in Jira.',
}); });
wrapper.setProps({ wrapper.setProps({
...@@ -153,7 +156,7 @@ describe('JiraIssuesListEmptyState', () => { ...@@ -153,7 +156,7 @@ describe('JiraIssuesListEmptyState', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(emptyStateEl.props('title')).toBe('Sorry, your filter produced no results'); expect(emptyStateEl.props('title')).toBe(externalIssuesListEmptyStateI18n.titleWhenFilters);
wrapper.setProps({ wrapper.setProps({
hasFiltersApplied: false, hasFiltersApplied: false,
...@@ -165,7 +168,9 @@ describe('JiraIssuesListEmptyState', () => { ...@@ -165,7 +168,9 @@ describe('JiraIssuesListEmptyState', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(emptyStateEl.props('title')).toBe('There are no open issues'); expect(emptyStateEl.props('title')).toBe(
externalIssuesListEmptyStateI18n.filterStateEmptyMessage[IssuableStates.Opened],
);
}); });
it('renders empty state description', () => { it('renders empty state description', () => {
...@@ -192,12 +197,12 @@ describe('JiraIssuesListEmptyState', () => { ...@@ -192,12 +197,12 @@ describe('JiraIssuesListEmptyState', () => {
expect(descriptionEl.exists()).toBe(false); expect(descriptionEl.exists()).toBe(false);
}); });
it('renders "Create new issue in Jira" button', () => { it('renders "create issue button', () => {
const buttonEl = wrapper.find(GlButton); const buttonEl = wrapper.findComponent(GlButton);
expect(buttonEl.exists()).toBe(true); expect(buttonEl.exists()).toBe(true);
expect(buttonEl.attributes('href')).toBe(mockProvide.issueCreateUrl); expect(buttonEl.attributes('href')).toBe(mockProvide.issueCreateUrl);
expect(buttonEl.text()).toBe('Create new issue in Jira'); expect(buttonEl.text()).toBe(mockProvide.createNewIssueText);
}); });
}); });
}); });
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue, mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import JiraIssuesListRoot from 'ee/integrations/jira/issues_list/components/jira_issues_list_root.vue'; import ExternalIssuesListRoot from 'ee/external_issues_list/components/external_issues_list_root.vue';
import { ISSUES_LIST_FETCH_ERROR } from 'ee/integrations/jira/issues_list/constants'; import jiraIssuesResolver from 'ee/integrations/jira/issues_list/graphql/resolvers/jira_issues';
import jiraIssues from 'ee/integrations/jira/issues_list/graphql/resolvers/jira_issues';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash'; import createFlash from '~/flash';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { i18n } from '~/issues_list/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { mockProvide, mockJiraIssues } from '../mock_data'; import {
mockProvide,
mockJiraIssues as mockExternalIssues,
mockJiraIssue4 as mockJiraIssueNoReference,
} from '../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/issuable_list/constants', () => ({ jest.mock('~/issuable_list/constants', () => ({
...@@ -31,16 +35,16 @@ jest.mock( ...@@ -31,16 +35,16 @@ jest.mock(
const resolvedValue = { const resolvedValue = {
headers: { headers: {
'x-page': 1, 'x-page': 1,
'x-total': mockJiraIssues.length, 'x-total': mockExternalIssues.length,
}, },
data: mockJiraIssues, data: mockExternalIssues,
}; };
const localVue = createLocalVue(); const localVue = createLocalVue();
const resolvers = { const resolvers = {
Query: { Query: {
jiraIssues, externalIssues: jiraIssuesResolver,
}, },
}; };
...@@ -49,7 +53,7 @@ function createMockApolloProvider(mockResolvers = resolvers) { ...@@ -49,7 +53,7 @@ function createMockApolloProvider(mockResolvers = resolvers) {
return createMockApollo([], mockResolvers); return createMockApollo([], mockResolvers);
} }
describe('JiraIssuesListRoot', () => { describe('ExternalIssuesListRoot', () => {
let wrapper; let wrapper;
let mock; let mock;
...@@ -65,7 +69,7 @@ describe('JiraIssuesListRoot', () => { ...@@ -65,7 +69,7 @@ describe('JiraIssuesListRoot', () => {
provide = mockProvide, provide = mockProvide,
initialFilterParams = {}, initialFilterParams = {},
} = {}) => { } = {}) => {
wrapper = shallowMount(JiraIssuesListRoot, { wrapper = shallowMount(ExternalIssuesListRoot, {
propsData: { propsData: {
initialFilterParams, initialFilterParams,
}, },
...@@ -155,6 +159,45 @@ describe('JiraIssuesListRoot', () => { ...@@ -155,6 +159,45 @@ describe('JiraIssuesListRoot', () => {
expect(issuableList.props()).toMatchSnapshot(); expect(issuableList.props()).toMatchSnapshot();
}); });
describe('issuable-list reference section', () => {
it('renders issuable-list component with correct reference', async () => {
jest.spyOn(axios, 'get').mockResolvedValue(resolvedValue);
wrapper = mount(ExternalIssuesListRoot, {
propsData: {
initialFilterParams: {},
},
provide: mockProvide,
localVue,
apolloProvider: createMockApolloProvider(),
});
await waitForPromises();
expect(wrapper.find('.issuable-info').text()).toContain(
resolvedValue.data[0].references.relative,
);
});
it('renders issuable-list component with id when references is not presence', async () => {
jest.spyOn(axios, 'get').mockResolvedValue({
...resolvedValue,
data: [mockJiraIssueNoReference],
});
wrapper = mount(ExternalIssuesListRoot, {
propsData: {
initialFilterParams: {},
},
provide: mockProvide,
localVue,
apolloProvider: createMockApolloProvider(),
});
await waitForPromises();
// Since Jira transformer transforms references.relative into id, we can only test
// whether it exists.
expect(wrapper.find('.issuable-info').exists()).toBe(false);
});
});
describe('issuable-list events', () => { describe('issuable-list events', () => {
it('"click-tab" event executes GET request correctly', async () => { it('"click-tab" event executes GET request correctly', async () => {
const issuableList = findIssuableList(); const issuableList = findIssuableList();
...@@ -181,7 +224,7 @@ describe('JiraIssuesListRoot', () => { ...@@ -181,7 +224,7 @@ describe('JiraIssuesListRoot', () => {
const issuableList = findIssuableList(); const issuableList = findIssuableList();
jest.spyOn(axios, 'get').mockResolvedValue({ jest.spyOn(axios, 'get').mockResolvedValue({
...resolvedValue, ...resolvedValue,
headers: { 'x-page': mockPage, 'x-total': mockJiraIssues.length }, headers: { 'x-page': mockPage, 'x-total': mockExternalIssues.length },
}); });
issuableList.vm.$emit('page-change', mockPage); issuableList.vm.$emit('page-change', mockPage);
...@@ -261,7 +304,7 @@ describe('JiraIssuesListRoot', () => { ...@@ -261,7 +304,7 @@ describe('JiraIssuesListRoot', () => {
it.each` it.each`
APIErrors | expectedRenderedErrorMessage APIErrors | expectedRenderedErrorMessage
${['API error']} | ${'API error'} ${['API error']} | ${'API error'}
${undefined} | ${ISSUES_LIST_FETCH_ERROR} ${undefined} | ${i18n.errorFetchingIssues}
`( `(
'calls `createFlash` with "$expectedRenderedErrorMessage" when API responds with "$APIErrors"', 'calls `createFlash` with "$expectedRenderedErrorMessage" when API responds with "$APIErrors"',
async ({ APIErrors, expectedRenderedErrorMessage }) => { async ({ APIErrors, expectedRenderedErrorMessage }) => {
...@@ -287,14 +330,14 @@ describe('JiraIssuesListRoot', () => { ...@@ -287,14 +330,14 @@ describe('JiraIssuesListRoot', () => {
createComponent({ createComponent({
apolloProvider: createMockApolloProvider({ apolloProvider: createMockApolloProvider({
Query: { Query: {
jiraIssues: jest.fn().mockRejectedValue(new Error('GraphQL networkError')), externalIssues: jest.fn().mockRejectedValue(new Error('GraphQL networkError')),
}, },
}), }),
}); });
await waitForPromises(); await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ expect(createFlash).toHaveBeenCalledWith({
message: ISSUES_LIST_FETCH_ERROR, message: i18n.errorFetchingIssues,
captureError: true, captureError: true,
error: expect.any(Object), error: expect.any(Object),
}); });
...@@ -304,10 +347,10 @@ describe('JiraIssuesListRoot', () => { ...@@ -304,10 +347,10 @@ describe('JiraIssuesListRoot', () => {
describe('pagination', () => { describe('pagination', () => {
it.each` it.each`
scenario | issuesListLoadFailed | issues | shouldShowPaginationControls scenario | issuesListLoadFailed | issues | shouldShowPaginationControls
${'fails'} | ${true} | ${[]} | ${false} ${'fails'} | ${true} | ${[]} | ${false}
${'returns no issues'} | ${false} | ${[]} | ${false} ${'returns no issues'} | ${false} | ${[]} | ${false}
${`returns some issues`} | ${false} | ${mockJiraIssues} | ${true} ${`returns some issues`} | ${false} | ${mockExternalIssues} | ${true}
`( `(
'sets `showPaginationControls` prop to $shouldShowPaginationControls when request $scenario', 'sets `showPaginationControls` prop to $shouldShowPaginationControls when request $scenario',
async ({ issuesListLoadFailed, issues, shouldShowPaginationControls }) => { async ({ issuesListLoadFailed, issues, shouldShowPaginationControls }) => {
......
import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg';
import mockGetJiraIssuesQuery from 'ee/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql';
export const mockProvide = { export const mockProvide = {
initialState: 'opened', initialState: 'opened',
initialSortBy: 'created_desc', initialSortBy: 'created_desc',
...@@ -6,6 +9,15 @@ export const mockProvide = { ...@@ -6,6 +9,15 @@ export const mockProvide = {
projectFullPath: 'gitlab-org/gitlab-test', projectFullPath: 'gitlab-org/gitlab-test',
issueCreateUrl: 'https://gitlab-jira.atlassian.net/secure/CreateIssue!default.jspa', issueCreateUrl: 'https://gitlab-jira.atlassian.net/secure/CreateIssue!default.jspa',
emptyStatePath: '/assets/illustrations/issues.svg', emptyStatePath: '/assets/illustrations/issues.svg',
getIssuesQuery: mockGetJiraIssuesQuery,
externalIssuesLogo: jiraLogo,
externalIssueTrackerName: 'Jira',
emptyStateNoIssueText:
'Issues created in Jira are shown here once you have created the issues in project setup in Jira.',
recentSearchesStorageKey: 'jira_issues',
createNewIssueText: 'Create new issue in Jira',
searchInputPlaceholderText: 'Search Jira issues',
}; };
export const mockJiraIssue1 = { export const mockJiraIssue1 = {
...@@ -87,4 +99,24 @@ export const mockJiraIssue3 = { ...@@ -87,4 +99,24 @@ export const mockJiraIssue3 = {
external_tracker: 'jira', external_tracker: 'jira',
}; };
// issue without reference presence
export const mockJiraIssue4 = {
project_id: 1,
title: 'Alias ut modi est labore.',
created_at: '2020-03-19T14:31:50.012Z',
updated_at: '2020-03-19T14:31:50.012Z',
closed_at: null,
status: 'Backlog',
labels: [],
author: {
name: 'Gabe Weaver',
web_url: 'https://gitlab-jira.atlassian.net/people/5e320a31fe03e20c9d1dccde',
avatar_url: null,
},
assignees: [],
web_url: 'https://gitlab-jira.atlassian.net/browse/IG-31594',
gitlab_web_url: '',
external_tracker: 'jira',
};
export const mockJiraIssues = [mockJiraIssue1, mockJiraIssue2, mockJiraIssue3]; export const mockJiraIssues = [mockJiraIssue1, mockJiraIssue2, mockJiraIssue3];
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