Commit 13e3ca75 authored by Jeremy Wu's avatar Jeremy Wu Committed by Nicolò Maria Mezzopera

Feature ZenTao integration: Integrate ZenTao with External Issue

parent 510ee2a4
<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" fill="none">
<path fill="url(#SVGID_1_)" d="M8,1C4.1,1,1,4.1,1,8s3.1,7,7,7s7-3.1,7-7S11.9,1,8,1L8,1z M11.3,8.2C9.8,7.7,7.9,6.1,5.8,7.6
C4,8.9,4.8,11.1,6,11.8c0.9,0.6,2.3,0.7,3,0.1C9.7,11.4,10,10,9,9.5C8.6,9.4,7.9,9.3,7.5,9.8c-0.5,0.6-0.3,1.4,0.4,1.7
c0,0-1.2-0.1-1.4-1.3C6.2,7.9,9,7.6,10.3,8.4c2.4,1.5,1.5,4.8-2,5.4c-1.8,0.3-4.8-0.3-5.9-2.7c-0.4-0.9-0.3-0.7-0.3-0.7
c0.1,0.1,0.3,0.3,0.4,0.4c0.8,0.6,1.6,0.1,1.4-0.8C3.3,7.2,4.4,6.7,5.1,6.2s0.4-1.5-0.9-1.3c-1.9,0.3-2.4,3-2.4,3s-0.3-4.6,3.7-5
c4.1-0.4,4.7,3.2,6.5,3.7c2.5,0.8,1.3-2.6,1.3-2.6s1.1,1.7,0.6,3.2C13.5,8.2,12.3,8.5,11.3,8.2z" />
<defs>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="8" y1="-271.1102" x2="8" y2="-257.1102"
gradientTransform="matrix(1 0 0 -1 0 -256.1102)">
<stop offset="0" style="stop-color:#445470" />
<stop offset="1" style="stop-color:#7A869A" />
</linearGradient>
</defs>
</svg>
#import "../fragments/zentao_label.fragment.graphql"
#import "../fragments/zentao_user.fragment.graphql"
query externalIssues(
$issuesFetchPath: String
$search: String
$labels: String
$sort: String
$state: String
$page: Integer
) {
externalIssues(
issuesFetchPath: $issuesFetchPath
search: $search
labels: $labels
sort: $sort
state: $state
page: $page
) @client {
errors
pageInfo {
total
page
}
nodes {
id
projectId
createdAt
updatedAt
closedAt
title
webUrl
gitlabWebUrl
status
labels {
...ZentaoLabel
}
assignees {
...ZentaoUser
}
author {
...ZentaoUser
}
}
}
}
import { DEFAULT_PAGE_SIZE } from '~/issuable_list/constants';
import { i18n } from '~/issues_list/constants';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const transformZentaoIssueAssignees = (zentaoIssue) => {
return zentaoIssue.assignees.map((assignee) => ({
__typename: 'UserCore',
...assignee,
}));
};
const transformZentaoIssueAuthor = (zentaoIssue, authorId) => {
return {
__typename: 'UserCore',
...zentaoIssue.author,
id: authorId,
};
};
const transformZentaoIssueLabels = (zentaoIssue) => {
return zentaoIssue.labels.map((label) => ({
__typename: 'Label', // eslint-disable-line @gitlab/require-i18n-strings
...label,
}));
};
const transformZentaoIssuePageInfo = (responseHeaders = {}) => {
return {
__typename: 'ZentaoIssuesPageInfo',
page: parseInt(responseHeaders['x-page'], 10) ?? 1,
total: parseInt(responseHeaders['x-total'], 10) ?? 0,
};
};
export const transformZentaoIssuesREST = (response) => {
const { headers, data: zentaoIssues } = response;
return {
__typename: 'ZentaoIssues',
errors: [],
pageInfo: transformZentaoIssuePageInfo(headers),
nodes: zentaoIssues.map((rawIssue, index) => {
const zentaoIssue = convertObjectPropsToCamelCase(rawIssue, { deep: true });
return {
__typename: 'ZentaoIssue',
...zentaoIssue,
id: rawIssue.id,
author: transformZentaoIssueAuthor(zentaoIssue, index),
labels: transformZentaoIssueLabels(zentaoIssue),
assignees: transformZentaoIssueAssignees(zentaoIssue),
};
}),
};
};
export default function zentaoIssuesResolver(
_,
{ issuesFetchPath, search, page, state, sort, labels },
) {
return axios
.get(issuesFetchPath, {
params: {
limit: DEFAULT_PAGE_SIZE,
page,
state,
sort,
labels,
search,
},
})
.then((res) => {
return transformZentaoIssuesREST(res);
})
.catch((error) => {
return {
__typename: 'ZentaoIssues',
errors: error?.response?.data?.errors || [i18n.errorFetchingIssues],
pageInfo: transformZentaoIssuePageInfo(),
nodes: [],
};
});
}
import externalIssuesListFactory from 'ee/external_issues_list';
import zentaoLogo from 'images/logos/zentao.svg';
import { s__ } from '~/locale';
import getIssuesQuery from './graphql/queries/get_zentao_issues.query.graphql';
import zentaoIssues from './graphql/resolvers/zentao_issues';
export default externalIssuesListFactory({
query: zentaoIssues,
provides: {
getIssuesQuery,
externalIssuesLogo: zentaoLogo,
// This like below is passed to <gl-sprintf :message="%authorName in {}" />
// So we don't translate it since this should be a proper noun
externalIssueTrackerName: 'ZenTao',
searchInputPlaceholderText: s__('Integrations|Search ZenTao issues'),
recentSearchesStorageKey: 'zentao_issues',
createNewIssueText: s__('Integrations|Create new issue in ZenTao'),
logoContainerClass: 'logo-container',
emptyStateNoIssueText: s__(
'Integrations|ZenTao issues display here when you create issues in your project in ZenTao.',
),
},
});
import initZentaoIssuesList from 'ee/integrations/zentao/issues_list/zentao_issues_list_bundle';
initZentaoIssuesList({ mountPointSelector: '.js-zentao-issues-list' });
export const mockZentaoIssue1 = {
project_id: 1,
id: 1,
title: 'Eius fuga voluptates.',
created_at: '2020-03-19T14:31:51.281Z',
updated_at: '2020-10-20T07:01:45.865Z',
closed_at: null,
status: 'Selected for Development',
labels: [
{
title: 'backend',
name: 'backend',
color: '#0052CC',
text_color: '#FFFFFF',
},
],
author: {
name: 'jhope',
web_url: 'https://gitlab-zentao.atlassian.net/people/5e32f803e127810e82875bc1',
avatar_url: null,
},
assignees: [
{
name: 'Kushal Pandya',
web_url: 'https://gitlab-zentao.atlassian.net/people/1920938475',
avatar_url: null,
},
],
web_url: 'https://gitlab-zentao.atlassian.net/browse/IG-31596',
gitlab_web_url: '',
};
export const mockZentaoIssue2 = {
project_id: 1,
id: 2,
title: 'Hic sit sint ducimus ea et sint.',
created_at: '2020-03-19T14:31:50.677Z',
updated_at: '2020-03-19T14:31:50.677Z',
closed_at: null,
status: 'Backlog',
labels: [],
author: {
name: 'Gabe Weaver',
web_url: 'https://gitlab-zentao.atlassian.net/people/5e320a31fe03e20c9d1dccde',
avatar_url: null,
},
assignees: [],
web_url: 'https://gitlab-zentao.atlassian.net/browse/IG-31595',
gitlab_web_url: '',
};
export const mockZentaoIssue3 = {
project_id: 1,
id: 3,
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-zentao.atlassian.net/people/5e320a31fe03e20c9d1dccde',
avatar_url: null,
},
assignees: [],
web_url: 'https://gitlab-zentao.atlassian.net/browse/IG-31594',
gitlab_web_url: '',
};
export const mockZentaoIssues = [mockZentaoIssue1, mockZentaoIssue2, mockZentaoIssue3];
import MockAdapter from 'axios-mock-adapter';
import createApolloProvider from 'ee/external_issues_list/graphql';
import getZentaoIssues from 'ee/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql';
import zentaoIssuesResolver from 'ee/integrations/zentao/issues_list/graphql/resolvers/zentao_issues';
import { DEFAULT_PAGE_SIZE } from '~/issuable_list/constants';
import { i18n } from '~/issues_list/constants';
import axios from '~/lib/utils/axios_utils';
import { mockZentaoIssues } from '../mock_data';
const DEFAULT_ISSUES_FETCH_PATH = '/test/issues/fetch';
const DEFAULT_VARIABLES = {
issuesFetchPath: DEFAULT_ISSUES_FETCH_PATH,
search: '',
labels: '',
sort: '',
state: '',
page: 1,
};
const TEST_ERROR_RESPONSE = { errors: ['lorem ipsum'] };
const TEST_PAGE_HEADERS = {
'x-page': '10',
'x-total': '13',
};
const TYPE_ZENTAO_ISSUES = 'ZentaoIssues';
describe('ee/integrations/zentao/issues_list/graphql/resolvers/zentao_issues', () => {
let mock;
let apolloClient;
let issuesApiSpy;
const createPageInfo = ({ page, total }) => ({
__typename: 'ZentaoIssuesPageInfo',
page,
total,
});
const createUserCore = ({ avatar_url, web_url, ...props }) => ({
__typename: 'UserCore',
avatarUrl: avatar_url,
webUrl: web_url,
...props,
});
const createLabel = ({ text_color, ...props }) => ({
__typename: 'Label',
textColor: text_color,
...props,
});
const createZentaoIssue = ({
assignees,
author,
labels,
closed_at,
created_at,
gitlab_web_url,
updated_at,
web_url,
project_id,
...props
}) => ({
__typename: 'ZentaoIssue',
assignees: assignees.map(createUserCore),
author: createUserCore(author),
labels: labels.map(createLabel),
closedAt: closed_at,
createdAt: created_at,
gitlabWebUrl: gitlab_web_url,
updatedAt: updated_at,
webUrl: web_url,
projectId: project_id,
...props,
});
const query = (variables = {}) =>
apolloClient.query({
variables: {
...DEFAULT_VARIABLES,
...variables,
},
query: getZentaoIssues,
});
beforeEach(() => {
issuesApiSpy = jest.fn();
mock = new MockAdapter(axios);
mock.onGet(DEFAULT_ISSUES_FETCH_PATH).reply((...args) => issuesApiSpy(...args));
({ defaultClient: apolloClient } = createApolloProvider(zentaoIssuesResolver));
});
afterEach(() => {
mock.restore();
});
describe.each`
desc | errorResponse | expectedErrors
${'when api request fails with data.errors'} | ${TEST_ERROR_RESPONSE} | ${TEST_ERROR_RESPONSE.errors}
${'when api request fails with unknown erorr'} | ${{}} | ${[i18n.errorFetchingIssues]}
`('$desc', ({ errorResponse, expectedErrors }) => {
beforeEach(() => {
issuesApiSpy.mockReturnValue([400, errorResponse]);
});
it('returns error data', async () => {
const response = await query();
expect(response.data).toEqual({
externalIssues: {
__typename: TYPE_ZENTAO_ISSUES,
errors: expectedErrors,
pageInfo: createPageInfo({ page: Number.NaN, total: Number.NaN }),
nodes: [],
},
});
});
});
describe('with successful api request', () => {
beforeEach(() => {
issuesApiSpy.mockReturnValue([200, mockZentaoIssues, TEST_PAGE_HEADERS]);
});
it('sends expected params', async () => {
const variables = {
search: 'test search',
page: 5,
state: 'test state',
sort: 'test sort',
labels: 'test labels',
};
expect(issuesApiSpy).not.toHaveBeenCalled();
await query(variables);
expect(issuesApiSpy).toHaveBeenCalledWith(
expect.objectContaining({
params: {
limit: DEFAULT_PAGE_SIZE,
...variables,
},
}),
);
});
it('returns transformed data', async () => {
const response = await query();
expect(response.data).toEqual({
externalIssues: {
__typename: TYPE_ZENTAO_ISSUES,
errors: [],
pageInfo: createPageInfo({ page: 10, total: 13 }),
nodes: mockZentaoIssues.map(createZentaoIssue),
},
});
});
});
});
......@@ -18209,6 +18209,9 @@ msgstr ""
msgid "Integrations|Create new issue in Jira"
msgstr ""
msgid "Integrations|Create new issue in ZenTao"
msgstr ""
msgid "Integrations|Default settings are inherited from the group level."
msgstr ""
......@@ -18302,6 +18305,9 @@ msgstr ""
msgid "Integrations|Search Jira issues"
msgstr ""
msgid "Integrations|Search ZenTao issues"
msgstr ""
msgid "Integrations|Send notifications about project events to Unify Circuit."
msgstr ""
......@@ -18347,6 +18353,9 @@ msgstr ""
msgid "Integrations|You've activated every integration 🎉"
msgstr ""
msgid "Integrations|ZenTao issues display here when you create issues in your project in ZenTao."
msgstr ""
msgid "Interactive mode"
msgstr ""
......
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