diff --git a/app/assets/images/logos/zentao.svg b/app/assets/images/logos/zentao.svg new file mode 100644 index 0000000000000000000000000000000000000000..d2115b72aee056ed8b7f30ca0d370134979d850a --- /dev/null +++ b/app/assets/images/logos/zentao.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_label.fragment.graphql b/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_label.fragment.graphql new file mode 100644 index 0000000000000000000000000000000000000000..3efbcc1f2b67d1ef7d187321412c321b09a209bf --- /dev/null +++ b/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_label.fragment.graphql @@ -0,0 +1,6 @@ +fragment ZentaoLabel on Label { + title + name + color + textColor +} diff --git a/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_user.fragment.graphql b/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_user.fragment.graphql new file mode 100644 index 0000000000000000000000000000000000000000..873e5b15c564f5209730249d74154d053be539c7 --- /dev/null +++ b/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_user.fragment.graphql @@ -0,0 +1,5 @@ +fragment ZentaoUser on UserCore { + avatarUrl + name + webUrl +} diff --git a/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql b/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..af9ff4a33c473064ee864c43bad0a37496bad2c6 --- /dev/null +++ b/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql @@ -0,0 +1,46 @@ +#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 + } + } + } +} diff --git a/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/resolvers/zentao_issues.js b/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/resolvers/zentao_issues.js new file mode 100644 index 0000000000000000000000000000000000000000..d770d11e0ad9b653887692598b97d61de66db8ab --- /dev/null +++ b/ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/resolvers/zentao_issues.js @@ -0,0 +1,83 @@ +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: [], + }; + }); +} diff --git a/ee/app/assets/javascripts/integrations/zentao/issues_list/zentao_issues_list_bundle.js b/ee/app/assets/javascripts/integrations/zentao/issues_list/zentao_issues_list_bundle.js new file mode 100644 index 0000000000000000000000000000000000000000..1c12e95c1612d708c6cc284f300c92b42b0babd5 --- /dev/null +++ b/ee/app/assets/javascripts/integrations/zentao/issues_list/zentao_issues_list_bundle.js @@ -0,0 +1,23 @@ +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 + // 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.', + ), + }, +}); diff --git a/ee/app/assets/javascripts/pages/projects/integrations/zentao/issues/index/index.js b/ee/app/assets/javascripts/pages/projects/integrations/zentao/issues/index/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0ce4618e0bf9f7880e87e49c15c2d3b84b5e2e46 --- /dev/null +++ b/ee/app/assets/javascripts/pages/projects/integrations/zentao/issues/index/index.js @@ -0,0 +1,3 @@ +import initZentaoIssuesList from 'ee/integrations/zentao/issues_list/zentao_issues_list_bundle'; + +initZentaoIssuesList({ mountPointSelector: '.js-zentao-issues-list' }); diff --git a/ee/spec/frontend/integrations/zentao/issues_list/mock_data.js b/ee/spec/frontend/integrations/zentao/issues_list/mock_data.js new file mode 100644 index 0000000000000000000000000000000000000000..d704178ada37e2941009d22fb934116803f6a062 --- /dev/null +++ b/ee/spec/frontend/integrations/zentao/issues_list/mock_data.js @@ -0,0 +1,71 @@ +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]; diff --git a/ee/spec/frontend/integrations/zentao/issues_list/resolvers/zentao_issues_spec.js b/ee/spec/frontend/integrations/zentao/issues_list/resolvers/zentao_issues_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..694c84725913c76aea58df3517e29cb90b3541cc --- /dev/null +++ b/ee/spec/frontend/integrations/zentao/issues_list/resolvers/zentao_issues_spec.js @@ -0,0 +1,163 @@ +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), + }, + }); + }); + }); +}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f6c91b6df33ab6835000f9f61e740e577dd53769..50c61497e6f3f2ae9d939aa032b0c2869fa2de05 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 ""