Commit 466dc337 authored by Phil Hughes's avatar Phil Hughes

Merge branch '6927-epic-dates-from-milestone' into 'master'

Frontend for Milestone dates integrated into Epics sidebar

Closes #6927, #6928, and #6471

See merge request gitlab-org/gitlab-ee!6658
parents 0ea25ab5 ddc2ad8f
......@@ -82,14 +82,52 @@
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,
......@@ -190,8 +228,16 @@
:epic-id="epicId"
:endpoint="endpoint"
:editable="canUpdate"
:initial-start-date-is-fixed="startDateIsFixed"
:initial-start-date-fixed="startDateFixed"
:start-date-from-milestones="startDateFromMilestones"
:initial-start-date="startDate"
:initial-due-date-is-fixed="dueDateIsFixed"
:initial-due-date-fixed="dueDateFixed"
:due-date-from-milestones="dueDateFromMilestones"
:initial-end-date="endDate"
:start-date-sourcing-milestone-title="startDateSourcingMilestoneTitle"
:due-date-sourcing-milestone-title="dueDateSourcingMilestoneTitle"
:initial-labels="labels"
:initial-participants="participants"
:initial-subscribed="subscribed"
......
<script>
import _ from 'underscore';
import { __, s__ } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import popover from '~/vue_shared/directives/popover';
import Icon from '~/vue_shared/components/icon.vue';
import DatePicker from '~/vue_shared/components/pikaday.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
const label = __('Date picker');
const pickerLabel = __('Fixed date');
export default {
directives: {
tooltip,
popover,
},
components: {
Icon,
DatePicker,
LoadingIcon,
CollapsedCalendarIcon,
},
props: {
blockClass: {
type: String,
required: false,
default: '',
},
collapsed: {
type: Boolean,
required: false,
default: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
editable: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: label,
},
datePickerLabel: {
type: String,
required: false,
default: pickerLabel,
},
selectedDate: {
type: Date,
required: false,
default: null,
},
selectedDateIsFixed: {
type: Boolean,
required: false,
default: true,
},
dateFromMilestones: {
type: Date,
required: false,
default: null,
},
dateFromMilestonesTooltip: {
type: String,
required: false,
default: '',
},
isDateInvalid: {
type: Boolean,
required: false,
default: true,
},
dateInvalidTooltip: {
type: String,
required: false,
default: '',
},
},
data() {
return {
fieldName: _.uniqueId('dateType_'),
editing: false,
};
},
computed: {
selectedAndEditable() {
return this.selectedDate && this.editable;
},
selectedDateWords() {
return dateInWords(this.selectedDate, true);
},
dateFromMilestonesWords() {
return this.dateFromMilestones ? dateInWords(this.dateFromMilestones, true) : __('None');
},
collapsedText() {
return this.selectedDateWords ? this.selectedDateWords : __('None');
},
popoverOptions() {
return this.getPopoverConfig({
title: s__('Epics|These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely.'),
content: `
<a href="https://docs.gitlab.com/ee/user/group/epics/#Dates">${s__('Epics|More information')}</a>
`,
});
},
dateInvalidPopoverOptions() {
return this.getPopoverConfig({
title: this.dateInvalidTooltip,
content: `
<a href="https://docs.gitlab.com/ee/user/group/epics/#Dates">${s__('Epics|How can I solve this?')}</a>
`,
});
},
},
methods: {
getPopoverConfig({ title, content }) {
return {
html: true,
trigger: 'focus',
container: 'body',
boundary: 'viewport',
template: `
<div class="popover" role="tooltip">
<div class="arrow"></div>
<div class="popover-header"></div>
<div class="popover-body"></div>
</div>
`,
title,
content,
};
},
stopEditing() {
this.editing = false;
this.$emit('toggleDateType', true, true);
},
toggleDatePicker() {
this.editing = !this.editing;
},
newDateSelected(date = null) {
this.editing = false;
this.$emit('saveDate', date);
},
toggleDateType(dateTypeFixed) {
this.$emit('toggleDateType', dateTypeFixed);
},
},
};
</script>
<template>
<div
:class="blockClass"
class="block date"
>
<collapsed-calendar-icon
:text="collapsedText"
class="sidebar-collapsed-icon"
/>
<div class="title">
{{ label }}
<loading-icon
v-if="isLoading"
:inline="true"
/>
<div class="float-right d-flex">
<icon
v-popover="popoverOptions"
name="question-o"
css-classes="help-icon append-right-5"
tab-index="0"
/>
<button
v-show="editable && !editing"
type="button"
class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action"
@click="toggleDatePicker"
>
{{ __('Edit') }}
</button>
</div>
</div>
<div class="value">
<div
:class="{ 'is-option-selected': selectedDateIsFixed, 'd-flex': !editing }"
class="value-type-fixed"
>
<input
v-if="!editing && editable"
:name="fieldName"
:checked="selectedDateIsFixed"
type="radio"
@click="toggleDateType(true)"
/>
<span
v-show="!editing"
class="prepend-left-5"
>{{ __('Fixed:') }}</span>
<date-picker
v-if="editing"
:selected-date="selectedDate"
:label="datePickerLabel"
@newDateSelected="newDateSelected"
@hidePicker="stopEditing"
/>
<span
v-else
class="d-flex value-content"
>
<template v-if="selectedDate">
<span>{{ selectedDateWords }}</span>
<icon
v-popover="dateInvalidPopoverOptions"
v-if="isDateInvalid && selectedDateIsFixed"
name="warning"
css-classes="date-warning-icon append-right-5 prepend-left-5"
tab-index="0"
/>
<span
v-if="selectedAndEditable"
class="no-value"
>
-
<button
type="button"
class="btn-blank btn-link btn-default-hover-link"
@click="newDateSelected(null)"
>
{{ __('remove') }}
</button>
</span>
</template>
<span
v-else
class="no-value"
>
{{ __('None') }}
</span>
</span>
</div>
<abbr
v-tooltip
:title="dateFromMilestonesTooltip"
:class="{ 'is-option-selected': !selectedDateIsFixed }"
class="value-type-dynamic d-flex prepend-top-10"
data-placement="bottom"
data-html="true"
>
<input
v-if="editable"
:name="fieldName"
:checked="!selectedDateIsFixed"
type="radio"
@click="toggleDateType(false)"
/>
<span class="prepend-left-5">{{ __('From milestones:') }}</span>
<span class="value-content">{{ dateFromMilestonesWords }}</span>
<icon
v-popover="dateInvalidPopoverOptions"
v-if="isDateInvalid && !selectedDateIsFixed"
name="warning"
css-classes="date-warning-icon prepend-left-5"
tab-index="0"
/>
</abbr>
</div>
</div>
</template>
......@@ -7,12 +7,28 @@ export default class SidebarService {
this.todoPath = todoPath;
}
updateStartDate(startDate) {
return axios.put(this.endpoint, { start_date: startDate });
updateStartDate({ dateValue, isFixed }) {
const requestBody = {
start_date_is_fixed: isFixed,
};
if (isFixed) {
requestBody.start_date_fixed = dateValue;
}
return axios.put(this.endpoint, requestBody);
}
updateEndDate(endDate) {
return axios.put(this.endpoint, { end_date: endDate });
updateEndDate({ dateValue, isFixed }) {
const requestBody = {
due_date_is_fixed: isFixed,
};
if (isFixed) {
requestBody.due_date_fixed = dateValue;
}
return axios.put(this.endpoint, requestBody);
}
toggleSubscribed() {
......
import { parsePikadayDate } from '~/lib/utils/datefix';
export default class SidebarStore {
constructor({ startDate, endDate, subscribed, todoExists, todoDeletePath }) {
constructor({
startDateIsFixed,
startDateFromMilestones,
startDate,
dueDateIsFixed,
dueDateFromMilestones,
endDate,
subscribed,
todoExists,
todoDeletePath,
}) {
this.startDateIsFixed = startDateIsFixed;
this.startDateFromMilestones = startDateFromMilestones;
this.startDate = startDate;
this.dueDateIsFixed = dueDateIsFixed;
this.dueDateFromMilestones = dueDateFromMilestones;
this.endDate = endDate;
this.subscribed = subscribed;
this.todoExists = todoExists;
......@@ -13,10 +27,18 @@ export default class SidebarStore {
return this.startDate ? parsePikadayDate(this.startDate) : null;
}
get startDateTimeFromMilestones() {
return this.startDateFromMilestones ? parsePikadayDate(this.startDateFromMilestones) : null;
}
get endDateTime() {
return this.endDate ? parsePikadayDate(this.endDate) : null;
}
get dueDateTimeFromMilestones() {
return this.dueDateFromMilestones ? parsePikadayDate(this.dueDateFromMilestones) : null;
}
setSubscribed(subscribed) {
this.subscribed = subscribed;
}
......
......@@ -33,9 +33,18 @@ export default class RoadmapStore {
}
setEpics(epics) {
this.state.epics = epics.map(
epic => RoadmapStore.formatEpicDetails(epic, this.timeframeStartDate, this.timeframeEndDate),
);
this.state.epics = epics.reduce((filteredEpics, epic) => {
const formattedEpic = RoadmapStore.formatEpicDetails(
epic,
this.timeframeStartDate,
this.timeframeEndDate,
);
// Exclude any Epic that has invalid dates
if (formattedEpic.startDate <= formattedEpic.endDate) {
filteredEpics.push(formattedEpic);
}
return filteredEpics;
}, []);
}
getEpics() {
......
.epic-sidebar {
.block.date {
.help-icon,
.date-warning-icon {
&:hover {
cursor: pointer;
}
}
.help-icon {
color: $gl-text-color-secondary;
}
.date-warning-icon {
color: $gl-warning;
margin-top: -1px;
}
.value-type-fixed,
.value-type-dynamic {
color: $gl-text-color-secondary;
.value-content {
margin-left: 2px;
}
&.is-option-selected {
> span {
color: $gl-text-color;
font-weight: $gl-font-weight-bold;
}
}
}
}
}
.tooltip .tooltip-inner .milestone-date-range {
color: $gl-text-color-tertiary;
}
......@@ -3,7 +3,6 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import epicShowApp from 'ee/epics/epic_show/components/epic_show_app.vue';
import epicHeader from 'ee/epics/epic_show/components/epic_header.vue';
import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue';
import issuableApp from '~/issue_show/components/app.vue';
import issuableAppEventHub from '~/issue_show/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
......@@ -15,7 +14,6 @@ describe('EpicShowApp', () => {
let vm;
let headerVm;
let issuableAppVm;
let sidebarVm;
beforeEach((done) => {
mock = new MockAdapter(axios);
......@@ -23,30 +21,17 @@ describe('EpicShowApp', () => {
mock.onAny().reply(404, null);
const {
epicId,
canUpdate,
canDestroy,
endpoint,
updateEndpoint,
initialTitleHtml,
initialTitleText,
startDate,
endDate,
markdownPreviewPath,
markdownDocsPath,
author,
created,
namespace,
labelsPath,
labelsWebUrl,
epicsWebUrl,
labels,
participants,
subscribed,
toggleSubscriptionPath,
todoExists,
todoPath,
todoDeletePath,
} = props;
const EpicShowApp = Vue.extend(epicShowApp);
......@@ -78,27 +63,6 @@ describe('EpicShowApp', () => {
toggleSubscriptionPath,
});
const EpicSidebar = Vue.extend(epicSidebar);
sidebarVm = mountComponent(EpicSidebar, {
epicId,
endpoint,
editable: canUpdate,
initialStartDate: startDate,
initialEndDate: endDate,
initialLabels: labels,
initialParticipants: participants,
initialSubscribed: subscribed,
initialTodoExists: todoExists,
updatePath: updateEndpoint,
toggleSubscriptionPath,
labelsPath,
labelsWebUrl,
epicsWebUrl,
namespace,
todoPath,
todoDeletePath,
});
setTimeout(done);
});
......@@ -115,7 +79,7 @@ describe('EpicShowApp', () => {
});
it('should render epic-sidebar', () => {
expect(vm.$el.innerHTML.indexOf(sidebarVm.$el.innerHTML) !== -1).toEqual(true);
expect(vm.$el.querySelector('aside.right-sidebar.epic-sidebar')).not.toBe(null);
});
it('should emit delete.issuable when epic is deleted', () => {
......
......@@ -49,6 +49,15 @@ export const contentProps = {
initialTitleText: '',
startDate: '2017-01-01',
endDate: '2017-10-10',
dueDate: '2017-10-10',
startDateFixed: '2017-01-01',
startDateIsFixed: true,
startDateFromMilestones: '',
dueDateFixed: '2017-10-10',
dueDateIsFixed: true,
dueDateFromMilestones: '',
startDateSourcingMilestoneTitle: 'Milestone for Start Date',
dueDateSourcingMilestoneTitle: 'Milestone for End Date',
labels: mockLabels,
participants: mockParticipants,
subscribed: true,
......@@ -65,4 +74,20 @@ export const headerProps = {
created: (new Date()).toISOString(),
};
export const mockDatePickerProps = {
blockClass: 'epic-date',
collapsed: false,
showToggleSidebar: false,
isLoading: false,
editable: true,
label: 'Date',
datePickerLabel: 'Fixed date',
selectedDate: null,
selectedDateIsFixed: true,
dateFromMilestones: null,
dateFromMilestonesTooltip: 'Select an issue with milestone to set date',
isDateInvalid: false,
dateInvalidTooltip: 'Selected date is invalid',
};
export const props = Object.assign({}, contentProps, headerProps);
......@@ -25,6 +25,14 @@ describe('epicSidebar', () => {
todoExists,
todoPath,
todoDeletePath,
startDateIsFixed,
startDateFixed,
startDateFromMilestones,
dueDateIsFixed,
dueDateFixed,
dueDateFromMilestones,
startDateSourcingMilestoneTitle,
dueDateSourcingMilestoneTitle,
} = props;
const defaultPropsData = {
......@@ -34,7 +42,15 @@ describe('epicSidebar', () => {
initialParticipants: participants,
initialSubscribed: subscribed,
initialTodoExists: todoExists,
initialStartDateIsFixed: startDateIsFixed,
initialStartDateFixed: startDateFixed,
startDateFromMilestones,
initialDueDateIsFixed: dueDateIsFixed,
initialDueDateFixed: dueDateFixed,
dueDateFromMilestones,
updatePath: updateEndpoint,
startDateSourcingMilestoneTitle,
dueDateSourcingMilestoneTitle,
toggleSubscriptionPath,
labelsPath,
labelsWebUrl,
......@@ -64,18 +80,6 @@ describe('epicSidebar', () => {
expect(vm.$el.classList.contains('right-sidebar-expanded')).toEqual(true);
});
it('should render min date sidebar-date-picker', () => {
vm = mountComponent(EpicSidebar, Object.assign({}, defaultPropsData, { initialStartDate: '2017-01-01' }));
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2017');
});
it('should render max date sidebar-date-picker', () => {
vm = mountComponent(EpicSidebar, Object.assign({}, defaultPropsData, { initialEndDate: '2018-01-01' }));
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2018');
});
it('should render both sidebar-date-picker', () => {
vm = mountComponent(EpicSidebar, Object.assign({}, defaultPropsData, {
initialStartDate: '2017-01-01',
......@@ -84,8 +88,52 @@ describe('epicSidebar', () => {
const startDatePicker = vm.$el.querySelector('.block.start-date');
const endDatePicker = vm.$el.querySelector('.block.end-date');
expect(startDatePicker.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2017');
expect(endDatePicker.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2018');
expect(startDatePicker.querySelector('.value-type-fixed .value-content').innerText.trim()).toEqual('Jan 1, 2017');
expect(endDatePicker.querySelector('.value-type-fixed .value-content').innerText.trim()).toEqual('Jan 1, 2018');
});
describe('computed prop', () => {
const getComponent = (customPropsData = {
initialStartDateIsFixed: true,
startDateFromMilestones: '2018-01-01',
initialStartDate: '2017-01-01',
initialDueDateIsFixed: true,
dueDateFromMilestones: '2018-11-31',
initialEndDate: '2018-01-01',
}) => new EpicSidebar({
propsData: Object.assign({}, defaultPropsData, customPropsData),
});
describe('isDateValid', () => {
it('returns true when fixed start and end dates are valid', () => {
const component = getComponent();
expect(component.isDateValid).toBe(true);
});
it('returns false when fixed start and end dates are invalid', () => {
const component = getComponent({
initialStartDate: '2018-01-01',
initialEndDate: '2017-01-01',
});
expect(component.isDateValid).toBe(false);
});
it('returns true when milestone start date and fixed end date is valid', () => {
const component = getComponent({
initialStartDateIsFixed: false,
initialEndDate: '2018-11-31',
});
expect(component.isDateValid).toBe(true);
});
it('returns true when milestone start date and milestone end date is valid', () => {
const component = getComponent({
initialStartDateIsFixed: false,
initialDueDateIsFixed: false,
});
expect(component.isDateValid).toBe(true);
});
});
});
describe('when collapsed', () => {
......@@ -107,6 +155,12 @@ describe('epicSidebar', () => {
});
});
describe('getDateFromMilestonesTooltip', () => {
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 due date to any issue in the epic.');
});
});
describe('toggleSidebar', () => {
it('should toggle collapsed_gutter cookie', () => {
expect(vm.$el.classList.contains('right-sidebar-expanded')).toEqual(true);
......@@ -165,7 +219,35 @@ describe('epicSidebar', () => {
.catch(done.fail);
});
it('should handle errors gracefully', () => {});
it('should change start date type', (done) => {
spyOn(component.service, 'updateStartDate').and.callThrough();
const dateValue = '2017-01-01';
component.saveDate('start', dateValue, false);
Vue.nextTick()
.then(() => {
expect(component.service.updateStartDate).toHaveBeenCalledWith({
dateValue,
isFixed: false,
});
})
.then(done)
.catch(done.fail);
});
it('should change end date type', (done) => {
spyOn(component.service, 'updateEndDate').and.callThrough();
const dateValue = '2017-01-01';
component.saveDate('end', dateValue, false);
Vue.nextTick()
.then(() => {
expect(component.service.updateEndDate).toHaveBeenCalledWith({
dateValue,
isFixed: false,
});
})
.then(done)
.catch(done.fail);
});
});
describe('handleLabelClick', () => {
......
import Vue from 'vue';
import SidebarDatepicker from 'ee/epics/sidebar/components/sidebar_date_picker.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockDatePickerProps } from 'ee_spec/epics/epic_show/mock_data';
const createComponent = (datePickerProps = mockDatePickerProps) => {
const Component = Vue.extend(SidebarDatepicker);
return mountComponent(Component, datePickerProps);
};
describe('SidebarParticipants', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('data', () => {
it('return data props with uniqueId for `fieldName`', () => {
expect(vm.fieldName).toContain('dateType_');
});
});
describe('computed', () => {
describe('selectedAndEditable', () => {
it('returns `true` when both `selectedDate` is defined and `editable` is true', done => {
vm.selectedDate = new Date();
Vue.nextTick()
.then(() => {
expect(vm.selectedAndEditable).toBe(true);
})
.then(done)
.catch(done.fail);
});
});
describe('selectedDateWords', () => {
it('returns full date string in words based on `selectedDate` prop value', done => {
vm.selectedDate = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.selectedDateWords).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
});
describe('dateFromMilestonesWords', () => {
it('returns full date string in words when `dateFromMilestones` is defined', done => {
vm.dateFromMilestones = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.dateFromMilestonesWords).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
it('returns `None` when `dateFromMilestones` is not defined', () => {
expect(vm.dateFromMilestonesWords).toBe('None');
});
});
describe('collapsedText', () => {
it('returns value of `selectedDateWords` when it is defined', done => {
vm.selectedDate = new Date(2018, 0, 1);
Vue.nextTick()
.then(() => {
expect(vm.collapsedText).toBe('Jan 1, 2018');
})
.then(done)
.catch(done.fail);
});
it('returns `None` when `selectedDateWords` is not defined', () => {
expect(vm.collapsedText).toBe('None');
});
});
describe('popoverOptions', () => {
it('returns popover config object containing title with appropriate string', () => {
expect(vm.popoverOptions.title).toBe('These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely.');
});
it('returns popover config object containing `content` with href pointing to correct documentation', () => {
expect(vm.popoverOptions.content.trim()).toBe('<a href="https://docs.gitlab.com/ee/user/group/epics/#Dates">More information</a>');
});
});
describe('dateInvalidPopoverOptions', () => {
it('returns popover config object containing title with appropriate string', () => {
expect(vm.dateInvalidPopoverOptions.title).toBe('Selected date is invalid');
});
it('returns popover config object containing `content` with href pointing to correct documentation', () => {
expect(vm.dateInvalidPopoverOptions.content.trim()).toBe('<a href="https://docs.gitlab.com/ee/user/group/epics/#Dates">How can I solve this?</a>');
});
});
});
describe('methods', () => {
describe('getPopoverConfig', () => {
it('returns popover config object with provided `title` and `content` values', () => {
const title = 'Popover title';
const content = 'This is a popover content';
const popoverConfig = vm.getPopoverConfig({ title, content });
const expectedPopoverConfig = {
html: true,
trigger: 'focus',
container: 'body',
boundary: 'viewport',
template: '<div class="popover-header"></div>',
title,
content,
};
Object.keys(popoverConfig).forEach((key) => {
if (key === 'template') {
expect(popoverConfig[key]).toContain(expectedPopoverConfig[key]);
} else {
expect(popoverConfig[key]).toBe(expectedPopoverConfig[key]);
}
});
});
});
describe('stopEditing', () => {
it('sets `editing` prop to `false` and emits `toggleDateType` event on component', () => {
spyOn(vm, '$emit');
vm.stopEditing();
expect(vm.editing).toBe(false);
expect(vm.$emit).toHaveBeenCalledWith('toggleDateType', true, true);
});
});
describe('toggleDatePicker', () => {
it('flips value of `editing` prop from `true` to `false` and vice-versa', () => {
vm.editing = true;
vm.toggleDatePicker();
expect(vm.editing).toBe(false);
});
});
describe('newDateSelected', () => {
it('sets `editing` prop to `false` and emits `saveDate` event on component', () => {
spyOn(vm, '$emit');
const date = new Date();
vm.newDateSelected(date);
expect(vm.editing).toBe(false);
expect(vm.$emit).toHaveBeenCalledWith('saveDate', date);
});
});
describe('toggleDateType', () => {
it('emits `toggleDateType` event on component', () => {
spyOn(vm, '$emit');
vm.toggleDateType(true);
expect(vm.$emit).toHaveBeenCalledWith('toggleDateType', true);
});
});
});
describe('template', () => {
it('renders component container element', () => {
expect(vm.$el.classList.contains('block', 'date', 'epic-date')).toBe(true);
});
it('renders collapsed calendar icon component', () => {
expect(vm.$el.querySelector('.sidebar-collapsed-icon')).not.toBe(null);
});
it('renders title element', () => {
expect(vm.$el.querySelector('.title')).not.toBe(null);
});
it('renders loading icon when `isLoading` prop is true', done => {
vm.isLoading = true;
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
})
.then(done)
.catch(done.fail);
});
it('renders help icon', () => {
const helpIconEl = vm.$el.querySelector('.help-icon');
expect(helpIconEl).not.toBe(null);
expect(helpIconEl.getAttribute('tabindex')).toBe('0');
expect(helpIconEl.querySelector('use').getAttribute('xlink:href')).toContain('question-o');
});
it('renderts edit button', () => {
const buttonEl = vm.$el.querySelector('button.btn-sidebar-action');
expect(buttonEl).not.toBe(null);
expect(buttonEl.innerText.trim()).toBe('Edit');
});
it('renders value container element', () => {
expect(vm.$el.querySelector('.value .value-type-fixed')).not.toBe(null);
expect(vm.$el.querySelector('.value .value-type-dynamic')).not.toBe(null);
});
it('renders fixed type date selection element', () => {
const valueFixedEl = vm.$el.querySelector('.value .value-type-fixed');
expect(valueFixedEl.querySelector('input[type="radio"]')).not.toBe(null);
expect(valueFixedEl.innerText.trim()).toContain('Fixed:');
expect(valueFixedEl.querySelector('.value-content').innerText.trim()).toContain('None');
});
it('renders dynamic type date selection element', () => {
const valueDynamicEl = vm.$el.querySelector('.value abbr.value-type-dynamic');
expect(valueDynamicEl.querySelector('input[type="radio"]')).not.toBe(null);
expect(valueDynamicEl.innerText.trim()).toContain('From milestones:');
expect(valueDynamicEl.querySelector('.value-content').innerText.trim()).toContain('None');
});
it('renders date warning icon when `isDateInvalid` prop is `true`', done => {
vm.isDateInvalid = true;
vm.selectedDateIsFixed = false;
Vue.nextTick()
.then(() => {
const warningIconEl = vm.$el.querySelector('.date-warning-icon');
expect(warningIconEl).not.toBe(null);
expect(warningIconEl.getAttribute('tabindex')).toBe('0');
expect(warningIconEl.querySelector('use').getAttribute('xlink:href')).toContain('warning');
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -14,23 +14,25 @@ describe('Sidebar Service', () => {
});
describe('updateStartDate', () => {
it('returns axios instance with PUT for `endpoint` and `start_date` as request body', () => {
it('returns axios instance with PUT for `endpoint` with `start_date_is_fixed` and `start_date_fixed` as request body', () => {
spyOn(axios, 'put').and.stub();
const startDate = '2018-06-21';
service.updateStartDate(startDate);
const dateValue = '2018-06-21';
service.updateStartDate({ dateValue, isFixed: true });
expect(axios.put).toHaveBeenCalledWith(service.endpoint, {
start_date: startDate,
start_date_is_fixed: true,
start_date_fixed: dateValue,
});
});
});
describe('updateEndDate', () => {
it('returns axios instance with PUT for `endpoint` and `end_date` as request body', () => {
it('returns axios instance with PUT for `endpoint` with `due_date_is_fixed` and `due_date_fixed` as request body', () => {
spyOn(axios, 'put').and.stub();
const endDate = '2018-06-21';
service.updateEndDate(endDate);
const dateValue = '2018-06-21';
service.updateEndDate({ dateValue, isFixed: true });
expect(axios.put).toHaveBeenCalledWith(service.endpoint, {
end_date: endDate,
due_date_is_fixed: true,
due_date_fixed: dateValue,
});
});
});
......
......@@ -2400,6 +2400,9 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
msgid "Date picker"
msgstr ""
msgid "Dec"
msgstr ""
......@@ -2879,6 +2882,27 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
msgid "Epics|An error occurred while saving %{epicDateType} date"
msgstr ""
msgid "Epics|How can I solve this?"
msgstr ""
msgid "Epics|More information"
msgstr ""
msgid "Epics|These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely."
msgstr ""
msgid "Epics|To schedule your epic's %{epicDateType} date based on milestones, assign a milestone with a due date to any issue in the epic."
msgstr ""
msgid "Epics|finish"
msgstr ""
msgid "Epics|start"
msgstr ""
msgid "Error"
msgstr ""
......@@ -3095,6 +3119,18 @@ msgstr ""
msgid "FirstPushedBy|pushed by"
msgstr ""
msgid "Fixed date"
msgstr ""
msgid "Fixed finish date"
msgstr ""
msgid "Fixed start date"
msgstr ""
msgid "Fixed:"
msgstr ""
msgid "FogBugz Email"
msgstr ""
......@@ -3172,6 +3208,9 @@ msgstr ""
msgid "From merge request merge until deploy to production"
msgstr ""
msgid "From milestones:"
msgstr ""
msgid "From the Kubernetes cluster details view, install Runner from the applications list"
msgstr ""
......@@ -6887,6 +6926,12 @@ msgstr ""
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
msgid "This date is after the planned finish date, so this epic won't appear in the roadmap."
msgstr ""
msgid "This date is before the planned start date, so this epic won't appear in the roadmap."
msgstr ""
msgid "This diff is collapsed."
msgstr ""
......@@ -8465,6 +8510,9 @@ msgstr ""
msgid "remaining"
msgstr ""
msgid "remove"
msgstr ""
msgid "remove due date"
msgstr ""
......
......@@ -168,5 +168,16 @@ export const rawEpics = [
end_date: '2018-06-02',
web_url: '/groups/gitlab-org/-/epics/1',
},
{
id: 22,
iid: 2,
description: null,
title: 'Epic with invalid dates',
group_id: 56,
group_name: 'Marketing',
group_full_name: 'Gitlab Org / Marketing',
start_date: '2018-12-26',
end_date: '2018-03-10',
web_url: '/groups/gitlab-org/marketing/-/epics/22',
},
];
......@@ -22,9 +22,9 @@ describe('RoadmapStore', () => {
});
describe('setEpics', () => {
it('sets Epics list to state', () => {
it('sets Epics list to state while filtering out Epics with invalid dates', () => {
store.setEpics(rawEpics);
expect(store.getEpics().length).toBe(rawEpics.length);
expect(store.getEpics().length).toBe(rawEpics.length - 2); // 2 epics have invalid dates
});
});
......
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