diff --git a/ee/app/assets/javascripts/integrations/jira/issues_show/components/sidebar/issue_due_date.vue b/ee/app/assets/javascripts/integrations/jira/issues_show/components/sidebar/issue_due_date.vue new file mode 100644 index 0000000000000000000000000000000000000000..66360feda1abfc4b0f9772d2eca2cf24fa7a95ae --- /dev/null +++ b/ee/app/assets/javascripts/integrations/jira/issues_show/components/sidebar/issue_due_date.vue @@ -0,0 +1,76 @@ +<script> +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { dateInWords, getDayDifference, parsePikadayDate } from '~/lib/utils/datetime_utility'; +import { __ } from '~/locale'; + +export default { + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + GlIcon, + }, + props: { + dueDate: { + type: String, + required: false, + default: null, + }, + }, + computed: { + dueDateInWords() { + const date = parsePikadayDate(this.dueDate); + + return dateInWords(date, true); + }, + formattedDueDate() { + const today = new Date(); + const date = parsePikadayDate(this.dueDate); + const isPastDue = getDayDifference(today, date) < 0; + + let formattedDate = this.dueDateInWords; + + if (isPastDue) { + formattedDate += ` (${__('Past due')})`; + } + + return formattedDate; + }, + dueDateTooltipProps() { + return { + boundary: 'viewport', + placement: 'left', + title: this.dueDate + ? `${this.$options.i18n.dueDateTitle}<br>${this.formattedDueDate}` + : this.$options.i18n.dueDateTitle, + }; + }, + }, + i18n: { + dueDateTitle: __('Due date'), + none: __('None'), + }, +}; +</script> + +<template> + <div class="block"> + <div + v-gl-tooltip.html="dueDateTooltipProps" + class="sidebar-collapsed-icon" + data-testid="due-date-collapsed" + > + <gl-icon name="calendar" /> + <span v-if="dueDate">{{ dueDateInWords }}</span> + <span v-else>{{ $options.i18n.none }}</span> + </div> + + <div class="hide-collapsed"> + <div class="title">{{ $options.i18n.dueDateTitle }}</div> + <div class="value" data-testid="due-date-value"> + <strong v-if="dueDate">{{ formattedDueDate }}</strong> + <span v-else class="no-value">{{ $options.i18n.none }}</span> + </div> + </div> + </div> +</template> diff --git a/ee/app/assets/javascripts/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue b/ee/app/assets/javascripts/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue index 9341fa76951ca44efe72b47e66006cd055b27d3c..5a64c7287a7a5a886ea05d315c0994b2902227b5 100644 --- a/ee/app/assets/javascripts/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue +++ b/ee/app/assets/javascripts/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue @@ -3,12 +3,14 @@ import { labelsFilterParam } from 'ee/integrations/jira/issues_show/constants'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import Assignee from './assignee.vue'; +import IssueDueDate from './issue_due_date.vue'; import IssueReference from './issue_reference.vue'; export default { name: 'JiraIssuesSidebar', components: { Assignee, + IssueDueDate, IssueReference, LabelsSelect, }, @@ -30,10 +32,10 @@ export default { computed: { assignee() { // Jira issues have at most 1 assignee - return (this.issue?.assignees || [])[0]; + return (this.issue.assignees || [])[0]; }, reference() { - return this.issue?.references?.relative; + return this.issue.references?.relative; }, }, labelsFilterParam, @@ -44,6 +46,8 @@ export default { <div> <assignee class="block" :assignee="assignee" /> + <issue-due-date :due-date="issue.dueDate" /> + <labels-select :selected-labels="issue.labels" :labels-filter-base-path="issuesListPath" diff --git a/ee/spec/frontend/integrations/jira/issues_show/components/sidebar/issue_due_date_spec.js b/ee/spec/frontend/integrations/jira/issues_show/components/sidebar/issue_due_date_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..3d24e4198edf3a97d82ed2a6a2099149d3df8c16 --- /dev/null +++ b/ee/spec/frontend/integrations/jira/issues_show/components/sidebar/issue_due_date_spec.js @@ -0,0 +1,69 @@ +import { shallowMount } from '@vue/test-utils'; + +import IssueDueDate from 'ee/integrations/jira/issues_show/components/sidebar/issue_due_date.vue'; + +import { useFakeDate } from 'helpers/fake_date'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; + +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/ee/spec/frontend/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root_spec.js b/ee/spec/frontend/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root_spec.js index d4a0447c5c5fb4f2fb80c9b9df3b70cbbc27dfb9..59a9e3fb4bd74102f3151929dc3d1748810c81ac 100644 --- a/ee/spec/frontend/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root_spec.js +++ b/ee/spec/frontend/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import Assignee from 'ee/integrations/jira/issues_show/components/sidebar/assignee.vue'; +import IssueDueDate from 'ee/integrations/jira/issues_show/components/sidebar/issue_due_date.vue'; import IssueReference from 'ee/integrations/jira/issues_show/components/sidebar/issue_reference.vue'; import Sidebar from 'ee/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; @@ -31,21 +32,27 @@ describe('JiraIssuesSidebar', () => { const findLabelsSelect = () => wrapper.findComponent(LabelsSelect); const findAssignee = () => wrapper.findComponent(Assignee); + const findIssueDueDate = () => wrapper.findComponent(IssueDueDate); const findIssueReference = () => wrapper.findComponent(IssueReference); - it('renders Labels block', async () => { + it('renders Labels block', () => { createComponent(); - expect(findLabelsSelect().exists()).toBe(true); - expect(findLabelsSelect().props('selectedLabels')).toEqual(mockJiraIssue.labels); + expect(findLabelsSelect().props('selectedLabels')).toBe(mockJiraIssue.labels); }); - it('renders Assignee block', async () => { + it('renders Assignee block', () => { createComponent(); const assignee = findAssignee(); - expect(assignee.exists()).toBe(true); - expect(assignee.props('assignee')).toEqual(mockJiraIssue.assignees[0]); + expect(assignee.props('assignee')).toBe(mockJiraIssue.assignees[0]); + }); + + it('renders IssueDueDate', () => { + createComponent(); + const dueDate = findIssueDueDate(); + + expect(dueDate.props('dueDate')).toBe(mockJiraIssue.dueDate); }); describe('when references.relative is null', () => { diff --git a/ee/spec/frontend/integrations/jira/issues_show/mock_data.js b/ee/spec/frontend/integrations/jira/issues_show/mock_data.js index 18858dc8f563e1d745f2b4cc73cecff657763d71..893d6353cb2f2115c5f379e3ccc3206c78c8364e 100644 --- a/ee/spec/frontend/integrations/jira/issues_show/mock_data.js +++ b/ee/spec/frontend/integrations/jira/issues_show/mock_data.js @@ -1,6 +1,5 @@ export const mockJiraIssue = { - title_html: - '<a href="https://jira.reali.sh:8080/projects/FE/issues/FE-2">FE-2</a> The second FE issue on Jira', + title: 'FE-2 The second FE issue on Jira', description_html: '<a href="https://jira.reali.sh:8080/projects/FE/issues/FE-2">FE-2</a> The second FE issue on Jira', created_at: '"2021-02-01T04:04:40.833Z"', @@ -16,6 +15,7 @@ export const mockJiraIssue = { 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',