diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 21:18:33 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 21:18:33 +0300 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /app/assets/javascripts/issues_list | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'app/assets/javascripts/issues_list')
-rw-r--r-- | app/assets/javascripts/issues_list/components/issuable.vue | 31 | ||||
-rw-r--r-- | app/assets/javascripts/issues_list/components/issue_card_time_info.vue | 124 | ||||
-rw-r--r-- | app/assets/javascripts/issues_list/components/issues_list_app.vue | 143 | ||||
-rw-r--r-- | app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue (renamed from app/assets/javascripts/issues_list/components/jira_issues_list_root.vue) | 6 | ||||
-rw-r--r-- | app/assets/javascripts/issues_list/index.js | 42 | ||||
-rw-r--r-- | app/assets/javascripts/issues_list/service_desk_helper.js | 30 |
6 files changed, 341 insertions, 35 deletions
diff --git a/app/assets/javascripts/issues_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue index b7af6e098e1..60b01a6d37f 100644 --- a/app/assets/javascripts/issues_list/components/issuable.vue +++ b/app/assets/javascripts/issues_list/components/issuable.vue @@ -25,16 +25,16 @@ import { newDateAsLocaleTime, } from '~/lib/utils/datetime_utility'; import { convertToCamelCase } from '~/lib/utils/text_utility'; -import { mergeUrlParams } from '~/lib/utils/url_utility'; +import { mergeUrlParams, setUrlFragment, isExternal } from '~/lib/utils/url_utility'; import { sprintf, __ } from '~/locale'; import initUserPopovers from '~/user_popovers'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; export default { i18n: { - openedAgo: __('opened %{timeAgoString} by %{user}'), - openedAgoJira: __('opened %{timeAgoString} by %{user} in Jira'), - openedAgoServiceDesk: __('opened %{timeAgoString} by %{email} via %{user}'), + openedAgo: __('created %{timeAgoString} by %{user}'), + openedAgoJira: __('created %{timeAgoString} by %{user} in Jira'), + openedAgoServiceDesk: __('created %{timeAgoString} by %{email} via %{user}'), }, components: { IssueAssignees, @@ -102,8 +102,14 @@ export default { isJiraIssue() { return this.issuable.external_tracker === 'jira'; }, + webUrl() { + return this.issuable.gitlab_web_url || this.issuable.web_url; + }, + isIssuableUrlExternal() { + return isExternal(this.webUrl); + }, linkTarget() { - return this.isJiraIssue ? '_blank' : null; + return this.isIssuableUrlExternal ? '_blank' : null; }, issueCreatedToday() { return getDayDifference(new Date(this.issuable.created_at), new Date()) < 1; @@ -188,7 +194,7 @@ export default { value: this.issuable.blocking_issues_count, title: __('Blocking issues'), dataTestId: 'blocking-issues', - href: `${this.issuable.web_url}#related-issues`, + href: setUrlFragment(this.webUrl, 'related-issues'), icon: 'issue-block', }, { @@ -197,7 +203,7 @@ export default { value: this.issuable.user_notes_count, title: __('Comments'), dataTestId: 'notes-count', - href: `${this.issuable.web_url}#notes`, + href: setUrlFragment(this.webUrl, 'notes'), class: { 'no-comments': !this.issuable.user_notes_count, 'issuable-comments': true }, icon: 'comments', }, @@ -252,7 +258,7 @@ export default { :class="{ today: issueCreatedToday, closed: isClosed }" :data-id="issuable.id" :data-labels="labelIdsString" - :data-url="issuable.web_url" + :data-url="webUrl" data-qa-selector="issue_container" :data-qa-issue-title="issuable.title" > @@ -284,13 +290,14 @@ export default { :aria-label="$options.confidentialTooltipText" /> <gl-link - :href="issuable.web_url" + :href="webUrl" :target="linkTarget" data-testid="issuable-title" data-qa-selector="issue_link" - >{{ issuable.title - }}<gl-icon - v-if="isJiraIssue" + > + {{ issuable.title }} + <gl-icon + v-if="isIssuableUrlExternal" name="external-link" class="gl-vertical-align-text-bottom gl-ml-2" /> diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue new file mode 100644 index 00000000000..8d00d337bac --- /dev/null +++ b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue @@ -0,0 +1,124 @@ +<script> +import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { + dateInWords, + getTimeRemainingInWords, + isInFuture, + isInPast, + isToday, +} from '~/lib/utils/datetime_utility'; +import { convertToCamelCase } from '~/lib/utils/text_utility'; +import { __ } from '~/locale'; + +export default { + components: { + GlLink, + GlIcon, + IssueHealthStatus: () => + import('ee_component/related_items_tree/components/issue_health_status.vue'), + WeightCount: () => import('ee_component/issues/components/weight_count.vue'), + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + inject: { + hasIssuableHealthStatusFeature: { + default: false, + }, + }, + props: { + issue: { + type: Object, + required: true, + }, + }, + computed: { + milestoneDate() { + if (this.issue.milestone?.dueDate) { + const { dueDate, startDate } = this.issue.milestone; + const date = dateInWords(new Date(dueDate), true); + const remainingTime = this.milestoneRemainingTime(dueDate, startDate); + return `${date} (${remainingTime})`; + } + return __('Milestone'); + }, + dueDate() { + return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true); + }, + isDueDateInPast() { + return isInPast(new Date(this.issue.dueDate)); + }, + timeEstimate() { + return this.issue.timeStats?.humanTimeEstimate; + }, + showHealthStatus() { + return this.hasIssuableHealthStatusFeature && this.issue.healthStatus; + }, + healthStatus() { + return convertToCamelCase(this.issue.healthStatus); + }, + }, + methods: { + milestoneRemainingTime(dueDate, startDate) { + const due = new Date(dueDate); + const start = new Date(startDate); + + if (dueDate && isInPast(due)) { + return __('Past due'); + } else if (dueDate && isToday(due)) { + return __('Today'); + } else if (startDate && isInFuture(start)) { + return __('Upcoming'); + } else if (dueDate) { + return getTimeRemainingInWords(due); + } + return ''; + }, + }, +}; +</script> + +<template> + <span> + <span + v-if="issue.milestone" + class="issuable-milestone gl-display-none gl-sm-display-inline-block! gl-mr-3" + data-testid="issuable-milestone" + > + <gl-link v-gl-tooltip :href="issue.milestone.webUrl" :title="milestoneDate"> + <gl-icon name="clock" /> + {{ issue.milestone.title }} + </gl-link> + </span> + <span + v-if="issue.dueDate" + v-gl-tooltip + class="issuable-due-date gl-display-none gl-sm-display-inline-block! gl-mr-3" + :class="{ 'gl-text-red-500': isDueDateInPast }" + :title="__('Due date')" + data-testid="issuable-due-date" + > + <gl-icon name="calendar" /> + {{ dueDate }} + </span> + <span + v-if="timeEstimate" + v-gl-tooltip + class="gl-display-none gl-sm-display-inline-block! gl-mr-3" + :title="__('Estimate')" + data-testid="time-estimate" + > + <gl-icon name="timer" /> + {{ timeEstimate }} + </span> + <weight-count + class="gl-display-none gl-sm-display-inline-block gl-mr-3" + :weight="issue.weight" + /> + <issue-health-status + v-if="showHealthStatus" + class="gl-display-none gl-sm-display-inline-block" + :health-status="healthStatus" + /> + </span> +</template> diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue new file mode 100644 index 00000000000..c57fa5a82fa --- /dev/null +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -0,0 +1,143 @@ +<script> +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { toNumber } from 'lodash'; +import createFlash from '~/flash'; +import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; +import { IssuableStatus } from '~/issue_show/constants'; +import { PAGE_SIZE } from '~/issues_list/constants'; +import axios from '~/lib/utils/axios_utils'; +import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils'; +import { __ } from '~/locale'; +import IssueCardTimeInfo from './issue_card_time_info.vue'; + +export default { + PAGE_SIZE, + components: { + GlIcon, + IssuableList, + IssueCardTimeInfo, + BlockingIssuesCount: () => import('ee_component/issues/components/blocking_issues_count.vue'), + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + inject: { + endpoint: { + default: '', + }, + fullPath: { + default: '', + }, + }, + data() { + return { + currentPage: toNumber(getParameterByName('page')) || 1, + isLoading: false, + issues: [], + totalIssues: 0, + }; + }, + computed: { + urlParams() { + return { + page: this.currentPage, + state: IssuableStatus.Open, + }; + }, + }, + mounted() { + this.fetchIssues(); + }, + methods: { + fetchIssues(pageToFetch) { + this.isLoading = true; + + return axios + .get(this.endpoint, { + params: { + page: pageToFetch || this.currentPage, + per_page: this.$options.PAGE_SIZE, + state: IssuableStatus.Open, + with_labels_details: true, + }, + }) + .then(({ data, headers }) => { + this.currentPage = Number(headers['x-page']); + this.totalIssues = Number(headers['x-total']); + this.issues = data.map((issue) => convertObjectPropsToCamelCase(issue, { deep: true })); + }) + .catch(() => { + createFlash({ message: __('An error occurred while loading issues') }); + }) + .finally(() => { + this.isLoading = false; + }); + }, + handlePageChange(page) { + this.fetchIssues(page); + }, + }, +}; +</script> + +<template> + <issuable-list + :namespace="fullPath" + recent-searches-storage-key="issues" + :search-input-placeholder="__('Search or filter results…')" + :search-tokens="[]" + :sort-options="[]" + :issuables="issues" + :tabs="[]" + current-tab="" + :issuables-loading="isLoading" + :show-pagination-controls="true" + :total-items="totalIssues" + :current-page="currentPage" + :previous-page="currentPage - 1" + :next-page="currentPage + 1" + :url-params="urlParams" + @page-change="handlePageChange" + > + <template #timeframe="{ issuable = {} }"> + <issue-card-time-info :issue="issuable" /> + </template> + <template #statistics="{ issuable = {} }"> + <li + v-if="issuable.mergeRequestsCount" + v-gl-tooltip + class="gl-display-none gl-sm-display-block" + :title="__('Related merge requests')" + data-testid="issuable-mr" + > + <gl-icon name="merge-request" /> + {{ issuable.mergeRequestsCount }} + </li> + <li + v-if="issuable.upvotes" + v-gl-tooltip + class="gl-display-none gl-sm-display-block" + :title="__('Upvotes')" + data-testid="issuable-upvotes" + > + <gl-icon name="thumb-up" /> + {{ issuable.upvotes }} + </li> + <li + v-if="issuable.downvotes" + v-gl-tooltip + class="gl-display-none gl-sm-display-block" + :title="__('Downvotes')" + data-testid="issuable-downvotes" + > + <gl-icon name="thumb-down" /> + {{ issuable.downvotes }} + </li> + <blocking-issues-count + class="gl-display-none gl-sm-display-block" + :blocking-issues-count="issuable.blockingIssuesCount" + :is-list-item="true" + /> + </template> + </issuable-list> +</template> diff --git a/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue b/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue index 7396cfe27b3..ba0ca57523a 100644 --- a/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue +++ b/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue @@ -11,7 +11,7 @@ import { n__ } from '~/locale'; import getIssuesListDetailsQuery from '../queries/get_issues_list_details.query.graphql'; export default { - name: 'JiraIssuesList', + name: 'JiraIssuesImportStatus', components: { GlAlert, GlLabel, @@ -89,13 +89,13 @@ export default { </script> <template> - <div class="issuable-list-root"> + <div class="gl-my-5"> <gl-alert v-if="jiraImport.shouldShowInProgressAlert" @dismiss="hideInProgressAlert"> {{ __('Import in progress. Refresh page to see newly added issues.') }} </gl-alert> <gl-alert - v-if="jiraImport.shouldShowFinishedAlert" + v-else-if="jiraImport.shouldShowFinishedAlert" variant="success" @dismiss="hideFinishedAlert" > diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js index 5c3910955bc..a283cbdc86b 100644 --- a/app/assets/javascripts/issues_list/index.js +++ b/app/assets/javascripts/issues_list/index.js @@ -1,12 +1,13 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import IssuesListApp from '~/issues_list/components/issues_list_app.vue'; import createDefaultClient from '~/lib/graphql'; -import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; import IssuablesListApp from './components/issuables_list_app.vue'; -import JiraIssuesListRoot from './components/jira_issues_list_root.vue'; +import JiraIssuesImportStatusRoot from './components/jira_issues_import_status_app.vue'; function mountJiraIssuesListApp() { - const el = document.querySelector('.js-projects-issues-root'); + const el = document.querySelector('.js-jira-issues-import-status'); if (!el) { return false; @@ -23,7 +24,7 @@ function mountJiraIssuesListApp() { el, apolloProvider, render(createComponent) { - return createComponent(JiraIssuesListRoot, { + return createComponent(JiraIssuesImportStatusRoot, { props: { canEdit: parseBoolean(el.dataset.canEdit), isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured), @@ -36,7 +37,7 @@ function mountJiraIssuesListApp() { } function mountIssuablesListApp() { - if (!gon.features?.vueIssuablesList && !gon.features?.jiraIssuesIntegration) { + if (!gon.features?.vueIssuablesList) { return; } @@ -64,6 +65,37 @@ function mountIssuablesListApp() { }); } +export function initIssuesListApp() { + const el = document.querySelector('.js-issues-list'); + + if (!el) { + return false; + } + + const { + endpoint, + fullPath, + hasBlockedIssuesFeature, + hasIssuableHealthStatusFeature, + hasIssueWeightsFeature, + } = el.dataset; + + return new Vue({ + el, + // Currently does not use Vue Apollo, but need to provide {} for now until the + // issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153 + apolloProvider: {}, + provide: { + endpoint, + fullPath, + hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature), + hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature), + hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature), + }, + render: (createComponent) => createComponent(IssuesListApp), + }); +} + export default function initIssuablesList() { mountJiraIssuesListApp(); mountIssuablesListApp(); diff --git a/app/assets/javascripts/issues_list/service_desk_helper.js b/app/assets/javascripts/issues_list/service_desk_helper.js index 0a34b754377..f96567ef53b 100644 --- a/app/assets/javascripts/issues_list/service_desk_helper.js +++ b/app/assets/javascripts/issues_list/service_desk_helper.js @@ -1,4 +1,4 @@ -import { __ } from '~/locale'; +import { s__ } from '~/locale'; /** * Generates empty state messages for Service Desk issues list. @@ -15,23 +15,23 @@ export function generateMessages(emptyStateMeta) { incomingEmailHelpPage, } = emptyStateMeta; - const serviceDeskSupportedTitle = __( - 'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab', + const serviceDeskSupportedTitle = s__( + 'ServiceDesk|Use Service Desk to connect with your users and offer customer support through email right inside GitLab', ); - const serviceDeskSupportedMessage = __( - 'Those emails automatically become issues (with the comments becoming the email conversation) listed here.', + const serviceDeskSupportedMessage = s__( + 'ServiceDesk|Issues created from Service Desk emails appear here. Each comment becomes part of the email conversation.', ); const commonDescription = ` <span>${serviceDeskSupportedMessage}</span> - <a href="${serviceDeskHelpPage}">${__('Read more')}</a>`; + <a href="${serviceDeskHelpPage}">${s__('Learn more.')}</a>`; return { serviceDeskEnabledAndCanEditProjectSettings: { title: serviceDeskSupportedTitle, svgPath, - description: `<p>${__('Have your users email')} + description: `<p>${s__('ServiceDesk|Your users can send emails to this address:')} <code>${serviceDeskAddress}</code> </p> ${commonDescription}`, @@ -46,7 +46,7 @@ export function generateMessages(emptyStateMeta) { svgPath, description: commonDescription, primaryLink: editProjectPage, - primaryText: __('Turn on Service Desk'), + primaryText: s__('ServiceDesk|Enable Service Desk'), }, serviceDeskDisabledAndCannotEditProjectSettings: { title: serviceDeskSupportedTitle, @@ -54,19 +54,19 @@ export function generateMessages(emptyStateMeta) { description: commonDescription, }, serviceDeskIsNotSupported: { - title: __('Service Desk is not supported'), + title: s__('ServiceDesk|Service Desk is not supported'), svgPath, - description: __( - 'In order to enable Service Desk for your instance, you must first set up incoming email.', + description: s__( + 'ServiceDesk|To enable Service Desk on this instance, an instance administrator must first set up incoming email.', ), primaryLink: incomingEmailHelpPage, - primaryText: __('More information'), + primaryText: s__('Learn more.'), }, serviceDeskIsNotEnabled: { - title: __('Service Desk is not enabled'), + title: s__('ServiceDesk|Service Desk is not enabled'), svgPath, - description: __( - 'For help setting up the Service Desk for your instance, please contact an administrator.', + description: s__( + 'ServiceDesk|For help setting up the Service Desk for your instance, please contact an administrator.', ), }, }; |