Commit cbd67f5a authored by Kushal Pandya's avatar Kushal Pandya Committed by Phil Hughes

Show actual Milestone dates within tooltips for Milestones in Epics sidebar

parent 5ba9095b
......@@ -61,7 +61,7 @@
const dateWords = dateInWords(date, true);
const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
return date ? parsedDateWords : 'None';
return date ? parsedDateWords : __('None');
},
tooltipText(dateType = 'min') {
const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
......@@ -72,7 +72,10 @@
`(${timeAgo})`,
].join(' ') : '';
return [defaultText, dateText].join('<br />');
if (date) {
return [defaultText, dateText].join('<br />');
}
return __('Start and due date');
},
},
};
......
<script>
/* eslint-disable vue/require-default-prop */
import $ from 'jquery';
import issuableApp from '~/issue_show/components/app.vue';
import flash from '~/flash';
import { __ } from '~/locale';
import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue';
import SidebarContext from '../sidebar_context';
import epicHeader from './epic_header.vue';
import EpicsService from '../../service/epics_service';
import { status, stateEvent } from '../../constants';
/* eslint-disable vue/require-default-prop */
import $ from 'jquery';
import issuableApp from '~/issue_show/components/app.vue';
import flash from '~/flash';
import { __ } from '~/locale';
import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue';
import SidebarContext from '../sidebar_context';
import epicHeader from './epic_header.vue';
import EpicsService from '../../service/epics_service';
import { status, stateEvent } from '../../constants';
export default {
name: 'EpicShowApp',
components: {
epicHeader,
epicSidebar,
issuableApp,
relatedIssuesRoot,
},
props: {
epicId: {
type: Number,
required: true,
},
endpoint: {
type: String,
required: true,
},
updateEndpoint: {
type: String,
required: true,
},
canUpdate: {
required: true,
type: Boolean,
},
canDestroy: {
required: true,
type: Boolean,
},
canAdmin: {
required: true,
type: Boolean,
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
groupPath: {
type: String,
required: true,
},
initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
created: {
type: String,
required: true,
},
author: {
type: Object,
required: true,
},
issueLinksEndpoint: {
type: String,
required: true,
},
startDateIsFixed: {
type: Boolean,
required: true,
},
startDateFixed: {
type: String,
required: false,
default: '',
},
startDateFromMilestones: {
type: String,
required: false,
default: '',
},
startDate: {
type: String,
required: false,
},
dueDateIsFixed: {
type: Boolean,
required: true,
},
dueDateFixed: {
type: String,
required: false,
default: '',
},
dueDateFromMilestones: {
type: String,
required: false,
default: '',
},
endDate: {
type: String,
required: false,
},
startDateSourcingMilestoneTitle: {
type: String,
required: false,
default: '',
},
dueDateSourcingMilestoneTitle: {
type: String,
required: false,
default: '',
},
labels: {
type: Array,
required: true,
},
participants: {
type: Array,
required: true,
},
subscribed: {
type: Boolean,
required: true,
},
todoExists: {
type: Boolean,
required: true,
},
namespace: {
type: String,
required: false,
default: '#',
},
labelsPath: {
type: String,
required: true,
},
toggleSubscriptionPath: {
type: String,
required: true,
},
todoPath: {
type: String,
required: true,
},
todoDeletePath: {
type: String,
required: false,
default: '',
},
labelsWebUrl: {
type: String,
required: true,
},
epicsWebUrl: {
type: String,
required: true,
},
state: {
type: String,
required: true,
default: status.open,
},
},
data() {
return {
// Epics specific configuration
issuableRef: '',
projectPath: this.groupPath,
projectNamespace: '',
service: new EpicsService({
endpoint: this.endpoint,
}),
};
},
computed: {
open() {
return this.state === status.open;
},
},
mounted() {
this.sidebarContext = new SidebarContext();
},
methods: {
triggerDocumentEvent(eventName, isClosed) {
$(document).trigger(eventName, isClosed);
},
toggleEpicStatus(stateEventType) {
return this.service
.updateStatus(stateEventType)
.then(() => {
const isClosed = stateEventType === stateEvent.close;
export default {
name: 'EpicShowApp',
components: {
epicHeader,
epicSidebar,
issuableApp,
relatedIssuesRoot,
},
props: {
epicId: {
type: Number,
required: true,
},
endpoint: {
type: String,
required: true,
},
updateEndpoint: {
type: String,
required: true,
},
canUpdate: {
required: true,
type: Boolean,
},
canDestroy: {
required: true,
type: Boolean,
},
canAdmin: {
required: true,
type: Boolean,
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
groupPath: {
type: String,
required: true,
},
initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
created: {
type: String,
required: true,
},
author: {
type: Object,
required: true,
},
issueLinksEndpoint: {
type: String,
required: true,
},
startDateIsFixed: {
type: Boolean,
required: true,
},
startDateFixed: {
type: String,
required: false,
default: '',
},
startDateFromMilestones: {
type: String,
required: false,
default: '',
},
startDate: {
type: String,
required: false,
},
dueDateIsFixed: {
type: Boolean,
required: true,
},
dueDateFixed: {
type: String,
required: false,
default: '',
},
dueDateFromMilestones: {
type: String,
required: false,
default: '',
},
endDate: {
type: String,
required: false,
},
startDateSourcingMilestoneTitle: {
type: String,
required: false,
default: '',
},
startDateSourcingMilestoneDates: {
type: Object,
required: true,
default: () => ({ startDate: '', dueDate: '' }),
},
dueDateSourcingMilestoneTitle: {
type: String,
required: false,
default: '',
},
dueDateSourcingMilestoneDates: {
type: Object,
required: true,
default: () => ({ startDate: '', dueDate: '' }),
},
labels: {
type: Array,
required: true,
},
participants: {
type: Array,
required: true,
},
subscribed: {
type: Boolean,
required: true,
},
todoExists: {
type: Boolean,
required: true,
},
namespace: {
type: String,
required: false,
default: '#',
},
labelsPath: {
type: String,
required: true,
},
toggleSubscriptionPath: {
type: String,
required: true,
},
todoPath: {
type: String,
required: true,
},
todoDeletePath: {
type: String,
required: false,
default: '',
},
labelsWebUrl: {
type: String,
required: true,
},
epicsWebUrl: {
type: String,
required: true,
},
state: {
type: String,
required: true,
default: status.open,
},
},
data() {
return {
// Epics specific configuration
issuableRef: '',
projectPath: this.groupPath,
projectNamespace: '',
service: new EpicsService({
endpoint: this.endpoint,
}),
};
},
computed: {
open() {
return this.state === status.open;
},
},
mounted() {
this.sidebarContext = new SidebarContext();
},
methods: {
triggerDocumentEvent(eventName, isClosed) {
$(document).trigger(eventName, isClosed);
},
toggleEpicStatus(stateEventType) {
return this.service
.updateStatus(stateEventType)
.then(() => {
const isClosed = stateEventType === stateEvent.close;
// Ensure that status change is reflected across the page.
// As `Close`/`Reopen` button is also present under
// comment form (part of Notes app)
// We've wrapped call to `$(document).trigger` for ease of testing
this.triggerDocumentEvent('issuable_vue_app:change', isClosed);
this.triggerDocumentEvent('issuable:change', isClosed);
})
.catch(() => {
flash(__('Unable to update this epic at this time.'));
const isClosed = stateEventType !== stateEvent.close;
this.triggerDocumentEvent('issuable_vue_app:change', isClosed);
this.triggerDocumentEvent('issuable:change', isClosed);
});
},
},
};
// Ensure that status change is reflected across the page.
// As `Close`/`Reopen` button is also present under
// comment form (part of Notes app)
// We've wrapped call to `$(document).trigger` for ease of testing
this.triggerDocumentEvent('issuable_vue_app:change', isClosed);
this.triggerDocumentEvent('issuable:change', isClosed);
})
.catch(() => {
flash(__('Unable to update this epic at this time.'));
const isClosed = stateEventType !== stateEvent.close;
this.triggerDocumentEvent('issuable_vue_app:change', isClosed);
this.triggerDocumentEvent('issuable:change', isClosed);
});
},
},
};
</script>
<template>
......@@ -276,7 +286,9 @@
:due-date-from-milestones="dueDateFromMilestones"
:initial-end-date="endDate"
:start-date-sourcing-milestone-title="startDateSourcingMilestoneTitle"
:start-date-sourcing-milestone-dates="startDateSourcingMilestoneDates"
:due-date-sourcing-milestone-title="dueDateSourcingMilestoneTitle"
:due-date-sourcing-milestone-dates="dueDateSourcingMilestoneDates"
:initial-labels="labels"
:initial-participants="participants"
:initial-subscribed="subscribed"
......
......@@ -6,7 +6,7 @@ import EpicShowApp from './components/epic_show_app.vue';
export default () => {
const el = document.querySelector('#epic-show-app');
const metaData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta));
const metaData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true });
const initialData = JSON.parse(el.dataset.initial);
// Collapse the sidebar on mobile screens by default
......@@ -22,8 +22,9 @@ export default () => {
components: {
'epic-show-app': EpicShowApp,
},
render: createElement => createElement('epic-show-app', {
props,
}),
render: createElement =>
createElement('epic-show-app', {
props,
}),
});
};
......@@ -6,6 +6,7 @@ import Cookies from 'js-cookie';
import flash from '~/flash';
import { __, s__, sprintf } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { dateInWords, parsePikadayDate } from '~/lib/utils/datetime_utility';
import ListLabel from '~/vue_shared/models/label';
import SidebarTodo from '~/sidebar/components/todo_toggle/todo.vue';
import SidebarCollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
......@@ -85,11 +86,19 @@ export default {
required: false,
default: '',
},
startDateSourcingMilestoneDates: {
type: Object,
required: true,
},
dueDateSourcingMilestoneTitle: {
type: String,
required: false,
default: '',
},
dueDateSourcingMilestoneDates: {
type: Object,
required: true,
},
initialLabels: {
type: Array,
required: true,
......@@ -233,17 +242,33 @@ export default {
getDateTypeString(dateType) {
return dateType === DateTypes.start ? s__('Epics|start') : s__('Epics|due');
},
getDateFromMilestonesTooltip(dateType = 'start') {
getDateFromMilestonesTooltip(dateType = DateTypes.start) {
const { startDateTimeFromMilestones, dueDateTimeFromMilestones } = this.store;
const dateSourcingMilestoneTitle = this[`${dateType}DateSourcingMilestoneTitle`];
const sourcingMilestoneDates =
dateType === DateTypes.start
? this.startDateSourcingMilestoneDates
: this.dueDateSourcingMilestoneDates;
if (startDateTimeFromMilestones && dueDateTimeFromMilestones) {
return dateSourcingMilestoneTitle;
const startDate = parsePikadayDate(sourcingMilestoneDates.startDate);
const dueDate = parsePikadayDate(sourcingMilestoneDates.dueDate);
return `${dateSourcingMilestoneTitle}<br/><span class="text-tertiary">${dateInWords(
startDate,
true,
startDate.getFullYear() === dueDate.getFullYear(),
)}${dateInWords(dueDate, true)}</span>`;
}
return sprintf(s__('Epics|To schedule your epic\'s %{epicDateType} date based on milestones, assign a milestone with a %{epicDateType} date to any issue in the epic.'), {
epicDateType: this.getDateTypeString(dateType),
});
return sprintf(
s__(
"Epics|To schedule your epic's %{epicDateType} date based on milestones, assign a milestone with a %{epicDateType} date to any issue in the epic.",
),
{
epicDateType: this.getDateTypeString(dateType),
},
);
},
toggleSidebar() {
this.collapsed = !this.collapsed;
......@@ -293,9 +318,11 @@ export default {
})
.catch(() => {
this[savingBoolean] = false;
flash(sprintf(s__('Epics|An error occurred while saving %{epicDateType} date'), {
epicDateType: this.getDateTypeString(dateType),
}));
flash(
sprintf(s__('Epics|An error occurred while saving %{epicDateType} date'), {
epicDateType: this.getDateTypeString(dateType),
}),
);
});
},
changeStartDateType(dateTypeIsFixed, typeChangeOnEdit) {
......
......@@ -20,11 +20,19 @@ module EpicsHelper
start_date_fixed: epic.start_date_fixed,
start_date_from_milestones: epic.start_date_from_milestones,
start_date_sourcing_milestone_title: epic.start_date_sourcing_milestone&.title,
start_date_sourcing_milestone_dates: {
start_date: epic.start_date_sourcing_milestone&.start_date,
due_date: epic.start_date_sourcing_milestone&.due_date
},
due_date: epic.due_date,
due_date_is_fixed: epic.due_date_is_fixed?,
due_date_fixed: epic.due_date_fixed,
due_date_from_milestones: epic.due_date_from_milestones,
due_date_sourcing_milestone_title: epic.due_date_sourcing_milestone&.title,
due_date_sourcing_milestone_dates: {
start_date: epic.due_date_sourcing_milestone&.start_date,
due_date: epic.due_date_sourcing_milestone&.due_date
},
end_date: epic.end_date,
state: epic.state
}
......
---
title: Show actual Milestone dates within tooltips for Milestones in Epics sidebar
merge_request: 8048
author:
type: added
......@@ -5,14 +5,15 @@ describe EpicsHelper do
describe '#epic_show_app_data' do
let(:user) { create(:user) }
let(:milestone) { create(:milestone, title: 'make me a sandwich') }
let(:milestone1) { create(:milestone, title: 'make me a sandwich', start_date: '2010-01-01', due_date: '2019-12-31') }
let(:milestone2) { create(:milestone, title: 'make me a pizza', start_date: '2020-01-01', due_date: '2029-12-31') }
let!(:epic) do
create(
:epic,
author: user,
start_date_sourcing_milestone: milestone,
start_date_sourcing_milestone: milestone1,
start_date: Date.new(2000, 1, 1),
due_date_sourcing_milestone: milestone,
due_date_sourcing_milestone: milestone2,
due_date: Date.new(2000, 1, 2)
)
end
......@@ -32,6 +33,7 @@ describe EpicsHelper do
created author epic_id todo_exists todo_path state
start_date start_date_fixed start_date_is_fixed start_date_from_milestones start_date_sourcing_milestone_title
end_date due_date due_date_fixed due_date_is_fixed due_date_from_milestones due_date_sourcing_milestone_title
start_date_sourcing_milestone_dates due_date_sourcing_milestone_dates
])
expect(meta_data['author']).to eq({
'name' => user.name,
......@@ -40,9 +42,13 @@ describe EpicsHelper do
'src' => 'icon_path'
})
expect(meta_data['start_date']).to eq('2000-01-01')
expect(meta_data['start_date_sourcing_milestone_title']).to eq(milestone.title)
expect(meta_data['start_date_sourcing_milestone_title']).to eq(milestone1.title)
expect(meta_data['start_date_sourcing_milestone_dates']['start_date']).to eq(milestone1.start_date.to_s)
expect(meta_data['start_date_sourcing_milestone_dates']['due_date']).to eq(milestone1.due_date.to_s)
expect(meta_data['due_date']).to eq('2000-01-02')
expect(meta_data['due_date_sourcing_milestone_title']).to eq(milestone.title)
expect(meta_data['due_date_sourcing_milestone_title']).to eq(milestone2.title)
expect(meta_data['due_date_sourcing_milestone_dates']['start_date']).to eq(milestone2.start_date.to_s)
expect(meta_data['due_date_sourcing_milestone_dates']['due_date']).to eq(milestone2.due_date.to_s)
end
context 'when a user can update an epic' do
......@@ -52,9 +58,9 @@ describe EpicsHelper do
create(
:epic,
author: user,
start_date_sourcing_milestone: milestone,
start_date_sourcing_milestone: milestone1,
start_date: Date.new(2000, 1, 1),
due_date_sourcing_milestone: milestone,
due_date_sourcing_milestone: milestone2,
due_date: Date.new(2000, 1, 2)
)
end
......@@ -71,11 +77,12 @@ describe EpicsHelper do
created author epic_id todo_exists todo_path state
start_date start_date_fixed start_date_is_fixed start_date_from_milestones start_date_sourcing_milestone_title
end_date due_date due_date_fixed due_date_is_fixed due_date_from_milestones due_date_sourcing_milestone_title
start_date_sourcing_milestone_dates due_date_sourcing_milestone_dates
])
expect(meta_data['start_date']).to eq('2000-01-01')
expect(meta_data['start_date_sourcing_milestone_title']).to eq(milestone.title)
expect(meta_data['start_date_sourcing_milestone_title']).to eq(milestone1.title)
expect(meta_data['due_date']).to eq('2000-01-02')
expect(meta_data['due_date_sourcing_milestone_title']).to eq(milestone.title)
expect(meta_data['due_date_sourcing_milestone_title']).to eq(milestone2.title)
end
end
end
......
......@@ -57,7 +57,15 @@ export const contentProps = {
dueDateIsFixed: true,
dueDateFromMilestones: '',
startDateSourcingMilestoneTitle: 'Milestone for Start Date',
startDateSourcingMilestoneDates: {
startDate: '2010-01-01',
dueDate: '2019-12-31',
},
dueDateSourcingMilestoneTitle: 'Milestone for End Date',
dueDateSourcingMilestoneDates: {
startDate: '2020-01-01',
dueDate: '2029-12-31',
},
labels: mockLabels,
participants: mockParticipants,
subscribed: true,
......
......@@ -32,7 +32,9 @@ describe('epicSidebar', () => {
dueDateFixed,
dueDateFromMilestones,
startDateSourcingMilestoneTitle,
startDateSourcingMilestoneDates,
dueDateSourcingMilestoneTitle,
dueDateSourcingMilestoneDates,
} = props;
const defaultPropsData = {
......@@ -50,7 +52,9 @@ describe('epicSidebar', () => {
dueDateFromMilestones,
updatePath: updateEndpoint,
startDateSourcingMilestoneTitle,
startDateSourcingMilestoneDates,
dueDateSourcingMilestoneTitle,
dueDateSourcingMilestoneDates,
toggleSubscriptionPath,
labelsPath,
labelsWebUrl,
......@@ -174,6 +178,44 @@ describe('epicSidebar', () => {
it('returns tooltip string for milestone', () => {
expect(vm.getDateFromMilestonesTooltip('start')).toBe('To schedule your epic\'s start date based on milestones, assign a milestone with a start date to any issue in the epic.');
});
it('returns tooltip string with milestone dates', () => {
const vmDatesFromMilestones = mountComponent(
EpicSidebar,
Object.assign({}, defaultPropsData, {
startDateFromMilestones: startDateSourcingMilestoneDates.startDate,
dueDateFromMilestones: dueDateSourcingMilestoneDates.dueDate,
})
);
expect(vmDatesFromMilestones.getDateFromMilestonesTooltip('start')).toBe('Milestone for Start Date<br/><span class="text-tertiary">Jan 1, 2010 – Dec 31, 2019</span>');
vmDatesFromMilestones.$destroy();
});
it('returns tooltip string with milestone dates when dates are from same year', () => {
const startDate = '2018-01-01';
const dueDate = '2018-03-31';
const vmDatesFromMilestones = mountComponent(
EpicSidebar,
Object.assign({}, defaultPropsData, {
startDateSourcingMilestoneDates: {
startDate,
dueDate,
},
dueDateSourcingMilestoneDates: {
startDate,
dueDate,
},
startDateFromMilestones: startDate,
dueDateFromMilestones: dueDate,
})
);
expect(vmDatesFromMilestones.getDateFromMilestonesTooltip('start')).toBe('Milestone for Start Date<br/><span class="text-tertiary">Jan 1 – Mar 31, 2018</span>');
vmDatesFromMilestones.$destroy();
});
});
describe('toggleSidebar', () => {
......
......@@ -7458,6 +7458,9 @@ msgstr ""
msgid "Start a review"
msgstr ""
msgid "Start and due date"
msgstr ""
msgid "Start date"
msgstr ""
......
......@@ -76,5 +76,11 @@ describe('collapsedGroupedDatePicker', () => {
expect(icons.length).toEqual(1);
expect(icons[0].innerText.trim()).toEqual('None');
});
it('should have tooltip as `Start and due dates`', () => {
const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
expect(icons[0].dataset.originalTitle).toBe('Start and due date');
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment