From 3584e6eb47502f25ef5ed4d0d4cd055b6c4967d2 Mon Sep 17 00:00:00 2001 From: Baodong Date: Wed, 25 Aug 2021 14:47:39 +0800 Subject: [PATCH 1/7] Add Zentao views See: https://gitlab.com/gitlab-org/gitlab/-/issues/338178 Changelog: added --- app/assets/images/logos/zentao.svg | 16 + .../zentao_issues_list_empty_state.vue | 71 ++++ .../components/zentao_issues_list_root.vue | 278 +++++++++++++++ .../zentao/issues_list/constants/index.js | 3 + .../fragments/zentao_label.fragment.graphql | 6 + .../fragments/zentao_user.fragment.graphql | 5 + .../zentao/issues_list/graphql/index.js | 18 + .../queries/get_zentao_issues.query.graphql | 46 +++ .../graphql/resolvers/zentao_issues.js | 83 +++++ .../issues_list/zentao_issues_list_bundle.js | 48 +++ .../integrations/zentao/issues_show/api.js | 37 ++ .../zentao/issues_show/components/note.vue | 100 ++++++ .../components/sidebar/assignee.vue | 85 +++++ .../components/sidebar/issue_due_date.vue | 76 ++++ .../components/sidebar/issue_field.vue | 141 ++++++++ .../sidebar/issue_field_dropdown.vue | 66 ++++ .../sidebar/zentao_issues_sidebar_root.vue | 162 +++++++++ .../components/zentao_issues_show_root.vue | 190 ++++++++++ .../zentao/issues_show/constants.js | 13 + .../issues_show/zentao_issues_show_bundle.js | 22 ++ .../issues_list/components/issuable.vue | 2 +- .../integrations/zentao/issues/index/index.js | 3 + .../integrations/zentao/issues/show/index.js | 4 + .../stylesheets/page_bundles/issues_list.scss | 6 + .../zentao/issues/index.html.haml | 10 + .../integrations/zentao/issues/show.html.haml | 5 + .../zentao_issues_list_root_spec.js.snap | 157 +++++++++ .../zentao_issues_list_empty_state_spec.js | 203 +++++++++++ .../zentao_issues_list_root_spec.js | 330 ++++++++++++++++++ .../zentao/issues_list/mock_data.js | 93 +++++ .../__snapshots__/assignee_spec.js.snap | 76 ++++ .../components/sidebar/assignee_spec.js | 121 +++++++ .../components/sidebar/issue_due_date_spec.js | 69 ++++ .../sidebar/issue_field_dropdown_spec.js | 57 +++ .../components/sidebar/issue_field_spec.js | 117 +++++++ .../zentao_issues_sidebar_root_spec.js | 63 ++++ .../zentao_issues_show_root_spec.js | 181 ++++++++++ .../zentao/issues_show/mock_data.js | 45 +++ 38 files changed, 3007 insertions(+), 1 deletion(-) create mode 100644 app/assets/images/logos/zentao.svg create mode 100644 app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_empty_state.vue create mode 100644 app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_root.vue create mode 100644 app/assets/javascripts/integrations/zentao/issues_list/constants/index.js create mode 100644 app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_label.fragment.graphql create mode 100644 app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_user.fragment.graphql create mode 100644 app/assets/javascripts/integrations/zentao/issues_list/graphql/index.js create mode 100644 app/assets/javascripts/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql create mode 100644 app/assets/javascripts/integrations/zentao/issues_list/graphql/resolvers/zentao_issues.js create mode 100644 app/assets/javascripts/integrations/zentao/issues_list/zentao_issues_list_bundle.js create mode 100644 app/assets/javascripts/integrations/zentao/issues_show/api.js create mode 100644 app/assets/javascripts/integrations/zentao/issues_show/components/note.vue create mode 100644 app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/assignee.vue create mode 100644 app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_due_date.vue create mode 100644 app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_field.vue create mode 100644 app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_field_dropdown.vue create mode 100644 app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue create mode 100644 app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue create mode 100644 app/assets/javascripts/integrations/zentao/issues_show/constants.js create mode 100644 app/assets/javascripts/integrations/zentao/issues_show/zentao_issues_show_bundle.js create mode 100644 app/assets/javascripts/pages/projects/integrations/zentao/issues/index/index.js create mode 100644 app/assets/javascripts/pages/projects/integrations/zentao/issues/show/index.js create mode 100644 app/views/projects/integrations/zentao/issues/index.html.haml create mode 100644 app/views/projects/integrations/zentao/issues/show.html.haml create mode 100644 spec/frontend/integrations/zentao/issues_list/components/__snapshots__/zentao_issues_list_root_spec.js.snap create mode 100644 spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js create mode 100644 spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_root_spec.js create mode 100644 spec/frontend/integrations/zentao/issues_list/mock_data.js create mode 100644 spec/frontend/integrations/zentao/issues_show/components/sidebar/__snapshots__/assignee_spec.js.snap create mode 100644 spec/frontend/integrations/zentao/issues_show/components/sidebar/assignee_spec.js create mode 100644 spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_due_date_spec.js create mode 100644 spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_field_dropdown_spec.js create mode 100644 spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_field_spec.js create mode 100644 spec/frontend/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root_spec.js create mode 100644 spec/frontend/integrations/zentao/issues_show/components/zentao_issues_show_root_spec.js create mode 100644 spec/frontend/integrations/zentao/issues_show/mock_data.js diff --git a/app/assets/images/logos/zentao.svg b/app/assets/images/logos/zentao.svg new file mode 100644 index 00000000000000..f2937c16454a2c --- /dev/null +++ b/app/assets/images/logos/zentao.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_empty_state.vue b/app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_empty_state.vue new file mode 100644 index 00000000000000..9a1ebdf320778d --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_empty_state.vue @@ -0,0 +1,71 @@ + + + diff --git a/app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_root.vue b/app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_root.vue new file mode 100644 index 00000000000000..023e9fc4e1aa1c --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_root.vue @@ -0,0 +1,278 @@ + + + diff --git a/app/assets/javascripts/integrations/zentao/issues_list/constants/index.js b/app/assets/javascripts/integrations/zentao/issues_list/constants/index.js new file mode 100644 index 00000000000000..b8d2532e108b11 --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_list/constants/index.js @@ -0,0 +1,3 @@ +import { __ } from '~/locale'; + +export const ISSUES_LIST_FETCH_ERROR = __('An error occurred while loading issues'); diff --git a/app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_label.fragment.graphql b/app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_label.fragment.graphql new file mode 100644 index 00000000000000..3efbcc1f2b67d1 --- /dev/null +++ b/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/app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_user.fragment.graphql b/app/assets/javascripts/integrations/zentao/issues_list/graphql/fragments/zentao_user.fragment.graphql new file mode 100644 index 00000000000000..873e5b15c564f5 --- /dev/null +++ b/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/app/assets/javascripts/integrations/zentao/issues_list/graphql/index.js b/app/assets/javascripts/integrations/zentao/issues_list/graphql/index.js new file mode 100644 index 00000000000000..c8721e86c0f6bc --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_list/graphql/index.js @@ -0,0 +1,18 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import zentaoIssues from './resolvers/zentao_issues'; + +Vue.use(VueApollo); + +const resolvers = { + Query: { + zentaoIssues, + }, +}; + +const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true }); + +export default new VueApollo({ + defaultClient, +}); diff --git a/app/assets/javascripts/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql b/app/assets/javascripts/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql new file mode 100644 index 00000000000000..7f1c210f506dc5 --- /dev/null +++ b/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 zentaoIssues( + $issuesFetchPath: String + $search: String + $labels: String + $sort: String + $state: String + $page: Integer +) { + zentaoIssues( + 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/app/assets/javascripts/integrations/zentao/issues_list/graphql/resolvers/zentao_issues.js b/app/assets/javascripts/integrations/zentao/issues_list/graphql/resolvers/zentao_issues.js new file mode 100644 index 00000000000000..9ce868225e1069 --- /dev/null +++ b/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 axios from '~/lib/utils/axios_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { ISSUES_LIST_FETCH_ERROR } from '../../constants'; + +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 || [ISSUES_LIST_FETCH_ERROR], + pageInfo: transformZentaoIssuePageInfo(), + nodes: [], + }; + }); +} diff --git a/app/assets/javascripts/integrations/zentao/issues_list/zentao_issues_list_bundle.js b/app/assets/javascripts/integrations/zentao/issues_list/zentao_issues_list_bundle.js new file mode 100644 index 00000000000000..c2dbfb3205c4dc --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_list/zentao_issues_list_bundle.js @@ -0,0 +1,48 @@ +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 ZentaoIssuesListApp from './components/zentao_issues_list_root.vue'; +import apolloProvider from './graphql'; + +export default function initZentaoIssuesList({ 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, + }, + apolloProvider, + render: (createElement) => + createElement(ZentaoIssuesListApp, { + props: { + initialFilterParams, + }, + }), + }); +} diff --git a/app/assets/javascripts/integrations/zentao/issues_show/api.js b/app/assets/javascripts/integrations/zentao/issues_show/api.js new file mode 100644 index 00000000000000..95fe28c90e2306 --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_show/api.js @@ -0,0 +1,37 @@ +import axios from '~/lib/utils/axios_utils'; + +export const fetchIssue = async (issuePath) => { + return axios.get(issuePath).then(({ data }) => { + return data; + }); +}; + +export const fetchIssueStatuses = () => { + // We are using mock data here which should come from the backend + return new Promise((resolve) => { + setTimeout(() => { + // eslint-disable-next-line @gitlab/require-i18n-strings + resolve([{ title: 'In Progress' }, { title: 'Done' }]); + }, 1000); + }); +}; + +export const updateIssue = (issue, { labels = [], status = undefined }) => { + // We are using mock call here which should become a backend call + return new Promise((resolve) => { + setTimeout(() => { + const addedLabels = labels.filter((label) => label.set); + const removedLabelsIds = labels.filter((label) => !label.set).map((label) => label.id); + + const finalLabels = [...issue.labels, ...addedLabels].filter( + (label) => !removedLabelsIds.includes(label.id), + ); + + resolve({ + ...issue, + ...(status ? { status } : {}), + labels: finalLabels, + }); + }, 1000); + }); +}; diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/note.vue b/app/assets/javascripts/integrations/zentao/issues_show/components/note.vue new file mode 100644 index 00000000000000..ff7fbd75ad4b05 --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_show/components/note.vue @@ -0,0 +1,100 @@ + + + diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/assignee.vue b/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/assignee.vue new file mode 100644 index 00000000000000..1c410c8ce1c190 --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/assignee.vue @@ -0,0 +1,85 @@ + + + diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_due_date.vue b/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_due_date.vue new file mode 100644 index 00000000000000..66360feda1abfc --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_due_date.vue @@ -0,0 +1,76 @@ + + + diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_field.vue b/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_field.vue new file mode 100644 index 00000000000000..2e24d77632f9a4 --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_field.vue @@ -0,0 +1,141 @@ + + + diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_field_dropdown.vue b/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_field_dropdown.vue new file mode 100644 index 00000000000000..37df42c80e5a52 --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/issue_field_dropdown.vue @@ -0,0 +1,66 @@ + + + diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue b/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue new file mode 100644 index 00000000000000..8dd71e512b64ed --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue @@ -0,0 +1,162 @@ + + + diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue b/app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue new file mode 100644 index 00000000000000..b7a1a2cb98dc01 --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue @@ -0,0 +1,190 @@ + + + diff --git a/app/assets/javascripts/integrations/zentao/issues_show/constants.js b/app/assets/javascripts/integrations/zentao/issues_show/constants.js new file mode 100644 index 00000000000000..29bb2dedfd2554 --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_show/constants.js @@ -0,0 +1,13 @@ +import { __ } from '~/locale'; + +export const issueStates = { + OPENED: 'opened', + CLOSED: 'closed', +}; + +export const issueStateLabels = { + [issueStates.OPENED]: __('Open'), + [issueStates.CLOSED]: __('Closed'), +}; + +export const labelsFilterParam = 'labels'; diff --git a/app/assets/javascripts/integrations/zentao/issues_show/zentao_issues_show_bundle.js b/app/assets/javascripts/integrations/zentao/issues_show/zentao_issues_show_bundle.js new file mode 100644 index 00000000000000..137de031eaced8 --- /dev/null +++ b/app/assets/javascripts/integrations/zentao/issues_show/zentao_issues_show_bundle.js @@ -0,0 +1,22 @@ +import Vue from 'vue'; + +import ZentaoIssuesShowApp from './components/zentao_issues_show_root.vue'; + +export default function initZentaoIssueShow({ mountPointSelector }) { + const mountPointEl = document.querySelector(mountPointSelector); + + if (!mountPointEl) { + return null; + } + + const { issuesShowPath, issuesListPath } = mountPointEl.dataset; + + return new Vue({ + el: mountPointEl, + provide: { + issuesShowPath, + issuesListPath, + }, + render: (createElement) => createElement(ZentaoIssuesShowApp), + }); +} diff --git a/app/assets/javascripts/issues_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue index 60b01a6d37f2eb..6dc7460b037421 100644 --- a/app/assets/javascripts/issues_list/components/issuable.vue +++ b/app/assets/javascripts/issues_list/components/issuable.vue @@ -315,7 +315,7 @@ export default { {{ referencePath }} diff --git a/app/assets/javascripts/pages/projects/integrations/zentao/issues/index/index.js b/app/assets/javascripts/pages/projects/integrations/zentao/issues/index/index.js new file mode 100644 index 00000000000000..90d70739460e0b --- /dev/null +++ b/app/assets/javascripts/pages/projects/integrations/zentao/issues/index/index.js @@ -0,0 +1,3 @@ +import initZentaoIssuesList from '~/integrations/zentao/issues_list/zentao_issues_list_bundle'; + +initZentaoIssuesList({ mountPointSelector: '.js-zentao-issues-list' }); diff --git a/app/assets/javascripts/pages/projects/integrations/zentao/issues/show/index.js b/app/assets/javascripts/pages/projects/integrations/zentao/issues/show/index.js new file mode 100644 index 00000000000000..e922b7141c461d --- /dev/null +++ b/app/assets/javascripts/pages/projects/integrations/zentao/issues/show/index.js @@ -0,0 +1,4 @@ +import initZentaoIssueShow from '~/integrations/zentao/issues_show/zentao_issues_show_bundle'; +// import initJiraIssueShow from 'ee/integrations/jira/issues_show/jira_issues_show_bundle'; + +initZentaoIssueShow({ mountPointSelector: '.js-zentao-issues-show-app' }); diff --git a/app/assets/stylesheets/page_bundles/issues_list.scss b/app/assets/stylesheets/page_bundles/issues_list.scss index 8a958bdf0c5750..5af8902949a63f 100644 --- a/app/assets/stylesheets/page_bundles/issues_list.scss +++ b/app/assets/stylesheets/page_bundles/issues_list.scss @@ -43,3 +43,9 @@ opacity: 0.3; pointer-events: none; } + +.svg-container.logo-container { + svg { + vertical-align: text-bottom; + } +} diff --git a/app/views/projects/integrations/zentao/issues/index.html.haml b/app/views/projects/integrations/zentao/issues/index.html.haml new file mode 100644 index 00000000000000..c58ff6297ce52f --- /dev/null +++ b/app/views/projects/integrations/zentao/issues/index.html.haml @@ -0,0 +1,10 @@ +- page_title _('Zentao issues') +- add_page_specific_style 'page_bundles/issues_list' + +.js-zentao-issues-list{ data: { issues_fetch_path: project_integrations_zentao_issues_path(@project, format: :json), +page: params[:page], +initial_state: params[:state], +initial_sort_by: params[:sort], +project_full_path: @project.full_path, +issue_create_url: @project.zentao_integration&.url, +empty_state_path: image_path('illustrations/issues.svg') } } diff --git a/app/views/projects/integrations/zentao/issues/show.html.haml b/app/views/projects/integrations/zentao/issues/show.html.haml new file mode 100644 index 00000000000000..c60a6a86f6b74c --- /dev/null +++ b/app/views/projects/integrations/zentao/issues/show.html.haml @@ -0,0 +1,5 @@ +- add_to_breadcrumbs _('Zentao issues'), project_integrations_zentao_issues_path(@project) +- breadcrumb_title zentao_issue_breadcrumb_link(@issue_json) +- page_title @issue_json[:title] + +.js-zentao-issues-show-app{ data: zentao_issues_show_data } diff --git a/spec/frontend/integrations/zentao/issues_list/components/__snapshots__/zentao_issues_list_root_spec.js.snap b/spec/frontend/integrations/zentao/issues_list/components/__snapshots__/zentao_issues_list_root_spec.js.snap new file mode 100644 index 00000000000000..a7188f06bf0149 --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_list/components/__snapshots__/zentao_issues_list_root_spec.js.snap @@ -0,0 +1,157 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ZentaoIssuesListRoot when request succeeds renders issuable-list component with correct props 1`] = ` +Object { + "currentPage": 1, + "currentTab": "opened", + "defaultPageSize": 2, + "enableLabelPermalinks": true, + "hasNextPage": false, + "hasPreviousPage": false, + "initialFilterValue": Array [], + "initialSortBy": "created_desc", + "isManualOrdering": false, + "issuableSymbol": "#", + "issuables": Array [ + Object { + "assignees": Array [ + Object { + "avatarUrl": null, + "name": "Kushal Pandya", + "webUrl": "https://gitlab-zentao.atlassian.net/people/1920938475", + }, + ], + "author": Object { + "avatarUrl": null, + "name": "jhope", + "webUrl": "https://gitlab-zentao.atlassian.net/people/5e32f803e127810e82875bc1", + }, + "closedAt": null, + "createdAt": "2020-03-19T14:31:51.281Z", + "gitlabWebUrl": "", + "id": 1, + "labels": Array [ + Object { + "color": "#0052CC", + "name": "backend", + "textColor": "#FFFFFF", + "title": "backend", + }, + ], + "projectId": 1, + "status": "Selected for Development", + "title": "Eius fuga voluptates.", + "updatedAt": "2020-10-20T07:01:45.865Z", + "webUrl": "https://gitlab-zentao.atlassian.net/browse/IG-31596", + }, + Object { + "assignees": Array [], + "author": Object { + "avatarUrl": null, + "name": "Gabe Weaver", + "webUrl": "https://gitlab-zentao.atlassian.net/people/5e320a31fe03e20c9d1dccde", + }, + "closedAt": null, + "createdAt": "2020-03-19T14:31:50.677Z", + "gitlabWebUrl": "", + "id": 2, + "labels": Array [], + "projectId": 1, + "status": "Backlog", + "title": "Hic sit sint ducimus ea et sint.", + "updatedAt": "2020-03-19T14:31:50.677Z", + "webUrl": "https://gitlab-zentao.atlassian.net/browse/IG-31595", + }, + Object { + "assignees": Array [], + "author": Object { + "avatarUrl": null, + "name": "Gabe Weaver", + "webUrl": "https://gitlab-zentao.atlassian.net/people/5e320a31fe03e20c9d1dccde", + }, + "closedAt": null, + "createdAt": "2020-03-19T14:31:50.012Z", + "gitlabWebUrl": "", + "id": 3, + "labels": Array [], + "projectId": 1, + "status": "Backlog", + "title": "Alias ut modi est labore.", + "updatedAt": "2020-03-19T14:31:50.012Z", + "webUrl": "https://gitlab-zentao.atlassian.net/browse/IG-31594", + }, + ], + "issuablesLoading": false, + "labelFilterParam": "labels", + "namespace": "gitlab-org/gitlab-test", + "nextPage": 2, + "previousPage": 0, + "recentSearchesStorageKey": "zentao_issues", + "searchInputPlaceholder": "Search Zentao issues", + "searchTokens": Array [ + Object { + "defaultLabels": Array [], + "fetchLabels": [Function], + "icon": "labels", + "operators": Array [ + Object { + "description": "is", + "value": "=", + }, + ], + "symbol": "~", + "title": "Label", + "token": "LabelTokenMock", + "type": "labels", + "unique": false, + }, + ], + "showBulkEditSidebar": false, + "showPaginationControls": true, + "sortOptions": Array [ + Object { + "id": 1, + "sortDirection": Object { + "ascending": "created_asc", + "descending": "created_desc", + }, + "title": "Created date", + }, + Object { + "id": 2, + "sortDirection": Object { + "ascending": "updated_asc", + "descending": "updated_desc", + }, + "title": "Last updated", + }, + ], + "tabCounts": null, + "tabs": Array [ + Object { + "id": "state-opened", + "name": "opened", + "title": "Open", + "titleTooltip": "Filter by issues that are currently opened.", + }, + Object { + "id": "state-closed", + "name": "closed", + "title": "Closed", + "titleTooltip": "Filter by issues that are currently closed.", + }, + Object { + "id": "state-all", + "name": "all", + "title": "All", + "titleTooltip": "Show all issues.", + }, + ], + "totalItems": 3, + "urlParams": Object { + "labels[]": undefined, + "search": undefined, + }, + "useKeysetPagination": false, +} +`; diff --git a/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js new file mode 100644 index 00000000000000..f1d17cf4b11e46 --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js @@ -0,0 +1,203 @@ +import { GlEmptyState, GlSprintf, GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; + +import ZentaoIssuesListEmptyState from '~/integrations/zentao/issues_list/components/zentao_issues_list_empty_state.vue'; +import { IssuableStates } from '~/issuable_list/constants'; + +import { mockProvide } from '../mock_data'; + +const createComponent = (props = {}) => + shallowMount(ZentaoIssuesListEmptyState, { + provide: mockProvide, + propsData: { + currentState: 'opened', + issuesCount: { + [IssuableStates.Opened]: 0, + [IssuableStates.Closed]: 0, + [IssuableStates.All]: 0, + }, + hasFiltersApplied: false, + ...props, + }, + stubs: { GlEmptyState }, + }); + +describe('ZentaoIssuesListEmptyState', () => { + const titleDefault = + 'Issues created in Zentao are shown here once you have created the issues in project setup in Zentao.'; + 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; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('computed', () => { + describe('hasIssues', () => { + it('returns false when total of opened and closed issues within `issuesCount` is 0', () => { + expect(wrapper.vm.hasIssues).toBe(false); + }); + + it('returns true when total of opened and closed issues within `issuesCount` is more than 0', async () => { + wrapper.setProps({ + issuesCount: { + [IssuableStates.Opened]: 1, + [IssuableStates.Closed]: 1, + }, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.hasIssues).toBe(true); + }); + }); + + describe('emptyStateTitle', () => { + it(`returns string "${titleWhenFilters}" when hasFiltersApplied prop is true`, async () => { + wrapper.setProps({ + hasFiltersApplied: true, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.emptyStateTitle).toBe(titleWhenFilters); + }); + + it(`returns string "${titleWhenIssues}" when hasFiltersApplied prop is false and hasIssues is true`, async () => { + wrapper.setProps({ + hasFiltersApplied: false, + issuesCount: { + [IssuableStates.Opened]: 1, + [IssuableStates.Closed]: 1, + }, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.emptyStateTitle).toBe(titleWhenIssues); + }); + + it('returns default title string when both hasFiltersApplied and hasIssues props are false', async () => { + wrapper.setProps({ + hasFiltersApplied: false, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.emptyStateTitle).toBe(titleDefault); + }); + }); + + describe('emptyStateDescription', () => { + it(`returns string "${descriptionWhenFilters}" when hasFiltersApplied prop is true`, async () => { + wrapper.setProps({ + hasFiltersApplied: true, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.emptyStateDescription).toBe(descriptionWhenFilters); + }); + + it(`returns string "${descriptionWhenNoIssues}" when both hasFiltersApplied and hasIssues props are false`, async () => { + wrapper.setProps({ + hasFiltersApplied: false, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.emptyStateDescription).toBe(descriptionWhenNoIssues); + }); + + it(`returns empty string when hasFiltersApplied is false and hasIssues is true`, async () => { + wrapper.setProps({ + hasFiltersApplied: false, + issuesCount: { + [IssuableStates.Opened]: 1, + [IssuableStates.Closed]: 1, + }, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.emptyStateDescription).toBe(''); + }); + }); + }); + + describe('template', () => { + it('renders gl-empty-state component', () => { + expect(wrapper.find(GlEmptyState).exists()).toBe(true); + }); + + it('renders empty state title', async () => { + const emptyStateEl = wrapper.find(GlEmptyState); + + expect(emptyStateEl.props()).toMatchObject({ + svgPath: mockProvide.emptyStatePath, + title: + 'Issues created in Zentao are shown here once you have created the issues in project setup in Zentao.', + }); + + wrapper.setProps({ + hasFiltersApplied: true, + }); + + await wrapper.vm.$nextTick(); + + expect(emptyStateEl.props('title')).toBe('Sorry, your filter produced no results'); + + wrapper.setProps({ + hasFiltersApplied: false, + issuesCount: { + [IssuableStates.Opened]: 1, + [IssuableStates.Closed]: 1, + }, + }); + + await wrapper.vm.$nextTick(); + + expect(emptyStateEl.props('title')).toBe('There are no open issues'); + }); + + it('renders empty state description', () => { + const descriptionEl = wrapper.find(GlSprintf); + + expect(descriptionEl.exists()).toBe(true); + expect(descriptionEl.attributes('message')).toBe( + 'To keep this project going, create a new issue.', + ); + }); + + it('does not render empty state description when issues are present', async () => { + wrapper.setProps({ + issuesCount: { + [IssuableStates.Opened]: 1, + [IssuableStates.Closed]: 1, + }, + }); + + await wrapper.vm.$nextTick(); + + const descriptionEl = wrapper.find(GlSprintf); + + expect(descriptionEl.exists()).toBe(false); + }); + + it('renders "Create new issue in Zentao" button', () => { + const buttonEl = wrapper.find(GlButton); + + expect(buttonEl.exists()).toBe(true); + expect(buttonEl.attributes('href')).toBe(mockProvide.issueCreateUrl); + expect(buttonEl.text()).toBe('Create new issue in Zentao'); + }); + }); +}); diff --git a/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_root_spec.js b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_root_spec.js new file mode 100644 index 00000000000000..429ea58374ed99 --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_root_spec.js @@ -0,0 +1,330 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import VueApollo from 'vue-apollo'; + +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; + +import createFlash from '~/flash'; +import ZentaoIssuesListRoot from '~/integrations/zentao/issues_list/components/zentao_issues_list_root.vue'; +import { ISSUES_LIST_FETCH_ERROR } from '~/integrations/zentao/issues_list/constants'; +import zentaoIssues from '~/integrations/zentao/issues_list/graphql/resolvers/zentao_issues'; + +import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; +import axios from '~/lib/utils/axios_utils'; +import httpStatus from '~/lib/utils/http_status'; + +import { mockProvide, mockZentaoIssues } from '../mock_data'; + +jest.mock('~/flash'); +jest.mock('~/issuable_list/constants', () => ({ + DEFAULT_PAGE_SIZE: 2, + IssuableStates: jest.requireActual('~/issuable_list/constants').IssuableStates, + IssuableListTabs: jest.requireActual('~/issuable_list/constants').IssuableListTabs, + AvailableSortOptions: jest.requireActual('~/issuable_list/constants').AvailableSortOptions, +})); +jest.mock( + '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue', + () => 'LabelTokenMock', +); + +const resolvedValue = { + headers: { + 'x-page': 1, + 'x-total': mockZentaoIssues.length, + }, + data: mockZentaoIssues, +}; + +const localVue = createLocalVue(); + +const resolvers = { + Query: { + zentaoIssues, + }, +}; + +function createMockApolloProvider(mockResolvers = resolvers) { + localVue.use(VueApollo); + return createMockApollo([], mockResolvers); +} + +describe('ZentaoIssuesListRoot', () => { + let wrapper; + let mock; + + const mockSearchTerm = 'test issue'; + const mockLabel = 'ecosystem'; + + const findIssuableList = () => wrapper.findComponent(IssuableList); + const createLabelFilterEvent = (data) => ({ type: 'labels', value: { data } }); + const createSearchFilterEvent = (data) => ({ type: 'filtered-search-term', value: { data } }); + + const createComponent = ({ + apolloProvider = createMockApolloProvider(), + provide = mockProvide, + initialFilterParams = {}, + } = {}) => { + wrapper = shallowMount(ZentaoIssuesListRoot, { + propsData: { + initialFilterParams, + }, + provide, + localVue, + apolloProvider, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + describe('while loading', () => { + it('sets issuesListLoading to `true`', async () => { + jest.spyOn(axios, 'get').mockResolvedValue(new Promise(() => {})); + + createComponent(); + await wrapper.vm.$nextTick(); + + const issuableList = findIssuableList(); + expect(issuableList.props('issuablesLoading')).toBe(true); + }); + + it('calls `axios.get` with `issuesFetchPath` and query params', async () => { + jest.spyOn(axios, 'get'); + + createComponent(); + await waitForPromises(); + + expect(axios.get).toHaveBeenCalledWith( + mockProvide.issuesFetchPath, + expect.objectContaining({ + params: { + page: wrapper.vm.currentPage, + limit: wrapper.vm.$options.defaultPageSize, + state: wrapper.vm.currentState, + sort: wrapper.vm.sortedBy, + search: wrapper.vm.filterParams.search, + }, + }), + ); + }); + }); + + describe('with `initialFilterParams` prop', () => { + beforeEach(async () => { + jest.spyOn(axios, 'get').mockResolvedValue(resolvedValue); + + createComponent({ + initialFilterParams: { + labels: [mockLabel], + search: mockSearchTerm, + }, + }); + await waitForPromises(); + }); + + it('renders issuable-list component with correct props', () => { + const issuableList = findIssuableList(); + + expect(issuableList.props('initialFilterValue')).toEqual([ + { type: 'labels', value: { data: mockLabel } }, + { type: 'filtered-search-term', value: { data: mockSearchTerm } }, + ]); + expect(issuableList.props('urlParams').search).toBe(mockSearchTerm); + }); + }); + + describe('when request succeeds', () => { + beforeEach(async () => { + jest.spyOn(axios, 'get').mockResolvedValue(resolvedValue); + + createComponent(); + await waitForPromises(); + }); + + it('renders issuable-list component with correct props', () => { + const issuableList = findIssuableList(); + expect(issuableList.exists()).toBe(true); + expect(issuableList.props()).toMatchSnapshot(); + }); + + describe('issuable-list events', () => { + it('"click-tab" event executes GET request correctly', async () => { + const issuableList = findIssuableList(); + + issuableList.vm.$emit('click-tab', 'closed'); + await waitForPromises(); + + expect(axios.get).toHaveBeenCalledWith(mockProvide.issuesFetchPath, { + params: { + labels: undefined, + page: 1, + limit: 2, + search: undefined, + sort: 'created_desc', + state: 'closed', + }, + }); + expect(issuableList.props('currentTab')).toBe('closed'); + }); + + it('"page-change" event executes GET request correctly', async () => { + const mockPage = 2; + const issuableList = findIssuableList(); + jest.spyOn(axios, 'get').mockResolvedValue({ + ...resolvedValue, + headers: { 'x-page': mockPage, 'x-total': mockZentaoIssues.length }, + }); + + issuableList.vm.$emit('page-change', mockPage); + await waitForPromises(); + + expect(axios.get).toHaveBeenCalledWith(mockProvide.issuesFetchPath, { + params: { + labels: undefined, + page: mockPage, + limit: 2, + search: undefined, + sort: 'created_desc', + state: 'opened', + }, + }); + + await wrapper.vm.$nextTick(); + expect(issuableList.props()).toMatchObject({ + currentPage: mockPage, + previousPage: mockPage - 1, + nextPage: mockPage + 1, + }); + }); + + it('"sort" event executes GET request correctly', async () => { + const mockSortBy = 'updated_asc'; + const issuableList = findIssuableList(); + + issuableList.vm.$emit('sort', mockSortBy); + await waitForPromises(); + + expect(axios.get).toHaveBeenCalledWith(mockProvide.issuesFetchPath, { + params: { + labels: undefined, + page: 1, + limit: 2, + search: undefined, + sort: 'created_desc', + state: 'opened', + }, + }); + expect(issuableList.props('initialSortBy')).toBe(mockSortBy); + }); + + it.each` + desc | input | expected + ${'with label and search'} | ${[createLabelFilterEvent(mockLabel), createSearchFilterEvent(mockSearchTerm)]} | ${{ labels: [mockLabel], search: mockSearchTerm }} + ${'with multiple lables'} | ${[createLabelFilterEvent('label1'), createLabelFilterEvent('label2')]} | ${{ labels: ['label1', 'label2'], search: undefined }} + ${'with multiple searches'} | ${[createSearchFilterEvent('foo bar'), createSearchFilterEvent('lorem')]} | ${{ labels: undefined, search: 'foo bar lorem' }} + `( + '$desc, filter event sets "filterParams" value and calls fetchIssues', + async ({ input, expected }) => { + const issuableList = findIssuableList(); + + issuableList.vm.$emit('filter', input); + await waitForPromises(); + + expect(axios.get).toHaveBeenCalledWith(mockProvide.issuesFetchPath, { + params: { + page: 1, + limit: 2, + sort: 'created_desc', + state: 'opened', + ...expected, + }, + }); + }, + ); + }); + }); + + describe('error handling', () => { + describe('when request fails', () => { + it.each` + APIErrors | expectedRenderedErrorMessage + ${['API error']} | ${'API error'} + ${undefined} | ${ISSUES_LIST_FETCH_ERROR} + `( + 'calls `createFlash` with "$expectedRenderedErrorMessage" when API responds with "$APIErrors"', + async ({ APIErrors, expectedRenderedErrorMessage }) => { + jest.spyOn(axios, 'get'); + mock + .onGet(mockProvide.issuesFetchPath) + .replyOnce(httpStatus.INTERNAL_SERVER_ERROR, { errors: APIErrors }); + + createComponent(); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith({ + message: expectedRenderedErrorMessage, + captureError: true, + error: expect.any(Object), + }); + }, + ); + }); + + describe('when GraphQL network error is encountered', () => { + it('calls `createFlash` correctly with default error message', async () => { + createComponent({ + apolloProvider: createMockApolloProvider({ + Query: { + zentaoIssues: jest.fn().mockRejectedValue(new Error('GraphQL networkError')), + }, + }), + }); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith({ + message: ISSUES_LIST_FETCH_ERROR, + captureError: true, + error: expect.any(Object), + }); + }); + }); + }); + + describe('pagination', () => { + it.each` + scenario | issuesListLoadFailed | issues | shouldShowPaginationControls + ${'fails'} | ${true} | ${[]} | ${false} + ${'returns no issues'} | ${false} | ${[]} | ${false} + ${`returns some issues`} | ${false} | ${mockZentaoIssues} | ${true} + `( + 'sets `showPaginationControls` prop to $shouldShowPaginationControls when request $scenario', + async ({ issuesListLoadFailed, issues, shouldShowPaginationControls }) => { + jest.spyOn(axios, 'get'); + mock + .onGet(mockProvide.issuesFetchPath) + .replyOnce( + issuesListLoadFailed ? httpStatus.INTERNAL_SERVER_ERROR : httpStatus.OK, + issues, + { + 'x-page': 1, + 'x-total': issues.length, + }, + ); + + createComponent(); + await waitForPromises(); + + expect(findIssuableList().props('showPaginationControls')).toBe( + shouldShowPaginationControls, + ); + }, + ); + }); +}); diff --git a/spec/frontend/integrations/zentao/issues_list/mock_data.js b/spec/frontend/integrations/zentao/issues_list/mock_data.js new file mode 100644 index 00000000000000..7a56163c6ab73a --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_list/mock_data.js @@ -0,0 +1,93 @@ +export const mockProvide = { + initialState: 'opened', + initialSortBy: 'created_desc', + page: 1, + issuesFetchPath: '/gitlab-org/gitlab-test/-/integrations/zentao/issues.json', + projectFullPath: 'gitlab-org/gitlab-test', + issueCreateUrl: 'https://gitlab-zentao.atlassian.net/secure/CreateIssue!default.jspa', + emptyStatePath: '/assets/illustrations/issues.svg', +}; + +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: '', + references: { + relative: 'IG-31596', + }, + external_tracker: 'zentao', +}; + +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: '', + references: { + relative: 'IG-31595', + }, + external_tracker: 'zentao', +}; + +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: '', + references: { + relative: 'IG-31594', + }, + external_tracker: 'zentao', +}; + +export const mockZentaoIssues = [mockZentaoIssue1, mockZentaoIssue2, mockZentaoIssue3]; diff --git a/spec/frontend/integrations/zentao/issues_show/components/sidebar/__snapshots__/assignee_spec.js.snap b/spec/frontend/integrations/zentao/issues_show/components/sidebar/__snapshots__/assignee_spec.js.snap new file mode 100644 index 00000000000000..1a8829cc135ce6 --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_show/components/sidebar/__snapshots__/assignee_spec.js.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ZentaoIssuesSidebarAssignee with assignee template renders avatar components 1`] = ` +
+
+ + + + + +
+ + +
+`; + +exports[`ZentaoIssuesSidebarAssignee with no assignee template renders template without avatar components (the "None" state) 1`] = ` +
+
+ + + + None + +
+ + +
+`; diff --git a/spec/frontend/integrations/zentao/issues_show/components/sidebar/assignee_spec.js b/spec/frontend/integrations/zentao/issues_show/components/sidebar/assignee_spec.js new file mode 100644 index 00000000000000..4d2ec1531fb359 --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_show/components/sidebar/assignee_spec.js @@ -0,0 +1,121 @@ +import { GlAvatarLabeled, GlAvatarLink, GlAvatar } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import Assignee from '~/integrations/zentao/issues_show/components/sidebar/assignee.vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue'; + +import { mockZentaoIssue } from '../../mock_data'; + +const mockAssignee = convertObjectPropsToCamelCase(mockZentaoIssue.assignees[0], { deep: true }); + +describe('ZentaoIssuesSidebarAssignee', () => { + let wrapper; + + const findNoAssigneeText = () => wrapper.findByTestId('no-assignee-text'); + const findNoAssigneeIcon = () => wrapper.findByTestId('no-assignee-text'); + const findAvatar = () => wrapper.find(GlAvatar); + const findAvatarLabeled = () => wrapper.find(GlAvatarLabeled); + const findAvatarLink = () => wrapper.find(GlAvatarLink); + const findSidebarCollapsedIconWrapper = () => + wrapper.findByTestId('sidebar-collapsed-icon-wrapper'); + + const createComponent = ({ assignee } = {}) => { + wrapper = extendedWrapper( + shallowMount(Assignee, { + propsData: { + assignee, + }, + }), + ); + }; + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + describe('with assignee', () => { + beforeEach(() => { + createComponent({ assignee: mockAssignee }); + }); + + describe('template', () => { + it('renders avatar components', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('renders GlAvatarLink with correct props', () => { + const avatarLink = findAvatarLink(); + + expect(avatarLink.exists()).toBe(true); + expect(avatarLink.attributes()).toMatchObject({ + href: mockAssignee.webUrl, + title: mockAssignee.name, + }); + }); + + it('renders GlAvatarLabeled with correct props', () => { + const avatarLabeled = findAvatarLabeled(); + + expect(avatarLabeled.exists()).toBe(true); + expect(avatarLabeled.attributes()).toMatchObject({ + src: mockAssignee.avatarUrl, + alt: mockAssignee.name, + 'entity-name': mockAssignee.name, + }); + expect(avatarLabeled.props('label')).toBe(mockAssignee.name); + }); + + it('renders GlAvatar with correct props', () => { + const avatar = findAvatar(); + + expect(avatar.exists()).toBe(true); + expect(avatar.attributes()).toMatchObject({ + src: mockAssignee.avatarUrl, + alt: mockAssignee.name, + }); + expect(avatar.props('entityName')).toBe(mockAssignee.name); + }); + + it('renders AssigneeTitle with correct props', () => { + const title = wrapper.find(AssigneeTitle); + + expect(title.exists()).toBe(true); + expect(title.props('numberOfAssignees')).toBe(1); + }); + + it('does not render "No assignee" text', () => { + expect(findNoAssigneeText().exists()).toBe(false); + }); + + it('does not render "No assignee" icon', () => { + expect(findNoAssigneeIcon().exists()).toBe(false); + }); + + it('sets `title` attribute of collapsed sidebar wrapper correctly', () => { + const iconWrapper = findSidebarCollapsedIconWrapper(); + expect(iconWrapper.attributes('title')).toBe(mockAssignee.name); + }); + }); + }); + + describe('with no assignee', () => { + beforeEach(() => { + createComponent({ assignee: undefined }); + }); + + describe('template', () => { + it('renders template without avatar components (the "None" state)', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('sets `title` attribute of collapsed sidebar wrapper correctly', () => { + const iconWrapper = findSidebarCollapsedIconWrapper(); + expect(iconWrapper.attributes('title')).toBe('No assignee'); + }); + }); + }); +}); diff --git a/spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_due_date_spec.js b/spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_due_date_spec.js new file mode 100644 index 00000000000000..ceaee1791b952e --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_due_date_spec.js @@ -0,0 +1,69 @@ +import { shallowMount } from '@vue/test-utils'; + +import { useFakeDate } from 'helpers/fake_date'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; + +import IssueDueDate from '~/integrations/zentao/issues_show/components/sidebar/issue_due_date.vue'; + +describe('IssueDueDate', () => { + let wrapper; + + const createComponent = ({ props = {} } = {}) => { + wrapper = extendedWrapper( + shallowMount(IssueDueDate, { + propsData: props, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findDueDateCollapsed = () => wrapper.findByTestId('due-date-collapsed'); + const findDueDateValue = () => wrapper.findByTestId('due-date-value'); + + describe('when dueDate is null', () => { + it('renders "None" as value', () => { + createComponent(); + + expect(findDueDateCollapsed().text()).toBe('None'); + expect(findDueDateValue().text()).toBe('None'); + }); + }); + + describe('when dueDate is in the past', () => { + const dueDate = '2021-02-14T00:00:00.000Z'; + + useFakeDate(2021, 2, 18); + + it('renders formatted dueDate', () => { + createComponent({ + props: { + dueDate, + }, + }); + + expect(findDueDateCollapsed().text()).toBe('Feb 14, 2021'); + expect(findDueDateValue().text()).toBe('Feb 14, 2021 (Past due)'); + }); + }); + + describe('when dueDate is in the future', () => { + const dueDate = '2021-02-14T00:00:00.000Z'; + + useFakeDate(2020, 12, 20); + + it('renders formatted dueDate', () => { + createComponent({ + props: { + dueDate, + }, + }); + + expect(findDueDateCollapsed().text()).toBe('Feb 14, 2021'); + expect(findDueDateValue().text()).toBe('Feb 14, 2021'); + }); + }); +}); diff --git a/spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_field_dropdown_spec.js b/spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_field_dropdown_spec.js new file mode 100644 index 00000000000000..7407afe787e997 --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_field_dropdown_spec.js @@ -0,0 +1,57 @@ +import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; + +import IssueFieldDropdown from '~/integrations/zentao/issues_show/components/sidebar/issue_field_dropdown.vue'; + +import { mockZentaoIssueStatuses } from '../../mock_data'; + +describe('IssueFieldDropdown', () => { + let wrapper; + + const emptyText = 'empty text'; + const defaultProps = { + emptyText, + text: 'issue field text', + title: 'issue field header text', + }; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(IssueFieldDropdown, { + propsData: { ...defaultProps, ...props }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findAllGlDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + + it.each` + loading | items + ${true} | ${[]} + ${true} | ${mockZentaoIssueStatuses} + ${false} | ${[]} + ${false} | ${mockZentaoIssueStatuses} + `('with loading = $loading, items = $items', ({ loading, items }) => { + createComponent({ + props: { + loading, + items, + }, + }); + + expect(findGlLoadingIcon().exists()).toBe(loading); + + if (!loading) { + if (items.length) { + findAllGlDropdownItems().wrappers.forEach((itemWrapper, index) => { + expect(itemWrapper.text()).toBe(mockZentaoIssueStatuses[index].title); + }); + } else { + expect(wrapper.text()).toBe(emptyText); + } + } + }); +}); diff --git a/spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_field_spec.js b/spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_field_spec.js new file mode 100644 index 00000000000000..d32cfded353b4c --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_show/components/sidebar/issue_field_spec.js @@ -0,0 +1,117 @@ +import { GlButton, GlIcon } from '@gitlab/ui'; + +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import IssueField from '~/integrations/zentao/issues_show/components/sidebar/issue_field.vue'; + +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; + +describe('IssueField', () => { + let wrapper; + + const defaultProps = { + icon: 'calendar', + title: 'Field Title', + }; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMountExtended(IssueField, { + directives: { + GlTooltip: createMockDirective(), + }, + propsData: { ...defaultProps, ...props }, + stubs: { + SidebarEditableItem, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findEditableItem = () => wrapper.findComponent(SidebarEditableItem); + const findEditButton = () => wrapper.findComponent(GlButton); + const findFieldCollapsed = () => wrapper.findByTestId('field-collapsed'); + const findFieldCollapsedTooltip = () => getBinding(findFieldCollapsed().element, 'gl-tooltip'); + const findFieldValue = () => wrapper.findByTestId('field-value'); + const findGlIcon = () => wrapper.findComponent(GlIcon); + + describe('template', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders title', () => { + expect(findEditableItem().props('title')).toBe(defaultProps.title); + }); + + it('renders GlIcon (when collapsed)', () => { + expect(findGlIcon().props('name')).toBe(defaultProps.icon); + }); + + it('does not render "Edit" button', () => { + expect(findEditButton().exists()).toBe(false); + }); + }); + + describe('without value prop', () => { + beforeEach(() => { + createComponent(); + }); + + it('falls back to "None"', () => { + expect(findFieldValue().text()).toBe('None'); + }); + + it('renders tooltip (when collapsed) with "value" = title', () => { + const tooltip = findFieldCollapsedTooltip(); + + expect(tooltip).toBeDefined(); + expect(tooltip.value.title).toBe(defaultProps.title); + }); + }); + + describe('with value prop', () => { + const value = 'field value'; + + beforeEach(() => { + createComponent({ + props: { value }, + }); + }); + + it('renders the value', () => { + expect(findFieldValue().text()).toBe(value); + }); + + it('renders tooltip (when collapsed) with "value" = value', () => { + const tooltip = findFieldCollapsedTooltip(); + + expect(tooltip).toBeDefined(); + expect(tooltip.value.title).toBe(value); + }); + }); + + describe('with canUpdate = true', () => { + beforeEach(() => { + createComponent({ + props: { canUpdate: true }, + }); + }); + + it('renders "Edit" button', () => { + expect(findEditButton().text()).toBe('Edit'); + }); + + it('emits "issue-field-fetch" when dropdown is opened', () => { + wrapper.vm.$refs.dropdown.showDropdown = jest.fn(); + + findEditableItem().vm.$emit('open'); + + expect(wrapper.vm.$refs.dropdown.showDropdown).toHaveBeenCalled(); + expect(wrapper.emitted('issue-field-fetch')).toHaveLength(1); + }); + }); +}); diff --git a/spec/frontend/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root_spec.js b/spec/frontend/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root_spec.js new file mode 100644 index 00000000000000..78e35d46eacdef --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root_spec.js @@ -0,0 +1,63 @@ +import { shallowMount } from '@vue/test-utils'; +import Assignee from '~/integrations/zentao/issues_show/components/sidebar/assignee.vue'; +import IssueDueDate from '~/integrations/zentao/issues_show/components/sidebar/issue_due_date.vue'; +import IssueField from '~/integrations/zentao/issues_show/components/sidebar/issue_field.vue'; +import Sidebar from '~/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; +import { mockZentaoIssue as mockZentaoIssueData } from '../../mock_data'; + +const mockZentaoIssue = convertObjectPropsToCamelCase(mockZentaoIssueData, { deep: true }); + +describe('ZentaoIssuesSidebar', () => { + let wrapper; + + const defaultProps = { + sidebarExpanded: false, + issue: mockZentaoIssue, + }; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(Sidebar, { + propsData: { ...defaultProps, ...props }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findLabelsSelect = () => wrapper.findComponent(LabelsSelect); + const findAssignee = () => wrapper.findComponent(Assignee); + const findIssueDueDate = () => wrapper.findComponent(IssueDueDate); + const findIssueField = () => wrapper.findComponent(IssueField); + + it('renders Labels block', () => { + createComponent(); + + expect(findLabelsSelect().props('selectedLabels')).toBe(mockZentaoIssue.labels); + }); + + it('renders Assignee block', () => { + createComponent(); + const assignee = findAssignee(); + + expect(assignee.props('assignee')).toBe(mockZentaoIssue.assignees[0]); + }); + + it('renders IssueDueDate', () => { + createComponent(); + const dueDate = findIssueDueDate(); + + expect(dueDate.props('dueDate')).toBe(mockZentaoIssue.dueDate); + }); + + it('renders IssueField', () => { + createComponent(); + const field = findIssueField(); + + expect(field.props('icon')).toBe('progress'); + expect(field.props('title')).toBe('Status'); + expect(field.props('value')).toBe(mockZentaoIssue.status); + }); +}); diff --git a/spec/frontend/integrations/zentao/issues_show/components/zentao_issues_show_root_spec.js b/spec/frontend/integrations/zentao/issues_show/components/zentao_issues_show_root_spec.js new file mode 100644 index 00000000000000..8a804cfb70b255 --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_show/components/zentao_issues_show_root_spec.js @@ -0,0 +1,181 @@ +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; + +import waitForPromises from 'helpers/wait_for_promises'; +import * as ZentaoIssuesShowApi from '~/integrations/zentao/issues_show/api'; +import ZentaoIssueSidebar from '~/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue'; +import ZentaoIssuesShow from '~/integrations/zentao/issues_show/components/zentao_issues_show_root.vue'; +import { issueStates } from '~/integrations/zentao/issues_show/constants'; +import IssuableHeader from '~/issuable_show/components/issuable_header.vue'; +import IssuableShow from '~/issuable_show/components/issuable_show_root.vue'; +import IssuableSidebar from '~/issuable_sidebar/components/issuable_sidebar_root.vue'; +import axios from '~/lib/utils/axios_utils'; +import { mockZentaoIssue } from '../mock_data'; + +const mockZentaoIssuesShowPath = 'zentao_issues_show_path'; + +describe('ZentaoIssuesShow', () => { + let wrapper; + let mockAxios; + + const findGlAlert = () => wrapper.findComponent(GlAlert); + const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findIssuableShow = () => wrapper.findComponent(IssuableShow); + const findZentaoIssueSidebar = () => wrapper.findComponent(ZentaoIssueSidebar); + const findIssuableShowStatusBadge = () => + wrapper.findComponent(IssuableHeader).find('[data-testid="status"]'); + + const createComponent = () => { + wrapper = shallowMount(ZentaoIssuesShow, { + stubs: { + IssuableHeader, + IssuableShow, + IssuableSidebar, + }, + provide: { + issuesShowPath: mockZentaoIssuesShowPath, + }, + }); + }; + + beforeEach(() => { + mockAxios = new MockAdapter(axios); + }); + + afterEach(() => { + mockAxios.restore(); + wrapper.destroy(); + }); + + describe('when issue is loading', () => { + it('renders GlLoadingIcon', () => { + createComponent(); + + expect(findGlLoadingIcon().exists()).toBe(true); + expect(findGlAlert().exists()).toBe(false); + expect(findIssuableShow().exists()).toBe(false); + }); + }); + + describe('when error occurs during fetch', () => { + it('renders error message', async () => { + mockAxios.onGet(mockZentaoIssuesShowPath).replyOnce(500); + createComponent(); + + await waitForPromises(); + + const alert = findGlAlert(); + + expect(findGlLoadingIcon().exists()).toBe(false); + expect(alert.exists()).toBe(true); + expect(alert.text()).toBe( + 'Failed to load Zentao issue. View the issue in Zentao, or reload the page.', + ); + expect(alert.props('variant')).toBe('danger'); + expect(findIssuableShow().exists()).toBe(false); + }); + }); + + it('renders IssuableShow', async () => { + mockAxios.onGet(mockZentaoIssuesShowPath).replyOnce(200, mockZentaoIssue); + createComponent(); + + await waitForPromises(); + + expect(findGlLoadingIcon().exists()).toBe(false); + expect(findIssuableShow().exists()).toBe(true); + }); + + describe.each` + state | statusIcon | statusBadgeClass | badgeText + ${issueStates.OPENED} | ${'issue-open-m'} | ${'status-box-open'} | ${'Open'} + ${issueStates.CLOSED} | ${'mobile-issue-close'} | ${'status-box-issue-closed'} | ${'Closed'} + `('when issue state is `$state`', ({ state, statusIcon, statusBadgeClass, badgeText }) => { + beforeEach(async () => { + mockAxios.onGet(mockZentaoIssuesShowPath).replyOnce(200, { ...mockZentaoIssue, state }); + createComponent(); + + await waitForPromises(); + }); + + it('sets `statusIcon` prop correctly', () => { + expect(findIssuableShow().props('statusIcon')).toBe(statusIcon); + }); + + it('sets `statusBadgeClass` prop correctly', () => { + expect(findIssuableShow().props('statusBadgeClass')).toBe(statusBadgeClass); + }); + + it('renders correct status badge text', () => { + expect(findIssuableShowStatusBadge().text()).toBe(badgeText); + }); + }); + + describe('ZentaoIssueSidebar events', () => { + beforeEach(async () => { + mockAxios.onGet(mockZentaoIssuesShowPath).replyOnce(200, mockZentaoIssue); + createComponent(); + + await waitForPromises(); + }); + + it('updates issue labels on issue-labels-updated', async () => { + const updateIssueSpy = jest.spyOn(ZentaoIssuesShowApi, 'updateIssue').mockResolvedValue(); + + const labels = [{ id: 'ecosystem' }]; + + findZentaoIssueSidebar().vm.$emit('issue-labels-updated', labels); + await wrapper.vm.$nextTick(); + + expect(updateIssueSpy).toHaveBeenCalledWith(expect.any(Object), { labels }); + expect(findZentaoIssueSidebar().props('isUpdatingLabels')).toBe(true); + + await waitForPromises(); + + expect(findZentaoIssueSidebar().props('isUpdatingLabels')).toBe(false); + }); + + it('fetches issue statuses on issue-status-fetch', async () => { + const fetchIssueStatusesSpy = jest + .spyOn(ZentaoIssuesShowApi, 'fetchIssueStatuses') + .mockResolvedValue(); + + findZentaoIssueSidebar().vm.$emit('issue-status-fetch'); + await wrapper.vm.$nextTick(); + + expect(fetchIssueStatusesSpy).toHaveBeenCalled(); + expect(findZentaoIssueSidebar().props('isLoadingStatus')).toBe(true); + + await waitForPromises(); + + expect(findZentaoIssueSidebar().props('isLoadingStatus')).toBe(false); + }); + + it('updates issue status on issue-status-updated', async () => { + const updateIssueSpy = jest.spyOn(ZentaoIssuesShowApi, 'updateIssue').mockResolvedValue(); + + const status = 'In Review'; + + findZentaoIssueSidebar().vm.$emit('issue-status-updated', status); + await wrapper.vm.$nextTick(); + + expect(updateIssueSpy).toHaveBeenCalledWith(expect.any(Object), { status }); + expect(findZentaoIssueSidebar().props('isUpdatingStatus')).toBe(true); + + await waitForPromises(); + + expect(findZentaoIssueSidebar().props('isUpdatingStatus')).toBe(false); + }); + + it('updates `sidebarExpanded` prop on `sidebar-toggle` event', async () => { + const zentaoIssueSidebar = findZentaoIssueSidebar(); + expect(zentaoIssueSidebar.props('sidebarExpanded')).toBe(true); + + zentaoIssueSidebar.vm.$emit('sidebar-toggle'); + await wrapper.vm.$nextTick(); + + expect(zentaoIssueSidebar.props('sidebarExpanded')).toBe(false); + }); + }); +}); diff --git a/spec/frontend/integrations/zentao/issues_show/mock_data.js b/spec/frontend/integrations/zentao/issues_show/mock_data.js new file mode 100644 index 00000000000000..8c7f9147c98ac3 --- /dev/null +++ b/spec/frontend/integrations/zentao/issues_show/mock_data.js @@ -0,0 +1,45 @@ +export const mockZentaoIssue = { + title: 'FE-2 The second FE issue on Zentao', + description_html: + 'FE-2 The second FE issue on Zentao', + created_at: '"2021-02-01T04:04:40.833Z"', + author: { + name: 'Justin Ho', + web_url: 'http://127.0.0.1:3000/root', + avatar_url: 'http://127.0.0.1:3000/uploads/-/system/user/avatar/1/avatar.png?width=90', + }, + assignees: [ + { + name: 'Justin Ho', + web_url: 'http://127.0.0.1:3000/root', + avatar_url: 'http://127.0.0.1:3000/uploads/-/system/user/avatar/1/avatar.png?width=90', + }, + ], + due_date: '2021-02-14T00:00:00.000Z', + labels: [ + { + title: 'In Progress', + description: 'Work that is still in progress', + color: '#0052CC', + text_color: '#FFFFFF', + }, + ], + references: { + relative: 'FE-2', + }, + state: 'opened', + status: 'In Progress', +}; + +export const mockZentaoIssueComment = { + body_html: '

hi

', + created_at: '"2021-02-01T04:04:40.833Z"', + author: { + name: 'Justin Ho', + web_url: 'http://127.0.0.1:3000/root', + avatar_url: 'http://127.0.0.1:3000/uploads/-/system/user/avatar/1/avatar.png?width=90', + }, + id: 10000, +}; + +export const mockZentaoIssueStatuses = [{ title: 'In Progress' }, { title: 'Done' }]; -- GitLab From dab739a29a776d31c9193c0f8c5b8ec403c53759 Mon Sep 17 00:00:00 2001 From: Baodong Date: Wed, 25 Aug 2021 14:57:47 +0800 Subject: [PATCH 2/7] Update gitlab.pot --- locale/gitlab.pot | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a43c54298db3cd..885275a0c1ed50 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18058,6 +18058,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 "" @@ -18151,6 +18154,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 "" @@ -18193,6 +18199,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 "" @@ -39084,15 +39093,42 @@ msgstr[1] "" msgid "Your username is %{username}." msgstr "" +msgid "Zentao issues" +msgstr "" + +msgid "Zentao user" +msgstr "" + msgid "ZentaoIntegration|Base URL of the Zentao instance." msgstr "" msgid "ZentaoIntegration|Enter API token" msgstr "" +msgid "ZentaoIntegration|Failed to load Zentao issue statuses. View the issue in Zentao, or reload the page." +msgstr "" + +msgid "ZentaoIntegration|Failed to load Zentao issue. View the issue in Zentao, or reload the page." +msgstr "" + +msgid "ZentaoIntegration|Failed to update Zentao issue labels. View the issue in Zentao, or reload the page." +msgstr "" + +msgid "ZentaoIntegration|Failed to update Zentao issue status. View the issue in Zentao, or reload the page." +msgstr "" + msgid "ZentaoIntegration|If different from Web URL." msgstr "" +msgid "ZentaoIntegration|No available statuses" +msgstr "" + +msgid "ZentaoIntegration|Not all data may be displayed here. To view more details or make changes to this issue, go to %{linkStart}Zentao%{linkEnd}." +msgstr "" + +msgid "ZentaoIntegration|This issue is synchronized with Zentao" +msgstr "" + msgid "ZentaoIntegration|Use Zentao as this project's issue tracker." msgstr "" -- GitLab From d47669e6ee9978b52e5553857a8e590fc2c60f26 Mon Sep 17 00:00:00 2001 From: Baodong Date: Wed, 25 Aug 2021 16:00:08 +0800 Subject: [PATCH 3/7] Fix display texts --- .../components/zentao_issues_list_empty_state_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js index f1d17cf4b11e46..ebb19c559c9972 100644 --- a/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js +++ b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js @@ -24,7 +24,7 @@ const createComponent = (props = {}) => describe('ZentaoIssuesListEmptyState', () => { const titleDefault = - 'Issues created in Zentao are shown here once you have created the issues in project setup in Zentao.'; + 'Zentao issues display here when you create issues in your project in Zentao.'; const titleWhenFilters = 'Sorry, your filter produced no results'; const titleWhenIssues = 'There are no open issues'; @@ -144,7 +144,7 @@ describe('ZentaoIssuesListEmptyState', () => { expect(emptyStateEl.props()).toMatchObject({ svgPath: mockProvide.emptyStatePath, title: - 'Issues created in Zentao are shown here once you have created the issues in project setup in Zentao.', + 'Zentao issues display here when you create issues in your project in Zentao.', }); wrapper.setProps({ -- GitLab From f3e5c9c6219e04bd983afa371cc16796d32d910e Mon Sep 17 00:00:00 2001 From: Baodong Date: Thu, 26 Aug 2021 09:27:27 +0800 Subject: [PATCH 4/7] Fix for prettier --- .../components/zentao_issues_list_empty_state_spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js index ebb19c559c9972..5064d2679f8cdb 100644 --- a/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js +++ b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js @@ -143,8 +143,7 @@ describe('ZentaoIssuesListEmptyState', () => { expect(emptyStateEl.props()).toMatchObject({ svgPath: mockProvide.emptyStatePath, - title: - 'Zentao issues display here when you create issues in your project in Zentao.', + title: 'Zentao issues display here when you create issues in your project in Zentao.', }); wrapper.setProps({ -- GitLab From 73ba03980d9d2cd973b15426a5c31427e037f7bc Mon Sep 17 00:00:00 2001 From: JeremyWuuuuu Date: Thu, 2 Sep 2021 19:29:21 +0800 Subject: [PATCH 5/7] Extracting common code for Jira and Zentao list --- .../external_issues_list_empty_state.vue} | 9 +- .../components/external_issues_list_root.vue} | 44 +-- .../external_issue/issues_list/constants.js | 3 + .../issues_list/external_issue_list_bundle.js | 66 +++++ .../issues_list/graphql/index.js | 19 ++ .../zentao/issues_list/graphql/index.js | 18 -- .../queries/get_zentao_issues.query.graphql | 4 +- .../issues_list/zentao_issues_list_bundle.js | 68 ++--- .../jira_issues_list_empty_state.vue | 71 ----- .../components/jira_issues_list_root.vue | 278 ------------------ .../jira/issues_list/graphql/index.js | 18 -- .../queries/get_jira_issues.query.graphql | 4 +- .../issues_list/jira_issues_list_bundle.js | 68 ++--- .../jira_issues_list_empty_state_spec.js | 2 +- .../components/jira_issues_list_root_spec.js | 9 +- .../jira/issues_list/mock_data.js | 13 + .../zentao_issues_list_empty_state_spec.js | 2 +- .../zentao_issues_list_root_spec.js | 8 +- .../zentao/issues_list/mock_data.js | 13 + 19 files changed, 201 insertions(+), 516 deletions(-) rename app/assets/javascripts/integrations/{zentao/issues_list/components/zentao_issues_list_empty_state.vue => external_issue/issues_list/components/external_issues_list_empty_state.vue} (88%) rename app/assets/javascripts/integrations/{zentao/issues_list/components/zentao_issues_list_root.vue => external_issue/issues_list/components/external_issues_list_root.vue} (86%) create mode 100644 app/assets/javascripts/integrations/external_issue/issues_list/constants.js create mode 100644 app/assets/javascripts/integrations/external_issue/issues_list/external_issue_list_bundle.js create mode 100644 app/assets/javascripts/integrations/external_issue/issues_list/graphql/index.js delete mode 100644 app/assets/javascripts/integrations/zentao/issues_list/graphql/index.js delete mode 100644 ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_empty_state.vue delete mode 100644 ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue delete mode 100644 ee/app/assets/javascripts/integrations/jira/issues_list/graphql/index.js diff --git a/app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_empty_state.vue b/app/assets/javascripts/integrations/external_issue/issues_list/components/external_issues_list_empty_state.vue similarity index 88% rename from app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_empty_state.vue rename to app/assets/javascripts/integrations/external_issue/issues_list/components/external_issues_list_empty_state.vue index 9a1ebdf320778d..264326fc081634 100644 --- a/app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_empty_state.vue +++ b/app/assets/javascripts/integrations/external_issue/issues_list/components/external_issues_list_empty_state.vue @@ -14,7 +14,8 @@ export default { GlIcon, GlSprintf, }, - inject: ['emptyStatePath', 'issueCreateUrl'], + // The text injected is sanitized. + inject: ['emptyStatePath', 'issueCreateUrl', 'emptyStateNoIssueText', 'createNewIssueText'], props: { currentState: { type: String, @@ -40,9 +41,7 @@ export default { } else if (this.hasIssues) { return this.$options.FilterStateEmptyMessage[this.currentState]; } - return s__( - 'Integrations|Zentao issues display here when you create issues in your project in Zentao.', - ); + return this.emptyStateNoIssueText; }, emptyStateDescription() { if (this.hasFiltersApplied) { @@ -63,7 +62,7 @@ export default { diff --git a/app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_root.vue b/app/assets/javascripts/integrations/external_issue/issues_list/components/external_issues_list_root.vue similarity index 86% rename from app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_root.vue rename to app/assets/javascripts/integrations/external_issue/issues_list/components/external_issues_list_root.vue index 023e9fc4e1aa1c..acb2a6c7833646 100644 --- a/app/assets/javascripts/integrations/zentao/issues_list/components/zentao_issues_list_root.vue +++ b/app/assets/javascripts/integrations/external_issue/issues_list/components/external_issues_list_root.vue @@ -1,7 +1,6 @@ - - diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue b/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue deleted file mode 100644 index 018e37d5b10e63..00000000000000 --- a/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue +++ /dev/null @@ -1,278 +0,0 @@ - - - diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/index.js b/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/index.js deleted file mode 100644 index 0c4f014e4ad1b2..00000000000000 --- a/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createDefaultClient from '~/lib/graphql'; -import jiraIssues from './resolvers/jira_issues'; - -Vue.use(VueApollo); - -const resolvers = { - Query: { - jiraIssues, - }, -}; - -const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true }); - -export default new VueApollo({ - defaultClient, -}); diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql b/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql index e4e55dbbbfe51a..9acbfb83156762 100644 --- a/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql +++ b/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql @@ -1,7 +1,7 @@ #import "../fragments/jira_label.fragment.graphql" #import "../fragments/jira_user.fragment.graphql" -query jiraIssues( +query externalIssues( $issuesFetchPath: String $search: String $labels: String @@ -9,7 +9,7 @@ query jiraIssues( $state: String $page: Integer ) { - jiraIssues( + externalIssues( issuesFetchPath: $issuesFetchPath search: $search labels: $labels diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/jira_issues_list_bundle.js b/ee/app/assets/javascripts/integrations/jira/issues_list/jira_issues_list_bundle.js index 60e0b2e75c214d..7297d1b84c5a83 100644 --- a/ee/app/assets/javascripts/integrations/jira/issues_list/jira_issues_list_bundle.js +++ b/ee/app/assets/javascripts/integrations/jira/issues_list/jira_issues_list_bundle.js @@ -1,48 +1,24 @@ -import Vue from 'vue'; +import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg'; +import externalIssuesListFactory from '~/integrations/external_issue/issues_list/external_issue_list_bundle'; +import { s__ } from '~/locale'; +import getIssuesQuery from './graphql/queries/get_jira_issues.query.graphql'; +import jiraIssues from './graphql/resolvers/jira_issues'; -import { IssuableStates } from '~/issuable_list/constants'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { queryToObject } from '~/lib/utils/url_utility'; - -import JiraIssuesListApp from './components/jira_issues_list_root.vue'; -import apolloProvider from './graphql'; - -export default function initJiraIssuesList({ 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 - }, +export default externalIssuesListFactory({ + query: jiraIssues, + provides: { + getIssuesQuery, + externalIssuesLogo: jiraLogo, + // This like below is passed to + // So we don't translate it since this should be a proper noun + // eslint-disable-next-line @gitlab/require-i18n-strings + externalIssueName: 'Jira', + searchInputPlaceholderText: s__('Integrations|Search Jira issues'), + recentSearchesStorageKey: 'jira_issues', + createNewIssueText: s__('Integrations|Create new issue in Jira'), + logoContainerClass: 'jira-logo-container', + emptyStateNoIssueText: s__( + 'Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira.', ), - ); - - return new Vue({ - el: mountPointEl, - provide: { - ...mountPointEl.dataset, - page: parseInt(page, 10), - initialState, - initialSortBy, - }, - apolloProvider, - render: (createElement) => - createElement(JiraIssuesListApp, { - props: { - initialFilterParams, - }, - }), - }); -} + }, +}); diff --git a/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_empty_state_spec.js b/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_empty_state_spec.js index 03a1ca5390c5aa..aa1e038817acfe 100644 --- a/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_empty_state_spec.js +++ b/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_empty_state_spec.js @@ -1,7 +1,7 @@ import { GlEmptyState, GlSprintf, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import JiraIssuesListEmptyState from 'ee/integrations/jira/issues_list/components/jira_issues_list_empty_state.vue'; +import JiraIssuesListEmptyState from '~/integrations/external_issue/issues_list/components/external_issues_list_empty_state.vue'; import { IssuableStates } from '~/issuable_list/constants'; import { mockProvide } from '../mock_data'; diff --git a/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_root_spec.js b/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_root_spec.js index bf8ee08ad3ac2e..af1930e212c6de 100644 --- a/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_root_spec.js +++ b/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_root_spec.js @@ -2,7 +2,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import VueApollo from 'vue-apollo'; -import JiraIssuesListRoot from 'ee/integrations/jira/issues_list/components/jira_issues_list_root.vue'; import { ISSUES_LIST_FETCH_ERROR } from 'ee/integrations/jira/issues_list/constants'; import jiraIssues from 'ee/integrations/jira/issues_list/graphql/resolvers/jira_issues'; @@ -10,6 +9,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; +import ExternalIssuesListRoot from '~/integrations/external_issue/issues_list/components/external_issues_list_root.vue'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; @@ -40,7 +40,7 @@ const localVue = createLocalVue(); const resolvers = { Query: { - jiraIssues, + externalIssues: jiraIssues, }, }; @@ -65,7 +65,7 @@ describe('JiraIssuesListRoot', () => { provide = mockProvide, initialFilterParams = {}, } = {}) => { - wrapper = shallowMount(JiraIssuesListRoot, { + wrapper = shallowMount(ExternalIssuesListRoot, { propsData: { initialFilterParams, }, @@ -287,7 +287,7 @@ describe('JiraIssuesListRoot', () => { createComponent({ apolloProvider: createMockApolloProvider({ Query: { - jiraIssues: jest.fn().mockRejectedValue(new Error('GraphQL networkError')), + externalIssues: jest.fn().mockRejectedValue(new Error('GraphQL networkError')), }, }), }); @@ -325,7 +325,6 @@ describe('JiraIssuesListRoot', () => { createComponent(); await waitForPromises(); - expect(findIssuableList().props('showPaginationControls')).toBe( shouldShowPaginationControls, ); diff --git a/ee/spec/frontend/integrations/jira/issues_list/mock_data.js b/ee/spec/frontend/integrations/jira/issues_list/mock_data.js index 1e3d9be3caeac8..741aa2781544a5 100644 --- a/ee/spec/frontend/integrations/jira/issues_list/mock_data.js +++ b/ee/spec/frontend/integrations/jira/issues_list/mock_data.js @@ -1,3 +1,6 @@ +import getIssuesQuery from 'ee/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql'; +import { s__ } from '~/locale'; + export const mockProvide = { initialState: 'opened', initialSortBy: 'created_desc', @@ -6,6 +9,16 @@ export const mockProvide = { projectFullPath: 'gitlab-org/gitlab-test', issueCreateUrl: 'https://gitlab-jira.atlassian.net/secure/CreateIssue!default.jspa', emptyStatePath: '/assets/illustrations/issues.svg', + getIssuesQuery, + externalIssuesLogo: ``, + externalIssueName: 'Jira', + searchInputPlaceholderText: s__('Integrations|Search Jira issues'), + recentSearchesStorageKey: 'jira_issues', + createNewIssueText: s__('Integrations|Create new issue in Jira'), + logoContainerClass: 'logo-container', + emptyStateNoIssueText: s__( + 'Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira.', + ), }; export const mockJiraIssue1 = { diff --git a/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js index 5064d2679f8cdb..2ee7e10ae0550f 100644 --- a/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js +++ b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_empty_state_spec.js @@ -1,7 +1,7 @@ import { GlEmptyState, GlSprintf, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import ZentaoIssuesListEmptyState from '~/integrations/zentao/issues_list/components/zentao_issues_list_empty_state.vue'; +import ZentaoIssuesListEmptyState from '~/integrations/external_issue/issues_list/components/external_issues_list_empty_state.vue'; import { IssuableStates } from '~/issuable_list/constants'; import { mockProvide } from '../mock_data'; diff --git a/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_root_spec.js b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_root_spec.js index 429ea58374ed99..cc3e2053189ac3 100644 --- a/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_root_spec.js +++ b/spec/frontend/integrations/zentao/issues_list/components/zentao_issues_list_root_spec.js @@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; -import ZentaoIssuesListRoot from '~/integrations/zentao/issues_list/components/zentao_issues_list_root.vue'; +import ExternalIssuesListRoot from '~/integrations/external_issue/issues_list/components/external_issues_list_root.vue'; import { ISSUES_LIST_FETCH_ERROR } from '~/integrations/zentao/issues_list/constants'; import zentaoIssues from '~/integrations/zentao/issues_list/graphql/resolvers/zentao_issues'; @@ -40,7 +40,7 @@ const localVue = createLocalVue(); const resolvers = { Query: { - zentaoIssues, + externalIssues: zentaoIssues, }, }; @@ -65,7 +65,7 @@ describe('ZentaoIssuesListRoot', () => { provide = mockProvide, initialFilterParams = {}, } = {}) => { - wrapper = shallowMount(ZentaoIssuesListRoot, { + wrapper = shallowMount(ExternalIssuesListRoot, { propsData: { initialFilterParams, }, @@ -282,7 +282,7 @@ describe('ZentaoIssuesListRoot', () => { createComponent({ apolloProvider: createMockApolloProvider({ Query: { - zentaoIssues: jest.fn().mockRejectedValue(new Error('GraphQL networkError')), + externalIssues: jest.fn().mockRejectedValue(new Error('GraphQL networkError')), }, }), }); diff --git a/spec/frontend/integrations/zentao/issues_list/mock_data.js b/spec/frontend/integrations/zentao/issues_list/mock_data.js index 7a56163c6ab73a..f29efd472ab6fa 100644 --- a/spec/frontend/integrations/zentao/issues_list/mock_data.js +++ b/spec/frontend/integrations/zentao/issues_list/mock_data.js @@ -1,3 +1,6 @@ +import getIssuesQuery from '~/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql'; +import { s__ } from '~/locale'; + export const mockProvide = { initialState: 'opened', initialSortBy: 'created_desc', @@ -6,6 +9,16 @@ export const mockProvide = { projectFullPath: 'gitlab-org/gitlab-test', issueCreateUrl: 'https://gitlab-zentao.atlassian.net/secure/CreateIssue!default.jspa', emptyStatePath: '/assets/illustrations/issues.svg', + getIssuesQuery, + externalIssuesLogo: ``, + externalIssueName: '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.', + ), }; export const mockZentaoIssue1 = { -- GitLab From 0bb5b3a1143daf87d83f0cb403eba6d20f4deb73 Mon Sep 17 00:00:00 2001 From: JeremyWuuuuu Date: Fri, 3 Sep 2021 10:34:10 +0800 Subject: [PATCH 6/7] Revert changes applied to Jira issues --- .../jira_issues_list_empty_state.vue | 71 +++++ .../components/jira_issues_list_root.vue | 278 ++++++++++++++++++ .../jira/issues_list/graphql/index.js | 18 ++ .../queries/get_jira_issues.query.graphql | 4 +- .../issues_list/jira_issues_list_bundle.js | 68 +++-- .../jira_issues_list_empty_state_spec.js | 2 +- .../components/jira_issues_list_root_spec.js | 9 +- .../jira/issues_list/mock_data.js | 13 - 8 files changed, 421 insertions(+), 42 deletions(-) create mode 100644 ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_empty_state.vue create mode 100644 ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue create mode 100644 ee/app/assets/javascripts/integrations/jira/issues_list/graphql/index.js diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_empty_state.vue b/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_empty_state.vue new file mode 100644 index 00000000000000..90c08497c7df99 --- /dev/null +++ b/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_empty_state.vue @@ -0,0 +1,71 @@ + + + diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue b/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue new file mode 100644 index 00000000000000..018e37d5b10e63 --- /dev/null +++ b/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue @@ -0,0 +1,278 @@ + + + diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/index.js b/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/index.js new file mode 100644 index 00000000000000..0c4f014e4ad1b2 --- /dev/null +++ b/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/index.js @@ -0,0 +1,18 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import jiraIssues from './resolvers/jira_issues'; + +Vue.use(VueApollo); + +const resolvers = { + Query: { + jiraIssues, + }, +}; + +const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true }); + +export default new VueApollo({ + defaultClient, +}); diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql b/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql index 9acbfb83156762..e4e55dbbbfe51a 100644 --- a/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql +++ b/ee/app/assets/javascripts/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql @@ -1,7 +1,7 @@ #import "../fragments/jira_label.fragment.graphql" #import "../fragments/jira_user.fragment.graphql" -query externalIssues( +query jiraIssues( $issuesFetchPath: String $search: String $labels: String @@ -9,7 +9,7 @@ query externalIssues( $state: String $page: Integer ) { - externalIssues( + jiraIssues( issuesFetchPath: $issuesFetchPath search: $search labels: $labels diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/jira_issues_list_bundle.js b/ee/app/assets/javascripts/integrations/jira/issues_list/jira_issues_list_bundle.js index 7297d1b84c5a83..60e0b2e75c214d 100644 --- a/ee/app/assets/javascripts/integrations/jira/issues_list/jira_issues_list_bundle.js +++ b/ee/app/assets/javascripts/integrations/jira/issues_list/jira_issues_list_bundle.js @@ -1,24 +1,48 @@ -import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg'; -import externalIssuesListFactory from '~/integrations/external_issue/issues_list/external_issue_list_bundle'; -import { s__ } from '~/locale'; -import getIssuesQuery from './graphql/queries/get_jira_issues.query.graphql'; -import jiraIssues from './graphql/resolvers/jira_issues'; +import Vue from 'vue'; -export default externalIssuesListFactory({ - query: jiraIssues, - provides: { - getIssuesQuery, - externalIssuesLogo: jiraLogo, - // This like below is passed to - // So we don't translate it since this should be a proper noun - // eslint-disable-next-line @gitlab/require-i18n-strings - externalIssueName: 'Jira', - searchInputPlaceholderText: s__('Integrations|Search Jira issues'), - recentSearchesStorageKey: 'jira_issues', - createNewIssueText: s__('Integrations|Create new issue in Jira'), - logoContainerClass: 'jira-logo-container', - emptyStateNoIssueText: s__( - 'Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira.', +import { IssuableStates } from '~/issuable_list/constants'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { queryToObject } from '~/lib/utils/url_utility'; + +import JiraIssuesListApp from './components/jira_issues_list_root.vue'; +import apolloProvider from './graphql'; + +export default function initJiraIssuesList({ 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, + }, + apolloProvider, + render: (createElement) => + createElement(JiraIssuesListApp, { + props: { + initialFilterParams, + }, + }), + }); +} diff --git a/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_empty_state_spec.js b/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_empty_state_spec.js index aa1e038817acfe..03a1ca5390c5aa 100644 --- a/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_empty_state_spec.js +++ b/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_empty_state_spec.js @@ -1,7 +1,7 @@ import { GlEmptyState, GlSprintf, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import JiraIssuesListEmptyState from '~/integrations/external_issue/issues_list/components/external_issues_list_empty_state.vue'; +import JiraIssuesListEmptyState from 'ee/integrations/jira/issues_list/components/jira_issues_list_empty_state.vue'; import { IssuableStates } from '~/issuable_list/constants'; import { mockProvide } from '../mock_data'; diff --git a/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_root_spec.js b/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_root_spec.js index af1930e212c6de..bf8ee08ad3ac2e 100644 --- a/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_root_spec.js +++ b/ee/spec/frontend/integrations/jira/issues_list/components/jira_issues_list_root_spec.js @@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import VueApollo from 'vue-apollo'; +import JiraIssuesListRoot from 'ee/integrations/jira/issues_list/components/jira_issues_list_root.vue'; import { ISSUES_LIST_FETCH_ERROR } from 'ee/integrations/jira/issues_list/constants'; import jiraIssues from 'ee/integrations/jira/issues_list/graphql/resolvers/jira_issues'; @@ -9,7 +10,6 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; -import ExternalIssuesListRoot from '~/integrations/external_issue/issues_list/components/external_issues_list_root.vue'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; @@ -40,7 +40,7 @@ const localVue = createLocalVue(); const resolvers = { Query: { - externalIssues: jiraIssues, + jiraIssues, }, }; @@ -65,7 +65,7 @@ describe('JiraIssuesListRoot', () => { provide = mockProvide, initialFilterParams = {}, } = {}) => { - wrapper = shallowMount(ExternalIssuesListRoot, { + wrapper = shallowMount(JiraIssuesListRoot, { propsData: { initialFilterParams, }, @@ -287,7 +287,7 @@ describe('JiraIssuesListRoot', () => { createComponent({ apolloProvider: createMockApolloProvider({ Query: { - externalIssues: jest.fn().mockRejectedValue(new Error('GraphQL networkError')), + jiraIssues: jest.fn().mockRejectedValue(new Error('GraphQL networkError')), }, }), }); @@ -325,6 +325,7 @@ describe('JiraIssuesListRoot', () => { createComponent(); await waitForPromises(); + expect(findIssuableList().props('showPaginationControls')).toBe( shouldShowPaginationControls, ); diff --git a/ee/spec/frontend/integrations/jira/issues_list/mock_data.js b/ee/spec/frontend/integrations/jira/issues_list/mock_data.js index 741aa2781544a5..1e3d9be3caeac8 100644 --- a/ee/spec/frontend/integrations/jira/issues_list/mock_data.js +++ b/ee/spec/frontend/integrations/jira/issues_list/mock_data.js @@ -1,6 +1,3 @@ -import getIssuesQuery from 'ee/integrations/jira/issues_list/graphql/queries/get_jira_issues.query.graphql'; -import { s__ } from '~/locale'; - export const mockProvide = { initialState: 'opened', initialSortBy: 'created_desc', @@ -9,16 +6,6 @@ export const mockProvide = { projectFullPath: 'gitlab-org/gitlab-test', issueCreateUrl: 'https://gitlab-jira.atlassian.net/secure/CreateIssue!default.jspa', emptyStatePath: '/assets/illustrations/issues.svg', - getIssuesQuery, - externalIssuesLogo: ``, - externalIssueName: 'Jira', - searchInputPlaceholderText: s__('Integrations|Search Jira issues'), - recentSearchesStorageKey: 'jira_issues', - createNewIssueText: s__('Integrations|Create new issue in Jira'), - logoContainerClass: 'logo-container', - emptyStateNoIssueText: s__( - 'Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira.', - ), }; export const mockJiraIssue1 = { -- GitLab From 5a7f3ecc109d9a77ceea282ae8718e54ebe64fce Mon Sep 17 00:00:00 2001 From: JeremyWuuuuu Date: Mon, 6 Sep 2021 18:01:59 +0800 Subject: [PATCH 7/7] Extracting common issue_detail_show code. Update Text on Page accordingly from Zentao to ZenTao. --- .../issues_show/api.js | 0 .../components/external_issues_show_root.vue} | 97 ++++++++++++------- .../issues_show/components/note.vue | 0 .../components/sidebar/assignee.vue | 5 +- .../sidebar/external_issues_sidebar_root.vue} | 22 +++-- .../components/sidebar/issue_due_date.vue | 0 .../components/sidebar/issue_field.vue | 0 .../sidebar/issue_field_dropdown.vue | 0 .../issues_show/constants.js | 0 .../external_issues_show_bundle.js | 43 ++++++++ .../issues_show/zentao_issues_show_bundle.js | 46 +++++---- locale/gitlab.pot | 28 +++--- .../__snapshots__/assignee_spec.js.snap | 2 +- .../components/sidebar/assignee_spec.js | 7 +- .../components/sidebar/issue_due_date_spec.js | 2 +- .../sidebar/issue_field_dropdown_spec.js | 2 +- .../components/sidebar/issue_field_spec.js | 2 +- .../zentao_issues_sidebar_root_spec.js | 11 ++- .../zentao_issues_show_root_spec.js | 16 +-- .../zentao/issues_show/mock_data.js | 26 +++++ 20 files changed, 214 insertions(+), 95 deletions(-) rename app/assets/javascripts/integrations/{zentao => external_issue}/issues_show/api.js (100%) rename app/assets/javascripts/integrations/{zentao/issues_show/components/zentao_issues_show_root.vue => external_issue/issues_show/components/external_issues_show_root.vue} (65%) rename app/assets/javascripts/integrations/{zentao => external_issue}/issues_show/components/note.vue (100%) rename app/assets/javascripts/integrations/{zentao => external_issue}/issues_show/components/sidebar/assignee.vue (94%) rename app/assets/javascripts/integrations/{zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue => external_issue/issues_show/components/sidebar/external_issues_sidebar_root.vue} (88%) rename app/assets/javascripts/integrations/{zentao => external_issue}/issues_show/components/sidebar/issue_due_date.vue (100%) rename app/assets/javascripts/integrations/{zentao => external_issue}/issues_show/components/sidebar/issue_field.vue (100%) rename app/assets/javascripts/integrations/{zentao => external_issue}/issues_show/components/sidebar/issue_field_dropdown.vue (100%) rename app/assets/javascripts/integrations/{zentao => external_issue}/issues_show/constants.js (100%) create mode 100644 app/assets/javascripts/integrations/external_issue/issues_show/external_issues_show_bundle.js diff --git a/app/assets/javascripts/integrations/zentao/issues_show/api.js b/app/assets/javascripts/integrations/external_issue/issues_show/api.js similarity index 100% rename from app/assets/javascripts/integrations/zentao/issues_show/api.js rename to app/assets/javascripts/integrations/external_issue/issues_show/api.js diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue b/app/assets/javascripts/integrations/external_issue/issues_show/components/external_issues_show_root.vue similarity index 65% rename from app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue rename to app/assets/javascripts/integrations/external_issue/issues_show/components/external_issues_show_root.vue index b7a1a2cb98dc01..77d736fcc2fb24 100644 --- a/app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue +++ b/app/assets/javascripts/integrations/external_issue/issues_show/components/external_issues_show_root.vue @@ -4,34 +4,62 @@ import { GlSprintf, GlLink, GlLoadingIcon, + GlBadge, GlTooltipDirective as GlTooltip, } from '@gitlab/ui'; import createFlash from '~/flash'; -import { fetchIssue, fetchIssueStatuses, updateIssue } from '~/integrations/zentao/issues_show/api'; +import { + fetchIssue, + fetchIssueStatuses, + updateIssue, +} from '~/integrations/external_issue/issues_show/api'; -import ZentaoIssueSidebar from '~/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue'; -import { issueStates, issueStateLabels } from '~/integrations/zentao/issues_show/constants'; +import ExternalIssueSidebar from '~/integrations/external_issue/issues_show/components/sidebar/external_issues_sidebar_root.vue'; +import { issueStates, issueStateLabels } from '~/integrations/external_issue/issues_show/constants'; import IssuableShow from '~/issuable_show/components/issuable_show_root.vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { s__ } from '~/locale'; +import Note from './note.vue'; export default { - name: 'ZentaoIssuesShow', + name: 'ExternalIssuesShow', components: { GlAlert, GlSprintf, GlLink, + GlBadge, GlLoadingIcon, IssuableShow, - ZentaoIssueSidebar, + ExternalIssueSidebar, + Note, }, directives: { GlTooltip, }, inject: { + userTypeText: { + default: null, + }, + userTypeTooltipText: { + default: null, + }, + failFetchingIssueText: { + default: null, + }, + failFetchingIssueStatusText: { + default: null, + }, + failUpdatingIssueStatusText: { + default: null, + }, issuesShowPath: { default: '', }, + seeMoreDetailText: { + default: null, + }, + titleText: { + default: null, + }, }, data() { return { @@ -68,17 +96,15 @@ export default { this.issue = convertObjectPropsToCamelCase(issue, { deep: true }); }) .catch(() => { - this.errorMessage = s__( - 'ZentaoIntegration|Failed to load Zentao issue. View the issue in Zentao, or reload the page.', - ); + this.errorMessage = this.failFetchingIssueText; }) .finally(() => { this.isLoading = false; }); }, - zentaoIssueCommentId(id) { - return `zentao_note_${id}`; + externalIssueCommentId(id) { + return `external_note_${id}`; }, onIssueLabelsUpdated(labels) { @@ -89,9 +115,7 @@ export default { }) .catch(() => { createFlash({ - message: s__( - 'ZentaoIntegration|Failed to update Zentao issue labels. View the issue in Zentao, or reload the page.', - ), + message: this.failUpdatingIssueLabelsText, }); }) .finally(() => { @@ -106,9 +130,7 @@ export default { }) .catch(() => { createFlash({ - message: s__( - 'ZentaoIntegration|Failed to load Zentao issue statuses. View the issue in Zentao, or reload the page.', - ), + message: this.failFetchingIssueStatusText, }); }) .finally(() => { @@ -123,9 +145,7 @@ export default { }) .catch(() => { createFlash({ - message: s__( - 'ZentaoIntegration|Failed to update Zentao issue status. View the issue in Zentao, or reload the page.', - ), + message: this.failUpdatingIssueStatusText, }); }) .finally(() => { @@ -143,19 +163,8 @@ export default { {{ errorMessage }} diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/note.vue b/app/assets/javascripts/integrations/external_issue/issues_show/components/note.vue similarity index 100% rename from app/assets/javascripts/integrations/zentao/issues_show/components/note.vue rename to app/assets/javascripts/integrations/external_issue/issues_show/components/note.vue diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/assignee.vue b/app/assets/javascripts/integrations/external_issue/issues_show/components/sidebar/assignee.vue similarity index 94% rename from app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/assignee.vue rename to app/assets/javascripts/integrations/external_issue/issues_show/components/sidebar/assignee.vue index 1c410c8ce1c190..81048183987d10 100644 --- a/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/assignee.vue +++ b/app/assets/javascripts/integrations/external_issue/issues_show/components/sidebar/assignee.vue @@ -4,7 +4,7 @@ import { __ } from '~/locale'; import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue'; export default { - name: 'ZentaoIssuesSidebarAssignee', + name: 'ExternalIssuesSidebarAssignee', components: { GlAvatarLabeled, GlAvatarLink, @@ -15,6 +15,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + inject: ['userTypeText'], props: { assignee: { type: Object, @@ -60,7 +61,7 @@ export default { :alt="assignee.name" :entity-name="assignee.name" :label="assignee.name" - :sub-label="__('Zentao user')" + :sub-label="userTypeText" /> {{ __('None') }} diff --git a/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue b/app/assets/javascripts/integrations/external_issue/issues_show/components/sidebar/external_issues_sidebar_root.vue similarity index 88% rename from app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue rename to app/assets/javascripts/integrations/external_issue/issues_show/components/sidebar/external_issues_sidebar_root.vue index 8dd71e512b64ed..f255bc7468a82f 100644 --- a/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue +++ b/app/assets/javascripts/integrations/external_issue/issues_show/components/sidebar/external_issues_sidebar_root.vue @@ -1,6 +1,6 @@